Skip to content

Instantly share code, notes, and snippets.

@plmi
Created May 8, 2026 17:26
Show Gist options
  • Select an option

  • Save plmi/d03c22f820277ec8764aafb33848a5c5 to your computer and use it in GitHub Desktop.

Select an option

Save plmi/d03c22f820277ec8764aafb33848a5c5 to your computer and use it in GitHub Desktop.
Marks matching xREL releases that were pred after their product-page release date.
// ==UserScript==
// @name xREL Pre Date Indicator
// @namespace https://www.xrel.to/
// @version 0.2.0
// @description Marks matching xREL releases that were pred after their product-page release date.
// @match https://www.xrel.to/releases.html*
// @connect bluray-disc.de
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function () {
'use strict';
const SETTING_RELEASE_REGEX = 'xpti.releaseRegex';
const SETTING_DATE_LABEL_REGEX = 'xpti.dateLabelRegex';
const SETTING_BLURAY_DISC_HIGH_DELTA_DAYS = 'xpti.blurayDiscHighDeltaDays';
const SETTING_BLURAY_DISC_TRIGGER_REGEX = 'xpti.blurayDiscTriggerRegex';
const CACHE_KEY = 'xpti.productDateCache.v1';
const BLURAY_DISC_CACHE_KEY = 'xbdi.imdbBlurayCache.v1';
const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
const BLURAY_DISC_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
const BLURAY_DISC_NULL_CACHE_TTL_MS = 60 * 60 * 1000;
const DEFAULT_RELEASE_REGEX = 'German.*BluRay';
const DEFAULT_DATE_LABEL_REGEX =
'Retail|Blu-?ray|DVD|UHD|HD|VoD|VOD|Video on Demand|Digital|Steam|GOG|Epic|Windows|Linux|Mac|PlayStation|Xbox|Switch|Nintendo';
const DEFAULT_BLURAY_DISC_HIGH_DELTA_DAYS = 500;
const DEFAULT_BLURAY_DISC_TRIGGER_REGEX =
"\\b(?:REMASTERED|THEATRICAL|UNCUT|UNRATED|EXTENDED|RESTORED|DIRECTOR'?S?[._ -]?CUT|FINAL[._ -]?CUT)\\b";
const EXCLUDED_DATE_LABEL_REGEX = /\b(?:cine|cinema|kino|kinostart|theater|theatrical|web)\b/i;
const processedRows = new WeakSet();
const productDateRequests = new Map();
const imdbRequests = new Map();
const blurayDiscDateRequests = new Map();
const productDateCache = loadCache();
const blurayDiscCache = loadBlurayDiscCache();
const taskQueue = [];
let activeTasks = 0;
const maxConcurrentRequests = 4;
let releaseRegex = compileSettingRegex(
SETTING_RELEASE_REGEX,
DEFAULT_RELEASE_REGEX,
'i',
'release regex',
);
let dateLabelRegex = compileSettingRegex(
SETTING_DATE_LABEL_REGEX,
DEFAULT_DATE_LABEL_REGEX,
'i',
'date label regex',
);
let blurayDiscTriggerRegex = compileSettingRegex(
SETTING_BLURAY_DISC_TRIGGER_REGEX,
DEFAULT_BLURAY_DISC_TRIGGER_REGEX,
'i',
'bluray-disc trigger regex',
);
let blurayDiscHighDeltaDays = getNumberSetting(
SETTING_BLURAY_DISC_HIGH_DELTA_DAYS,
DEFAULT_BLURAY_DISC_HIGH_DELTA_DAYS,
);
injectStyles();
registerMenuCommands();
scanReleaseRows();
observeReleaseList();
function registerMenuCommands() {
registerMenuCommand('Set release regex', () => {
promptForRegex(
SETTING_RELEASE_REGEX,
DEFAULT_RELEASE_REGEX,
'Only rows whose dirname matches this regex are checked. You can use /pattern/flags syntax.',
);
});
registerMenuCommand('Set date label regex', () => {
promptForRegex(
SETTING_DATE_LABEL_REGEX,
DEFAULT_DATE_LABEL_REGEX,
'Product-page dates whose label matches this regex are preferred. If none match, all dates are considered.',
);
});
registerMenuCommand('Set bluray-disc high-delta days', () => {
promptForNumber(
SETTING_BLURAY_DISC_HIGH_DELTA_DAYS,
DEFAULT_BLURAY_DISC_HIGH_DELTA_DAYS,
'Late indicators above this number of days are rechecked on bluray-disc.de.',
);
});
registerMenuCommand('Set bluray-disc trigger regex', () => {
promptForRegex(
SETTING_BLURAY_DISC_TRIGGER_REGEX,
DEFAULT_BLURAY_DISC_TRIGGER_REGEX,
'Rows whose dirname matches this regex are rechecked on bluray-disc.de when they are late.',
);
});
registerMenuCommand('Clear product date cache', () => {
productDateCache.entries = {};
saveCache();
location.reload();
});
}
function promptForRegex(settingKey, fallback, message) {
const current = getSetting(settingKey, fallback);
const next = prompt(message + '\n\nCurrent value:', current);
if (next === null) {
return;
}
try {
parseUserRegex(next.trim() || fallback, 'i');
} catch (error) {
alert('Invalid regex: ' + error.message);
return;
}
setSetting(settingKey, next.trim() || fallback);
location.reload();
}
function promptForNumber(settingKey, fallback, message) {
const current = getSetting(settingKey, String(fallback));
const next = prompt(message + '\n\nCurrent value:', current);
if (next === null) {
return;
}
const value = Number(next.trim() || fallback);
if (!Number.isFinite(value) || value < 0) {
alert('Invalid number: ' + next);
return;
}
setSetting(settingKey, String(Math.floor(value)));
location.reload();
}
function scanReleaseRows() {
document.querySelectorAll('.release_item').forEach((row) => {
if (processedRows.has(row)) {
return;
}
processedRows.add(row);
enqueueTask(() => processReleaseRow(row));
});
}
function observeReleaseList() {
const target = document.querySelector('#middle_spawn') || document.body;
const observer = new MutationObserver(() => scanReleaseRows());
observer.observe(target, { childList: true, subtree: true });
}
async function processReleaseRow(row) {
const release = readReleaseRow(row);
if (!release) {
return;
}
releaseRegex.lastIndex = 0;
if (!releaseRegex.test(release.dirname)) {
return;
}
try {
const productDates = await getProductDates(release.productUrl);
const officialDate = selectOfficialDate(productDates);
if (!officialDate) {
return;
}
const dayDelta = dateOrdinal(release.preDateKey) - dateOrdinal(officialDate.dateKey);
if (dayDelta > 0) {
const lateDate = await resolveLateDate(release, officialDate, dayDelta);
if (lateDate.dayDelta > 0) {
showLateIndicator(
row,
lateDate.officialDate,
lateDate.dayDelta,
lateDate.isBlurayDiscDate,
lateDate.blurayDiscDate,
);
}
}
} catch (error) {
console.warn('[xREL Pre Date Indicator] Could not check release', release.dirname, error);
}
}
function readReleaseRow(row) {
const titleCell = row.querySelector('.release_title, .release_title_p2p');
const dateCell = row.querySelector('.release_date');
if (!titleCell || !dateCell) {
return null;
}
const dirnameLink =
titleCell.querySelector('.dirname-truncated a[title]') ||
titleCell.querySelector('a.sub[href*="-nfo/"]') ||
titleCell.querySelector('a.sub[href$="/nfo.html"]') ||
titleCell.querySelector('a.sub');
const dirname = readDirname(dirnameLink);
if (!dirname) {
return null;
}
const productLink = Array.from(titleCell.querySelectorAll('a[href]')).find((link) => {
const href = link.getAttribute('href') || '';
const url = new URL(href, location.href);
return (
url.origin === location.origin &&
/^\/(?:movie|title|tv|game|software|console)\/\d+\//.test(url.pathname) &&
!url.pathname.includes('-nfo/')
);
});
if (!productLink) {
return null;
}
const preDateKey = parseXrelListDate(dateCell.textContent || '');
if (!preDateKey) {
return null;
}
return {
dirname,
imdbId: readImdbId(row),
preDateKey,
productUrl: new URL(productLink.getAttribute('href'), location.href).href,
};
}
function readDirname(link) {
if (!link) {
return null;
}
return (link.getAttribute('title') || link.textContent || '').trim() || null;
}
function readImdbId(root) {
const link = root.querySelector('a[href*="imdb.com/title/tt"], a[title*="IMDb"]');
if (!link) {
return null;
}
return extractImdbId((link.getAttribute('href') || '') + ' ' + (link.getAttribute('title') || ''));
}
function parseXrelListDate(text) {
const match = text.match(/(\d{2})\.(\d{2})\.(\d{2})(?:\s+(\d{2}):(\d{2}))?/);
if (!match) {
return null;
}
const [, day, month, shortYear] = match;
const yearNumber = Number(shortYear);
const fullYear = yearNumber >= 70 ? 1900 + yearNumber : 2000 + yearNumber;
return fullYear + '-' + month + '-' + day;
}
async function getProductDates(productUrl) {
const cached = productDateCache.entries[productUrl];
if (cached && Date.now() - cached.cachedAt < CACHE_TTL_MS) {
return cached.dates;
}
if (productDateRequests.has(productUrl)) {
return productDateRequests.get(productUrl);
}
const request = fetch(productUrl, {
credentials: 'include',
headers: { accept: 'text/html,application/xhtml+xml' },
})
.then((response) => {
if (!response.ok) {
throw new Error('Product page request failed with ' + response.status);
}
return response.text();
})
.then((html) => {
const doc = new DOMParser().parseFromString(html, 'text/html');
const dates = extractProductDates(doc);
productDateCache.entries[productUrl] = {
cachedAt: Date.now(),
dates,
};
saveCache();
return dates;
})
.finally(() => productDateRequests.delete(productUrl));
productDateRequests.set(productUrl, request);
return request;
}
function extractProductDates(doc) {
const datesBox = doc.querySelector('.extinfo_box_dates');
if (!datesBox) {
return [];
}
return Array.from(datesBox.querySelectorAll('.extinfo_box_date'))
.map((dateNode) => {
const dateMatch = (dateNode.textContent || '').match(/(\d{2})\.(\d{2})\.(\d{4})/);
if (!dateMatch) {
return null;
}
const [, day, month, year] = dateMatch;
const labels = [];
let next = dateNode.nextElementSibling;
while (next && !next.classList.contains('extinfo_box_date')) {
if (next.matches('ul')) {
labels.push(
...Array.from(next.querySelectorAll('li'))
.map((item) => (item.textContent || '').trim())
.filter(Boolean),
);
}
next = next.nextElementSibling;
}
return {
dateKey: year + '-' + month + '-' + day,
labels,
};
})
.filter(Boolean);
}
function selectOfficialDate(productDates) {
const comparableDates = productDates.filter(isComparableProductDate);
if (!comparableDates.length) {
return null;
}
const matchingDates = comparableDates.filter((date) =>
date.labels.some((label) => {
dateLabelRegex.lastIndex = 0;
return dateLabelRegex.test(label);
}),
);
const candidates = matchingDates.length ? matchingDates : comparableDates;
const sortedCandidates = candidates
.slice()
.sort((left, right) => dateOrdinal(left.dateKey) - dateOrdinal(right.dateKey));
return sortedCandidates[sortedCandidates.length - 1];
}
function isComparableProductDate(productDate) {
return productDate.labels.every((label) => !EXCLUDED_DATE_LABEL_REGEX.test(label));
}
async function resolveLateDate(release, xrelOfficialDate, xrelDayDelta) {
const fallback = {
officialDate: xrelOfficialDate,
dayDelta: xrelDayDelta,
isBlurayDiscDate: false,
blurayDiscDate: null,
};
if (!shouldCheckBlurayDisc(release, xrelDayDelta)) {
return fallback;
}
let blurayDiscDate;
try {
blurayDiscDate = await getBlurayDiscReleaseDate(release);
} catch (error) {
console.warn('[xREL Pre Date Indicator] Could not check bluray-disc.de', release.dirname, error);
return fallback;
}
if (!blurayDiscDate || dateOrdinal(blurayDiscDate.dateKey) <= dateOrdinal(xrelOfficialDate.dateKey)) {
return {
...fallback,
blurayDiscDate: blurayDiscDate
? {
dateKey: blurayDiscDate.dateKey,
dayDelta: dateOrdinal(release.preDateKey) - dateOrdinal(blurayDiscDate.dateKey),
}
: null,
};
}
const blurayDiscDayDelta = dateOrdinal(release.preDateKey) - dateOrdinal(blurayDiscDate.dateKey);
return {
officialDate: {
dateKey: blurayDiscDate.dateKey,
labels: ['bluray-disc.de release date'],
source: 'bluray-disc.de',
sourceTitle: blurayDiscDate.title,
sourceUrl: blurayDiscDate.url,
xrelDateKey: xrelOfficialDate.dateKey,
xrelDayDelta,
},
dayDelta: blurayDiscDayDelta,
isBlurayDiscDate: true,
blurayDiscDate: {
dateKey: blurayDiscDate.dateKey,
dayDelta: blurayDiscDayDelta,
},
};
}
function shouldCheckBlurayDisc(release, dayDelta) {
if (dayDelta > blurayDiscHighDeltaDays) {
return true;
}
blurayDiscTriggerRegex.lastIndex = 0;
return blurayDiscTriggerRegex.test(release.dirname);
}
async function getBlurayDiscReleaseDate(release) {
const match = await findBlurayDiscMatch(release);
if (!match) {
return null;
}
const date = await getBlurayDiscPageReleaseDate(match.url);
if (!date) {
return null;
}
return {
dateKey: date.dateKey,
title: match.title,
url: match.url,
};
}
async function findBlurayDiscMatch(release) {
const directKey = 'direct:' + release.productUrl;
const directCached = blurayDiscCache.entries[directKey];
if (directCached) {
const directTtl = directCached.bdUrl ? BLURAY_DISC_CACHE_TTL_MS : BLURAY_DISC_NULL_CACHE_TTL_MS;
if (Date.now() - directCached.cachedAt < directTtl) {
return directCached.bdUrl
? { url: directCached.bdUrl, title: directCached.bdTitle || directCached.bdUrl }
: null;
}
}
const imdbId = release.imdbId || (await fetchProductImdbId(release.productUrl));
if (!imdbId) {
cacheDirectResult(directKey, null, null);
return null;
}
const match = await findBlurayDiscMovie(imdbId);
cacheDirectResult(directKey, imdbId, match);
return match;
}
function cacheDirectResult(directKey, imdbId, match) {
blurayDiscCache.entries[directKey] = {
cachedAt: Date.now(),
imdbId,
bdUrl: match ? match.url : null,
bdTitle: match ? match.title : null,
};
saveBlurayDiscCache();
}
async function fetchProductImdbId(productUrl) {
const cacheKey = 'product:' + productUrl;
const cached = blurayDiscCache.entries[cacheKey];
if (cached) {
const ttl = cached.imdbId ? BLURAY_DISC_CACHE_TTL_MS : BLURAY_DISC_NULL_CACHE_TTL_MS;
if (Date.now() - cached.cachedAt < ttl) {
return cached.imdbId || null;
}
}
try {
const response = await fetch(productUrl, {
credentials: 'include',
headers: { accept: 'text/html,application/xhtml+xml' },
});
if (!response.ok) {
throw new Error('Product page request failed with ' + response.status);
}
const html = await response.text();
const imdbId = extractImdbId(html);
blurayDiscCache.entries[cacheKey] = {
cachedAt: Date.now(),
imdbId,
};
saveBlurayDiscCache();
return imdbId;
} catch (error) {
console.warn('[xREL Pre Date Indicator] Could not fetch xREL product IMDb id', productUrl, error);
return null;
}
}
function extractImdbId(text) {
const match = String(text || '').match(/tt\d{7,9}/i);
return match ? match[0].toLowerCase() : null;
}
async function findBlurayDiscMovie(imdbId) {
const cacheKey = 'imdb:' + imdbId;
const cached = blurayDiscCache.entries[cacheKey];
if (cached) {
const ttl = cached.url ? BLURAY_DISC_CACHE_TTL_MS : BLURAY_DISC_NULL_CACHE_TTL_MS;
if (Date.now() - cached.cachedAt < ttl) {
return cached.url ? { url: cached.url, title: cached.title || imdbId } : null;
}
}
if (imdbRequests.has(imdbId)) {
return imdbRequests.get(imdbId);
}
const request = searchBlurayDisc(imdbId)
.then((match) => {
blurayDiscCache.entries[cacheKey] = {
cachedAt: Date.now(),
url: match ? match.url : null,
title: match ? match.title : null,
};
saveBlurayDiscCache();
return match;
})
.finally(() => imdbRequests.delete(imdbId));
imdbRequests.set(imdbId, request);
return request;
}
async function searchBlurayDisc(imdbId) {
const body =
'SearchString=' +
encodeURIComponent(imdbId) +
'&x=57&y=25&section_movie=on&imports_all=on';
const html = await requestText({
method: 'POST',
url: 'https://bluray-disc.de/suche',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
data: body,
});
const doc = new DOMParser().parseFromString(html, 'text/html');
const movieResult = doc.querySelector('#movie-result');
const item = movieResult && movieResult.querySelector('.result-item');
if (!item) {
return null;
}
const link = item.querySelector('a[href*="/blu-ray-filme/"]');
if (!link) {
return null;
}
const url = new URL(link.getAttribute('href'), 'https://bluray-disc.de/').href;
const titleNode = item.querySelector('.item-info b') || link;
const title = (titleNode.textContent || '').trim() || url;
return { url, title };
}
async function getBlurayDiscPageReleaseDate(url) {
const cacheKey = 'bdDate:' + url;
const cached = blurayDiscCache.entries[cacheKey];
if (cached) {
const ttl = cached.dateKey ? BLURAY_DISC_CACHE_TTL_MS : BLURAY_DISC_NULL_CACHE_TTL_MS;
if (Date.now() - cached.cachedAt < ttl) {
return cached.dateKey ? { dateKey: cached.dateKey } : null;
}
}
if (blurayDiscDateRequests.has(url)) {
return blurayDiscDateRequests.get(url);
}
const request = requestText({
method: 'GET',
url,
headers: { accept: 'text/html,application/xhtml+xml' },
})
.then((html) => {
const doc = new DOMParser().parseFromString(html, 'text/html');
const dateKey = extractBlurayDiscReleaseDate(doc);
blurayDiscCache.entries[cacheKey] = {
cachedAt: Date.now(),
dateKey,
};
saveBlurayDiscCache();
return dateKey ? { dateKey } : null;
})
.finally(() => blurayDiscDateRequests.delete(url));
blurayDiscDateRequests.set(url, request);
return request;
}
function extractBlurayDiscReleaseDate(doc) {
const releaseKey = Array.from(doc.querySelectorAll('.key')).find((node) =>
/ver.+ffentlichung/i.test(normalizeText(node.textContent || '')),
);
if (releaseKey) {
const valueNode = releaseKey.nextElementSibling;
const dateKey = valueNode ? parseBlurayDiscDate(valueNode.textContent || '') : null;
if (dateKey) {
return dateKey;
}
}
const pageText = doc.body ? doc.body.textContent || '' : doc.textContent || '';
const markerMatch = pageText.match(/ver.+ffentlichung/i);
if (markerMatch && typeof markerMatch.index === 'number') {
const markerIndex = markerMatch.index;
return parseBlurayDiscDate(pageText.slice(markerIndex, markerIndex + 200));
}
return null;
}
function parseBlurayDiscDate(text) {
const match = String(text || '').match(/\b(?:ab\s*)?(\d{1,2})\.(\d{1,2})\.(\d{4})\b/i);
if (!match) {
return null;
}
const dayNumber = Number(match[1]);
const monthNumber = Number(match[2]);
const yearNumber = Number(match[3]);
if (dayNumber < 1 || dayNumber > 31 || monthNumber < 1 || monthNumber > 12 || yearNumber < 1900) {
return null;
}
return (
String(yearNumber).padStart(4, '0') +
'-' +
String(monthNumber).padStart(2, '0') +
'-' +
String(dayNumber).padStart(2, '0')
);
}
function normalizeText(text) {
return String(text || '').replace(/\s+/g, ' ').trim();
}
function showLateIndicator(row, officialDate, dayDelta, isBlurayDiscDate, blurayDiscDate) {
const titleCell = row.querySelector('.release_title, .release_title_p2p');
if (!titleCell || titleCell.querySelector('.xpti-late-indicator')) {
return;
}
const oldDateBadge = row.querySelector('.release_date .xpti-late-indicator');
if (oldDateBadge) {
oldDateBadge.remove();
}
const badge = document.createElement('span');
badge.className = 'xpti-late-indicator';
badge.textContent = '+' + dayDelta + 'd' + (isBlurayDiscDate ? '!' : '');
badge.title = buildLateIndicatorTitle(officialDate, dayDelta, blurayDiscDate);
const firstBreak = titleCell.querySelector('br');
titleCell.insertBefore(document.createTextNode(' '), firstBreak);
titleCell.insertBefore(badge, firstBreak);
row.classList.add('xpti-late-row');
}
function buildLateIndicatorTitle(officialDate, dayDelta, blurayDiscDate) {
const xrelDateKey = officialDate.xrelDateKey || officialDate.dateKey;
const xrelDayDelta =
typeof officialDate.xrelDayDelta === 'number' ? officialDate.xrelDayDelta : dayDelta;
const lines = ['xREL: ' + formatDateKey(xrelDateKey) + ' (' + formatDayDelta(xrelDayDelta) + ')'];
if (blurayDiscDate) {
lines.push(
'Unsicherheitsprüfung, bluray-disc.de: ' +
formatDateKey(blurayDiscDate.dateKey) +
' (' +
formatDayDelta(blurayDiscDate.dayDelta) +
')',
);
}
return lines.join('\n');
}
function formatDayDelta(dayDelta) {
return (dayDelta > 0 ? '+' : '') + dayDelta + 'd';
}
function formatDateKey(dateKey) {
const match = String(dateKey || '').match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return dateKey;
}
return match[3] + '.' + match[2] + '.' + match[1];
}
function requestText(options) {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest === 'function') {
GM_xmlhttpRequest({
method: options.method,
url: options.url,
headers: options.headers || {},
data: options.data,
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
resolve(response.responseText);
return;
}
reject(new Error('Request failed with ' + response.status));
},
onerror: () => reject(new Error('Request failed')),
ontimeout: () => reject(new Error('Request timed out')),
});
return;
}
fetch(options.url, {
method: options.method,
headers: options.headers,
body: options.data,
credentials: 'include',
})
.then((response) => {
if (!response.ok) {
throw new Error('Request failed with ' + response.status);
}
return response.text();
})
.then(resolve)
.catch(reject);
});
}
function enqueueTask(task) {
taskQueue.push(task);
drainQueue();
}
function drainQueue() {
while (activeTasks < maxConcurrentRequests && taskQueue.length) {
const task = taskQueue.shift();
activeTasks += 1;
Promise.resolve()
.then(task)
.catch((error) => console.warn('[xREL Pre Date Indicator] Task failed', error))
.finally(() => {
activeTasks -= 1;
drainQueue();
});
}
}
function dateOrdinal(dateKey) {
const [year, month, day] = dateKey.split('-').map(Number);
return Math.floor(Date.UTC(year, month - 1, day) / 86_400_000);
}
function compileSettingRegex(settingKey, fallback, defaultFlags, label) {
const value = getSetting(settingKey, fallback);
try {
return parseUserRegex(value, defaultFlags);
} catch (error) {
console.warn('[xREL Pre Date Indicator] Invalid ' + label + '; using default', error);
return parseUserRegex(fallback, defaultFlags);
}
}
function parseUserRegex(value, defaultFlags) {
const trimmed = value.trim();
if (trimmed.startsWith('/')) {
const lastSlash = trimmed.lastIndexOf('/');
if (lastSlash > 0) {
const pattern = trimmed.slice(1, lastSlash);
const flags = sanitizeRegexFlags(trimmed.slice(lastSlash + 1));
return new RegExp(pattern, flags);
}
}
return new RegExp(trimmed, sanitizeRegexFlags(defaultFlags));
}
function sanitizeRegexFlags(flags) {
return Array.from(new Set(flags.replace(/[gy]/g, '').split(''))).join('');
}
function getNumberSetting(key, fallback) {
const value = Number(getSetting(key, String(fallback)));
if (!Number.isFinite(value) || value < 0) {
return fallback;
}
return Math.floor(value);
}
function getSetting(key, fallback) {
try {
if (typeof GM_getValue === 'function') {
return GM_getValue(key, fallback);
}
} catch (error) {
// Fall back to localStorage.
}
return localStorage.getItem(key) || fallback;
}
function setSetting(key, value) {
try {
if (typeof GM_setValue === 'function') {
GM_setValue(key, value);
return;
}
} catch (error) {
// Fall back to localStorage.
}
localStorage.setItem(key, value);
}
function registerMenuCommand(label, callback) {
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand(label, callback);
}
}
function loadCache() {
try {
const parsed = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}');
if (parsed && typeof parsed === 'object' && parsed.entries) {
return parsed;
}
} catch (error) {
// Ignore malformed cache entries.
}
return { entries: {} };
}
function saveCache() {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify(productDateCache));
} catch (error) {
console.warn('[xREL Pre Date Indicator] Could not save product date cache', error);
}
}
function loadBlurayDiscCache() {
try {
if (typeof GM_getValue === 'function') {
const gmCache = parseCacheValue(GM_getValue(BLURAY_DISC_CACHE_KEY, null));
if (gmCache) {
return gmCache;
}
}
} catch (error) {
// Fall back to localStorage.
}
try {
const localCache = parseCacheValue(localStorage.getItem(BLURAY_DISC_CACHE_KEY));
if (localCache) {
try {
if (typeof GM_setValue === 'function') {
GM_setValue(BLURAY_DISC_CACHE_KEY, JSON.stringify(localCache));
}
} catch (error) {
// Keep using the localStorage copy.
}
return localCache;
}
} catch (error) {
// Ignore malformed cache entries.
}
return { entries: {} };
}
function parseCacheValue(value) {
if (!value) {
return null;
}
try {
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
if (parsed && typeof parsed === 'object' && parsed.entries && typeof parsed.entries === 'object') {
return parsed;
}
} catch (error) {
// Ignore malformed cache entries.
}
return null;
}
function saveBlurayDiscCache() {
const serialized = JSON.stringify(blurayDiscCache);
let saved = false;
try {
if (typeof GM_setValue === 'function') {
GM_setValue(BLURAY_DISC_CACHE_KEY, serialized);
saved = true;
}
} catch (error) {
// Fall back to localStorage.
}
try {
localStorage.setItem(BLURAY_DISC_CACHE_KEY, serialized);
saved = true;
} catch (error) {
if (saved) {
return;
}
console.warn('[xREL Pre Date Indicator] Could not save bluray-disc cache', error);
}
}
function injectStyles() {
const style = document.createElement('style');
style.textContent = [
'.xpti-late-indicator {',
' display: inline;',
' margin-left: 4px;',
' color: #9d3328;',
' font-size: 10px;',
' font-weight: 700;',
' line-height: 14px;',
' vertical-align: 1px;',
' white-space: nowrap;',
'}',
].join('\n');
document.head.appendChild(style);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment