feat: import library from media root and stream tracks
Add a filesystem scanner that ingests supported audio files from MEDIA_ROOT into SQLite using embedded tags with filename fallbacks. Wire startup scanning, manual rescan, and authenticated audio streaming into the backend, then connect the web player to the real stream endpoint.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -13,18 +14,21 @@ import (
|
||||
"github.com/benya/temporserv/internal/auth"
|
||||
"github.com/benya/temporserv/internal/config"
|
||||
"github.com/benya/temporserv/internal/library"
|
||||
"github.com/benya/temporserv/internal/scanner"
|
||||
"github.com/benya/temporserv/internal/subsonic"
|
||||
)
|
||||
|
||||
type app struct {
|
||||
auth *auth.Service
|
||||
library *library.Service
|
||||
scanner *scanner.Service
|
||||
}
|
||||
|
||||
func NewRouter(cfg config.Config, database *sql.DB) http.Handler {
|
||||
func NewRouter(cfg config.Config, database *sql.DB, scanService *scanner.Service) http.Handler {
|
||||
application := app{
|
||||
auth: auth.NewService(database),
|
||||
library: library.NewService(database),
|
||||
scanner: scanService,
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
@@ -48,7 +52,10 @@ func NewRouter(cfg config.Config, database *sql.DB) http.Handler {
|
||||
private.Get("/me", application.me)
|
||||
private.Get("/home", application.home)
|
||||
private.Get("/tracks", application.tracks)
|
||||
private.Post("/admin/scan", application.scanLibrary)
|
||||
})
|
||||
|
||||
api.Get("/stream/{id}", application.streamTrack)
|
||||
})
|
||||
|
||||
r.Route("/rest", func(rest chi.Router) {
|
||||
@@ -133,6 +140,49 @@ func (a app) subsonicRandomSongs(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, subsonic.RandomSongsResponse(tracks))
|
||||
}
|
||||
|
||||
func (a app) scanLibrary(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := a.scanner.Scan(r.Context())
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (a app) streamTrack(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "))
|
||||
}
|
||||
|
||||
if _, err := a.auth.CurrentUserByToken(r.Context(), token); err != nil {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
track, err := a.library.TrackByID(r.Context(), chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "track not found"})
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.Open(track.FilePath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "audio file not available"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to stat file"})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", track.ContentType)
|
||||
http.ServeContent(w, r, info.Name(), info.ModTime(), file)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
|
||||
Reference in New Issue
Block a user