.cleanup
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"}>
|
||||||
|
|||||||
@@ -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 && ", "}
|
||||||
|
|||||||
@@ -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 && ", "}
|
||||||
|
|||||||
@@ -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 "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user