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
|
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
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
12
files.go
12
files.go
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
46
importer.go
46
importer.go
|
|
@ -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 ===")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
47
monitor.go
47
monitor.go
|
|
@ -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 {
|
||||||
|
|
|
||||||
BIN
music-import
BIN
music-import
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue