diff --git a/config.json b/config.json index c0dff2b..4973bf0 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,10 @@ { - "library": "videos", + "library": [ + { + "path": "videos", + "prefix": "" + } + ], "server": { "host": "127.0.0.1", "port": 0 diff --git a/pkg/app/app.go b/pkg/app/app.go index d5a909a..baf3ccb 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -56,8 +56,11 @@ func NewApp(cfg *Config) (*App, error) { 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( @@ -71,12 +74,21 @@ func NewApp(cfg *Config) (*App, error) { // Run imports the library and starts server. func (a *App) Run() error { - path := a.Config.LibraryPath - err := a.Library.Import(path) - if err != nil { - return err + 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) } - a.Watcher.Add(path) go a.watch() return http.Serve(a.Listener, a.Router) } @@ -124,6 +136,10 @@ func (a *App) indexHandler(w http.ResponseWriter, r *http.Request) { 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 { @@ -150,6 +166,10 @@ func (a *App) pageHandler(w http.ResponseWriter, r *http.Request) { 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 { @@ -159,14 +179,17 @@ func (a *App) videoHandler(w http.ResponseWriter, r *http.Request) { disposition := "attachment; filename=\"" + title + ".mp4\"" w.Header().Set("Content-Disposition", disposition) w.Header().Set("Content-Type", "video/mp4") - path := a.Config.LibraryPath + "/" + id + ".mp4" - http.ServeFile(w, r, path) + 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 { diff --git a/pkg/app/config.go b/pkg/app/config.go index f8ca2e8..c2ecb45 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -7,9 +7,15 @@ import ( // Config settings for main App. type Config struct { - LibraryPath string `json:"library"` - Server *ServerConfig `json:"server"` - Feed *FeedConfig `json:"feed"` + Library []*PathConfig `json:"library"` + Server *ServerConfig `json:"server"` + Feed *FeedConfig `json:"feed"` +} + +// PathConfig settings for media library path. +type PathConfig struct { + Path string `json:"path"` + Prefix string `json:"prefix"` } // ServerConfig settings for App Server. @@ -34,7 +40,12 @@ type FeedConfig struct { // DefaultConfig returns Config initialized with default values. func DefaultConfig() *Config { return &Config{ - LibraryPath: "videos", + Library: []*PathConfig{ + &PathConfig{ + Path: "videos", + Prefix: "", + }, + }, Server: &ServerConfig{ Host: "127.0.0.1", Port: 0, diff --git a/pkg/media/library.go b/pkg/media/library.go index 3d58aab..e22f8ef 100644 --- a/pkg/media/library.go +++ b/pkg/media/library.go @@ -1,9 +1,10 @@ package media import ( + "errors" "io/ioutil" "log" - "path/filepath" + "path" "sort" "strings" "sync" @@ -12,25 +13,44 @@ import ( // Library manages importing and retrieving video data. type Library struct { mu sync.RWMutex + Paths map[string]*Path Videos map[string]*Video } // NewLibrary returns new instance of Library. func NewLibrary() *Library { lib := &Library{ + Paths: make(map[string]*Path), Videos: make(map[string]*Video), } return lib } +// AddPath adds a media path to the library. +func (lib *Library) AddPath(p *Path) error { + lib.mu.Lock() + defer lib.mu.Unlock() + // make sure new path doesn't collide with existing ones + for _, p2 := range lib.Paths { + if p.Path == p2.Path { + return errors.New("media: duplicate library path") + } + if p.Prefix == p2.Prefix { + return errors.New("media: duplicate library prefix") + } + } + lib.Paths[p.Path] = p + return nil +} + // Import adds all valid videos from a given path. -func (lib *Library) Import(path string) error { - files, err := ioutil.ReadDir(path) +func (lib *Library) Import(p *Path) error { + files, err := ioutil.ReadDir(p.Path) if err != nil { return err } for _, info := range files { - err = lib.Add(path + "/" + info.Name()) + err = lib.Add(path.Join(p.Path, info.Name())) if err != nil { // Ignore files that can't be parsed continue @@ -40,33 +60,48 @@ func (lib *Library) Import(path string) error { } // Add adds a single video from a given file path. -func (lib *Library) Add(path string) error { - v, err := ParseVideo(path) +func (lib *Library) Add(filepath string) error { + lib.mu.Lock() + defer lib.mu.Unlock() + d := path.Dir(filepath) + p, ok := lib.Paths[d] + if !ok { + log.Println(d) + return errors.New("media: path not found") + } + n := path.Base(filepath) + v, err := ParseVideo(p, n) if err != nil { return err } - lib.mu.Lock() - defer lib.mu.Unlock() lib.Videos[v.ID] = v - log.Println("Added:", path) + log.Println("Added:", v.Path) return nil } // Remove removes a single video from a given file path. -func (lib *Library) Remove(path string) { - name := filepath.Base(path) - // ID is name without extension - idx := strings.LastIndex(name, ".") - if idx == -1 { - idx = len(name) - } - id := name[:idx] +func (lib *Library) Remove(filepath string) { lib.mu.Lock() defer lib.mu.Unlock() - _, ok := lib.Videos[id] + d := path.Dir(filepath) + p, ok := lib.Paths[d] + if !ok { + return + } + n := path.Base(filepath) + // ID is name without extension + idx := strings.LastIndex(n, ".") + if idx == -1 { + idx = len(n) + } + id := n[:idx] + if len(p.Prefix) > 0 { + id = path.Join(p.Prefix, id) + } + v, ok := lib.Videos[id] if ok { delete(lib.Videos, id) - log.Println("Removed:", path) + log.Println("Removed:", v.Path) } } diff --git a/pkg/media/path.go b/pkg/media/path.go new file mode 100644 index 0000000..1248ed5 --- /dev/null +++ b/pkg/media/path.go @@ -0,0 +1,7 @@ +package media + +// Path represents a media library path. +type Path struct { + Path string + Prefix string +} diff --git a/pkg/media/video.go b/pkg/media/video.go index 90ec595..24f89b7 100644 --- a/pkg/media/video.go +++ b/pkg/media/video.go @@ -2,6 +2,7 @@ package media import ( "os" + "path" "strings" "time" @@ -18,12 +19,14 @@ type Video struct { ThumbType string Modified string Size int64 + Path string Timestamp time.Time } // ParseVideo parses a video file's metadata and returns a Video. -func ParseVideo(path string) (*Video, error) { - f, err := os.Open(path) +func ParseVideo(p *Path, name string) (*Video, error) { + pth := path.Join(p.Path, name) + f, err := os.Open(pth) if err != nil { return nil, err } @@ -34,13 +37,16 @@ func ParseVideo(path string) (*Video, error) { size := info.Size() timestamp := info.ModTime() modified := timestamp.Format("2006-01-02 03:04 PM") - name := info.Name() // ID is name without extension idx := strings.LastIndex(name, ".") if idx == -1 { idx = len(name) } id := name[:idx] + if len(p.Prefix) > 0 { + // if there's a prefix prepend it to the ID + id = path.Join(p.Prefix, name[:idx]) + } m, err := tag.ReadFrom(f) if err != nil { return nil, err @@ -57,13 +63,14 @@ func ParseVideo(path string) (*Video, error) { Description: m.Comment(), Modified: modified, Size: size, + Path: pth, Timestamp: timestamp, } // Add thumbnail (if exists) - p := m.Picture() - if p != nil { - v.Thumb = p.Data - v.ThumbType = p.MIMEType + pic := m.Picture() + if pic != nil { + v.Thumb = pic.Data + v.ThumbType = pic.MIMEType } return v, nil }