Skip to content
Open
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
7 changes: 7 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,8 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
}
}

req.ArtistName, req.TrackName = backend.MoveFeaturedArtistsToTitle(req.ArtistName, req.TrackName, metadataSeparator)

if req.TrackName != "" && req.ArtistName != "" {
expectedFilename := backend.BuildExpectedFilename(req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.SpotifyDiscNumber, req.UseAlbumTrackNumber, req.ISRC)
expectedPath := filepath.Join(req.OutputDir, expectedFilename)
Expand Down Expand Up @@ -1889,6 +1891,7 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
defaultFilenameFormat := "title-artist"
redownloadWithSuffix := backend.GetRedownloadWithSuffixSetting()
existingFileCheckMode := backend.GetExistingFileCheckModeSetting()
existenceSeparator := backend.GetSeparator()
scanRoot := outputDir
if rootDir != "" {
scanRoot = rootDir
Expand Down Expand Up @@ -1923,6 +1926,10 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che
return
}

t.ArtistName, t.TrackName = backend.MoveFeaturedArtistsToTitle(t.ArtistName, t.TrackName, existenceSeparator)
res.TrackName = t.TrackName
res.ArtistName = t.ArtistName

filenameFormat := t.FilenameFormat
if filenameFormat == "" {
filenameFormat = defaultFilenameFormat
Expand Down
33 changes: 33 additions & 0 deletions backend/artist_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,39 @@ func SplitArtistCredits(artistStr, separator string) []string {
return result
}

// MoveFeaturedArtistsToTitle, when the "moveFeaturedArtistsToTitle" setting is
// enabled and the track has multiple credited artists, returns the lead artist
// alone alongside a title that has " (feat. ...)" appended for the remaining
// artists. When the setting is disabled, only one artist is credited, or the
// title already advertises a feature, the values are returned unchanged
// (except that, when the setting is on, the artist is still narrowed to the
// lead). The setting is read inside this helper so call sites stay simple.
func MoveFeaturedArtistsToTitle(artist, title, separator string) (string, string) {
if !GetMoveFeaturedArtistsToTitleSetting() {
return artist, title
}

credits := SplitArtistCredits(artist, separator)
if len(credits) <= 1 {
return artist, title
}

main := credits[0]
feats := credits[1:]

lowerTitle := strings.ToLower(title)
featMarkers := []string{"(feat.", "(ft.", "(featuring", "[feat.", "[ft.", "[featuring"}
for _, marker := range featMarkers {
if strings.Contains(lowerTitle, marker) {
return main, title
}
}

suffix := " (feat. " + strings.Join(feats, " & ") + ")"
newTitle := strings.TrimRight(title, " \t") + suffix
return main, newTitle
}

func SplitMetadataValues(value, separator string) []string {
rawParts := splitArtistSegment(value, separator)
if len(rawParts) == 0 {
Expand Down
10 changes: 10 additions & 0 deletions backend/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ func GetRedownloadWithSuffixSetting() bool {
return enabled
}

func GetMoveFeaturedArtistsToTitleSetting() bool {
settings, err := LoadConfigSettings()
if err != nil || settings == nil {
return false
}

enabled, _ := settings["moveFeaturedArtistsToTitle"].(bool)
return enabled
}

func GetCustomTidalAPISetting() string {
settings, err := LoadConfigSettings()
if err != nil || settings == nil {
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/components/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,16 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
</Label>
</div>

<div className="flex items-center gap-3">
<Switch id="move-featured-artists-to-title" checked={tempSettings.moveFeaturedArtistsToTitle} onCheckedChange={(checked) => setTempSettings((prev) => ({
...prev,
moveFeaturedArtistsToTitle: checked,
}))}/>
<Label htmlFor="move-featured-artists-to-title" className="text-sm cursor-pointer font-normal">
Move featured artists from artist to title
</Label>
</div>


</div>

Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface Settings {
useSingleGenre: boolean;
embedGenre: boolean;
redownloadWithSuffix: boolean;
moveFeaturedArtistsToTitle: boolean;
separator: "comma" | "semicolon";
}
export const FOLDER_PRESETS: Record<FolderPreset, {
Expand Down Expand Up @@ -197,6 +198,7 @@ export const DEFAULT_SETTINGS: Settings = {
useSingleGenre: false,
embedGenre: false,
redownloadWithSuffix: false,
moveFeaturedArtistsToTitle: false,
separator: "semicolon",
};
export const FONT_OPTIONS: FontOption[] = [
Expand Down Expand Up @@ -624,6 +626,9 @@ function normalizeSettingsPayload(settings: SettingsPayload): SettingsPayload {
if (!("redownloadWithSuffix" in normalized)) {
normalized.redownloadWithSuffix = false;
}
if (!("moveFeaturedArtistsToTitle" in normalized)) {
normalized.moveFeaturedArtistsToTitle = false;
}
normalized.operatingSystem = detectOS();
const normalizedCustomFonts = normalizeCustomFonts(normalized.customFonts);
normalized.customFonts = normalizedCustomFonts;
Expand Down