const allLinks = [...document.getElementsByTagName('a')]; const bodyInner = document.querySelector('.inner'); const tableElem = document.querySelector('table'); const infoRegex = /(-thumb)?\.(jpg|nfo|srt)$/; let infoFiles = 0; let infoFilesAccounted = 0; const arrow = ` `; function reencodeURI(uri) { return encodeURIComponent(decodeURIComponent(uri)); } function getHref(tag) { return tag.getAttribute('href'); } function findByHref(href) { return allLinks.filter((link) => { return reencodeURI(getHref(link)) === reencodeURI(href) }); } function findWholeNumberInText(text, matchNumber) { const numbers = text.match(/\d+/g); return numbers && numbers.length && numbers.map((x) => parseInt(x, 10)).some((number) => number === matchNumber); } function findByInfoFile(infoFile) { const found = ['mkv', 'mp4', 'avi'].reduce((last, current) => { const element = findByHref(infoFile.replace(infoRegex, '.' + current))[0]; return element || last; }, null); return found; } function deduplicateJointEpisode(textContent) { return new Promise((resolve, reject) => { const lns = textContent.split('\n'); const result = []; let passed = 0; for (const line of lns) { if (line.startsWith('')) { if (passed === 1) { break; } passed = 1; } result.push(line); } resolve(result.join('\n')); }); } const parser = new window.DOMParser(); async function createXMLRequest(nfo) { return fetch(nfo) .then((response) => response.text()) .then((text) => deduplicateJointEpisode(text)) .then((str) => parser.parseFromString(str, 'text/xml')); } // Fill metadata boxes, generate buttons function fillMeta( content, original, nfo, movieTitle, movieDescription, movieButtons ) { infoFilesAccounted += 1; if (!content) { return; } const titleEl = content.querySelector('title'); const plotEl = content.querySelector('plot'); const airedEl = content.querySelector('aired'); const premieredEl = content.querySelector('premiered'); const imdbEl = content.querySelector('uniqueid[type="imdb"]'); const tvdbEl = content.querySelector('uniqueid[type="tvdb"]'); if (!titleEl || !plotEl) { return; } if (airedEl || premieredEl) { const timestamp = original.parentElement.parentElement.querySelector('.timestamp'); if (timestamp) { timestamp.innerText = (airedEl || premieredEl).textContent; } } const episode = content.querySelector('episode'); const season = content.querySelector('season'); let title = titleEl.textContent if (episode && season) { title = `(S${season.textContent} E${episode.textContent}) ` + title; original.parentElement.setAttribute('data-episode', episode.textContent); } movieTitle.innerText = title; movieDescription.innerText = plotEl.textContent; if (imdbEl) { const imlink = document.createElement('a'); imlink.target = '_blank'; imlink.innerText = 'IMDb'; imlink.href = `https://www.imdb.com/title/${imdbEl.textContent}`; movieButtons.appendChild(imlink); } if (tvdbEl) { const tvlink = document.createElement('a'); tvlink.target = '_blank'; tvlink.innerText = 'The TVDB'; tvlink.href = `http://www.thetvdb.com/?tab=series&id=${tvdbEl.textContent}`; movieButtons.appendChild(tvlink); } // Add "copy direct url" button if ( getHref(nfo) !== 'tvshow.nfo' && navigator && navigator.clipboard && navigator.clipboard.writeText ) { const copybutton = document.createElement('button'); copybutton.innerText = 'Copy direct URL'; movieButtons.appendChild(copybutton); copybutton.addEventListener('click', (e) => { e.preventDefault(); const popup = document.createElement('div'); popup.className = 'tooltip'; popup.innerText = 'Copied!'; const url = window.location.href + reencodeURI(getHref(original)); navigator.clipboard.writeText(url).then(() => { copybutton.appendChild(popup); setTimeout(() => copybutton.removeChild(popup), 1000); }); }); } } // Replace list entry with a metadata box // Either create or populate function createOrImproveMovieMeta(original, thumbnail, nfo) { let imgTag = ``; let movieTitle = `
${original.innerText}
`; let movieDescription = `
`; let movieButtons = `
`; if (!thumbnail && !nfo) { return; } if (original.parentElement.classList.contains('enhanced')) { imgTag = original.querySelector('.thumbnail'); movieTitle = original.querySelector('.movie-title'); movieDescription = original.querySelector('.movie-description'); movieButtons = original.querySelector('.movie-buttons'); if (thumbnail) { imgTag.src = reencodeURI(getHref(thumbnail)); } } else { original.classList.add('movie'); original.innerHTML = imgTag + '
' + movieTitle + movieDescription + movieButtons + '
'; original.parentElement.classList.add('enhanced'); // quick innerHTML hack to create elements here lol imgTag = original.querySelector('.thumbnail'); movieTitle = original.querySelector('.movie-title'); movieDescription = original.querySelector('.movie-description'); movieButtons = original.querySelector('.movie-buttons'); } // Fetch metadata from the nfo file and populate the info box if (nfo) { createXMLRequest(reencodeURI(getHref(nfo))) .then((content) => fillMeta(content, original, nfo, movieTitle, movieDescription, movieButtons)) .catch((e) => { infoFilesAccounted += 1; console.error(e); }); } } // Promise that resolves when all info files that were found have been parsed function waitUntilInfoComplete() { return new Promise((resolve, reject) => { if (infoFilesAccounted >= infoFiles) { resolve(true); return; } let seconds = 0; const accountWait = setInterval(() => { if (infoFilesAccounted >= infoFiles || seconds > 20) { clearInterval(accountWait); resolve(true); return; } seconds += 1; }, 500); }); } // Get episode name or name of file const getCellValue = (tr, idx) => tr.children[idx].getAttribute('data-episode') || tr.children[idx].innerText || tr.children[idx].textContent; // Compare numeric or (locale-aware, numeric-aware) by string values const comparer = (idx, asc) => (a, b) => ((v1, v2) => v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2, 'et', { numeric: true }) )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx)); // Table sort by column index const sortTableValues = (index) => Array.from(tableElem.querySelectorAll('tr:nth-child(n+2):not(.btn-back)')) .sort(comparer(index, this.asc = !this.asc)) .forEach((tr) => tableElem.appendChild(tr)); let tvShow; function accountForMetadata() { allLinks.forEach((link) => { const url = getHref(link); // If nfo or jpg (or subtitle, for hiding it).. if (url.match(infoRegex)) { // Hide metadata file link.parentElement.parentElement.style.display = 'none'; let findOriginal // Place TV show information in the beginning of the table if (url === 'tvshow.nfo' || url === 'poster.jpg') { if (url === 'poster.jpg' && !allLinks.find((found) => getHref(found) === 'tvshow.nfo')) { findOriginal = document.querySelector('a:not([href=".."])'); } else { if (tvShow) { findOriginal = tvShow; } else { const wrapper = document.createElement('div'); wrapper.className = 'highlight'; tvShow = document.createElement('a'); tvShow.className = 'movie'; wrapper.appendChild(tvShow); bodyInner.insertBefore(wrapper, tableElem); findOriginal = tvShow; } } // Give posters to seasons } else if (url.startsWith('season') && url.includes('poster')) { // Find season by number const sn = url.match(/season(\d+)/); if (sn) { findOriginal = allLinks.find( ({ innerText }) => { return innerText.endsWith('/') && findWholeNumberInText(innerText, parseInt(sn[1], 10)); }); // Find specials folder } else if (url.includes('specials')) { findOriginal = allLinks.find(({ innerText }) => { const matchAgainst = innerText.toLowerCase(); return (matchAgainst.startsWith('specials') || matchAgainst.startsWith('extras')) && innerText.endsWith('/'); }); } // Find the video file associated with this info file } else { findOriginal = findByInfoFile(url); } // Populate video file/season folder/tv show info box if (findOriginal) { if (url.match(/.nfo$/)) { infoFiles += 1; } createOrImproveMovieMeta(findOriginal, url.match(/.jpg$/) ? link : undefined, url.match(/.nfo$/) ? link : undefined, ); } } // Replace the back button with a more descriptive one if (url === '..') { link.parentElement.parentElement.classList.add('btn-back'); link.innerHTML = arrow + 'Back (..)'; } }); waitUntilInfoComplete().then(() => sortTableValues(0)); } function createToggleCheckbox(field, value, description) { const form = document.createElement('div'); const checkbox = document.createElement('input'); const label = document.createElement('label'); form.className = 'form ' + field; checkbox.type = 'checkbox'; checkbox.id = field label.innerText = description; label.setAttribute('for', field); form.appendChild(checkbox); form.appendChild(label) bodyInner.appendChild(form); checkbox.addEventListener('change', (e) => { if (checkbox.checked) { window.localStorage.setItem(field, 'true'); } else { window.localStorage.removeItem(field); } window.location.reload(); }); checkbox.checked = value === 'true'; } if ('localStorage' in window) { const plainMode = window.localStorage.getItem('plainMode'); if (plainMode !== 'true') { accountForMetadata(); } createToggleCheckbox('plainMode', plainMode, 'Disable metadata / display plain index listing (less bandwidth required)'); } else { accountForMetadata(); } document.querySelectorAll('th').forEach((th) => th.addEventListener('click', (() => { sortTableValues(Array.from(th.parentNode.children).indexOf(th)); })));