i don't remember

This commit is contained in:
Gabe Farrell 2026-04-09 00:39:59 -04:00
parent c7d6a088ed
commit 4324de1271
6 changed files with 98 additions and 48 deletions

View file

@ -80,6 +80,23 @@ func searchMBArtists(query string) ([]mbArtist, error) {
return result.Artists, err 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, // 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. // paginating through the MusicBrainz browse API with the required 1 req/s rate limit.
func getMBArtistReleaseGroups(artistMBID string) ([]mbReleaseGroup, error) { func getMBArtistReleaseGroups(artistMBID string) ([]mbReleaseGroup, error) {
@ -136,15 +153,24 @@ func fetchArtist(artistMBID, artistName string, logf func(string)) error {
failed := 0 failed := 0
for i, rg := range groups { for i, rg := range groups {
logf(fmt.Sprintf("[%d/%d] %s", i+1, len(groups), rg.Title)) 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 { if err != nil {
log.Printf("[discover] fetch failed for %q by %s: %v", rg.Title, artistName, err) log.Printf("[discover] fetch failed for %q by %s: %v", rg.Title, artistName, err)
logf(fmt.Sprintf(" ↳ failed: %v", err)) logf(fmt.Sprintf(" ↳ failed: %v", err))
failed++ failed++
continue continue
} }
registerDownload(rg.ID, artistName, rg.Title, folder, nil) // Key the pending download by release group ID for dedup; beets uses releaseMBID.
logf(fmt.Sprintf(" ↳ registered for import (mbid: %s)", rg.ID)) registerDownload(rg.ID, releaseMBID, artistName, rg.Title, folder, nil)
logf(fmt.Sprintf(" ↳ registered for import (release mbid: %s)", releaseMBID))
} }
if failed > 0 { if failed > 0 {
@ -290,7 +316,7 @@ func handleDiscoverFetch(w http.ResponseWriter, r *http.Request) {
return return
} }
log.Printf("[discover] fetch complete for %q by %s, registering for import", body.Album, body.Artist) 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 // entry.finish is called by the monitor when import completes
}() }()

View file

@ -9,8 +9,9 @@ import (
"strings" "strings"
) )
// moveToLibrary moves a file to {libDir}/{artist}/[{date}] {album} [{quality}]/filename. // albumTargetDir returns the destination directory for an album without
func moveToLibrary(libDir string, md *MusicMetadata, srcPath string) error { // creating it. Use this to check for an existing import before moving files.
func albumTargetDir(libDir string, md *MusicMetadata) string {
date := md.Date date := md.Date
if date == "" { if date == "" {
date = md.Year date = md.Year
@ -19,7 +20,12 @@ func moveToLibrary(libDir string, md *MusicMetadata, srcPath string) error {
if md.Quality != "" { if md.Quality != "" {
albumDir += fmt.Sprintf(" [%s]", 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 { if err := os.MkdirAll(targetDir, 0755); err != nil {
return err return err
} }

View file

@ -233,33 +233,39 @@ func RunImporter() {
continue continue
} }
fmt.Println("→ Moving tracks into library for album:", albumPath) targetDir := albumTargetDir(libraryDir, md)
for _, track := range tracks { if _, err := os.Stat(targetDir); err == nil {
if err := moveToLibrary(libraryDir, md, track); err != nil { fmt.Println("→ Album already exists in library, skipping move:", targetDir)
fmt.Println("Failed to move track:", track, err) result.Move.Skipped = true
result.Move.Err = err // retains last error; all attempts are still made } 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) fmt.Println("→ Moving lyrics into library for album:", albumPath)
for _, file := range lyrics { for _, file := range lyrics {
if err := moveToLibrary(libraryDir, md, file); err != nil { if err := moveToLibrary(libraryDir, md, file); err != nil {
fmt.Println("Failed to move lyrics:", file, err) fmt.Println("Failed to move lyrics:", file, err)
result.Move.Err = err result.Move.Err = err
}
} }
}
fmt.Println("→ Moving album cover into library for album:", albumPath) fmt.Println("→ Moving album cover into library for album:", albumPath)
if coverImg, err := FindCoverImage(albumPath); err == nil { if coverImg, err := FindCoverImage(albumPath); err == nil {
if err := moveToLibrary(libraryDir, md, coverImg); err != nil { if err := moveToLibrary(libraryDir, md, coverImg); err != nil {
fmt.Println("Failed to cover image:", coverImg, err) fmt.Println("Failed to cover image:", coverImg, err)
result.Move.Err = err result.Move.Err = err
}
} }
}
os.Remove(albumPath) os.Remove(albumPath)
}
} }
fmt.Println("\n=== Import Complete ===") fmt.Println("\n=== Import Complete ===")

View file

@ -220,9 +220,10 @@ func tagWithBeets(path, mbid string) error {
defer os.Remove(logPath) defer os.Remove(logPath)
args := []string{"import", "-Cq", "-l", logPath} args := []string{"import", "-Cq", "-l", logPath}
if mbid != "" { // passing mbid to beet removed temporarily
args = append(args, "--search-id", mbid) // if mbid != "" {
} // args = append(args, "--search-id", mbid)
// }
args = append(args, path) args = append(args, path)
if err := runCmd("beet", args...); err != nil { if err := runCmd("beet", args...); err != nil {
return err return err

View file

@ -13,7 +13,8 @@ import (
// pendingDownload tracks a queued slskd download that should be auto-imported // pendingDownload tracks a queued slskd download that should be auto-imported
// once all files have transferred successfully. // once all files have transferred successfully.
type pendingDownload struct { 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 Artist string
Album string Album string
Username string // slskd peer username Username string // slskd peer username
@ -28,32 +29,35 @@ var (
) )
// registerDownload records a queued slskd download for monitoring and eventual // registerDownload records a queued slskd download for monitoring and eventual
// auto-import. If entry is nil a new fetchEntry is created, keyed by mbid, // auto-import. id is used as the dedup key; beetsMBID is the release MBID
// so the frontend can discover it via /discover/fetch/list. // forwarded to beets --search-id (may be empty or differ from id).
func registerDownload(mbid, artist, album string, folder *albumFolder, entry *fetchEntry) { // 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{ pd := &pendingDownload{
MBID: mbid, ID: id,
Artist: artist, BeetsMBID: beetsMBID,
Album: album, Artist: artist,
Username: folder.Username, Album: album,
Dir: folder.Dir, Username: folder.Username,
Files: folder.Files, Dir: folder.Dir,
Entry: entry, Files: folder.Files,
Entry: entry,
} }
if entry == nil { 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", e.appendLog(fmt.Sprintf("Queued %d files from %s — waiting for download",
len(folder.Files), folder.Username)) len(folder.Files), folder.Username))
pd.Entry = e pd.Entry = e
} }
pendingMu.Lock() pendingMu.Lock()
pendingDownloads[mbid] = pd pendingDownloads[id] = pd
pendingMu.Unlock() pendingMu.Unlock()
log.Printf("[monitor] registered: %q by %s (mbid: %s, peer: %s, %d files)", log.Printf("[monitor] registered: %q by %s (id: %s, beets mbid: %s, peer: %s, %d files)",
album, artist, mbid, folder.Username, len(folder.Files)) album, artist, id, beetsMBID, folder.Username, len(folder.Files))
} }
// startMonitor launches a background goroutine that periodically checks whether // 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. // Remove from pending before starting import to avoid double-import.
pendingMu.Lock() pendingMu.Lock()
delete(pendingDownloads, pd.MBID) delete(pendingDownloads, pd.ID)
pendingMu.Unlock() pendingMu.Unlock()
go importPendingRelease(pd, localDir) go importPendingRelease(pd, localDir)
@ -189,7 +193,7 @@ func importPendingRelease(pd *pendingDownload, localDir string) {
entry := pd.Entry entry := pd.Entry
logf := func(msg string) { logf := func(msg string) {
entry.appendLog("[import] " + msg) 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)) 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)) 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 { if err != nil {
entry.finish(fmt.Errorf("metadata failed: %w", err)) entry.finish(fmt.Errorf("metadata failed: %w", err))
return return
@ -244,6 +248,13 @@ func importPendingRelease(pd *pendingDownload, localDir string) {
} }
logf("Cover art embedded") 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 var moveErr error
for _, track := range tracks { for _, track := range tracks {
if err := moveToLibrary(libraryDir, md, track); err != nil { if err := moveToLibrary(libraryDir, md, track); err != nil {

Binary file not shown.