// ==UserScript==
// @name IA MIX (private)
// @author User:Martin@MixesDB (Subfader@GitHub)
// @version 2026.07.21.2
// @description Add MixesDB creation links to Inverted Audio IA MIX episodes.
// @homepageURL https://www.mixesdb.com/w/Help:MixesDB_userscripts
// @supportURL https://discord.com/channels/1258107262833262603/1261652394799005858
// @updateURL https://raw.githubusercontent.com/mixesdb/userscripts/refs/heads/main/private/Episodes_Importer/IA_MIX/script.user.js
// @downloadURL https://raw.githubusercontent.com/mixesdb/userscripts/refs/heads/main/private/Episodes_Importer/IA_MIX/script.user.js
// @require https://cdn.rawgit.com/mixesdb/userscripts/refs/heads/main/includes/jquery-3.7.1.min.js
// @require https://cdn.rawgit.com/mixesdb/userscripts/refs/heads/main/includes/waitForKeyElements.js
// @require https://raw.githubusercontent.com/mixesdb/userscripts/refs/heads/main/includes/global.js?v-IA_MIX_1
// @require https://raw.githubusercontent.com/mixesdb/userscripts/refs/heads/main/private/Episodes_Importer/funcs.js?v-2026.07.20.13
// @require https://raw.githubusercontent.com/mixesdb/userscripts/refs/heads/main/private/Player_URLs/funcs.js?v-2026.07.20.13
// @require https://raw.githubusercontent.com/mixesdb/userscripts/refs/heads/main/private/Episodes_Importer/IA_MIX/player_episodes.js?v-2026.07.20.13
// @include https://inverted-audio.com/mix*
// @include https://www.mixesdb.com/w/index.php*
// @icon https://www.google.com/s2/favicons?sz=64&domain=inverted-audio.com
// @noframes
// @grant unsafeWindow
// @run-at document-end
// ==/UserScript==
var cacheVersion = 1,
scriptName = "IA_MIX";
loadRawCss( githubPath_raw + "includes/global.css?v-" + scriptName + "_" + cacheVersion );
(function () {
'use strict';
const importer = window.MixesDBEpisodesImporter;
if (!importer) {
console.error('MixesDBEpisodesImporter dependency is not available.');
return;
}
const sourceHost = 'inverted-audio.com';
let playerEpisodes = window.MixesDBIaMixPlayerEpisodes || {};
const mixesdbHost = 'www.mixesdb.com';
const config = {
categoryTitle: 'Category:IA_MIX',
titleEpisodePattern: /IA MIX\s+(\d{1,4})\b/i,
showCategory: 'IA MIX',
storageKey: 'mdbIaMixRemoveExistingEpisodes',
visitedLinksStorageKey: 'mdbIaMixVisitedEpisodeLinks',
tleApiType: 'standard',
selectors: {
listContainer: '#post-load',
episodeWrapper: '#post-load article.type-mix, article.type-mix',
episodeHeading: '.entry-title',
episodeLink: '.entry-title a[href], .entry-img a[href]',
description: '.entry-content',
nextPage: 'nav.posts-navigation a.next[href]',
postedOn: '.posted-on',
postedOnTime: '.posted-on time.entry-date.published[datetime], .posted-on time[datetime]',
},
classNames: {
link: 'mdb-ia-mix-link',
closeButton: 'mdb-ia-mix-close-button',
toggle: 'mdb-ia-mix-remove-existing-toggle',
hidden: 'mdb-ia-mix-existing-episode-hidden',
copiedWrapper: 'mdb-ia-mix-copied-wrapper',
autoLoadMoreWaiter: 'mdb-ia-mix-auto-load-more-waiter',
},
autoLoadMoreDelay: 700,
};
let existingEpisodes = new Set();
let visitedEpisodeLinks = new Set();
let removeExistingEpisodes = false;
let autoLoadMoreTimer = null;
let autoLoadMoreInProgress = false;
let lastAutoLoadVisibleMissingCount = 0;
function logStep(step, details = '') {
importer.logValue(`IA MIX importer: ${step}`, typeof details === 'object' ? JSON.stringify(details) : details);
}
function loadVisitedEpisodeLinks() {
try {
visitedEpisodeLinks = new Set(JSON.parse(localStorage.getItem(config.visitedLinksStorageKey) || '[]'));
} catch (error) {
visitedEpisodeLinks = new Set();
}
}
function saveVisitedEpisodeLinks() {
localStorage.setItem(config.visitedLinksStorageKey, JSON.stringify(Array.from(visitedEpisodeLinks)));
}
function setLinkVisitedState(link) {
link.classList.toggle('is-visited', visitedEpisodeLinks.has(link.dataset.episodeNumber));
}
function markLinkVisited(link) {
visitedEpisodeLinks.add(link.dataset.episodeNumber);
saveVisitedEpisodeLinks();
setLinkVisitedState(link);
}
function parseEpisodeTitle(title) {
const match = String(title || '').match(/IA MIX\s+(\d{1,4})\s+(.+)/i);
if (!match) return null;
return {
episodeNumber: Number(match[1]),
artist: match[2].trim(),
};
}
function normalizeDate(dateText) {
const isoMatch = String(dateText || '').match(/\b(\d{4})-(\d{2})-(\d{2})(?=\b|T)/);
if (isoMatch) return isoMatch[0];
const longDateMatch = String(dateText || '').match(/\b([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})\b/);
if (!longDateMatch) return '';
const month = importer.getMonthNumber(longDateMatch[1]);
if (!month) return '';
return `${longDateMatch[3]}-${month}-${String(longDateMatch[2]).padStart(2, '0')}`;
}
function extractDateFromDocument(doc, episodeUrl = '') {
const postedOn = doc.querySelector('.posted-on');
const candidates = [
{ source: '.posted-on time.entry-date.published[datetime]', value: postedOn?.querySelector('time.entry-date.published[datetime]')?.getAttribute('datetime') },
{ source: '.posted-on time[datetime]', value: postedOn?.querySelector('time[datetime]')?.getAttribute('datetime') },
{ source: '.posted-on text', value: postedOn?.textContent },
{ source: 'time[datetime]', value: doc.querySelector('time[datetime]')?.getAttribute('datetime') },
{ source: 'meta[property="article:published_time"]', value: doc.querySelector('meta[property="article:published_time"]')?.content },
{ source: 'meta[name="date"]', value: doc.querySelector('meta[name="date"]')?.content },
{ source: '.entry-date, .entry-meta text', value: doc.querySelector('.entry-date, .entry-meta')?.textContent },
];
const normalizedCandidates = candidates.map(candidate => ({
source: candidate.source,
value: String(candidate.value || '').trim(),
normalized: normalizeDate(candidate.value),
}));
const selected = normalizedCandidates.find(candidate => candidate.normalized);
logStep('date extraction candidates', { episodeUrl, candidates: normalizedCandidates, selected: selected || null });
return selected?.normalized || '';
}
function getTracklistHeadingParagraph(doc) {
return Array.from(doc.querySelectorAll(`${config.selectors.description} p`))
.find(paragraph => paragraph.textContent.trim().replace(/\s+/g, ' ').toUpperCase() === 'TRACKLIST');
}
function extractRawTracklistFromDocument(doc, episodeUrl = '') {
const tracklistHeading = getTracklistHeadingParagraph(doc);
const tracklistParagraph = tracklistHeading?.nextElementSibling;
if (!tracklistParagraph || tracklistParagraph.tagName !== 'P') {
logStep('detail tracklist paragraph not found', { episodeUrl, hasTracklistHeading: Boolean(tracklistHeading) });
return '';
}
const rawTracklist = importer.getNodeTextWithLinebreaks(tracklistParagraph)
.split('\n')
.map(importer.normalizeTracklistLine)
.filter(Boolean)
.join('\n');
logStep('detail tracklist extracted', { episodeUrl, lines: rawTracklist.split('\n').filter(Boolean).length, rawTracklist });
return rawTracklist;
}
async function formatTracklistForEpisode(rawTracklist, episodeUrl = '') {
try {
const tracklist = await importer.formatTracklist(rawTracklist, config.tleApiType);
logStep('formatTracklist success', { episodeUrl, status: tracklist.status, textLength: tracklist.text?.length || 0, hasFeedback: Boolean(tracklist.feedback) });
return tracklist;
} catch (error) {
logStep('formatTracklist failed', { episodeUrl, error: error.message || error });
const fallbackTracklist = importer.hasTracklistForApi(rawTracklist)
? rawTracklist.split('\n').filter(Boolean).map(line => `# ${line}`).join('\n')
: '\n\n
';
return {
text: fallbackTracklist,
status: importer.hasTracklistForApi(rawTracklist) ? 'incomplete' : 'none',
feedback: null,
};
}
}
async function fetchEpisodeDetails(episodeUrl) {
const response = await fetch(episodeUrl, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`Episode page responded with ${response.status}`);
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const rawTracklist = extractRawTracklistFromDocument(doc, episodeUrl);
return {
date: extractDateFromDocument(doc, episodeUrl),
playerUrl: doc.querySelector('iframe[src*="soundcloud.com"], iframe[src*="mixcloud.com"], iframe[src*="hearthis.at"], audio[src], audio source[src]')?.src || '',
tracklist: await formatTracklistForEpisode(rawTracklist, episodeUrl),
};
}
function buildMixesdbTitle(episode) {
const prefix = episode.date ? `${episode.date} - ` : '';
return `${prefix}${episode.artist} - IA MIX ${importer.padNumber(episode.episodeNumber)}`;
}
function normalizePlayerEpisodeEntry(entry) {
return typeof entry === 'object' && entry !== null ? entry.url : entry;
}
function getPlayerUrlsForEpisode(episode, fetchedPlayerUrl = '') {
const episodeKey = String(episode.episodeNumber);
const configuredPlayerUrls = [
normalizePlayerEpisodeEntry(playerEpisodes.applePodcasts?.[episodeKey]),
normalizePlayerEpisodeEntry(playerEpisodes.mixcloud?.[episodeKey]),
normalizePlayerEpisodeEntry(playerEpisodes.soundcloud?.[episodeKey]),
].filter(Boolean);
const playerUrls = configuredPlayerUrls.length ? configuredPlayerUrls : [fetchedPlayerUrl].filter(Boolean);
const uniquePlayerUrls = Array.from(new Set(playerUrls));
return typeof sortPlayerUrlsByPreferredOrder === 'function'
? sortPlayerUrlsByPreferredOrder(uniquePlayerUrls)
: uniquePlayerUrls;
}
function getMixcloudUrlForEpisode(episode, fetchedPlayerUrl = '') {
const episodeKey = String(episode.episodeNumber);
const configuredMixcloudUrl = normalizePlayerEpisodeEntry(playerEpisodes.mixcloud?.[episodeKey]);
if (configuredMixcloudUrl) return configuredMixcloudUrl;
return String(fetchedPlayerUrl || '').includes('mixcloud.com') ? fetchedPlayerUrl : '';
}
function getEpisodeDurationSeconds(episode) {
const episodeKey = String(episode.episodeNumber);
const entries = [
playerEpisodes.applePodcasts?.[episodeKey],
playerEpisodes.mixcloud?.[episodeKey],
playerEpisodes.soundcloud?.[episodeKey],
];
const duration = entries
.map(entry => Number(entry?.duration))
.find(candidate => Number.isFinite(candidate) && candidate > 0);
return duration || null;
}
function formatDuration(seconds) {
const totalSeconds = Math.round(seconds);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const remainingSeconds = totalSeconds % 60;
return `${hours}:${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
}
function buildFileDetailsText(episode) {
const durationSeconds = getEpisodeDurationSeconds(episode);
const duration = durationSeconds ? formatDuration(durationSeconds) : '';
return `{|{{NormalTableFormat}}\n! dur\n! MB\n! kbps\n|-\n| ${duration}\n| \n| \n|}`;
}
function buildPlayerText(playerUrls) {
if (!playerUrls.length) return '';
const playerLines = playerUrls.map((playerUrl, index) => {
const prefix = playerUrls.length > 1 || playerUrl.includes('=') ? `${index + 1}=` : '';
return ` |${prefix}${playerUrl}`;
});
if (playerUrls.length > 1) {
return `\n\n{{Player|mode=mirrors\n${playerLines.join('\n')}\n}}`;
}
return `\n\n{{Player\n${playerLines.join('\n')}\n}}`;
}
function buildImageReference(episode) {
return `[[File:${buildMixesdbTitle(episode)}.jpg|right|360px]]`;
}
function buildEpisodePageText(episode, episodeUrl = '', fetchedPlayerUrl = '', tracklistResult = { text: '\n\n
', status: 'none' }) {
const categories = [
...[episode.date ? episode.date.slice(0, 4) : '', episode.artist, config.showCategory]
.filter(Boolean)
.map(category => `[[Category:${category}]]`),
'[[Category:]]',
`[[Category:Tracklist: ${tracklistResult.status}]]`,
].join('\n');
const imageReference = buildImageReference(episode);
const imageText = imageReference ? `${imageReference}\n\n` : '';
const notesText = episodeUrl ? `\n\n== Notes ==\n\n${episodeUrl}` : '';
return `${imageText}== File details ==\n\n${buildFileDetailsText(episode)}${buildPlayerText(getPlayerUrlsForEpisode(episode, fetchedPlayerUrl))}${notesText}\n\n== Tracklist ==\n\n${tracklistResult.text}\n\n${categories}`;
}
function setCreateLinkHref(link, episode, episodeUrl, fetchedPlayerUrl = '', tracklistResult) {
link.href = importer.buildMixesdbUrl(buildMixesdbTitle(episode), episodeUrl, buildEpisodePageText(episode, episodeUrl, fetchedPlayerUrl, tracklistResult));
}
function bindCreateLinkRefreshOnClick(link, episode, episodeUrl) {
if (link.dataset.mdbIaMixCreateLinkRefresh === '1') return;
link.dataset.mdbIaMixCreateLinkRefresh = '1';
link.addEventListener('click', () => {
if (existingEpisodes.has(episode.episodeNumber)) return;
setCreateLinkHref(link, episode, episodeUrl, link.dataset.playerUrl || '', link.mdbIaMixTracklistResult);
logStep('refreshed create link on click', {
episodeNumber: episode.episodeNumber,
episodeUrl,
title: buildMixesdbTitle(episode),
date: episode.date || '',
playerUrl: link.dataset.playerUrl || '',
tracklistStatus: link.mdbIaMixTracklistResult?.status || 'none',
});
}, { capture: true });
}
async function updateMixesdbLink(link, episode, wrapper) {
const episodeUrl = link.dataset.episodeUrl || location.href;
const isExisting = existingEpisodes.has(episode.episodeNumber);
link.className = `${config.classNames.link} ${isExisting ? 'is-existing' : 'is-pending'}`;
link.textContent = isExisting ? 'See on MixesDB' : `Checking IA MIX ${importer.padNumber(episode.episodeNumber)}`;
setLinkVisitedState(link);
bindCreateLinkRefreshOnClick(link, episode, episodeUrl);
try {
const details = await fetchEpisodeDetails(episodeUrl);
episode.date = details.date || episode.date;
link.dataset.playerUrl = details.playerUrl || '';
link.mdbIaMixTracklistResult = details.tracklist;
logStep('fetched episode details', { episodeNumber: episode.episodeNumber, episodeUrl, date: details.date, playerUrl: details.playerUrl, tracklistStatus: details.tracklist?.status || 'none' });
if (isExisting) {
link.href = importer.buildMixesdbUrl(buildMixesdbTitle(episode), episodeUrl);
} else {
setCreateLinkHref(link, episode, episodeUrl, details.playerUrl, details.tracklist);
}
} catch (error) {
logStep('failed to fetch episode details', { episodeUrl, error: error.message || error });
if (isExisting) {
link.href = importer.buildMixesdbUrl(buildMixesdbTitle(episode), episodeUrl);
} else {
setCreateLinkHref(link, episode, episodeUrl);
}
}
link.className = `${config.classNames.link} ${isExisting ? 'is-existing' : 'is-missing'}`;
link.textContent = isExisting ? 'See on MixesDB' : 'Copy to MixesDB';
link.title = buildMixesdbTitle(episode);
setLinkVisitedState(link);
}
function setEpisodeVisibility(wrapper, episode) {
wrapper.classList.toggle(config.classNames.hidden, removeExistingEpisodes && existingEpisodes.has(episode.episodeNumber));
}
function updateEpisodeVisibility() {
document.querySelectorAll(`${config.selectors.episodeWrapper}[data-mdb-episode-number]`).forEach(wrapper => {
setEpisodeVisibility(wrapper, { episodeNumber: Number(wrapper.dataset.mdbEpisodeNumber) });
});
scheduleAutoLoadMoreWhenNoNewEpisodes();
}
function getVisibleMissingEpisodeCount() {
return Array.from(document.querySelectorAll(`${config.selectors.episodeWrapper}[data-mdb-episode-number]`))
.filter(wrapper => !wrapper.classList.contains(config.classNames.hidden))
.filter(wrapper => !existingEpisodes.has(Number(wrapper.dataset.mdbEpisodeNumber)))
.length;
}
function getNextPageLink() {
return document.querySelector(config.selectors.nextPage);
}
function setAutoLoadMoreFeedback(isLoading) {
const nextPage = getNextPageLink();
if (!nextPage) return;
let waiter = nextPage.parentElement?.querySelector(`.${config.classNames.autoLoadMoreWaiter}`);
if (!isLoading) {
waiter?.remove();
return;
}
if (!waiter) {
waiter = document.createElement('span');
waiter.className = config.classNames.autoLoadMoreWaiter;
nextPage.insertAdjacentElement('afterend', waiter);
}
waiter.textContent = ' Loading hidden episodes…';
}
function scheduleAutoLoadMoreWhenNoNewEpisodes() {
clearTimeout(autoLoadMoreTimer);
if (!removeExistingEpisodes || autoLoadMoreInProgress) return;
autoLoadMoreTimer = setTimeout(autoLoadMoreWhenNoNewEpisodes, config.autoLoadMoreDelay);
}
function autoLoadMoreWhenNoNewEpisodes() {
const nextPage = getNextPageLink();
if (!removeExistingEpisodes || autoLoadMoreInProgress || !nextPage) return;
const visibleMissingCount = getVisibleMissingEpisodeCount();
if (visibleMissingCount > 0 || visibleMissingCount > lastAutoLoadVisibleMissingCount) {
lastAutoLoadVisibleMissingCount = visibleMissingCount;
return;
}
autoLoadMoreInProgress = true;
setAutoLoadMoreFeedback(true);
nextPage.click();
setTimeout(() => {
autoLoadMoreInProgress = false;
setAutoLoadMoreFeedback(false);
scheduleAutoLoadMoreWhenNoNewEpisodes();
}, config.autoLoadMoreDelay);
}
function createRemoveExistingToggle() {
const toggleWrapper = document.createElement('label');
toggleWrapper.className = config.classNames.toggle;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = removeExistingEpisodes;
checkbox.addEventListener('change', () => {
removeExistingEpisodes = checkbox.checked;
localStorage.setItem(config.storageKey, removeExistingEpisodes ? 'true' : 'false');
lastAutoLoadVisibleMissingCount = getVisibleMissingEpisodeCount();
updateEpisodeVisibility();
});
toggleWrapper.append(checkbox, document.createTextNode('Hide episodes that exist on MixesDB'));
return toggleWrapper;
}
function addRemoveExistingToggle() {
if (document.querySelector(`.${config.classNames.toggle}`)) return;
const listContainer = document.querySelector(config.selectors.listContainer) || document.body;
listContainer.insertAdjacentElement('beforebegin', createRemoveExistingToggle());
}
function getEpisodeUrl(wrapper) {
const episodeLink = wrapper.querySelector(config.selectors.episodeLink)
|| wrapper.querySelector('a[rel=\"bookmark\"][href*=\"/mix/\"], a[href*=\"/mix/\"]');
return episodeLink ? episodeLink.href : location.href;
}
function hydrateEpisodeDateFromWrapper(episode, wrapper) {
if (episode.date) return false;
const postedOn = wrapper.querySelector(config.selectors.postedOn);
const datetime = postedOn?.querySelector(config.selectors.postedOnTime)?.getAttribute('datetime');
episode.date = normalizeDate(datetime) || normalizeDate(postedOn?.textContent);
return Boolean(episode.date);
}
function updateLinkDateFromPostedOn(link, episode, wrapper, postedOn) {
if (episode.date) return;
const datetime = postedOn.querySelector(config.selectors.postedOnTime)?.getAttribute('datetime');
const date = normalizeDate(datetime) || normalizeDate(postedOn.textContent);
if (!date) return;
episode.date = date;
setCreateLinkHref(link, episode, link.dataset.episodeUrl || location.href, link.dataset.playerUrl || '');
link.title = buildMixesdbTitle(episode);
logStep('hydrated detail page date from .posted-on', {
episodeNumber: episode.episodeNumber,
episodeUrl: link.dataset.episodeUrl || location.href,
date,
});
}
function waitForPostedOnDate(link, episode, wrapper) {
if (episode.date || typeof waitForKeyElements !== 'function') return;
const selector = `${config.selectors.episodeWrapper}[data-mdb-episode-number="${episode.episodeNumber}"] ${config.selectors.postedOn}`;
waitForKeyElements(selector, jNode => {
updateLinkDateFromPostedOn(link, episode, wrapper, jNode[0]);
return !episode.date;
}, true);
}
function createMixesdbLink(heading, episode, wrapper) {
hydrateEpisodeDateFromWrapper(episode, wrapper);
const episodeUrl = getEpisodeUrl(wrapper);
const link = document.createElement('a');
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.dataset.episodeNumber = String(episode.episodeNumber);
link.dataset.episodeUrl = episodeUrl;
link.href = link.dataset.episodeUrl;
link.addEventListener('click', () => {
const mixcloudUrl = getMixcloudUrlForEpisode(episode, link.dataset.playerUrl || '');
markLinkVisited(link);
wrapper.classList.add(config.classNames.copiedWrapper);
if (!existingEpisodes.has(episode.episodeNumber) && mixcloudUrl) {
window.open(mixcloudUrl, '_blank', 'noopener,noreferrer');
logStep('opened Mixcloud URL on copy click', {
episodeNumber: episode.episodeNumber,
episodeUrl,
mixcloudUrl,
});
}
});
updateMixesdbLink(link, episode, wrapper);
waitForPostedOnDate(link, episode, wrapper);
return link;
}
function createEpisodeCloseButton() {
const button = document.createElement('button');
button.type = 'button';
button.className = config.classNames.closeButton;
button.setAttribute('aria-label', 'Hide this episode');
button.title = 'Hide this episode';
button.textContent = 'Ă—';
button.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
button.closest('article')?.remove();
});
return button;
}
function addEpisodeCloseButton(wrapper) {
if (wrapper.querySelector(`:scope > .${config.classNames.closeButton}`)) return;
wrapper.append(createEpisodeCloseButton());
}
function addStyles() {
const style = document.createElement('style');
style.textContent = `
${config.selectors.episodeWrapper}[data-mdb-episode-number] { position: relative; }
.${config.classNames.closeButton} {
align-items: center; background: #00000080; border: 1px solid #ffffff80; border-radius: 50%;
color: #fff; cursor: pointer; display: flex; font-size: 1.25rem; font-weight: 700; height: 1.7rem;
justify-content: center; line-height: 1; padding: 0; position: absolute; right: 0.6rem; top: 0.6rem; width: 1.7rem; z-index: 2;
}
.${config.classNames.closeButton}:hover, .${config.classNames.closeButton}:focus { background: #ff6600; border-color: #ff6600; outline: none; }
.${config.classNames.link} { display: inline-block; margin: 0.35em 0 0.75em; padding: 0.25em 0.6em; border-radius: 4px; font-size: 0.85rem; font-weight: 700; line-height: 1.4; text-decoration: none; }
.${config.classNames.link}.is-visited { opacity: .62 !important; }
.${config.classNames.link}.is-existing { background: #2ea70060; color: #fff !important; border: 1px solid #4d9f4d; }
.${config.classNames.link}.is-missing, .${config.classNames.link}.is-pending { background: #ff660050; border: 1px solid #f60; color: #fff !important; }
.${config.classNames.toggle} { display: block; margin: 0 0 1rem; padding: 0.6em 0.8em; border: 1px solid #4d9f4d; border-radius: 4px; background: #2ea70030; color: #fff; font-size: 0.95rem; font-weight: 700; line-height: 1.4; }
.${config.classNames.toggle} input { margin-right: 0.35em; vertical-align: middle; }
.${config.classNames.hidden} { display: none !important; }
.${config.classNames.autoLoadMoreWaiter} { display: inline-block; margin-left: 0.5rem; color: #fff; font-size: 0.85rem; font-weight: 700; }
`;
document.head.appendChild(style);
}
function addEpisodeLinks() {
document.querySelectorAll(config.selectors.episodeWrapper).forEach(wrapper => {
const heading = wrapper.querySelector(config.selectors.episodeHeading);
if (!heading || heading.dataset.mdbImporterProcessed === 'true') return;
const episode = parseEpisodeTitle(heading.textContent.trim());
if (!episode) return;
wrapper.dataset.mdbEpisodeNumber = String(episode.episodeNumber);
addEpisodeCloseButton(wrapper);
heading.dataset.mdbImporterProcessed = 'true';
heading.insertAdjacentElement('afterend', createMixesdbLink(heading, episode, wrapper));
setEpisodeVisibility(wrapper, episode);
});
scheduleAutoLoadMoreWhenNoNewEpisodes();
}
async function loadPlayerEpisodes() {
if (Object.keys(playerEpisodes).length) return;
const url = 'https://raw.githubusercontent.com/mixesdb/userscripts/refs/heads/main/private/Episodes_Importer/IA_MIX/player_episodes.js?fresh=' + Date.now();
const response = await fetch(url, { cache: 'no-store' });
if (!response.ok) throw new Error(`player_episodes.js responded with ${response.status}`);
const code = await response.text();
(0, eval)(code);
playerEpisodes = window.MixesDBIaMixPlayerEpisodes || {};
}
function startEpisodeObserver() {
const observer = new MutationObserver(addEpisodeLinks);
observer.observe(document.body, { childList: true, subtree: true });
}
if (location.hostname === mixesdbHost) {
importer.insertMixesdbTextFromUrl();
return;
}
if (location.hostname !== sourceHost) return;
removeExistingEpisodes = localStorage.getItem(config.storageKey) === 'true';
loadVisitedEpisodeLinks();
addStyles();
addRemoveExistingToggle();
loadPlayerEpisodes()
.catch(error => importer.logValue('Failed to load fresh IA MIX player episodes', error.message || error))
.then(() => importer.fetchExistingEpisodes(config))
.then(episodeNumbers => {
existingEpisodes = new Set(episodeNumbers);
addRemoveExistingToggle();
addEpisodeLinks();
startEpisodeObserver();
})
.catch(error => {
importer.logValue('Failed to load existing IA MIX episodes from MixesDB', error.message || error);
addEpisodeLinks();
startEpisodeObserver();
});
}());