// ==UserScript== // @name Mediux - YAML to Kometa // @version 2.3.0 // @description Adds buttons to transform MediUX TV and movie YAML into Kometa-compatible metadata. // @author Journey Over // @license MIT // @match *://mediux.pro/* // @require https://cdn.jsdelivr.net/gh/StylusThemes/Userscripts@0171b6b6f24caea737beafbc2a8dacd220b729d8/libs/utils/utils.min.js // @grant none // @run-at document-end // @icon https://www.google.com/s2/favicons?sz=64&domain=mediux.pro // @homepageURL https://github.com/StylusThemes/Userscripts // @downloadURL https://github.com/StylusThemes/Userscripts/raw/main/userscripts/mediux-yaml-to-kometa.user.js // @updateURL https://github.com/StylusThemes/Userscripts/raw/main/userscripts/mediux-yaml-to-kometa.user.js // ==/UserScript== (function() { 'use strict'; const logger = Logger('Mediux - YAML to Kometa', { debug: false }); const App = { utils: { getYear(setId) { const setLinkText = document.querySelector(`a[href="/sets/${setId}"]`)?.textContent?.trim() || ''; const headingText = document.querySelector('h1')?.textContent?.trim() || ''; const headerYearText = document.querySelector('header p:first-of-type')?.textContent?.trim() || ''; return setLinkText.match(/\((\d{4})\)/)?.[1] || headingText.match(/\((\d{4})\)/)?.[1] || headerYearText.match(/^(\d{4})$/)?.[1] || 'Unknown'; }, showNotification(message, targetButton, duration = 3000) { const tooltip = document.createElement('div'); tooltip.textContent = message; Object.assign(tooltip.style, { position: 'fixed', bottom: (window.innerHeight - targetButton.getBoundingClientRect().top + 6) + 'px', left: (targetButton.getBoundingClientRect().left + targetButton.offsetWidth / 2) + 'px', transform: 'translateX(-50%)', background: '#1f2937', color: '#f3f4f6', padding: '4px 10px', borderRadius: '6px', fontSize: '11px', lineHeight: '1.4', whiteSpace: 'nowrap', zIndex: '999', pointerEvents: 'none', boxShadow: '0 4px 6px -1px rgba(0,0,0,0.3)' }); document.body.appendChild(tooltip); setTimeout(() => tooltip.remove(), duration); }, updateButtonState(button, success = true) { const successClass = success ? 'text-green-500' : 'text-red-500'; button.classList.remove('text-gray-400'); button.classList.add(successClass); setTimeout(() => { button.classList.remove('text-green-500', 'text-red-500'); button.classList.add('text-gray-400'); }, 3000); } }, yaml: { formatTvYml(codeblock, button) { let yamlContent = codeblock.textContent; const regexSetInfo = /(null|\d+): # TVDB id for (.*?)\. Set by (.*?) on MediUX\. (https:\/\/mediux\.pro\/sets\/(\d+))/; const setMatch = yamlContent.match(regexSetInfo); if (setMatch) { const tvdbId = setMatch[1]; const showTitle = setMatch[2]; const setUrl = setMatch[4]; const setId = setMatch[5]; const year = App.utils.getYear(setId); yamlContent = yamlContent.replace(regexSetInfo, `# Posters from:\n# ${setUrl}\n\nmetadata:\n\n ${tvdbId}: # ${showTitle} (${year})`); } yamlContent = yamlContent.replace(/^\s+# Posters from:/m, `# Posters from:`); yamlContent = yamlContent.replace(/(url_poster|url_background): (https:\/\/api\.mediux\.pro\/assets\/[a-z0-9\-]+)/g, '$1: "$2"'); yamlContent = yamlContent.replace(/(\d+):\n\s+url_poster: (https:\/\/api\.mediux\.pro\/assets\/[a-z0-9\-]+)\n/g, (match, season, posterUrl) => ` ${season}:\n url_poster: "${posterUrl}"\n`); codeblock.innerText = yamlContent; navigator.clipboard.writeText(yamlContent) .then(() => { App.utils.showNotification('YAML transformed and copied to clipboard!', button); App.utils.updateButtonState(button); }) .catch(error => { logger.error('Clipboard write failed', error); App.utils.updateButtonState(button, false); }); }, formatMovieYml(codeblock, button) { let yamlContent = codeblock.textContent; const regexSetUrl = /https:\/\/mediux\.pro\/sets\/\d+/; const urlMatch = yamlContent.match(regexSetUrl); const setUrl = urlMatch ? urlMatch[0] : null; if (setUrl) { yamlContent = yamlContent.replace( /(\d+):\s*#\s*(.*?)\s*\((\d{4})\).*?(https:\/\/mediux\.pro\/sets\/\d+)/g, (match, movieId, movieTitle, releaseYear) => `${movieId}: # ${movieTitle.trim()} (${releaseYear})` ); const yamlHeader = `# Posters from:\n# ${setUrl}\n\nmetadata:\n\n`; yamlContent = yamlContent.replace(/(^|\n)metadata:\n/g, ''); yamlContent = yamlHeader + yamlContent; yamlContent = yamlContent .replace(/(url_poster|url_background): (https:\/\/api\.mediux\.pro\/assets\/\S+)/g, '$1: "$2"') .replace(/(\n\n)(\s+\n)/g, '\n\n') .replace(/\n{3,}/g, '\n\n'); } codeblock.innerText = yamlContent; navigator.clipboard.writeText(yamlContent) .then(() => { App.utils.showNotification('YAML transformed and copied to clipboard!', button); App.utils.updateButtonState(button); }) .catch(error => { logger.error('Clipboard write failed', error); App.utils.updateButtonState(button, false); }); } }, ui: { createInterface(codeblock) { if (!codeblock) return; const dialog = codeblock.closest('[role="dialog"]'); if (!dialog) return; if (dialog.querySelector('#extbuttons')) return; const buttonConfigs = [ { id: 'fytvbutton', title: 'Copy TV YAML to clipboard', icon: '', text: 'TV', action: (button) => App.yaml.formatTvYml(codeblock, button) }, { id: 'fymoviebutton', title: 'Copy Movie YAML to clipboard', icon: '', text: 'Movie', action: (button) => App.yaml.formatMovieYml(codeblock, button) } ]; const extensionButtons = document.createElement('div'); extensionButtons.id = 'extbuttons'; extensionButtons.className = 'flex flex-wrap gap-2'; extensionButtons.setAttribute('role', 'group'); extensionButtons.setAttribute('aria-label', 'YAML actions'); for (const config of buttonConfigs) { const button = document.createElement('button'); button.id = config.id; button.type = 'button'; button.title = config.title; button.className = 'inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium text-gray-400 hover:text-white border border-gray-700 hover:border-gray-500 transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-gray-500'; button.innerHTML = config.icon + '' + config.text + ''; button.addEventListener('click', () => config.action(button)); extensionButtons.appendChild(button); } // Find the dialog's direct child that contains the code block const codeBlockContainer = [...dialog.children].find(child => child.contains(codeblock)); if (codeBlockContainer) { codeBlockContainer.before(extensionButtons); } else { dialog.appendChild(extensionButtons); } } }, init() { observeElement('code.whitespace-pre-wrap', (codeblock) => { this.ui.createInterface(codeblock); logger('Initialized'); }); } }; App.init(); function observeElement(selector, callback) { const existing = document.querySelector(selector); if (existing) { callback(existing); return; } const observer = new MutationObserver(() => { const element = document.querySelector(selector); if (element) { callback(element); } }); observer.observe(document.body, { childList: true, subtree: true }); } })();