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>