mirror of
https://github.com/19JVJeffery/tiddl-gui.git
synced 2026-10-22 19:02:16 -05:00
Agent-Logs-Url: https://github.com/19JVJeffery/tiddl-gui/sessions/5e0218fc-7235-4e8b-8e82-52759274b754 Co-authored-by: 19JVJeffery <94115228+19JVJeffery@users.noreply.github.com>
208 lines
6.8 KiB
JavaScript
208 lines
6.8 KiB
JavaScript
/**
|
|
* tiddl-web — Tidal API client
|
|
*
|
|
* Thin wrapper around api.tidal.com/v1 — mirrors the Python TidalAPI class.
|
|
*/
|
|
|
|
import { API_URL, proxied } from "./config.js";
|
|
import { getValidToken, loadAuth } from "./auth.js";
|
|
|
|
async function apiFetch(endpoint, params = {}) {
|
|
const token = await getValidToken();
|
|
if (!token) throw new Error("Not authenticated");
|
|
|
|
const auth = loadAuth();
|
|
const defaultParams = { countryCode: auth?.country_code || "US" };
|
|
const merged = { ...defaultParams, ...params };
|
|
|
|
const qs = new URLSearchParams(merged).toString();
|
|
const url = `${API_URL}/${endpoint}?${qs}`;
|
|
|
|
const res = await fetch(proxied(url), {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
Accept: "application/json",
|
|
},
|
|
credentials: "omit",
|
|
});
|
|
|
|
const json = await res.json();
|
|
if (!res.ok) {
|
|
const msg = json?.userMessage || json?.error_description || `HTTP ${res.status}`;
|
|
throw new Error(msg);
|
|
}
|
|
return json;
|
|
}
|
|
|
|
// ─── Resources ─────────────────────────────────────────────────────────────
|
|
|
|
export async function getTrack(id) {
|
|
return apiFetch(`tracks/${id}`);
|
|
}
|
|
|
|
export async function getAlbum(id) {
|
|
return apiFetch(`albums/${id}`);
|
|
}
|
|
|
|
export async function getAlbumItems(id, limit = 100, offset = 0) {
|
|
return apiFetch(`albums/${id}/items`, { limit, offset });
|
|
}
|
|
|
|
export async function getArtist(id) {
|
|
return apiFetch(`artists/${id}`);
|
|
}
|
|
|
|
export async function getArtistAlbums(id, limit = 50, offset = 0) {
|
|
return apiFetch(`artists/${id}/albums`, { limit, offset, filter: "ALBUMS" });
|
|
}
|
|
|
|
export async function getArtistSingles(id, limit = 50, offset = 0) {
|
|
return apiFetch(`artists/${id}/albums`, { limit, offset, filter: "EPSANDSINGLES" });
|
|
}
|
|
|
|
export async function getPlaylist(uuid) {
|
|
return apiFetch(`playlists/${uuid}`);
|
|
}
|
|
|
|
export async function getPlaylistItems(uuid, limit = 100, offset = 0) {
|
|
return apiFetch(`playlists/${uuid}/items`, { limit, offset });
|
|
}
|
|
|
|
export async function getMixItems(mixId, limit = 100, offset = 0) {
|
|
return apiFetch(`mixes/${mixId}/items`, { limit, offset });
|
|
}
|
|
|
|
export async function getVideo(id) {
|
|
return apiFetch(`videos/${id}`);
|
|
}
|
|
|
|
export async function search(query, limit = 20) {
|
|
return apiFetch("search", { query, limit });
|
|
}
|
|
|
|
export async function getFavorites() {
|
|
const auth = loadAuth();
|
|
if (!auth?.user_id) throw new Error("No user id");
|
|
return apiFetch(`users/${auth.user_id}/favorites/ids`);
|
|
}
|
|
|
|
export async function getUserFavoriteTracks(limit = 50, offset = 0) {
|
|
const auth = loadAuth();
|
|
if (!auth?.user_id) throw new Error("No user id");
|
|
return apiFetch(`users/${auth.user_id}/favorites/tracks`, { limit, offset });
|
|
}
|
|
|
|
export async function getUserFavoriteAlbums(limit = 50, offset = 0) {
|
|
const auth = loadAuth();
|
|
if (!auth?.user_id) throw new Error("No user id");
|
|
return apiFetch(`users/${auth.user_id}/favorites/albums`, { limit, offset });
|
|
}
|
|
|
|
export async function getUserFavoritePlaylists(limit = 50, offset = 0) {
|
|
const auth = loadAuth();
|
|
if (!auth?.user_id) throw new Error("No user id");
|
|
return apiFetch(`users/${auth.user_id}/favorites/playlists`, { limit, offset });
|
|
}
|
|
|
|
export async function getUserPlaylists(limit = 50, offset = 0) {
|
|
const auth = loadAuth();
|
|
if (!auth?.user_id) throw new Error("No user id");
|
|
return apiFetch(`users/${auth.user_id}/playlists`, { limit, offset });
|
|
}
|
|
|
|
// ─── Fetch-all helpers ─────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Fetch every page of a paginated Tidal endpoint.
|
|
* `fetchFn(limit, offset)` must return `{ items: [...], totalNumberOfItems: N }`.
|
|
* Pages are fetched sequentially to avoid rate-limiting.
|
|
*/
|
|
async function fetchAllItems(fetchFn, pageSize = 50) {
|
|
const first = await fetchFn(pageSize, 0);
|
|
const firstItems = Array.isArray(first.items) ? first.items : [];
|
|
const hasKnownTotal = Number.isFinite(first.totalNumberOfItems);
|
|
const total = hasKnownTotal ? first.totalNumberOfItems : null;
|
|
let items = firstItems.slice();
|
|
|
|
const pageSignature = (pageItems) => {
|
|
// Handles both endpoint shapes:
|
|
// 1) flat items: { id, type, ... }
|
|
// 2) wrapped items: { type, item: { id, ... } }
|
|
const keys = pageItems
|
|
.map((it) => `${it?.type ?? ""}:${it?.item?.id ?? it?.id ?? ""}`)
|
|
.filter((k) => k !== ":");
|
|
if (!keys.length) return "";
|
|
return `${pageItems.length}|${keys.join("|")}`;
|
|
};
|
|
let lastSignature = pageSignature(firstItems);
|
|
|
|
let offset = items.length;
|
|
while (!hasKnownTotal || offset < total) {
|
|
const page = await fetchFn(pageSize, offset);
|
|
const pageItems = Array.isArray(page.items) ? page.items : [];
|
|
if (!pageItems.length) break;
|
|
const signature = pageSignature(pageItems);
|
|
if (!hasKnownTotal && signature && signature === lastSignature) break;
|
|
items = items.concat(pageItems);
|
|
lastSignature = signature;
|
|
offset += pageItems.length;
|
|
if (!hasKnownTotal && pageItems.length < pageSize) break;
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
export async function getAllUserFavoriteTracks() {
|
|
return fetchAllItems((limit, offset) => getUserFavoriteTracks(limit, offset));
|
|
}
|
|
|
|
export async function getAllUserFavoriteAlbums() {
|
|
return fetchAllItems((limit, offset) => getUserFavoriteAlbums(limit, offset));
|
|
}
|
|
|
|
export async function getAllUserFavoritePlaylists() {
|
|
return fetchAllItems((limit, offset) => getUserFavoritePlaylists(limit, offset));
|
|
}
|
|
|
|
export async function getAllUserPlaylists() {
|
|
return fetchAllItems((limit, offset) => getUserPlaylists(limit, offset));
|
|
}
|
|
|
|
export async function getAllAlbumItems(id) {
|
|
return fetchAllItems((limit, offset) => getAlbumItems(id, limit, offset));
|
|
}
|
|
|
|
export async function getAllPlaylistItems(uuid) {
|
|
return fetchAllItems((limit, offset) => getPlaylistItems(uuid, limit, offset));
|
|
}
|
|
|
|
export async function getAllMixItems(mixId) {
|
|
return fetchAllItems((limit, offset) => getMixItems(mixId, limit, offset));
|
|
}
|
|
|
|
export async function getSession() {
|
|
return apiFetch("sessions");
|
|
}
|
|
|
|
// ─── Streams ───────────────────────────────────────────────────────────────
|
|
|
|
export async function getTrackLyrics(trackId) {
|
|
return apiFetch(`tracks/${trackId}/lyrics`);
|
|
}
|
|
|
|
export async function getTrackStream(trackId, quality = "HIGH") {
|
|
return apiFetch(`tracks/${trackId}/playbackinfopostpaywall`, {
|
|
audioquality: quality,
|
|
playbackmode: "STREAM",
|
|
assetpresentation: "FULL",
|
|
});
|
|
}
|
|
|
|
export async function getVideoStream(videoId, quality = "HIGH") {
|
|
return apiFetch(`videos/${videoId}/playbackinfopostpaywall`, {
|
|
videoquality: quality,
|
|
playbackmode: "STREAM",
|
|
assetpresentation: "FULL",
|
|
});
|
|
}
|