From 1b00badd93de6a8e2cfa20a4e16df18b625c6be5 Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Mon, 13 Apr 2026 20:56:24 +0700 Subject: [PATCH] .playlist owner folder name --- frontend/src/App.tsx | 5 ++++- frontend/src/components/PlaylistInfo.tsx | 16 +++++++++------- frontend/src/components/SettingsPage.tsx | 10 ++++++++++ frontend/src/lib/playlist.ts | 14 ++++++++++++++ frontend/src/lib/settings.ts | 8 ++++++++ 5 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 frontend/src/lib/playlist.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bff17ac..d8f51e1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -35,6 +35,7 @@ import { useAvailability } from "@/hooks/useAvailability"; import { ensureApiStatusCheckStarted } from "@/lib/api-status"; import { useDownloadQueueDialog } from "@/hooks/useDownloadQueueDialog"; import { useDownloadProgress } from "@/hooks/useDownloadProgress"; +import { buildPlaylistFolderName } from "@/lib/playlist"; const HISTORY_KEY = "spotiflac_fetch_history"; const MAX_HISTORY = 5; function extractSpotifyEntityFromURL(url: string): { @@ -433,7 +434,9 @@ function App() { } if ("playlist_info" in metadata.metadata) { const { playlist_info, track_list } = metadata.metadata; - return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlist_info.owner.name, position, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, playlist_info.owner.name, position, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, playlist_info.owner.name)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, playlist_info.owner.name)} onDownloadAll={() => download.handleDownloadAll(track_list, playlist_info.owner.name)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, playlist_info.owner.name)} onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onPageChange={setCurrentListPage} onBack={metadata.resetMetadata} onAlbumClick={metadata.handleAlbumClick} onArtistClick={async (artist) => { + const settings = getSettings(); + const playlistFolderName = buildPlaylistFolderName(playlist_info.owner.name, playlist_info.owner.display_name, settings.playlistOwnerFolderName); + return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlistFolderName, position, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, playlistFolderName, position, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, playlistFolderName)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, playlistFolderName)} onDownloadAll={() => download.handleDownloadAll(track_list, playlistFolderName)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, playlistFolderName)} onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onPageChange={setCurrentListPage} onBack={metadata.resetMetadata} onAlbumClick={metadata.handleAlbumClick} onArtistClick={async (artist) => { const pendingArtistUrl = artist.external_urls.replace(/\/$/, "") + "/discography/all"; setSpotifyUrl(pendingArtistUrl); const artistUrl = await metadata.handleArtistClick(artist); diff --git a/frontend/src/components/PlaylistInfo.tsx b/frontend/src/components/PlaylistInfo.tsx index 50af90e..1b7609b 100644 --- a/frontend/src/components/PlaylistInfo.tsx +++ b/frontend/src/components/PlaylistInfo.tsx @@ -12,6 +12,7 @@ import { useState } from "react"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; import { joinPath, sanitizePath } from "@/lib/utils"; import { parseTemplate, type TemplateData } from "@/lib/settings"; +import { buildPlaylistFolderName } from "@/lib/playlist"; import type { TrackMetadata, TrackAvailability } from "@/types/api"; interface PlaylistInfoProps { playlistInfo: { @@ -88,6 +89,8 @@ interface PlaylistInfoProps { } export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, selectedTracks, downloadedTracks, failedTracks, skippedTracks, downloadingTrack, isDownloading, bulkDownloadType, downloadProgress, currentDownloadInfo, currentPage, itemsPerPage, downloadedLyrics, failedLyrics, skippedLyrics, downloadingLyricsTrack, checkingAvailabilityTrack, availabilityMap, downloadedCovers, failedCovers, skippedCovers, downloadingCoverTrack, isBulkDownloadingCovers, isBulkDownloadingLyrics, onSearchChange, onSortChange, onToggleTrack, onToggleSelectAll, onDownloadTrack, onDownloadLyrics, onDownloadCover, onCheckAvailability, onDownloadAllLyrics, onDownloadAllCovers, onDownloadAll, onDownloadSelected, onStopDownload, onOpenFolder, onPageChange, onAlbumClick, onArtistClick, onTrackClick, onBack, }: PlaylistInfoProps) { const settings = getSettings(); + const playlistName = playlistInfo.owner.name; + const playlistFolderName = buildPlaylistFolderName(playlistName, playlistInfo.owner.display_name, settings.playlistOwnerFolderName); const [downloadingPlaylistCover, setDownloadingPlaylistCover] = useState(false); const handleDownloadPlaylistCover = async () => { if (!playlistInfo.cover) @@ -96,17 +99,16 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel try { const os = settings.operatingSystem; let outputDir = settings.downloadPath; - const playlistName = playlistInfo.owner.name; const placeholder = "__SLASH_PLACEHOLDER__"; const templateData: TemplateData = { artist: "", album: "", album_artist: "", title: playlistName.replace(/\//g, placeholder), - playlist: playlistName.replace(/\//g, placeholder), + playlist: playlistFolderName.replace(/\//g, placeholder), }; - if (settings.createPlaylistFolder && playlistName) { - outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os)); + if (settings.createPlaylistFolder && playlistFolderName) { + outputDir = joinPath(os, outputDir, sanitizePath(playlistFolderName.replace(/\//g, " "), os)); } if (settings.folderTemplate) { const folderPath = parseTemplate(settings.folderTemplate, templateData); @@ -157,7 +159,7 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel
{playlistInfo.cover && (
- {playlistInfo.owner.name} + {playlistName}
@@ -172,7 +174,7 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel

Playlist

-

{playlistInfo.owner.name}

+

{playlistName}

{playlistInfo.description && (

{playlistInfo.description}

)}
@@ -234,7 +236,7 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel
- +
); } diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx index 60b0218..d02588f 100644 --- a/frontend/src/components/SettingsPage.tsx +++ b/frontend/src/components/SettingsPage.tsx @@ -584,6 +584,16 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
+
+ setTempSettings((prev) => ({ + ...prev, + playlistOwnerFolderName: checked, + }))}/> + +
+
setTempSettings((prev) => ({ ...prev, diff --git a/frontend/src/lib/playlist.ts b/frontend/src/lib/playlist.ts new file mode 100644 index 0000000..c2338c2 --- /dev/null +++ b/frontend/src/lib/playlist.ts @@ -0,0 +1,14 @@ +export function buildPlaylistFolderName(playlistName?: string, ownerName?: string, includeOwner = false): string { + const normalizedPlaylistName = playlistName?.trim() || ""; + if (!normalizedPlaylistName) { + return ""; + } + if (!includeOwner) { + return normalizedPlaylistName; + } + const normalizedOwnerName = ownerName?.trim() || ""; + if (!normalizedOwnerName || normalizedOwnerName.toLowerCase() === normalizedPlaylistName.toLowerCase()) { + return normalizedPlaylistName; + } + return `${normalizedPlaylistName}, ${normalizedOwnerName}`; +} diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts index a1e1259..7667b79 100644 --- a/frontend/src/lib/settings.ts +++ b/frontend/src/lib/settings.ts @@ -31,6 +31,7 @@ export interface Settings { useSpotFetchAPI: boolean; spotFetchAPIUrl: string; createPlaylistFolder: boolean; + playlistOwnerFolderName: boolean; createM3u8File: boolean; useFirstArtistOnly: boolean; useSingleGenre: boolean; @@ -118,6 +119,7 @@ export const DEFAULT_SETTINGS: Settings = { useSpotFetchAPI: false, spotFetchAPIUrl: "https://sp.afkarxyz.qzz.io/api", createPlaylistFolder: true, + playlistOwnerFolderName: false, createM3u8File: false, useFirstArtistOnly: false, useSingleGenre: false, @@ -235,6 +237,9 @@ function getSettingsFromLocalStorage(): Settings { if (!('allowResolverFallback' in parsed)) { parsed.allowResolverFallback = true; } + if (!('playlistOwnerFolderName' in parsed)) { + parsed.playlistOwnerFolderName = false; + } if (!('separator' in parsed)) { parsed.separator = "semicolon"; } @@ -323,6 +328,9 @@ export async function loadSettings(): Promise { if (!('createPlaylistFolder' in parsed)) { parsed.createPlaylistFolder = true; } + if (!('playlistOwnerFolderName' in parsed)) { + parsed.playlistOwnerFolderName = false; + } if (!('createM3u8File' in parsed)) { parsed.createM3u8File = false; }