This commit is contained in:
afkarxyz
2026-04-14 07:28:39 +07:00
parent ce1e6cc65a
commit a9c52e7b6d
10 changed files with 35 additions and 58 deletions
+6 -6
View File
@@ -211,10 +211,10 @@ export function AlbumInfo({ albumInfo, trackList, searchQuery, sortBy, selectedT
<span className="font-medium"> <span className="font-medium">
{clickableAlbumArtists.length > 0 ? clickableAlbumArtists.map((artist, index) => (<span key={`${artist.id || artist.name}-${index}`}> {clickableAlbumArtists.length > 0 ? clickableAlbumArtists.map((artist, index) => (<span key={`${artist.id || artist.name}-${index}`}>
{onArtistClick && artist.external_urls ? (<span className="cursor-pointer hover:underline" onClick={() => onArtistClick({ {onArtistClick && artist.external_urls ? (<span className="cursor-pointer hover:underline" onClick={() => onArtistClick({
id: artist.id, id: artist.id,
name: artist.name, name: artist.name,
external_urls: artist.external_urls, external_urls: artist.external_urls,
})}> })}>
{artist.name} {artist.name}
</span>) : (artist.name)} </span>) : (artist.name)}
{index < clickableAlbumArtists.length - 1 && artistSeparator} {index < clickableAlbumArtists.length - 1 && artistSeparator}
@@ -225,8 +225,8 @@ export function AlbumInfo({ albumInfo, trackList, searchQuery, sortBy, selectedT
<span></span> <span></span>
<span> <span>
{showStreamingProgress {showStreamingProgress
? `${fetchedTrackCount.toLocaleString()} / ${totalTrackCount.toLocaleString()} tracks` ? `${fetchedTrackCount.toLocaleString()} / ${totalTrackCount.toLocaleString()} tracks`
: `${Math.max(totalTrackCount, fetchedTrackCount).toLocaleString()} ${Math.max(totalTrackCount, fetchedTrackCount) === 1 ? "track" : "tracks"}`} : `${Math.max(totalTrackCount, fetchedTrackCount).toLocaleString()} ${Math.max(totalTrackCount, fetchedTrackCount) === 1 ? "track" : "tracks"}`}
</span> </span>
</div> </div>
</div> </div>
+5 -26
View File
@@ -2,19 +2,16 @@ import type { ReactNode } from "react";
import type { TrackAvailability } from "@/types/api"; import type { TrackAvailability } from "@/types/api";
import { openExternal } from "@/lib/utils"; import { openExternal } from "@/lib/utils";
import { AmazonAvailabilityIcon, QobuzAvailabilityIcon, TidalAvailabilityIcon } from "./PlatformIcons"; import { AmazonAvailabilityIcon, QobuzAvailabilityIcon, TidalAvailabilityIcon } from "./PlatformIcons";
interface AvailabilityLinkEntry { interface AvailabilityLinkEntry {
id: string; id: string;
found: boolean; found: boolean;
url?: string; url?: string;
icon: ReactNode; icon: ReactNode;
} }
function getAvailabilityLinkEntries(availability: TrackAvailability): AvailabilityLinkEntry[] { function getAvailabilityLinkEntries(availability: TrackAvailability): AvailabilityLinkEntry[] {
const tidalUrl = availability.tidal_url?.trim() || ""; const tidalUrl = availability.tidal_url?.trim() || "";
const qobuzUrl = availability.qobuz_url?.trim() || ""; const qobuzUrl = availability.qobuz_url?.trim() || "";
const amazonUrl = availability.amazon_url?.trim() || ""; const amazonUrl = availability.amazon_url?.trim() || "";
return [ return [
{ {
id: "tidal", id: "tidal",
@@ -36,48 +33,30 @@ function getAvailabilityLinkEntries(availability: TrackAvailability): Availabili
}, },
]; ];
} }
export function hasAvailabilityLinks(availability?: TrackAvailability): boolean { export function hasAvailabilityLinks(availability?: TrackAvailability): boolean {
if (!availability) { if (!availability) {
return false; return false;
} }
return getAvailabilityLinkEntries(availability).some((entry) => entry.found); return getAvailabilityLinkEntries(availability).some((entry) => entry.found);
} }
export function AvailabilityLinks({ availability }: { export function AvailabilityLinks({ availability }: {
availability?: TrackAvailability; availability?: TrackAvailability;
}) { }) {
if (!availability) { if (!availability) {
return <p>Check Availability</p>; return <p>Check Availability</p>;
} }
const entries = getAvailabilityLinkEntries(availability); const entries = getAvailabilityLinkEntries(availability);
return ( return (<div className="flex flex-col gap-1.5 w-[260px] max-w-[260px] pointer-events-auto">
<div className="flex flex-col gap-1.5 w-[260px] max-w-[260px] pointer-events-auto"> {entries.map((entry) => entry.found ? (<button key={entry.id} type="button" onClick={() => entry.url && openExternal(entry.url)} className="flex items-center gap-2 text-left text-xs hover:underline min-w-0 cursor-pointer" title={entry.url}>
{entries.map((entry) => entry.found ? (
<button
key={entry.id}
type="button"
onClick={() => entry.url && openExternal(entry.url)}
className="flex items-center gap-2 text-left text-xs hover:underline min-w-0 cursor-pointer"
title={entry.url}
>
{entry.icon} {entry.icon}
<span className="truncate whitespace-nowrap leading-5 min-w-0"> <span className="truncate whitespace-nowrap leading-5 min-w-0">
{entry.url} {entry.url}
</span> </span>
</button> </button>) : (<div key={entry.id} className="flex items-center gap-2 text-left text-xs min-w-0">
) : (
<div
key={entry.id}
className="flex items-center gap-2 text-left text-xs min-w-0"
>
{entry.icon} {entry.icon}
<span className="truncate whitespace-nowrap leading-5 min-w-0 text-red-500"> <span className="truncate whitespace-nowrap leading-5 min-w-0 text-red-500">
Not Found Not Found
</span> </span>
</div> </div>))}
))} </div>);
</div>
);
} }
+2 -2
View File
@@ -188,8 +188,8 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel
<span></span> <span></span>
<span> <span>
{showStreamingProgress {showStreamingProgress
? `${fetchedTrackCount.toLocaleString()} / ${totalTrackCount.toLocaleString()} tracks` ? `${fetchedTrackCount.toLocaleString()} / ${totalTrackCount.toLocaleString()} tracks`
: `${Math.max(totalTrackCount, fetchedTrackCount).toLocaleString()} ${Math.max(totalTrackCount, fetchedTrackCount) === 1 ? "track" : "tracks"}`} : `${Math.max(totalTrackCount, fetchedTrackCount).toLocaleString()} ${Math.max(totalTrackCount, fetchedTrackCount) === 1 ? "track" : "tracks"}`}
</span> </span>
<span></span> <span></span>
<span>{playlistInfo.followers.total.toLocaleString()} {playlistInfo.followers.total === 1 ? "follower" : "followers"}</span> <span>{playlistInfo.followers.total.toLocaleString()} {playlistInfo.followers.total === 1 ? "follower" : "followers"}</span>
+6 -8
View File
@@ -5,7 +5,6 @@ import { fetchCurrentIPInfo } from "@/lib/api";
import type { CurrentIPInfo } from "@/types/api"; import type { CurrentIPInfo } from "@/types/api";
import { openExternal } from "@/lib/utils"; import { openExternal } from "@/lib/utils";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
const IP_INFO_REFRESH_INTERVAL_MS = 30000; const IP_INFO_REFRESH_INTERVAL_MS = 30000;
const SPOTIFY_BLOCKED_COUNTRY_CODES = new Set([ const SPOTIFY_BLOCKED_COUNTRY_CODES = new Set([
"AF", "AF",
@@ -25,7 +24,6 @@ const SPOTIFY_BLOCKED_COUNTRY_CODES = new Set([
"TM", "TM",
"YE", "YE",
]); ]);
export function TitleBar() { export function TitleBar() {
const [currentIPInfo, setCurrentIPInfo] = useState<CurrentIPInfo | null>(null); const [currentIPInfo, setCurrentIPInfo] = useState<CurrentIPInfo | null>(null);
const [isLoadingCurrentIPInfo, setIsLoadingCurrentIPInfo] = useState(false); const [isLoadingCurrentIPInfo, setIsLoadingCurrentIPInfo] = useState(false);
@@ -117,12 +115,12 @@ export function TitleBar() {
{detectedFlagPath ? (<img src={detectedFlagPath} alt={detectedCountryCode} className="h-3.5 w-[18px] rounded-[2px] border object-cover bg-muted"/>) : (<Globe className="w-4 h-4 opacity-70"/>)} {detectedFlagPath ? (<img src={detectedFlagPath} alt={detectedCountryCode} className="h-3.5 w-[18px] rounded-[2px] border object-cover bg-muted"/>) : (<Globe className="w-4 h-4 opacity-70"/>)}
<span className="font-mono text-xs truncate"> <span className="font-mono text-xs truncate">
{isLoadingCurrentIPInfo {isLoadingCurrentIPInfo
? "Detecting..." ? "Detecting..."
: currentIPInfo : currentIPInfo
? showIPAddress ? showIPAddress
? `${currentIPInfo.ip} - ${currentIPInfo.country}${detectedCountryCode ? ` (${detectedCountryCode})` : ""}` ? `${currentIPInfo.ip} - ${currentIPInfo.country}${detectedCountryCode ? ` (${detectedCountryCode})` : ""}`
: `${currentIPInfo.country}${detectedCountryCode ? ` (${detectedCountryCode})` : ""}` : `${currentIPInfo.country}${detectedCountryCode ? ` (${detectedCountryCode})` : ""}`
: "Unavailable"} : "Unavailable"}
</span> </span>
</div> </div>
{currentIPInfo && !isLoadingCurrentIPInfo && (<button type="button" onClick={() => setShowIPAddress((prev) => !prev)} className="inline-flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground transition-colors" aria-label={showIPAddress ? "Hide IP" : "Show IP"}> {currentIPInfo && !isLoadingCurrentIPInfo && (<button type="button" onClick={() => setShowIPAddress((prev) => !prev)} className="inline-flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground transition-colors" aria-label={showIPAddress ? "Hide IP" : "Show IP"}>
+4 -4
View File
@@ -85,10 +85,10 @@ export function TrackInfo({ track, isDownloading, downloadingTrack, isDownloaded
<p className="text-lg text-muted-foreground"> <p className="text-lg text-muted-foreground">
{clickableArtists.length > 0 ? clickableArtists.map((artist, index) => (<span key={`${artist.id || artist.name}-${index}`}> {clickableArtists.length > 0 ? clickableArtists.map((artist, index) => (<span key={`${artist.id || artist.name}-${index}`}>
{onArtistClick ? (<span className="cursor-pointer hover:underline" onClick={() => onArtistClick({ {onArtistClick ? (<span className="cursor-pointer hover:underline" onClick={() => onArtistClick({
id: artist.id, id: artist.id,
name: artist.name, name: artist.name,
external_urls: artist.external_urls, external_urls: artist.external_urls,
})}> })}>
{artist.name} {artist.name}
</span>) : (artist.name)} </span>) : (artist.name)}
{index < clickableArtists.length - 1 && ", "} {index < clickableArtists.length - 1 && ", "}
+4 -4
View File
@@ -257,10 +257,10 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
} }
return clickableArtists.map((artist, i) => (<span key={`${artist.id || artist.name}-${i}`}> return clickableArtists.map((artist, i) => (<span key={`${artist.id || artist.name}-${i}`}>
{onArtistClick ? (<span className="cursor-pointer hover:underline" onClick={() => onArtistClick({ {onArtistClick ? (<span className="cursor-pointer hover:underline" onClick={() => onArtistClick({
id: artist.id, id: artist.id,
name: artist.name, name: artist.name,
external_urls: artist.external_urls, external_urls: artist.external_urls,
})}> })}>
{artist.name} {artist.name}
</span>) : (artist.name)} </span>) : (artist.name)}
{i < clickableArtists.length - 1 && ", "} {i < clickableArtists.length - 1 && ", "}
+4 -2
View File
@@ -33,8 +33,10 @@ const CheckFilesExistence = (outputDir: string, rootDir: string, tracks: CheckFi
const SkipDownloadItem = (itemID: string, filePath: string): Promise<void> => (window as any)["go"]["main"]["App"]["SkipDownloadItem"](itemID, filePath); const SkipDownloadItem = (itemID: string, filePath: string): Promise<void> => (window as any)["go"]["main"]["App"]["SkipDownloadItem"](itemID, filePath);
const CreateM3U8File = (playlistName: string, outputDir: string, filePaths: string[]): Promise<void> => (window as any)["go"]["main"]["App"]["CreateM3U8File"](playlistName, outputDir, filePaths); const CreateM3U8File = (playlistName: string, outputDir: string, filePaths: string[]): Promise<void> => (window as any)["go"]["main"]["App"]["CreateM3U8File"](playlistName, outputDir, filePaths);
const GetTrackISRC = (spotifyId: string): Promise<string> => (window as any)["go"]["main"]["App"]["GetTrackISRC"](spotifyId); const GetTrackISRC = (spotifyId: string): Promise<string> => (window as any)["go"]["main"]["App"]["GetTrackISRC"](spotifyId);
async function resolveTemplateISRC(settings: {
async function resolveTemplateISRC(settings: { folderTemplate?: string; filenameTemplate?: string }, spotifyId?: string): Promise<string> { folderTemplate?: string;
filenameTemplate?: string;
}, spotifyId?: string): Promise<string> {
if (!spotifyId) { if (!spotifyId) {
return ""; return "";
} }
+4 -2
View File
@@ -6,8 +6,10 @@ import { joinPath, sanitizePath, getFirstArtist } from "@/lib/utils";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
import type { TrackMetadata } from "@/types/api"; import type { TrackMetadata } from "@/types/api";
const GetTrackISRC = (spotifyId: string): Promise<string> => (window as any)["go"]["main"]["App"]["GetTrackISRC"](spotifyId); const GetTrackISRC = (spotifyId: string): Promise<string> => (window as any)["go"]["main"]["App"]["GetTrackISRC"](spotifyId);
async function resolveTemplateISRC(settings: {
async function resolveTemplateISRC(settings: { folderTemplate?: string; filenameTemplate?: string }, spotifyId?: string): Promise<string> { folderTemplate?: string;
filenameTemplate?: string;
}, spotifyId?: string): Promise<string> {
if (!spotifyId) { if (!spotifyId) {
return ""; return "";
} }
-1
View File
@@ -32,7 +32,6 @@ let apiStatusState: ApiStatusState = {
}; };
let activeCheckAll: Promise<void> | null = null; let activeCheckAll: Promise<void> | null = null;
const listeners = new Set<() => void>(); const listeners = new Set<() => void>();
type SpotiFLACUnifiedStatusResponse = { type SpotiFLACUnifiedStatusResponse = {
tidal?: string; tidal?: string;
qobuz_a?: string; qobuz_a?: string;
-3
View File
@@ -1,11 +1,9 @@
import type { ArtistSimple } from "@/types/api"; import type { ArtistSimple } from "@/types/api";
export interface ClickableArtist { export interface ClickableArtist {
id: string; id: string;
name: string; name: string;
external_urls: string; external_urls: string;
} }
export function splitArtistNames(value: string): string[] { export function splitArtistNames(value: string): string[] {
const trimmed = value.trim(); const trimmed = value.trim();
if (!trimmed) { if (!trimmed) {
@@ -14,7 +12,6 @@ export function splitArtistNames(value: string): string[] {
const parts = trimmed.split(/\s*[;,]\s*/).map((part) => part.trim()).filter(Boolean); const parts = trimmed.split(/\s*[;,]\s*/).map((part) => part.trim()).filter(Boolean);
return parts.length > 0 ? parts : [trimmed]; return parts.length > 0 ? parts : [trimmed];
} }
export function buildClickableArtists(artists: string, artistsData?: ArtistSimple[], fallbackArtistId?: string, fallbackArtistUrl?: string): ClickableArtist[] { export function buildClickableArtists(artists: string, artistsData?: ArtistSimple[], fallbackArtistId?: string, fallbackArtistUrl?: string): ClickableArtist[] {
const names = splitArtistNames(artists); const names = splitArtistNames(artists);
if (names.length === 0) { if (names.length === 0) {