package api import ( "encoding/json" "net/http" "github.com/fujin/music-agregator/internal/database" "github.com/fujin/music-agregator/internal/indexer" "github.com/fujin/music-agregator/internal/metadata" "github.com/fujin/music-agregator/internal/services" "github.com/go-chi/chi/v5" ) type Handlers struct { IndexerService *services.IndexerService TorrentService *services.TorrentService MetadataClient *metadata.Client DB *database.DB } func (h *Handlers) Health(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (h *Handlers) ListIndexers(w http.ResponseWriter, r *http.Request) { indexers := h.IndexerService.GetIndexers(r.Context()) writeJSON(w, http.StatusOK, indexers) } type searchRequest struct { Artist string `json:"artist"` Album *string `json:"album,omitempty"` Year *uint32 `json:"year,omitempty"` Limit int `json:"limit,omitempty"` Offset int `json:"offset,omitempty"` } func (h *Handlers) SearchIndexers(w http.ResponseWriter, r *http.Request) { var req searchRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Limit == 0 { req.Limit = 20 } criteria := &indexer.MusicSearchCriteria{ Artist: req.Artist, Album: req.Album, Year: req.Year, Limit: req.Limit, Offset: req.Offset, } results, err := h.IndexerService.Search(r.Context(), criteria, nil) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, results) } func (h *Handlers) ListTorrents(w http.ResponseWriter, r *http.Request) { torrents, err := h.TorrentService.ListTorrents(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, torrents) } func (h *Handlers) GetTorrent(w http.ResponseWriter, r *http.Request) { hash := chi.URLParam(r, "hash") torrent, err := h.TorrentService.GetTorrent(r.Context(), hash) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, torrent) } type addTorrentRequest struct { URL string `json:"url"` SavePath *string `json:"save_path,omitempty"` } func (h *Handlers) AddTorrent(w http.ResponseWriter, r *http.Request) { var req addTorrentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if err := h.TorrentService.AddTorrentURL(r.Context(), req.URL, req.SavePath); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "added"}) } type removeTorrentRequest struct { DeleteFiles bool `json:"delete_files"` } func (h *Handlers) RemoveTorrent(w http.ResponseWriter, r *http.Request) { hash := chi.URLParam(r, "hash") var req removeTorrentRequest json.NewDecoder(r.Body).Decode(&req) if err := h.TorrentService.RemoveTorrent(r.Context(), hash, req.DeleteFiles); err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "removed"}) } func (h *Handlers) PauseTorrent(w http.ResponseWriter, r *http.Request) { hash := chi.URLParam(r, "hash") if err := h.TorrentService.PauseTorrent(r.Context(), hash); err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "paused"}) } func (h *Handlers) ResumeTorrent(w http.ResponseWriter, r *http.Request) { hash := chi.URLParam(r, "hash") if err := h.TorrentService.ResumeTorrent(r.Context(), hash); err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "resumed"}) } func (h *Handlers) SearchArtists(w http.ResponseWriter, r *http.Request) { var req struct { Query string `json:"query"` Limit int32 `json:"limit,omitempty"` Offset int32 `json:"offset,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Limit == 0 { req.Limit = 10 } result, err := h.MetadataClient.SearchArtists(r.Context(), req.Query, req.Limit, req.Offset) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, result) } func (h *Handlers) GetArtistAlbums(w http.ResponseWriter, r *http.Request) { artistID := chi.URLParam(r, "id") result, err := h.MetadataClient.GetArtistAlbums(r.Context(), artistID, 500, 0) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, result) } func (h *Handlers) Sync(w http.ResponseWriter, r *http.Request) { var req struct { Artist string `json:"artist"` Album *string `json:"album,omitempty"` Download *bool `json:"download,omitempty"` Store *bool `json:"store,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } download := true if req.Download != nil { download = *req.Download } store := true if req.Store != nil { store = *req.Store } options := services.SyncOptions{ Artist: req.Artist, Album: req.Album, Download: download, Store: store, } result, err := services.Sync(r.Context(), options, h.MetadataClient, h.IndexerService, h.TorrentService, h.DB) if err != nil { if _, ok := err.(*services.NotFoundError); ok { writeError(w, http.StatusNotFound, err.Error()) return } writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, result) } func (h *Handlers) ListLibraryArtists(w http.ResponseWriter, r *http.Request) { if h.DB == nil { writeError(w, http.StatusServiceUnavailable, "database not connected") return } artists, err := h.DB.ListArtists(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, artists) } func (h *Handlers) ListLibraryAlbums(w http.ResponseWriter, r *http.Request) { if h.DB == nil { writeError(w, http.StatusServiceUnavailable, "database not connected") return } albums, err := h.DB.ListAllAlbums(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, albums) } func (h *Handlers) LibraryStats(w http.ResponseWriter, r *http.Request) { if h.DB == nil { writeError(w, http.StatusServiceUnavailable, "database not connected") return } artistCount, err := h.DB.CountArtists(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } albumCount, err := h.DB.CountAlbums(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]int64{ "artists": artistCount, "albums": albumCount, }) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]string{"error": message}) }