diff --git a/discover.go b/discover.go index c6f9ee9..b24f020 100644 --- a/discover.go +++ b/discover.go @@ -80,6 +80,23 @@ func searchMBArtists(query string) ([]mbArtist, error) { return result.Artists, err } +// getFirstReleaseMBID returns the MBID of the first release listed under a +// release group. This is needed because beets --search-id requires a release +// MBID, not a release group MBID. +// Returns empty string on error so callers can fall back gracefully. +func getFirstReleaseMBID(rgMBID string) string { + var result struct { + Releases []struct { + ID string `json:"id"` + } `json:"releases"` + } + path := fmt.Sprintf("/ws/2/release-group/%s?fmt=json&inc=releases", url.QueryEscape(rgMBID)) + if err := mbGet(path, &result); err != nil || len(result.Releases) == 0 { + return "" + } + return result.Releases[0].ID +} + // getMBArtistReleaseGroups returns all Album and EP release groups for an artist, // paginating through the MusicBrainz browse API with the required 1 req/s rate limit. func getMBArtistReleaseGroups(artistMBID string) ([]mbReleaseGroup, error) { @@ -136,15 +153,24 @@ func fetchArtist(artistMBID, artistName string, logf func(string)) error { failed := 0 for i, rg := range groups { logf(fmt.Sprintf("[%d/%d] %s", i+1, len(groups), rg.Title)) - folder, err := fetchRelease(artistName, rg.Title, rg.ID, logf) + // Resolve a release MBID for this release group. beets --search-id + // requires a release MBID; release group MBIDs are not accepted. + time.Sleep(time.Second) // MusicBrainz rate limit + releaseMBID := getFirstReleaseMBID(rg.ID) + if releaseMBID == "" { + logf(fmt.Sprintf(" ↳ warning: could not resolve release MBID for group %s, beets will search by name", rg.ID)) + } + + folder, err := fetchRelease(artistName, rg.Title, releaseMBID, logf) if err != nil { log.Printf("[discover] fetch failed for %q by %s: %v", rg.Title, artistName, err) logf(fmt.Sprintf(" ↳ failed: %v", err)) failed++ continue } - registerDownload(rg.ID, artistName, rg.Title, folder, nil) - logf(fmt.Sprintf(" ↳ registered for import (mbid: %s)", rg.ID)) + // Key the pending download by release group ID for dedup; beets uses releaseMBID. + registerDownload(rg.ID, releaseMBID, artistName, rg.Title, folder, nil) + logf(fmt.Sprintf(" ↳ registered for import (release mbid: %s)", releaseMBID)) } if failed > 0 { @@ -290,7 +316,7 @@ func handleDiscoverFetch(w http.ResponseWriter, r *http.Request) { return } log.Printf("[discover] fetch complete for %q by %s, registering for import", body.Album, body.Artist) - registerDownload(body.ID, body.Artist, body.Album, folder, entry) + registerDownload(body.ID, body.ID, body.Artist, body.Album, folder, entry) // entry.finish is called by the monitor when import completes }() diff --git a/files.go b/files.go index 943aebd..75a388c 100644 --- a/files.go +++ b/files.go @@ -9,8 +9,9 @@ import ( "strings" ) -// moveToLibrary moves a file to {libDir}/{artist}/[{date}] {album} [{quality}]/filename. -func moveToLibrary(libDir string, md *MusicMetadata, srcPath string) error { +// albumTargetDir returns the destination directory for an album without +// creating it. Use this to check for an existing import before moving files. +func albumTargetDir(libDir string, md *MusicMetadata) string { date := md.Date if date == "" { date = md.Year @@ -19,7 +20,12 @@ func moveToLibrary(libDir string, md *MusicMetadata, srcPath string) error { if md.Quality != "" { albumDir += fmt.Sprintf(" [%s]", md.Quality) } - targetDir := filepath.Join(libDir, sanitize(md.Artist), sanitize(albumDir)) + return filepath.Join(libDir, sanitize(md.Artist), sanitize(albumDir)) +} + +// moveToLibrary moves a file to {libDir}/{artist}/[{date}] {album} [{quality}]/filename. +func moveToLibrary(libDir string, md *MusicMetadata, srcPath string) error { + targetDir := albumTargetDir(libDir, md) if err := os.MkdirAll(targetDir, 0755); err != nil { return err } diff --git a/importer.go b/importer.go index 6a27f9c..a7d5c5c 100644 --- a/importer.go +++ b/importer.go @@ -233,33 +233,39 @@ func RunImporter() { continue } - fmt.Println("→ Moving tracks into library for album:", albumPath) - for _, track := range tracks { - if err := moveToLibrary(libraryDir, md, track); err != nil { - fmt.Println("Failed to move track:", track, err) - result.Move.Err = err // retains last error; all attempts are still made + targetDir := albumTargetDir(libraryDir, md) + if _, err := os.Stat(targetDir); err == nil { + fmt.Println("→ Album already exists in library, skipping move:", targetDir) + result.Move.Skipped = true + } else { + fmt.Println("→ Moving tracks into library for album:", albumPath) + for _, track := range tracks { + if err := moveToLibrary(libraryDir, md, track); err != nil { + fmt.Println("Failed to move track:", track, err) + result.Move.Err = err // retains last error; all attempts are still made + } } - } - lyrics, _ := getLyricFiles(albumPath) + lyrics, _ := getLyricFiles(albumPath) - fmt.Println("→ Moving lyrics into library for album:", albumPath) - for _, file := range lyrics { - if err := moveToLibrary(libraryDir, md, file); err != nil { - fmt.Println("Failed to move lyrics:", file, err) - result.Move.Err = err + fmt.Println("→ Moving lyrics into library for album:", albumPath) + for _, file := range lyrics { + if err := moveToLibrary(libraryDir, md, file); err != nil { + fmt.Println("Failed to move lyrics:", file, err) + result.Move.Err = err + } } - } - fmt.Println("→ Moving album cover into library for album:", albumPath) - if coverImg, err := FindCoverImage(albumPath); err == nil { - if err := moveToLibrary(libraryDir, md, coverImg); err != nil { - fmt.Println("Failed to cover image:", coverImg, err) - result.Move.Err = err + fmt.Println("→ Moving album cover into library for album:", albumPath) + if coverImg, err := FindCoverImage(albumPath); err == nil { + if err := moveToLibrary(libraryDir, md, coverImg); err != nil { + fmt.Println("Failed to cover image:", coverImg, err) + result.Move.Err = err + } } - } - os.Remove(albumPath) + os.Remove(albumPath) + } } fmt.Println("\n=== Import Complete ===") diff --git a/metadata.go b/metadata.go index af7734c..19f622f 100644 --- a/metadata.go +++ b/metadata.go @@ -220,9 +220,10 @@ func tagWithBeets(path, mbid string) error { defer os.Remove(logPath) args := []string{"import", "-Cq", "-l", logPath} - if mbid != "" { - args = append(args, "--search-id", mbid) - } + // passing mbid to beet removed temporarily + // if mbid != "" { + // args = append(args, "--search-id", mbid) + // } args = append(args, path) if err := runCmd("beet", args...); err != nil { return err diff --git a/monitor.go b/monitor.go index 2b842df..77f06b7 100644 --- a/monitor.go +++ b/monitor.go @@ -13,7 +13,8 @@ import ( // pendingDownload tracks a queued slskd download that should be auto-imported // once all files have transferred successfully. type pendingDownload struct { - MBID string + ID string // dedup key (release MBID for single fetches; release group MBID for artist fetches) + BeetsMBID string // release MBID passed to beets --search-id (may differ from ID) Artist string Album string Username string // slskd peer username @@ -28,32 +29,35 @@ var ( ) // registerDownload records a queued slskd download for monitoring and eventual -// auto-import. If entry is nil a new fetchEntry is created, keyed by mbid, -// so the frontend can discover it via /discover/fetch/list. -func registerDownload(mbid, artist, album string, folder *albumFolder, entry *fetchEntry) { +// auto-import. id is used as the dedup key; beetsMBID is the release MBID +// forwarded to beets --search-id (may be empty or differ from id). +// If entry is nil a new fetchEntry is created so the frontend can discover it +// via /discover/fetch/list. +func registerDownload(id, beetsMBID, artist, album string, folder *albumFolder, entry *fetchEntry) { pd := &pendingDownload{ - MBID: mbid, - Artist: artist, - Album: album, - Username: folder.Username, - Dir: folder.Dir, - Files: folder.Files, - Entry: entry, + ID: id, + BeetsMBID: beetsMBID, + Artist: artist, + Album: album, + Username: folder.Username, + Dir: folder.Dir, + Files: folder.Files, + Entry: entry, } if entry == nil { - e := newFetchEntry(mbid, artist, album) + e := newFetchEntry(id, artist, album) e.appendLog(fmt.Sprintf("Queued %d files from %s — waiting for download", len(folder.Files), folder.Username)) pd.Entry = e } pendingMu.Lock() - pendingDownloads[mbid] = pd + pendingDownloads[id] = pd pendingMu.Unlock() - log.Printf("[monitor] registered: %q by %s (mbid: %s, peer: %s, %d files)", - album, artist, mbid, folder.Username, len(folder.Files)) + log.Printf("[monitor] registered: %q by %s (id: %s, beets mbid: %s, peer: %s, %d files)", + album, artist, id, beetsMBID, folder.Username, len(folder.Files)) } // startMonitor launches a background goroutine that periodically checks whether @@ -128,7 +132,7 @@ func checkPendingDownloads() { // Remove from pending before starting import to avoid double-import. pendingMu.Lock() - delete(pendingDownloads, pd.MBID) + delete(pendingDownloads, pd.ID) pendingMu.Unlock() go importPendingRelease(pd, localDir) @@ -189,7 +193,7 @@ func importPendingRelease(pd *pendingDownload, localDir string) { entry := pd.Entry logf := func(msg string) { entry.appendLog("[import] " + msg) - log.Printf("[monitor/import %s] %s", pd.MBID, msg) + log.Printf("[monitor/import %s] %s", pd.ID, msg) } logf(fmt.Sprintf("Starting import from %s", localDir)) @@ -215,7 +219,7 @@ func importPendingRelease(pd *pendingDownload, localDir string) { logf(fmt.Sprintf("Clean tags warning: %v", err)) } - md, src, err := getAlbumMetadata(localDir, tracks[0], pd.MBID) + md, src, err := getAlbumMetadata(localDir, tracks[0], pd.BeetsMBID) if err != nil { entry.finish(fmt.Errorf("metadata failed: %w", err)) return @@ -244,6 +248,13 @@ func importPendingRelease(pd *pendingDownload, localDir string) { } logf("Cover art embedded") + targetDir := albumTargetDir(libraryDir, md) + if _, err := os.Stat(targetDir); err == nil { + logf(fmt.Sprintf("Album already exists in library, skipping move: %s", targetDir)) + entry.finish(nil) + return + } + var moveErr error for _, track := range tracks { if err := moveToLibrary(libraryDir, md, track); err != nil { diff --git a/music-import b/music-import deleted file mode 100755 index 553b9ee..0000000 Binary files a/music-import and /dev/null differ