Release info + metadata panel using Beatsource API
// ==UserScript==
// @name Beatsource Metadata Inspector
// @namespace beatsource-metadata-inspector
// @version 1.2.12
// @description Release info + metadata panel using Beatsource API
// @match https://www.beatsource.com/release/*
// @license MIT
// @grant none
// ==/UserScript==
(function () {
"use strict";
/******************************************************************
* CONFIG
******************************************************************/
const PANEL_ID = "bs-release-info-panel";
const RELEASE_DATE_FIELDS = [
"new_release_date",
"publish_date",
"pre_order_date",
"encoded_date",
"updated"
];
const TRACK_DATE_FIELDS = [
"new_release_date",
"publish_date",
"pre_order_date",
"encoded_date"
];
/******************************************************************
* HELPERS
******************************************************************/
function log(...a) {
console.log("[BS Panel]", ...a);
}
function isReleasePage() {
return /^\/release\/.+/.test(location.pathname);
}
function removePanel() {
document.getElementById(PANEL_ID)?.remove();
}
function getBuildId() {
return window.__NEXT_DATA__?.buildId;
}
function getReleaseIdFromUrl() {
const urlPath = location.pathname;
const parts = urlPath.split("/");
return parts[3]; // /release/title/ID
}
function normalize(val) {
if (!val) return null;
return String(val).trim();
}
function equalNormalized(a, b) {
return normalize(a) && normalize(a) === normalize(b);
}
function extractIdentifiers(release) {
if (!release) return {};
return {
upc: normalize(release.upc),
catalog_number: normalize(release.catalog_number),
gridName: normalize(release.gridName),
grid: normalize(release.grid)
};
}
function determinePrimaryCatalog({ upc, catalog_number, gridName, grid }) {
if (catalog_number && !equalNormalized(catalog_number, upc)) {
return { source: "catalog_number", value: catalog_number };
}
if (gridName) return { source: "gridName", value: gridName };
if (grid) return { source: "grid", value: grid };
return null;
}
function extractDates(obj, fields) {
const out = {};
for (const f of fields) {
const v = obj?.[f];
if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}/.test(v)) {
out[f] = v.slice(0, 10);
}
}
return out;
}
function getConflictingDates(trackDates, releaseDates) {
const conflicts = {};
for (const key in trackDates) {
if (!releaseDates[key]) continue;
if (trackDates[key] !== releaseDates[key]) {
conflicts[key] = trackDates[key];
}
}
return conflicts;
}
function detectReleaseWarnings(dates) {
const warnings = [];
const main = dates.new_release_date;
if (!main) return warnings;
// YYYY-01-01 placeholder detection
if (/^\d{4}-01-01$/.test(main)) {
warnings.push("⚠ 1 January placeholder date");
}
// pre Spotify launch
if (main < "2008-10-07") {
warnings.push("⚠ Pre-Spotify launch date");
}
// pre iTunes launch
if (main < "2001-01-09") {
warnings.push("⚠ Pre-iTunes launch date");
}
return warnings;
}
/******************************************************************
* API FETCH
******************************************************************/
async function fetchReleaseData() {
const buildId = getBuildId();
const id = getReleaseIdFromUrl();
if (!buildId || !id) return null;
try {
const res = await fetch(
`/_next/data/${buildId}/release.json?id=${id}`,
{ credentials: "same-origin" }
);
if (!res.ok) throw new Error(res.status);
const json = await res.json();
return {
release: json?.pageProps?.release || null,
tracks: json?.pageProps?.tracks?.results || []
};
} catch (e) {
log("Fetch failed:", e);
return null;
}
}
/******************************************************************
* METADATA EXTRACTION
******************************************************************/
function extractReleaseMeta(release) {
return {
dates: extractDates(release, RELEASE_DATE_FIELDS),
warnings: detectReleaseWarnings(
extractDates(release, RELEASE_DATE_FIELDS)
)
};
}
/******************************************************************
* PANEL UI
******************************************************************/
function createPanel(release, tracks) {
removePanel();
const meta = extractReleaseMeta(release);
const releaseDates = meta.dates;
const container = document.createElement("div");
container.id = PANEL_ID;
// Gray container styling (like your preferred scripts)
container.style.cssText = `
position: fixed;
right: 16px;
top: 80px;
z-index: 9999;
width: 340px;
font-size: 12px;
font-family: system-ui, sans-serif;
`;
const details = document.createElement("details");
details.open = false;
details.style.cssText = `
background: #f2f2f2;
border: 1px solid #d0d0d0;
border-radius: 10px;
padding: 10px;
color: #222;
box-shadow: 0 2px 8px rgba(0,0,0,.15);
max-height: 70vh;
overflow: auto;
`;
const summary = document.createElement("summary");
summary.textContent = "Release Info";
summary.style.cssText = `
cursor: pointer;
font-weight: 600;
font-size: 14px;
margin-bottom: 6px;
`;
const body = document.createElement("div");
/***** RELEASE METADATA *****/
const ids = extractIdentifiers(release);
const primaryCatalog = determinePrimaryCatalog(ids);
let html = `
<div style="margin-bottom:8px">
`;
if (primaryCatalog) {
html += `<div><b>Primary catalog:</b> ${primaryCatalog.value} (${primaryCatalog.source})</div>`;
}
if (ids.catalog_number && equalNormalized(ids.catalog_number, ids.upc)) {
html += `<div><b>UPC / catalog_number:</b> ${ids.upc} (same value)</div>`;
} else {
html += `<div><b>UPC:</b> ${ids.upc ?? "—"}</div>`;
html += `<div><b>catalog_number:</b> ${ids.catalog_number ?? "—"}</div>`;
}
if (ids.gridName)
html += `
<div><b>Grid Name:</b> ${ids.gridName}</div>
`;
if (ids.grid )
html += `
<div><b>Grid:</b> ${ids.grid }</div>
`;
html += `<div><b>Release timestamps:</b></div>`;
RELEASE_DATE_FIELDS.forEach(field => {
if (releaseDates[field]) {
html += `
<div style="margin-left:10px">
${field}: ${releaseDates[field]}
</div>
`;
}
});
if (meta.warnings.length) {
html += `<div style="color:#b00020;margin-top:4px">`;
meta.warnings.forEach(w => {
html += `<div>${w}</div>`;
});
html += `</div>`;
}
html += `
<div><b>Tracks:</b> ${tracks.length}</div>
</div>
<hr>
`;
/***** TRACK LIST *****/
let tracknumber = 1;
tracks.forEach(track => {
const trackDates = extractDates(track, TRACK_DATE_FIELDS);
const conflicts = getConflictingDates(trackDates, releaseDates);
html += `
<div style="margin-bottom:8px">
${tracknumber}. ${track.name ?? "Untitled"}
${track.mix_name ? `(${track.mix_name})` : ""}
<br>
${track.isrc ? `<span style="opacity:.7">ISRC: ${track.isrc}</span><br>` : ""}
`;
if (Object.keys(conflicts).length > 0)
Object.entries(conflicts).forEach(([k, v]) => {
html += `
<div style="color:#b00020;margin-left:10px;opacity:.8">
⚠ ${k}: ${v}
</div>
`;
});
html += `</div>`;
tracknumber++;
});
body.innerHTML = html;
details.append(summary, body);
container.appendChild(details);
document.body.appendChild(container);
}
/******************************************************************
* MAIN RENDER
******************************************************************/
async function render() {
if (!isReleasePage()) {
removePanel();
return;
}
const data = await fetchReleaseData();
if (!data?.release) return;
createPanel(data.release, data.tracks);
}
/******************************************************************
* SPA ROUTE DETECTION
******************************************************************/
function hookHistoryEvents() {
const pushState = history.pushState;
const replaceState = history.replaceState;
function trigger() {
setTimeout(render, 200);
}
history.pushState = function () {
pushState.apply(this, arguments);
trigger();
};
history.replaceState = function () {
replaceState.apply(this, arguments);
trigger();
};
window.addEventListener("popstate", trigger);
}
/******************************************************************
* INIT
******************************************************************/
function init() {
hookHistoryEvents();
render();
}
if (document.readyState === "loading")
document.addEventListener("DOMContentLoaded", init);
else
init();
})();