<!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>