.cleanup
This commit is contained in:
@@ -18,4 +18,3 @@ func NewRequestWithDefaultHeaders(method string, rawURL string, body io.Reader)
|
|||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { SearchCheck, CheckCircle2, XCircle, Loader2 } from "lucide-react";
|
|||||||
import { TidalIcon, QobuzIcon, AmazonIcon, MusicBrainzIcon, AppleMusicIcon, DeezerIcon } from "./PlatformIcons";
|
import { TidalIcon, QobuzIcon, AmazonIcon, MusicBrainzIcon, AppleMusicIcon, DeezerIcon } from "./PlatformIcons";
|
||||||
import { useApiStatus } from "@/hooks/useApiStatus";
|
import { useApiStatus } from "@/hooks/useApiStatus";
|
||||||
import { SPOTIFLAC_NEXT_SOURCES } from "@/lib/api-status";
|
import { SPOTIFLAC_NEXT_SOURCES } from "@/lib/api-status";
|
||||||
|
|
||||||
function renderStatusIcon(status: "checking" | "online" | "offline" | "idle") {
|
function renderStatusIcon(status: "checking" | "online" | "offline" | "idle") {
|
||||||
if (status === "online") {
|
if (status === "online") {
|
||||||
return <CheckCircle2 className="h-5 w-5 text-emerald-500"/>;
|
return <CheckCircle2 className="h-5 w-5 text-emerald-500"/>;
|
||||||
@@ -13,7 +12,6 @@ function renderStatusIcon(status: "checking" | "online" | "offline" | "idle") {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPlatformIcon(type: string) {
|
function renderPlatformIcon(type: string) {
|
||||||
if (type === "tidal") {
|
if (type === "tidal") {
|
||||||
return <TidalIcon className="w-5 h-5 shrink-0 text-muted-foreground"/>;
|
return <TidalIcon className="w-5 h-5 shrink-0 text-muted-foreground"/>;
|
||||||
@@ -32,7 +30,6 @@ function renderPlatformIcon(type: string) {
|
|||||||
}
|
}
|
||||||
return <QobuzIcon className="w-5 h-5 shrink-0 text-muted-foreground"/>;
|
return <QobuzIcon className="w-5 h-5 shrink-0 text-muted-foreground"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ApiStatusTab() {
|
export function ApiStatusTab() {
|
||||||
const { sources, statuses, nextStatuses, checkingSources, checkOne } = useApiStatus();
|
const { sources, statuses, nextStatuses, checkingSources, checkOne } = useApiStatus();
|
||||||
return (<div className="space-y-6">
|
return (<div className="space-y-6">
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import { CheckAPIStatus } from "../../wailsjs/go/main/App";
|
import { CheckAPIStatus } from "../../wailsjs/go/main/App";
|
||||||
import { CHECK_TIMEOUT_MS, withTimeout } from "@/lib/async-timeout";
|
import { CHECK_TIMEOUT_MS, withTimeout } from "@/lib/async-timeout";
|
||||||
|
|
||||||
export type ApiCheckStatus = "checking" | "online" | "offline" | "idle";
|
export type ApiCheckStatus = "checking" | "online" | "offline" | "idle";
|
||||||
|
|
||||||
export interface ApiSource {
|
export interface ApiSource {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpotiFLACNextSource {
|
interface SpotiFLACNextSource {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpotiFLACNextStatusResponse = {
|
type SpotiFLACNextStatusResponse = {
|
||||||
tidal?: string;
|
tidal?: string;
|
||||||
qobuz_a?: string;
|
qobuz_a?: string;
|
||||||
@@ -27,14 +23,12 @@ type SpotiFLACNextStatusResponse = {
|
|||||||
amazon_c?: string;
|
amazon_c?: string;
|
||||||
apple?: string;
|
apple?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const API_SOURCES: ApiSource[] = [
|
export const API_SOURCES: ApiSource[] = [
|
||||||
{ id: "tidal", type: "tidal", name: "Tidal", url: "" },
|
{ id: "tidal", type: "tidal", name: "Tidal", url: "" },
|
||||||
{ id: "qobuz", type: "qobuz", name: "Qobuz", url: "" },
|
{ id: "qobuz", type: "qobuz", name: "Qobuz", url: "" },
|
||||||
{ id: "amazon", type: "amazon", name: "Amazon Music", url: "" },
|
{ id: "amazon", type: "amazon", name: "Amazon Music", url: "" },
|
||||||
{ id: "musicbrainz", type: "musicbrainz", name: "MusicBrainz", url: "https://musicbrainz.org" },
|
{ id: "musicbrainz", type: "musicbrainz", name: "MusicBrainz", url: "https://musicbrainz.org" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SPOTIFLAC_NEXT_SOURCES: SpotiFLACNextSource[] = [
|
export const SPOTIFLAC_NEXT_SOURCES: SpotiFLACNextSource[] = [
|
||||||
{ id: "tidal", name: "Tidal" },
|
{ id: "tidal", name: "Tidal" },
|
||||||
{ id: "qobuz", name: "Qobuz" },
|
{ id: "qobuz", name: "Qobuz" },
|
||||||
@@ -42,38 +36,31 @@ export const SPOTIFLAC_NEXT_SOURCES: SpotiFLACNextSource[] = [
|
|||||||
{ id: "deezer", name: "Deezer" },
|
{ id: "deezer", name: "Deezer" },
|
||||||
{ id: "apple", name: "Apple Music" },
|
{ id: "apple", name: "Apple Music" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const SPOTIFLAC_NEXT_STATUS_URL = "https://status.spotbye.qzz.io/status";
|
const SPOTIFLAC_NEXT_STATUS_URL = "https://status.spotbye.qzz.io/status";
|
||||||
const SPOTIFLAC_NEXT_MAX_ATTEMPTS = 3;
|
const SPOTIFLAC_NEXT_MAX_ATTEMPTS = 3;
|
||||||
const SPOTIFLAC_NEXT_RETRY_DELAY_MS = 1200;
|
const SPOTIFLAC_NEXT_RETRY_DELAY_MS = 1200;
|
||||||
|
|
||||||
type ApiStatusState = {
|
type ApiStatusState = {
|
||||||
checkingSources: Record<string, boolean>;
|
checkingSources: Record<string, boolean>;
|
||||||
statuses: Record<string, ApiCheckStatus>;
|
statuses: Record<string, ApiCheckStatus>;
|
||||||
nextStatuses: Record<string, ApiCheckStatus>;
|
nextStatuses: Record<string, ApiCheckStatus>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let apiStatusState: ApiStatusState = {
|
let apiStatusState: ApiStatusState = {
|
||||||
checkingSources: {},
|
checkingSources: {},
|
||||||
statuses: {},
|
statuses: {},
|
||||||
nextStatuses: {},
|
nextStatuses: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
let activeCheckNextOnly: Promise<void> | null = null;
|
let activeCheckNextOnly: Promise<void> | null = null;
|
||||||
const activeSourceChecks = new Map<string, Promise<void>>();
|
const activeSourceChecks = new Map<string, Promise<void>>();
|
||||||
const listeners = new Set<() => void>();
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
function emitApiStatusChange() {
|
function emitApiStatusChange() {
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
listener();
|
listener();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setApiStatusState(updater: (current: ApiStatusState) => ApiStatusState) {
|
function setApiStatusState(updater: (current: ApiStatusState) => ApiStatusState) {
|
||||||
apiStatusState = updater(apiStatusState);
|
apiStatusState = updater(apiStatusState);
|
||||||
emitApiStatusChange();
|
emitApiStatusChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkSourceStatus(source: ApiSource): Promise<ApiCheckStatus> {
|
async function checkSourceStatus(source: ApiSource): Promise<ApiCheckStatus> {
|
||||||
try {
|
try {
|
||||||
const isOnline = await withTimeout(CheckAPIStatus(source.type, source.url), CHECK_TIMEOUT_MS, `API status check timed out after 10 seconds for ${source.name}`);
|
const isOnline = await withTimeout(CheckAPIStatus(source.type, source.url), CHECK_TIMEOUT_MS, `API status check timed out after 10 seconds for ${source.name}`);
|
||||||
@@ -83,19 +70,15 @@ async function checkSourceStatus(source: ApiSource): Promise<ApiCheckStatus> {
|
|||||||
return "offline";
|
return "offline";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusFromNextValue(value: string | undefined): ApiCheckStatus {
|
function statusFromNextValue(value: string | undefined): ApiCheckStatus {
|
||||||
return value === "up" ? "online" : "offline";
|
return value === "up" ? "online" : "offline";
|
||||||
}
|
}
|
||||||
|
|
||||||
function anyNextVariantUp(values: Array<string | undefined>): ApiCheckStatus {
|
function anyNextVariantUp(values: Array<string | undefined>): ApiCheckStatus {
|
||||||
return values.some((value) => value === "up") ? "online" : "offline";
|
return values.some((value) => value === "up") ? "online" : "offline";
|
||||||
}
|
}
|
||||||
|
|
||||||
function delay(ms: number): Promise<void> {
|
function delay(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSafeNextStatusesFallback(currentStatuses: Record<string, ApiCheckStatus>): Record<string, ApiCheckStatus> {
|
function getSafeNextStatusesFallback(currentStatuses: Record<string, ApiCheckStatus>): Record<string, ApiCheckStatus> {
|
||||||
return SPOTIFLAC_NEXT_SOURCES.reduce<Record<string, ApiCheckStatus>>((acc, source) => {
|
return SPOTIFLAC_NEXT_SOURCES.reduce<Record<string, ApiCheckStatus>>((acc, source) => {
|
||||||
const current = currentStatuses[source.id];
|
const current = currentStatuses[source.id];
|
||||||
@@ -103,7 +86,6 @@ function getSafeNextStatusesFallback(currentStatuses: Record<string, ApiCheckSta
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSpotiFLACNextStatusesOnce(): Promise<Record<string, ApiCheckStatus>> {
|
async function fetchSpotiFLACNextStatusesOnce(): Promise<Record<string, ApiCheckStatus>> {
|
||||||
const response = await withTimeout(fetch(SPOTIFLAC_NEXT_STATUS_URL, {
|
const response = await withTimeout(fetch(SPOTIFLAC_NEXT_STATUS_URL, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -112,11 +94,9 @@ async function fetchSpotiFLACNextStatusesOnce(): Promise<Record<string, ApiCheck
|
|||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
}), CHECK_TIMEOUT_MS, "SpotiFLAC Next status check timed out after 10 seconds");
|
}), CHECK_TIMEOUT_MS, "SpotiFLAC Next status check timed out after 10 seconds");
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`SpotiFLAC Next status returned ${response.status}`);
|
throw new Error(`SpotiFLAC Next status returned ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = (await response.json()) as SpotiFLACNextStatusResponse;
|
const payload = (await response.json()) as SpotiFLACNextStatusResponse;
|
||||||
return {
|
return {
|
||||||
tidal: statusFromNextValue(payload.tidal),
|
tidal: statusFromNextValue(payload.tidal),
|
||||||
@@ -126,10 +106,8 @@ async function fetchSpotiFLACNextStatusesOnce(): Promise<Record<string, ApiCheck
|
|||||||
apple: statusFromNextValue(payload.apple),
|
apple: statusFromNextValue(payload.apple),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkSpotiFLACNextStatuses(): Promise<Record<string, ApiCheckStatus>> {
|
async function checkSpotiFLACNextStatuses(): Promise<Record<string, ApiCheckStatus>> {
|
||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= SPOTIFLAC_NEXT_MAX_ATTEMPTS; attempt++) {
|
for (let attempt = 1; attempt <= SPOTIFLAC_NEXT_MAX_ATTEMPTS; attempt++) {
|
||||||
try {
|
try {
|
||||||
return await fetchSpotiFLACNextStatusesOnce();
|
return await fetchSpotiFLACNextStatusesOnce();
|
||||||
@@ -141,33 +119,27 @@ async function checkSpotiFLACNextStatuses(): Promise<Record<string, ApiCheckStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw lastError instanceof Error ? lastError : new Error("SpotiFLAC Next status check failed");
|
throw lastError instanceof Error ? lastError : new Error("SpotiFLAC Next status check failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getApiStatusState(): ApiStatusState {
|
export function getApiStatusState(): ApiStatusState {
|
||||||
return apiStatusState;
|
return apiStatusState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subscribeApiStatus(listener: () => void): () => void {
|
export function subscribeApiStatus(listener: () => void): () => void {
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
return () => {
|
return () => {
|
||||||
listeners.delete(listener);
|
listeners.delete(listener);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSpotiFLACNextResults(): boolean {
|
function hasSpotiFLACNextResults(): boolean {
|
||||||
return SPOTIFLAC_NEXT_SOURCES.some((source) => {
|
return SPOTIFLAC_NEXT_SOURCES.some((source) => {
|
||||||
const status = apiStatusState.nextStatuses[source.id];
|
const status = apiStatusState.nextStatuses[source.id];
|
||||||
return status === "online" || status === "offline";
|
return status === "online" || status === "offline";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkSpotiFLACNextStatusesOnly(): Promise<void> {
|
export async function checkSpotiFLACNextStatusesOnly(): Promise<void> {
|
||||||
if (activeCheckNextOnly) {
|
if (activeCheckNextOnly) {
|
||||||
return activeCheckNextOnly;
|
return activeCheckNextOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeCheckNextOnly = (async () => {
|
activeCheckNextOnly = (async () => {
|
||||||
const checkingNextStatuses = Object.fromEntries(SPOTIFLAC_NEXT_SOURCES.map((source) => [source.id, "checking" as ApiCheckStatus]));
|
const checkingNextStatuses = Object.fromEntries(SPOTIFLAC_NEXT_SOURCES.map((source) => [source.id, "checking" as ApiCheckStatus]));
|
||||||
setApiStatusState((current) => ({
|
setApiStatusState((current) => ({
|
||||||
@@ -177,15 +149,12 @@ export async function checkSpotiFLACNextStatusesOnly(): Promise<void> {
|
|||||||
...checkingNextStatuses,
|
...checkingNextStatuses,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setApiStatusState((current) => ({
|
setApiStatusState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
nextStatuses: { ...current.nextStatuses },
|
nextStatuses: { ...current.nextStatuses },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const nextStatuses = await checkSpotiFLACNextStatuses();
|
const nextStatuses = await checkSpotiFLACNextStatuses();
|
||||||
|
|
||||||
setApiStatusState((current) => ({
|
setApiStatusState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
nextStatuses: {
|
nextStatuses: {
|
||||||
@@ -204,27 +173,22 @@ export async function checkSpotiFLACNextStatusesOnly(): Promise<void> {
|
|||||||
activeCheckNextOnly = null;
|
activeCheckNextOnly = null;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return activeCheckNextOnly;
|
return activeCheckNextOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureSpotiFLACNextStatusCheckStarted(): void {
|
export function ensureSpotiFLACNextStatusCheckStarted(): void {
|
||||||
if (!activeCheckNextOnly && !hasSpotiFLACNextResults()) {
|
if (!activeCheckNextOnly && !hasSpotiFLACNextResults()) {
|
||||||
void checkSpotiFLACNextStatusesOnly();
|
void checkSpotiFLACNextStatusesOnly();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkApiStatus(sourceId: string): Promise<void> {
|
export async function checkApiStatus(sourceId: string): Promise<void> {
|
||||||
const source = API_SOURCES.find((item) => item.id === sourceId);
|
const source = API_SOURCES.find((item) => item.id === sourceId);
|
||||||
if (!source) {
|
if (!source) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeCheck = activeSourceChecks.get(sourceId);
|
const activeCheck = activeSourceChecks.get(sourceId);
|
||||||
if (activeCheck) {
|
if (activeCheck) {
|
||||||
return activeCheck;
|
return activeCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
const task = (async () => {
|
const task = (async () => {
|
||||||
setApiStatusState((current) => ({
|
setApiStatusState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -237,10 +201,8 @@ export async function checkApiStatus(sourceId: string): Promise<void> {
|
|||||||
[sourceId]: "checking",
|
[sourceId]: "checking",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const status = await checkSourceStatus(source);
|
const status = await checkSourceStatus(source);
|
||||||
|
|
||||||
setApiStatusState((current) => ({
|
setApiStatusState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
statuses: {
|
statuses: {
|
||||||
@@ -260,7 +222,6 @@ export async function checkApiStatus(sourceId: string): Promise<void> {
|
|||||||
activeSourceChecks.delete(sourceId);
|
activeSourceChecks.delete(sourceId);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
activeSourceChecks.set(sourceId, task);
|
activeSourceChecks.set(sourceId, task);
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user