mirror of
https://github.com/meowarex/TidaLuna-Plugins.git
synced 2026-06-17 19:33:10 +10:00
API Overhaul & Fixed Volume Slider <3
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
import { Tracer } from "@luna/core";
|
||||
|
||||
import { settings } from "./Settings";
|
||||
|
||||
const { trace } = Tracer("[Radiant Lyrics]");
|
||||
|
||||
const sylTrace = (...args: unknown[]) => {
|
||||
if (settings.syllableLogging) trace.log(...args);
|
||||
};
|
||||
|
||||
export const RL_PLATFORM = "Radiant Lyrics";
|
||||
|
||||
const RL_ACCESS_TOKEN_ID = "58hy4s86";
|
||||
const RL_ACCESS_TOKEN = "xjehy2lfg5h5mjwotoxrcqugam";
|
||||
// Yup that's right, plaintext token in a public Repo!!
|
||||
// The API does not return sensitive data & won't be plain text like this in the future <3
|
||||
|
||||
let cachedPublicIP: string | undefined;
|
||||
|
||||
export async function ip(): Promise<string | undefined> {
|
||||
if (cachedPublicIP) return cachedPublicIP;
|
||||
try {
|
||||
const res = await fetch("https://api.ipify.org?format=text");
|
||||
if (res.ok) cachedPublicIP = (await res.text()).trim();
|
||||
} catch {}
|
||||
return cachedPublicIP;
|
||||
}
|
||||
|
||||
export async function auth(): Promise<Record<string, string>> {
|
||||
const clientIP = await ip();
|
||||
return {
|
||||
"P-Access-Token-Id": RL_ACCESS_TOKEN_ID,
|
||||
"P-Access-Token": RL_ACCESS_TOKEN,
|
||||
"x-client-ip": clientIP ?? "null",
|
||||
};
|
||||
}
|
||||
|
||||
// Platform param (just for DX logging)
|
||||
const platformQs = `platform=${encodeURIComponent(RL_PLATFORM)}`;
|
||||
|
||||
// Query string & params
|
||||
function query(
|
||||
title: string,
|
||||
artist: string,
|
||||
isrc: string | undefined,
|
||||
options?: { romanize?: boolean; flush?: boolean },
|
||||
): string {
|
||||
let q = `?title=${encodeURIComponent(title)}&artist=${encodeURIComponent(artist)}`;
|
||||
if (isrc) q += `&isrc=${encodeURIComponent(isrc)}`;
|
||||
if (options?.romanize) q += "&romanize=true";
|
||||
if (options?.flush) q += "&flush=true";
|
||||
q += `&${platformQs}`;
|
||||
return q;
|
||||
}
|
||||
|
||||
// Response types
|
||||
|
||||
export interface WordTiming {
|
||||
text: string;
|
||||
time: number;
|
||||
duration: number;
|
||||
isBackground: boolean;
|
||||
romanized?: string;
|
||||
}
|
||||
|
||||
export interface WordLine {
|
||||
text: string;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
endTime: number;
|
||||
syllabus: WordTiming[];
|
||||
element: {
|
||||
key: string;
|
||||
songPart?: string;
|
||||
songPartIndex?: number;
|
||||
singer: string;
|
||||
};
|
||||
translation: string | null;
|
||||
romanized?: string;
|
||||
}
|
||||
|
||||
export interface ApiLine {
|
||||
text: string;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
endTime: number;
|
||||
syllabus?: WordTiming[];
|
||||
element?: {
|
||||
key: string;
|
||||
songPart?: string;
|
||||
songPartIndex?: number;
|
||||
singer?: string;
|
||||
};
|
||||
translation?: string | null;
|
||||
romanized?: string;
|
||||
}
|
||||
|
||||
export interface WordLyricsResponse {
|
||||
type: "Word";
|
||||
data: WordLine[];
|
||||
metadata: {
|
||||
source: string;
|
||||
title: string;
|
||||
language: string;
|
||||
totalDuration: string;
|
||||
agents?: Record<string, { type: string; name: string; alias: string }>;
|
||||
songParts?: Array<{ name: string; time: number; duration: number }>;
|
||||
};
|
||||
_cached?: boolean;
|
||||
}
|
||||
|
||||
export interface LineLyricsResponse {
|
||||
type: "Line";
|
||||
data: ApiLine[];
|
||||
metadata: {
|
||||
source: string;
|
||||
title: string;
|
||||
language: string;
|
||||
totalDuration: string;
|
||||
agents?: Record<string, { type: string; name: string; alias: string }>;
|
||||
songParts?: Array<{ name: string; time: number; duration: number }>;
|
||||
};
|
||||
_cached?: boolean;
|
||||
}
|
||||
|
||||
export type LyricsApiResponse = WordLyricsResponse | LineLyricsResponse;
|
||||
|
||||
type FetchOutcome =
|
||||
| { status: "ok"; data: LyricsApiResponse | null }
|
||||
| { status: "404" }
|
||||
| { status: "500" }
|
||||
| { status: "err" };
|
||||
|
||||
// Lyrics lookup (network)
|
||||
export async function fetchLyrics(
|
||||
title: string,
|
||||
artist: string,
|
||||
isrc: string | undefined,
|
||||
romanize: boolean,
|
||||
): Promise<LyricsApiResponse | null> {
|
||||
const params = query(title, artist, isrc, { romanize });
|
||||
const atomixUrl = `https://api.atomix.one/rl-api${params}`;
|
||||
const fallbackUrl = `https://rl-api.kineticsand.net/lyrics${params}`;
|
||||
|
||||
const rlApiHeaders = await auth();
|
||||
|
||||
const tryFetch = async (url: string, useAtomixAuth: boolean): Promise<FetchOutcome> => {
|
||||
try {
|
||||
sylTrace(`RL API: Fetching lyrics: ${url}`);
|
||||
const res = await fetch(url, {
|
||||
headers: useAtomixAuth ? rlApiHeaders : undefined,
|
||||
});
|
||||
if (!res.ok) {
|
||||
trace.log(`RL API: fetch failed: ${res.status} from ${url}`);
|
||||
if (res.status === 404) return { status: "404" };
|
||||
return res.status === 500 ? { status: "500" } : { status: "err" };
|
||||
}
|
||||
const data = (await res.json()) as LyricsApiResponse;
|
||||
if (!data?.data || !Array.isArray(data.data)) {
|
||||
trace.log("Lyrics API returned invalid payload");
|
||||
return { status: "ok", data: null };
|
||||
}
|
||||
if (data.type !== "Word" && data.type !== "Line") {
|
||||
trace.log("Lyrics not available in supported format");
|
||||
return { status: "ok", data: null };
|
||||
}
|
||||
return { status: "ok", data };
|
||||
} catch (err) {
|
||||
trace.log(`RL API: fetch error from ${url}: ${err}`);
|
||||
return { status: "err" };
|
||||
}
|
||||
};
|
||||
|
||||
const primary = await tryFetch(atomixUrl, true);
|
||||
if (primary.status === "ok") return primary.data;
|
||||
if (primary.status === "404") {
|
||||
trace.log("RL API: 404 — no API lyrics exist for this track");
|
||||
return null;
|
||||
}
|
||||
if (primary.status === "500") {
|
||||
trace.log("RL API: 500 (Execution Timeout) — fallback");
|
||||
}
|
||||
|
||||
const fallback = await tryFetch(fallbackUrl, false);
|
||||
if (fallback.status === "ok") return fallback.data;
|
||||
if (fallback.status === "404") {
|
||||
trace.log("RL API: 404 from fallback — no API lyrics exist for this track");
|
||||
return null;
|
||||
}
|
||||
if (fallback.status === "500") {
|
||||
trace.log("RL API: 500 from fallback — API IS ACTUALLY BORKED!");
|
||||
return null;
|
||||
}
|
||||
|
||||
trace.log("RL API: All Endpoints Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function flushLyrics(track: {
|
||||
title: string;
|
||||
artist: string;
|
||||
isrc?: string;
|
||||
}): Promise<
|
||||
| { ok: true; data: LyricsApiResponse & { _flush?: string } }
|
||||
| { ok: false; status: number; notFound: boolean }
|
||||
> {
|
||||
const q = query(track.title, track.artist, track.isrc, {
|
||||
flush: true,
|
||||
});
|
||||
const url = `https://api.atomix.one/rl-api${q}`;
|
||||
const headers = await auth();
|
||||
const res = await fetch(url, { headers });
|
||||
if (res.status === 404) {
|
||||
return { ok: false, status: 404, notFound: true };
|
||||
}
|
||||
if (!res.ok) {
|
||||
return { ok: false, status: res.status, notFound: false };
|
||||
}
|
||||
const data = (await res.json()) as LyricsApiResponse & { _flush?: string };
|
||||
return { ok: true, data };
|
||||
}
|
||||
|
||||
// Romanize
|
||||
export async function romanizeLyrics(
|
||||
lineTexts: string[],
|
||||
): Promise<string[] | null> {
|
||||
if (lineTexts.length === 0) return null;
|
||||
|
||||
const payload = {
|
||||
type: "Line" as const,
|
||||
data: lineTexts.map((text, idx) => ({
|
||||
text,
|
||||
startTime: idx,
|
||||
duration: 0,
|
||||
endTime: idx,
|
||||
})),
|
||||
};
|
||||
|
||||
const romanizeQuery = `?${platformQs}`;
|
||||
const urls: { url: string; useAtomixAuth: boolean }[] = [
|
||||
{
|
||||
url: `https://api.atomix.one/rl-api/romanize${romanizeQuery}`,
|
||||
useAtomixAuth: true,
|
||||
},
|
||||
{
|
||||
url: `https://rl-api.kineticsand.net/romanize${romanizeQuery}`,
|
||||
useAtomixAuth: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { url, useAtomixAuth } of urls) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
try {
|
||||
const romanizeHeaders: Record<string, string> = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
if (useAtomixAuth) {
|
||||
Object.assign(romanizeHeaders, await auth());
|
||||
}
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: romanizeHeaders,
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
if (!res.ok) {
|
||||
trace.log(`Romanize: request failed ${res.status} | ${url}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = (await res.json()) as {
|
||||
type?: string;
|
||||
data?: Array<{ text?: string; romanized?: string }>;
|
||||
};
|
||||
if (!Array.isArray(data?.data)) continue;
|
||||
|
||||
return lineTexts.map((original, idx) => {
|
||||
const item = data.data?.[idx];
|
||||
return item?.romanized ?? item?.text ?? original;
|
||||
});
|
||||
} catch (err) {
|
||||
clearTimeout(timeout);
|
||||
if (err instanceof DOMException && err.name === "AbortError") {
|
||||
trace.log(`Romanize: request timed out | ${url}`);
|
||||
} else {
|
||||
trace.log(`Romanize: request error | ${url} | ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -9,6 +9,15 @@ import {
|
||||
safeTimeout,
|
||||
redux,
|
||||
} from "@luna/lib";
|
||||
import {
|
||||
type ApiLine,
|
||||
type LyricsApiResponse,
|
||||
type WordLine,
|
||||
type WordTiming,
|
||||
fetchLyrics as fetchLyricsApi,
|
||||
flushLyrics as flushLyricsApi,
|
||||
romanizeLyrics as romanizeLyricsApi,
|
||||
} from "./api";
|
||||
import { Settings, settings } from "./Settings";
|
||||
|
||||
// Interpret integer backgroundScale (e.g., 10=1.0x, 20=2.0x)
|
||||
@@ -40,16 +49,6 @@ const toastErr = (msg: string) =>
|
||||
// clean up resources
|
||||
export const unloads = new Set<LunaUnload>();
|
||||
|
||||
let cachedPublicIP: string | undefined;
|
||||
async function getPublicIPv4(): Promise<string | undefined> {
|
||||
if (cachedPublicIP) return cachedPublicIP;
|
||||
try {
|
||||
const res = await fetch("https://api.ipify.org?format=text");
|
||||
if (res.ok) cachedPublicIP = (await res.text()).trim();
|
||||
} catch {}
|
||||
return cachedPublicIP;
|
||||
}
|
||||
|
||||
// MARKER: Player Market UI (Ensure new UI is enabled)
|
||||
|
||||
function enablePlayerMarketUI() {
|
||||
@@ -199,11 +198,8 @@ const applyFloatingPlayerBar = (): void => {
|
||||
// Alias for settings callback
|
||||
const updateRadiantLyricsPlayerBarTint = applyFloatingPlayerBar;
|
||||
|
||||
// Apply floating player bar + tint on load
|
||||
// Apply floating player bar on load (Fixes race condition with flush button)
|
||||
applyFloatingPlayerBar();
|
||||
observe<HTMLElement>(unloads, '[data-test="footer-player"]', () => {
|
||||
applyPlayerBarTintToElement();
|
||||
});
|
||||
|
||||
// MARKER: Quality-Based Seeker Color
|
||||
// Maps data-test-quality-badge-streaming-quality values to colors
|
||||
@@ -417,9 +413,10 @@ const applyIntegratedSeekBar = (): void => {
|
||||
}, 250);
|
||||
};
|
||||
|
||||
// Apply on load
|
||||
// Apply integrated seek bar on load (fixes race condition with flush button)
|
||||
applyIntegratedSeekBar();
|
||||
observe<HTMLElement>(unloads, '[data-test="footer-player"]', () => {
|
||||
applyPlayerBarTintToElement();
|
||||
applyIntegratedSeekBar();
|
||||
});
|
||||
|
||||
@@ -572,37 +569,26 @@ const flushLyrics = async (): Promise<void> => {
|
||||
|
||||
lockFlush();
|
||||
|
||||
let params = `?title=${encodeURIComponent(trackInfo.title)}&artist=${encodeURIComponent(trackInfo.artist)}`;
|
||||
if (trackInfo.isrc) params += `&isrc=${encodeURIComponent(trackInfo.isrc)}`;
|
||||
params += "&flush=true&platform=" + encodeURIComponent("Radiant Lyrics");
|
||||
|
||||
const url = `https://api.atomix.one/rl-api${params}`;
|
||||
try {
|
||||
const clientIP = await getPublicIPv4();
|
||||
const flushHeaders: Record<string, string> = {
|
||||
"P-Access-Token-Id": "58hy4s86",
|
||||
"P-Access-Token": "xjehy2lfg5h5mjwotoxrcqugam",
|
||||
};
|
||||
flushHeaders["x-client-ip"] = clientIP ?? "null";
|
||||
const res = await fetch(url, { headers: flushHeaders });
|
||||
if (res.status === 404) {
|
||||
const result = await flushLyricsApi(trackInfo);
|
||||
if (!result.ok) {
|
||||
if (result.notFound) {
|
||||
toast("No lyrics found for this track");
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
toastErr(`Flush failed (${res.status})`);
|
||||
toastErr(`Flush failed (${result.status})`);
|
||||
return;
|
||||
}
|
||||
const data = (await res.json()) as LyricsApiResponse & { _flush?: string };
|
||||
const flush = data?._flush ?? "";
|
||||
const data = result.data;
|
||||
const note = data?._flush ?? "";
|
||||
|
||||
const needsReload = flush.startsWith("Created") || flush.startsWith("Updated");
|
||||
if (flush) {
|
||||
toast(flush);
|
||||
const needsReload = note.startsWith("Created") || note.startsWith("Updated");
|
||||
if (note) {
|
||||
toast(note);
|
||||
} else {
|
||||
toast("Lyrics flushed");
|
||||
}
|
||||
if (needsReload || !flush) {
|
||||
if (needsReload || !note) {
|
||||
cachedLyricsKey = null;
|
||||
cachedLyricsData = null;
|
||||
onTrackChange();
|
||||
@@ -612,7 +598,7 @@ const flushLyrics = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
const createFlushButton = function (): void {
|
||||
const flushBtn = function (): void {
|
||||
const closeButton = document.querySelector(
|
||||
'[data-test="new-now-playing-close"]',
|
||||
) as HTMLButtonElement;
|
||||
@@ -1605,75 +1591,6 @@ const onGlobalTrackChange = (listener: () => void): void => {
|
||||
|
||||
// MARKER: Syllable Lyrics
|
||||
|
||||
interface WordTiming {
|
||||
text: string;
|
||||
time: number; // ms
|
||||
duration: number; // ms
|
||||
isBackground: boolean;
|
||||
romanized?: string;
|
||||
}
|
||||
|
||||
interface WordLine {
|
||||
text: string;
|
||||
startTime: number; // s
|
||||
duration: number; // s
|
||||
endTime: number; // s
|
||||
syllabus: WordTiming[];
|
||||
element: {
|
||||
key: string;
|
||||
songPart?: string;
|
||||
songPartIndex?: number;
|
||||
singer: string;
|
||||
};
|
||||
translation: string | null;
|
||||
romanized?: string;
|
||||
}
|
||||
|
||||
interface ApiLine {
|
||||
text: string;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
endTime: number;
|
||||
syllabus?: WordTiming[];
|
||||
element?: {
|
||||
key: string;
|
||||
songPart?: string;
|
||||
songPartIndex?: number;
|
||||
singer?: string;
|
||||
};
|
||||
translation?: string | null;
|
||||
romanized?: string;
|
||||
}
|
||||
|
||||
interface WordLyricsResponse {
|
||||
type: "Word";
|
||||
data: WordLine[];
|
||||
metadata: {
|
||||
source: string;
|
||||
title: string;
|
||||
language: string;
|
||||
totalDuration: string;
|
||||
agents?: Record<string, { type: string; name: string; alias: string }>;
|
||||
songParts?: Array<{ name: string; time: number; duration: number }>;
|
||||
};
|
||||
_cached?: boolean;
|
||||
}
|
||||
|
||||
interface LineLyricsResponse {
|
||||
type: "Line";
|
||||
data: ApiLine[];
|
||||
metadata: {
|
||||
source: string;
|
||||
title: string;
|
||||
language: string;
|
||||
totalDuration: string;
|
||||
agents?: Record<string, { type: string; name: string; alias: string }>;
|
||||
songParts?: Array<{ name: string; time: number; duration: number }>;
|
||||
};
|
||||
_cached?: boolean;
|
||||
}
|
||||
|
||||
type LyricsApiResponse = WordLyricsResponse | LineLyricsResponse;
|
||||
type LyricsOverlayMode = "none" | "word" | "line-api" | "line-tidal";
|
||||
|
||||
interface TrackInfo {
|
||||
@@ -2345,99 +2262,15 @@ const fetchLyrics = async (
|
||||
sylLog(`[RL-Syllable] Cache hit for "${title}" by "${artist}"`);
|
||||
return cachedLyricsData;
|
||||
}
|
||||
|
||||
let params = `?title=${encodeURIComponent(title)}&artist=${encodeURIComponent(artist)}`;
|
||||
if (isrc) params += `&isrc=${encodeURIComponent(isrc)}`;
|
||||
if (settings.romanizeLyrics) params += "&romanize=true";
|
||||
|
||||
const platformParam = "&platform=" + encodeURIComponent("Radiant Lyrics");
|
||||
const primaryUrls = [
|
||||
`https://api.atomix.one/rl-api${params}${platformParam}`,
|
||||
`https://lyricsplus-api.atomix.one/lyrics${params}${platformParam}`,
|
||||
];
|
||||
const fallbackUrl = `https://rl-api.kineticsand.net/lyrics${params}`;
|
||||
|
||||
// "ok" = got a response (data may still be null if type is unsupported)
|
||||
// "404" = lyrics not found, stop all attempts immediately
|
||||
// "500" = serverless timeout, skip remaining primaries and go to fallback
|
||||
// "err" = network/other error, try next host
|
||||
type FetchOutcome =
|
||||
| { status: "ok"; data: LyricsApiResponse | null }
|
||||
| { status: "404" }
|
||||
| { status: "500" }
|
||||
| { status: "err" };
|
||||
|
||||
const rlApiHeaders: Record<string, string> = {
|
||||
"P-Access-Token-Id": "58hy4s86",
|
||||
"P-Access-Token": "xjehy2lfg5h5mjwotoxrcqugam",
|
||||
};
|
||||
const clientIP = await getPublicIPv4();
|
||||
rlApiHeaders["x-client-ip"] = clientIP ?? "null";
|
||||
|
||||
const tryFetch = async (url: string): Promise<FetchOutcome> => {
|
||||
try {
|
||||
sylTrace(`RL API: Fetching lyrics: ${url}`);
|
||||
const res = await fetch(url, {
|
||||
headers: url.includes("api.atomix.one") ? rlApiHeaders : undefined,
|
||||
});
|
||||
if (!res.ok) {
|
||||
trace.log(`RL API: fetch failed: ${res.status} from ${url}`);
|
||||
if (res.status === 404) return { status: "404" };
|
||||
return res.status === 500 ? { status: "500" } : { status: "err" };
|
||||
}
|
||||
const data = (await res.json()) as LyricsApiResponse;
|
||||
if (!data?.data || !Array.isArray(data.data)) {
|
||||
trace.log("Lyrics API returned invalid payload");
|
||||
return { status: "ok", data: null };
|
||||
}
|
||||
if (data.type !== "Word" && data.type !== "Line") {
|
||||
trace.log("Lyrics not available in supported format");
|
||||
return { status: "ok", data: null };
|
||||
}
|
||||
return { status: "ok", data };
|
||||
} catch (err) {
|
||||
trace.log(`RL API: fetch error from ${url}: ${err}`);
|
||||
return { status: "err" };
|
||||
}
|
||||
};
|
||||
|
||||
const finish = (data: LyricsApiResponse | null): LyricsApiResponse | null => {
|
||||
const data = await fetchLyricsApi(
|
||||
title,
|
||||
artist,
|
||||
isrc,
|
||||
settings.romanizeLyrics,
|
||||
);
|
||||
cachedLyricsKey = cacheKey;
|
||||
cachedLyricsData = data;
|
||||
return data;
|
||||
};
|
||||
|
||||
// Try primary hosts; bail to fallback immediately on 500, stop entirely on 404
|
||||
for (const url of primaryUrls) {
|
||||
const outcome = await tryFetch(url);
|
||||
if (outcome.status === "ok") return finish(outcome.data);
|
||||
if (outcome.status === "404") {
|
||||
trace.log("RL API: 404 — no API lyrics exist for this track");
|
||||
return finish(null);
|
||||
}
|
||||
if (outcome.status === "500") {
|
||||
trace.log("RL API: 500 (Execution Timeout) — fallback");
|
||||
break;
|
||||
}
|
||||
// "err" → try next primary
|
||||
}
|
||||
|
||||
// Fallback: kineticsand (no serverless timeout)
|
||||
const fallback = await tryFetch(fallbackUrl);
|
||||
if (fallback.status === "ok") return finish(fallback.data);
|
||||
if (fallback.status === "404") {
|
||||
trace.log("RL API: 404 from fallback — no API lyrics exist for this track");
|
||||
return finish(null);
|
||||
}
|
||||
if (fallback.status === "500") {
|
||||
trace.log("RL API: 500 from fallback — API IS ACTUALLY BORKED!");
|
||||
return finish(null);
|
||||
}
|
||||
|
||||
trace.log("RL API: All Endpoints Failed");
|
||||
cachedLyricsKey = cacheKey;
|
||||
cachedLyricsData = null;
|
||||
return null;
|
||||
};
|
||||
|
||||
const normalizeLineData = (data: ApiLine[]): WordLine[] => {
|
||||
@@ -2497,7 +2330,7 @@ const getTidalLines = (): string[] => {
|
||||
.filter((text) => text.trim().length > 0);
|
||||
};
|
||||
|
||||
const romanizeLines = async (lineTexts: string[]): Promise<string[] | null> => {
|
||||
const romanizeLyrics = async (lineTexts: string[]): Promise<string[] | null> => {
|
||||
if (!settings.romanizeLyrics || lineTexts.length === 0) return null;
|
||||
|
||||
const cacheKey = `${lineTexts.join("\n")}\0r`;
|
||||
@@ -2505,70 +2338,12 @@ const romanizeLines = async (lineTexts: string[]): Promise<string[] | null> => {
|
||||
return cachedTidalRomanizedLines;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
type: "Line" as const,
|
||||
data: lineTexts.map((text, idx) => ({
|
||||
text,
|
||||
startTime: idx,
|
||||
duration: 0,
|
||||
endTime: idx,
|
||||
})),
|
||||
};
|
||||
|
||||
const romanizePlatform = "?platform=" + encodeURIComponent("Radiant Lyrics");
|
||||
const urls = [
|
||||
`https://api.atomix.one/rl-api/romanize${romanizePlatform}`,
|
||||
`https://lyricsplus-api.atomix.one/romanize${romanizePlatform}`,
|
||||
"https://rl-api.kineticsand.net/romanize",
|
||||
];
|
||||
|
||||
for (const url of urls) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
try {
|
||||
const romanizeHeaders: Record<string, string> = { "content-type": "application/json" };
|
||||
if (url.includes("api.atomix.one")) {
|
||||
romanizeHeaders["P-Access-Token-Id"] = "58hy4s86";
|
||||
romanizeHeaders["P-Access-Token"] = "xjehy2lfg5h5mjwotoxrcqugam";
|
||||
const ip = await getPublicIPv4();
|
||||
romanizeHeaders["x-client-ip"] = ip ?? "null";
|
||||
}
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: romanizeHeaders,
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
if (!res.ok) {
|
||||
trace.log(`Romanize: request failed ${res.status} from ${url}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = (await res.json()) as {
|
||||
type?: string;
|
||||
data?: Array<{ text?: string; romanized?: string }>;
|
||||
};
|
||||
if (!Array.isArray(data?.data)) continue;
|
||||
|
||||
const romanized = lineTexts.map((original, idx) => {
|
||||
const item = data.data?.[idx];
|
||||
return item?.romanized ?? item?.text ?? original;
|
||||
});
|
||||
const romanized = await romanizeLyricsApi(lineTexts);
|
||||
if (romanized) {
|
||||
cachedTidalRomanizeKey = cacheKey;
|
||||
cachedTidalRomanizedLines = romanized;
|
||||
}
|
||||
return romanized;
|
||||
} catch (err) {
|
||||
clearTimeout(timeout);
|
||||
if (err instanceof DOMException && err.name === "AbortError") {
|
||||
trace.log(`Romanize: request timed out from ${url}`);
|
||||
} else {
|
||||
trace.log(`Romanize: request error from ${url}: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// strip tidal css classes (prevent conflict)
|
||||
@@ -4142,7 +3917,7 @@ const onTrackChange = async (): Promise<void> => {
|
||||
disableFlushNoLyrics();
|
||||
const tidalTexts = getTidalLines();
|
||||
const romanized = settings.romanizeLyrics
|
||||
? await romanizeLines(tidalTexts)
|
||||
? await romanizeLyrics(tidalTexts)
|
||||
: null;
|
||||
if (token !== trackChangeToken) return;
|
||||
cachedTidalRomanizedLines = romanized;
|
||||
@@ -4303,7 +4078,7 @@ const reapplyTidalLines = async (): Promise<void> => {
|
||||
lyricsMode = "line-tidal";
|
||||
const tidalTexts = getTidalLines();
|
||||
const romanized = settings.romanizeLyrics
|
||||
? await romanizeLines(tidalTexts)
|
||||
? await romanizeLyrics(tidalTexts)
|
||||
: null;
|
||||
hideTidalLyrics();
|
||||
const result = buildTidalLines(romanized);
|
||||
@@ -4380,7 +4155,7 @@ const setupTrackChangeListener = (): void => {
|
||||
|
||||
function setupHeaderObserver(): void {
|
||||
const injectButtons = () => {
|
||||
if (!document.querySelector(".flush-lyrics-button")) createFlushButton();
|
||||
if (!document.querySelector(".flush-lyrics-button")) flushBtn();
|
||||
if (!document.querySelector(".hide-ui-button")) createHideUIButton();
|
||||
};
|
||||
|
||||
|
||||
@@ -322,101 +322,12 @@ body.rl-integrated-seekbar .rl-seekbar-bar:hover [data-test="progress-bar"] [dat
|
||||
height: 5px !important;
|
||||
}
|
||||
|
||||
|
||||
/* MARKER: PATCHES (Random Fixes for Tidals Changes) */
|
||||
/* These change a lot so I gave them their own section */
|
||||
|
||||
/* Remove max-width cap on now-playing content */
|
||||
[class*="_contentInner"] {
|
||||
max-width: none !important;
|
||||
/* Z-Index Fix for Integrated Seekbar (Volume slider) */
|
||||
body.rl-integrated-seekbar [data-test="footer-player"] [class*="playerContent"] > [class*="utilityContainer"] {
|
||||
position: relative;
|
||||
z-index: 110;
|
||||
}
|
||||
|
||||
/* Round now-playing artwork corners */
|
||||
[data-test="now-playing-artwork"] {
|
||||
/* biome-ignore lint: Override flat corners */
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
/* Hide the Overlay Scrollbar (people just use mouse scroll) */
|
||||
.os-scrollbar {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
._artworkTilt_1c6d5cc {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Hide fullscreen button — breaks Radiant Lyrics */
|
||||
[data-test="new-now-playing-expand"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Restore the Old Quality Tag style | thx Aya <3 */
|
||||
|
||||
._gradientMax_9111fba {
|
||||
background-color: #ffd4321a !important;
|
||||
box-shadow: none;
|
||||
border-style: none;
|
||||
border-radius: 0.75em;
|
||||
}
|
||||
|
||||
._max_894bc7c ._badgeText_1c9dd30 {
|
||||
color: #ffd432 !important;
|
||||
text-shadow: 0 0 10px #0000 !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 90% !important;
|
||||
}
|
||||
|
||||
._gradientHigh_87f2c3b {
|
||||
background-color: #073430 !important;
|
||||
box-shadow: none;
|
||||
border-style: none;
|
||||
border-radius: 0.75em;
|
||||
}
|
||||
|
||||
._high_4b5525b ._badgeText_1c9dd30 {
|
||||
color: #33ffee !important;
|
||||
text-shadow: none !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 90% !important;
|
||||
}
|
||||
|
||||
._gradientLow_3f9bc0d {
|
||||
background-color: #ffffff1a !important;
|
||||
box-shadow: none;
|
||||
border-style: none;
|
||||
border-radius: 0.75em;
|
||||
}
|
||||
|
||||
._badgeText_1c9dd30 {
|
||||
color: #e4e4e7 !important;
|
||||
text-shadow: none;
|
||||
font-weight: 600 !important;
|
||||
font-size: 90% !important;
|
||||
}
|
||||
|
||||
._badge_7b2911e {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
height: 33px;
|
||||
padding: 0 !important;
|
||||
width: 111px;
|
||||
min-width: 111px;
|
||||
border-radius: 0.75em;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
._badge_7b2911e:hover {
|
||||
transition: 100ms;
|
||||
filter: saturate(1.25) brightness(1.1);
|
||||
}
|
||||
|
||||
._glowEffect_74c5e85 {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* MARKER: Lyrics core CSS (always loaded) */
|
||||
|
||||
@@ -880,3 +791,99 @@ body.rl-integrated-seekbar .rl-seekbar-bar:hover [data-test="progress-bar"] [dat
|
||||
padding-right: calc(var(--rl-glow-outer, 20px) + 0px) !important; /* 4px cushion (not needed atm) */
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
|
||||
/* MARKER: PATCHES (Random Fixes for Tidals Changes) */
|
||||
/* These change a lot so I gave them their own section */
|
||||
|
||||
/* Remove max-width cap on now-playing content */
|
||||
[class*="_contentInner"] {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Round now-playing artwork corners */
|
||||
[data-test="now-playing-artwork"] {
|
||||
/* biome-ignore lint: Override flat corners */
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
/* Hide the Overlay Scrollbar (people just use mouse scroll) */
|
||||
.os-scrollbar {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
._artworkTilt_1c6d5cc {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Hide fullscreen button — breaks Radiant Lyrics */
|
||||
[data-test="new-now-playing-expand"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Restore the Old Quality Tag style | thx Aya <3 */
|
||||
|
||||
._gradientMax_9111fba {
|
||||
background-color: #ffd4321a !important;
|
||||
box-shadow: none;
|
||||
border-style: none;
|
||||
border-radius: 0.75em;
|
||||
}
|
||||
|
||||
._max_894bc7c ._badgeText_1c9dd30 {
|
||||
color: #ffd432 !important;
|
||||
text-shadow: 0 0 10px #0000 !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 90% !important;
|
||||
}
|
||||
|
||||
._gradientHigh_87f2c3b {
|
||||
background-color: #073430 !important;
|
||||
box-shadow: none;
|
||||
border-style: none;
|
||||
border-radius: 0.75em;
|
||||
}
|
||||
|
||||
._high_4b5525b ._badgeText_1c9dd30 {
|
||||
color: #33ffee !important;
|
||||
text-shadow: none !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 90% !important;
|
||||
}
|
||||
|
||||
._gradientLow_3f9bc0d {
|
||||
background-color: #ffffff1a !important;
|
||||
box-shadow: none;
|
||||
border-style: none;
|
||||
border-radius: 0.75em;
|
||||
}
|
||||
|
||||
._badgeText_1c9dd30 {
|
||||
color: #e4e4e7 !important;
|
||||
text-shadow: none;
|
||||
font-weight: 600 !important;
|
||||
font-size: 90% !important;
|
||||
}
|
||||
|
||||
._badge_7b2911e {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
height: 33px;
|
||||
padding: 0 !important;
|
||||
width: 111px;
|
||||
min-width: 111px;
|
||||
border-radius: 0.75em;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
._badge_7b2911e:hover {
|
||||
transition: 100ms;
|
||||
filter: saturate(1.25) brightness(1.1);
|
||||
}
|
||||
|
||||
._glowEffect_74c5e85 {
|
||||
display: none !important;
|
||||
}
|
||||
Reference in New Issue
Block a user