WIP as of May 12th 2024

This commit is contained in:
Alex Thomassen 2024-05-12 16:56:36 +02:00
parent fef4b5bb83
commit d11742ec8a
10 changed files with 347 additions and 143 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
config.php

View File

@ -2,4 +2,4 @@
The code powering [tv.cocks.no](https://tv.cocks.no/)
Most of it relies on parsing the [Sonarr iCalendar feed](https://wiki.servarr.com/sonarr/calendar) and rendering it with [FullCalendar](https://fullcalendar.io/).
Most of it relies on parsing the [Sonarr iCalendar feed](https://wiki.servarr.com/sonarr/calendar) and rendering it with [FullCalendar](https://fullcalendar.io/).

1
config.sample.php Normal file
View File

@ -0,0 +1 @@
<?php

View File

@ -1,128 +0,0 @@
const apiUrls = [
{
url: '/sonarr-hs-series',
name: 'hidden-sea',
domain: 'sonarr-hs',
},
{
url: '/sonarr-cb-series',
name: 'cold-badlands',
domain: 'sonarr-cb',
},
{
url: '/sonarr-fv-series',
name: 'fancy-valley',
domain: 'sonarr-fv',
},
{
url: '/radarr-hs-movies',
name: 'hidden-sea',
domain: 'radarr-hs',
},
{
url: '/radarr-cb-movies',
name: 'cold-badlands',
domain: 'radarr-cb',
},
{
url: '/radarr-fv-movies',
name: 'fancy-valley',
domain: 'radarr-fv',
},
];
let media = [];
function removeMessage()
{
const message = document.querySelector('#status-message');
if (message) {
message.remove();
}
}
async function initial()
{
for (const apiUrl of apiUrls) {
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;
});
media = [...media, ...json];
}
media = media.sort((a, b) => {
return a.sortTitle.localeCompare(b.sortTitle);
});
let rows = media.map((mediaItem) => {
const { title, mediaType, year, server } = mediaItem;
return [
title,
mediaType,
year,
server,
];
});
const tblData = {
headings: [
'Series/Movie name',
'Type',
'Year',
'Server',
],
data: rows,
};
const 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: [3],
render: function(data, cell, row) {
const [name, domain, slug, id] = data.split('|');
let url = `https://${domain}.decicus.com`;
if (domain.includes('sonarr')) {
url += `/series/${slug}`;
}
else {
url += `/movie/${id}`;
}
return `<a href="${url}" target="_blank">${name}</a>`;
},
}
],
};
removeMessage();
const dataTbl = new simpleDatatables.DataTable(tbl, dataTblConfig);
dataTbl.on('datatable.init', function() {
const search = document.querySelector('.dataTable-search input');
search.focus();
});
}
window.addEventListener('DOMContentLoaded', function() {
initial();
});

View File

@ -5,13 +5,29 @@ const icsUrls = [
url: '/sonarr-hs-ics',
name: 'hidden-sea',
},
// {
// url: '/sonarr-cb-ics',
// name: 'cold-badlands',
// },
// {
// url: '/sonarr-fv-ics',
// name: 'fancy-valley',
// },
{
url: '/sonarr-cb-ics',
name: 'cold-badlands',
url: '/sonarr-ag-ics',
name: 'ancient-grove',
},
{
url: '/sonarr-fv-ics',
name: 'fancy-valley',
url: '/sonarr-bs-ics',
name: 'bold-silence',
},
{
url: '/sonarr-fd-ics',
name: 'frosty-darkness',
},
{
url: '/sonarr-rs-ics',
name: 'royal-stream [4K]',
},
];
@ -206,18 +222,16 @@ async function loadSonarrCalendar(shouldForce = false)
calendar.render();
if (message) {
message.remove();
message.textContent = '';
}
keyHandlers(calendar);
if (shouldScroll) {
scrollToCalendarDay();
}
}
catch (err) {
console.error(err);
message.textContent = 'An error occurred loading schedule...';
if (message) {
message.textContent = 'An error occurred loading schedule...';
}
}
}

View File

@ -6,8 +6,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.2.0/superhero/bootstrap.min.css" integrity="sha512-pCTSMcZZ+tTaq3FXSWGhMmO/OZ+52FqEdhlExLz8PTBQKMyqxAdav13kofJWiyI5zeieBo8tZ++SMZ2ZgueRBA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" integrity="sha256-8M+b2Hj+vy/2J5tZ9pYDHeuPD59KsaEZn1XXj3xVhjg=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-datatables@3.2.2/dist/style.css" integrity="sha256-ZerMjX+PoTwR33srlBlYteG2MwTBUFimpp4wcT1w/lg=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-datatables@6.0.3/dist/style.css" integrity="sha256-402MmqzPksqYutg5VrSQeobxcgKkv9k001JWBTAQ0Dc=" crossorigin="anonymous">
<link rel="stylesheet" href="https://decicus-cdn.b-cdn.net/fontawesome/v5.15.4/css/all.min.css" integrity="sha384-rqn26AG5Pj86AF4SO72RK5fyefcQ/x32DNQfChxWvbXIyXFePlEktwD18fEz+kQU" crossorigin="anonymous">
<title>Cactflix [Main] &mdash; Plex &mdash; Existing Media</title>
<style type="text/css">
/**
@ -38,13 +38,21 @@
<div class="container-fluid">
<h1 class="mt-4">Cactflix [Main] &mdash; Plex &mdash; Existing Media</h1>
<p>Existing media tracked in the different Sonarr &amp; Radarr instances.</p>
<p><a href="./">Click here</a> for the homepage</p>
<p id="status-message">Loading... Please wait 🙂</p>
<div id="errors"></div>
<div class="update-status">
<p id="last-updated"></p>
<button type="button" class="btn btn-primary" id="refresh-button"><i class="fas fa-circle-notch"></i> Refresh table</button>
</div>
<table id="media-table" class="table table-striped table-hover mt-4">
</table>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@3.2.2/dist/umd/simple-datatables.js" integrity="sha256-Usm730G3l59Ux42era3GIRJOYXFLU7K9k7JFInXTeG0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@6.0.3/dist/umd/simple-datatables.js" integrity="sha256-lFa2JqMhBajGYOmnSpjWyNDEbaH70Wtpkw1Gm755yow=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js" integrity="sha512-qoCTmFwBtCPvFhA+WAqatSOrghwpDhFHxwAGh+cppWonXbHA09nG1z5zi4/NGnp8dUhXiVrzA6EnKgJA+fyrpw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="existing.js"></script>
</html>

306
public/existing.js Normal file
View File

@ -0,0 +1,306 @@
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)
{
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();
});

View File

View File

@ -9,7 +9,7 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css" integrity="sha256-5veQuRbWaECuYxwap/IOE/DAwNxgm4ikX7nrgsqYp88=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fullcalendar/daygrid@5.11.3/main.min.css" integrity="sha256-dPWx9VoFn91TsfLKiK60fNYizBuynczRmMVDO/Yzluo=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fullcalendar/list@5.11.3/main.min.css" integrity="sha256-b1BoveasAh93I+XvngCpnzp5pVCQlXxGPdijHWDjDXc=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" integrity="sha256-8M+b2Hj+vy/2J5tZ9pYDHeuPD59KsaEZn1XXj3xVhjg=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css" integrity="sha256-4RctOgogjPAdwGbwq+rxfwAmSpZhWaafcZR9btzUk18=" crossorigin="anonymous">
<style type="text/css">
:root {
--fc-list-event-hover-bg-color: #458680;
@ -36,6 +36,8 @@
<h2 class="mt-4">Upcoming TV show episodes &mdash; English, Norwegian &amp; Anime:</h2>
<p><a href="./existing.html">Click here</a> to search for existing series &amp; movies</p>
<table class="table table-striped table-hover mt-4">
<tr>
<th>Icon</th>