Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions src/client/emby.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,11 @@ func (c *Emby) SearchSongs(tracks []*models.Track) error {

musicBrainzMatch := track.MusicBrainzTrackID != "" && item.ProviderIds.MusicBrainzTrack == track.MusicBrainzTrackID
titleMatch := normalizedItemTitle == normalizedCleanTitle
albumMatch := util.ContainsFold(item.Album, track.Album)
artistMatch := strings.EqualFold(item.AlbumArtist, track.MainArtist) || (len(item.Artists) > 0 && strings.EqualFold(item.Artists[0], track.MainArtist))
pathMatch := util.ContainsFold(item.Path,track.File)
pathMatch := util.ContainsFold(item.Path, track.File)

if musicBrainzMatch || (titleMatch && artistMatch) {
if musicBrainzMatch || (titleMatch && (albumMatch || artistMatch)) {
track.ID = item.ID
track.Present = true
break
Expand Down
5 changes: 3 additions & 2 deletions src/client/jellyfin.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,11 @@ func (c *Jellyfin) SearchSongs(tracks []*models.Track) error {

musicBrainzMatch := track.MusicBrainzTrackID != "" && item.ProviderIds.MusicBrainzTrack == track.MusicBrainzTrackID
titleMatch := normalizedItemTitle == normalizedCleanTitle
albumMatch := util.ContainsFold(item.Album, track.Album)
artistMatch := strings.EqualFold(item.AlbumArtist, track.MainArtist) || (len(item.Artists) > 0 && strings.EqualFold(item.Artists[0], track.MainArtist))
pathMatch := util.ContainsFold(item.Path,track.File)
pathMatch := util.ContainsFold(item.Path, track.File)

if musicBrainzMatch || (titleMatch && artistMatch) {
if musicBrainzMatch || (titleMatch && (albumMatch || artistMatch)) {
track.ID = item.ID
track.Present = true
break
Expand Down
74 changes: 43 additions & 31 deletions src/discovery/listenbrainz.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ type MBRecording struct {
} `json:"releases"`
}

type TitleArgs struct {
CleanTitle string
FeatArtists []string
}

type ListenBrainz struct {
HttpClient *util.HttpClient
Headers map[string]string
Expand Down Expand Up @@ -356,17 +361,13 @@ func (c *ListenBrainz) getTracks(mbids []string, singleArtist bool) ([]*models.T
if len(recArtists) > 1 {
mainArtist = recArtists[0].Name
if singleArtist {
var b strings.Builder
b.WriteString(title)
b.WriteString(" feat. ")
b.WriteString(recArtists[1].Name)

for _, a := range recArtists[2:] {
b.WriteString(", ")
b.WriteString(a.Name)
}
var titleArgs TitleArgs
titleArgs.CleanTitle = rec.Name
for _, artist := range recArtists[1:] {

title = b.String()
titleArgs.FeatArtists = append(titleArgs.FeatArtists, artist.Name)
}
title = c.buildTrackTitle(titleArgs)
artist = mainArtist
}
}
Expand Down Expand Up @@ -455,17 +456,12 @@ func (c *ListenBrainz) enrichTracks(tracks []*models.Track, singleArtist bool) (
if len(recArtists) > 1 {
mainArtist = recArtists[0].Name
if singleArtist {
var b strings.Builder
b.WriteString(title)
b.WriteString(" feat. ")
b.WriteString(recArtists[1].Name)

for _, a := range recArtists[2:] {
b.WriteString(", ")
b.WriteString(a.Name)
var titleArgs TitleArgs
titleArgs.CleanTitle = rec.Name
for _, artist := range recArtists[1:] {
titleArgs.FeatArtists = append(titleArgs.FeatArtists, artist.Name)
}

title = b.String()
title = c.buildTrackTitle(titleArgs)
artist = mainArtist
} else {
for _, recArtist := range recArtists {
Expand Down Expand Up @@ -637,7 +633,6 @@ func (c *ListenBrainz) getImportPlaylist(user string) (string, error) {
bestID = parts[len(parts)-1]
}
}

if playlists.Count+playlists.Offset >= playlists.PlaylistCount || playlists.Count == 0 {
break
}
Expand Down Expand Up @@ -707,17 +702,13 @@ func (c *ListenBrainz) parsePlaylist(identifier string, singleArtist bool) (stri
if len(trackMeta.Artists) > 1 {
mainArtist = trackMeta.Artists[0].ArtistCreditName
if singleArtist {
var b strings.Builder
b.WriteString(title)
b.WriteString(" feat. ")
b.WriteString(trackArtists[1].ArtistCreditName)

for _, a := range trackArtists[2:] {
b.WriteString(", ")
b.WriteString(a.ArtistCreditName)
var titleargs TitleArgs
titleargs.CleanTitle = track.Title
for _, artist := range trackArtists[1:] {
titleargs.FeatArtists = append(titleargs.FeatArtists, artist.ArtistCreditName)
}
title = b.String()
artist = trackArtists[0].ArtistCreditName
title = c.buildTrackTitle(titleargs)
artist = trackArtists[0].ArtistCreditName
}
}

Expand Down Expand Up @@ -750,6 +741,27 @@ func (c *ListenBrainz) parsePlaylist(identifier string, singleArtist bool) (stri

}

func (c *ListenBrainz) buildTrackTitle(args TitleArgs) string {
var b strings.Builder

if len(args.FeatArtists) == 0 {
return args.CleanTitle
}

b.WriteString(args.CleanTitle)
b.WriteString(" (feat. ")
if len(args.FeatArtists) == 1 {
b.WriteString(args.FeatArtists[0])
} else {
b.WriteString(strings.Join(args.FeatArtists[:len(args.FeatArtists)-1], ", "))
b.WriteString(" & ")
b.WriteString(args.FeatArtists[len(args.FeatArtists)-1])
}
b.WriteString(")")

return b.String()
}

// Handle ListenBrainz API requests
func (c *ListenBrainz) lbRequest(path string) ([]byte, error) {

Expand Down
55 changes: 32 additions & 23 deletions src/downloader/slskd.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,38 +275,47 @@ func (c Slskd) CollectFiles(track models.Track, searchResults SearchResults) ([]

files := slices.Collect(func(yield func(File) bool) {
for _, result := range searchResults {
if result.FileCount > 0 && result.HasFreeUploadSlot {
for _, file := range result.Files {
file.Extension = strings.TrimPrefix(strings.ToLower(file.Extension), ".")
if file.Extension == "" {
extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(string(file.Name))), ".")
file.Extension = util.AlnumOnly(extension) // sanitize extension incase of bad chars
if result.FileCount == 0 || !result.HasFreeUploadSlot {
continue
}
for _, file := range result.Files {
nameExt := util.AlnumOnly(strings.TrimPrefix(strings.ToLower(filepath.Ext(string(file.Name))), "."))
reportedExt := strings.TrimPrefix(strings.ToLower(file.Extension), ".")
if nameExt != "" {
if reportedExt != "" && reportedExt != nameExt {
slog.Debug("extension mismatch between filename and metadata, using filename ext",
"track", track.CleanTitle, "filename", file.Name, "reportedExtension", reportedExt, "actualExtension", nameExt)
}

if !slices.Contains(c.Cfg.Filters.Extensions, file.Extension) && ContainsKeyword(track, file.Name, c.Cfg.Filters.FilterList) {
continue
file.Extension = nameExt
} else {
file.Extension = reportedExt
}

if track.Duration > 0 && util.Abs(track.Duration/1000-file.Length) > 10 { // skip song if track lengths have a 10s+ difference
continue
}
if !slices.Contains(c.Cfg.Filters.Extensions, file.Extension) || ContainsKeyword(track, file.Name, c.Cfg.Filters.FilterList) {
continue
}

sanitizedFilename := util.AlnumOnly(string(file.Name))
if (containsLower(sanitizedFilename, sanitizedArtist) || containsLower(sanitizedFilename, sanitizedAlbum)) && containsLower(sanitizedFilename, sanitizedTitle) {
file.Username = result.Username
if !yield(file) {
if track.Duration > 0 && util.Abs(track.Duration/1000-file.Length) > 10 { // skip song if track lengths have a 10s+ difference
continue
}

sanitizedFilename := util.AlnumOnly(string(file.Name))
matchesArtist := containsLower(sanitizedFilename, sanitizedArtist)
matchesAlbum := containsLower(sanitizedFilename, sanitizedAlbum)
matchesTitle := containsLower(sanitizedFilename, sanitizedTitle)
if (matchesArtist || matchesAlbum) && matchesTitle {
file.Username = result.Username
if !yield(file) {
return
}
}
}
}
}
})
if len(files) != 0 {
return files, nil
} else {
if len(files) == 0 {
return nil, fmt.Errorf("no tracks passed collection for %s - %s", track.MainArtist, track.CleanTitle)
}
}
return files, nil
}

func (c Slskd) filterFiles(files []File) ([]File, error) {
Expand All @@ -318,11 +327,11 @@ func (c Slskd) filterFiles(files []File) ([]File, error) {
continue
}

if file.BitRate > 0 && file.BitRate <= c.Cfg.Filters.MinBitRate {
if file.BitRate > 0 && file.BitRate < c.Cfg.Filters.MinBitRate {
continue
}

if file.BitDepth > 0 && file.BitDepth <= c.Cfg.Filters.MinBitDepth {
if file.BitDepth > 0 && file.BitDepth < c.Cfg.Filters.MinBitDepth {
continue
}

Expand Down
56 changes: 0 additions & 56 deletions src/web/backend/playlist/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,62 +318,6 @@ func (p *Playlist) HandleDeleteCustomPlaylist(w http.ResponseWriter, r *http.Req
w.WriteHeader(http.StatusNoContent)
}

// handleSaveSchedule updates a single playlist's schedule in the .env file.
func (p *Playlist) HandleSaveSchedule(w http.ResponseWriter, r *http.Request) {
var body struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Day int `json:"day"` // 0=Sun…6=Sat, -1=every day
Hour int `json:"hour"`
Minute int `json:"minute"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}

var envPrefix string
var defaultFlags string

if def, ok := defs.PlaylistDefs[body.Name]; ok {
envPrefix = def.EnvPrefix
defaultFlags = def.DefaultFlags
} else if defs.CustomIDRe.MatchString(body.Name) {
envPrefix = util.CustomEnvPrefix(body.Name)
defaultFlags = "--playlist " + body.Name
} else {
http.Error(w, "unknown playlist name", http.StatusBadRequest)
return
}

updates := map[string]string{}
if !body.Enabled {
// Toggle off — truly disable, regardless of day value carried over from state
updates[envPrefix+"_SCHEDULE"] = ""
updates[envPrefix+"_FLAGS"] = ""
} else if body.Day == -2 {
// "Never" — keep playlist active for manual runs but remove auto-schedule
updates[envPrefix+"_SCHEDULE"] = ""
updates[envPrefix+"_FLAGS"] = defaultFlags
} else {
dom := "*"
dow := "*"
if body.Day == 100 {
dom = "1"
} else if body.Day >= 0 {
dow = fmt.Sprintf("%d", body.Day)
}
updates[envPrefix+"_SCHEDULE"] = fmt.Sprintf("%d %d %s * %s", body.Minute, body.Hour, dom, dow)
updates[envPrefix+"_FLAGS"] = defaultFlags
}

if err := p.settings.UpdateEnvKeys(updates, web.SampleEnv); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

// handleGetPlaylist serves the tracklist cache written by explo during its last run.
// Returns an empty track list if no cache exists yet.
func (p *Playlist) HandleGetPlaylist(w http.ResponseWriter, r *http.Request) {
Expand Down
4 changes: 3 additions & 1 deletion src/web/backend/playlist/playlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ var validPlaylistTypes = func() map[string]bool {
}()

func NewPlaylist(cfg app.Config, settings *settings.Settings) *Playlist {
return &Playlist{cfg: cfg}
return &Playlist{
cfg: cfg,
settings: settings}
}

// isValidPlaylistID accepts built-in playlist types and custom-* IDs (blocks path traversal).
Expand Down
14 changes: 8 additions & 6 deletions src/web/backend/settings/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func (s *Settings) HandleResetConfig(w http.ResponseWriter, r *http.Request) {
// handleSaveSchedule updates a single playlist's schedule in the .env file.
func (s *Settings) HandleSaveSchedule(w http.ResponseWriter, r *http.Request) {
var body struct {
ID string `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Day int `json:"day"` // 0=Sun…6=Sat, -1=every day
Expand All @@ -108,12 +109,12 @@ func (s *Settings) HandleSaveSchedule(w http.ResponseWriter, r *http.Request) {
var envPrefix string
var defaultFlags string

if def, ok := defs.PlaylistDefs[body.Name]; ok {
if def, ok := defs.PlaylistDefs[body.ID]; ok {
envPrefix = def.EnvPrefix
defaultFlags = def.DefaultFlags
} else if defs.CustomIDRe.MatchString(body.Name) {
} else if defs.CustomIDRe.MatchString(body.ID) {
envPrefix = util.CustomEnvPrefix(body.Name)
defaultFlags = "--playlist " + body.Name
defaultFlags = "--playlist " + body.ID
} else {
http.Error(w, "unknown playlist name", http.StatusBadRequest)
return
Expand Down Expand Up @@ -211,6 +212,7 @@ func (s *Settings) HandleSaveEnrichMetadata(w http.ResponseWriter, r *http.Reque
// HandleSaveReplacePlaylist injects or removes --replace-playlist=false from a playlist's FLAGS env var.
func (s *Settings) HandleSaveReplacePlaylist(w http.ResponseWriter, r *http.Request) {
var body struct {
ID string `json:"id"`
Name string `json:"name"`
Replace bool `json:"replace"`
}
Expand All @@ -221,12 +223,12 @@ func (s *Settings) HandleSaveReplacePlaylist(w http.ResponseWriter, r *http.Requ

var envPrefix string
var defaultFlags string
if def, ok := defs.PlaylistDefs[body.Name]; ok {
if def, ok := defs.PlaylistDefs[body.ID]; ok {
envPrefix = def.EnvPrefix
defaultFlags = def.DefaultFlags
} else if defs.CustomIDRe.MatchString(body.Name) {
} else if defs.CustomIDRe.MatchString(body.ID) {
envPrefix = util.CustomEnvPrefix(body.Name)
defaultFlags = "--playlist " + body.Name
defaultFlags = "--playlist " + body.ID
} else {
http.Error(w, "unknown playlist name", http.StatusBadRequest)
return
Expand Down
10 changes: 5 additions & 5 deletions src/web/frontend/src/components/Settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,10 @@ function HomeSection() {
}
for (const cp of customList) {
s[cp.id] = cp.schedule
? { enabled: true, editing: false, ...cronToFields(cp.schedule) }
? { name: cp.name, enabled: true, editing: false, ...cronToFields(cp.schedule) }
: cp.flags
? { enabled: true, editing: false, day: -2, hour: 4, minute: 0 }
: { enabled: false, day: -1, hour: 4, minute: 0, editing: false }
? { name: cp.name, enabled: true, editing: false, day: -2, hour: 4, minute: 0 }
: { name: cp.name, enabled: false, day: -1, hour: 4, minute: 0, editing: false }
}
setSchedules(s)
})
Expand Down Expand Up @@ -300,15 +300,15 @@ function HomeSection() {
scheduleSaveStatus: scheduleSaveStatus[id] || '',
onToggle: v => {
setSchedules(prev => ({ ...prev, [id]: { ...prev[id], enabled: v } }))
saveSchedule(id, v, s.day, s.hour, s.minute)
saveSchedule(id, s.name, v, s.day, s.hour, s.minute)
.then(() => flashStatus(id, 'Saved.'))
.catch(() => flashStatus(id, 'Error saving.'))
},
onToggleEdit: () => setSchedules(prev => ({
...prev, [id]: { ...prev[id], editing: !prev[id].editing }
})),
onSave: () => {
saveSchedule(id, s.enabled, s.day, s.hour, s.minute)
saveSchedule(id, s.name, s.enabled, s.day, s.hour, s.minute)
.then(() => flashStatus(id, 'Saved.'))
.catch(() => flashStatus(id, 'Error saving.'))
setSchedules(prev => ({ ...prev, [id]: { ...prev[id], editing: false } }))
Expand Down
2 changes: 1 addition & 1 deletion src/web/frontend/src/components/ui/PlaylistCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ export function PlaylistCard({
boxShadow: '0 8px 24px #00000088',
}}
>
{sourceUrl && (
{sourceUrl && window.isSecureContext && (
<button
onClick={e => {
e.stopPropagation()
Expand Down
Loading
Loading