589 lines
15 KiB
Go
589 lines
15 KiB
Go
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"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
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 (h *Handlers) RefreshArtist(w http.ResponseWriter, r *http.Request) {
|
|
if h.DB == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
|
return
|
|
}
|
|
|
|
artistID := chi.URLParam(r, "id")
|
|
if artistID == "" {
|
|
writeError(w, http.StatusBadRequest, "artist ID required")
|
|
return
|
|
}
|
|
|
|
result, err := services.RefreshArtist(r.Context(), artistID, h.MetadataClient, 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) DeleteArtist(w http.ResponseWriter, r *http.Request) {
|
|
if h.DB == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
|
return
|
|
}
|
|
|
|
artistID := chi.URLParam(r, "id")
|
|
if artistID == "" {
|
|
writeError(w, http.StatusBadRequest, "artist ID required")
|
|
return
|
|
}
|
|
|
|
deleted, err := h.DB.DeleteArtistByForeignID(r.Context(), artistID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
if !deleted {
|
|
writeError(w, http.StatusNotFound, "artist not found: "+artistID)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"deleted": true,
|
|
"message": "artist and related data deleted",
|
|
})
|
|
}
|
|
|
|
func (h *Handlers) GetArtist(w http.ResponseWriter, r *http.Request) {
|
|
if h.DB == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
|
return
|
|
}
|
|
|
|
artistID := chi.URLParam(r, "id")
|
|
if artistID == "" {
|
|
writeError(w, http.StatusBadRequest, "artist ID required")
|
|
return
|
|
}
|
|
|
|
artist, err := h.DB.GetArtistByForeignID(r.Context(), artistID)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "artist not found: "+artistID)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, artist)
|
|
}
|
|
|
|
func (h *Handlers) EditArtist(w http.ResponseWriter, r *http.Request) {
|
|
if h.DB == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
|
return
|
|
}
|
|
|
|
artistID := chi.URLParam(r, "id")
|
|
if artistID == "" {
|
|
writeError(w, http.StatusBadRequest, "artist ID required")
|
|
return
|
|
}
|
|
|
|
var update database.ArtistUpdate
|
|
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
artist, err := h.DB.UpdateArtistByForeignID(r.Context(), artistID, update)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "artist not found: "+artistID)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, artist)
|
|
}
|
|
|
|
func (h *Handlers) GetAlbum(w http.ResponseWriter, r *http.Request) {
|
|
if h.DB == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
|
return
|
|
}
|
|
|
|
albumIDStr := chi.URLParam(r, "id")
|
|
albumID, err := parseUUID(albumIDStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid album ID")
|
|
return
|
|
}
|
|
|
|
album, err := h.DB.GetAlbumDetailByID(r.Context(), albumID)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "album not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, album)
|
|
}
|
|
|
|
func (h *Handlers) EditAlbum(w http.ResponseWriter, r *http.Request) {
|
|
if h.DB == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
|
return
|
|
}
|
|
|
|
albumIDStr := chi.URLParam(r, "id")
|
|
albumID, err := parseUUID(albumIDStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid album ID")
|
|
return
|
|
}
|
|
|
|
var update struct {
|
|
Monitored *bool `json:"monitored"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if update.Monitored != nil {
|
|
if err := h.DB.UpdateAlbumMonitored(r.Context(), albumID, *update.Monitored); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
if *update.Monitored {
|
|
hasFiles, _ := h.DB.HasTrackFiles(r.Context(), albumID)
|
|
if !hasFiles {
|
|
h.DB.AddToWantedAlbums(r.Context(), albumID)
|
|
}
|
|
} else {
|
|
h.DB.RemoveFromWantedAlbums(r.Context(), albumID)
|
|
}
|
|
}
|
|
|
|
album, err := h.DB.GetAlbumDetailByID(r.Context(), albumID)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "album not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, album)
|
|
}
|
|
|
|
func (h *Handlers) SearchAlbum(w http.ResponseWriter, r *http.Request) {
|
|
if h.DB == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
|
return
|
|
}
|
|
|
|
albumIDStr := chi.URLParam(r, "id")
|
|
albumID, err := parseUUID(albumIDStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid album ID")
|
|
return
|
|
}
|
|
|
|
result, err := services.SearchAlbum(r.Context(), albumID, h.DB, h.IndexerService)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "album not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (h *Handlers) BulkMonitorArtistAlbums(w http.ResponseWriter, r *http.Request) {
|
|
if h.DB == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
|
return
|
|
}
|
|
|
|
artistID := chi.URLParam(r, "id")
|
|
if artistID == "" {
|
|
writeError(w, http.StatusBadRequest, "artist ID required")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Monitored bool `json:"monitored"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
artist, err := h.DB.GetArtistMetadataByForeignID(r.Context(), artistID)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "artist not found")
|
|
return
|
|
}
|
|
|
|
updatedCount, err := h.DB.BulkUpdateAlbumsMonitored(r.Context(), artist.ID, req.Monitored)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
albums, _ := h.DB.ListAlbumsByArtist(r.Context(), artist.ID)
|
|
for _, album := range albums {
|
|
if req.Monitored {
|
|
hasFiles, _ := h.DB.HasTrackFiles(r.Context(), album.ID)
|
|
if !hasFiles {
|
|
h.DB.AddToWantedAlbums(r.Context(), album.ID)
|
|
}
|
|
} else {
|
|
h.DB.RemoveFromWantedAlbums(r.Context(), album.ID)
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"updated_count": updatedCount,
|
|
"monitored": req.Monitored,
|
|
})
|
|
}
|
|
|
|
func (h *Handlers) SearchArtistAlbums(w http.ResponseWriter, r *http.Request) {
|
|
if h.DB == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
|
return
|
|
}
|
|
|
|
artistID := chi.URLParam(r, "id")
|
|
if artistID == "" {
|
|
writeError(w, http.StatusBadRequest, "artist ID required")
|
|
return
|
|
}
|
|
|
|
result, err := services.SearchArtistAlbums(r.Context(), artistID, h.DB, h.IndexerService)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "artist not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (h *Handlers) AddToBlocklist(w http.ResponseWriter, r *http.Request) {
|
|
if h.DB == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
AlbumID string `json:"album_id"`
|
|
SourceTitle string `json:"source_title"`
|
|
GUID *string `json:"guid"`
|
|
TorrentHash *string `json:"torrent_hash"`
|
|
Indexer *string `json:"indexer"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
albumID, err := parseUUID(req.AlbumID)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid album_id")
|
|
return
|
|
}
|
|
|
|
artistID, err := h.DB.GetArtistIDByAlbum(r.Context(), albumID)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "album not found")
|
|
return
|
|
}
|
|
|
|
if err := h.DB.AddToBlocklist(r.Context(), *artistID, albumID, req.SourceTitle, req.TorrentHash, req.Indexer); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"added": true,
|
|
})
|
|
}
|
|
|
|
func parseUUID(s string) (uuid.UUID, error) {
|
|
return uuid.Parse(s)
|
|
}
|
|
|
|
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})
|
|
}
|