435 lines
13 KiB
JavaScript
435 lines
13 KiB
JavaScript
const allLinks = [...document.getElementsByTagName('a')];
|
|
const bodyInner = document.querySelector('.inner');
|
|
const tableElem = document.querySelector('table');
|
|
const dirnameElem = document.querySelector('#dirname');
|
|
const infoRegex = /(-thumb)?\.(jpg|nfo|srt)$/;
|
|
let infoFiles = 0;
|
|
let infoFilesAccounted = 0;
|
|
|
|
const arrow = `
|
|
<svg version="1.1" id="back-arrow" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
viewBox="0 0 492 492" style="enable-background:new 0 0 492 492;" xml:space="preserve">
|
|
<g>
|
|
<g>
|
|
<path fill="#fff" d="M464.344,207.418l0.768,0.168H135.888l103.496-103.724c5.068-5.064,7.848-11.924,7.848-19.124
|
|
c0-7.2-2.78-14.012-7.848-19.088L223.28,49.538c-5.064-5.064-11.812-7.864-19.008-7.864c-7.2,0-13.952,2.78-19.016,7.844
|
|
L7.844,226.914C2.76,231.998-0.02,238.77,0,245.974c-0.02,7.244,2.76,14.02,7.844,19.096l177.412,177.412
|
|
c5.064,5.06,11.812,7.844,19.016,7.844c7.196,0,13.944-2.788,19.008-7.844l16.104-16.112c5.068-5.056,7.848-11.808,7.848-19.008
|
|
c0-7.196-2.78-13.592-7.848-18.652L134.72,284.406h329.992c14.828,0,27.288-12.78,27.288-27.6v-22.788
|
|
C492,219.198,479.172,207.418,464.344,207.418z"/>
|
|
</g>
|
|
</g>
|
|
</svg>
|
|
`;
|
|
|
|
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', 'webm'].reduce((last, current) => {
|
|
const element = findByHref(infoFile.replace(infoRegex, '.' + current))[0];
|
|
return element || last;
|
|
}, null);
|
|
return found;
|
|
}
|
|
|
|
const pZ = (n) => n.toString().padStart(2, '0');
|
|
function restampDateTime(datetime, includeTime = true) {
|
|
const date = new Date(datetime);
|
|
const [month, day, year] = [
|
|
date.getMonth() + 1,
|
|
date.getDate(),
|
|
date.getFullYear(),
|
|
];
|
|
const [hour, minutes, seconds] = [
|
|
date.getHours(),
|
|
date.getMinutes(),
|
|
date.getSeconds(),
|
|
];
|
|
return `${year}/${pZ(month)}/${pZ(day)}${
|
|
includeTime ? ` ${pZ(hour)}:${pZ(minutes)}:${pZ(seconds)}` : ''
|
|
}`;
|
|
// return `${pZ(day)}/${pZ(month)}/${year}${includeTime ? ` ${pZ(hour)}:${pZ(minutes)}:${pZ(seconds)}` : ''}`;
|
|
}
|
|
|
|
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('<episodedetails>')) {
|
|
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) {
|
|
const original = timestamp.innerText;
|
|
const airdate = restampDateTime(
|
|
(airedEl || premieredEl).textContent,
|
|
false
|
|
);
|
|
const wrapper = `<span class="airdate">${airdate}</span>`;
|
|
const originalWrapper = ` <span class="original">(${original})</span>`;
|
|
timestamp.innerHTML = wrapper + originalWrapper;
|
|
}
|
|
}
|
|
|
|
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 = `<img class="thumbnail" src="${
|
|
thumbnail ? reencodeURI(getHref(thumbnail)) : ''
|
|
}"/>`;
|
|
let movieTitle = `<div class="movie-title">${original.innerText}</div>`;
|
|
let movieDescription = `<div class="movie-description"></div>`;
|
|
let movieButtons = `<div class="movie-buttons"></div>`;
|
|
|
|
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}<div class="meta-wrap">${movieTitle}${movieDescription}${movieButtons}</div>`;
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
|
|
function timestampify() {
|
|
Array.from(document.body.querySelectorAll('.timestamp')).forEach((stamp) => {
|
|
stamp.innerText = restampDateTime(stamp.innerText);
|
|
});
|
|
}
|
|
|
|
// 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';
|
|
}
|
|
|
|
function createDirTree() {
|
|
dirnameElem.classList.add('directory');
|
|
const dirSplit = dirnameElem.innerText.split('/').slice(1).filter((item) => !!item);
|
|
dirSplit.unshift('');
|
|
dirnameElem.innerHTML = '';
|
|
let lastPath = '';
|
|
for (const dir of dirSplit) {
|
|
lastPath += dir + '/';
|
|
const atag = document.createElement('a');
|
|
atag.innerText = dir;
|
|
atag.classList.add('directory-link');
|
|
atag.href = lastPath;
|
|
dirnameElem.appendChild(atag);
|
|
}
|
|
}
|
|
|
|
if ('localStorage' in window) {
|
|
const plainMode = window.localStorage.getItem('plainMode');
|
|
if (plainMode !== 'true') {
|
|
timestampify();
|
|
accountForMetadata();
|
|
}
|
|
createToggleCheckbox(
|
|
'plainMode',
|
|
plainMode,
|
|
'Disable metadata / display plain index listing (less bandwidth required)'
|
|
);
|
|
} else {
|
|
timestampify();
|
|
accountForMetadata();
|
|
}
|
|
|
|
document.querySelectorAll('th').forEach((th) =>
|
|
th.addEventListener('click', () => {
|
|
sortTableValues(Array.from(th.parentNode.children).indexOf(th));
|
|
})
|
|
);
|
|
|
|
createDirTree();
|