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