diff --git a/app.go b/app.go index 32c77d8f..a33e79a0 100644 --- a/app.go +++ b/app.go @@ -334,6 +334,7 @@ type DownloadRequest struct { UseSingleGenre bool `json:"use_single_genre,omitempty"` EmbedGenre bool `json:"embed_genre,omitempty"` Separator string `json:"separator,omitempty"` + IsExplicit bool `json:"is_explicit,omitempty"` } type DownloadResponse struct { @@ -501,6 +502,8 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { req.AudioFormat = "LOSSLESS" } + req.TrackName = backend.ApplyExplicitTitleSuffix(req.TrackName, req.IsExplicit) + var err error var filename string @@ -1804,6 +1807,7 @@ type CheckFileExistenceRequest struct { IncludeTrackNumber bool `json:"include_track_number,omitempty"` AudioFormat string `json:"audio_format,omitempty"` RelativePath string `json:"relative_path,omitempty"` + IsExplicit bool `json:"is_explicit,omitempty"` } type CheckFileExistenceResult struct { @@ -1947,7 +1951,7 @@ func (a *App) CheckFilesExistence(outputDir string, rootDir string, tracks []Che } expectedFilenameBase := backend.BuildExpectedFilename( - t.TrackName, + backend.ApplyExplicitTitleSuffix(t.TrackName, t.IsExplicit), t.ArtistName, t.AlbumName, t.AlbumArtist, diff --git a/backend/config.go b/backend/config.go index 15a9dcec..57e22707 100644 --- a/backend/config.go +++ b/backend/config.go @@ -60,6 +60,16 @@ func GetRedownloadWithSuffixSetting() bool { return enabled } +func GetAppendExplicitTagSetting() bool { + settings, err := LoadConfigSettings() + if err != nil || settings == nil { + return false + } + + enabled, _ := settings["appendExplicitTag"].(bool) + return enabled +} + func GetCustomTidalAPISetting() string { settings, err := LoadConfigSettings() if err != nil || settings == nil { diff --git a/backend/filename.go b/backend/filename.go index 91ae94d3..3289964f 100644 --- a/backend/filename.go +++ b/backend/filename.go @@ -10,6 +10,21 @@ import ( "unicode/utf8" ) +const explicitTitleSuffix = "🅴" + +func ApplyExplicitTitleSuffix(title string, isExplicit bool) string { + if !isExplicit || title == "" { + return title + } + if !GetAppendExplicitTagSetting() { + return title + } + if strings.HasSuffix(strings.TrimSpace(title), explicitTitleSuffix) { + return title + } + return strings.TrimRight(title, " ") + " " + explicitTitleSuffix +} + func buildFormattedFilenameBase(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat, playlistName, playlistOwner, isrc string, includeTrackNumber bool, position, discNumber int, useAlbumTrackNumber bool) string { safeTitle := SanitizeFilename(trackName) safeArtist := SanitizeFilename(artistName) diff --git a/frontend/src/components/AlbumInfo.tsx b/frontend/src/components/AlbumInfo.tsx index 6c3d7220..9c1a0e5c 100644 --- a/frontend/src/components/AlbumInfo.tsx +++ b/frontend/src/components/AlbumInfo.tsx @@ -59,7 +59,7 @@ interface AlbumInfoProps { onSortChange: (value: string) => void; onToggleTrack: (id: string) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void; - onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void; + onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string, isExplicit?: boolean) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void; onCheckAvailability?: (spotifyId: string) => void; diff --git a/frontend/src/components/ArtistInfo.tsx b/frontend/src/components/ArtistInfo.tsx index 8737cbb5..e7a5b790 100644 --- a/frontend/src/components/ArtistInfo.tsx +++ b/frontend/src/components/ArtistInfo.tsx @@ -72,7 +72,7 @@ interface ArtistInfoProps { onSortChange: (value: string) => void; onToggleTrack: (id: string) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void; - onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void; + onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string, isExplicit?: boolean) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void; onCheckAvailability?: (spotifyId: string) => void; diff --git a/frontend/src/components/PlaylistInfo.tsx b/frontend/src/components/PlaylistInfo.tsx index dc7cd29b..3e4af01a 100644 --- a/frontend/src/components/PlaylistInfo.tsx +++ b/frontend/src/components/PlaylistInfo.tsx @@ -65,7 +65,7 @@ interface PlaylistInfoProps { onSortChange: (value: string) => void; onToggleTrack: (id: string) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void; - onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void; + onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string, isExplicit?: boolean) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void; onCheckAvailability?: (spotifyId: string) => void; diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx index 96077c38..07eda461 100644 --- a/frontend/src/components/SettingsPage.tsx +++ b/frontend/src/components/SettingsPage.tsx @@ -763,6 +763,16 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin +
+ setTempSettings((prev) => ({ + ...prev, + appendExplicitTag: checked, + }))}/> + +
+ diff --git a/frontend/src/components/TrackList.tsx b/frontend/src/components/TrackList.tsx index c1618146..712f6c2f 100644 --- a/frontend/src/components/TrackList.tsx +++ b/frontend/src/components/TrackList.tsx @@ -36,7 +36,7 @@ interface TrackListProps { downloadingCoverTrack?: string | null; onToggleTrack: (id: string) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void; - onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void; + onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string, isExplicit?: boolean) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void; onCheckAvailability?: (spotifyId: string) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void; @@ -289,7 +289,7 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
{track.spotify_id && ( - diff --git a/frontend/src/hooks/useDownload.ts b/frontend/src/hooks/useDownload.ts index 8c96e7b8..413ebf5b 100644 --- a/frontend/src/hooks/useDownload.ts +++ b/frontend/src/hooks/useDownload.ts @@ -21,6 +21,7 @@ interface CheckFileExistenceRequest { include_track_number?: boolean; audio_format?: string; relative_path?: string; + is_explicit?: boolean; } interface FileExistenceResult { spotify_id: string; @@ -85,7 +86,7 @@ export function useDownload(region: string) { setDownloadProgress(safeTotalCount > 0 ? Math.min(100, Math.round((safeCompletedCount / safeTotalCount) * 100)) : 0); setDownloadRemainingCount(Math.max(0, safeTotalCount - safeCompletedCount)); }; - const downloadWithAutoFallback = async (id: string, settings: any, trackName?: string, artistName?: string, albumName?: string, playlistName?: string, position?: number, spotifyId?: string, durationMs?: number, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => { + const downloadWithAutoFallback = async (id: string, settings: any, trackName?: string, artistName?: string, albumName?: string, playlistName?: string, position?: number, spotifyId?: string, durationMs?: number, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string, isExplicit?: boolean) => { const service = settings.downloader; const query = trackName && artistName ? `${trackName} ${artistName} ` : undefined; const os = settings.operatingSystem; @@ -171,6 +172,7 @@ export function useDownload(region: string) { filename_format: settings.filenameTemplate || "", include_track_number: settings.trackNumber || false, audio_format: serviceForCheck, + is_explicit: isExplicit, }; const existenceResults = await CheckFilesExistence(outputDir, settings.downloadPath, [checkRequest]); if (existenceResults.length > 0 && existenceResults[0].exists) { @@ -247,6 +249,7 @@ export function useDownload(region: string) { use_first_artist_only: settings.useFirstArtistOnly, use_single_genre: settings.useSingleGenre, embed_genre: settings.embedGenre, + is_explicit: isExplicit, }); if (response.success) { logger.success(`Tidal: ${trackName} - ${artistName}`); @@ -294,6 +297,7 @@ export function useDownload(region: string) { publisher: publisher, use_single_genre: settings.useSingleGenre, embed_genre: settings.embedGenre, + is_explicit: isExplicit, }); if (response.success) { logger.success(`amazon: ${trackName} - ${artistName}`); @@ -341,6 +345,7 @@ export function useDownload(region: string) { publisher: publisher, use_single_genre: settings.useSingleGenre, embed_genre: settings.embedGenre, + is_explicit: isExplicit, }); if (response.success) { logger.success(`qobuz: ${trackName} - ${artistName}`); @@ -408,6 +413,7 @@ export function useDownload(region: string) { use_first_artist_only: settings.useFirstArtistOnly, use_single_genre: settings.useSingleGenre, embed_genre: settings.embedGenre, + is_explicit: isExplicit, }); if (!singleServiceResponse.success && itemID) { const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App"); @@ -415,7 +421,7 @@ export function useDownload(region: string) { } return singleServiceResponse; }; - const downloadWithItemID = async (settings: any, itemID: string, trackName?: string, artistName?: string, albumName?: string, folderName?: string, position?: number, spotifyId?: string, durationMs?: number, isAlbum?: boolean, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => { + const downloadWithItemID = async (settings: any, itemID: string, trackName?: string, artistName?: string, albumName?: string, folderName?: string, position?: number, spotifyId?: string, durationMs?: number, isAlbum?: boolean, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string, isExplicit?: boolean) => { const service = settings.downloader; const query = trackName && artistName ? `${trackName} ${artistName}` : undefined; const os = settings.operatingSystem; @@ -530,6 +536,7 @@ export function useDownload(region: string) { use_first_artist_only: settings.useFirstArtistOnly, use_single_genre: settings.useSingleGenre, embed_genre: settings.embedGenre, + is_explicit: isExplicit, }); if (response.success) { logger.success(`Tidal: ${trackName} - ${artistName}`); @@ -578,6 +585,7 @@ export function useDownload(region: string) { use_first_artist_only: settings.useFirstArtistOnly, use_single_genre: settings.useSingleGenre, embed_genre: settings.embedGenre, + is_explicit: isExplicit, }); if (response.success) { logger.success(`amazon: ${trackName} - ${artistName}`); @@ -627,6 +635,7 @@ export function useDownload(region: string) { use_first_artist_only: settings.useFirstArtistOnly, use_single_genre: settings.useSingleGenre, embed_genre: settings.embedGenre, + is_explicit: isExplicit, }); if (response.success) { logger.success(`qobuz: ${trackName} - ${artistName}`); @@ -689,6 +698,7 @@ export function useDownload(region: string) { use_first_artist_only: settings.useFirstArtistOnly, use_single_genre: settings.useSingleGenre, embed_genre: settings.embedGenre, + is_explicit: isExplicit, }); if (!singleServiceResponse.success && itemID) { const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App"); @@ -696,7 +706,7 @@ export function useDownload(region: string) { } return singleServiceResponse; }; - const handleDownloadTrack = async (id: string, trackName?: string, artistName?: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => { + const handleDownloadTrack = async (id: string, trackName?: string, artistName?: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string, isExplicit?: boolean) => { if (!id) { toast.error("No ID found for this track"); return; @@ -707,7 +717,7 @@ export function useDownload(region: string) { setDownloadingTrack(id); try { const releaseYear = releaseDate?.substring(0, 4); - const response = await downloadWithAutoFallback(id, settings, trackName, artistName, albumName, playlistName, position, spotifyId, durationMs, releaseYear, albumArtist || "", releaseDate, coverUrl, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, copyright, publisher); + const response = await downloadWithAutoFallback(id, settings, trackName, artistName, albumName, playlistName, position, spotifyId, durationMs, releaseYear, albumArtist || "", releaseDate, coverUrl, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, copyright, publisher, isExplicit); if (response.success) { if (response.already_exists) { toast.info(response.message); @@ -777,6 +787,7 @@ export function useDownload(region: string) { filename_format: settings.filenameTemplate || "", include_track_number: settings.trackNumber || false, audio_format: audioFormat, + is_explicit: track.is_explicit, }; }); const existenceResults = await CheckFilesExistence(outputDir, settings.downloadPath, existenceChecks); @@ -831,7 +842,7 @@ export function useDownload(region: string) { setCurrentDownloadInfo({ name: track.name, artists: displayArtist || "" }); try { const releaseYear = track.release_date?.substring(0, 4); - const response = await downloadWithItemID(settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher); + const response = await downloadWithItemID(settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher, track.is_explicit); if (response.success) { if (response.already_exists) { skippedCount++; @@ -952,6 +963,7 @@ export function useDownload(region: string) { filename_format: settings.filenameTemplate || "", include_track_number: settings.trackNumber || false, audio_format: audioFormat, + is_explicit: track.is_explicit, }; }); const existenceResults = await CheckFilesExistence(outputDir, settings.downloadPath, existenceChecks); @@ -1004,7 +1016,7 @@ export function useDownload(region: string) { setCurrentDownloadInfo({ name: track.name || "", artists: displayArtist || "" }); try { const releaseYear = track.release_date?.substring(0, 4); - const response = await downloadWithItemID(settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher); + const response = await downloadWithItemID(settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher, track.is_explicit); if (response.success) { if (response.already_exists) { skippedCount++; diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts index 306b58ef..1e8e58c8 100644 --- a/frontend/src/lib/settings.ts +++ b/frontend/src/lib/settings.ts @@ -54,6 +54,7 @@ export interface Settings { useSingleGenre: boolean; embedGenre: boolean; redownloadWithSuffix: boolean; + appendExplicitTag: boolean; separator: "comma" | "semicolon"; } export const FOLDER_PRESETS: Record