390 lines
13 KiB
HTML
390 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
|
|
<!-- TODO: when templating this, pre-populate the ignored shows value with something pulled from a config file -->
|
|
|
|
<head>
|
|
<style>
|
|
/* dark mode */
|
|
body {
|
|
background-color: #1e1e1e;
|
|
color: white;
|
|
}
|
|
|
|
/* enable borders on the table row */
|
|
table {
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
table td {
|
|
padding-right: 5px;
|
|
}
|
|
|
|
/* remove top & bottom margins */
|
|
.report-info *,
|
|
.episode * {
|
|
margin-bottom: 0;
|
|
margin-top: 0;
|
|
}
|
|
|
|
/* visually separate the report header from the contents */
|
|
.report-info .report {
|
|
background-color: #0c3c55;
|
|
border-radius: 7px;
|
|
display: inline-block;
|
|
}
|
|
|
|
.report h3 {
|
|
display: inline;
|
|
}
|
|
|
|
.report.stats {
|
|
margin-top: 4px;
|
|
}
|
|
|
|
details {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
summary {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* prevent the details from taking up the entire width of the screen */
|
|
.show>details {
|
|
max-width: 50%;
|
|
}
|
|
|
|
/* indent season headers some */
|
|
.season {
|
|
margin-left: 1em;
|
|
}
|
|
|
|
/* indent individual episode timestamps some more */
|
|
.episode {
|
|
margin-left: 1em;
|
|
}
|
|
|
|
/* if an intro was not found previously but is now, that's good */
|
|
.episode[data-warning="improvement"] {
|
|
background-color: #044b04;
|
|
}
|
|
|
|
/* if an intro was found previously but isn't now, that's bad */
|
|
.episode[data-warning="only_previous"],
|
|
.episode[data-warning="missing"] {
|
|
background-color: firebrick;
|
|
}
|
|
|
|
/* if an intro was found on both runs but the timestamps are pretty different, that's interesting */
|
|
.episode[data-warning="different"] {
|
|
background-color: #b77600;
|
|
}
|
|
|
|
#stats.warning {
|
|
border: 2px solid firebrick;
|
|
font-weight: bolder;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="report-info">
|
|
<h2 class="margin-bottom:1em">Intro Timestamp Differential</h2>
|
|
|
|
<div class="report old">
|
|
<h3 style="margin-top:0.5em">First report</h3>
|
|
|
|
{{ block "ReportInfo" .OldReport }}
|
|
<table>
|
|
<tbody>
|
|
<tr style="border-bottom: 1px solid black">
|
|
<td>Path</td>
|
|
<td><code>{{ .Path }}</code></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Jellyfin</td>
|
|
<td>{{ .ServerInfo.Version }} on {{ .ServerInfo.OperatingSystem }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Analysis Settings</td>
|
|
<td>{{ printAnalysisSettings .PluginConfig }}</td>
|
|
</tr>
|
|
<tr style="border-bottom: 1px solid black">
|
|
<td>Introduction Requirements</td>
|
|
<td>{{ printIntroductionReqs .PluginConfig }}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Start time</td>
|
|
<td>{{ printTime .StartedAt }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>End time</td>
|
|
<td>{{ printTime .FinishedAt }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Duration</td>
|
|
<td>{{ printDuration .Runtime }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
{{ end }}
|
|
</div>
|
|
|
|
<div class="report new">
|
|
<h3 style="padding-top:0.5em">Second report</h3>
|
|
|
|
{{ template "ReportInfo" .NewReport }}
|
|
</div>
|
|
<br />
|
|
|
|
<div class="report stats">
|
|
<h3 style="padding-top:0.5em">Statistics</h3>
|
|
|
|
<table>
|
|
<tbody>
|
|
<tr>
|
|
<td>Total episodes</td>
|
|
<td id="statTotal"></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Never found</td>
|
|
<td id="statMissing"></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Changed</td>
|
|
<td id="statChanged"></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gains</td>
|
|
<td id="statGain"></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Losses</td>
|
|
<td id="statLoss"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="report settings">
|
|
<h3 style="padding-top:0.5em">Settings</h3>
|
|
|
|
<form style="display:table">
|
|
<label for="minimumPercentage">Minimum percentage</label>
|
|
<input id="minimumPercentage" type="number" value="85" min="0" max="100"
|
|
style="margin-left: 5px; max-width: 100px" /> <br />
|
|
|
|
<label for="ignoreShows">Ignored shows</label>
|
|
<input id="ignoredShows" type="text" /> <br />
|
|
|
|
<button id="btnUpdate" type="button">Update</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{{/* store a reference to the data before the range query */}}
|
|
{{ $p := . }}
|
|
|
|
{{/* sort the show names and iterate over them */}}
|
|
{{ range $name := sortShows .OldReport.Shows }}
|
|
<div class="show" id="{{ $name }}">
|
|
<details>
|
|
{{/* get the unsorted seasons for this show */}}
|
|
{{ $seasons := index $p.OldReport.Shows $name }}
|
|
|
|
{{/* log the show name and number of seasons */}}
|
|
<summary>
|
|
<span class="showTitle">
|
|
<strong>{{ $name }}</strong>
|
|
<span id="stats"></span>
|
|
</span>
|
|
</summary>
|
|
|
|
<div class="seasons">
|
|
{{/* sort the seasons to ensure they display in numerical order */}}
|
|
{{ range $seasonNumber := (sortSeasons $seasons) }}
|
|
<div class="season" id="{{ $name }}-{{ $seasonNumber }}">
|
|
<details>
|
|
<summary>
|
|
<span>
|
|
<strong>Season {{ $seasonNumber }}</strong>
|
|
<span id="stats"></span>
|
|
</span>
|
|
</summary>
|
|
|
|
{{/* compare each episode in the old report to the same episode in the new report */}}
|
|
{{ range $episode := index $seasons $seasonNumber }}
|
|
|
|
{{/* lookup and compare both episodes */}}
|
|
{{ $comparison := compareEpisodes $episode.EpisodeId $p }}
|
|
{{ $old := $comparison.Old }}
|
|
{{ $new := $comparison.New }}
|
|
|
|
{{/* set attributes indicating if an intro was found in the old and new reports */}}
|
|
<div class="episode" data-warning="{{ $comparison.WarningShort }}">
|
|
<p>{{ $episode.Title }}</p>
|
|
|
|
<p>
|
|
Old: {{ $old.FormattedStart }} - {{ $old.FormattedEnd }}
|
|
(<span class="duration old">{{ $old.Duration }}</span>)
|
|
(valid: {{ $old.Valid }}) <br />
|
|
|
|
New: {{ $new.FormattedStart }} - {{ $new.FormattedEnd }}
|
|
(<span class="duration new">{{ $new.Duration }}</span>)
|
|
(valid: {{ $new.Valid }}) <br />
|
|
|
|
{{ if ne $comparison.WarningShort "okay" }}
|
|
Warning: {{ $comparison.Warning }}
|
|
{{ end }}
|
|
</p>
|
|
|
|
<br />
|
|
</div>
|
|
{{ end }}
|
|
</details>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
</details>
|
|
</div>
|
|
{{ end }}
|
|
|
|
<script>
|
|
function count(parent, warning) {
|
|
const sel = `div.episode[data-warning='${warning}']`
|
|
|
|
// Don't include hidden elements in the count
|
|
let count = 0;
|
|
for (const elem of parent.querySelectorAll(sel)) {
|
|
// offsetParent is defined when the element is not hidden
|
|
if (elem.offsetParent) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
function getPercent(part, whole) {
|
|
const percent = Math.round((part * 10_000) / whole) / 100;
|
|
return `${part} (${percent}%)`;
|
|
}
|
|
|
|
function setText(selector, text) {
|
|
document.querySelector(selector).textContent = text;
|
|
}
|
|
|
|
// Gets the minimum percentage of episodes in a group (a series or season)
|
|
// that must have a detected introduction.
|
|
function getMinimumPercentage() {
|
|
const value = document.querySelector("#minimumPercentage").value;
|
|
return Number(value);
|
|
}
|
|
|
|
// Gets the average duration for all episodes in a parent group.
|
|
// durationClass must be either "old" or "new".
|
|
function getAverageDuration(parent, durationClass) {
|
|
// Get all durations in the parent
|
|
const elems = parent.querySelectorAll(".duration." + durationClass);
|
|
|
|
// Calculate the average duration, ignoring any episode without an intro
|
|
let totalDuration = 0;
|
|
let totalEpisodes = 0;
|
|
for (const e of elems) {
|
|
const dur = Number(e.textContent);
|
|
if (dur === 0) {
|
|
continue;
|
|
}
|
|
|
|
totalDuration += dur;
|
|
totalEpisodes++;
|
|
}
|
|
|
|
if (totalEpisodes === 0) {
|
|
return 0;
|
|
}
|
|
|
|
return Math.round(totalDuration / totalEpisodes);
|
|
}
|
|
|
|
// Calculate statistics for all episodes in a parent element (a series or a season).
|
|
function setGroupStatistics(parent) {
|
|
// Count the total number of episodes.
|
|
const total = parent.querySelectorAll("div.episode").length;
|
|
|
|
// Count how many episodes have no warnings.
|
|
const okayCount = count(parent, "okay") + count(parent, "improvement");
|
|
const okayPercent = Math.round((okayCount * 100) / total);
|
|
const isOkay = okayPercent >= getMinimumPercentage();
|
|
|
|
// Calculate the previous and current average durations
|
|
const oldDuration = getAverageDuration(parent, "old");
|
|
const newDuration = getAverageDuration(parent, "new");
|
|
|
|
// Display the statistics
|
|
const stats = parent.querySelector("#stats");
|
|
stats.textContent = `${okayCount} / ${total} (${okayPercent}%) okay. r1 ${oldDuration} r2 ${newDuration}`;
|
|
|
|
if (!isOkay) {
|
|
stats.classList.add("warning");
|
|
} else {
|
|
stats.classList.remove("warning");
|
|
}
|
|
}
|
|
|
|
function updateGlobalStatistics() {
|
|
// Display all shows
|
|
for (const show of document.querySelectorAll("div.show")) {
|
|
show.style.display = "unset";
|
|
}
|
|
|
|
// Hide any shows that are ignored
|
|
for (let ignored of document.querySelector("#ignoredShows").value.split(",")) {
|
|
const elem = document.querySelector(`div.show[id='${ignored}']`);
|
|
if (!elem) {
|
|
console.warn("unable to find show", ignored);
|
|
continue;
|
|
}
|
|
|
|
elem.style.display = "none";
|
|
}
|
|
|
|
const total = document.querySelectorAll("div.episode").length;
|
|
const missing = count(document, "missing");
|
|
const different = count(document, "different")
|
|
const gain = count(document, "improvement");
|
|
const loss = count(document, "only_previous");
|
|
const okay = total - missing - different - loss;
|
|
|
|
setText("#statTotal", getPercent(okay, total));
|
|
setText("#statMissing", getPercent(missing, total));
|
|
setText("#statChanged", getPercent(different, total));
|
|
setText("#statGain", getPercent(gain, total));
|
|
setText("#statLoss", getPercent(loss, total));
|
|
}
|
|
|
|
function updateStatistics() {
|
|
for (const series of document.querySelectorAll("div.show")) {
|
|
setGroupStatistics(series);
|
|
|
|
for (const season of series.querySelectorAll("div.season")) {
|
|
setGroupStatistics(season);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Display statistics for all episodes and by groups
|
|
updateGlobalStatistics();
|
|
updateStatistics();
|
|
|
|
// Add event handlers
|
|
document.querySelector("#minimumPercentage").addEventListener("input", updateStatistics);
|
|
document.querySelector("#btnUpdate").addEventListener("click", updateGlobalStatistics);
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|