commit
3559c7b7d3
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: intro-skipper-${{ github.sha }}.dll
|
||||
name: ConfusedPolarBear.Plugin.IntroSkipper-${{ env.GIT_HASH }}.dll
|
||||
path: ConfusedPolarBear.Plugin.IntroSkipper/bin/Debug/net6.0/ConfusedPolarBear.Plugin.IntroSkipper.dll
|
||||
if-no-files-found: error
|
||||
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,4 +8,3 @@ docker/dist
|
||||
|
||||
# Visual Studio
|
||||
.vs/
|
||||
UpgradeLog*.htm
|
||||
|
@ -96,6 +96,11 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
|
||||
config.MaximumIntroDuration :
|
||||
config.MaximumEpisodeCreditsDuration;
|
||||
|
||||
if (chapters.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mode == AnalysisMode.Credits)
|
||||
{
|
||||
// Since the ending credits chapter may be the last chapter in the file, append a virtual
|
||||
@ -104,66 +109,119 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(episode.Duration).Ticks
|
||||
});
|
||||
|
||||
// Check all chapters in reverse order, skipping the virtual chapter
|
||||
for (int i = chapters.Count - 2; i >= 0; i--)
|
||||
{
|
||||
var current = chapters[i];
|
||||
var next = chapters[i + 1];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(current.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentRange = new TimeRange(
|
||||
TimeSpan.FromTicks(current.StartPositionTicks).TotalSeconds,
|
||||
TimeSpan.FromTicks(next.StartPositionTicks).TotalSeconds);
|
||||
|
||||
var baseMessage = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}: Chapter \"{1}\" ({2} - {3})",
|
||||
episode.Path,
|
||||
current.Name,
|
||||
currentRange.Start,
|
||||
currentRange.End);
|
||||
|
||||
if (currentRange.Duration < minDuration || currentRange.Duration > maxDuration)
|
||||
{
|
||||
_logger.LogTrace("{Base}: ignoring (invalid duration)", baseMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regex.IsMatch() is used here in order to allow the runtime to cache the compiled regex
|
||||
// between function invocations.
|
||||
var match = Regex.IsMatch(
|
||||
current.Name,
|
||||
expression,
|
||||
RegexOptions.None,
|
||||
TimeSpan.FromSeconds(1));
|
||||
|
||||
if (!match)
|
||||
{
|
||||
_logger.LogTrace("{Base}: ignoring (does not match regular expression)", baseMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
matchingChapter = new(episode.EpisodeId, currentRange);
|
||||
_logger.LogTrace("{Base}: okay", baseMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check all chapters
|
||||
for (int i = 0; i < chapters.Count - 1; i++)
|
||||
else
|
||||
{
|
||||
var current = chapters[i];
|
||||
var next = chapters[i + 1];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(current.Name))
|
||||
// Check all chapters
|
||||
for (int i = 0; i < chapters.Count - 1; i++)
|
||||
{
|
||||
continue;
|
||||
var current = chapters[i];
|
||||
var next = chapters[i + 1];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(current.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentRange = new TimeRange(
|
||||
TimeSpan.FromTicks(current.StartPositionTicks).TotalSeconds,
|
||||
TimeSpan.FromTicks(next.StartPositionTicks).TotalSeconds);
|
||||
|
||||
var baseMessage = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}: Chapter \"{1}\" ({2} - {3})",
|
||||
episode.Path,
|
||||
current.Name,
|
||||
currentRange.Start,
|
||||
currentRange.End);
|
||||
|
||||
if (currentRange.Duration < minDuration || currentRange.Duration > maxDuration)
|
||||
{
|
||||
_logger.LogTrace("{Base}: ignoring (invalid duration)", baseMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regex.IsMatch() is used here in order to allow the runtime to cache the compiled regex
|
||||
// between function invocations.
|
||||
var match = Regex.IsMatch(
|
||||
current.Name,
|
||||
expression,
|
||||
RegexOptions.None,
|
||||
TimeSpan.FromSeconds(1));
|
||||
|
||||
if (!match)
|
||||
{
|
||||
_logger.LogTrace("{Base}: ignoring (does not match regular expression)", baseMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(next.Name))
|
||||
{
|
||||
// Check for possibility of overlapping keywords
|
||||
var overlap = Regex.IsMatch(
|
||||
next.Name,
|
||||
expression,
|
||||
RegexOptions.None,
|
||||
TimeSpan.FromSeconds(1));
|
||||
|
||||
if (overlap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
matchingChapter = new(episode.EpisodeId, currentRange);
|
||||
_logger.LogTrace("{Base}: okay", baseMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
var currentRange = new TimeRange(
|
||||
TimeSpan.FromTicks(current.StartPositionTicks).TotalSeconds,
|
||||
TimeSpan.FromTicks(next.StartPositionTicks).TotalSeconds);
|
||||
|
||||
var baseMessage = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}: Chapter \"{1}\" ({2} - {3})",
|
||||
episode.Path,
|
||||
current.Name,
|
||||
currentRange.Start,
|
||||
currentRange.End);
|
||||
|
||||
if (currentRange.Duration < minDuration || currentRange.Duration > maxDuration)
|
||||
{
|
||||
_logger.LogTrace("{Base}: ignoring (invalid duration)", baseMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regex.IsMatch() is used here in order to allow the runtime to cache the compiled regex
|
||||
// between function invocations.
|
||||
var match = Regex.IsMatch(
|
||||
current.Name,
|
||||
expression,
|
||||
RegexOptions.None,
|
||||
TimeSpan.FromSeconds(1));
|
||||
|
||||
if (!match)
|
||||
{
|
||||
_logger.LogTrace("{Base}: ignoring (does not match regular expression)", baseMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for possibility of overlapping keywords
|
||||
var overlap = Regex.IsMatch(
|
||||
next.Name,
|
||||
expression,
|
||||
RegexOptions.None,
|
||||
TimeSpan.FromSeconds(1));
|
||||
|
||||
if (overlap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
matchingChapter = new(episode.EpisodeId, currentRange);
|
||||
_logger.LogTrace("{Base}: okay", baseMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
return matchingChapter;
|
||||
|
@ -103,7 +103,7 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// Gets or sets the regular expression used to detect ending credit chapters.
|
||||
/// </summary>
|
||||
public string ChapterAnalyzerEndCreditsPattern { get; set; } =
|
||||
@"(^|\s)(Credits?|Ending)(\s|$)";
|
||||
@"(^|\s)(Credits?|ED|Ending)(\s|$)";
|
||||
|
||||
// ===== Playback settings =====
|
||||
|
||||
|
@ -454,33 +454,17 @@
|
||||
<input style="width:4em" type="number" min="0" id="editRightEpisodeStart"> to
|
||||
<input style="width:4em;margin-bottom:10px" type="number" min="0" id="editRightEpisodeEnd">
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<button id="btnUpdateTimestamps" type="button">
|
||||
Update timestamps
|
||||
</button>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<textarea id="timestampError" style="display:none;" rows="2" cols="75" readonly></textarea>
|
||||
<br />
|
||||
|
||||
<button id="btnEraseSeasonTimestamps" type="button" style="display:none;">
|
||||
Erase all timestamps for this season
|
||||
</button>
|
||||
<hr />
|
||||
|
||||
<button id="btnEraseIntroTimestamps">
|
||||
Erase all introduction timestamps (globally)
|
||||
</button>
|
||||
<br />
|
||||
|
||||
<button id="btnEraseCreditTimestamps">
|
||||
Erase all end credits timestamps (globally)
|
||||
</button>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h3>Fingerprint Visualizer</h3>
|
||||
<p>
|
||||
Interactively compare the audio fingerprints of two episodes. <br />
|
||||
@ -526,11 +510,27 @@
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<canvas id="troubleshooter"></canvas>
|
||||
<canvas id="troubleshooter" style="display:none;"></canvas>
|
||||
<span id="timestampContainer">
|
||||
<span id="timestamps"></span> <br />
|
||||
<span id="intros"></span>
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<button id="btnEraseSeasonTimestamps" type="button" style="display:none;">
|
||||
Erase all timestamps for this season
|
||||
</button>
|
||||
<hr />
|
||||
|
||||
<button id="btnEraseIntroTimestamps">
|
||||
Erase all introduction timestamps (globally)
|
||||
</button>
|
||||
<br />
|
||||
|
||||
<button id="btnEraseCreditTimestamps">
|
||||
Erase all end credits timestamps (globally)
|
||||
</button>
|
||||
</details>
|
||||
</fieldset>
|
||||
</div>
|
||||
@ -602,6 +602,7 @@
|
||||
var txtSuggested = document.querySelector("span#suggestedShifts");
|
||||
var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
|
||||
var timestampError = document.querySelector("textarea#timestampError");
|
||||
var timestampEditor = document.querySelector("#timestampEditor");
|
||||
var btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps");
|
||||
var timeContainer = document.querySelector("span#timestampContainer");
|
||||
|
||||
@ -613,8 +614,8 @@
|
||||
|
||||
async function autoSkipChanged() {
|
||||
if (autoSkip.checked) {
|
||||
skipFirstEpisode.style.display = 'block';
|
||||
autoSkipNotificationText.style.display = 'block';
|
||||
skipFirstEpisode.style.display = 'unset';
|
||||
autoSkipNotificationText.style.display = 'unset';
|
||||
} else {
|
||||
skipFirstEpisode.style.display = 'none';
|
||||
autoSkipNotificationText.style.display = 'none';
|
||||
@ -633,8 +634,8 @@
|
||||
showAdjustment.style.display = 'none';
|
||||
hideAdjustment.style.display = 'none';
|
||||
} else {
|
||||
showAdjustment.style.display = 'block';
|
||||
hideAdjustment.style.display = 'block';
|
||||
showAdjustment.style.display = 'unset';
|
||||
hideAdjustment.style.display = 'unset';
|
||||
}
|
||||
}
|
||||
|
||||
@ -716,7 +717,7 @@
|
||||
|
||||
clearSelect(selectEpisode1);
|
||||
clearSelect(selectEpisode2);
|
||||
btnSeasonEraseTimestamps.style.display = "block";
|
||||
btnSeasonEraseTimestamps.style.display = "unset";
|
||||
|
||||
let i = 1;
|
||||
for (let episode of episodes) {
|
||||
@ -742,20 +743,29 @@
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
timestampError.value = "";
|
||||
canvas.style.display = "none";
|
||||
|
||||
lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint");
|
||||
rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint");
|
||||
|
||||
if (lhs === undefined) {
|
||||
if (lhs === null) {
|
||||
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints missing!\n";
|
||||
}
|
||||
else if (lhs === undefined) {
|
||||
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints failed!\n";
|
||||
}
|
||||
|
||||
if (rhs === null) {
|
||||
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints missing!\n";
|
||||
}
|
||||
if (rhs === undefined) {
|
||||
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!";
|
||||
}
|
||||
|
||||
if (timestampError.value == "") {
|
||||
timestampError.style.display = "none";
|
||||
} else {
|
||||
timestampError.style.display = "block";
|
||||
timestampError.style.display = "unset";
|
||||
}
|
||||
|
||||
Dashboard.hideLoadingMsg();
|
||||
@ -785,7 +795,7 @@
|
||||
}
|
||||
|
||||
// Update the editor for the first and second episodes
|
||||
document.querySelector("#timestampEditor").style.display = "unset";
|
||||
timestampEditor.style.display = "unset";
|
||||
document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text;
|
||||
document.querySelector("#editLeftEpisodeStart").value = Math.round(leftEpisodeIntro.IntroStart);
|
||||
document.querySelector("#editLeftEpisodeEnd").value = Math.round(leftEpisodeIntro.IntroEnd);
|
||||
@ -805,6 +815,9 @@
|
||||
function clearSelect(select) {
|
||||
timestampError.value = "";
|
||||
timestampError.style.display = "none";
|
||||
timestampEditor.style.display = "none";
|
||||
timeContainer.style.display = "none";
|
||||
canvas.style.display = "none";
|
||||
let i, L = select.options.length - 1;
|
||||
for (i = L; i >= 0; i--) {
|
||||
select.remove(i);
|
||||
@ -842,12 +855,16 @@
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
// if the control key is pressed, shift LHS by 10s. Otherwise, shift by 1.
|
||||
offsetDelta = e.ctrlKey ? 10 / 0.128 : 1;
|
||||
if (timestampError.value != "") {
|
||||
// if the control key is pressed, shift LHS by 10s. Otherwise, shift by 1.
|
||||
offsetDelta = e.ctrlKey ? 10 / 0.128 : 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
offsetDelta = e.ctrlKey ? -10 / 0.128 : -1;
|
||||
if (timestampError.value != "") {
|
||||
offsetDelta = e.ctrlKey ? -10 / 0.128 : -1;
|
||||
}
|
||||
break;
|
||||
|
||||
case "ArrowRight":
|
||||
|
@ -169,6 +169,8 @@ function paintFingerprintDiff(canvas, fp1, fp2, offset) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.style.display = "unset";
|
||||
|
||||
let leftOffset = 0, rightOffset = 0;
|
||||
if (offset < 0) {
|
||||
leftOffset -= offset;
|
||||
|
@ -2,8 +2,8 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>ConfusedPolarBear.Plugin.IntroSkipper</RootNamespace>
|
||||
<AssemblyVersion>0.1.16.0</AssemblyVersion>
|
||||
<FileVersion>0.1.16.0</FileVersion>
|
||||
<AssemblyVersion>0.1.16.1</AssemblyVersion>
|
||||
<FileVersion>0.1.16.1</FileVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
|
@ -605,6 +605,25 @@ public static class FFmpegWrapper
|
||||
Encoding.UTF8).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a cached episode fingerprint from disk.
|
||||
/// </summary>
|
||||
/// <param name="episodeId">Episode to remove from cache.</param>
|
||||
/// <param name="mode">Analysis mode.</param>
|
||||
public static void DeleteEpisodeCache(string episodeId, AnalysisMode mode)
|
||||
{
|
||||
var cachePath = Path.Join(
|
||||
Plugin.Instance!.FingerprintCachePath,
|
||||
episodeId);
|
||||
|
||||
if (mode == AnalysisMode.Credits)
|
||||
{
|
||||
cachePath += "-credits";
|
||||
}
|
||||
|
||||
File.Delete(cachePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the path an episode should be cached at.
|
||||
/// This function was created before the unified caching mechanism was introduced (in v0.1.7).
|
||||
|
@ -8,6 +8,14 @@
|
||||
"category": "General",
|
||||
"imageUrl": "https://raw.githubusercontent.com/jumoog/intro-skipper/master/images/logo.png",
|
||||
"versions": [
|
||||
{
|
||||
"version": "0.1.16.1",
|
||||
"changelog": "- See the full changelog at [GitHub](https://github.com/jumoog/intro-skipper/blob/master/CHANGELOG.md)\n",
|
||||
"targetAbi": "10.8.4.0",
|
||||
"sourceUrl": "https://github.com/jumoog/intro-skipper/releases/download/v0.1.16.1/intro-skipper-v0.1.16.1.zip",
|
||||
"checksum": "d8e5370f974bd5624206f87b3fed05bb",
|
||||
"timestamp": "2024-03-06T10:17:25Z"
|
||||
},
|
||||
{
|
||||
"version": "0.1.16.0",
|
||||
"changelog": "- See the full changelog at [GitHub](https://github.com/jumoog/intro-skipper/blob/master/CHANGELOG.md)\n",
|
||||
|
Loading…
x
Reference in New Issue
Block a user