Skip to content

Commit f839889

Browse files
Publish Skriptey Userscripts (2026-06-17)
1 parent 8512577 commit f839889

1 file changed

Lines changed: 57 additions & 21 deletions

File tree

scripts/ITAMenhancer/ITAMenhancer.user.js

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// ==UserScript==
22
// @name ITAM Enhancer
33
// @namespace https://github.com/Skriptey/Userscripts
4-
// @version 1.7.0
4+
// @version 1.7.1
55
// @description iTunes/Apple Music Enhancer — shows audio formats (album + per-track), barcodes (UPC) and per-track ISRCs with one-click copy and a MagicISRC link (resolved via a MusicBrainz barcode lookup), adds inline album-header buttons, a Harmony cross-service lookup, cover-art download (static + animated/motion artwork), synced/word-by-word lyrics download, and per-track ISWC lookup (MusicBrainz + credits.fm) with MusicBrainz seeding — on Apple Music (music.apple.com) and Apple Music Classical (classical.music.apple.com), with a per-track Work column for classical releases.
66
// @author Skriptey
77
// @license GPL-3.0-or-later
@@ -325,7 +325,27 @@
325325
);
326326
}
327327

328+
/** Show a status message. Renders an ON-PAGE toast (always visible) AND fires a
329+
* native notification. The on-page toast matters because GM_notification
330+
* silently no-ops when notifications aren't granted — which made downloads with
331+
* no obvious file (e.g. a single .lrc) look like they "did nothing". */
328332
function toast(text) {
333+
try {
334+
let host = document.getElementById('itam-toasts');
335+
if (!host) {
336+
host = el('div', { id: 'itam-toasts', class: 'itam-toasts' });
337+
(document.body || document.documentElement).appendChild(host);
338+
}
339+
const t = el('div', { class: 'itam-toast', text });
340+
host.appendChild(t);
341+
requestAnimationFrame(() => t.classList.add('show'));
342+
setTimeout(() => {
343+
t.classList.remove('show');
344+
setTimeout(() => t.remove(), 300);
345+
}, 4000);
346+
} catch {
347+
/* ignore */
348+
}
329349
try {
330350
GM_notification({ title: 'ITAM Enhancer', text, silent: true, timeout: 2500 });
331351
} catch {
@@ -821,8 +841,11 @@
821841
url: API_BASE + path,
822842
headers,
823843
onload: (res) => {
824-
if (res.status < 200 || res.status >= 300)
825-
return reject(new Error(`API HTTP ${res.status}`));
844+
if (res.status < 200 || res.status >= 300) {
845+
const e = new Error(`API HTTP ${res.status}`);
846+
e.status = res.status; // let callers (e.g. lyrics) report the code
847+
return reject(e);
848+
}
826849
try {
827850
resolve(JSON.parse(res.responseText));
828851
} catch {
@@ -1046,11 +1069,15 @@
10461069
devToken,
10471070
userToken,
10481071
);
1049-
} catch {
1072+
} catch (err) {
1073+
// Log the status (NOT the lyrics) so a failing endpoint/auth is diagnosable.
1074+
console.warn(`[ITAM] lyrics ${kind} ${songId}: ${err.status || err.message}`);
10501075
return null;
10511076
}
10521077
const ttml = json && json.data && json.data[0] && json.data[0].attributes?.ttml;
1053-
return typeof ttml === 'string' && ttml ? ttml : null;
1078+
if (typeof ttml === 'string' && ttml) return ttml;
1079+
console.warn(`[ITAM] lyrics ${kind} ${songId}: 200 but no ttml in response`);
1080+
return null;
10541081
}
10551082

10561083
/** Resolve one track's lyrics in the requested tier, AUTOMATICALLY FALLING BACK
@@ -1145,7 +1172,7 @@
11451172
if ((TIER_RANK[lyr.kind] || 0) < (TIER_RANK[tier] || 0)) fellBack.add(lyr.kind);
11461173
}
11471174
}
1148-
if (!out.length) return toast('No downloadable lyrics found for this release');
1175+
if (!out.length) return toast('No lyrics returned for this release (see console for status)');
11491176
const note = fellBack.size ? ` (some fell back to ${[...fellBack].sort().join(' / ')})` : '';
11501177

11511178
if (model.kind !== 'album' || out.length === 1) {
@@ -1527,7 +1554,7 @@
15271554
display:flex; align-items:flex-start; justify-content:center; padding:5vh 12px; }
15281555
/* Width grows with the viewport (up to a cap) and shrinks on narrow screens;
15291556
container-type lets the track table hide columns responsively (below). */
1530-
.itam-panel { width:min(1200px,94vw); max-height:90vh; overflow:auto; container-type:inline-size;
1557+
.itam-panel { width:min(1760px,94vw); max-height:90vh; overflow:auto; container-type:inline-size;
15311558
background:#1c1c1e; color:#f2f2f7; border:1px solid #3a3a3c; border-radius:14px;
15321559
padding:18px 20px; font:14px/1.5 system-ui,-apple-system,sans-serif;
15331560
box-shadow:0 18px 50px rgba(0,0,0,.6); }
@@ -1555,6 +1582,14 @@
15551582
.itam-mfit.off { opacity:.4; }
15561583
/* Hide the Composer column when the panel is too narrow to fit it. */
15571584
@container (max-width: 620px) { .itam-col-composer { display:none; } }
1585+
/* On-page status toasts (visible even when GM_notification isn't granted). */
1586+
.itam-toasts { position:fixed; left:50%; bottom:24px; transform:translateX(-50%);
1587+
z-index:2147483647; display:flex; flex-direction:column; gap:8px; align-items:center;
1588+
pointer-events:none; }
1589+
.itam-toast { background:#1c1c1e; color:#f2f2f7; border:1px solid #48484a; border-radius:10px;
1590+
padding:9px 14px; font:600 13px system-ui,sans-serif; box-shadow:0 8px 24px rgba(0,0,0,.5);
1591+
max-width:80vw; opacity:0; transform:translateY(8px); transition:opacity .25s, transform .25s; }
1592+
.itam-toast.show { opacity:1; transform:translateY(0); }
15581593
@media (prefers-color-scheme: light) {
15591594
.itam-panel { background:#fff; color:#1c1c1e; border-color:#d1d1d6; }
15601595
.itam-dd-menu { background:#fff; color:#1c1c1e; border-color:#d1d1d6; }
@@ -2202,34 +2237,35 @@
22022237
* maybeHeaderUI is serialised, so repeated attempts are cheap. */
22032238
function placeHeaderUI() {
22042239
if (placeStop) placeStop(); // cancel a previous route's attempts
2205-
const page = parsePage();
22062240
const placed = () =>
22072241
document.querySelector('.itam-inline-actions') || document.querySelector('.itam-badges');
2208-
const deadline = Date.now() + 20000; // overall cap (mostly waiting for the token)
2209-
let settledAt = 0; // when the model first became available (cached)
2242+
const startedAt = Date.now();
2243+
let everPlaced = false;
22102244
let debounce = null;
22112245
const attempt = () => {
2212-
if (placed() || Date.now() > deadline) return stop();
2213-
// Once the model is cached the header renders quickly — so if nothing has
2214-
// been placed ~3s after the data is ready, there is simply nothing to place
2215-
// here (e.g. a logged-out song page); stop, rather than churn for 20s.
2216-
if (page && entityCache.has(page.id)) {
2217-
if (!settledAt) settledAt = Date.now();
2218-
else if (Date.now() - settledAt > 3000) return stop();
2219-
}
2246+
// maybeHeaderUI is idempotent — it (re-)injects only when the badges/actions
2247+
// are MISSING. So this both places them late AND re-places them if Apple
2248+
// Music's React re-render wipes them — the cause of the inconsistent
2249+
// appearance (the inline UI would vanish on a re-render and never return).
22202250
maybeHeaderUI();
2251+
if (placed()) everPlaced = true;
2252+
// Give up only if nothing has EVER placed after 30s — i.e. there is
2253+
// genuinely nothing to place here (e.g. a logged-out song page). Once it has
2254+
// placed, keep watching for the route's lifetime so a later wipe is
2255+
// re-injected; the observer is torn down on the next route change (placeStop).
2256+
else if (!everPlaced && Date.now() - startedAt > 30000) stop();
22212257
};
2222-
// Debounce the (very chatty) SPA mutations so attempt() runs at most ~4×/s.
2258+
// Debounce the (very chatty) SPA mutations so attempt() runs at most ~3×/s.
22232259
const schedule = () => {
22242260
if (debounce) return;
22252261
debounce = setTimeout(() => {
22262262
debounce = null;
22272263
attempt();
2228-
}, 250);
2264+
}, 350);
22292265
};
22302266
const obs = new MutationObserver(schedule);
22312267
obs.observe(document.body, { childList: true, subtree: true });
2232-
const poll = setInterval(attempt, 700);
2268+
const poll = setInterval(attempt, 1200);
22332269
function stop() {
22342270
obs.disconnect();
22352271
clearInterval(poll);

0 commit comments

Comments
 (0)