package controller import ( "butterfliu/internal/service" "encoding/json" "log" "net/http" "strconv" "sync" "time" "github.com/go-chi/chi/v5" ) type ScanStatus struct { Running bool `json:"running"` LibraryID int `json:"library_id"` Report *service.ScanReport `json:"report,omitempty"` Error string `json:"error,omitempty"` StartedAt *time.Time `json:"started_at,omitempty"` FinishedAt *time.Time `json:"finished_at,omitempty"` } type LibraryController struct { libraryService *service.LibraryService coverService *service.CoverService scanMutex sync.Mutex scanStatus ScanStatus } func NewLibraryController(libraryService *service.LibraryService, coverSvc *service.CoverService) *LibraryController { return &LibraryController{libraryService: libraryService, coverService: coverSvc} } func (c *LibraryController) GetAll(w http.ResponseWriter, r *http.Request) { libraries, err := c.libraryService.GetAll() if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, libraries, http.StatusOK) } func (c *LibraryController) Create(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` Path string `json:"path"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } if req.Name == "" || req.Path == "" { jsonError(w, "name and path are required", http.StatusBadRequest) return } _, err := c.libraryService.Create(req.Name, req.Path) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonMsg(w, "Add library successfully.") } func (c *LibraryController) GetByID(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } l, err := c.libraryService.GetByID(id) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, l, http.StatusOK) } func (c *LibraryController) UpdateName(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } var name string if err := json.NewDecoder(r.Body).Decode(&name); err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } err = c.libraryService.UpdateName(id, name) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonMsg(w, "Updated library name.") } func (c *LibraryController) UpdatePath(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } var path string if err := json.NewDecoder(r.Body).Decode(&path); err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } err = c.libraryService.UpdatePath(id, path) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonMsg(w, "Updated library path.") } func (c *LibraryController) Scan(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } c.scanMutex.Lock() if c.scanStatus.Running { c.scanMutex.Unlock() jsonError(w, "Another scan is already in progress", http.StatusConflict) return } startedAt := time.Now() c.scanStatus = ScanStatus{ Running: true, LibraryID: id, Report: &service.ScanReport{}, StartedAt: &startedAt, } startedStatus := c.cloneScanStatusLocked() c.scanMutex.Unlock() go func() { report, err := c.libraryService.Scan(id, func(progress service.ScanReport) { c.scanMutex.Lock() if c.scanStatus.LibraryID == id { progressCopy := progress c.scanStatus.Report = &progressCopy } c.scanMutex.Unlock() }) c.scanMutex.Lock() defer c.scanMutex.Unlock() finishedAt := time.Now() c.scanStatus.Running = false c.scanStatus.FinishedAt = &finishedAt if report != nil { reportCopy := *report c.scanStatus.Report = &reportCopy } if err != nil { c.scanStatus.Error = err.Error() log.Printf("Scan failed for library %d: %v", id, err) return } c.scanStatus.Error = "" log.Printf("Scan completed for library %d: %+v", id, report) }() jsonResponse(w, startedStatus, http.StatusAccepted) } func (c *LibraryController) GetScanStatus(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } c.scanMutex.Lock() status := c.cloneScanStatusLocked() c.scanMutex.Unlock() if status.LibraryID != id { jsonResponse(w, ScanStatus{LibraryID: id, Report: &service.ScanReport{}}, http.StatusOK) return } jsonResponse(w, status, http.StatusOK) } func (c *LibraryController) cloneScanStatusLocked() ScanStatus { status := c.scanStatus if c.scanStatus.Report != nil { reportCopy := *c.scanStatus.Report reportCopy.FailedFiles = append([]string(nil), c.scanStatus.Report.FailedFiles...) status.Report = &reportCopy } return status } func (c *LibraryController) Delete(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } err = c.libraryService.Delete(id) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonMsg(w, "Deleted library.") } func (c *LibraryController) GetSongs(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } songs, err := c.libraryService.GetSongs(id) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, songs, http.StatusOK) } func (c *LibraryController) GetSongsByArtist(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } songs, err := c.libraryService.GetSongsByArtist(id) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, songs, http.StatusOK) } func (c *LibraryController) GetSongsByAlbum(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } songs, err := c.libraryService.GetSongsByAlbum(id) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, songs, http.StatusOK) } func (c *LibraryController) GetArtists(w http.ResponseWriter, r *http.Request) { artists, err := c.libraryService.GetArtists() if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, artists, http.StatusOK) } func (c *LibraryController) GetArtist(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } artist, err := c.libraryService.GetArtist(id) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, artist, http.StatusOK) } func (c *LibraryController) GetAlbums(w http.ResponseWriter, r *http.Request) { albums, err := c.libraryService.GetAlbums() if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, albums, http.StatusOK) } func (c *LibraryController) GetAlbumsByArtist(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } albums, err := c.libraryService.GetAlbumsByArtistWithDetail(id) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, albums, http.StatusOK) } func (c *LibraryController) GetAlbum(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } album, err := c.libraryService.GetAlbum(id) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, album, http.StatusOK) } func (c *LibraryController) GetAlbumCover(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.Atoi(idParam) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } coverPath, err := c.coverService.GetAlbumCover(id) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } http.ServeFile(w, r, coverPath) }