348 lines
8.6 KiB
JavaScript
348 lines
8.6 KiB
JavaScript
const apiUrls = [
|
|
{
|
|
url: '/sonarr-hs-series',
|
|
name: 'hidden-sea',
|
|
domain: 'sonarr-hs',
|
|
},
|
|
{
|
|
url: '/sonarr-ag-series',
|
|
name: 'ancient-grove',
|
|
domain: 'sonarr-ag',
|
|
},
|
|
{
|
|
url: '/sonarr-bs-series',
|
|
name: 'bold-silence',
|
|
domain: 'sonarr-bs',
|
|
},
|
|
{
|
|
url: '/sonarr-fd-series',
|
|
name: 'frosty-darkness',
|
|
domain: 'sonarr-fd',
|
|
},
|
|
{
|
|
url: '/sonarr-rs-series',
|
|
name: 'smooth-canyon [4K]',
|
|
domain: 'sonarr.srvr.no',
|
|
},
|
|
{
|
|
url: '/radarr-hs-movies',
|
|
name: 'hidden-sea',
|
|
domain: 'radarr-hs',
|
|
},
|
|
{
|
|
url: '/radarr-ag-movies',
|
|
name: 'ancient-grove',
|
|
domain: 'radarr-ag',
|
|
},
|
|
{
|
|
url: '/radarr-bs-movies',
|
|
name: 'bold-silence',
|
|
domain: 'radarr-bs',
|
|
},
|
|
{
|
|
url: '/radarr-fd-movies',
|
|
name: 'frosty-darkness',
|
|
domain: 'radarr-fd',
|
|
},
|
|
];
|
|
|
|
let media = [];
|
|
|
|
function removeMessage()
|
|
{
|
|
const message = document.querySelector('#status-message');
|
|
if (message) {
|
|
message.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For use with Array.prototype.map to map a series/movie to a row.
|
|
*
|
|
* @param {Object} mediaItem
|
|
* @returns Array
|
|
*/
|
|
const editionRegex = /\{edition-(.+)\}$/g;
|
|
function mediaToRow(mediaItem) {
|
|
let { title, mediaType, movieFile, year, server, folderName } = mediaItem;
|
|
|
|
let quality = '';
|
|
if (movieFile) {
|
|
quality = movieFile.quality.quality.name;
|
|
}
|
|
|
|
let size = 0;
|
|
|
|
if (mediaItem.sizeOnDisk) {
|
|
size = mediaItem.sizeOnDisk;
|
|
} else if (mediaItem.statistics && mediaItem.statistics.sizeOnDisk) {
|
|
size = mediaItem.statistics.sizeOnDisk;
|
|
}
|
|
|
|
if (editionRegex.test(folderName)) {
|
|
const match = folderName.match(editionRegex);
|
|
const edition = match[0].replace('{edition-', '').replace('}', '');
|
|
title += ` [${edition}]`;
|
|
}
|
|
|
|
return [
|
|
title,
|
|
mediaType,
|
|
quality,
|
|
year,
|
|
size,
|
|
server,
|
|
];
|
|
}
|
|
|
|
function humanFileSize(input) {
|
|
if (input <= 0) {
|
|
return '0 B';
|
|
}
|
|
|
|
const i = Math.floor(Math.log(input) / Math.log(1024));
|
|
return (input / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
|
|
}
|
|
|
|
let dataTbl = null;
|
|
let documentTableCopy = null;
|
|
function addDataTable(rows)
|
|
{
|
|
const tblData = {
|
|
headings: [
|
|
'Series/Movie name',
|
|
'Type',
|
|
'Quality',
|
|
'Year',
|
|
'Size on disk',
|
|
'Server',
|
|
],
|
|
data: rows,
|
|
};
|
|
|
|
let tbl = document.querySelector('#media-table');
|
|
if (!documentTableCopy) {
|
|
documentTableCopy = tbl.cloneNode(true);
|
|
}
|
|
|
|
const checkDatatableWrapper = document.querySelector('.datatable-wrapper');
|
|
if (checkDatatableWrapper) {
|
|
checkDatatableWrapper.insertAdjacentElement('afterend', documentTableCopy);
|
|
checkDatatableWrapper.remove();
|
|
|
|
tbl = document.querySelector('#media-table');
|
|
}
|
|
|
|
const dataTblConfig = {
|
|
searchable: true,
|
|
fixedHeight: false,
|
|
perPage: 100,
|
|
perPageSelect: [25, 50, 100, 250, 500, 750, 1000],
|
|
data: tblData,
|
|
layout: {
|
|
top: '{search}{select}',
|
|
bottom: '{pager}{info}',
|
|
},
|
|
columns: [
|
|
{
|
|
select: [tblData.headings.indexOf('Size on disk')],
|
|
render: function(data, cell, row) {
|
|
if (typeof data !== 'number') {
|
|
data = parseInt(data, 10);
|
|
}
|
|
|
|
return humanFileSize(data);
|
|
}
|
|
},
|
|
{
|
|
// Resolve based on the heading, since we might shift it from time to time.
|
|
select: [tblData.headings.indexOf('Server')],
|
|
render: function(data, cell, row) {
|
|
const [name, domain, slug, id] = data.split('|');
|
|
|
|
let url = `https://${domain}`;
|
|
|
|
if (!domain.includes('srvr.no')) {
|
|
url += '.decicus.com';
|
|
}
|
|
|
|
if (domain.includes('sonarr')) {
|
|
url += `/series/${slug}`;
|
|
}
|
|
else {
|
|
url += `/movie/${id}`;
|
|
}
|
|
|
|
return `<a href="${url}" target="_blank">${name}</a>`;
|
|
},
|
|
}
|
|
],
|
|
};
|
|
|
|
removeMessage();
|
|
|
|
if (dataTbl) {
|
|
dataTbl.rows.remove();
|
|
}
|
|
|
|
dataTbl = new simpleDatatables.DataTable(tbl, dataTblConfig);
|
|
dataTbl.on('datatable.init', function() {
|
|
const search = document.querySelector('.datatable-search input');
|
|
|
|
if (search) {
|
|
search.focus();
|
|
}
|
|
});
|
|
|
|
// try {
|
|
// dataTbl.refresh();
|
|
// }
|
|
// catch (err) {
|
|
// console.error(err);
|
|
// }
|
|
}
|
|
|
|
async function apiFetch(apiUrl)
|
|
{
|
|
const errors = document.querySelector('#errors');
|
|
|
|
try {
|
|
const response = await fetch(apiUrl.url);
|
|
let json = await response.json();
|
|
|
|
json.map((item) => {
|
|
item.server = `${apiUrl.name}|${apiUrl.domain}|${item.titleSlug}|${item.tmdbId || item.id}`;
|
|
item.mediaType = apiUrl.url.includes('sonarr') ? 'Series' : 'Movie';
|
|
|
|
return item;
|
|
});
|
|
|
|
return json;
|
|
}
|
|
catch (err) {
|
|
const element = document.createElement('p');
|
|
element.textContent = `Error fetching data from ${apiUrl.url}: ${err.message}`;
|
|
|
|
if (errors) {
|
|
errors.appendChild(element);
|
|
}
|
|
|
|
console.error(err);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Array} media Flatten array of media objects
|
|
*/
|
|
async function printTotalFilesize(media)
|
|
{
|
|
const totalSizeElement = document.querySelector('#total-filesize');
|
|
if (!totalSizeElement) {
|
|
return;
|
|
}
|
|
|
|
let totalSizeOnDisk = 0;
|
|
for (const item of media)
|
|
{
|
|
const { path } = item;
|
|
// Ignore the 4K Plex server
|
|
if (path && (path.includes('/data/echo/MediaServer') || path.includes('/data/media/MediaServer'))) {
|
|
continue;
|
|
}
|
|
|
|
if (item.sizeOnDisk) {
|
|
totalSizeOnDisk += item.sizeOnDisk;
|
|
continue;
|
|
}
|
|
|
|
if (item.statistics && item.statistics.sizeOnDisk) {
|
|
totalSizeOnDisk += item.statistics.sizeOnDisk;
|
|
}
|
|
}
|
|
|
|
totalSizeElement.innerHTML = `💾 Total media storage size, minus <code>smooth-canyon</code> (4K server): <kbd>${humanFileSize(totalSizeOnDisk)}</kbd>`;
|
|
totalSizeElement.classList.remove('hidden');
|
|
}
|
|
|
|
async function initial()
|
|
{
|
|
media = [];
|
|
|
|
const refreshBtn = document.querySelector('#refresh-button');
|
|
let icon = null;
|
|
if (refreshBtn) {
|
|
icon = refreshBtn.querySelector('.fas');
|
|
refreshBtn.disabled = true;
|
|
icon.classList.add('fa-spin');
|
|
}
|
|
|
|
let promises = [];
|
|
for (const apiUrl of apiUrls) {
|
|
promises.push(apiFetch(apiUrl));
|
|
}
|
|
|
|
media = await Promise.all(promises);
|
|
media = media.flat();
|
|
|
|
printTotalFilesize(media);
|
|
|
|
media = media.sort((a, b) => {
|
|
return a.sortTitle.localeCompare(b.sortTitle);
|
|
});
|
|
|
|
let rows = media.map(mediaToRow);
|
|
|
|
addDataTable(rows);
|
|
|
|
const store = {
|
|
timestamp: Date.now(),
|
|
media,
|
|
};
|
|
|
|
const lastUpdated = document.querySelector('#last-updated');
|
|
lastUpdated.textContent = `Last updated: ${new Date(store.timestamp).toLocaleString()}`;
|
|
|
|
localStorage.setItem('cache', LZString.compress(JSON.stringify(store)));
|
|
|
|
if (refreshBtn && icon) {
|
|
icon.classList.remove('fa-spin');
|
|
refreshBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', function() {
|
|
const cache = localStorage.getItem('cache');
|
|
if (cache) {
|
|
const { timestamp, media } = JSON.parse(LZString.decompress(cache));
|
|
let rows = media.map(mediaToRow);
|
|
addDataTable(rows);
|
|
|
|
const lastUpdated = document.querySelector('#last-updated');
|
|
lastUpdated.textContent = `Last updated: ${new Date(timestamp).toLocaleString()}`;
|
|
}
|
|
|
|
const refreshBtn = document.querySelector('#refresh-button');
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener('click', async function() {
|
|
// I don't think this is necessary, but ayyy lmao
|
|
if (refreshBtn.disabled) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await initial();
|
|
}
|
|
catch (err) {
|
|
console.error(err);
|
|
icon.classList.remove('fa-spin');
|
|
refreshBtn.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
initial();
|
|
});
|