Subsonic: media retrieval endpoints (stream, download, getCoverArt, getLyrics, getAvatar, hls, getCaptions) with real transcoding, embedded lyrics extraction, and folder permission enforcement

This commit is contained in:
2026-08-23 00:15:00 -05:00
parent b6ec06f272
commit c7b472ab99
11 changed files with 792 additions and 1 deletions
+122 -1
View File
@@ -1,6 +1,10 @@
package database
import "database/sql"
import (
"database/sql"
"git.noroutes.net/NoRoutes/vibetunes/internal/idgen"
)
// ListVideoMediaFiles returns every media file the scanner identified as
// a video (as opposed to audio) track.
@@ -138,3 +142,120 @@ func AlbumsByRecentlyPlayed(db *sql.DB, userID string, limit, offset int) ([]*Al
defer rows.Close()
return scanAlbums(rows)
}
// SetSongLyrics replaces a song's stored (unsynced) lyrics with
// plainText, split into lines. Called by the scanner when a file has
// embedded lyrics.
func SetSongLyrics(db *sql.DB, songID, language, plainText string) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(`DELETE FROM lyrics WHERE song_id = ?`, songID); err != nil {
return err
}
if plainText == "" {
return tx.Commit()
}
lyricsID := idgen.New("ly")
if _, err := tx.Exec(`INSERT INTO lyrics (id, song_id, language, source, synced) VALUES (?, ?, ?, 'embedded', 0)`,
lyricsID, songID, language); err != nil {
return err
}
for i, line := range splitLines(plainText) {
if _, err := tx.Exec(`INSERT INTO lyrics_lines (id, lyrics_id, line_index, text) VALUES (?, ?, ?, ?)`,
idgen.New("ll"), lyricsID, i, line); err != nil {
return err
}
}
return tx.Commit()
}
func splitLines(s string) []string {
var lines []string
start := 0
for i := 0; i < len(s); i++ {
if s[i] == '\n' {
line := s[start:i]
line = trimCR(line)
lines = append(lines, line)
start = i + 1
}
}
lines = append(lines, trimCR(s[start:]))
return lines
}
func trimCR(s string) string {
if len(s) > 0 && s[len(s)-1] == '\r' {
return s[:len(s)-1]
}
return s
}
// SongLyricsPlainText returns a song's stored lyrics as plain text
// (lines joined with "\n"), and whether any were found.
func SongLyricsPlainText(db *sql.DB, songID string) (string, bool, error) {
var lyricsID string
err := db.QueryRow(`SELECT id FROM lyrics WHERE song_id = ? ORDER BY created_at DESC LIMIT 1`, songID).Scan(&lyricsID)
if err == sql.ErrNoRows {
return "", false, nil
}
if err != nil {
return "", false, err
}
rows, err := db.Query(`SELECT text FROM lyrics_lines WHERE lyrics_id = ? ORDER BY line_index`, lyricsID)
if err != nil {
return "", false, err
}
defer rows.Close()
var lines []string
for rows.Next() {
var t string
if err := rows.Scan(&t); err != nil {
return "", false, err
}
lines = append(lines, t)
}
if err := rows.Err(); err != nil {
return "", false, err
}
text := ""
for i, l := range lines {
if i > 0 {
text += "\n"
}
text += l
}
return text, true, nil
}
// FindSongByTitleArtist searches for a song by title (and optionally
// artist name), used by the legacy getLyrics endpoint.
func FindSongByTitleArtist(db *sql.DB, title, artist string) (*Song, error) {
if artist == "" {
var id string
err := db.QueryRow(`SELECT id FROM songs WHERE title = ? COLLATE NOCASE LIMIT 1`, title).Scan(&id)
if err == sql.ErrNoRows {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
return GetSong(db, id)
}
var id string
err := db.QueryRow(`SELECT s.id FROM songs s
JOIN song_artists sa ON sa.song_id = s.id
JOIN artists a ON a.id = sa.artist_id
WHERE s.title = ? COLLATE NOCASE AND a.name = ? COLLATE NOCASE LIMIT 1`, title, artist).Scan(&id)
if err == sql.ErrNoRows {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
return GetSong(db, id)
}