.clickable variables
This commit is contained in:
@@ -414,7 +414,12 @@ function App() {
|
||||
if ("track" in metadata.metadata) {
|
||||
const { track } = metadata.metadata;
|
||||
const trackId = track.spotify_id || "";
|
||||
return (<TrackInfo track={track} isDownloading={download.isDownloading} downloadingTrack={download.downloadingTrack} isDownloaded={download.downloadedTracks.has(trackId)} isFailed={download.failedTracks.has(trackId)} isSkipped={download.skippedTracks.has(trackId)} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} downloadedLyrics={lyrics.downloadedLyrics.has(track.spotify_id || "")} failedLyrics={lyrics.failedLyrics.has(track.spotify_id || "")} skippedLyrics={lyrics.skippedLyrics.has(track.spotify_id || "")} checkingAvailability={availability.checkingTrackId === track.spotify_id} availability={availability.availabilityMap.get(track.spotify_id || "")} downloadingCover={cover.downloadingCoverTrack === (track.spotify_id || `${track.name}-${track.artists}`)} downloadedCover={cover.downloadedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} failedCover={cover.failedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} skippedCover={cover.skippedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} onDownload={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, albumArtist, releaseDate, discNumber) => lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, undefined, undefined, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, undefined, undefined, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onOpenFolder={handleOpenFolder} onBack={metadata.resetMetadata}/>);
|
||||
return (<TrackInfo track={track} isDownloading={download.isDownloading} downloadingTrack={download.downloadingTrack} isDownloaded={download.downloadedTracks.has(trackId)} isFailed={download.failedTracks.has(trackId)} isSkipped={download.skippedTracks.has(trackId)} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} downloadedLyrics={lyrics.downloadedLyrics.has(track.spotify_id || "")} failedLyrics={lyrics.failedLyrics.has(track.spotify_id || "")} skippedLyrics={lyrics.skippedLyrics.has(track.spotify_id || "")} checkingAvailability={availability.checkingTrackId === track.spotify_id} availability={availability.availabilityMap.get(track.spotify_id || "")} downloadingCover={cover.downloadingCoverTrack === (track.spotify_id || `${track.name}-${track.artists}`)} downloadedCover={cover.downloadedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} failedCover={cover.failedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} skippedCover={cover.skippedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} onDownload={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, albumArtist, releaseDate, discNumber) => lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, undefined, undefined, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, undefined, undefined, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onOpenFolder={handleOpenFolder} onAlbumClick={metadata.handleAlbumClick} onArtistClick={async (artist) => {
|
||||
const artistUrl = await metadata.handleArtistClick(artist);
|
||||
if (artistUrl) {
|
||||
setSpotifyUrl(artistUrl);
|
||||
}
|
||||
}} onBack={metadata.resetMetadata}/>);
|
||||
}
|
||||
if ("album_info" in metadata.metadata) {
|
||||
const { album_info, track_list } = metadata.metadata;
|
||||
|
||||
@@ -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 { buildClickableArtists, splitArtistNames } from "@/lib/artist-links";
|
||||
import type { TrackMetadata, TrackAvailability } from "@/types/api";
|
||||
interface AlbumInfoProps {
|
||||
albumInfo: {
|
||||
@@ -77,6 +78,47 @@ interface AlbumInfoProps {
|
||||
}
|
||||
export function AlbumInfo({ albumInfo, 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, onArtistClick, onTrackClick, onBack, }: AlbumInfoProps) {
|
||||
const settings = getSettings();
|
||||
const albumArtistNames = splitArtistNames(albumInfo.artists);
|
||||
const artistSeparator = albumInfo.artists.includes(";") ? "; " : ", ";
|
||||
const clickableAlbumArtists = (() => {
|
||||
const artistsByName = new Map<string, {
|
||||
id: string;
|
||||
name: string;
|
||||
external_urls: string;
|
||||
}>();
|
||||
for (const track of trackList) {
|
||||
const clickableTrackArtists = buildClickableArtists(track.artists, track.artists_data, track.artist_id, track.artist_url);
|
||||
for (const artist of clickableTrackArtists) {
|
||||
const normalizedName = artist.name.trim().toLowerCase();
|
||||
if (!normalizedName || !artist.external_urls || artistsByName.has(normalizedName)) {
|
||||
continue;
|
||||
}
|
||||
artistsByName.set(normalizedName, artist);
|
||||
}
|
||||
}
|
||||
return albumArtistNames.map((name) => {
|
||||
const normalizedName = name.trim().toLowerCase();
|
||||
const matchedArtist = artistsByName.get(normalizedName);
|
||||
if (matchedArtist) {
|
||||
return {
|
||||
...matchedArtist,
|
||||
name,
|
||||
};
|
||||
}
|
||||
if (albumArtistNames.length === 1 && albumInfo.artist_id && albumInfo.artist_url) {
|
||||
return {
|
||||
id: albumInfo.artist_id,
|
||||
name,
|
||||
external_urls: albumInfo.artist_url,
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: "",
|
||||
name,
|
||||
external_urls: "",
|
||||
};
|
||||
});
|
||||
})();
|
||||
const [downloadingAlbumCover, setDownloadingAlbumCover] = useState(false);
|
||||
const handleDownloadAlbumCover = async () => {
|
||||
if (!albumInfo.images)
|
||||
@@ -162,13 +204,18 @@ export function AlbumInfo({ albumInfo, trackList, searchQuery, sortBy, selectedT
|
||||
<p className="text-sm font-medium">Album</p>
|
||||
<h2 className="text-4xl font-bold">{albumInfo.name}</h2>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
{onArtistClick && albumInfo.artist_id && albumInfo.artist_url ? (<span className="font-medium cursor-pointer hover:underline" onClick={() => onArtistClick({
|
||||
id: albumInfo.artist_id!,
|
||||
name: albumInfo.artists,
|
||||
external_urls: albumInfo.artist_url!,
|
||||
<span className="font-medium">
|
||||
{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({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
external_urls: artist.external_urls,
|
||||
})}>
|
||||
{albumInfo.artists}
|
||||
</span>) : (<span className="font-medium">{albumInfo.artists}</span>)}
|
||||
{artist.name}
|
||||
</span>) : (artist.name)}
|
||||
{index < clickableAlbumArtists.length - 1 && artistSeparator}
|
||||
</span>)) : albumInfo.artists}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>{albumInfo.release_date}</span>
|
||||
<span>•</span>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/toolti
|
||||
import type { TrackMetadata, TrackAvailability } from "@/types/api";
|
||||
import { usePreview } from "@/hooks/usePreview";
|
||||
import { AvailabilityLinks, hasAvailabilityLinks } from "./AvailabilityLinks";
|
||||
import { buildClickableArtists } from "@/lib/artist-links";
|
||||
interface TrackInfoProps {
|
||||
track: TrackMetadata & {
|
||||
album_name: string;
|
||||
@@ -31,10 +32,22 @@ interface TrackInfoProps {
|
||||
onCheckAvailability?: (spotifyId: string) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName?: string, playlistName?: string, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onOpenFolder: () => void;
|
||||
onAlbumClick?: (album: {
|
||||
id: string;
|
||||
name: string;
|
||||
external_urls: string;
|
||||
}) => void;
|
||||
onArtistClick?: (artist: {
|
||||
id: string;
|
||||
name: string;
|
||||
external_urls: string;
|
||||
}) => void;
|
||||
onBack?: () => void;
|
||||
}
|
||||
export function TrackInfo({ track, isDownloading, downloadingTrack, isDownloaded, isFailed, isSkipped, downloadingLyricsTrack, downloadedLyrics, failedLyrics, skippedLyrics, checkingAvailability, availability, downloadingCover, downloadedCover, failedCover, skippedCover, onDownload, onDownloadLyrics, onCheckAvailability, onDownloadCover, onOpenFolder, onBack, }: TrackInfoProps) {
|
||||
export function TrackInfo({ track, isDownloading, downloadingTrack, isDownloaded, isFailed, isSkipped, downloadingLyricsTrack, downloadedLyrics, failedLyrics, skippedLyrics, checkingAvailability, availability, downloadingCover, downloadedCover, failedCover, skippedCover, onDownload, onDownloadLyrics, onCheckAvailability, onDownloadCover, onOpenFolder, onAlbumClick, onArtistClick, onBack, }: TrackInfoProps) {
|
||||
const { playPreview, loadingPreview, playingTrack } = usePreview();
|
||||
const hasAlbumClick = !!(onAlbumClick && track.album_id && track.album_url);
|
||||
const clickableArtists = buildClickableArtists(track.artists, track.artists_data, track.artist_id, track.artist_url);
|
||||
const formatDuration = (ms: number) => {
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds = Math.floor((ms % 60000) / 1000);
|
||||
@@ -69,13 +82,30 @@ export function TrackInfo({ track, isDownloading, downloadingTrack, isDownloaded
|
||||
{track.is_explicit && (<span className="inline-flex items-center justify-center bg-red-600 text-white text-[10px] h-4 w-4 rounded shrink-0" title="Explicit">E</span>)}
|
||||
{isSkipped ? (<FileCheck className="h-6 w-6 text-yellow-500 shrink-0"/>) : isDownloaded ? (<CheckCircle className="h-6 w-6 text-green-500 shrink-0"/>) : isFailed ? (<XCircle className="h-6 w-6 text-red-500 shrink-0"/>) : null}
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground">{track.artists}</p>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
{clickableArtists.length > 0 ? clickableArtists.map((artist, index) => (<span key={`${artist.id || artist.name}-${index}`}>
|
||||
{onArtistClick ? (<span className="cursor-pointer hover:underline" onClick={() => onArtistClick({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
external_urls: artist.external_urls,
|
||||
})}>
|
||||
{artist.name}
|
||||
</span>) : (artist.name)}
|
||||
{index < clickableArtists.length - 1 && ", "}
|
||||
</span>)) : track.artists}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Album</p>
|
||||
<p className="font-medium truncate">{track.album_name}</p>
|
||||
<p className="font-medium truncate">{hasAlbumClick ? (<span className="cursor-pointer hover:underline" onClick={() => onAlbumClick?.({
|
||||
id: track.album_id!,
|
||||
name: track.album_name,
|
||||
external_urls: track.album_url!,
|
||||
})}>
|
||||
{track.album_name}
|
||||
</span>) : (track.album_name)}</p>
|
||||
</div>
|
||||
{track.plays && (<div>
|
||||
<p className="text-xs text-muted-foreground">Total Plays</p>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, Pagi
|
||||
import type { TrackMetadata, TrackAvailability } from "@/types/api";
|
||||
import { usePreview } from "@/hooks/usePreview";
|
||||
import { AvailabilityLinks, hasAvailabilityLinks } from "./AvailabilityLinks";
|
||||
import { buildClickableArtists } from "@/lib/artist-links";
|
||||
interface TrackListProps {
|
||||
tracks: TrackMetadata[];
|
||||
searchQuery: string;
|
||||
@@ -249,29 +250,22 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
||||
{track.spotify_id && skippedTracks.has(track.spotify_id) ? (<FileCheck className="h-4 w-4 text-yellow-500 shrink-0"/>) : track.spotify_id && downloadedTracks.has(track.spotify_id) ? (<CheckCircle className="h-4 w-4 text-green-500 shrink-0"/>) : track.spotify_id && failedTracks.has(track.spotify_id) ? (<XCircle className="h-4 w-4 text-red-500 shrink-0"/>) : null}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{track.artists_data && track.artists_data.length > 0 ? ((() => {
|
||||
const artistNames = track.artists.split(", ").map(name => name.trim());
|
||||
return artistNames.map((name, i) => {
|
||||
const artistData = track.artists_data![i];
|
||||
const hasArtistData = artistData && artistData.id && artistData.external_urls;
|
||||
return (<span key={artistData?.id || i}>
|
||||
{onArtistClick && hasArtistData ? (<span className="cursor-pointer hover:underline" onClick={() => onArtistClick({
|
||||
id: artistData.id,
|
||||
name: name,
|
||||
external_urls: artistData.external_urls,
|
||||
{(() => {
|
||||
const clickableArtists = buildClickableArtists(track.artists, track.artists_data, track.artist_id, track.artist_url);
|
||||
if (clickableArtists.length === 0) {
|
||||
return track.artists;
|
||||
}
|
||||
return clickableArtists.map((artist, i) => (<span key={`${artist.id || artist.name}-${i}`}>
|
||||
{onArtistClick ? (<span className="cursor-pointer hover:underline" onClick={() => onArtistClick({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
external_urls: artist.external_urls,
|
||||
})}>
|
||||
{name}
|
||||
</span>) : (name)}
|
||||
{i < artistNames.length - 1 && ", "}
|
||||
</span>);
|
||||
});
|
||||
})()) : onArtistClick && track.artist_id && track.artist_url ? (<span className="cursor-pointer hover:underline" onClick={() => onArtistClick({
|
||||
id: track.artist_id!,
|
||||
name: track.artists,
|
||||
external_urls: track.artist_url!,
|
||||
})}>
|
||||
{track.artists}
|
||||
</span>) : (track.artists)}
|
||||
{artist.name}
|
||||
</span>) : (artist.name)}
|
||||
{i < clickableArtists.length - 1 && ", "}
|
||||
</span>));
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getSettings } from "@/lib/settings";
|
||||
import { fetchSpotifyMetadata } from "@/lib/api";
|
||||
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { AddFetchHistory } from "../../wailsjs/go/main/App";
|
||||
import { AddFetchHistory, SearchSpotifyByType } from "../../wailsjs/go/main/App";
|
||||
import { EventsOff, EventsOn } from "../../wailsjs/runtime/runtime";
|
||||
import type { SpotifyMetadataResponse } from "@/types/api";
|
||||
export function useMetadata() {
|
||||
@@ -20,6 +20,19 @@ export function useMetadata() {
|
||||
external_urls: string;
|
||||
} | null>(null);
|
||||
const [pendingArtistName, setPendingArtistName] = useState<string | null>(null);
|
||||
const resolveArtistUrlBySearch = async (artistName: string): Promise<string | null> => {
|
||||
const query = artistName.trim();
|
||||
if (!query) {
|
||||
return null;
|
||||
}
|
||||
const results = await SearchSpotifyByType({
|
||||
query,
|
||||
search_type: "artist",
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
});
|
||||
return results[0]?.external_urls || null;
|
||||
};
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
fetchedCount.current = 0;
|
||||
@@ -262,10 +275,17 @@ export function useMetadata() {
|
||||
external_urls: string;
|
||||
}) => {
|
||||
logger.debug(`artist clicked: ${artist.name}`);
|
||||
const artistUrl = artist.external_urls.replace(/\/$/, "") + "/discography/all";
|
||||
const resolvedArtistUrl = artist.external_urls.trim() || (await resolveArtistUrlBySearch(artist.name)) || "";
|
||||
if (!resolvedArtistUrl) {
|
||||
toast.error(`Artist not found: ${artist.name}`);
|
||||
return "";
|
||||
}
|
||||
const artistUrl = resolvedArtistUrl.includes("/discography")
|
||||
? resolvedArtistUrl
|
||||
: resolvedArtistUrl.replace(/\/$/, "") + "/discography/all";
|
||||
setPendingArtistName(artist.name);
|
||||
await fetchMetadataDirectly(artistUrl);
|
||||
return artistUrl;
|
||||
return resolvedArtistUrl;
|
||||
};
|
||||
const handleConfirmAlbumFetch = async () => {
|
||||
if (!selectedAlbum)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ArtistSimple } from "@/types/api";
|
||||
|
||||
export interface ClickableArtist {
|
||||
id: string;
|
||||
name: string;
|
||||
external_urls: string;
|
||||
}
|
||||
|
||||
export function splitArtistNames(value: string): string[] {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return [];
|
||||
}
|
||||
const parts = trimmed.split(/\s*[;,]\s*/).map((part) => part.trim()).filter(Boolean);
|
||||
return parts.length > 0 ? parts : [trimmed];
|
||||
}
|
||||
|
||||
export function buildClickableArtists(artists: string, artistsData?: ArtistSimple[], fallbackArtistId?: string, fallbackArtistUrl?: string): ClickableArtist[] {
|
||||
const names = splitArtistNames(artists);
|
||||
if (names.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return names.map((name, index) => {
|
||||
const artistData = artistsData?.[index];
|
||||
if (artistData && (artistData.id || artistData.external_urls)) {
|
||||
return {
|
||||
id: artistData.id || "",
|
||||
name,
|
||||
external_urls: artistData.external_urls || "",
|
||||
};
|
||||
}
|
||||
if (names.length === 1) {
|
||||
return {
|
||||
id: fallbackArtistId || "",
|
||||
name,
|
||||
external_urls: fallbackArtistUrl || "",
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: "",
|
||||
name,
|
||||
external_urls: "",
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user