feat: add subsonic token auth and starred endpoints

This commit is contained in:
2026-04-02 23:14:10 +03:00
parent a640251e7d
commit b16f9de6c8
10 changed files with 535 additions and 68 deletions

View File

@@ -6,6 +6,7 @@ import (
"errors"
"net/http"
"os"
"strconv"
"strings"
"time"
@@ -26,7 +27,7 @@ type app struct {
func NewRouter(cfg config.Config, database *sql.DB, scanService *scanner.Service) http.Handler {
application := app{
auth: auth.NewService(database),
auth: auth.NewService(database, cfg.EncryptionKey),
library: library.NewService(database),
scanner: scanService,
}
@@ -80,6 +81,10 @@ func NewRouter(cfg config.Config, database *sql.DB, scanService *scanner.Service
authed.Get("/getAlbum.view", application.subsonicAlbumByID)
authed.Get("/getSong.view", application.subsonicSongByID)
authed.Get("/getRandomSongs.view", application.subsonicRandomSongs)
authed.Get("/search3.view", application.subsonicSearch3)
authed.Get("/getStarred2.view", application.subsonicStarred2)
authed.Get("/star.view", application.subsonicStar)
authed.Get("/unstar.view", application.subsonicUnstar)
authed.Get("/getScanStatus.view", application.subsonicScanStatus)
authed.Get("/startScan.view", application.subsonicStartScan)
authed.Get("/getCoverArt.view", application.subsonicCoverArt)
@@ -222,7 +227,13 @@ func (a app) subsonicArtists(w http.ResponseWriter, r *http.Request) {
}
func (a app) subsonicRandomSongs(w http.ResponseWriter, r *http.Request) {
tracks, err := a.library.Tracks(r.Context(), 20)
size := 20
if raw := strings.TrimSpace(r.URL.Query().Get("size")); raw != "" {
if parsed := parsePositiveInt(raw); parsed > 0 {
size = parsed
}
}
tracks, err := a.library.Tracks(r.Context(), size)
if err != nil {
writeJSON(w, http.StatusInternalServerError, subsonic.PingResponse())
return
@@ -230,6 +241,54 @@ func (a app) subsonicRandomSongs(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, subsonic.RandomSongsResponse(tracks))
}
func (a app) subsonicSearch3(w http.ResponseWriter, r *http.Request) {
query := strings.TrimSpace(r.URL.Query().Get("query"))
if query == "" {
writeJSON(w, http.StatusOK, subsonic.Search3Response(library.SearchResults{}))
return
}
count := parsePositiveInt(r.URL.Query().Get("songCount"))
if count == 0 {
count = 20
}
results, err := a.library.Search(r.Context(), query, count)
if err != nil {
writeJSON(w, http.StatusInternalServerError, subsonic.ErrorResponse(0, "search failed"))
return
}
writeJSON(w, http.StatusOK, subsonic.Search3Response(results))
}
func (a app) subsonicStarred2(w http.ResponseWriter, r *http.Request) {
user := currentUserFromContext(r)
results, err := a.library.Starred(r.Context(), user.ID)
if err != nil {
writeJSON(w, http.StatusInternalServerError, subsonic.ErrorResponse(0, "failed to load starred items"))
return
}
writeJSON(w, http.StatusOK, subsonic.Starred2Response(results))
}
func (a app) subsonicStar(w http.ResponseWriter, r *http.Request) {
user := currentUserFromContext(r)
if err := a.library.Star(r.Context(), user.ID, readMultiValue(r, "id"), readMultiValue(r, "albumId"), readMultiValue(r, "artistId")); err != nil {
writeJSON(w, http.StatusInternalServerError, subsonic.ErrorResponse(0, "failed to star items"))
return
}
writeJSON(w, http.StatusOK, subsonic.PingResponse())
}
func (a app) subsonicUnstar(w http.ResponseWriter, r *http.Request) {
user := currentUserFromContext(r)
if err := a.library.Unstar(r.Context(), user.ID, readMultiValue(r, "id"), readMultiValue(r, "albumId"), readMultiValue(r, "artistId")); err != nil {
writeJSON(w, http.StatusInternalServerError, subsonic.ErrorResponse(0, "failed to unstar items"))
return
}
writeJSON(w, http.StatusOK, subsonic.PingResponse())
}
func (a app) subsonicArtistByID(w http.ResponseWriter, r *http.Request) {
item, err := a.library.ArtistByID(r.Context(), r.URL.Query().Get("id"))
if err != nil {
@@ -401,6 +460,25 @@ func writeJSON(w http.ResponseWriter, status int, payload any) {
_ = json.NewEncoder(w).Encode(payload)
}
func readMultiValue(r *http.Request, key string) []string {
values := r.URL.Query()[key]
if len(values) > 0 {
return values
}
if single := strings.TrimSpace(r.URL.Query().Get(key)); single != "" {
return []string{single}
}
return nil
}
func parsePositiveInt(raw string) int {
value, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil || value < 1 {
return 0
}
return value
}
func spaFallback(next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api") || strings.HasPrefix(r.URL.Path, "/rest") || strings.HasPrefix(r.URL.Path, "/health") {