.refine check availibility
This commit is contained in:
+84
-5
@@ -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"`
|
||||
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 +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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user