package subsonic import ( "context" "net/http" "path" "sort" "strings" "github.com/mithral/vellum/internal/auth" ) // directoryForLibrary renders the root of one audio library. func (h *Handlers) directoryForLibrary(w http.ResponseWriter, r *http.Request, u auth.User, libID int64) { libs, err := h.audioLibraries(r.Context(), u) if err != nil { h.internal(w, r, "directory", err) return } name := "" for _, l := range libs { if l.id == libID { name = l.name break } } if name == "" { h.fail(w, r, errDataNotFound, "directory not found") return } h.writeDirectory(w, r, u, libID, "", name) } // directoryForPath renders one directory inside an audio library. func (h *Handlers) directoryForPath(w http.ResponseWriter, r *http.Request, u auth.User, libID int64, rel string) { // Confirm the library is one the caller may read before exposing anything. libs, err := h.audioLibraryIDs(r.Context(), u) if err != nil { h.internal(w, r, "directory", err) return } if !containsID(libs, libID) { h.fail(w, r, errDataNotFound, "directory not found") return } rel = normalizeRel(rel) name := path.Base(rel) if rel == "." { name = "" } h.writeDirectory(w, r, u, libID, rel, name) } // writeDirectory builds a element for a library path prefix. // // Vellum's scanner groups audio by directory and never writes parent_id, so the // tree is reconstructed here from media_items.path: every item whose parent // directory is the prefix is a direct album child, and every deeper item // contributes one directory child for the next path segment. func (h *Handlers) writeDirectory(w http.ResponseWriter, r *http.Request, u auth.User, libID int64, rel, name string) { rel = normalizeRel(rel) if name == "" { name = path.Base(rel) if rel == "." { if root, err := h.libraryRoot(r.Context(), libID); err == nil { name = path.Base(root) } if name == "" || name == "." || name == "/" { name = "Music" } } } rows, err := h.db.QueryContext(r.Context(), ` SELECT id, path, title FROM media_items WHERE library_id = ? AND missing = 0 AND kind IN ('music','audiobooks') ORDER BY path`, libID) if err != nil { h.internal(w, r, "directory", err) return } defer rows.Close() var ( childAlbumIDs []int64 subdirs = map[string]bool{} ) for rows.Next() { var ( id int64 ipath string title string ) if err := rows.Scan(&id, &ipath, &title); err != nil { h.internal(w, r, "directory", err) return } dir := normalizeRel(ipath) parent := path.Dir(dir) if parent == "" { parent = "." } switch { case parent == rel: childAlbumIDs = append(childAlbumIDs, id) case rel == "." && dir != ".": subdirs[firstSegment(dir)] = true case rel != "." && strings.HasPrefix(dir, rel+"/"): rest := strings.TrimPrefix(dir, rel+"/") if seg := firstSegment(rest); seg != "" { subdirs[seg] = true } } } if err := rows.Err(); err != nil { h.internal(w, r, "directory", err) return } dirNode := el("directory", at("id", dirID(libID, rel)), at("name", name), ).arrays("child") subNames := make([]string, 0, len(subdirs)) for s := range subdirs { subNames = append(subNames, s) } sort.Strings(subNames) for _, seg := range subNames { sub := seg if rel != "." { sub = rel + "/" + seg } dirNode.add(el("child", at("id", dirID(libID, sub)), at("parent", dirID(libID, rel)), at("isDir", true), at("title", path.Base(seg)), at("album", path.Base(seg)), at("artist", ""), )) } albums, err := h.albumsByIDs(r.Context(), libID, childAlbumIDs) if err != nil { h.internal(w, r, "directory albums", err) return } for _, a := range albums { dirNode.add(childNode(a)) } root := envelope("ok") root.add(dirNode) h.write(w, r, root) } // directoryForAlbum renders an album as a directory whose children are songs. func (h *Handlers) directoryForAlbum(w http.ResponseWriter, r *http.Request, u auth.User, itemID int64) { a, ok, err := h.loadAlbum(r.Context(), u, itemID) if err != nil { h.internal(w, r, "directory", err) return } if !ok { h.fail(w, r, errDataNotFound, "directory not found") return } tracks, err := h.tracksForItem(r.Context(), itemID) if err != nil { h.internal(w, r, "directory songs", err) return } dirNode := el("directory", at("id", albumID(a.ID)), at("parent", dirID(a.LibraryID, parentDirOf(a.Path))), at("name", a.Title), ).arrays("child") for _, t := range tracks { child := songNode(t) child.name = "child" dirNode.add(child) } root := envelope("ok") root.add(dirNode) h.write(w, r, root) } // directoryForArtist renders an artist as a directory whose children are the // artist's albums. Legacy clients reach an artist through getMusicDirectory // rather than getArtist. func (h *Handlers) directoryForArtist(w http.ResponseWriter, r *http.Request, u auth.User, personID int64) { libs, err := h.audioLibraryIDs(r.Context(), u) if err != nil { h.internal(w, r, "directory", err) return } albums, err := h.albumsByArtist(r.Context(), libs, personID) if err != nil { h.internal(w, r, "directory albums", err) return } if len(albums) == 0 { h.fail(w, r, errDataNotFound, "directory not found") return } name, _ := h.artistName(r.Context(), personID) dirNode := el("directory", at("id", artistID(personID)), at("name", name), ).arrays("child") for _, a := range albums { dirNode.add(childNode(a)) } root := envelope("ok") root.add(dirNode) h.write(w, r, root) } // albumsByIDs loads the album projection for a specific id set within one // library, preserving album-title order. func (h *Handlers) albumsByIDs(ctx context.Context, libID int64, ids []int64) ([]albumInfo, error) { if len(ids) == 0 { return []albumInfo{}, nil } in, args := inClause(ids) return h.albumsWhere(ctx, []int64{libID}, " AND i.id IN ("+in+")", args, "i.sort_title", 500, 0) } func containsID(ids []int64, id int64) bool { for _, v := range ids { if v == id { return true } } return false } // normalizeRel maps the library root, spelled "" or ".", to the canonical ".". func normalizeRel(rel string) string { rel = strings.Trim(path.Clean("/"+rel), "/") if rel == "" { return "." } return rel } func firstSegment(p string) string { if i := strings.IndexByte(p, '/'); i >= 0 { return p[:i] } return p } // parentDirOf returns the parent directory of a media_items path, mapping the // root to ".". func parentDirOf(p string) string { p = normalizeRel(p) return path.Dir(p) }