Files
vibesonic/internal/subsonic/browsing_extra.go
T

251 lines
6.8 KiB
Go

package subsonic
import (
"database/sql"
"os"
"path/filepath"
"strconv"
"strings"
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
"git.noroutes.net/NoRoutes/vibetunes/internal/streaming"
)
func handleGetVideos(rc *Context) *apiError {
db := rc.Server.DB
files, err := database.ListVideoMediaFiles(db)
if err != nil {
return newError(ErrGeneric, err.Error())
}
out := &Videos{}
for _, mf := range files {
if canSee, cerr := UserCanSeeMediaFile(db, rc.User, mf); cerr != nil || !canSee {
continue
}
song, serr := database.GetSongByMediaFileID(db, mf.ID)
if serr != nil {
continue
}
out.Video = append(out.Video, childFromSong(db, song, mf, albumOfSong(db, song), rc.User.ID))
}
rc.Response = newResponse()
rc.Response.Videos = out
return nil
}
// sidecarCaptionExtensions are checked next to a video file for a
// same-basename subtitle track — the common convention most media
// managers and players use (movie.mp4 + movie.srt/.vtt/.ass).
var sidecarCaptionExtensions = []string{".srt", ".vtt", ".ass"}
func findSidecarCaptions(videoAbsPath string) []string {
dir := filepath.Dir(videoAbsPath)
base := strings.TrimSuffix(filepath.Base(videoAbsPath), filepath.Ext(videoAbsPath))
var found []string
for _, ext := range sidecarCaptionExtensions {
p := filepath.Join(dir, base+ext)
if _, err := os.Stat(p); err == nil {
found = append(found, p)
}
}
return found
}
func handleGetVideoInfo(rc *Context) *apiError {
id, aerr := rc.Params.Required("id")
if aerr != nil {
return aerr
}
db := rc.Server.DB
song, err := database.GetSong(db, id)
if err != nil {
return errNotFound("Video")
}
mf, err := database.GetMediaFile(db, song.MediaFileID)
if err != nil {
return errNotFound("Video")
}
if canSee, cerr := UserCanSeeMediaFile(db, rc.User, mf); cerr != nil || !canSee {
return errUnauthorized("video")
}
folder, err := database.GetMusicFolder(db, mf.MusicFolderID)
if err != nil {
return newError(ErrGeneric, err.Error())
}
absPath, perr := streaming.ResolvePath(folder.Path, mf.RelativePath)
if perr != nil {
return newError(ErrGeneric, perr.Error())
}
info := &VideoInfo{ID: id, AudioTrack: []AudioTrack{{ID: id, Name: "Default"}}}
for i, capPath := range findSidecarCaptions(absPath) {
info.Captions = append(info.Captions, Captions{ID: id + ":cap:" + strconv.Itoa(i), Name: filepath.Base(capPath)})
}
rc.Response = newResponse()
rc.Response.VideoInfo = info
return nil
}
// --- Artist/album info: VibeSonic has no external metadata provider
// (last.fm etc.) in Phase 2, so these honestly return only what's known
// locally — which for biography/images/similar-artists is nothing —
// rather than fabricating placeholder content. ---
func handleGetArtistInfo(rc *Context) *apiError {
if _, aerr := rc.Params.Required("id"); aerr != nil {
return aerr
}
rc.Response = newResponse()
rc.Response.ArtistInfo = &ArtistInfo{}
return nil
}
func handleGetArtistInfo2(rc *Context) *apiError {
if _, aerr := rc.Params.Required("id"); aerr != nil {
return aerr
}
rc.Response = newResponse()
rc.Response.ArtistInfo2 = &ArtistInfo2{}
return nil
}
func handleGetAlbumInfo(rc *Context) *apiError {
if _, aerr := rc.Params.Required("id"); aerr != nil {
return aerr
}
rc.Response = newResponse()
rc.Response.AlbumInfo = &AlbumInfo{}
return nil
}
func handleGetAlbumInfo2(rc *Context) *apiError {
if _, aerr := rc.Params.Required("id"); aerr != nil {
return aerr
}
rc.Response = newResponse()
rc.Response.AlbumInfo = &AlbumInfo{}
return nil
}
// --- Similar/top songs: a real, local, genre-based heuristic rather
// than a fabricated or hardcoded list. ---
func handleGetSimilarSongs(rc *Context) *apiError {
id, aerr := rc.Params.Required("id")
if aerr != nil {
return aerr
}
count := rc.Params.Int("count", 50)
db := rc.Server.DB
songs, serr := similarSongsFor(db, id, count)
if serr != nil {
return errNotFound("Item")
}
out := &SimilarSongs{}
for _, s := range songs {
mf, merr := database.GetMediaFile(db, s.MediaFileID)
if merr != nil {
continue
}
out.Song = append(out.Song, childFromSong(db, s, mf, albumOfSong(db, s), rc.User.ID))
}
rc.Response = newResponse()
rc.Response.SimilarSongs = out
return nil
}
func handleGetSimilarSongs2(rc *Context) *apiError {
id, aerr := rc.Params.Required("id")
if aerr != nil {
return aerr
}
count := rc.Params.Int("count", 50)
db := rc.Server.DB
// id here names an artist (ID3 context).
songs, err := database.SimilarSongsByArtist(db, id, count)
if err != nil {
return newError(ErrGeneric, err.Error())
}
out := &SimilarSongs2{}
for _, s := range songs {
mf, merr := database.GetMediaFile(db, s.MediaFileID)
if merr != nil {
continue
}
out.Song = append(out.Song, childFromSong(db, s, mf, albumOfSong(db, s), rc.User.ID))
}
rc.Response = newResponse()
rc.Response.SimilarSongs2 = out
return nil
}
// similarSongsFor resolves id as a song, album, or artist (getSimilarSongs's
// "id" can be any of the three in real Subsonic usage) and finds
// genre-similar songs for it.
func similarSongsFor(db *sql.DB, id string, count int) ([]*database.Song, error) {
if song, err := database.GetSong(db, id); err == nil {
return database.SimilarSongsByGenre(db, song.ID, count)
}
if _, err := database.GetAlbum(db, id); err == nil {
songs, err := database.ListSongsByAlbum(db, id)
if err != nil || len(songs) == 0 {
return nil, err
}
return database.SimilarSongsByGenre(db, songs[0].ID, count)
}
if _, err := database.GetArtist(db, id); err == nil {
return database.SimilarSongsByArtist(db, id, count)
}
return nil, database.ErrNotFound
}
func handleGetTopSongs(rc *Context) *apiError {
count := rc.Params.Int("count", 50)
db := rc.Server.DB
// topSongsByArtistId: an explicit artist ID (the canonical internal
// ID, same as everywhere else) takes priority over the legacy
// name-based lookup when both are given, since it's unambiguous
// where a name match can collide.
var artistID string
if id := rc.Params.Get("id"); id != "" {
artist, err := database.GetArtist(db, id)
if err != nil {
rc.Response = newResponse()
rc.Response.TopSongs = &TopSongs{}
return nil
}
artistID = artist.ID
} else {
artistName, aerr := rc.Params.Required("artist")
if aerr != nil {
return aerr
}
artist, err := database.FindArtistByName(db, artistName)
if err != nil {
rc.Response = newResponse()
rc.Response.TopSongs = &TopSongs{}
return nil
}
artistID = artist.ID
}
songs, err := database.TopSongsByArtist(db, artistID, count)
if err != nil {
return newError(ErrGeneric, err.Error())
}
out := &TopSongs{}
for _, s := range songs {
mf, merr := database.GetMediaFile(db, s.MediaFileID)
if merr != nil {
continue
}
out.Song = append(out.Song, childFromSong(db, s, mf, albumOfSong(db, s), rc.User.ID))
}
rc.Response = newResponse()
rc.Response.TopSongs = out
return nil
}