.status icons update
@@ -85,6 +85,42 @@ func containsStreamingURL(body []byte) bool {
|
|||||||
return isStreamingURL(trimmedBody)
|
return isStreamingURL(trimmedBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func containsLRCLIBResults(body []byte) bool {
|
||||||
|
trimmedBody := strings.TrimSpace(string(body))
|
||||||
|
if trimmedBody == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchResults []map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &searchResults); err == nil {
|
||||||
|
return len(searchResults) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var exactResult map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &exactResult); err == nil {
|
||||||
|
return len(exactResult) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsMusicBrainzResults(body []byte) bool {
|
||||||
|
trimmedBody := strings.TrimSpace(string(body))
|
||||||
|
if trimmedBody == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Recordings []json.RawMessage `json:"recordings"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload.Count > 0 || len(payload.Recordings) > 0
|
||||||
|
}
|
||||||
|
|
||||||
func isStreamingURL(raw string) bool {
|
func isStreamingURL(raw string) bool {
|
||||||
candidate := strings.TrimSpace(raw)
|
candidate := strings.TrimSpace(raw)
|
||||||
if candidate == "" {
|
if candidate == "" {
|
||||||
@@ -948,6 +984,10 @@ func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
|||||||
checkURL = fmt.Sprintf("%s/api/track/360735657?quality=27", apiURL)
|
checkURL = fmt.Sprintf("%s/api/track/360735657?quality=27", apiURL)
|
||||||
} else if apiType == "amazon" {
|
} else if apiType == "amazon" {
|
||||||
checkURL = fmt.Sprintf("%s/status", apiURL)
|
checkURL = fmt.Sprintf("%s/status", apiURL)
|
||||||
|
} else if apiType == "lrclib" {
|
||||||
|
checkURL = fmt.Sprintf("%s/api/search?artist_name=Adele&track_name=Hello", strings.TrimRight(apiURL, "/"))
|
||||||
|
} else if apiType == "musicbrainz" {
|
||||||
|
checkURL = fmt.Sprintf("%s/ws/2/recording?query=%s&fmt=json&limit=1", strings.TrimRight(apiURL, "/"), url.QueryEscape(`recording:"Hello" AND artist:"Adele"`))
|
||||||
} else {
|
} else {
|
||||||
checkURL = apiURL
|
checkURL = apiURL
|
||||||
}
|
}
|
||||||
@@ -958,6 +998,7 @@ func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
maxRetries := 3
|
maxRetries := 3
|
||||||
for i := 0; i < maxRetries; i++ {
|
for i := 0; i < maxRetries; i++ {
|
||||||
@@ -981,7 +1022,15 @@ func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if apiType != "amazon" && apiType != "qobuz" && apiType != "qbz" && statusCode == 200 {
|
if apiType == "lrclib" && statusCode == 200 && containsLRCLIBResults(body) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiType == "musicbrainz" && statusCode == 200 && containsMusicBrainzResults(body) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiType != "amazon" && apiType != "qobuz" && apiType != "qbz" && apiType != "lrclib" && apiType != "musicbrainz" && statusCode == 200 {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 345 B |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
@@ -1,6 +1,6 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RefreshCw, CheckCircle2, XCircle, Loader2 } from "lucide-react";
|
import { RefreshCw, CheckCircle2, XCircle, Loader2 } from "lucide-react";
|
||||||
import { TidalIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons";
|
import { TidalIcon, QobuzIcon, AmazonIcon, LrclibIcon, MusicBrainzIcon } from "./PlatformIcons";
|
||||||
import { useApiStatus } from "@/hooks/useApiStatus";
|
import { useApiStatus } from "@/hooks/useApiStatus";
|
||||||
export function ApiStatusTab() {
|
export function ApiStatusTab() {
|
||||||
const { sources, statuses, isCheckingAll, refreshAll } = useApiStatus();
|
const { sources, statuses, isCheckingAll, refreshAll } = useApiStatus();
|
||||||
@@ -12,12 +12,12 @@ export function ApiStatusTab() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
{sources.map((source) => {
|
{sources.map((source) => {
|
||||||
const status = statuses[source.id] || "idle";
|
const status = statuses[source.id] || "idle";
|
||||||
return (<div key={source.id} className="flex items-center justify-between p-4 border rounded-lg bg-card text-card-foreground shadow-sm">
|
return (<div key={source.id} className="flex items-center justify-between p-4 border rounded-lg bg-card text-card-foreground shadow-sm">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{source.type === "tidal" ? <TidalIcon className="w-5 h-5 shrink-0 text-muted-foreground"/> : source.type === "amazon" ? <AmazonIcon className="w-5 h-5 shrink-0 text-muted-foreground"/> : <QobuzIcon className="w-5 h-5 shrink-0 text-muted-foreground"/>}
|
{source.type === "tidal" ? <TidalIcon className="w-5 h-5 shrink-0 text-muted-foreground"/> : source.type === "amazon" ? <AmazonIcon className="w-5 h-5 shrink-0 text-muted-foreground"/> : source.type === "lrclib" ? <LrclibIcon className="w-5 h-5 shrink-0 text-muted-foreground"/> : source.type === "musicbrainz" ? <MusicBrainzIcon className="w-5 h-5 shrink-0 text-muted-foreground"/> : <QobuzIcon className="w-5 h-5 shrink-0 text-muted-foreground"/>}
|
||||||
<p className="font-medium leading-none">{source.name}</p>
|
<p className="font-medium leading-none">{source.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import amazonMusicIcon from "../assets/icons/amazon-music.png";
|
import amazonMusicIcon from "../assets/icons/amzn.png";
|
||||||
import qobuzIcon from "../assets/icons/qobuz.png";
|
import lrclibIcon from "../assets/icons/lrclib.png";
|
||||||
import tidalIcon from "../assets/icons/tidal.png";
|
import musicBrainzDarkIcon from "../assets/icons/musicbrainz_d.png";
|
||||||
const PLATFORM_ICON_URLS = {
|
import musicBrainzLightIcon from "../assets/icons/musicbrainz_l.png";
|
||||||
tidal: tidalIcon,
|
import qobuzIcon from "../assets/icons/qbz.png";
|
||||||
qobuz: qobuzIcon,
|
import songlinkDarkIcon from "../assets/icons/songlink_d.png";
|
||||||
amazon: amazonMusicIcon,
|
import songlinkLightIcon from "../assets/icons/songlink_l.png";
|
||||||
} as const;
|
import songstatsIcon from "../assets/icons/songstats.png";
|
||||||
|
import tidalDarkIcon from "../assets/icons/tidal_d.png";
|
||||||
|
import tidalLightIcon from "../assets/icons/tidal_l.png";
|
||||||
type PlatformIconProps = {
|
type PlatformIconProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
@@ -48,14 +50,48 @@ function PlatformIcon({ src, alt, className = "w-4 h-4", defaultClassName = "" }
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
return <img src={src} alt={alt} className={imageClassName} loading="lazy" referrerPolicy="no-referrer"/>;
|
return <img src={src} alt={alt} className={imageClassName} loading="lazy" referrerPolicy="no-referrer"/>;
|
||||||
}
|
}
|
||||||
|
function ThemedPlatformIcon({ lightSrc, darkSrc, alt, className = "w-4 h-4", defaultClassName = "" }: {
|
||||||
|
lightSrc: string;
|
||||||
|
darkSrc: string;
|
||||||
|
alt: string;
|
||||||
|
className?: string;
|
||||||
|
defaultClassName?: string;
|
||||||
|
}) {
|
||||||
|
const cleanedClassName = sanitizeClassName(className);
|
||||||
|
const statusClasses = getStatusClasses(className);
|
||||||
|
const wrapperClassName = [
|
||||||
|
cleanedClassName || "w-4 h-4",
|
||||||
|
"relative inline-flex shrink-0",
|
||||||
|
!hasRoundedClass(cleanedClassName) ? defaultClassName : "",
|
||||||
|
statusClasses,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
return <span role="img" aria-label={alt} className={wrapperClassName}>
|
||||||
|
<img src={lightSrc} alt="" aria-hidden="true" className="h-full w-full object-contain dark:hidden" loading="lazy" referrerPolicy="no-referrer"/>
|
||||||
|
<img src={darkSrc} alt="" aria-hidden="true" className="hidden h-full w-full object-contain dark:block" loading="lazy" referrerPolicy="no-referrer"/>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
export function TidalIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
export function TidalIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
return <PlatformIcon src={PLATFORM_ICON_URLS.tidal} alt="Tidal" className={className} defaultClassName="rounded-[4px]"/>;
|
return <ThemedPlatformIcon lightSrc={tidalLightIcon} darkSrc={tidalDarkIcon} alt="Tidal" className={className} defaultClassName="rounded-[4px]"/>;
|
||||||
}
|
}
|
||||||
export function QobuzIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
export function QobuzIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
return <PlatformIcon src={PLATFORM_ICON_URLS.qobuz} alt="Qobuz" className={className}/>;
|
return <PlatformIcon src={qobuzIcon} alt="Qobuz" className={className}/>;
|
||||||
}
|
}
|
||||||
export function AmazonIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
export function AmazonIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
return <PlatformIcon src={PLATFORM_ICON_URLS.amazon} alt="Amazon Music" className={className} defaultClassName="rounded-[4px]"/>;
|
return <PlatformIcon src={amazonMusicIcon} alt="Amazon Music" className={className} defaultClassName="rounded-[4px]"/>;
|
||||||
|
}
|
||||||
|
export function LrclibIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
|
return <PlatformIcon src={lrclibIcon} alt="LRCLIB" className={className}/>;
|
||||||
|
}
|
||||||
|
export function MusicBrainzIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
|
return <ThemedPlatformIcon lightSrc={musicBrainzLightIcon} darkSrc={musicBrainzDarkIcon} alt="MusicBrainz" className={className}/>;
|
||||||
|
}
|
||||||
|
export function SonglinkIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
|
return <ThemedPlatformIcon lightSrc={songlinkLightIcon} darkSrc={songlinkDarkIcon} alt="Songlink" className={className} defaultClassName="rounded-[3px]"/>;
|
||||||
|
}
|
||||||
|
export function SongstatsIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
|
return <PlatformIcon src={songstatsIcon} alt="Songstats" className={className} defaultClassName="rounded-[3px]"/>;
|
||||||
}
|
}
|
||||||
export function TidalAvailabilityIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
export function TidalAvailabilityIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
return <svg viewBox="0 0 24 24" className={`${className} fill-current`}>
|
return <svg viewBox="0 0 24 24" className={`${className} fill-current`}>
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import { themes, applyTheme } from "@/lib/themes";
|
|||||||
import { SelectFolder, OpenConfigFolder } from "../../wailsjs/go/main/App";
|
import { SelectFolder, OpenConfigFolder } from "../../wailsjs/go/main/App";
|
||||||
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
||||||
import { ApiStatusTab } from "./ApiStatusTab";
|
import { ApiStatusTab } from "./ApiStatusTab";
|
||||||
import { AmazonIcon, QobuzIcon, TidalIcon } from "./PlatformIcons";
|
import { AmazonIcon, QobuzIcon, SonglinkIcon, SongstatsIcon, TidalIcon } from "./PlatformIcons";
|
||||||
import songlinkIcon from "@/assets/icons/songlink.ico";
|
|
||||||
import songstatsIcon from "@/assets/icons/songstats.png";
|
|
||||||
interface SettingsPageProps {
|
interface SettingsPageProps {
|
||||||
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
||||||
onResetRequest?: (resetFn: () => void) => void;
|
onResetRequest?: (resetFn: () => void) => void;
|
||||||
@@ -245,13 +243,13 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="songlink">
|
<SelectItem value="songlink">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<img src={songlinkIcon} alt="Songlink" className="h-4 w-4 shrink-0 rounded-[3px] object-contain" loading="lazy"/>
|
<SonglinkIcon className="h-4 w-4 shrink-0"/>
|
||||||
Songlink
|
Songlink
|
||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="songstats">
|
<SelectItem value="songstats">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<img src={songstatsIcon} alt="Songstats" className="h-4 w-4 shrink-0 rounded-[3px] object-contain" loading="lazy"/>
|
<SongstatsIcon className="h-4 w-4 shrink-0"/>
|
||||||
Songstats
|
Songstats
|
||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export const API_SOURCES: ApiSource[] = [
|
|||||||
{ id: "qobuz2", type: "qobuz", name: "Qobuz B", url: "https://dabmusic.xyz" },
|
{ id: "qobuz2", type: "qobuz", name: "Qobuz B", url: "https://dabmusic.xyz" },
|
||||||
{ id: "qobuz3", type: "qbz", name: "Qobuz C", url: "https://qbz.afkarxyz.qzz.io" },
|
{ id: "qobuz3", type: "qbz", name: "Qobuz C", url: "https://qbz.afkarxyz.qzz.io" },
|
||||||
{ id: "amazon1", type: "amazon", name: "Amazon Music", url: "https://amzn.afkarxyz.qzz.io" },
|
{ id: "amazon1", type: "amazon", name: "Amazon Music", url: "https://amzn.afkarxyz.qzz.io" },
|
||||||
|
{ id: "lrclib", type: "lrclib", name: "LRCLIB", url: "https://lrclib.net" },
|
||||||
|
{ id: "musicbrainz", type: "musicbrainz", name: "MusicBrainz", url: "https://musicbrainz.org" },
|
||||||
];
|
];
|
||||||
type ApiStatusState = {
|
type ApiStatusState = {
|
||||||
isCheckingAll: boolean;
|
isCheckingAll: boolean;
|
||||||
|
|||||||