.playlist owner folder name

This commit is contained in:
afkarxyz
2026-04-13 20:56:24 +07:00
parent 24d640443a
commit 1b00badd93
5 changed files with 45 additions and 8 deletions
+4 -1
View File
@@ -35,6 +35,7 @@ import { useAvailability } from "@/hooks/useAvailability";
import { ensureApiStatusCheckStarted } from "@/lib/api-status"; import { ensureApiStatusCheckStarted } from "@/lib/api-status";
import { useDownloadQueueDialog } from "@/hooks/useDownloadQueueDialog"; import { useDownloadQueueDialog } from "@/hooks/useDownloadQueueDialog";
import { useDownloadProgress } from "@/hooks/useDownloadProgress"; import { useDownloadProgress } from "@/hooks/useDownloadProgress";
import { buildPlaylistFolderName } from "@/lib/playlist";
const HISTORY_KEY = "spotiflac_fetch_history"; const HISTORY_KEY = "spotiflac_fetch_history";
const MAX_HISTORY = 5; const MAX_HISTORY = 5;
function extractSpotifyEntityFromURL(url: string): { function extractSpotifyEntityFromURL(url: string): {
@@ -433,7 +434,9 @@ function App() {
} }
if ("playlist_info" in metadata.metadata) { if ("playlist_info" in metadata.metadata) {
const { playlist_info, track_list } = metadata.metadata; const { playlist_info, track_list } = metadata.metadata;
return (<PlaylistInfo playlistInfo={playlist_info} trackList={track_list} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} downloadProgress={download.downloadProgress} currentDownloadInfo={download.currentDownloadInfo} currentPage={currentListPage} itemsPerPage={ITEMS_PER_PAGE} downloadedLyrics={lyrics.downloadedLyrics} failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} checkingAvailabilityTrack={availability.checkingTrackId} availabilityMap={availability.availabilityMap} downloadedCovers={cover.downloadedCovers} failedCovers={cover.failedCovers} skippedCovers={cover.skippedCovers} downloadingCoverTrack={cover.downloadingCoverTrack} isBulkDownloadingCovers={cover.isBulkDownloadingCovers} isBulkDownloadingLyrics={lyrics.isBulkDownloadingLyrics} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) => 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 (<PlaylistInfo playlistInfo={playlist_info} trackList={track_list} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} downloadProgress={download.downloadProgress} currentDownloadInfo={download.currentDownloadInfo} currentPage={currentListPage} itemsPerPage={ITEMS_PER_PAGE} downloadedLyrics={lyrics.downloadedLyrics} failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} checkingAvailabilityTrack={availability.checkingTrackId} availabilityMap={availability.availabilityMap} downloadedCovers={cover.downloadedCovers} failedCovers={cover.failedCovers} skippedCovers={cover.skippedCovers} downloadingCoverTrack={cover.downloadingCoverTrack} isBulkDownloadingCovers={cover.isBulkDownloadingCovers} isBulkDownloadingLyrics={lyrics.isBulkDownloadingLyrics} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) => 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"; const pendingArtistUrl = artist.external_urls.replace(/\/$/, "") + "/discography/all";
setSpotifyUrl(pendingArtistUrl); setSpotifyUrl(pendingArtistUrl);
const artistUrl = await metadata.handleArtistClick(artist); const artistUrl = await metadata.handleArtistClick(artist);
+9 -7
View File
@@ -12,6 +12,7 @@ import { useState } from "react";
import { toastWithSound as toast } from "@/lib/toast-with-sound"; import { toastWithSound as toast } from "@/lib/toast-with-sound";
import { joinPath, sanitizePath } from "@/lib/utils"; import { joinPath, sanitizePath } from "@/lib/utils";
import { parseTemplate, type TemplateData } from "@/lib/settings"; import { parseTemplate, type TemplateData } from "@/lib/settings";
import { buildPlaylistFolderName } from "@/lib/playlist";
import type { TrackMetadata, TrackAvailability } from "@/types/api"; import type { TrackMetadata, TrackAvailability } from "@/types/api";
interface PlaylistInfoProps { interface PlaylistInfoProps {
playlistInfo: { 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) { 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 settings = getSettings();
const playlistName = playlistInfo.owner.name;
const playlistFolderName = buildPlaylistFolderName(playlistName, playlistInfo.owner.display_name, settings.playlistOwnerFolderName);
const [downloadingPlaylistCover, setDownloadingPlaylistCover] = useState(false); const [downloadingPlaylistCover, setDownloadingPlaylistCover] = useState(false);
const handleDownloadPlaylistCover = async () => { const handleDownloadPlaylistCover = async () => {
if (!playlistInfo.cover) if (!playlistInfo.cover)
@@ -96,17 +99,16 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel
try { try {
const os = settings.operatingSystem; const os = settings.operatingSystem;
let outputDir = settings.downloadPath; let outputDir = settings.downloadPath;
const playlistName = playlistInfo.owner.name;
const placeholder = "__SLASH_PLACEHOLDER__"; const placeholder = "__SLASH_PLACEHOLDER__";
const templateData: TemplateData = { const templateData: TemplateData = {
artist: "", artist: "",
album: "", album: "",
album_artist: "", album_artist: "",
title: playlistName.replace(/\//g, placeholder), title: playlistName.replace(/\//g, placeholder),
playlist: playlistName.replace(/\//g, placeholder), playlist: playlistFolderName.replace(/\//g, placeholder),
}; };
if (settings.createPlaylistFolder && playlistName) { if (settings.createPlaylistFolder && playlistFolderName) {
outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os)); outputDir = joinPath(os, outputDir, sanitizePath(playlistFolderName.replace(/\//g, " "), os));
} }
if (settings.folderTemplate) { if (settings.folderTemplate) {
const folderPath = parseTemplate(settings.folderTemplate, templateData); const folderPath = parseTemplate(settings.folderTemplate, templateData);
@@ -157,7 +159,7 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel
<CardContent className="px-6"> <CardContent className="px-6">
<div className="flex gap-6 items-start"> <div className="flex gap-6 items-start">
{playlistInfo.cover && (<div className="relative group shrink-0 w-48 h-48"> {playlistInfo.cover && (<div className="relative group shrink-0 w-48 h-48">
<img src={playlistInfo.cover} alt={playlistInfo.owner.name} className="w-48 h-48 rounded-md shadow-lg object-cover"/> <img src={playlistInfo.cover} alt={playlistName} className="w-48 h-48 rounded-md shadow-lg object-cover"/>
<div className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity rounded-md"> <div className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity rounded-md">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -172,7 +174,7 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel
<div className="flex-1 space-y-4"> <div className="flex-1 space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm font-medium">Playlist</p> <p className="text-sm font-medium">Playlist</p>
<h2 className="text-4xl font-bold">{playlistInfo.owner.name}</h2> <h2 className="text-4xl font-bold">{playlistName}</h2>
{playlistInfo.description && (<p className="text-sm text-muted-foreground">{playlistInfo.description}</p>)} {playlistInfo.description && (<p className="text-sm text-muted-foreground">{playlistInfo.description}</p>)}
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -234,7 +236,7 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel
</Card> </Card>
<div className="space-y-4"> <div className="space-y-4">
<SearchAndSort searchQuery={searchQuery} sortBy={sortBy} onSearchChange={onSearchChange} onSortChange={onSortChange}/> <SearchAndSort searchQuery={searchQuery} sortBy={sortBy} onSearchChange={onSearchChange} onSortChange={onSortChange}/>
<TrackList tracks={trackList} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={downloadedTracks} failedTracks={failedTracks} skippedTracks={skippedTracks} downloadingTrack={downloadingTrack} isDownloading={isDownloading} currentPage={currentPage} itemsPerPage={itemsPerPage} showCheckboxes={true} hideAlbumColumn={false} folderName={playlistInfo.owner.name} downloadedLyrics={downloadedLyrics} failedLyrics={failedLyrics} skippedLyrics={skippedLyrics} downloadingLyricsTrack={downloadingLyricsTrack} checkingAvailabilityTrack={checkingAvailabilityTrack} availabilityMap={availabilityMap} downloadedCovers={downloadedCovers} failedCovers={failedCovers} skippedCovers={skippedCovers} downloadingCoverTrack={downloadingCoverTrack} onToggleTrack={onToggleTrack} onToggleSelectAll={onToggleSelectAll} onDownloadTrack={onDownloadTrack} onDownloadLyrics={onDownloadLyrics} onDownloadCover={onDownloadCover} onCheckAvailability={onCheckAvailability} onPageChange={onPageChange} onAlbumClick={onAlbumClick} onArtistClick={onArtistClick} onTrackClick={onTrackClick}/> <TrackList tracks={trackList} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={downloadedTracks} failedTracks={failedTracks} skippedTracks={skippedTracks} downloadingTrack={downloadingTrack} isDownloading={isDownloading} currentPage={currentPage} itemsPerPage={itemsPerPage} showCheckboxes={true} hideAlbumColumn={false} folderName={playlistFolderName} downloadedLyrics={downloadedLyrics} failedLyrics={failedLyrics} skippedLyrics={skippedLyrics} downloadingLyricsTrack={downloadingLyricsTrack} checkingAvailabilityTrack={checkingAvailabilityTrack} availabilityMap={availabilityMap} downloadedCovers={downloadedCovers} failedCovers={failedCovers} skippedCovers={skippedCovers} downloadingCoverTrack={downloadingCoverTrack} onToggleTrack={onToggleTrack} onToggleSelectAll={onToggleSelectAll} onDownloadTrack={onDownloadTrack} onDownloadLyrics={onDownloadLyrics} onDownloadCover={onDownloadCover} onCheckAvailability={onCheckAvailability} onPageChange={onPageChange} onAlbumClick={onAlbumClick} onArtistClick={onArtistClick} onTrackClick={onTrackClick}/>
</div> </div>
</div>); </div>);
} }
+10
View File
@@ -584,6 +584,16 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
</Label> </Label>
</div> </div>
<div className="flex items-center gap-3">
<Switch id="playlist-owner-folder-name" checked={tempSettings.playlistOwnerFolderName} onCheckedChange={(checked) => setTempSettings((prev) => ({
...prev,
playlistOwnerFolderName: checked,
}))}/>
<Label htmlFor="playlist-owner-folder-name" className="text-sm cursor-pointer font-normal">
Playlist Owner Folder Name
</Label>
</div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Switch id="create-m3u8-file" checked={tempSettings.createM3u8File} onCheckedChange={(checked) => setTempSettings((prev) => ({ <Switch id="create-m3u8-file" checked={tempSettings.createM3u8File} onCheckedChange={(checked) => setTempSettings((prev) => ({
...prev, ...prev,
+14
View File
@@ -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}`;
}
+8
View File
@@ -31,6 +31,7 @@ export interface Settings {
useSpotFetchAPI: boolean; useSpotFetchAPI: boolean;
spotFetchAPIUrl: string; spotFetchAPIUrl: string;
createPlaylistFolder: boolean; createPlaylistFolder: boolean;
playlistOwnerFolderName: boolean;
createM3u8File: boolean; createM3u8File: boolean;
useFirstArtistOnly: boolean; useFirstArtistOnly: boolean;
useSingleGenre: boolean; useSingleGenre: boolean;
@@ -118,6 +119,7 @@ export const DEFAULT_SETTINGS: Settings = {
useSpotFetchAPI: false, useSpotFetchAPI: false,
spotFetchAPIUrl: "https://sp.afkarxyz.qzz.io/api", spotFetchAPIUrl: "https://sp.afkarxyz.qzz.io/api",
createPlaylistFolder: true, createPlaylistFolder: true,
playlistOwnerFolderName: false,
createM3u8File: false, createM3u8File: false,
useFirstArtistOnly: false, useFirstArtistOnly: false,
useSingleGenre: false, useSingleGenre: false,
@@ -235,6 +237,9 @@ function getSettingsFromLocalStorage(): Settings {
if (!('allowResolverFallback' in parsed)) { if (!('allowResolverFallback' in parsed)) {
parsed.allowResolverFallback = true; parsed.allowResolverFallback = true;
} }
if (!('playlistOwnerFolderName' in parsed)) {
parsed.playlistOwnerFolderName = false;
}
if (!('separator' in parsed)) { if (!('separator' in parsed)) {
parsed.separator = "semicolon"; parsed.separator = "semicolon";
} }
@@ -323,6 +328,9 @@ export async function loadSettings(): Promise<Settings> {
if (!('createPlaylistFolder' in parsed)) { if (!('createPlaylistFolder' in parsed)) {
parsed.createPlaylistFolder = true; parsed.createPlaylistFolder = true;
} }
if (!('playlistOwnerFolderName' in parsed)) {
parsed.playlistOwnerFolderName = false;
}
if (!('createM3u8File' in parsed)) { if (!('createM3u8File' in parsed)) {
parsed.createM3u8File = false; parsed.createM3u8File = false;
} }