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 `${name}`; }, } ], }; 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) { 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}`; errors.appendChild(element); console.error(err); } return []; } async function initial() { media = []; const errors = document.querySelector('#errors'); 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(); 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(); });