mirror of
https://github.com/gabehf/music-importer.git
synced 2026-04-22 03:21:52 -07:00
i don't remember
This commit is contained in:
parent
c7d6a088ed
commit
4324de1271
6 changed files with 98 additions and 48 deletions
34
discover.go
34
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
|
||||
}()
|
||||
|
||||
|
|
|
|||
12
files.go
12
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
|
||||
}
|
||||
|
|
|
|||
46
importer.go
46
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 ===")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
47
monitor.go
47
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 {
|
||||
|
|
|
|||
BIN
music-import
BIN
music-import
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue