Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
58080f7
Implement initial support for Lidarr downloader
avedor Apr 30, 2025
d0c80b1
Use main artist in search
avedor May 4, 2025
2fdc8fa
Use raw api calls
avedor May 5, 2025
e9125b7
DRY downloader checker
avedor Jul 22, 2025
55265af
Update gui to include lidarr
avedor Jun 4, 2026
c5a9384
Don't add an album that exists
avedor Jun 5, 2026
0237393
check trackID when monitoring; check Lidarr history for completed dow…
LumePart Jul 7, 2026
815751e
use queueID when deleting lidarr downloads
LumePart Jul 7, 2026
af71199
implement checking search status
LumePart Jul 7, 2026
628bf30
change lidarr config values
LumePart Jul 7, 2026
826301e
use helper; handle edge-cases
LumePart Jul 7, 2026
88225e7
add pagination and some fixes
LumePart Jul 7, 2026
3e1ed83
replace print with slog
LumePart Jul 7, 2026
ff44358
add rootfolder search by name
LumePart Jul 7, 2026
b1e197a
wire LidarrURL + LidarrAPIKey to wizard handler, persist MBID's throu…
dammitjeff Jul 13, 2026
4d15c33
add ForeignArtistID to Album struct and update getReleaseGroupId logic
dammitjeff Jul 13, 2026
92be129
save root dir when initializing lidarr
LumePart Jul 31, 2026
abec979
Track ID based lidarr monitoring
LumePart Jul 31, 2026
85546d0
create album cache
LumePart Jul 31, 2026
a9a5df5
cache tracks from lidarr response
LumePart Jul 31, 2026
d7862cf
split GetTrack to subfunctions
LumePart Jul 31, 2026
ca75a3d
check cache before searching track
LumePart Jul 31, 2026
04d9ae7
add pointe
LumePart Jul 31, 2026
570552c
move MoveDownload under monitor struct
LumePart Jul 31, 2026
d215c02
remove MoveDownload from downloader
LumePart Jul 31, 2026
5701fd1
remove comparison
LumePart Jul 31, 2026
37643d0
create albumcache on init
LumePart Aug 6, 2026
e0240de
add post search album filtering
LumePart Aug 7, 2026
3386373
fall back to first available rootfolder, when not found by name
LumePart Aug 7, 2026
82134a5
add preliminary album moving, improve album caching and fix a few bugs
LumePart Aug 23, 2026
b474a50
fix linter
LumePart Aug 23, 2026
b58f99e
fix path templating in lidarr
LumePart Aug 25, 2026
ba3aa5c
add max time to monitor track/album
LumePart Aug 25, 2026
64c8c15
add lidarr config options to UI
LumePart Aug 27, 2026
abe52fd
propagate file monitor changes to slskd
LumePart Aug 28, 2026
4fe45eb
helper function to move track
LumePart Aug 28, 2026
efafc1b
improve logging on function
LumePart Aug 28, 2026
f3e524a
linter
LumePart Aug 28, 2026
12fc2fc
Remove branch builds
avedor Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ LIBRARY_NAME=
# Comma-separated (without spaces) keywords to avoid, when filtering slskd results (default: live,remix,instrumental,extended,clean,acapella)
# FILTER_LIST=live,remix,instrumental,extended,clean,acapella

# === Lidarr Configuration ===

# LIDARR_API_KEY=
# LIDARR_RETRY=
# LIDARR_DL_ATTEMPTS=
# LIDARR_DIR=
# MIGRATE_DOWNLOADS=
# LIDARR_TIMEOUT=
# LIDARR_SCHEME=
# LIDARR_URL=

# === Metadata / Formatting ===

# Set to true to merge featured artists into title (recommended), false appends them to artist field (default: true)
Expand Down
83 changes: 61 additions & 22 deletions src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,15 @@ type SubsonicConfig struct {
}

type DownloadConfig struct {
DownloadDir string `env:"DOWNLOAD_DIR" env-default:"/data/"`
PathTemplate string `env:"PATH_TEMPLATE"`
DownloadDir string `env:"DOWNLOAD_DIR" env-default:"/data/"`
PathTemplate string `env:"PATH_TEMPLATE"`
Youtube Youtube
YoutubeMusic YoutubeMusic
Slskd Slskd
Lidarr Lidarr
ExcludeLocal bool
DownloadLimiter int `env:"DOWNLOAD_LIMITER" env-default:"1"` // rate limit download operations
OverwriteMetadata bool `env:"OVERWRITE_METADATA" env-default:"false"` // overwrite metadata when migrating downloads
DownloadLimiter int `env:"DOWNLOAD_LIMITER" env-default:"1"` // rate limit download operations
OverwriteMetadata bool `env:"OVERWRITE_METADATA" env-default:"false"` // overwrite metadata when migrating downloads
KeepPermissions bool `env:"KEEP_PERMISSIONS" env-default:"true"` // keep original file permissions when migrating download
RenameTrack bool `env:"RENAME_TRACK" env-default:"false"` // Rename track in {title}-{artist} format
UseSubDir bool `env:"USE_SUBDIRECTORY" env-default:"true"`
Expand Down Expand Up @@ -134,26 +135,52 @@ type YoutubeMusic struct {
}

type Slskd struct {
APIKey string `env:"SLSKD_API_KEY"`
URL string `env:"SLSKD_URL"`
Retry int `env:"SLSKD_RETRY" env-default:"5"` // Number of times to check search status before skipping the track
DownloadAttempts int `env:"SLSKD_DL_ATTEMPTS" env-default:"3"` // Max number of files to attempt downloading per track
SlskdDir string `env:"SLSKD_DIR" env-default:"/slskd/"`
MigrateDL bool `env:"MIGRATE_DOWNLOADS" env-default:"false"` // Move downloads from SlskdDir to DownloadDir
Timeout int `env:"SLSKD_TIMEOUT" env-default:"20"`
Filters Filters
MonitorConfig SlskdMon
APIKey string `env:"SLSKD_API_KEY"`
URL string `env:"SLSKD_URL"`
Retry int `env:"SLSKD_RETRY" env-default:"5"` // Number of times to check search status before skipping the track
DownloadAttempts int `env:"SLSKD_DL_ATTEMPTS" env-default:"3"` // Max number of files to attempt downloading per track
SlskdDir string `env:"SLSKD_DIR" env-default:"/slskd/"`
MigrateDL bool `env:"SLSKD_MIGRATE_DOWNLOADS" env-default:"false"` // Move downloads from SlskdDir to DownloadDir
MigrateDLOld bool `env:"MIGRATE_DOWNLOADS" env-default:"false"` // Move downloads from SlskdDir to DownloadDir (Old)
RenameTrack bool `env:"RENAME_TRACK" env-default:"false"` // Rename track in {title}-{artist} format (will be deprecated)
PathTemplate string `env:"PATH_TEMPLATE"`
OverwriteMetadata bool `env:"SLSKD_OVERWRITE_METADATA" env-default:"false"` // overwrite metadata when migrating downloads
KeepPermissions bool `env:"SLSKD_KEEP_PERMISSIONS" env-default:"true"` // keep original file permissions when migrating download
Timeout int `env:"SLSKD_TIMEOUT" env-default:"20"`
Filters Filters
MonitorConfig SlskdMon
}

type SlskdMon struct {
Interval int `env:"SLSKD_MONITOR_INTERVAL" env-default:"1"`
Duration int `env:"SLSKD_MONITOR_DURATION" env-default:"15"`
Interval int `env:"SLSKD_MONITOR_INTERVAL" env-default:"1"` // in minutes
Duration int `env:"SLSKD_MONITOR_DURATION" env-default:"15"` // in minutes
MaxDuration int `env:"SLSKD_MONITOR_MAX_DURATION" env-default:"120"` // in minutes
}

type Lidarr struct {
APIKey string `env:"LIDARR_API_KEY"`
URL string `env:"LIDARR_URL"`
Retry int `env:"LIDARR_RETRY" env-default:"8"` // Number of times to check album/track search status before skipping the track
LidarrDir string `env:"LIDARR_DIR" env-default:"/lidarr/"`
MigrateDL bool `env:"LIDARR_MIGRATE_DOWNLOADS" env-default:"false"` // Move downloads from LidarrDir to DownloadDir
Timeout int `env:"LIDARR_TIMEOUT" env-default:"20"`
RootFolder string `env:"LIDARR_ROOT_FOLDER" env-default:"Music"` // Root folder name to use in Lidarr
PathTemplate string `env:"PATH_TEMPLATE"`
KeepPermissions bool `env:"LIDARR_KEEP_PERMISSIONS" env-default:"true"` // keep original file permissions when migrating download
Filters Filters
MonitorConfig LidarrMon
}

type LidarrMon struct {
Interval int `env:"LIDARR_MONITOR_INTERVAL" env-default:"1"` // in minutes
Duration int `env:"LIDARR_MONITOR_DURATION" env-default:"20"` // in minutes
MaxDuration int `env:"LIDARR_MONITOR_MAX_DURATION" env-default:"120"` // in minutes
}

type DiscoveryConfig struct {
Discovery string `env:"DISCOVERY_SERVICE" env-default:"listenbrainz"`
Discovery string `env:"DISCOVERY_SERVICE" env-default:"listenbrainz"`
ArtistBlacklist []string `env:"ARTIST_BLACKLIST"`
Listenbrainz Listenbrainz
Listenbrainz Listenbrainz
}
type Listenbrainz struct {
Discovery string `env:"LISTENBRAINZ_DISCOVERY" env-default:"playlist"`
Expand Down Expand Up @@ -223,6 +250,7 @@ func (cfg *Config) CommonFixes() {
cfg.DownloadCfg.Youtube.CoversDir = filepath.Join(filepath.Dir(cfg.ServerCfg.WebDataDir), "cache", "covers")
cfg.ClientCfg.URL = fixBaseURL(cfg.ClientCfg.URL)
cfg.DownloadCfg.Slskd.URL = fixBaseURL(cfg.DownloadCfg.Slskd.URL)
cfg.DownloadCfg.Lidarr.URL = fixBaseURL(cfg.DownloadCfg.Lidarr.URL)
cfg.NormalizeDir()
}

Expand All @@ -231,6 +259,7 @@ func (cfg *Config) NormalizeDir() {
cfg.ClientCfg.PlaylistDir = fixDir(cfg.ClientCfg.PlaylistDir)
}
cfg.DownloadCfg.Slskd.SlskdDir = fixDir(cfg.DownloadCfg.Slskd.SlskdDir)
cfg.DownloadCfg.Lidarr.LidarrDir = fixDir(cfg.DownloadCfg.Lidarr.LidarrDir)
cfg.DownloadCfg.DownloadDir = fixDir(cfg.DownloadCfg.DownloadDir)
}

Expand All @@ -252,11 +281,9 @@ func fixBaseURL(rawURL string) string {
return strings.TrimRight(u, "/")
}

func (cfg *Config) HandleDeprecation() { //
if cfg.Debug {
slog.Warn("'DEBUG' variable is deprecated, please use LOG_LEVEL=DEBUG instead")
cfg.LogLevel = "DEBUG"
}

// deprecation logs and remappings
func (cfg *Config) HandleDeprecation() {
if cfg.Flags.PersistSet {
slog.Warn("--persist has been deprecated since v1.1.3, please use --replace-playlist")
}
Expand All @@ -267,6 +294,18 @@ func (cfg *Config) HandleDeprecation() { //
if cfg.Flags.CleanDownloads && !cfg.DownloadCfg.UseSubDir {
slog.Warn("Deleting tracks requires 'USE_SUBDIRECTORY' to be true")
}

if cfg.DownloadCfg.OverwriteMetadata {
cfg.DownloadCfg.Slskd.OverwriteMetadata = cfg.DownloadCfg.OverwriteMetadata
}

if !cfg.DownloadCfg.KeepPermissions {
cfg.DownloadCfg.Slskd.KeepPermissions = cfg.DownloadCfg.KeepPermissions
}

if cfg.DownloadCfg.Slskd.MigrateDLOld {
cfg.DownloadCfg.Slskd.MigrateDL = cfg.DownloadCfg.Slskd.MigrateDLOld
}
}

// Generate playlist name and description
Expand Down
23 changes: 15 additions & 8 deletions src/discovery/listenbrainz.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type Metadata struct {
Artist struct {
ArtistCreditID int `json:"artist_credit_id"`
Artists []struct {
ArtistMbid string `json:"artist_mbid"`
MusicBrainzArtistID string `json:"artist_mbid"`
BeginYear int `json:"begin_year"`
EndYear int `json:"end_year,omitempty"`
JoinPhrase string `json:"join_phrase"`
Expand Down Expand Up @@ -121,7 +121,7 @@ type Exploration struct {
AdditionalMetadata struct {
Artists []struct {
ArtistCreditName string `json:"artist_credit_name"`
ArtistMbid string `json:"artist_mbid"`
MusicBrainzArtistID string `json:"artist_mbid"`
JoinPhrase string `json:"join_phrase"`
} `json:"artists"`
CaaID int64 `json:"caa_id"`
Expand Down Expand Up @@ -383,7 +383,7 @@ func (c *ListenBrainz) getTracks(mbids []string, singleArtist bool) ([]*models.T
MusicBrainzTrackID: mbTrackID,
MusicBrainzAlbumID: rel.CaaReleaseMbid,
MusicBrainzReleaseGroupID: rel.ReleaseGroupMbid,
MusicBrainzArtistID: recArtists[0].ArtistMbid,
MusicBrainzArtistID: recArtists[0].MusicBrainzArtistID,
})
}

Expand Down Expand Up @@ -432,7 +432,7 @@ func (c *ListenBrainz) enrichTracks(tracks []*models.Track, singleArtist bool) (
mainArtist := recording.Artist.Name
mainArtistID := ""
if len(recording.Artist.Artists) > 0 {
mainArtistID = recording.Artist.Artists[0].ArtistMbid
mainArtistID = recording.Artist.Artists[0].MusicBrainzArtistID
}

recArtists := recording.Artist.Artists
Expand Down Expand Up @@ -581,7 +581,7 @@ func (c *ListenBrainz) enrichTracks(tracks []*models.Track, singleArtist bool) (
if len(recArtists) == 0 {
return ""
}
return recArtists[0].ArtistMbid
return recArtists[0].MusicBrainzArtistID
}(),
}
}
Expand Down Expand Up @@ -658,6 +658,13 @@ func FetchTopRecordings(httpClient *util.HttpClient, user string) ([]*models.Tra
return lb.getTopRecordings(user)
}

// EnrichTracks fetches MusicBrainz metadata for tracks that have recording MBIDs,
// populating fields like MusicBrainzReleaseGroupID and MusicBrainzArtistID.
func EnrichTracks(httpClient *util.HttpClient, lbCfg cfg.Listenbrainz, tracks []*models.Track) ([]*models.Track, error) {
lb := &ListenBrainz{HttpClient: httpClient, cfg: lbCfg}
return lb.enrichTracks(tracks, lbCfg.SingleArtist)
}

// FetchMostRecentPlaylistByType finds and fetches the most recent LB-generated playlist of the given type for the user.
func FetchMostRecentPlaylistByType(httpClient *util.HttpClient, user, playlistType string) ([]*models.Track, error) {
lb := &ListenBrainz{HttpClient: httpClient, cfg: cfg.Listenbrainz{ImportPlaylist: playlistType}}
Expand Down Expand Up @@ -718,9 +725,9 @@ func (c *ListenBrainz) parsePlaylist(identifier string, singleArtist bool) (stri
recordingMBID = parts[len(parts)-1]
}

artistMBID := ""
MusicBrainzArtistID := ""
if len(trackMeta.Artists) > 0 {
artistMBID = trackMeta.Artists[0].ArtistMbid
MusicBrainzArtistID = trackMeta.Artists[0].MusicBrainzArtistID
}

tracks = append(tracks, &models.Track{
Expand All @@ -732,7 +739,7 @@ func (c *ListenBrainz) parsePlaylist(identifier string, singleArtist bool) (stri
Duration: track.Duration,
CoverURL: coverURL,
MusicBrainzTrackID: recordingMBID,
MusicBrainzArtistID: artistMBID,
MusicBrainzArtistID: MusicBrainzArtistID,
MusicBrainzAlbumID: trackMeta.CaaReleaseMbid,
})
}
Expand Down
Loading
Loading