-
-
+
+
SpotiFLAC Services
+
+
+ {sources.map((source) => {
+ const status = statuses[source.id] || "idle";
+ const isChecking = checkingSources[source.id] === true;
+ return (
+
+
+ {renderPlatformIcon(source.type)}
+
{source.name}
+
+
{renderStatusIcon(status)}
+
+
+
);
+ })}
+
-
- {sources.map((source) => {
- const status = statuses[source.id] || "idle";
+
+
+
+
SpotiFLAC Next Services
+
+
+ {SPOTIFLAC_NEXT_SOURCES.map((source) => {
+ const status = nextStatuses[source.id] || "idle";
return (
- {source.type === "tidal" ?
: source.type === "amazon" ?
: source.type === "lrclib" ?
: source.type === "musicbrainz" ?
:
}
+ {renderPlatformIcon(source.id)}
{source.name}
-
-
- {status === "checking" &&
}
- {status === "online" &&
}
- {status === "offline" &&
}
- {status === "idle" &&
}
-
+
{renderStatusIcon(status)}
);
})}
+
);
}
diff --git a/frontend/src/components/PlatformIcons.tsx b/frontend/src/components/PlatformIcons.tsx
index e944837..388e1c6 100644
--- a/frontend/src/components/PlatformIcons.tsx
+++ b/frontend/src/components/PlatformIcons.tsx
@@ -1,4 +1,6 @@
import amazonMusicIcon from "../assets/icons/amzn.png";
+import appleMusicIcon from "../assets/icons/am.png";
+import deezerIcon from "../assets/icons/dzr.png";
import lrclibIcon from "../assets/icons/lrclib.png";
import musicBrainzDarkIcon from "../assets/icons/musicbrainz_d.png";
import musicBrainzLightIcon from "../assets/icons/musicbrainz_l.png";
@@ -81,6 +83,12 @@ export function QobuzIcon({ className = "w-4 h-4" }: PlatformIconProps) {
export function AmazonIcon({ className = "w-4 h-4" }: PlatformIconProps) {
return
;
}
+export function AppleMusicIcon({ className = "w-4 h-4" }: PlatformIconProps) {
+ return
;
+}
+export function DeezerIcon({ className = "w-4 h-4" }: PlatformIconProps) {
+ return
;
+}
export function LrclibIcon({ className = "w-4 h-4" }: PlatformIconProps) {
return
;
}
diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx
index 04e01e7..30e539a 100644
--- a/frontend/src/components/SettingsPage.tsx
+++ b/frontend/src/components/SettingsPage.tsx
@@ -102,6 +102,9 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
const handleTidalQualityChange = async (value: "LOSSLESS" | "HI_RES_LOSSLESS") => {
setTempSettings((prev) => ({ ...prev, tidalQuality: value }));
};
+ const handleTidalVariantChange = (value: "tidal" | "alt") => {
+ setTempSettings((prev) => ({ ...prev, tidalVariant: value }));
+ };
const handleQobuzQualityChange = (value: "6" | "7" | "27") => {
setTempSettings((prev) => ({ ...prev, qobuzQuality: value }));
};
@@ -424,17 +427,19 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
>)}
- {tempSettings.downloader === "tidal" && ()}
+ {tempSettings.downloader === "tidal" && (tempSettings.tidalVariant === "alt" ? (
+ 16-bit/44.1kHz
+
) : ())}
{tempSettings.downloader === "qobuz" && (
+ {(tempSettings.downloader === "tidal" || tempSettings.downloader === "auto") && (
+
+
+
)}
+
{((tempSettings.downloader === "tidal" &&
+ tempSettings.tidalVariant !== "alt" &&
tempSettings.tidalQuality === "HI_RES_LOSSLESS") ||
(tempSettings.downloader === "qobuz" &&
tempSettings.qobuzQuality === "27") ||
diff --git a/frontend/src/hooks/useApiStatus.ts b/frontend/src/hooks/useApiStatus.ts
index 24a8b8b..b311a45 100644
--- a/frontend/src/hooks/useApiStatus.ts
+++ b/frontend/src/hooks/useApiStatus.ts
@@ -1,9 +1,8 @@
import { useEffect, useState } from "react";
-import { API_SOURCES, checkAllApiStatuses, ensureApiStatusCheckStarted, getApiStatusState, subscribeApiStatus, } from "@/lib/api-status";
+import { API_SOURCES, checkApiStatus, getApiStatusState, subscribeApiStatus, } from "@/lib/api-status";
export function useApiStatus() {
const [state, setState] = useState(getApiStatusState);
useEffect(() => {
- ensureApiStatusCheckStarted();
return subscribeApiStatus(() => {
setState(getApiStatusState());
});
@@ -11,6 +10,6 @@ export function useApiStatus() {
return {
...state,
sources: API_SOURCES,
- refreshAll: () => checkAllApiStatuses(true),
+ checkOne: (sourceId: string) => checkApiStatus(sourceId),
};
}
diff --git a/frontend/src/hooks/useDownload.ts b/frontend/src/hooks/useDownload.ts
index ea773c6..4e91dfc 100644
--- a/frontend/src/hooks/useDownload.ts
+++ b/frontend/src/hooks/useDownload.ts
@@ -52,6 +52,24 @@ async function resolveTemplateISRC(settings: {
return "";
}
}
+function getTidalVariant(settings: any): "tidal" | "alt" {
+ return settings?.tidalVariant === "alt" ? "alt" : "tidal";
+}
+function isTidalAltVariant(settings: any): boolean {
+ return getTidalVariant(settings) === "alt";
+}
+function getTidalAudioFormat(settings: any, mode: "single" | "auto"): "LOSSLESS" | "HI_RES_LOSSLESS" {
+ if (isTidalAltVariant(settings)) {
+ return "LOSSLESS";
+ }
+ if (mode === "auto") {
+ return (settings.autoQuality || "24") === "24" ? "HI_RES_LOSSLESS" : "LOSSLESS";
+ }
+ return settings.tidalQuality || "LOSSLESS";
+}
+function shouldFetchStreamingURLs(order: string[], settings: any): boolean {
+ return order.includes("amazon") || (order.includes("tidal") && !isTidalAltVariant(settings));
+}
export function useDownload(region: string) {
const [downloadProgress, setDownloadProgress] = useState
(0);
const [isDownloading, setIsDownloading] = useState(false);
@@ -170,8 +188,11 @@ export function useDownload(region: string) {
itemID = await AddToDownloadQueue(id, trackName || "", displayArtist || "", albumName || "");
}
if (service === "auto") {
+ const order = (settings.autoOrder || "tidal-amazon-qobuz").split("-");
+ const tidalVariant = getTidalVariant(settings);
+ const tidalLabel = tidalVariant === "alt" ? "Tidal Alt." : "Tidal";
let streamingURLs: any = null;
- if (spotifyId) {
+ if (spotifyId && shouldFetchStreamingURLs(order, settings)) {
try {
const { GetStreamingURLs } = await import("../../wailsjs/go/main/App");
const urlsJson = await GetStreamingURLs(spotifyId, region);
@@ -182,16 +203,15 @@ export function useDownload(region: string) {
}
}
const durationSeconds = durationMs ? Math.round(durationMs / 1000) : undefined;
- const order = (settings.autoOrder || "tidal-amazon-qobuz").split("-");
let lastResponse: any = { success: false, error: "No matching services found" };
const fallbackErrors: string[] = [];
+ const tidalQuality = getTidalAudioFormat(settings, "auto");
const is24Bit = (settings.autoQuality || "24") === "24";
- const tidalQuality = is24Bit ? "HI_RES_LOSSLESS" : "LOSSLESS";
const qobuzQuality = is24Bit ? "27" : "6";
for (const s of order) {
- if (s === "tidal" && streamingURLs?.tidal_url) {
+ if (s === "tidal" && ((tidalVariant === "alt" && spotifyId) || streamingURLs?.tidal_url)) {
try {
- logger.debug(`trying tidal for: ${trackName} - ${artistName}`);
+ logger.debug(`trying ${tidalLabel} for: ${trackName} - ${artistName}`);
const response = await downloadTrack({
service: "tidal",
query,
@@ -209,7 +229,8 @@ export function useDownload(region: string) {
spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
- service_url: streamingURLs.tidal_url,
+ service_url: tidalVariant === "alt" ? undefined : streamingURLs?.tidal_url,
+ tidal_variant: tidalVariant,
duration: durationSeconds,
item_id: itemID,
audio_format: tidalQuality,
@@ -225,17 +246,17 @@ export function useDownload(region: string) {
embed_genre: settings.embedGenre,
});
if (response.success) {
- logger.success(`tidal: ${trackName} - ${artistName}`);
+ logger.success(`${tidalLabel}: ${trackName} - ${artistName}`);
return response;
}
const errMsg = response.error || response.message || "Failed";
- fallbackErrors.push(`[Tidal] ${errMsg}`);
+ fallbackErrors.push(`[${tidalLabel}] ${errMsg}`);
lastResponse = response;
- logger.warning(`tidal failed, trying next...`);
+ logger.warning(`${tidalLabel} failed, trying next...`);
}
catch (err) {
- logger.error(`tidal error: ${err}`);
- fallbackErrors.push(`[Tidal] ${String(err)}`);
+ logger.error(`${tidalLabel} error: ${err}`);
+ fallbackErrors.push(`[${tidalLabel}] ${String(err)}`);
lastResponse = { success: false, error: String(err) };
}
}
@@ -344,7 +365,7 @@ export function useDownload(region: string) {
const durationSecondsForFallback = durationMs ? Math.round(durationMs / 1000) : undefined;
let audioFormat: string | undefined;
if (service === "tidal") {
- audioFormat = settings.tidalQuality || "LOSSLESS";
+ audioFormat = getTidalAudioFormat(settings, "single");
}
else if (service === "qobuz") {
audioFormat = settings.qobuzQuality || "6";
@@ -373,6 +394,7 @@ export function useDownload(region: string) {
duration: durationSecondsForFallback,
item_id: itemID,
audio_format: audioFormat,
+ tidal_variant: service === "tidal" ? getTidalVariant(settings) : undefined,
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
@@ -380,6 +402,7 @@ export function useDownload(region: string) {
isrc: resolvedTemplateISRC || undefined,
copyright: copyright,
publisher: publisher,
+ use_first_artist_only: settings.useFirstArtistOnly,
use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre,
});
@@ -451,8 +474,11 @@ export function useDownload(region: string) {
}
}
if (service === "auto") {
+ const order = (settings.autoOrder || "tidal-amazon-qobuz").split("-");
+ const tidalVariant = getTidalVariant(settings);
+ const tidalLabel = tidalVariant === "alt" ? "Tidal Alt." : "Tidal";
let streamingURLs: any = null;
- if (spotifyId) {
+ if (spotifyId && shouldFetchStreamingURLs(order, settings)) {
try {
const { GetStreamingURLs } = await import("../../wailsjs/go/main/App");
const urlsJson = await GetStreamingURLs(spotifyId, region);
@@ -463,16 +489,15 @@ export function useDownload(region: string) {
}
}
const durationSeconds = durationMs ? Math.round(durationMs / 1000) : undefined;
- const order = (settings.autoOrder || "tidal-amazon-qobuz").split("-");
let lastResponse: any = { success: false, error: "No matching services found" };
const fallbackErrors: string[] = [];
+ const tidalQuality = getTidalAudioFormat(settings, "auto");
const is24Bit = (settings.autoQuality || "24") === "24";
- const tidalQuality = is24Bit ? "HI_RES_LOSSLESS" : "LOSSLESS";
const qobuzQuality = is24Bit ? "27" : "6";
for (const s of order) {
- if (s === "tidal" && streamingURLs?.tidal_url) {
+ if (s === "tidal" && ((tidalVariant === "alt" && spotifyId) || streamingURLs?.tidal_url)) {
try {
- logger.debug(`trying tidal for: ${trackName} - ${artistName}`);
+ logger.debug(`trying ${tidalLabel} for: ${trackName} - ${artistName}`);
const response = await downloadTrack({
service: "tidal",
query,
@@ -490,7 +515,8 @@ export function useDownload(region: string) {
spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
- service_url: streamingURLs.tidal_url,
+ service_url: tidalVariant === "alt" ? undefined : streamingURLs?.tidal_url,
+ tidal_variant: tidalVariant,
duration: durationSeconds,
item_id: itemID,
audio_format: tidalQuality,
@@ -506,17 +532,17 @@ export function useDownload(region: string) {
embed_genre: settings.embedGenre,
});
if (response.success) {
- logger.success(`tidal: ${trackName} - ${artistName}`);
+ logger.success(`${tidalLabel}: ${trackName} - ${artistName}`);
return response;
}
const errMsg = response.error || response.message || "Failed";
- fallbackErrors.push(`[Tidal] ${errMsg}`);
+ fallbackErrors.push(`[${tidalLabel}] ${errMsg}`);
lastResponse = response;
- logger.warning(`tidal failed, trying next...`);
+ logger.warning(`${tidalLabel} failed, trying next...`);
}
catch (err) {
- logger.error(`tidal error: ${err}`);
- fallbackErrors.push(`[Tidal] ${String(err)}`);
+ logger.error(`${tidalLabel} error: ${err}`);
+ fallbackErrors.push(`[${tidalLabel}] ${String(err)}`);
lastResponse = { success: false, error: String(err) };
}
}
@@ -628,7 +654,7 @@ export function useDownload(region: string) {
const durationSecondsForFallback = durationMs ? Math.round(durationMs / 1000) : undefined;
let audioFormat: string | undefined;
if (service === "tidal") {
- audioFormat = settings.tidalQuality || "LOSSLESS";
+ audioFormat = getTidalAudioFormat(settings, "single");
}
else if (service === "qobuz") {
audioFormat = settings.qobuzQuality || "6";
@@ -653,6 +679,7 @@ export function useDownload(region: string) {
duration: durationSecondsForFallback,
item_id: itemID,
audio_format: audioFormat,
+ tidal_variant: service === "tidal" ? getTidalVariant(settings) : undefined,
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 4698284..dbb2340 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -73,6 +73,13 @@
}
@layer base {
+ html,
+ body,
+ #root {
+ height: 100%;
+ overflow: hidden;
+ }
+
* {
@apply border-border outline-ring/50;
}
@@ -265,4 +272,4 @@
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
filter: brightness(1.2);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/lib/api-status.ts b/frontend/src/lib/api-status.ts
index dfe1126..ad6b915 100644
--- a/frontend/src/lib/api-status.ts
+++ b/frontend/src/lib/api-status.ts
@@ -1,4 +1,4 @@
-import { CheckAPIStatus, FetchUnifiedAPIStatus } from "../../wailsjs/go/main/App";
+import { CheckAPIStatus } from "../../wailsjs/go/main/App";
import { CHECK_TIMEOUT_MS, withTimeout } from "@/lib/async-timeout";
export type ApiCheckStatus = "checking" | "online" | "offline" | "idle";
export interface ApiSource {
@@ -7,39 +7,51 @@ export interface ApiSource {
name: string;
url: string;
}
-export const API_SOURCES: ApiSource[] = [
- { id: "tidal1", type: "tidal", name: "Tidal A", url: "https://hifi-one.spotisaver.net" },
- { id: "tidal2", type: "tidal", name: "Tidal B", url: "https://hifi-two.spotisaver.net" },
- { id: "tidal3", type: "tidal", name: "Tidal C", url: "https://eu-central.monochrome.tf" },
- { id: "tidal4", type: "tidal", name: "Tidal D", url: "https://us-west.monochrome.tf" },
- { id: "tidal5", type: "tidal", name: "Tidal E", url: "https://api.monochrome.tf" },
- { id: "tidal6", type: "tidal", name: "Tidal F", url: "https://monochrome-api.samidy.com" },
- { id: "tidal7", type: "tidal", name: "Tidal G", url: "https://tidal.kinoplus.online" },
- { id: "qobuz1", type: "qobuz", name: "Qobuz A", url: "https://dab.yeet.su" },
- { 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: "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 = {
- isCheckingAll: boolean;
- statuses: Record;
-};
-let apiStatusState: ApiStatusState = {
- isCheckingAll: false,
- statuses: {},
-};
-let activeCheckAll: Promise | null = null;
-const listeners = new Set<() => void>();
-type SpotiFLACUnifiedStatusResponse = {
+interface SpotiFLACNextSource {
+ id: string;
+ name: string;
+}
+type SpotiFLACNextStatusResponse = {
tidal?: string;
qobuz_a?: string;
qobuz_b?: string;
qobuz_c?: string;
- amazon?: string;
- lrclib?: string;
+ deezer_a?: string;
+ deezer_b?: string;
+ amazon_a?: string;
+ amazon_b?: string;
+ amazon_c?: string;
+ apple?: string;
};
+export const API_SOURCES: ApiSource[] = [
+ { id: "tidal", type: "tidal", name: "Tidal", url: "" },
+ { id: "qobuz", type: "qobuz", name: "Qobuz", url: "" },
+ { id: "amazon", type: "amazon", name: "Amazon Music", url: "" },
+ { id: "musicbrainz", type: "musicbrainz", name: "MusicBrainz", url: "https://musicbrainz.org" },
+];
+export const SPOTIFLAC_NEXT_SOURCES: SpotiFLACNextSource[] = [
+ { id: "tidal", name: "Tidal" },
+ { id: "qobuz", name: "Qobuz" },
+ { id: "amazon", name: "Amazon Music" },
+ { id: "deezer", name: "Deezer" },
+ { id: "apple", name: "Apple Music" },
+];
+const SPOTIFLAC_NEXT_STATUS_URL = "https://status.spotbye.qzz.io/status";
+const SPOTIFLAC_NEXT_MAX_ATTEMPTS = 3;
+const SPOTIFLAC_NEXT_RETRY_DELAY_MS = 1200;
+type ApiStatusState = {
+ checkingSources: Record;
+ statuses: Record;
+ nextStatuses: Record;
+};
+let apiStatusState: ApiStatusState = {
+ checkingSources: {},
+ statuses: {},
+ nextStatuses: {},
+};
+let activeCheckNextOnly: Promise | null = null;
+const activeSourceChecks = new Map>();
+const listeners = new Set<() => void>();
function emitApiStatusChange() {
for (const listener of listeners) {
listener();
@@ -49,39 +61,66 @@ function setApiStatusState(updater: (current: ApiStatusState) => ApiStatusState)
apiStatusState = updater(apiStatusState);
emitApiStatusChange();
}
-function statusFromUnifiedValue(value: string | undefined): ApiCheckStatus {
- return value === "up" ? "online" : "offline";
-}
-async function fetchUnifiedStatuses(forceRefresh: boolean): Promise> {
- const response = await FetchUnifiedAPIStatus(forceRefresh);
- const payload = JSON.parse(response) as SpotiFLACUnifiedStatusResponse;
- const tidalStatus = statusFromUnifiedValue(payload.tidal);
- return {
- statuses: {
- tidal1: tidalStatus,
- tidal2: tidalStatus,
- tidal3: tidalStatus,
- tidal4: tidalStatus,
- tidal5: tidalStatus,
- tidal6: tidalStatus,
- tidal7: tidalStatus,
- qobuz1: statusFromUnifiedValue(payload.qobuz_a),
- qobuz2: statusFromUnifiedValue(payload.qobuz_b),
- qobuz3: statusFromUnifiedValue(payload.qobuz_c),
- amazon1: statusFromUnifiedValue(payload.amazon),
- lrclib: statusFromUnifiedValue(payload.lrclib),
- },
- };
-}
-async function checkMusicBrainzStatus(): Promise {
+async function checkSourceStatus(source: ApiSource): Promise {
try {
- const isOnline = await withTimeout(CheckAPIStatus("musicbrainz", "https://musicbrainz.org"), CHECK_TIMEOUT_MS, "API status check timed out after 10 seconds for MusicBrainz");
+ const isOnline = await withTimeout(CheckAPIStatus(source.type, source.url), CHECK_TIMEOUT_MS, `API status check timed out after 10 seconds for ${source.name}`);
return isOnline ? "online" : "offline";
}
catch {
return "offline";
}
}
+function statusFromNextValue(value: string | undefined): ApiCheckStatus {
+ return value === "up" ? "online" : "offline";
+}
+function anyNextVariantUp(values: Array): ApiCheckStatus {
+ return values.some((value) => value === "up") ? "online" : "offline";
+}
+function delay(ms: number): Promise {
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
+}
+function getSafeNextStatusesFallback(currentStatuses: Record): Record {
+ return SPOTIFLAC_NEXT_SOURCES.reduce>((acc, source) => {
+ const current = currentStatuses[source.id];
+ acc[source.id] = current === "online" || current === "offline" ? current : "idle";
+ return acc;
+ }, {});
+}
+async function fetchSpotiFLACNextStatusesOnce(): Promise> {
+ const response = await withTimeout(fetch(SPOTIFLAC_NEXT_STATUS_URL, {
+ method: "GET",
+ cache: "no-store",
+ headers: {
+ Accept: "application/json",
+ },
+ }), CHECK_TIMEOUT_MS, "SpotiFLAC Next status check timed out after 10 seconds");
+ if (!response.ok) {
+ throw new Error(`SpotiFLAC Next status returned ${response.status}`);
+ }
+ const payload = (await response.json()) as SpotiFLACNextStatusResponse;
+ return {
+ tidal: statusFromNextValue(payload.tidal),
+ qobuz: anyNextVariantUp([payload.qobuz_a, payload.qobuz_b, payload.qobuz_c]),
+ deezer: anyNextVariantUp([payload.deezer_a, payload.deezer_b]),
+ amazon: anyNextVariantUp([payload.amazon_a, payload.amazon_b, payload.amazon_c]),
+ apple: statusFromNextValue(payload.apple),
+ };
+}
+async function checkSpotiFLACNextStatuses(): Promise> {
+ let lastError: unknown = null;
+ for (let attempt = 1; attempt <= SPOTIFLAC_NEXT_MAX_ATTEMPTS; attempt++) {
+ try {
+ return await fetchSpotiFLACNextStatusesOnce();
+ }
+ catch (error) {
+ lastError = error;
+ if (attempt < SPOTIFLAC_NEXT_MAX_ATTEMPTS) {
+ await delay(SPOTIFLAC_NEXT_RETRY_DELAY_MS * attempt);
+ }
+ }
+ }
+ throw lastError instanceof Error ? lastError : new Error("SpotiFLAC Next status check failed");
+}
export function getApiStatusState(): ApiStatusState {
return apiStatusState;
}
@@ -91,70 +130,98 @@ export function subscribeApiStatus(listener: () => void): () => void {
listeners.delete(listener);
};
}
-export function hasApiStatusResults(): boolean {
- return API_SOURCES.some((source) => {
- const status = apiStatusState.statuses[source.id];
+function hasSpotiFLACNextResults(): boolean {
+ return SPOTIFLAC_NEXT_SOURCES.some((source) => {
+ const status = apiStatusState.nextStatuses[source.id];
return status === "online" || status === "offline";
});
}
-export function ensureApiStatusCheckStarted(): void {
- if (!activeCheckAll && !hasApiStatusResults()) {
- void checkAllApiStatuses(false);
+export async function checkSpotiFLACNextStatusesOnly(): Promise {
+ if (activeCheckNextOnly) {
+ return activeCheckNextOnly;
}
-}
-export async function checkAllApiStatuses(forceRefresh: boolean = false): Promise {
- if (activeCheckAll) {
- return activeCheckAll;
- }
- activeCheckAll = (async () => {
- const checkingStatuses = Object.fromEntries(API_SOURCES.map((source) => [source.id, "checking" as ApiCheckStatus]));
+ activeCheckNextOnly = (async () => {
+ const checkingNextStatuses = Object.fromEntries(SPOTIFLAC_NEXT_SOURCES.map((source) => [source.id, "checking" as ApiCheckStatus]));
setApiStatusState((current) => ({
...current,
- isCheckingAll: true,
- statuses: {
- ...current.statuses,
- ...checkingStatuses,
+ nextStatuses: {
+ ...current.nextStatuses,
+ ...checkingNextStatuses,
},
}));
try {
- const [unifiedResult, musicBrainzStatus] = await Promise.allSettled([
- withTimeout(fetchUnifiedStatuses(forceRefresh), CHECK_TIMEOUT_MS, "Unified SpotiFLAC status check timed out after 10 seconds"),
- checkMusicBrainzStatus(),
- ]);
- setApiStatusState((current) => {
- const nextStatuses = { ...current.statuses };
- if (unifiedResult.status === "fulfilled") {
- Object.assign(nextStatuses, unifiedResult.value.statuses);
- }
- else {
- nextStatuses.tidal1 = "offline";
- nextStatuses.tidal2 = "offline";
- nextStatuses.tidal3 = "offline";
- nextStatuses.tidal4 = "offline";
- nextStatuses.tidal5 = "offline";
- nextStatuses.tidal6 = "offline";
- nextStatuses.tidal7 = "offline";
- nextStatuses.qobuz1 = "offline";
- nextStatuses.qobuz2 = "offline";
- nextStatuses.qobuz3 = "offline";
- nextStatuses.amazon1 = "offline";
- nextStatuses.lrclib = "offline";
- }
- nextStatuses.musicbrainz =
- musicBrainzStatus.status === "fulfilled" ? musicBrainzStatus.value : "offline";
- return {
- ...current,
- statuses: nextStatuses,
- };
- });
+ setApiStatusState((current) => ({
+ ...current,
+ nextStatuses: { ...current.nextStatuses },
+ }));
+ const nextStatuses = await checkSpotiFLACNextStatuses();
+ setApiStatusState((current) => ({
+ ...current,
+ nextStatuses: {
+ ...current.nextStatuses,
+ ...nextStatuses,
+ },
+ }));
+ }
+ catch {
+ setApiStatusState((current) => ({
+ ...current,
+ nextStatuses: getSafeNextStatusesFallback(current.nextStatuses),
+ }));
+ }
+ finally {
+ activeCheckNextOnly = null;
+ }
+ })();
+ return activeCheckNextOnly;
+}
+export function ensureSpotiFLACNextStatusCheckStarted(): void {
+ if (!activeCheckNextOnly && !hasSpotiFLACNextResults()) {
+ void checkSpotiFLACNextStatusesOnly();
+ }
+}
+export async function checkApiStatus(sourceId: string): Promise {
+ const source = API_SOURCES.find((item) => item.id === sourceId);
+ if (!source) {
+ return;
+ }
+ const activeCheck = activeSourceChecks.get(sourceId);
+ if (activeCheck) {
+ return activeCheck;
+ }
+ const task = (async () => {
+ setApiStatusState((current) => ({
+ ...current,
+ checkingSources: {
+ ...current.checkingSources,
+ [sourceId]: true,
+ },
+ statuses: {
+ ...current.statuses,
+ [sourceId]: "checking",
+ },
+ }));
+ try {
+ const status = await checkSourceStatus(source);
+ setApiStatusState((current) => ({
+ ...current,
+ statuses: {
+ ...current.statuses,
+ [sourceId]: status,
+ },
+ }));
}
finally {
setApiStatusState((current) => ({
...current,
- isCheckingAll: false,
+ checkingSources: {
+ ...current.checkingSources,
+ [sourceId]: false,
+ },
}));
- activeCheckAll = null;
+ activeSourceChecks.delete(sourceId);
}
})();
- return activeCheckAll;
+ activeSourceChecks.set(sourceId, task);
+ return task;
}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index d2160f1..ef9c56a 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -13,6 +13,9 @@ export async function fetchSpotifyMetadata(url: string, batch: boolean = true, d
}
export async function downloadTrack(request: DownloadRequest): Promise {
const req = new main.DownloadRequest(request);
+ if (request.tidal_variant !== undefined) {
+ (req as any).tidal_variant = request.tidal_variant;
+ }
if (request.use_single_genre !== undefined) {
(req as any).use_single_genre = request.use_single_genre;
}
diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts
index 291e6ec..a4b3d59 100644
--- a/frontend/src/lib/settings.ts
+++ b/frontend/src/lib/settings.ts
@@ -22,6 +22,7 @@ export interface Settings {
embedLyrics: boolean;
embedMaxQualityCover: boolean;
operatingSystem: "Windows" | "linux/MacOS";
+ tidalVariant: "tidal" | "alt";
tidalQuality: "LOSSLESS" | "HI_RES_LOSSLESS";
qobuzQuality: "6" | "7" | "27";
amazonQuality: "original";
@@ -110,6 +111,7 @@ export const DEFAULT_SETTINGS: Settings = {
embedLyrics: false,
embedMaxQualityCover: false,
operatingSystem: detectOS(),
+ tidalVariant: "tidal",
tidalQuality: "LOSSLESS",
qobuzQuality: "6",
amazonQuality: "original",
@@ -215,6 +217,9 @@ function getSettingsFromLocalStorage(): Settings {
if (!('tidalQuality' in parsed)) {
parsed.tidalQuality = "LOSSLESS";
}
+ if (!('tidalVariant' in parsed)) {
+ parsed.tidalVariant = "tidal";
+ }
if (!('qobuzQuality' in parsed)) {
parsed.qobuzQuality = "6";
}
@@ -306,6 +311,9 @@ export async function loadSettings(): Promise {
if (!('tidalQuality' in parsed)) {
parsed.tidalQuality = "LOSSLESS";
}
+ if (!('tidalVariant' in parsed)) {
+ parsed.tidalVariant = "tidal";
+ }
if (!('qobuzQuality' in parsed)) {
parsed.qobuzQuality = "6";
}
diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts
index 48dc659..6e0e007 100644
--- a/frontend/src/types/api.ts
+++ b/frontend/src/types/api.ts
@@ -120,6 +120,7 @@ export interface DownloadRequest {
release_date?: string;
cover_url?: string;
tidal_api_url?: string;
+ tidal_variant?: "tidal" | "alt";
output_dir?: string;
audio_format?: string;
folder_name?: string;
diff --git a/wails.json b/wails.json
index ff85543..891084c 100644
--- a/wails.json
+++ b/wails.json
@@ -12,7 +12,7 @@
},
"info": {
"productName": "SpotiFLAC",
- "productVersion": "7.1.4",
+ "productVersion": "7.1.5",
"copyright": "© 2026 afkarxyz"
},
"wailsjsdir": "./frontend",