Switch from XML Files to SQLite DB (#365)
Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com> Co-authored-by: Kilian von Pflugk <github@jumoog.io>
This commit is contained in:
parent
29ca500ef1
commit
a1d634b66e
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -26,7 +27,7 @@ public class BlackFrameAnalyzer(ILogger<BlackFrameAnalyzer> logger) : IMediaFile
|
|||||||
private readonly int _blackFrameMinimumPercentage = _config.BlackFrameMinimumPercentage;
|
private readonly int _blackFrameMinimumPercentage = _config.BlackFrameMinimumPercentage;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles(
|
public async Task<IReadOnlyList<QueuedEpisode>> AnalyzeMediaFiles(
|
||||||
IReadOnlyList<QueuedEpisode> analysisQueue,
|
IReadOnlyList<QueuedEpisode> analysisQueue,
|
||||||
AnalysisMode mode,
|
AnalysisMode mode,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@ -46,7 +47,7 @@ public class BlackFrameAnalyzer(ILogger<BlackFrameAnalyzer> logger) : IMediaFile
|
|||||||
|
|
||||||
var searchDistance = 2 * _minimumCreditsDuration;
|
var searchDistance = 2 * _minimumCreditsDuration;
|
||||||
|
|
||||||
foreach (var episode in episodeAnalysisQueue.Where(e => !e.State.IsAnalyzed(mode)))
|
foreach (var episode in episodeAnalysisQueue.Where(e => !e.GetAnalyzed(mode)))
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@ -116,13 +117,13 @@ public class BlackFrameAnalyzer(ILogger<BlackFrameAnalyzer> logger) : IMediaFile
|
|||||||
searchStart = episode.Duration - credit.Start + (0.5 * searchDistance);
|
searchStart = episode.Duration - credit.Start + (0.5 * searchDistance);
|
||||||
|
|
||||||
creditTimes.Add(episode.EpisodeId, credit);
|
creditTimes.Add(episode.EpisodeId, credit);
|
||||||
episode.State.SetAnalyzed(mode, true);
|
episode.SetAnalyzed(mode, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
var analyzerHelper = new AnalyzerHelper(_logger);
|
var analyzerHelper = new AnalyzerHelper(_logger);
|
||||||
creditTimes = analyzerHelper.AdjustIntroTimes(analysisQueue, creditTimes, mode);
|
creditTimes = analyzerHelper.AdjustIntroTimes(analysisQueue, creditTimes, mode);
|
||||||
|
|
||||||
Plugin.Instance!.UpdateTimestamps(creditTimes, mode);
|
await Plugin.Instance!.UpdateTimestamps(creditTimes, mode).ConfigureAwait(false);
|
||||||
|
|
||||||
return episodeAnalysisQueue;
|
return episodeAnalysisQueue;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@ -26,7 +27,7 @@ public class ChapterAnalyzer(ILogger<ChapterAnalyzer> logger) : IMediaFileAnalyz
|
|||||||
private ILogger<ChapterAnalyzer> _logger = logger;
|
private ILogger<ChapterAnalyzer> _logger = logger;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles(
|
public async Task<IReadOnlyList<QueuedEpisode>> AnalyzeMediaFiles(
|
||||||
IReadOnlyList<QueuedEpisode> analysisQueue,
|
IReadOnlyList<QueuedEpisode> analysisQueue,
|
||||||
AnalysisMode mode,
|
AnalysisMode mode,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@ -45,7 +46,7 @@ public class ChapterAnalyzer(ILogger<ChapterAnalyzer> logger) : IMediaFileAnalyz
|
|||||||
return analysisQueue;
|
return analysisQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var episode in episodeAnalysisQueue.Where(e => !e.State.IsAnalyzed(mode)))
|
foreach (var episode in episodeAnalysisQueue.Where(e => !e.GetAnalyzed(mode)))
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@ -64,10 +65,10 @@ public class ChapterAnalyzer(ILogger<ChapterAnalyzer> logger) : IMediaFileAnalyz
|
|||||||
}
|
}
|
||||||
|
|
||||||
skippableRanges.Add(episode.EpisodeId, skipRange);
|
skippableRanges.Add(episode.EpisodeId, skipRange);
|
||||||
episode.State.SetAnalyzed(mode, true);
|
episode.SetAnalyzed(mode, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Instance.UpdateTimestamps(skippableRanges, mode);
|
await Plugin.Instance.UpdateTimestamps(skippableRanges, mode).ConfigureAwait(false);
|
||||||
|
|
||||||
return episodeAnalysisQueue;
|
return episodeAnalysisQueue;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -52,7 +53,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles(
|
public async Task<IReadOnlyList<QueuedEpisode>> AnalyzeMediaFiles(
|
||||||
IReadOnlyList<QueuedEpisode> analysisQueue,
|
IReadOnlyList<QueuedEpisode> analysisQueue,
|
||||||
AnalysisMode mode,
|
AnalysisMode mode,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@ -67,7 +68,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
|||||||
var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue);
|
var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue);
|
||||||
|
|
||||||
// Episodes that were analyzed and do not have an introduction.
|
// Episodes that were analyzed and do not have an introduction.
|
||||||
var episodesWithoutIntros = episodeAnalysisQueue.Where(e => !e.State.IsAnalyzed(mode)).ToList();
|
var episodesWithoutIntros = episodeAnalysisQueue.Where(e => !e.GetAnalyzed(mode)).ToList();
|
||||||
|
|
||||||
_analysisMode = mode;
|
_analysisMode = mode;
|
||||||
|
|
||||||
@ -79,7 +80,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
|||||||
var episodesWithFingerprint = new List<QueuedEpisode>(episodesWithoutIntros);
|
var episodesWithFingerprint = new List<QueuedEpisode>(episodesWithoutIntros);
|
||||||
|
|
||||||
// Load fingerprints from cache if available.
|
// Load fingerprints from cache if available.
|
||||||
episodesWithFingerprint.AddRange(episodeAnalysisQueue.Where(e => e.State.IsAnalyzed(mode) && File.Exists(FFmpegWrapper.GetFingerprintCachePath(e, mode))));
|
episodesWithFingerprint.AddRange(episodeAnalysisQueue.Where(e => e.GetAnalyzed(mode) && File.Exists(FFmpegWrapper.GetFingerprintCachePath(e, mode))));
|
||||||
|
|
||||||
// Ensure at least two fingerprints are present.
|
// Ensure at least two fingerprints are present.
|
||||||
if (episodesWithFingerprint.Count == 1)
|
if (episodesWithFingerprint.Count == 1)
|
||||||
@ -89,7 +90,9 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
|||||||
.Where((episode, index) => Math.Abs(index - indexInAnalysisQueue) <= 1 && index != indexInAnalysisQueue));
|
.Where((episode, index) => Math.Abs(index - indexInAnalysisQueue) <= 1 && index != indexInAnalysisQueue));
|
||||||
}
|
}
|
||||||
|
|
||||||
seasonIntros = episodesWithFingerprint.Where(e => e.State.IsAnalyzed(mode)).ToDictionary(e => e.EpisodeId, e => Plugin.GetIntroByMode(e.EpisodeId, mode));
|
seasonIntros = episodesWithFingerprint
|
||||||
|
.Where(e => e.GetAnalyzed(mode))
|
||||||
|
.ToDictionary(e => e.EpisodeId, e => Plugin.Instance!.GetSegmentByMode(e.EpisodeId, mode));
|
||||||
|
|
||||||
// Compute fingerprints for all episodes in the season
|
// Compute fingerprints for all episodes in the season
|
||||||
foreach (var episode in episodesWithFingerprint)
|
foreach (var episode in episodesWithFingerprint)
|
||||||
@ -193,7 +196,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
|||||||
if (seasonIntros.ContainsKey(currentEpisode.EpisodeId))
|
if (seasonIntros.ContainsKey(currentEpisode.EpisodeId))
|
||||||
{
|
{
|
||||||
episodesWithFingerprint.Add(currentEpisode);
|
episodesWithFingerprint.Add(currentEpisode);
|
||||||
episodeAnalysisQueue.FirstOrDefault(x => x.EpisodeId == currentEpisode.EpisodeId)?.State.SetAnalyzed(mode, true);
|
episodeAnalysisQueue.FirstOrDefault(x => x.EpisodeId == currentEpisode.EpisodeId)?.SetAnalyzed(mode, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +210,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
|||||||
var analyzerHelper = new AnalyzerHelper(_logger);
|
var analyzerHelper = new AnalyzerHelper(_logger);
|
||||||
seasonIntros = analyzerHelper.AdjustIntroTimes(analysisQueue, seasonIntros, _analysisMode);
|
seasonIntros = analyzerHelper.AdjustIntroTimes(analysisQueue, seasonIntros, _analysisMode);
|
||||||
|
|
||||||
Plugin.Instance!.UpdateTimestamps(seasonIntros, _analysisMode);
|
await Plugin.Instance!.UpdateTimestamps(seasonIntros, _analysisMode).ConfigureAwait(false);
|
||||||
|
|
||||||
return episodeAnalysisQueue;
|
return episodeAnalysisQueue;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
|
|
||||||
namespace IntroSkipper.Analyzers;
|
namespace IntroSkipper.Analyzers;
|
||||||
@ -19,7 +20,7 @@ public interface IMediaFileAnalyzer
|
|||||||
/// <param name="mode">Analysis mode.</param>
|
/// <param name="mode">Analysis mode.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token from scheduled task.</param>
|
/// <param name="cancellationToken">Cancellation token from scheduled task.</param>
|
||||||
/// <returns>Collection of media files that were **unsuccessfully analyzed**.</returns>
|
/// <returns>Collection of media files that were **unsuccessfully analyzed**.</returns>
|
||||||
public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles(
|
Task<IReadOnlyList<QueuedEpisode>> AnalyzeMediaFiles(
|
||||||
IReadOnlyList<QueuedEpisode> analysisQueue,
|
IReadOnlyList<QueuedEpisode> analysisQueue,
|
||||||
AnalysisMode mode,
|
AnalysisMode mode,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
@ -103,9 +103,9 @@
|
|||||||
<summary>Modify Segment Parameters</summary>
|
<summary>Modify Segment Parameters</summary>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<b style="color:orange">Changing segment parameters requires regenerating media segments before changes take effect.</b>
|
<b style="color: orange">Changing segment parameters requires regenerating media segments before changes take effect.</b>
|
||||||
<br />
|
<br />
|
||||||
Per the jellyfin MediaSegments API, records must be updated individually and may be slow to regenerate.
|
Per the jellyfin MediaSegments API, records must be updated individually and may be slow to regenerate.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
@ -242,9 +242,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<p align="center" style="font-size: 0.75em">
|
<p align="center" style="font-size: 0.75em">EDL file generation has been removed. Please use endrl's <a href="https://github.com/endrl/jellyfin-plugin-edl">EDL plugin</a>.</p>
|
||||||
EDL file generation has been removed. Please use endrl's <a href="https://github.com/endrl/jellyfin-plugin-edl">EDL plugin</a>.
|
|
||||||
</p>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="verticalSection-extrabottompadding">
|
<fieldset class="verticalSection-extrabottompadding">
|
||||||
@ -324,9 +322,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="warningMessage" style="color: #721c24; background-color: #f7cf1f; border: 1px solid #f5c6cb; border-radius: 4px; padding: 10px; margin-bottom: 10px">
|
<div id="warningMessage" style="color: #721c24; background-color: #f7cf1f; border: 1px solid #f5c6cb; border-radius: 4px; padding: 10px; margin-bottom: 10px">Failed to add skip button to web interface. See <a href="https://github.com/intro-skipper/intro-skipper/wiki/Troubleshooting#skip-button-is-not-visible" target="_blank" rel="noopener noreferrer">troubleshooting guide</a> for the most common issues.</div>
|
||||||
Failed to add skip button to web interface. See <a href="https://github.com/intro-skipper/intro-skipper/wiki/Troubleshooting#skip-button-is-not-visible" target="_blank" rel="noopener noreferrer">troubleshooting guide</a> for the most common issues.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>User Interface Customization</summary>
|
<summary>User Interface Customization</summary>
|
||||||
@ -409,28 +405,39 @@
|
|||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div id="ignorelistSection" style="display: none">
|
<div id="analyzerActionsSection" style="display: none">
|
||||||
<h3 style="margin: 0">Ignore list editor</h3>
|
<h3 style="margin: 0">Analyzer actions</h3>
|
||||||
<p style="margin: 0">
|
<p style="margin: 0">
|
||||||
Add or remove items from the ignore list. Items on the ignore list will not be analyzed.<br />
|
Choose how segments should be analyzed for this season.<br />
|
||||||
You can apply the changes for the entire series or just the selected season.
|
Default uses all available detection methods (Chromaprint, Chapter, and BlackFrame for credits).<br />
|
||||||
|
Select specific methods to limit analysis, or None to skip detection entirely.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div id="ignoreListCheckboxContainer">
|
<div id="analyzerActionsContainer">
|
||||||
<label for="ignorelistIntro" style="margin-right: 1.5em; display: inline-block">
|
<label for="actionIntro" style="margin-right: 1.5em; display: inline-block">
|
||||||
<span>Ignore intros</span>
|
<span>Introduction analysis</span>
|
||||||
<input type="checkbox" id="ignorelistIntro" />
|
<select is="emby-select" id="actionIntro" class="emby-select-withcolor emby-select">
|
||||||
|
<option value="Default">Default</option>
|
||||||
|
<option value="Chapter">Chapter</option>
|
||||||
|
<option value="Chromaprint">Chromaprint</option>
|
||||||
|
<option value="None">None</option>
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label for="ignorelistCredits" style="margin-right: 1.5em; display: inline-block">
|
<label for="actionCredits" style="margin-right: 1.5em; display: inline-block">
|
||||||
<span>Ignore credits</span>
|
<span>Credits analysis</span>
|
||||||
<input type="checkbox" id="ignorelistCredits" />
|
<select is="emby-select" id="actionCredits" class="emby-select-withcolor emby-select">
|
||||||
|
<option value="Default">Default</option>
|
||||||
|
<option value="Chapter">Chapter</option>
|
||||||
|
<option value="Chromaprint">Chromaprint</option>
|
||||||
|
<option value="BlackFrame">BlackFrame</option>
|
||||||
|
<option value="None">None</option>
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<button is="emby-button" id="saveIgnoreListSeries" class="raised button-submit block emby-button">Apply to series / movie</button>
|
<button is="emby-button" id="saveAnalyzerActions" class="raised button-submit block emby-button" style="display: none">Apply changes.</button>
|
||||||
<button is="emby-button" id="saveIgnoreListSeason" class="raised button-submit block emby-button" style="display: none">Apply to season</button>
|
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
@ -513,7 +520,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="fingerprintVisualizer" style="display: none">
|
<div id="fingerprintVisualizer" style="display: none">
|
||||||
|
|
||||||
<h3>Fingerprint Visualizer</h3>
|
<h3>Fingerprint Visualizer</h3>
|
||||||
<p>
|
<p>
|
||||||
Interactively compare the audio fingerprints of two episodes. <br />
|
Interactively compare the audio fingerprints of two episodes. <br />
|
||||||
@ -624,8 +630,6 @@
|
|||||||
// seasons grouped by show
|
// seasons grouped by show
|
||||||
var shows = {};
|
var shows = {};
|
||||||
|
|
||||||
var IgnoreListSeasonId;
|
|
||||||
|
|
||||||
// settings elements
|
// settings elements
|
||||||
var visualizer = document.querySelector("details#visualizer");
|
var visualizer = document.querySelector("details#visualizer");
|
||||||
var support = document.querySelector("details#support");
|
var support = document.querySelector("details#support");
|
||||||
@ -667,11 +671,10 @@
|
|||||||
var booleanConfigurationFields = ["AutoDetectIntros", "AutoDetectCredits", "AnalyzeMovies", "AnalyzeSeasonZero", "SelectAllLibraries", "UpdateMediaSegments", "RegenerateMediaSegments", "CacheFingerprints", "AutoSkip", "AutoSkipCredits", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonEnabled"];
|
var booleanConfigurationFields = ["AutoDetectIntros", "AutoDetectCredits", "AnalyzeMovies", "AnalyzeSeasonZero", "SelectAllLibraries", "UpdateMediaSegments", "RegenerateMediaSegments", "CacheFingerprints", "AutoSkip", "AutoSkipCredits", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonEnabled"];
|
||||||
|
|
||||||
// visualizer elements
|
// visualizer elements
|
||||||
var ignorelistSection = document.querySelector("div#ignorelistSection");
|
var analyzerActionsSection = document.querySelector("div#analyzerActionsSection");
|
||||||
var ignorelistIntro = ignorelistSection.querySelector("input#ignorelistIntro");
|
var actionIntro = analyzerActionsSection.querySelector("select#actionIntro");
|
||||||
var ignorelistCredits = ignorelistSection.querySelector("input#ignorelistCredits");
|
var actionCredits = analyzerActionsSection.querySelector("select#actionCredits");
|
||||||
var saveIgnoreListSeasonButton = ignorelistSection.querySelector("button#saveIgnoreListSeason");
|
var saveAnalyzerActionsButton = analyzerActionsSection.querySelector("button#saveAnalyzerActions");
|
||||||
var saveIgnoreListSeriesButton = ignorelistSection.querySelector("button#saveIgnoreListSeries");
|
|
||||||
var canvas = document.querySelector("canvas#troubleshooter");
|
var canvas = document.querySelector("canvas#troubleshooter");
|
||||||
var selectShow = document.querySelector("select#troubleshooterShow");
|
var selectShow = document.querySelector("select#troubleshooterShow");
|
||||||
var seasonSelection = document.getElementById("seasonSelection");
|
var seasonSelection = document.getElementById("seasonSelection");
|
||||||
@ -852,8 +855,8 @@
|
|||||||
// when the fingerprint visualizer opens, populate show names
|
// when the fingerprint visualizer opens, populate show names
|
||||||
async function visualizerToggled() {
|
async function visualizerToggled() {
|
||||||
if (!visualizer.open) {
|
if (!visualizer.open) {
|
||||||
ignorelistSection.style.display = "none";
|
analyzerActionsSection.style.display = "none";
|
||||||
saveIgnoreListSeasonButton.style.display = "none";
|
saveAnalyzerActionsButton.style.display = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -948,22 +951,11 @@
|
|||||||
clearSelect(selectEpisode1);
|
clearSelect(selectEpisode1);
|
||||||
clearSelect(selectEpisode2);
|
clearSelect(selectEpisode2);
|
||||||
|
|
||||||
// show the ignore list editor.
|
|
||||||
Dashboard.showLoadingMsg();
|
|
||||||
const IgnoreList = await getJson("Intros/IgnoreListSeries/" + encodeURI(selectShow.value));
|
|
||||||
ignorelistIntro.checked = IgnoreList.IgnoreIntro;
|
|
||||||
ignorelistCredits.checked = IgnoreList.IgnoreCredits;
|
|
||||||
ignorelistSection.style.display = "unset";
|
|
||||||
saveIgnoreListSeasonButton.style.display = "none";
|
|
||||||
Dashboard.hideLoadingMsg();
|
|
||||||
|
|
||||||
if (shows[selectShow.value].IsMovie) {
|
if (shows[selectShow.value].IsMovie) {
|
||||||
movieLoaded();
|
movieLoaded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveIgnoreListSeriesButton.textContent = "Apply to series";
|
|
||||||
|
|
||||||
// add all seasons from this show to the season select
|
// add all seasons from this show to the season select
|
||||||
for (const season in shows[selectShow.value].Seasons) {
|
for (const season in shows[selectShow.value].Seasons) {
|
||||||
addItem(selectSeason, "Season " + shows[selectShow.value].Seasons[season], season);
|
addItem(selectSeason, "Season " + shows[selectShow.value].Seasons[season], season);
|
||||||
@ -976,12 +968,12 @@
|
|||||||
async function seasonChanged() {
|
async function seasonChanged() {
|
||||||
const seasonData = encodeURI(selectShow.value) + "/" + encodeURI(selectSeason.value);
|
const seasonData = encodeURI(selectShow.value) + "/" + encodeURI(selectSeason.value);
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
// show the ignore list editor.
|
// show the analyzer actions editor.
|
||||||
saveIgnoreListSeasonButton.style.display = "block";
|
saveAnalyzerActionsButton.style.display = "block";
|
||||||
const IgnoreList = await getJson("Intros/IgnoreListSeason/" + encodeURI(selectSeason.value));
|
const analyzerActions = (await getJson("Intros/AnalyzerActions/" + encodeURI(selectSeason.value))) || { Introduction: "Default", Credits: "Default" };
|
||||||
ignorelistIntro.checked = IgnoreList.IgnoreIntro;
|
actionIntro.value = analyzerActions.Introduction || "Default";
|
||||||
ignorelistCredits.checked = IgnoreList.IgnoreCredits;
|
actionCredits.value = analyzerActions.Credits || "Default";
|
||||||
IgnoreListSeasonId = IgnoreList.SeasonId;
|
analyzerActionsSection.style.display = "unset";
|
||||||
|
|
||||||
// show the erase season button
|
// show the erase season button
|
||||||
eraseSeasonContainer.style.display = "unset";
|
eraseSeasonContainer.style.display = "unset";
|
||||||
@ -1049,10 +1041,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function movieLoaded() {
|
async function movieLoaded() {
|
||||||
|
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
|
|
||||||
saveIgnoreListSeriesButton.textContent = "Apply to movie";
|
saveAnalyzerActionsButton.textContent = "Apply to movie";
|
||||||
seasonSelection.style.display = "none";
|
seasonSelection.style.display = "none";
|
||||||
episodeSelection.style.display = "none";
|
episodeSelection.style.display = "none";
|
||||||
eraseMovieContainer.style.display = "unset";
|
eraseMovieContainer.style.display = "unset";
|
||||||
@ -1088,7 +1079,7 @@
|
|||||||
document.querySelector("#editLeftIntroEpisodeStartEdit").value = leftEpisodeJson.Introduction.Start;
|
document.querySelector("#editLeftIntroEpisodeStartEdit").value = leftEpisodeJson.Introduction.Start;
|
||||||
document.querySelector("#editLeftIntroEpisodeEndEdit").value = leftEpisodeJson.Introduction.End;
|
document.querySelector("#editLeftIntroEpisodeEndEdit").value = leftEpisodeJson.Introduction.End;
|
||||||
document.querySelector("#editLeftCreditEpisodeStartEdit").value = leftEpisodeJson.Credits.Start;
|
document.querySelector("#editLeftCreditEpisodeStartEdit").value = leftEpisodeJson.Credits.Start;
|
||||||
document.querySelector("#editLeftCreditEpisodeEndEdit").value = leftEpisodeJson.Credits.End
|
document.querySelector("#editLeftCreditEpisodeEndEdit").value = leftEpisodeJson.Credits.End;
|
||||||
|
|
||||||
// Update display inputs
|
// Update display inputs
|
||||||
const inputs = document.querySelectorAll('#timestampEditor input[type="number"]');
|
const inputs = document.querySelectorAll('#timestampEditor input[type="number"]');
|
||||||
@ -1412,33 +1403,21 @@
|
|||||||
document.getElementById("eraseMovieCacheCheckbox").checked = false;
|
document.getElementById("eraseMovieCacheCheckbox").checked = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
saveIgnoreListSeasonButton.addEventListener("click", () => {
|
saveAnalyzerActionsButton.addEventListener("click", () => {
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
|
|
||||||
var url = "Intros/IgnoreList/UpdateSeason";
|
var url = "Intros/AnalyzerActions/UpdateSeason";
|
||||||
const newRhs = {
|
const actions = {
|
||||||
IgnoreIntro: ignorelistIntro.checked,
|
id: selectSeason.value,
|
||||||
IgnoreCredits: ignorelistCredits.checked,
|
analyzerActions: {
|
||||||
SeasonId: IgnoreListSeasonId,
|
Introduction: actionIntro.value,
|
||||||
|
Credits: actionCredits.value,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchWithAuth(url, "POST", JSON.stringify(newRhs));
|
fetchWithAuth(url, "POST", JSON.stringify(actions));
|
||||||
|
|
||||||
Dashboard.alert("Ignore list updated for " + selectSeason.value + " of " + selectShow.value);
|
Dashboard.alert("Analyzer actions updated for " + selectSeason.value + " of " + selectShow.value);
|
||||||
Dashboard.hideLoadingMsg();
|
|
||||||
});
|
|
||||||
saveIgnoreListSeriesButton.addEventListener("click", () => {
|
|
||||||
Dashboard.showLoadingMsg();
|
|
||||||
|
|
||||||
var url = "Intros/IgnoreList/UpdateSeries" + "/" + encodeURIComponent(selectShow.value);
|
|
||||||
const newRhs = {
|
|
||||||
IgnoreIntro: ignorelistIntro.checked,
|
|
||||||
IgnoreCredits: ignorelistCredits.checked,
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchWithAuth(url, "POST", JSON.stringify(newRhs));
|
|
||||||
|
|
||||||
Dashboard.alert("Ignore list updated for " + selectShow.value);
|
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
});
|
});
|
||||||
btnUpdateTimestamps.addEventListener("click", () => {
|
btnUpdateTimestamps.addEventListener("click", () => {
|
||||||
|
@ -9,12 +9,14 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
|
using IntroSkipper.Db;
|
||||||
using IntroSkipper.Manager;
|
using IntroSkipper.Manager;
|
||||||
using MediaBrowser.Common.Api;
|
using MediaBrowser.Common.Api;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace IntroSkipper.Controllers;
|
namespace IntroSkipper.Controllers;
|
||||||
|
|
||||||
@ -42,9 +44,8 @@ public class SkipIntroController(MediaSegmentUpdateManager mediaSegmentUpdateMan
|
|||||||
[FromRoute] Guid id,
|
[FromRoute] Guid id,
|
||||||
[FromQuery] AnalysisMode mode = AnalysisMode.Introduction)
|
[FromQuery] AnalysisMode mode = AnalysisMode.Introduction)
|
||||||
{
|
{
|
||||||
var intro = GetIntro(id, mode);
|
var intros = GetIntros(id);
|
||||||
|
if (!intros.TryGetValue(mode, out var intro))
|
||||||
if (intro is null || !intro.Valid)
|
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
@ -67,7 +68,7 @@ public class SkipIntroController(MediaSegmentUpdateManager mediaSegmentUpdateMan
|
|||||||
{
|
{
|
||||||
// only update existing episodes
|
// only update existing episodes
|
||||||
var rawItem = Plugin.Instance!.GetItem(id);
|
var rawItem = Plugin.Instance!.GetItem(id);
|
||||||
if (rawItem == null || rawItem is not Episode and not Movie)
|
if (rawItem is not Episode and not Movie)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
@ -75,18 +76,15 @@ public class SkipIntroController(MediaSegmentUpdateManager mediaSegmentUpdateMan
|
|||||||
if (timestamps?.Introduction.End > 0.0)
|
if (timestamps?.Introduction.End > 0.0)
|
||||||
{
|
{
|
||||||
var tr = new TimeRange(timestamps.Introduction.Start, timestamps.Introduction.End);
|
var tr = new TimeRange(timestamps.Introduction.Start, timestamps.Introduction.End);
|
||||||
Plugin.Instance!.Intros[id] = new Segment(id, tr);
|
await Plugin.Instance!.UpdateTimestamps(new Dictionary<Guid, Segment> { [id] = new Segment(id, tr) }, AnalysisMode.Introduction).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timestamps?.Credits.End > 0.0)
|
if (timestamps?.Credits.End > 0.0)
|
||||||
{
|
{
|
||||||
var cr = new TimeRange(timestamps.Credits.Start, timestamps.Credits.End);
|
var tr = new TimeRange(timestamps.Credits.Start, timestamps.Credits.End);
|
||||||
Plugin.Instance!.Credits[id] = new Segment(id, cr);
|
await Plugin.Instance!.UpdateTimestamps(new Dictionary<Guid, Segment> { [id] = new Segment(id, tr) }, AnalysisMode.Credits).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Instance!.SaveTimestamps(AnalysisMode.Introduction);
|
|
||||||
Plugin.Instance!.SaveTimestamps(AnalysisMode.Credits);
|
|
||||||
|
|
||||||
if (Plugin.Instance.Configuration.UpdateMediaSegments)
|
if (Plugin.Instance.Configuration.UpdateMediaSegments)
|
||||||
{
|
{
|
||||||
var episode = Plugin.Instance!.QueuedMediaItems[rawItem is Episode e ? e.SeasonId : rawItem.Id]
|
var episode = Plugin.Instance!.QueuedMediaItems[rawItem is Episode e ? e.SeasonId : rawItem.Id]
|
||||||
@ -114,20 +112,22 @@ public class SkipIntroController(MediaSegmentUpdateManager mediaSegmentUpdateMan
|
|||||||
{
|
{
|
||||||
// only get return content for episodes
|
// only get return content for episodes
|
||||||
var rawItem = Plugin.Instance!.GetItem(id);
|
var rawItem = Plugin.Instance!.GetItem(id);
|
||||||
if (rawItem == null || rawItem is not Episode and not Movie)
|
if (rawItem is not Episode and not Movie)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var times = new TimeStamps();
|
var times = new TimeStamps();
|
||||||
if (Plugin.Instance!.Intros.TryGetValue(id, out var introValue))
|
var segments = Plugin.Instance!.GetSegmentsById(id);
|
||||||
|
|
||||||
|
if (segments.TryGetValue(AnalysisMode.Introduction, out var introSegment))
|
||||||
{
|
{
|
||||||
times.Introduction = introValue;
|
times.Introduction = introSegment;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Plugin.Instance!.Credits.TryGetValue(id, out var creditValue))
|
if (segments.TryGetValue(AnalysisMode.Credits, out var creditSegment))
|
||||||
{
|
{
|
||||||
times.Credits = creditValue;
|
times.Credits = creditSegment;
|
||||||
}
|
}
|
||||||
|
|
||||||
return times;
|
return times;
|
||||||
@ -142,16 +142,16 @@ public class SkipIntroController(MediaSegmentUpdateManager mediaSegmentUpdateMan
|
|||||||
[HttpGet("Episode/{id}/IntroSkipperSegments")]
|
[HttpGet("Episode/{id}/IntroSkipperSegments")]
|
||||||
public ActionResult<Dictionary<AnalysisMode, Intro>> GetSkippableSegments([FromRoute] Guid id)
|
public ActionResult<Dictionary<AnalysisMode, Intro>> GetSkippableSegments([FromRoute] Guid id)
|
||||||
{
|
{
|
||||||
var segments = new Dictionary<AnalysisMode, Intro>();
|
var segments = GetIntros(id);
|
||||||
|
|
||||||
if (GetIntro(id, AnalysisMode.Introduction) is Intro intro)
|
if (segments.TryGetValue(AnalysisMode.Introduction, out var introSegment))
|
||||||
{
|
{
|
||||||
segments[AnalysisMode.Introduction] = intro;
|
segments[AnalysisMode.Introduction] = introSegment;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GetIntro(id, AnalysisMode.Credits) is Intro credits)
|
if (segments.TryGetValue(AnalysisMode.Introduction, out var creditSegment))
|
||||||
{
|
{
|
||||||
segments[AnalysisMode.Credits] = credits;
|
segments[AnalysisMode.Credits] = creditSegment;
|
||||||
}
|
}
|
||||||
|
|
||||||
return segments;
|
return segments;
|
||||||
@ -159,41 +159,47 @@ public class SkipIntroController(MediaSegmentUpdateManager mediaSegmentUpdateMan
|
|||||||
|
|
||||||
/// <summary>Lookup and return the skippable timestamps for the provided item.</summary>
|
/// <summary>Lookup and return the skippable timestamps for the provided item.</summary>
|
||||||
/// <param name="id">Unique identifier of this episode.</param>
|
/// <param name="id">Unique identifier of this episode.</param>
|
||||||
/// <param name="mode">Mode.</param>
|
|
||||||
/// <returns>Intro object if the provided item has an intro, null otherwise.</returns>
|
/// <returns>Intro object if the provided item has an intro, null otherwise.</returns>
|
||||||
private static Intro? GetIntro(Guid id, AnalysisMode mode)
|
private static Dictionary<AnalysisMode, Intro> GetIntros(Guid id)
|
||||||
{
|
{
|
||||||
try
|
var timestamps = Plugin.Instance!.GetSegmentsById(id);
|
||||||
|
var intros = new Dictionary<AnalysisMode, Intro>();
|
||||||
|
|
||||||
|
foreach (var (mode, timestamp) in timestamps)
|
||||||
{
|
{
|
||||||
var timestamp = Plugin.GetIntroByMode(id, mode);
|
if (!timestamp.Valid)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Operate on a copy to avoid mutating the original Intro object stored in the dictionary.
|
// Create new Intro to avoid mutating the original stored in dictionary
|
||||||
var segment = new Intro(timestamp);
|
var segment = new Intro(timestamp);
|
||||||
|
var config = Plugin.Instance.Configuration;
|
||||||
|
|
||||||
var config = Plugin.Instance!.Configuration;
|
// Calculate intro end time based on mode
|
||||||
segment.IntroEnd = mode == AnalysisMode.Credits
|
segment.IntroEnd = mode == AnalysisMode.Credits
|
||||||
? GetAdjustedIntroEnd(id, segment.IntroEnd, config)
|
? GetAdjustedIntroEnd(id, segment.IntroEnd, config)
|
||||||
: segment.IntroEnd - config.RemainingSecondsOfIntro;
|
: segment.IntroEnd - config.RemainingSecondsOfIntro;
|
||||||
|
|
||||||
|
// Set skip button prompt visibility times
|
||||||
|
const double MIN_REMAINING_TIME = 3.0; // Minimum seconds before end to hide prompt
|
||||||
if (config.PersistSkipButton)
|
if (config.PersistSkipButton)
|
||||||
{
|
{
|
||||||
segment.ShowSkipPromptAt = segment.IntroStart;
|
segment.ShowSkipPromptAt = segment.IntroStart;
|
||||||
segment.HideSkipPromptAt = segment.IntroEnd - 3;
|
segment.HideSkipPromptAt = segment.IntroEnd - MIN_REMAINING_TIME;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
segment.ShowSkipPromptAt = Math.Max(0, segment.IntroStart - config.ShowPromptAdjustment);
|
segment.ShowSkipPromptAt = Math.Max(0, segment.IntroStart - config.ShowPromptAdjustment);
|
||||||
segment.HideSkipPromptAt = Math.Min(
|
segment.HideSkipPromptAt = Math.Min(
|
||||||
segment.IntroStart + config.HidePromptAdjustment,
|
segment.IntroStart + config.HidePromptAdjustment,
|
||||||
segment.IntroEnd - 3);
|
segment.IntroEnd - MIN_REMAINING_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
return segment;
|
intros[mode] = segment;
|
||||||
}
|
|
||||||
catch (KeyNotFoundException)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return intros;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double GetAdjustedIntroEnd(Guid id, double segmentEnd, PluginConfiguration config)
|
private static double GetAdjustedIntroEnd(Guid id, double segmentEnd, PluginConfiguration config)
|
||||||
@ -213,24 +219,22 @@ public class SkipIntroController(MediaSegmentUpdateManager mediaSegmentUpdateMan
|
|||||||
/// <returns>No content.</returns>
|
/// <returns>No content.</returns>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[HttpPost("Intros/EraseTimestamps")]
|
[HttpPost("Intros/EraseTimestamps")]
|
||||||
public ActionResult ResetIntroTimestamps([FromQuery] AnalysisMode mode, [FromQuery] bool eraseCache = false)
|
public async Task<ActionResult> ResetIntroTimestamps([FromQuery] AnalysisMode mode, [FromQuery] bool eraseCache = false)
|
||||||
{
|
{
|
||||||
if (mode == AnalysisMode.Introduction)
|
using var db = new IntroSkipperDbContext(Plugin.Instance!.DbPath);
|
||||||
{
|
var segments = await db.DbSegment
|
||||||
Plugin.Instance!.Intros.Clear();
|
.Where(s => s.Type == mode)
|
||||||
}
|
.ToListAsync()
|
||||||
else if (mode == AnalysisMode.Credits)
|
.ConfigureAwait(false);
|
||||||
{
|
|
||||||
Plugin.Instance!.Credits.Clear();
|
db.DbSegment.RemoveRange(segments);
|
||||||
}
|
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (eraseCache)
|
if (eraseCache)
|
||||||
{
|
{
|
||||||
FFmpegWrapper.DeleteCacheFiles(mode);
|
FFmpegWrapper.DeleteCacheFiles(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Instance!.EpisodeStates.Clear();
|
|
||||||
Plugin.Instance!.SaveTimestamps(mode);
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ using System.Net.Mime;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
|
using IntroSkipper.Db;
|
||||||
using IntroSkipper.Manager;
|
using IntroSkipper.Manager;
|
||||||
using MediaBrowser.Common.Api;
|
using MediaBrowser.Common.Api;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -83,49 +84,19 @@ public class VisualizationController(ILogger<VisualizationController> logger, Me
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the ignore list for the provided season.
|
/// Returns the analyzer actions for the provided season.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seasonId">Season ID.</param>
|
/// <param name="seasonId">Season ID.</param>
|
||||||
/// <returns>List of episode titles.</returns>
|
/// <returns>List of episode titles.</returns>
|
||||||
[HttpGet("IgnoreListSeason/{SeasonId}")]
|
[HttpGet("AnalyzerActions/{SeasonId}")]
|
||||||
public ActionResult<IgnoreListItem> GetIgnoreListSeason([FromRoute] Guid seasonId)
|
public ActionResult<IReadOnlyDictionary<AnalysisMode, AnalyzerAction>> GetAnalyzerAction([FromRoute] Guid seasonId)
|
||||||
{
|
{
|
||||||
if (!Plugin.Instance!.QueuedMediaItems.ContainsKey(seasonId))
|
if (!Plugin.Instance!.QueuedMediaItems.ContainsKey(seasonId))
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Plugin.Instance!.IgnoreList.TryGetValue(seasonId, out _))
|
return Ok(Plugin.Instance!.GetAnalyzerAction(seasonId));
|
||||||
{
|
|
||||||
return new IgnoreListItem(seasonId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new IgnoreListItem(Plugin.Instance!.IgnoreList[seasonId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the ignore list for the provided series.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="seriesId">Show ID.</param>
|
|
||||||
/// <returns>List of episode titles.</returns>
|
|
||||||
[HttpGet("IgnoreListSeries/{SeriesId}")]
|
|
||||||
public ActionResult<IgnoreListItem> GetIgnoreListSeries([FromRoute] Guid seriesId)
|
|
||||||
{
|
|
||||||
var seasonIds = Plugin.Instance!.QueuedMediaItems
|
|
||||||
.Where(kvp => kvp.Value.Any(e => e.SeriesId == seriesId))
|
|
||||||
.Select(kvp => kvp.Key)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (seasonIds.Count == 0)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new IgnoreListItem(Guid.Empty)
|
|
||||||
{
|
|
||||||
IgnoreIntro = seasonIds.All(seasonId => Plugin.Instance!.IsIgnored(seasonId, AnalysisMode.Introduction)),
|
|
||||||
IgnoreCredits = seasonIds.All(seasonId => Plugin.Instance!.IsIgnored(seasonId, AnalysisMode.Credits))
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -199,13 +170,22 @@ public class VisualizationController(ILogger<VisualizationController> logger, Me
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
using var db = new IntroSkipperDbContext(Plugin.Instance!.DbPath);
|
||||||
|
|
||||||
foreach (var episode in episodes)
|
foreach (var episode in episodes)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var segments = Plugin.Instance!.GetSegmentsById(episode.EpisodeId);
|
||||||
|
|
||||||
Plugin.Instance.Intros.TryRemove(episode.EpisodeId, out _);
|
if (segments.TryGetValue(AnalysisMode.Introduction, out var introSegment))
|
||||||
Plugin.Instance.Credits.TryRemove(episode.EpisodeId, out _);
|
{
|
||||||
episode.State.ResetStates();
|
db.DbSegment.Remove(new DbSegment(introSegment, AnalysisMode.Introduction));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments.TryGetValue(AnalysisMode.Introduction, out var creditSegment))
|
||||||
|
{
|
||||||
|
db.DbSegment.Remove(new DbSegment(creditSegment, AnalysisMode.Credits));
|
||||||
|
}
|
||||||
|
|
||||||
if (eraseCache)
|
if (eraseCache)
|
||||||
{
|
{
|
||||||
@ -213,7 +193,7 @@ public class VisualizationController(ILogger<VisualizationController> logger, Me
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Instance.SaveTimestamps(AnalysisMode.Introduction | AnalysisMode.Credits);
|
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (Plugin.Instance.Configuration.UpdateMediaSegments)
|
if (Plugin.Instance.Configuration.UpdateMediaSegments)
|
||||||
{
|
{
|
||||||
@ -229,82 +209,14 @@ public class VisualizationController(ILogger<VisualizationController> logger, Me
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the ignore list for the provided season.
|
/// Updates the analyzer actions for the provided season.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ignoreListItem">New ignore list items.</param>
|
/// <param name="request">Update analyzer actions request.</param>
|
||||||
/// <param name="save">Save the ignore list.</param>
|
|
||||||
/// <returns>No content.</returns>
|
/// <returns>No content.</returns>
|
||||||
[HttpPost("IgnoreList/UpdateSeason")]
|
[HttpPost("AnalyzerActions/UpdateSeason")]
|
||||||
public ActionResult UpdateIgnoreListSeason([FromBody] IgnoreListItem ignoreListItem, bool save = true)
|
public async Task<ActionResult> UpdateAnalyzerActions([FromBody] UpdateAnalyzerActionsRequest request)
|
||||||
{
|
{
|
||||||
if (!Plugin.Instance!.QueuedMediaItems.ContainsKey(ignoreListItem.SeasonId))
|
await Plugin.Instance!.UpdateAnalyzerActionAsync(request.Id, request.AnalyzerActions).ConfigureAwait(false);
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ignoreListItem.IgnoreIntro || ignoreListItem.IgnoreCredits)
|
|
||||||
{
|
|
||||||
Plugin.Instance!.IgnoreList.AddOrUpdate(ignoreListItem.SeasonId, ignoreListItem, (_, _) => ignoreListItem);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Plugin.Instance!.IgnoreList.TryRemove(ignoreListItem.SeasonId, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (save)
|
|
||||||
{
|
|
||||||
Plugin.Instance!.SaveIgnoreList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates the ignore list for the provided series.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="seriesId">Series ID.</param>
|
|
||||||
/// <param name="ignoreListItem">New ignore list items.</param>
|
|
||||||
/// <returns>No content.</returns>
|
|
||||||
[HttpPost("IgnoreList/UpdateSeries/{SeriesId}")]
|
|
||||||
public ActionResult UpdateIgnoreListSeries([FromRoute] Guid seriesId, [FromBody] IgnoreListItem ignoreListItem)
|
|
||||||
{
|
|
||||||
var seasonIds = Plugin.Instance!.QueuedMediaItems
|
|
||||||
.Where(kvp => kvp.Value.Any(e => e.SeriesId == seriesId))
|
|
||||||
.Select(kvp => kvp.Key)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (seasonIds.Count == 0)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var seasonId in seasonIds)
|
|
||||||
{
|
|
||||||
UpdateIgnoreListSeason(new IgnoreListItem(ignoreListItem) { SeasonId = seasonId }, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Plugin.Instance!.SaveIgnoreList();
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates the introduction timestamps for the provided episode.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">Episode ID to update timestamps for.</param>
|
|
||||||
/// <param name="timestamps">New introduction start and end times.</param>
|
|
||||||
/// <response code="204">New introduction timestamps saved.</response>
|
|
||||||
/// <returns>No content.</returns>
|
|
||||||
[HttpPost("Episode/{Id}/UpdateIntroTimestamps")]
|
|
||||||
[Obsolete("deprecated use Episode/{Id}/Timestamps")]
|
|
||||||
public ActionResult UpdateIntroTimestamps([FromRoute] Guid id, [FromBody] Intro timestamps)
|
|
||||||
{
|
|
||||||
if (timestamps.IntroEnd > 0.0)
|
|
||||||
{
|
|
||||||
var tr = new TimeRange(timestamps.IntroStart, timestamps.IntroEnd);
|
|
||||||
Plugin.Instance!.Intros[id] = new Segment(id, tr);
|
|
||||||
Plugin.Instance.SaveTimestamps(AnalysisMode.Introduction);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
35
IntroSkipper/Data/AnalyzerAction.cs
Normal file
35
IntroSkipper/Data/AnalyzerAction.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of media file analysis to perform.
|
||||||
|
/// </summary>
|
||||||
|
public enum AnalyzerAction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default action.
|
||||||
|
/// </summary>
|
||||||
|
Default,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detect chapters.
|
||||||
|
/// </summary>
|
||||||
|
Chapter,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detect chromaprint fingerprints.
|
||||||
|
/// </summary>
|
||||||
|
Chromaprint,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detect black frames.
|
||||||
|
/// </summary>
|
||||||
|
BlackFrame,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No action.
|
||||||
|
/// </summary>
|
||||||
|
None,
|
||||||
|
}
|
@ -1,53 +0,0 @@
|
|||||||
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace IntroSkipper.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the state of an episode regarding analysis and blacklist status.
|
|
||||||
/// </summary>
|
|
||||||
public class EpisodeState
|
|
||||||
{
|
|
||||||
private readonly bool[] _analyzedStates = new bool[2];
|
|
||||||
|
|
||||||
private readonly bool[] _blacklistedStates = new bool[2];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if the specified analysis mode has been analyzed.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mode">The analysis mode to check.</param>
|
|
||||||
/// <returns>True if the mode has been analyzed, false otherwise.</returns>
|
|
||||||
public bool IsAnalyzed(AnalysisMode mode) => _analyzedStates[(int)mode];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the analyzed state for the specified analysis mode.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mode">The analysis mode to set.</param>
|
|
||||||
/// <param name="value">The analyzed state to set.</param>
|
|
||||||
public void SetAnalyzed(AnalysisMode mode, bool value) => _analyzedStates[(int)mode] = value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if the specified analysis mode has been blacklisted.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mode">The analysis mode to check.</param>
|
|
||||||
/// <returns>True if the mode has been blacklisted, false otherwise.</returns>
|
|
||||||
public bool IsBlacklisted(AnalysisMode mode) => _blacklistedStates[(int)mode];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the blacklisted state for the specified analysis mode.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mode">The analysis mode to set.</param>
|
|
||||||
/// <param name="value">The blacklisted state to set.</param>
|
|
||||||
public void SetBlacklisted(AnalysisMode mode, bool value) => _blacklistedStates[(int)mode] = value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resets the analyzed states.
|
|
||||||
/// </summary>
|
|
||||||
public void ResetStates()
|
|
||||||
{
|
|
||||||
Array.Clear(_analyzedStates);
|
|
||||||
Array.Clear(_blacklistedStates);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
|
|
||||||
namespace IntroSkipper.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents an item to ignore.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract(Namespace = "http://schemas.datacontract.org/2004/07/ConfusedPolarBear.Plugin.IntroSkipper")]
|
|
||||||
public class IgnoreListItem
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="IgnoreListItem"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public IgnoreListItem()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="IgnoreListItem"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="seasonId">The season id.</param>
|
|
||||||
public IgnoreListItem(Guid seasonId)
|
|
||||||
{
|
|
||||||
SeasonId = seasonId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="IgnoreListItem"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The item to copy.</param>
|
|
||||||
public IgnoreListItem(IgnoreListItem item)
|
|
||||||
{
|
|
||||||
SeasonId = item.SeasonId;
|
|
||||||
IgnoreIntro = item.IgnoreIntro;
|
|
||||||
IgnoreCredits = item.IgnoreCredits;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the season id.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember]
|
|
||||||
public Guid SeasonId { get; set; } = Guid.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to ignore the intro.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember]
|
|
||||||
public bool IgnoreIntro { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to ignore the credits.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember]
|
|
||||||
public bool IgnoreCredits { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Toggles the provided mode to the provided value.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mode">Analysis mode.</param>
|
|
||||||
/// <param name="value">Value to set.</param>
|
|
||||||
public void Toggle(AnalysisMode mode, bool value)
|
|
||||||
{
|
|
||||||
switch (mode)
|
|
||||||
{
|
|
||||||
case AnalysisMode.Introduction:
|
|
||||||
IgnoreIntro = value;
|
|
||||||
break;
|
|
||||||
case AnalysisMode.Credits:
|
|
||||||
IgnoreCredits = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if the provided mode is ignored.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mode">Analysis mode.</param>
|
|
||||||
/// <returns>True if ignored, false otherwise.</returns>
|
|
||||||
public bool IsIgnored(AnalysisMode mode)
|
|
||||||
{
|
|
||||||
return mode switch
|
|
||||||
{
|
|
||||||
AnalysisMode.Introduction => IgnoreIntro,
|
|
||||||
AnalysisMode.Credits => IgnoreCredits,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only.
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace IntroSkipper.Data;
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
@ -10,6 +11,8 @@ namespace IntroSkipper.Data;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class QueuedEpisode
|
public class QueuedEpisode
|
||||||
{
|
{
|
||||||
|
private readonly Dictionary<AnalysisMode, bool> _isAnalyzed = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the series name.
|
/// Gets or sets the series name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -31,9 +34,9 @@ public class QueuedEpisode
|
|||||||
public Guid SeriesId { get; set; }
|
public Guid SeriesId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the state of the episode.
|
/// Gets a value indicating whether this media has been already analyzed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EpisodeState State => Plugin.Instance!.GetState(EpisodeId);
|
public IReadOnlyDictionary<AnalysisMode, bool> IsAnalyzed => _isAnalyzed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the full path to episode.
|
/// Gets or sets the full path to episode.
|
||||||
@ -69,4 +72,24 @@ public class QueuedEpisode
|
|||||||
/// Gets or sets the total duration of this media file (in seconds).
|
/// Gets or sets the total duration of this media file (in seconds).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Duration { get; set; }
|
public int Duration { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a value indicating whether this media has been already analyzed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mode">Analysis mode.</param>
|
||||||
|
/// <param name="value">Value to set.</param>
|
||||||
|
public void SetAnalyzed(AnalysisMode mode, bool value)
|
||||||
|
{
|
||||||
|
_isAnalyzed[mode] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a value indicating whether this media has been already analyzed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mode">Analysis mode.</param>
|
||||||
|
/// <returns>Value of the analyzed mode.</returns>
|
||||||
|
public bool GetAnalyzed(AnalysisMode mode)
|
||||||
|
{
|
||||||
|
return _isAnalyzed.TryGetValue(mode, out var value) && value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only.
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
21
IntroSkipper/Data/UpdateAnalyzerActionsRequest.cs
Normal file
21
IntroSkipper/Data/UpdateAnalyzerActionsRequest.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace IntroSkipper.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// /// Update analyzer actions request.
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateAnalyzerActionsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets season ID.
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets analyzer actions.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<AnalysisMode, AnalyzerAction> AnalyzerActions { get; set; } = new Dictionary<AnalysisMode, AnalyzerAction>();
|
||||||
|
}
|
||||||
|
}
|
51
IntroSkipper/Db/DbSeasonInfo.cs
Normal file
51
IntroSkipper/Db/DbSeasonInfo.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using IntroSkipper.Data;
|
||||||
|
|
||||||
|
namespace IntroSkipper.Db;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All times are measured in seconds relative to the beginning of the media file.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of the <see cref="DbSeasonInfo"/> class.
|
||||||
|
/// </remarks>
|
||||||
|
public class DbSeasonInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DbSeasonInfo"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seasonId">Season ID.</param>
|
||||||
|
/// <param name="mode">Analysis mode.</param>
|
||||||
|
/// <param name="action">Analyzer action.</param>
|
||||||
|
public DbSeasonInfo(Guid seasonId, AnalysisMode mode, AnalyzerAction action)
|
||||||
|
{
|
||||||
|
SeasonId = seasonId;
|
||||||
|
Type = mode;
|
||||||
|
Action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DbSeasonInfo"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public DbSeasonInfo()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the item ID.
|
||||||
|
/// </summary>
|
||||||
|
public Guid SeasonId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the analysis mode.
|
||||||
|
/// </summary>
|
||||||
|
public AnalysisMode Type { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the analyzer action.
|
||||||
|
/// </summary>
|
||||||
|
public AnalyzerAction Action { get; private set; }
|
||||||
|
}
|
56
IntroSkipper/Db/DbSegment.cs
Normal file
56
IntroSkipper/Db/DbSegment.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using IntroSkipper.Data;
|
||||||
|
|
||||||
|
namespace IntroSkipper.Db;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All times are measured in seconds relative to the beginning of the media file.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of the <see cref="DbSegment"/> class.
|
||||||
|
/// </remarks>
|
||||||
|
public class DbSegment
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DbSegment"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="segment">Segment.</param>
|
||||||
|
/// <param name="mode">Analysis mode.</param>
|
||||||
|
public DbSegment(Segment segment, AnalysisMode mode)
|
||||||
|
{
|
||||||
|
ItemId = segment.EpisodeId;
|
||||||
|
Start = segment.Start;
|
||||||
|
End = segment.End;
|
||||||
|
Type = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DbSegment"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public DbSegment()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the item ID.
|
||||||
|
/// </summary>
|
||||||
|
public Guid ItemId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the start time.
|
||||||
|
/// </summary>
|
||||||
|
public double Start { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the end time.
|
||||||
|
/// </summary>
|
||||||
|
public double End { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the analysis mode.
|
||||||
|
/// </summary>
|
||||||
|
public AnalysisMode Type { get; private set; }
|
||||||
|
}
|
80
IntroSkipper/Db/IntroSkipperDbContext.cs
Normal file
80
IntroSkipper/Db/IntroSkipperDbContext.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace IntroSkipper.Db;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin database.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of the <see cref="IntroSkipperDbContext"/> class.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="dbPath">The path to the SQLite database file.</param>
|
||||||
|
public class IntroSkipperDbContext(string dbPath) : DbContext
|
||||||
|
{
|
||||||
|
private readonly string _dbPath = dbPath ?? throw new ArgumentNullException(nameof(dbPath));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="DbSet{TEntity}"/> containing the segments.
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<DbSegment> DbSegment { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="DbSet{TEntity}"/> containing the season information.
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<DbSeasonInfo> DbSeasonInfo { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
optionsBuilder.UseSqlite($"Data Source={_dbPath}")
|
||||||
|
.EnableSensitiveDataLogging(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<DbSegment>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("DbSegment");
|
||||||
|
entity.HasKey(s => new { s.ItemId, s.Type });
|
||||||
|
|
||||||
|
entity.Property(e => e.ItemId)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
entity.Property(e => e.Type)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
entity.Property(e => e.Start);
|
||||||
|
|
||||||
|
entity.Property(e => e.End);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<DbSeasonInfo>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("DbSeasonInfo");
|
||||||
|
entity.HasKey(s => new { s.SeasonId, s.Type });
|
||||||
|
|
||||||
|
entity.Property(e => e.SeasonId)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
entity.Property(e => e.Type)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
entity.Property(e => e.Action);
|
||||||
|
});
|
||||||
|
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies any pending migrations to the database.
|
||||||
|
/// </summary>
|
||||||
|
public void ApplyMigrations()
|
||||||
|
{
|
||||||
|
Database.Migrate();
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.*-*" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.10.*-*" />
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.10.*-*" />
|
<PackageReference Include="Jellyfin.Model" Version="10.10.*-*" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.*-*" />
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556" PrivateAssets="All" />
|
||||||
|
@ -292,39 +292,31 @@ namespace IntroSkipper.Manager
|
|||||||
VerifyQueue(IReadOnlyList<QueuedEpisode> candidates, IReadOnlyCollection<AnalysisMode> modes)
|
VerifyQueue(IReadOnlyList<QueuedEpisode> candidates, IReadOnlyCollection<AnalysisMode> modes)
|
||||||
{
|
{
|
||||||
var verified = new List<QueuedEpisode>();
|
var verified = new List<QueuedEpisode>();
|
||||||
var reqModes = new HashSet<AnalysisMode>();
|
var reqModes = new HashSet<AnalysisMode>(modes); // Start with all modes and remove completed ones
|
||||||
|
|
||||||
foreach (var candidate in candidates)
|
foreach (var candidate in candidates)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var path = Plugin.Instance!.GetItemPath(candidate.EpisodeId);
|
var path = Plugin.Instance!.GetItemPath(candidate.EpisodeId);
|
||||||
|
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
verified.Add(candidate);
|
verified.Add(candidate);
|
||||||
|
var segments = Plugin.Instance!.GetSegmentsById(candidate.EpisodeId);
|
||||||
|
|
||||||
foreach (var mode in modes)
|
foreach (var mode in modes)
|
||||||
{
|
{
|
||||||
if (candidate.State.IsAnalyzed(mode) || candidate.State.IsBlacklisted(mode))
|
if (segments.TryGetValue(mode, out var segment))
|
||||||
{
|
{
|
||||||
continue;
|
if (segment.Valid)
|
||||||
}
|
{
|
||||||
|
candidate.SetAnalyzed(mode, true);
|
||||||
|
}
|
||||||
|
|
||||||
bool isAnalyzed = mode == AnalysisMode.Introduction
|
reqModes.Remove(mode);
|
||||||
? Plugin.Instance!.Intros.ContainsKey(candidate.EpisodeId)
|
|
||||||
: Plugin.Instance!.Credits.ContainsKey(candidate.EpisodeId);
|
|
||||||
|
|
||||||
if (isAnalyzed)
|
|
||||||
{
|
|
||||||
candidate.State.SetAnalyzed(mode, true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
reqModes.Add(mode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,12 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
|
using IntroSkipper.Db;
|
||||||
using IntroSkipper.Helper;
|
using IntroSkipper.Helper;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
@ -23,6 +25,7 @@ using MediaBrowser.Model.Entities;
|
|||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using MediaBrowser.Model.Updates;
|
using MediaBrowser.Model.Updates;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace IntroSkipper;
|
namespace IntroSkipper;
|
||||||
@ -32,15 +35,13 @@ namespace IntroSkipper;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||||
{
|
{
|
||||||
private readonly object _serializationLock = new();
|
|
||||||
private readonly object _introsLock = new();
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IItemRepository _itemRepository;
|
private readonly IItemRepository _itemRepository;
|
||||||
private readonly IApplicationHost _applicationHost;
|
private readonly IApplicationHost _applicationHost;
|
||||||
private readonly ILogger<Plugin> _logger;
|
private readonly ILogger<Plugin> _logger;
|
||||||
private readonly string _introPath;
|
private readonly string _introPath;
|
||||||
private readonly string _creditsPath;
|
private readonly string _creditsPath;
|
||||||
private string _ignorelistPath;
|
private readonly string _dbPath;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||||
@ -80,7 +81,7 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
FingerprintCachePath = Path.Join(introsDirectory, pluginCachePath);
|
FingerprintCachePath = Path.Join(introsDirectory, pluginCachePath);
|
||||||
_introPath = Path.Join(applicationPaths.DataPath, pluginDirName, "intros.xml");
|
_introPath = Path.Join(applicationPaths.DataPath, pluginDirName, "intros.xml");
|
||||||
_creditsPath = Path.Join(applicationPaths.DataPath, pluginDirName, "credits.xml");
|
_creditsPath = Path.Join(applicationPaths.DataPath, pluginDirName, "credits.xml");
|
||||||
_ignorelistPath = Path.Join(applicationPaths.DataPath, pluginDirName, "ignorelist.xml");
|
_dbPath = Path.Join(applicationPaths.DataPath, pluginDirName, "introskipper.db");
|
||||||
|
|
||||||
// Create the base & cache directories (if needed).
|
// Create the base & cache directories (if needed).
|
||||||
if (!Directory.Exists(FingerprintCachePath))
|
if (!Directory.Exists(FingerprintCachePath))
|
||||||
@ -99,22 +100,18 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
XmlSerializer serializer = new XmlSerializer(typeof(PluginConfiguration));
|
XmlSerializer serializer = new XmlSerializer(typeof(PluginConfiguration));
|
||||||
using (FileStream fileStream = new FileStream(oldConfigFile, FileMode.Open))
|
using FileStream fileStream = new FileStream(oldConfigFile, FileMode.Open);
|
||||||
|
var settings = new XmlReaderSettings
|
||||||
{
|
{
|
||||||
var settings = new XmlReaderSettings
|
DtdProcessing = DtdProcessing.Prohibit, // Disable DTD processing
|
||||||
{
|
XmlResolver = null // Disable the XmlResolver
|
||||||
DtdProcessing = DtdProcessing.Prohibit, // Disable DTD processing
|
};
|
||||||
XmlResolver = null // Disable the XmlResolver
|
|
||||||
};
|
|
||||||
|
|
||||||
using (var reader = XmlReader.Create(fileStream, settings))
|
using var reader = XmlReader.Create(fileStream, settings);
|
||||||
{
|
if (serializer.Deserialize(reader) is PluginConfiguration oldConfig)
|
||||||
if (serializer.Deserialize(reader) is PluginConfiguration oldConfig)
|
{
|
||||||
{
|
Instance.UpdateConfiguration(oldConfig);
|
||||||
Instance.UpdateConfiguration(oldConfig);
|
File.Delete(oldConfigFile);
|
||||||
File.Delete(oldConfigFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -126,23 +123,20 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
|
|
||||||
MigrateRepoUrl(serverConfiguration);
|
MigrateRepoUrl(serverConfiguration);
|
||||||
|
|
||||||
// TODO: remove when https://github.com/jellyfin/jellyfin-meta/discussions/30 is complete
|
// Initialize database, restore timestamps if available.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
RestoreTimestamps();
|
using var db = new IntroSkipperDbContext(_dbPath);
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
db.ApplyMigrations();
|
||||||
|
if (File.Exists(_introPath) || File.Exists(_creditsPath))
|
||||||
|
{
|
||||||
|
RestoreTimestampsAsync(db).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Unable to load introduction timestamps: {Exception}", ex);
|
_logger.LogWarning("Error initializing database: {Exception}", ex);
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
LoadIgnoreList();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Unable to load ignore list: {Exception}", ex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject the skip intro button code into the web interface.
|
// Inject the skip intro button code into the web interface.
|
||||||
@ -161,30 +155,15 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the results of fingerprinting all episodes.
|
/// Gets the path to the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ConcurrentDictionary<Guid, Segment> Intros { get; } = new();
|
public string DbPath => _dbPath;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all discovered ending credits.
|
|
||||||
/// </summary>
|
|
||||||
public ConcurrentDictionary<Guid, Segment> Credits { get; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the most recent media item queue.
|
/// Gets the most recent media item queue.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ConcurrentDictionary<Guid, List<QueuedEpisode>> QueuedMediaItems { get; } = new();
|
public ConcurrentDictionary<Guid, List<QueuedEpisode>> QueuedMediaItems { get; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all episode states.
|
|
||||||
/// </summary>
|
|
||||||
public ConcurrentDictionary<Guid, EpisodeState> EpisodeStates { get; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the ignore list.
|
|
||||||
/// </summary>
|
|
||||||
public ConcurrentDictionary<Guid, IgnoreListItem> IgnoreList { get; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the total number of episodes in the queue.
|
/// Gets or sets the total number of episodes in the queue.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -216,109 +195,39 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static Plugin? Instance { get; private set; }
|
public static Plugin? Instance { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save timestamps to disk.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mode">Mode.</param>
|
|
||||||
public void SaveTimestamps(AnalysisMode mode)
|
|
||||||
{
|
|
||||||
List<Segment> introList = [];
|
|
||||||
var filePath = mode == AnalysisMode.Introduction
|
|
||||||
? _introPath
|
|
||||||
: _creditsPath;
|
|
||||||
|
|
||||||
lock (_introsLock)
|
|
||||||
{
|
|
||||||
introList.AddRange(mode == AnalysisMode.Introduction
|
|
||||||
? Instance!.Intros.Values
|
|
||||||
: Instance!.Credits.Values);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_serializationLock)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
XmlSerializationHelper.SerializeToXml(introList, filePath);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError("SaveTimestamps {Message}", e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save IgnoreList to disk.
|
|
||||||
/// </summary>
|
|
||||||
public void SaveIgnoreList()
|
|
||||||
{
|
|
||||||
var ignorelist = Instance!.IgnoreList.Values.ToList();
|
|
||||||
|
|
||||||
lock (_serializationLock)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
XmlSerializationHelper.SerializeToXml(ignorelist, _ignorelistPath);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError("SaveIgnoreList {Message}", e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check if an item is ignored.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">Item id.</param>
|
|
||||||
/// <param name="mode">Mode.</param>
|
|
||||||
/// <returns>True if ignored, false otherwise.</returns>
|
|
||||||
public bool IsIgnored(Guid id, AnalysisMode mode)
|
|
||||||
{
|
|
||||||
return Instance!.IgnoreList.TryGetValue(id, out var item) && item.IsIgnored(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Load IgnoreList from disk.
|
|
||||||
/// </summary>
|
|
||||||
public void LoadIgnoreList()
|
|
||||||
{
|
|
||||||
if (File.Exists(_ignorelistPath))
|
|
||||||
{
|
|
||||||
var ignorelist = XmlSerializationHelper.DeserializeFromXml<IgnoreListItem>(_ignorelistPath);
|
|
||||||
|
|
||||||
foreach (var item in ignorelist)
|
|
||||||
{
|
|
||||||
Instance!.IgnoreList.TryAdd(item.SeasonId, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Restore previous analysis results from disk.
|
/// Restore previous analysis results from disk.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RestoreTimestamps()
|
/// <param name="db">IntroSkipperDbContext.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
public async Task RestoreTimestampsAsync(IntroSkipperDbContext db)
|
||||||
{
|
{
|
||||||
|
// Import intros
|
||||||
if (File.Exists(_introPath))
|
if (File.Exists(_introPath))
|
||||||
{
|
{
|
||||||
// Since dictionaries can't be easily serialized, analysis results are stored on disk as a list.
|
|
||||||
var introList = XmlSerializationHelper.DeserializeFromXml<Segment>(_introPath);
|
var introList = XmlSerializationHelper.DeserializeFromXml<Segment>(_introPath);
|
||||||
|
|
||||||
foreach (var intro in introList)
|
foreach (var intro in introList)
|
||||||
{
|
{
|
||||||
Instance!.Intros.TryAdd(intro.EpisodeId, intro);
|
var dbSegment = new DbSegment(intro, AnalysisMode.Introduction);
|
||||||
|
db.DbSegment.Add(dbSegment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Import credits
|
||||||
if (File.Exists(_creditsPath))
|
if (File.Exists(_creditsPath))
|
||||||
{
|
{
|
||||||
var creditList = XmlSerializationHelper.DeserializeFromXml<Segment>(_creditsPath);
|
var creditList = XmlSerializationHelper.DeserializeFromXml<Segment>(_creditsPath);
|
||||||
|
|
||||||
foreach (var credit in creditList)
|
foreach (var credit in creditList)
|
||||||
{
|
{
|
||||||
Instance!.Credits.TryAdd(credit.EpisodeId, credit);
|
var dbSegment = new DbSegment(credit, AnalysisMode.Credits);
|
||||||
|
db.DbSegment.Add(dbSegment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
File.Delete(_introPath);
|
||||||
|
File.Delete(_creditsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -344,19 +253,6 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the Intro for this item.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">Item id.</param>
|
|
||||||
/// <param name="mode">Mode.</param>
|
|
||||||
/// <returns>Intro.</returns>
|
|
||||||
internal static Segment GetIntroByMode(Guid id, AnalysisMode mode)
|
|
||||||
{
|
|
||||||
return mode == AnalysisMode.Introduction
|
|
||||||
? Instance!.Intros[id]
|
|
||||||
: Instance!.Credits[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
internal BaseItem? GetItem(Guid id)
|
internal BaseItem? GetItem(Guid id)
|
||||||
{
|
{
|
||||||
return id != Guid.Empty ? _libraryManager.GetItemById(id) : null;
|
return id != Guid.Empty ? _libraryManager.GetItemById(id) : null;
|
||||||
@ -404,46 +300,126 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
return _itemRepository.GetChapters(item);
|
return _itemRepository.GetChapters(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
internal async Task UpdateTimestamps(IReadOnlyDictionary<Guid, Segment> newTimestamps, AnalysisMode mode)
|
||||||
/// Gets the state for this item.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">Item ID.</param>
|
|
||||||
/// <returns>State of this item.</returns>
|
|
||||||
internal EpisodeState GetState(Guid id) => EpisodeStates.GetOrAdd(id, _ => new EpisodeState());
|
|
||||||
|
|
||||||
internal void UpdateTimestamps(IReadOnlyDictionary<Guid, Segment> newTimestamps, AnalysisMode mode)
|
|
||||||
{
|
{
|
||||||
foreach (var intro in newTimestamps)
|
using var db = new IntroSkipperDbContext(_dbPath);
|
||||||
|
|
||||||
|
// Get all existing segments in a single query
|
||||||
|
var existingSegments = db.DbSegment
|
||||||
|
.Where(s => newTimestamps.Keys.Contains(s.ItemId) && s.Type == mode)
|
||||||
|
.ToDictionary(s => s.ItemId);
|
||||||
|
|
||||||
|
// Batch updates and inserts
|
||||||
|
var segmentsToAdd = new List<DbSegment>();
|
||||||
|
|
||||||
|
foreach (var (itemId, segment) in newTimestamps)
|
||||||
{
|
{
|
||||||
if (mode == AnalysisMode.Introduction)
|
var dbSegment = new DbSegment(segment, mode);
|
||||||
|
if (existingSegments.TryGetValue(itemId, out var existing))
|
||||||
{
|
{
|
||||||
Instance!.Intros.AddOrUpdate(intro.Key, intro.Value, (key, oldValue) => intro.Value);
|
db.Entry(existing).CurrentValues.SetValues(dbSegment);
|
||||||
}
|
}
|
||||||
else if (mode == AnalysisMode.Credits)
|
else
|
||||||
{
|
{
|
||||||
Instance!.Credits.AddOrUpdate(intro.Key, intro.Value, (key, oldValue) => intro.Value);
|
segmentsToAdd.Add(dbSegment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveTimestamps(mode);
|
if (segmentsToAdd.Count > 0)
|
||||||
|
{
|
||||||
|
await db.DbSegment.AddRangeAsync(segmentsToAdd).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void CleanTimestamps(HashSet<Guid> validEpisodeIds)
|
internal async Task ClearInvalidSegments()
|
||||||
{
|
{
|
||||||
var allKeys = new HashSet<Guid>(Instance!.Intros.Keys);
|
using var db = new IntroSkipperDbContext(_dbPath);
|
||||||
allKeys.UnionWith(Instance!.Credits.Keys);
|
db.DbSegment.RemoveRange(db.DbSegment.Where(s => s.End == 0));
|
||||||
|
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var key in allKeys)
|
internal async Task CleanTimestamps(HashSet<Guid> episodeIds)
|
||||||
|
{
|
||||||
|
using var db = new IntroSkipperDbContext(_dbPath);
|
||||||
|
db.DbSegment.RemoveRange(db.DbSegment
|
||||||
|
.Where(s => !episodeIds.Contains(s.ItemId)));
|
||||||
|
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal IReadOnlyDictionary<AnalysisMode, Segment> GetSegmentsById(Guid id)
|
||||||
|
{
|
||||||
|
using var db = new IntroSkipperDbContext(_dbPath);
|
||||||
|
return db.DbSegment
|
||||||
|
.Where(s => s.ItemId == id)
|
||||||
|
.ToDictionary(
|
||||||
|
s => s.Type,
|
||||||
|
s => new Segment
|
||||||
|
{
|
||||||
|
EpisodeId = s.ItemId,
|
||||||
|
Start = s.Start,
|
||||||
|
End = s.End
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Segment GetSegmentByMode(Guid id, AnalysisMode mode)
|
||||||
|
{
|
||||||
|
using var db = new IntroSkipperDbContext(_dbPath);
|
||||||
|
return db.DbSegment
|
||||||
|
.Where(s => s.ItemId == id && s.Type == mode)
|
||||||
|
.Select(s => new Segment
|
||||||
|
{
|
||||||
|
EpisodeId = s.ItemId,
|
||||||
|
Start = s.Start,
|
||||||
|
End = s.End
|
||||||
|
}).FirstOrDefault() ?? new Segment(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task UpdateAnalyzerActionAsync(Guid id, IReadOnlyDictionary<AnalysisMode, AnalyzerAction> analyzerActions)
|
||||||
|
{
|
||||||
|
using var db = new IntroSkipperDbContext(_dbPath);
|
||||||
|
var existingEntries = await db.DbSeasonInfo
|
||||||
|
.Where(s => s.SeasonId == id)
|
||||||
|
.ToDictionaryAsync(s => s.Type)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var (mode, action) in analyzerActions)
|
||||||
{
|
{
|
||||||
if (!validEpisodeIds.Contains(key))
|
var dbSeasonInfo = new DbSeasonInfo(id, mode, action);
|
||||||
|
if (existingEntries.TryGetValue(mode, out var existing))
|
||||||
{
|
{
|
||||||
Instance!.Intros.TryRemove(key, out _);
|
db.Entry(existing).CurrentValues.SetValues(dbSeasonInfo);
|
||||||
Instance!.Credits.TryRemove(key, out _);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
db.DbSeasonInfo.Add(dbSeasonInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveTimestamps(AnalysisMode.Introduction);
|
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||||
SaveTimestamps(AnalysisMode.Credits);
|
}
|
||||||
|
|
||||||
|
internal IReadOnlyDictionary<AnalysisMode, AnalyzerAction> GetAnalyzerAction(Guid id)
|
||||||
|
{
|
||||||
|
using var db = new IntroSkipperDbContext(_dbPath);
|
||||||
|
return db.DbSeasonInfo.Where(s => s.SeasonId == id).ToDictionary(s => s.Type, s => s.Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal AnalyzerAction GetAnalyzerAction(Guid id, AnalysisMode mode)
|
||||||
|
{
|
||||||
|
using var db = new IntroSkipperDbContext(_dbPath);
|
||||||
|
return db.DbSeasonInfo.FirstOrDefault(s => s.SeasonId == id && s.Type == mode)?.Action ?? AnalyzerAction.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task CleanSeasonInfoAsync()
|
||||||
|
{
|
||||||
|
using var db = new IntroSkipperDbContext(_dbPath);
|
||||||
|
var obsoleteSeasons = await db.DbSeasonInfo
|
||||||
|
.Where(s => !Instance!.QueuedMediaItems.Keys.Contains(s.SeasonId))
|
||||||
|
.ToListAsync().ConfigureAwait(false);
|
||||||
|
db.DbSeasonInfo.RemoveRange(obsoleteSeasons);
|
||||||
|
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MigrateRepoUrl(IServerConfigurationManager serverConfiguration)
|
private void MigrateRepoUrl(IServerConfigurationManager serverConfiguration)
|
||||||
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using IntroSkipper.Data;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@ -26,30 +27,37 @@ namespace IntroSkipper.Providers
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task<IReadOnlyList<MediaSegmentDto>> GetMediaSegments(MediaSegmentGenerationRequest request, CancellationToken cancellationToken)
|
public Task<IReadOnlyList<MediaSegmentDto>> GetMediaSegments(MediaSegmentGenerationRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var segments = new List<MediaSegmentDto>();
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
var remainingTicks = (Plugin.Instance?.Configuration.RemainingSecondsOfIntro ?? 2) * TimeSpan.TicksPerSecond;
|
ArgumentNullException.ThrowIfNull(Plugin.Instance);
|
||||||
|
|
||||||
if (Plugin.Instance!.Intros.TryGetValue(request.ItemId, out var introValue))
|
var segments = new List<MediaSegmentDto>();
|
||||||
|
var remainingTicks = Plugin.Instance.Configuration.RemainingSecondsOfIntro * TimeSpan.TicksPerSecond;
|
||||||
|
var itemSegments = Plugin.Instance.GetSegmentsById(request.ItemId);
|
||||||
|
|
||||||
|
// Add intro segment if found
|
||||||
|
if (itemSegments.TryGetValue(AnalysisMode.Introduction, out var introSegment) && introSegment.Valid)
|
||||||
{
|
{
|
||||||
segments.Add(new MediaSegmentDto
|
segments.Add(new MediaSegmentDto
|
||||||
{
|
{
|
||||||
StartTicks = (long)(introValue.Start * TimeSpan.TicksPerSecond),
|
StartTicks = (long)(introSegment.Start * TimeSpan.TicksPerSecond),
|
||||||
EndTicks = (long)(introValue.End * TimeSpan.TicksPerSecond) - remainingTicks,
|
EndTicks = (long)(introSegment.End * TimeSpan.TicksPerSecond) - remainingTicks,
|
||||||
ItemId = request.ItemId,
|
ItemId = request.ItemId,
|
||||||
Type = MediaSegmentType.Intro
|
Type = MediaSegmentType.Intro
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Plugin.Instance!.Credits.TryGetValue(request.ItemId, out var creditValue))
|
// Add outro/credits segment if found
|
||||||
|
if (itemSegments.TryGetValue(AnalysisMode.Introduction, out var creditSegment) && creditSegment.Valid)
|
||||||
{
|
{
|
||||||
var creditEndTicks = (long)(creditValue.End * TimeSpan.TicksPerSecond);
|
var creditEndTicks = (long)(creditSegment.End * TimeSpan.TicksPerSecond);
|
||||||
var runTimeTicks = Plugin.Instance.GetItem(request.ItemId)?.RunTimeTicks ?? long.MaxValue;
|
var runTimeTicks = Plugin.Instance.GetItem(request.ItemId)?.RunTimeTicks ?? long.MaxValue;
|
||||||
|
|
||||||
segments.Add(new MediaSegmentDto
|
segments.Add(new MediaSegmentDto
|
||||||
{
|
{
|
||||||
StartTicks = (long)(creditValue.Start * TimeSpan.TicksPerSecond),
|
StartTicks = (long)(creditSegment.Start * TimeSpan.TicksPerSecond),
|
||||||
EndTicks = runTimeTicks > creditEndTicks + TimeSpan.TicksPerSecond
|
EndTicks = runTimeTicks > creditEndTicks + TimeSpan.TicksPerSecond
|
||||||
? creditEndTicks - remainingTicks
|
? creditEndTicks - remainingTicks
|
||||||
: runTimeTicks,
|
: runTimeTicks,
|
||||||
ItemId = request.ItemId,
|
ItemId = request.ItemId,
|
||||||
Type = MediaSegmentType.Outro
|
Type = MediaSegmentType.Outro
|
||||||
});
|
});
|
||||||
|
@ -102,7 +102,7 @@ public class BaseItemAnalyzerTask
|
|||||||
// of the current media items were deleted from Jellyfin since the task was started.
|
// of the current media items were deleted from Jellyfin since the task was started.
|
||||||
var (episodes, requiredModes) = queueManager.VerifyQueue(
|
var (episodes, requiredModes) = queueManager.VerifyQueue(
|
||||||
season.Value,
|
season.Value,
|
||||||
_analysisModes.Where(m => !Plugin.Instance!.IsIgnored(season.Key, m)).ToList());
|
_analysisModes);
|
||||||
|
|
||||||
if (episodes.Count == 0)
|
if (episodes.Count == 0)
|
||||||
{
|
{
|
||||||
@ -132,7 +132,8 @@ public class BaseItemAnalyzerTask
|
|||||||
|
|
||||||
foreach (AnalysisMode mode in requiredModes)
|
foreach (AnalysisMode mode in requiredModes)
|
||||||
{
|
{
|
||||||
var analyzed = AnalyzeItems(episodes, mode, ct);
|
var action = Plugin.Instance!.GetAnalyzerAction(season.Key, mode);
|
||||||
|
var analyzed = await AnalyzeItems(episodes, mode, action, ct).ConfigureAwait(false);
|
||||||
Interlocked.Add(ref totalProcessed, analyzed);
|
Interlocked.Add(ref totalProcessed, analyzed);
|
||||||
|
|
||||||
updateManagers = analyzed > 0 || updateManagers;
|
updateManagers = analyzed > 0 || updateManagers;
|
||||||
@ -177,14 +178,16 @@ public class BaseItemAnalyzerTask
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="items">Media items to analyze.</param>
|
/// <param name="items">Media items to analyze.</param>
|
||||||
/// <param name="mode">Analysis mode.</param>
|
/// <param name="mode">Analysis mode.</param>
|
||||||
|
/// <param name="action">Analyzer action.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>Number of items that were successfully analyzed.</returns>
|
/// <returns>Number of items that were successfully analyzed.</returns>
|
||||||
private int AnalyzeItems(
|
private async Task<int> AnalyzeItems(
|
||||||
IReadOnlyList<QueuedEpisode> items,
|
IReadOnlyList<QueuedEpisode> items,
|
||||||
AnalysisMode mode,
|
AnalysisMode mode,
|
||||||
|
AnalyzerAction action,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var totalItems = items.Count(e => !e.State.IsAnalyzed(mode));
|
var totalItems = items.Count(e => !e.GetAnalyzed(mode));
|
||||||
|
|
||||||
// Only analyze specials (season 0) if the user has opted in.
|
// Only analyze specials (season 0) if the user has opted in.
|
||||||
var first = items[0];
|
var first = items[0];
|
||||||
@ -193,12 +196,6 @@ public class BaseItemAnalyzerTask
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from Blacklist
|
|
||||||
foreach (var item in items.Where(e => e.State.IsBlacklisted(mode)))
|
|
||||||
{
|
|
||||||
item.State.SetBlacklisted(mode, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"[Mode: {Mode}] Analyzing {Count} files from {Name} season {Season}",
|
"[Mode: {Mode}] Analyzing {Count} files from {Name} season {Season}",
|
||||||
mode,
|
mode,
|
||||||
@ -206,22 +203,24 @@ public class BaseItemAnalyzerTask
|
|||||||
first.SeriesName,
|
first.SeriesName,
|
||||||
first.SeasonNumber);
|
first.SeasonNumber);
|
||||||
|
|
||||||
var analyzers = new Collection<IMediaFileAnalyzer>
|
var analyzers = new Collection<IMediaFileAnalyzer>();
|
||||||
{
|
|
||||||
new ChapterAnalyzer(_loggerFactory.CreateLogger<ChapterAnalyzer>())
|
|
||||||
};
|
|
||||||
|
|
||||||
if (first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint && !first.IsMovie)
|
if (action == AnalyzerAction.Chapter || action == AnalyzerAction.Default)
|
||||||
|
{
|
||||||
|
analyzers.Add(new ChapterAnalyzer(_loggerFactory.CreateLogger<ChapterAnalyzer>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first.IsAnime && !first.IsMovie && (action == AnalyzerAction.Chromaprint || action == AnalyzerAction.Default))
|
||||||
{
|
{
|
||||||
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode == AnalysisMode.Credits)
|
if (mode == AnalysisMode.Credits && (action == AnalyzerAction.BlackFrame || action == AnalyzerAction.Default))
|
||||||
{
|
{
|
||||||
analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>()));
|
analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint && !first.IsMovie)
|
if (!first.IsAnime && !first.IsMovie && (action == AnalyzerAction.Chromaprint || action == AnalyzerAction.Default))
|
||||||
{
|
{
|
||||||
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
||||||
}
|
}
|
||||||
@ -230,16 +229,14 @@ public class BaseItemAnalyzerTask
|
|||||||
// analyzed items from the queue.
|
// analyzed items from the queue.
|
||||||
foreach (var analyzer in analyzers)
|
foreach (var analyzer in analyzers)
|
||||||
{
|
{
|
||||||
items = analyzer.AnalyzeMediaFiles(items, mode, cancellationToken);
|
items = await analyzer.AnalyzeMediaFiles(items, mode, cancellationToken).ConfigureAwait(false);
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add items without intros/credits to blacklist.
|
// Add items without intros/credits to blacklist.
|
||||||
foreach (var item in items.Where(e => !e.State.IsAnalyzed(mode)))
|
var blacklisted = items.Where(e => !e.GetAnalyzed(mode)).ToList();
|
||||||
{
|
await Plugin.Instance!.UpdateTimestamps(blacklisted.ToDictionary(e => e.EpisodeId, e => new Segment(e.EpisodeId)), mode).ConfigureAwait(false);
|
||||||
item.State.SetBlacklisted(mode, true);
|
totalItems -= blacklisted.Count;
|
||||||
totalItems -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalItems;
|
return totalItems;
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ public class CleanCacheTask : IScheduledTask
|
|||||||
/// <param name="progress">Task progress.</param>
|
/// <param name="progress">Task progress.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_libraryManager is null)
|
if (_libraryManager is null)
|
||||||
{
|
{
|
||||||
@ -82,24 +82,19 @@ public class CleanCacheTask : IScheduledTask
|
|||||||
|
|
||||||
// Retrieve media items and get valid episode IDs
|
// Retrieve media items and get valid episode IDs
|
||||||
var queue = queueManager.GetMediaItems();
|
var queue = queueManager.GetMediaItems();
|
||||||
var validEpisodeIds = new HashSet<Guid>(queue.Values.SelectMany(episodes => episodes.Select(e => e.EpisodeId)));
|
var validEpisodeIds = queue.Values
|
||||||
|
.SelectMany(episodes => episodes.Select(e => e.EpisodeId))
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
Plugin.Instance!.CleanTimestamps(validEpisodeIds);
|
await Plugin.Instance!.ClearInvalidSegments().ConfigureAwait(false);
|
||||||
|
|
||||||
|
await Plugin.Instance!.CleanTimestamps(validEpisodeIds).ConfigureAwait(false);
|
||||||
|
|
||||||
// Identify invalid episode IDs
|
// Identify invalid episode IDs
|
||||||
var invalidEpisodeIds = Directory.EnumerateFiles(Plugin.Instance!.FingerprintCachePath)
|
var invalidEpisodeIds = Directory.EnumerateFiles(Plugin.Instance!.FingerprintCachePath)
|
||||||
.Select(filePath =>
|
.Select(filePath => Path.GetFileNameWithoutExtension(filePath).Split('-')[0])
|
||||||
{
|
.Where(episodeIdStr => Guid.TryParse(episodeIdStr, out var episodeId) && !validEpisodeIds.Contains(episodeId))
|
||||||
var fileName = Path.GetFileNameWithoutExtension(filePath);
|
.Select(Guid.Parse)
|
||||||
var episodeIdStr = fileName.Split('-')[0];
|
|
||||||
if (Guid.TryParse(episodeIdStr, out Guid episodeId))
|
|
||||||
{
|
|
||||||
return validEpisodeIds.Contains(episodeId) ? (Guid?)null : episodeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.OfType<Guid>()
|
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
// Delete cache files for invalid episode IDs
|
// Delete cache files for invalid episode IDs
|
||||||
@ -109,31 +104,10 @@ public class CleanCacheTask : IScheduledTask
|
|||||||
FFmpegWrapper.DeleteEpisodeCache(episodeId);
|
FFmpegWrapper.DeleteEpisodeCache(episodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up ignore list by removing items that are no longer exist..
|
// Clean up Season information by removing items that are no longer exist.
|
||||||
var removedItems = false;
|
await Plugin.Instance!.CleanSeasonInfoAsync().ConfigureAwait(false);
|
||||||
foreach (var ignoredItem in Plugin.Instance.IgnoreList.Values.ToList())
|
|
||||||
{
|
|
||||||
if (!queue.ContainsKey(ignoredItem.SeasonId))
|
|
||||||
{
|
|
||||||
removedItems = true;
|
|
||||||
Plugin.Instance.IgnoreList.TryRemove(ignoredItem.SeasonId, out _);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save ignore list if at least one item was removed.
|
progress.Report(100);
|
||||||
if (removedItems)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Plugin.Instance!.SaveIgnoreList();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError("Failed to save ignore list: {Error}", e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -8,6 +8,8 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Timers;
|
using System.Timers;
|
||||||
using IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
|
using IntroSkipper.Data;
|
||||||
|
using IntroSkipper.Db;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
@ -125,7 +127,8 @@ namespace IntroSkipper.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Assert that an intro was detected for this item.
|
// Assert that an intro was detected for this item.
|
||||||
if (!Plugin.Instance!.Intros.TryGetValue(itemId, out var intro) || !intro.Valid)
|
var intro = Plugin.Instance!.GetSegmentByMode(itemId, AnalysisMode.Introduction);
|
||||||
|
if (!intro.Valid)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Timers;
|
using System.Timers;
|
||||||
using IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
@ -125,7 +126,8 @@ namespace IntroSkipper.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Assert that credits were detected for this item.
|
// Assert that credits were detected for this item.
|
||||||
if (!Plugin.Instance!.Credits.TryGetValue(itemId, out var credit) || !credit.Valid)
|
var credit = Plugin.Instance!.GetSegmentByMode(itemId, AnalysisMode.Credits);
|
||||||
|
if (!credit.Valid)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,11 @@ namespace IntroSkipper.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSettingsChanged(object? sender, BasePluginConfiguration e) => _config = (PluginConfiguration)e;
|
private void OnSettingsChanged(object? sender, BasePluginConfiguration e)
|
||||||
|
{
|
||||||
|
_config = (PluginConfiguration)e;
|
||||||
|
_ = Plugin.Instance!.ClearInvalidSegments();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start timer to debounce analyzing.
|
/// Start timer to debounce analyzing.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user