.refine check availibility

This commit is contained in:
afkarxyz
2026-04-13 20:43:37 +07:00
parent 967feb93e1
commit 24d640443a
5 changed files with 192 additions and 25 deletions
+83 -4
View File
@@ -47,6 +47,16 @@ type songLinkAPIResponse struct {
} `json:"linksByPlatform"`
}
type qobuzAvailabilityTrack struct {
ID int64 `json:"id"`
Album struct {
ID string `json:"id"`
Title string `json:"title"`
URL string `json:"url"`
RelativeURL string `json:"relative_url"`
} `json:"album"`
}
func NewSongLinkClient() *SongLinkClient {
return &SongLinkClient{
client: &http.Client{
@@ -114,7 +124,7 @@ func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string) (*TrackAv
}
if isrc != "" {
availability.Qobuz = checkQobuzAvailability(isrc)
availability.Qobuz, availability.QobuzURL = checkQobuzAvailability(isrc)
}
if availability.Tidal || availability.Amazon || availability.Deezer || availability.Qobuz {
@@ -128,10 +138,63 @@ func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string) (*TrackAv
return availability, fmt.Errorf("no platforms found")
}
func checkQobuzAvailability(isrc string) bool {
func qobuzNormalizeRelativeURL(rawURL string) string {
rawURL = strings.TrimSpace(rawURL)
if rawURL == "" {
return ""
}
if strings.HasPrefix(rawURL, "http://") || strings.HasPrefix(rawURL, "https://") {
return rawURL
}
if strings.HasPrefix(rawURL, "/") {
return "https://www.qobuz.com" + rawURL
}
return "https://www.qobuz.com/" + rawURL
}
func qobuzSlugifySegment(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" {
return ""
}
var builder strings.Builder
lastDash := false
for _, r := range value {
switch {
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
builder.WriteRune(r)
lastDash = false
default:
if !lastDash {
builder.WriteByte('-')
lastDash = true
}
}
}
return strings.Trim(builder.String(), "-")
}
func qobuzAlbumSlugURL(albumTitle string, albumID string) string {
albumID = strings.TrimSpace(albumID)
if albumID == "" {
return ""
}
slug := qobuzSlugifySegment(albumTitle)
if slug == "" {
return fmt.Sprintf("https://www.qobuz.com/album/%s", albumID)
}
return fmt.Sprintf("https://www.qobuz.com/album/%s/%s", slug, albumID)
}
func checkQobuzAvailability(isrc string) (bool, string) {
var searchResp struct {
Tracks struct {
Total int `json:"total"`
Items []qobuzAvailabilityTrack `json:"items"`
} `json:"tracks"`
}
@@ -139,10 +202,26 @@ func checkQobuzAvailability(isrc string) bool {
"query": {strings.TrimSpace(isrc)},
"limit": {"1"},
}, &searchResp); err != nil {
return false
return false, ""
}
return searchResp.Tracks.Total > 0
if searchResp.Tracks.Total == 0 || len(searchResp.Tracks.Items) == 0 {
return false, ""
}
item := searchResp.Tracks.Items[0]
qobuzURL := strings.TrimSpace(item.Album.URL)
if qobuzURL == "" {
qobuzURL = qobuzNormalizeRelativeURL(item.Album.RelativeURL)
}
if qobuzURL == "" {
qobuzURL = qobuzAlbumSlugURL(item.Album.Title, item.Album.ID)
}
if qobuzURL == "" && item.ID > 0 {
qobuzURL = fmt.Sprintf("https://www.qobuz.com/us-en/track/%d", item.ID)
}
return true, qobuzURL
}
func (s *SongLinkClient) GetDeezerURLFromSpotify(spotifyTrackID string) (string, error) {
@@ -0,0 +1,83 @@
import type { ReactNode } from "react";
import type { TrackAvailability } from "@/types/api";
import { openExternal } from "@/lib/utils";
import { AmazonAvailabilityIcon, QobuzAvailabilityIcon, TidalAvailabilityIcon } from "./PlatformIcons";
interface AvailabilityLinkEntry {
id: string;
found: boolean;
url?: string;
icon: ReactNode;
}
function getAvailabilityLinkEntries(availability: TrackAvailability): AvailabilityLinkEntry[] {
const tidalUrl = availability.tidal_url?.trim() || "";
const qobuzUrl = availability.qobuz_url?.trim() || "";
const amazonUrl = availability.amazon_url?.trim() || "";
return [
{
id: "tidal",
found: tidalUrl !== "",
url: tidalUrl,
icon: <TidalAvailabilityIcon className={`w-4 h-4 shrink-0 ${tidalUrl ? "text-green-500" : "text-red-500"}`}/>,
},
{
id: "qobuz",
found: qobuzUrl !== "",
url: qobuzUrl,
icon: <QobuzAvailabilityIcon className={`w-4 h-4 shrink-0 ${qobuzUrl ? "text-green-500" : "text-red-500"}`}/>,
},
{
id: "amazon",
found: amazonUrl !== "",
url: amazonUrl,
icon: <AmazonAvailabilityIcon className={`w-4 h-4 shrink-0 ${amazonUrl ? "text-green-500" : "text-red-500"}`}/>,
},
];
}
export function hasAvailabilityLinks(availability?: TrackAvailability): boolean {
if (!availability) {
return false;
}
return getAvailabilityLinkEntries(availability).some((entry) => entry.found);
}
export function AvailabilityLinks({ availability }: {
availability?: TrackAvailability;
}) {
if (!availability) {
return <p>Check Availability</p>;
}
const entries = getAvailabilityLinkEntries(availability);
return (
<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}
>
{entry.icon}
<span className="truncate whitespace-nowrap leading-5 min-w-0">
{entry.url}
</span>
</button>
) : (
<div
key={entry.id}
className="flex items-center gap-2 text-left text-xs min-w-0"
>
{entry.icon}
<span className="truncate whitespace-nowrap leading-5 min-w-0 text-red-500">
Not Found
</span>
</div>
))}
</div>
);
}
+4 -8
View File
@@ -4,8 +4,8 @@ import { Download, FolderOpen, CheckCircle, XCircle, FileText, FileCheck, Globe,
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip";
import type { TrackMetadata, TrackAvailability } from "@/types/api";
import { TidalAvailabilityIcon, QobuzAvailabilityIcon, AmazonAvailabilityIcon } from "./PlatformIcons";
import { usePreview } from "@/hooks/usePreview";
import { AvailabilityLinks, hasAvailabilityLinks } from "./AvailabilityLinks";
interface TrackInfoProps {
track: TrackMetadata & {
album_name: string;
@@ -135,15 +135,11 @@ export function TrackInfo({ track, isDownloading, downloadingTrack, isDownloaded
{track.spotify_id && onCheckAvailability && (<Tooltip>
<TooltipTrigger asChild>
<Button onClick={() => onCheckAvailability(track.spotify_id!)} variant="outline" size="icon" disabled={checkingAvailability}>
{checkingAvailability ? (<Spinner />) : availability ? (<CheckCircle className="h-4 w-4 text-green-500"/>) : (<Globe className="h-4 w-4"/>)}
{checkingAvailability ? (<Spinner />) : availability ? (hasAvailabilityLinks(availability) ? (<CheckCircle className="h-4 w-4 text-green-500"/>) : (<XCircle className="h-4 w-4 text-red-500"/>)) : (<Globe className="h-4 w-4"/>)}
</Button>
</TooltipTrigger>
<TooltipContent>
{availability ? (<div className="flex items-center gap-2">
<TidalAvailabilityIcon className={`w-4 h-4 ${availability.tidal ? "text-green-500" : "text-red-500"}`}/>
<QobuzAvailabilityIcon className={`w-4 h-4 ${availability.qobuz ? "text-green-500" : "text-red-500"}`}/>
<AmazonAvailabilityIcon className={`w-4 h-4 ${availability.amazon ? "text-green-500" : "text-red-500"}`}/>
</div>) : (<p>Check Availability</p>)}
<TooltipContent className="pointer-events-auto">
<AvailabilityLinks availability={availability}/>
</TooltipContent>
</Tooltip>)}
{isDownloaded && (<Tooltip>
+20 -8
View File
@@ -5,8 +5,8 @@ import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip";
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination";
import type { TrackMetadata, TrackAvailability } from "@/types/api";
import { TidalAvailabilityIcon, QobuzAvailabilityIcon, AmazonAvailabilityIcon } from "./PlatformIcons";
import { usePreview } from "@/hooks/usePreview";
import { AvailabilityLinks, hasAvailabilityLinks } from "./AvailabilityLinks";
interface TrackListProps {
tracks: TrackMetadata[];
searchQuery: string;
@@ -172,6 +172,22 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
return plays;
return num.toLocaleString();
};
const getAvailabilityButtonIcon = (spotifyId?: string) => {
if (!spotifyId) {
return <Globe className="h-4 w-4"/>;
}
if (checkingAvailabilityTrack === spotifyId) {
return <Spinner />;
}
const availability = availabilityMap?.get(spotifyId);
if (!availability) {
return <Globe className="h-4 w-4"/>;
}
if (hasAvailabilityLinks(availability)) {
return <CheckCircle className="h-4 w-4 text-green-500"/>;
}
return <XCircle className="h-4 w-4 text-red-500"/>;
};
return (<div className="space-y-4">
<div className="rounded-md border">
<div className="overflow-x-auto">
@@ -323,15 +339,11 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
{track.spotify_id && onCheckAvailability && (<Tooltip>
<TooltipTrigger asChild>
<Button onClick={() => onCheckAvailability(track.spotify_id!)} size="icon" variant="outline" disabled={checkingAvailabilityTrack === track.spotify_id}>
{checkingAvailabilityTrack === track.spotify_id ? (<Spinner />) : availabilityMap?.has(track.spotify_id) ? (<CheckCircle className="h-4 w-4 text-green-500"/>) : (<Globe className="h-4 w-4"/>)}
{getAvailabilityButtonIcon(track.spotify_id)}
</Button>
</TooltipTrigger>
<TooltipContent>
{availabilityMap?.has(track.spotify_id) ? (<div className="flex items-center gap-2">
<TidalAvailabilityIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.tidal ? "text-green-500" : "text-red-500"}`}/>
<QobuzAvailabilityIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.qobuz ? "text-green-500" : "text-red-500"}`}/>
<AmazonAvailabilityIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.amazon ? "text-green-500" : "text-red-500"}`}/>
</div>) : (<p>Check Availability</p>)}
<TooltipContent className="pointer-events-auto">
<AvailabilityLinks availability={track.spotify_id ? availabilityMap?.get(track.spotify_id) : undefined}/>
</TooltipContent>
</Tooltip>)}
</div>
+1 -4
View File
@@ -13,9 +13,6 @@ export function useAvailability() {
setError("No Spotify ID provided");
return null;
}
if (availabilityMap.has(spotifyId)) {
return availabilityMap.get(spotifyId)!;
}
setChecking(true);
setCheckingTrackId(spotifyId);
setError(null);
@@ -41,7 +38,7 @@ export function useAvailability() {
setChecking(false);
setCheckingTrackId(null);
}
}, [availabilityMap]);
}, []);
const getAvailability = useCallback((spotifyId: string) => {
return availabilityMap.get(spotifyId);
}, [availabilityMap]);