WIP as of May 12th 2024
This commit is contained in:
parent
fef4b5bb83
commit
d11742ec8a
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
config.php
|
@ -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
1
config.sample.php
Normal file
@ -0,0 +1 @@
|
||||
<?php
|
128
existing.js
128
existing.js
@ -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();
|
||||
});
|
@ -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...';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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] — Plex — Existing Media</title>
|
||||
<style type="text/css">
|
||||
/**
|
||||
@ -38,13 +38,21 @@
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4">Cactflix [Main] — Plex — Existing Media</h1>
|
||||
<p>Existing media tracked in the different Sonarr & 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
306
public/existing.js
Normal 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();
|
||||
});
|
0
ical.min.js → public/ical.min.js
vendored
0
ical.min.js → public/ical.min.js
vendored
@ -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 — English, Norwegian & Anime:</h2>
|
||||
|
||||
<p><a href="./existing.html">Click here</a> to search for existing series & movies</p>
|
||||
|
||||
<table class="table table-striped table-hover mt-4">
|
||||
<tr>
|
||||
<th>Icon</th>
|
0
moment.min.js → public/moment.min.js
vendored
0
moment.min.js → public/moment.min.js
vendored
Loading…
Reference in New Issue
Block a user