summaryrefslogtreecommitdiff
path: root/pkg/app
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/app')
-rw-r--r--pkg/app/app.go222
-rw-r--r--pkg/app/config.go89
-rw-r--r--pkg/app/feed.go77
-rw-r--r--pkg/app/listener.go37
-rw-r--r--pkg/app/tor.go45
-rw-r--r--pkg/app/watcher.go63
6 files changed, 0 insertions, 533 deletions
diff --git a/pkg/app/app.go b/pkg/app/app.go
deleted file mode 100644
index 0efd22a..0000000
--- a/pkg/app/app.go
+++ /dev/null
@@ -1,222 +0,0 @@
-// Package app manages main application server.
-package app
-
-import (
- "errors"
- "fmt"
- "html/template"
- "log"
- "net"
- "net/http"
- "path"
-
- "github.com/fsnotify/fsnotify"
- "github.com/gorilla/mux"
- "github.com/wybiral/tube/pkg/media"
- "github.com/wybiral/tube/pkg/onionkey"
-)
-
-// App represents main application.
-type App struct {
- Config *Config
- Library *media.Library
- Watcher *fsnotify.Watcher
- Templates *template.Template
- Feed []byte
- Tor *tor
- Listener net.Listener
- Router *mux.Router
-}
-
-// NewApp returns a new instance of App from Config.
-func NewApp(cfg *Config) (*App, error) {
- if cfg == nil {
- cfg = DefaultConfig()
- }
- a := &App{
- Config: cfg,
- }
- // Setup Library
- a.Library = media.NewLibrary()
- // Setup Watcher
- w, err := fsnotify.NewWatcher()
- if err != nil {
- return nil, err
- }
- a.Watcher = w
- // Setup Listener
- ln, err := newListener(cfg.Server)
- if err != nil {
- return nil, err
- }
- a.Listener = ln
- // Setup Templates
- a.Templates = template.Must(template.ParseGlob("templates/*"))
- // Setup Tor
- if cfg.Tor.Enable {
- t, err := newTor(cfg.Tor)
- if err != nil {
- return nil, err
- }
- a.Tor = t
- }
- // Setup Router
- r := mux.NewRouter().StrictSlash(true)
- r.HandleFunc("/", a.indexHandler).Methods("GET")
- r.HandleFunc("/v/{id}.mp4", a.videoHandler).Methods("GET")
- r.HandleFunc("/v/{prefix}/{id}.mp4", a.videoHandler).Methods("GET")
- r.HandleFunc("/t/{id}", a.thumbHandler).Methods("GET")
- r.HandleFunc("/t/{prefix}/{id}", a.thumbHandler).Methods("GET")
- r.HandleFunc("/v/{id}", a.pageHandler).Methods("GET")
- r.HandleFunc("/v/{prefix}/{id}", a.pageHandler).Methods("GET")
- r.HandleFunc("/feed.xml", a.rssHandler).Methods("GET")
- // Static file handler
- fsHandler := http.StripPrefix(
- "/static/",
- http.FileServer(http.Dir("./static/")),
- )
- r.PathPrefix("/static/").Handler(fsHandler).Methods("GET")
- a.Router = r
- return a, nil
-}
-
-// Run imports the library and starts server.
-func (a *App) Run() error {
- if a.Tor != nil {
- var err error
- cs := a.Config.Server
- key := a.Tor.OnionKey
- if key == nil {
- key, err = onionkey.GenerateKey()
- if err != nil {
- return err
- }
- a.Tor.OnionKey = key
- }
- onion, err := key.Onion()
- if err != nil {
- return err
- }
- onion.Ports[80] = fmt.Sprintf("%s:%d", cs.Host, cs.Port)
- err = a.Tor.Controller.AddOnion(onion)
- if err != nil {
- return errors.New("unable to start Tor onion service")
- }
- log.Printf("Onion service: http://%s.onion", onion.ServiceID)
- }
- for _, pc := range a.Config.Library {
- p := &media.Path{
- Path: pc.Path,
- Prefix: pc.Prefix,
- }
- err := a.Library.AddPath(p)
- if err != nil {
- return err
- }
- err = a.Library.Import(p)
- if err != nil {
- return err
- }
- a.Watcher.Add(p.Path)
- }
- buildFeed(a)
- go startWatcher(a)
- return http.Serve(a.Listener, a.Router)
-}
-
-// HTTP handler for /
-func (a *App) indexHandler(w http.ResponseWriter, r *http.Request) {
- log.Printf("/")
- pl := a.Library.Playlist()
- if len(pl) > 0 {
- http.Redirect(w, r, "/v/"+pl[0].ID, 302)
- } else {
- a.Templates.ExecuteTemplate(w, "index.html", &struct {
- Playing *media.Video
- Playlist media.Playlist
- }{
- Playing: &media.Video{ID: ""},
- Playlist: a.Library.Playlist(),
- })
- }
-}
-
-// HTTP handler for /v/id
-func (a *App) pageHandler(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- id := vars["id"]
- prefix, ok := vars["prefix"]
- if ok {
- id = path.Join(prefix, id)
- }
- log.Printf("/v/%s", id)
- playing, ok := a.Library.Videos[id]
- if !ok {
- a.Templates.ExecuteTemplate(w, "index.html", &struct {
- Playing *media.Video
- Playlist media.Playlist
- }{
- Playing: &media.Video{ID: ""},
- Playlist: a.Library.Playlist(),
- })
- return
- }
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- a.Templates.ExecuteTemplate(w, "index.html", &struct {
- Playing *media.Video
- Playlist media.Playlist
- }{
- Playing: playing,
- Playlist: a.Library.Playlist(),
- })
-}
-
-// HTTP handler for /v/id.mp4
-func (a *App) videoHandler(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- id := vars["id"]
- prefix, ok := vars["prefix"]
- if ok {
- id = path.Join(prefix, id)
- }
- log.Printf("/v/%s", id)
- m, ok := a.Library.Videos[id]
- if !ok {
- return
- }
- title := m.Title
- disposition := "attachment; filename=\"" + title + ".mp4\""
- w.Header().Set("Content-Disposition", disposition)
- w.Header().Set("Content-Type", "video/mp4")
- http.ServeFile(w, r, m.Path)
-}
-
-// HTTP handler for /t/id
-func (a *App) thumbHandler(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- id := vars["id"]
- prefix, ok := vars["prefix"]
- if ok {
- id = path.Join(prefix, id)
- }
- log.Printf("/t/%s", id)
- m, ok := a.Library.Videos[id]
- if !ok {
- return
- }
- w.Header().Set("Cache-Control", "public, max-age=7776000")
- if m.ThumbType == "" {
- w.Header().Set("Content-Type", "image/jpeg")
- http.ServeFile(w, r, "static/defaulticon.jpg")
- } else {
- w.Header().Set("Content-Type", m.ThumbType)
- w.Write(m.Thumb)
- }
-}
-
-// HTTP handler for /feed.xml
-func (a *App) rssHandler(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Cache-Control", "public, max-age=7776000")
- w.Header().Set("Content-Type", "text/xml")
- w.Write(a.Feed)
-}
diff --git a/pkg/app/config.go b/pkg/app/config.go
deleted file mode 100644
index ec27435..0000000
--- a/pkg/app/config.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package app
-
-import (
- "encoding/json"
- "os"
-)
-
-// Config settings for main App.
-type Config struct {
- Library []*PathConfig `json:"library"`
- Server *ServerConfig `json:"server"`
- Feed *FeedConfig `json:"feed"`
- Tor *TorConfig `json:"tor,omitempty"`
-}
-
-// PathConfig settings for media library path.
-type PathConfig struct {
- Path string `json:"path"`
- Prefix string `json:"prefix"`
-}
-
-// ServerConfig settings for App Server.
-type ServerConfig struct {
- Host string `json:"host"`
- Port int `json:"port"`
-}
-
-// FeedConfig settings for App Feed.
-type FeedConfig struct {
- ExternalURL string `json:"external_url"`
- Title string `json:"title"`
- Link string `json:"link"`
- Description string `json:"description"`
- Author struct {
- Name string `json:"name"`
- Email string `json:"email"`
- } `json:"author"`
- Copyright string `json:"copyright"`
-}
-
-// TorConfig stores tor configuration.
-type TorConfig struct {
- Enable bool `json:"enable"`
- Controller *TorControllerConfig `json:"controller"`
-}
-
-// TorControllerConfig stores tor controller configuration.
-type TorControllerConfig struct {
- Host string `json:"host"`
- Port int `json:"port"`
- Password string `json:"password,omitempty"`
-}
-
-// DefaultConfig returns Config initialized with default values.
-func DefaultConfig() *Config {
- return &Config{
- Library: []*PathConfig{
- &PathConfig{
- Path: "videos",
- Prefix: "",
- },
- },
- Server: &ServerConfig{
- Host: "127.0.0.1",
- Port: 0,
- },
- Feed: &FeedConfig{
- ExternalURL: "http://localhost",
- },
- Tor: &TorConfig{
- Enable: false,
- Controller: &TorControllerConfig{
- Host: "127.0.0.1",
- Port: 9051,
- },
- },
- }
-}
-
-// ReadFile reads a JSON file into Config.
-func (c *Config) ReadFile(path string) error {
- f, err := os.Open(path)
- if err != nil {
- return err
- }
- defer f.Close()
- d := json.NewDecoder(f)
- return d.Decode(c)
-}
diff --git a/pkg/app/feed.go b/pkg/app/feed.go
deleted file mode 100644
index e4c1ce2..0000000
--- a/pkg/app/feed.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package app
-
-import (
- "fmt"
- "net/url"
- "os"
- "path"
- "strconv"
- "time"
-
- "github.com/wybiral/feeds"
-)
-
-// buildFeed creates RSS feed attribute for App based on Library contents.
-func buildFeed(a *App) {
- cfg := a.Config.Feed
- now := time.Now()
- f := &feeds.Feed{
- Title: cfg.Title,
- Link: &feeds.Link{Href: cfg.Link},
- Description: cfg.Description,
- Author: &feeds.Author{
- Name: cfg.Author.Name,
- Email: cfg.Author.Email,
- },
- Created: now,
- Copyright: cfg.Copyright,
- }
- var externalURL string
- if len(cfg.ExternalURL) > 0 {
- externalURL = cfg.ExternalURL
- } else if a.Tor != nil {
- onion, err := a.Tor.OnionKey.Onion()
- if err != nil {
- return
- }
- externalURL = fmt.Sprintf("http://%s.onion", onion.ServiceID)
- } else {
- hostname, err := os.Hostname()
- if err != nil {
- host := a.Config.Server.Host
- port := a.Config.Server.Port
- externalURL = fmt.Sprintf("http://%s:%d", host, port)
- } else {
- externalURL = fmt.Sprintf("http://%s", hostname)
- }
- }
- for _, v := range a.Library.Playlist() {
- u, err := url.Parse(externalURL)
- if err != nil {
- return
- }
- u.Path = path.Join(u.Path, "v", v.ID)
- id := u.String()
- f.Items = append(f.Items, &feeds.Item{
- Id: id,
- Title: v.Title,
- Link: &feeds.Link{Href: id},
- Description: v.Description,
- Enclosure: &feeds.Enclosure{
- Url: id + ".mp4",
- Length: strconv.FormatInt(v.Size, 10),
- Type: "video/mp4",
- },
- Author: &feeds.Author{
- Name: cfg.Author.Name,
- Email: cfg.Author.Email,
- },
- Created: v.Timestamp,
- })
- }
- feed, err := f.ToRss()
- if err != nil {
- return
- }
- a.Feed = []byte(feed)
-}
diff --git a/pkg/app/listener.go b/pkg/app/listener.go
deleted file mode 100644
index a4236d8..0000000
--- a/pkg/app/listener.go
+++ /dev/null
@@ -1,37 +0,0 @@
-// Instead of using the default new.Listener this file will construct a custom
-// one. The main purpose for this is to have more control over the settings
-// (like keep-alive) and to retrieve the assigned port when using port 0.
-
-package app
-
-import (
- "fmt"
- "net"
- "time"
-)
-
-func newListener(cfg *ServerConfig) (net.Listener, error) {
- addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
- ln, err := net.Listen("tcp", addr)
- if err != nil {
- return nil, err
- }
- // set actual port on config object (in case original port was 0)
- cfg.Port = ln.Addr().(*net.TCPAddr).Port
- return tcpListener{ln.(*net.TCPListener)}, nil
-}
-
-// custom TCP listener with keep-alive timeout
-type tcpListener struct {
- *net.TCPListener
-}
-
-func (ln tcpListener) Accept() (net.Conn, error) {
- tc, err := ln.AcceptTCP()
- if err != nil {
- return nil, err
- }
- tc.SetKeepAlive(true)
- tc.SetKeepAlivePeriod(3 * time.Minute)
- return tc, nil
-}
diff --git a/pkg/app/tor.go b/pkg/app/tor.go
deleted file mode 100644
index d06751f..0000000
--- a/pkg/app/tor.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package app
-
-import (
- "errors"
- "fmt"
- "os"
-
- "github.com/wybiral/torgo"
- "github.com/wybiral/tube/pkg/onionkey"
-)
-
-type tor struct {
- OnionKey onionkey.Key
- Controller *torgo.Controller
-}
-
-func newTor(ct *TorConfig) (*tor, error) {
- addr := fmt.Sprintf("%s:%d", ct.Controller.Host, ct.Controller.Port)
- ctrl, err := torgo.NewController(addr)
- if err != nil {
- return nil, errors.New("unable to connect to Tor controller")
- }
- if len(ct.Controller.Password) > 0 {
- err = ctrl.AuthenticatePassword(ct.Controller.Password)
- } else {
- err = ctrl.AuthenticateCookie()
- if err != nil {
- err = ctrl.AuthenticateNone()
- }
- }
- if err != nil {
- return nil, errors.New("unable to authenticate to Tor controller")
- }
- key, err := onionkey.ReadFile("onion.key")
- if os.IsNotExist(err) {
- key = nil
- } else if err != nil {
- return nil, err
- }
- t := &tor{
- Controller: ctrl,
- OnionKey: key,
- }
- return t, nil
-}
diff --git a/pkg/app/watcher.go b/pkg/app/watcher.go
deleted file mode 100644
index 74db715..0000000
--- a/pkg/app/watcher.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package app
-
-import (
- "time"
-
- fs "github.com/fsnotify/fsnotify"
-)
-
-// This is the amount of time to wait after changes before reacting to them.
-// Debounce is done because moving files into the watched directories causes
-// many rapid "Write" events to fire which would cause excessive Remove/Add
-// method calls on the Library. To avoid this we accumulate the changes and
-// only perform them once the events have stopped for this amount of time.
-const debounceTimeout = time.Second * 5
-
-// create, write, and chmod all require an add event
-const addFlags = fs.Create | fs.Write | fs.Chmod
-
-// remove, rename, write, and chmod all require a remove event
-const removeFlags = fs.Remove | fs.Rename | fs.Write | fs.Chmod
-
-// watch library paths and update Library with changes.
-func startWatcher(a *App) {
- timer := time.NewTimer(debounceTimeout)
- addEvents := make(map[string]struct{})
- removeEvents := make(map[string]struct{})
- for {
- select {
- case e := <-a.Watcher.Events:
- if e.Op&removeFlags != 0 {
- removeEvents[e.Name] = struct{}{}
- }
- if e.Op&addFlags != 0 {
- addEvents[e.Name] = struct{}{}
- }
- // reset timer
- timer.Reset(debounceTimeout)
- case <-timer.C:
- eventCount := len(removeEvents) + len(addEvents)
- // handle remove events first
- if len(removeEvents) > 0 {
- for p := range removeEvents {
- a.Library.Remove(p)
- }
- // clear map
- removeEvents = make(map[string]struct{})
- }
- // then handle add events
- if len(addEvents) > 0 {
- for p := range addEvents {
- a.Library.Add(p)
- }
- // clear map
- addEvents = make(map[string]struct{})
- }
- if eventCount > 0 {
- buildFeed(a)
- }
- // reset timer
- timer.Reset(debounceTimeout)
- }
- }
-}