commit
3559c7b7d3
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
|||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4.3.1
|
uses: actions/upload-artifact@v4.3.1
|
||||||
with:
|
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
|
path: ConfusedPolarBear.Plugin.IntroSkipper/bin/Debug/net6.0/ConfusedPolarBear.Plugin.IntroSkipper.dll
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,4 +8,3 @@ docker/dist
|
|||||||
|
|
||||||
# Visual Studio
|
# Visual Studio
|
||||||
.vs/
|
.vs/
|
||||||
UpgradeLog*.htm
|
|
||||||
|
@ -96,6 +96,11 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
|
|||||||
config.MaximumIntroDuration :
|
config.MaximumIntroDuration :
|
||||||
config.MaximumEpisodeCreditsDuration;
|
config.MaximumEpisodeCreditsDuration;
|
||||||
|
|
||||||
|
if (chapters.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (mode == AnalysisMode.Credits)
|
if (mode == AnalysisMode.Credits)
|
||||||
{
|
{
|
||||||
// Since the ending credits chapter may be the last chapter in the file, append a virtual
|
// 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
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
// Check all chapters
|
|
||||||
for (int i = 0; i < chapters.Count - 1; i++)
|
|
||||||
{
|
{
|
||||||
var current = chapters[i];
|
// Check all chapters
|
||||||
var next = chapters[i + 1];
|
for (int i = 0; i < chapters.Count - 1; i++)
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(current.Name))
|
|
||||||
{
|
{
|
||||||
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;
|
return matchingChapter;
|
||||||
|
@ -103,7 +103,7 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// Gets or sets the regular expression used to detect ending credit chapters.
|
/// Gets or sets the regular expression used to detect ending credit chapters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ChapterAnalyzerEndCreditsPattern { get; set; } =
|
public string ChapterAnalyzerEndCreditsPattern { get; set; } =
|
||||||
@"(^|\s)(Credits?|Ending)(\s|$)";
|
@"(^|\s)(Credits?|ED|Ending)(\s|$)";
|
||||||
|
|
||||||
// ===== Playback settings =====
|
// ===== Playback settings =====
|
||||||
|
|
||||||
|
@ -454,33 +454,17 @@
|
|||||||
<input style="width:4em" type="number" min="0" id="editRightEpisodeStart"> to
|
<input style="width:4em" type="number" min="0" id="editRightEpisodeStart"> to
|
||||||
<input style="width:4em;margin-bottom:10px" type="number" min="0" id="editRightEpisodeEnd">
|
<input style="width:4em;margin-bottom:10px" type="number" min="0" id="editRightEpisodeEnd">
|
||||||
<br />
|
<br />
|
||||||
|
<br />
|
||||||
|
|
||||||
<button id="btnUpdateTimestamps" type="button">
|
<button id="btnUpdateTimestamps" type="button">
|
||||||
Update timestamps
|
Update timestamps
|
||||||
</button>
|
</button>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<textarea id="timestampError" style="display:none;" rows="2" cols="75" readonly></textarea>
|
<textarea id="timestampError" style="display:none;" rows="2" cols="75" readonly></textarea>
|
||||||
<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>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<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 />
|
||||||
@ -526,11 +510,27 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<canvas id="troubleshooter"></canvas>
|
<canvas id="troubleshooter" style="display:none;"></canvas>
|
||||||
<span id="timestampContainer">
|
<span id="timestampContainer">
|
||||||
<span id="timestamps"></span> <br />
|
<span id="timestamps"></span> <br />
|
||||||
<span id="intros"></span>
|
<span id="intros"></span>
|
||||||
</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>
|
</details>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
@ -602,6 +602,7 @@
|
|||||||
var txtSuggested = document.querySelector("span#suggestedShifts");
|
var txtSuggested = document.querySelector("span#suggestedShifts");
|
||||||
var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
|
var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
|
||||||
var timestampError = document.querySelector("textarea#timestampError");
|
var timestampError = document.querySelector("textarea#timestampError");
|
||||||
|
var timestampEditor = document.querySelector("#timestampEditor");
|
||||||
var btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps");
|
var btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps");
|
||||||
var timeContainer = document.querySelector("span#timestampContainer");
|
var timeContainer = document.querySelector("span#timestampContainer");
|
||||||
|
|
||||||
@ -613,8 +614,8 @@
|
|||||||
|
|
||||||
async function autoSkipChanged() {
|
async function autoSkipChanged() {
|
||||||
if (autoSkip.checked) {
|
if (autoSkip.checked) {
|
||||||
skipFirstEpisode.style.display = 'block';
|
skipFirstEpisode.style.display = 'unset';
|
||||||
autoSkipNotificationText.style.display = 'block';
|
autoSkipNotificationText.style.display = 'unset';
|
||||||
} else {
|
} else {
|
||||||
skipFirstEpisode.style.display = 'none';
|
skipFirstEpisode.style.display = 'none';
|
||||||
autoSkipNotificationText.style.display = 'none';
|
autoSkipNotificationText.style.display = 'none';
|
||||||
@ -633,8 +634,8 @@
|
|||||||
showAdjustment.style.display = 'none';
|
showAdjustment.style.display = 'none';
|
||||||
hideAdjustment.style.display = 'none';
|
hideAdjustment.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
showAdjustment.style.display = 'block';
|
showAdjustment.style.display = 'unset';
|
||||||
hideAdjustment.style.display = 'block';
|
hideAdjustment.style.display = 'unset';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -716,7 +717,7 @@
|
|||||||
|
|
||||||
clearSelect(selectEpisode1);
|
clearSelect(selectEpisode1);
|
||||||
clearSelect(selectEpisode2);
|
clearSelect(selectEpisode2);
|
||||||
btnSeasonEraseTimestamps.style.display = "block";
|
btnSeasonEraseTimestamps.style.display = "unset";
|
||||||
|
|
||||||
let i = 1;
|
let i = 1;
|
||||||
for (let episode of episodes) {
|
for (let episode of episodes) {
|
||||||
@ -742,20 +743,29 @@
|
|||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
|
|
||||||
timestampError.value = "";
|
timestampError.value = "";
|
||||||
|
canvas.style.display = "none";
|
||||||
|
|
||||||
lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint");
|
lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint");
|
||||||
rhs = await getJson("Intros/Episode/" + selectEpisode2.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";
|
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints failed!\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rhs === null) {
|
||||||
|
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints missing!\n";
|
||||||
|
}
|
||||||
if (rhs === undefined) {
|
if (rhs === undefined) {
|
||||||
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!";
|
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timestampError.value == "") {
|
if (timestampError.value == "") {
|
||||||
timestampError.style.display = "none";
|
timestampError.style.display = "none";
|
||||||
} else {
|
} else {
|
||||||
timestampError.style.display = "block";
|
timestampError.style.display = "unset";
|
||||||
}
|
}
|
||||||
|
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
@ -785,7 +795,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the editor for the first and second episodes
|
// 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("#editLeftEpisodeTitle").textContent = leftEpisode.text;
|
||||||
document.querySelector("#editLeftEpisodeStart").value = Math.round(leftEpisodeIntro.IntroStart);
|
document.querySelector("#editLeftEpisodeStart").value = Math.round(leftEpisodeIntro.IntroStart);
|
||||||
document.querySelector("#editLeftEpisodeEnd").value = Math.round(leftEpisodeIntro.IntroEnd);
|
document.querySelector("#editLeftEpisodeEnd").value = Math.round(leftEpisodeIntro.IntroEnd);
|
||||||
@ -805,6 +815,9 @@
|
|||||||
function clearSelect(select) {
|
function clearSelect(select) {
|
||||||
timestampError.value = "";
|
timestampError.value = "";
|
||||||
timestampError.style.display = "none";
|
timestampError.style.display = "none";
|
||||||
|
timestampEditor.style.display = "none";
|
||||||
|
timeContainer.style.display = "none";
|
||||||
|
canvas.style.display = "none";
|
||||||
let i, L = select.options.length - 1;
|
let i, L = select.options.length - 1;
|
||||||
for (i = L; i >= 0; i--) {
|
for (i = L; i >= 0; i--) {
|
||||||
select.remove(i);
|
select.remove(i);
|
||||||
@ -842,12 +855,16 @@
|
|||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
// if the control key is pressed, shift LHS by 10s. Otherwise, shift by 1.
|
if (timestampError.value != "") {
|
||||||
offsetDelta = e.ctrlKey ? 10 / 0.128 : 1;
|
// if the control key is pressed, shift LHS by 10s. Otherwise, shift by 1.
|
||||||
|
offsetDelta = e.ctrlKey ? 10 / 0.128 : 1;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
offsetDelta = e.ctrlKey ? -10 / 0.128 : -1;
|
if (timestampError.value != "") {
|
||||||
|
offsetDelta = e.ctrlKey ? -10 / 0.128 : -1;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
|
@ -169,6 +169,8 @@ function paintFingerprintDiff(canvas, fp1, fp2, offset) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas.style.display = "unset";
|
||||||
|
|
||||||
let leftOffset = 0, rightOffset = 0;
|
let leftOffset = 0, rightOffset = 0;
|
||||||
if (offset < 0) {
|
if (offset < 0) {
|
||||||
leftOffset -= offset;
|
leftOffset -= offset;
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<RootNamespace>ConfusedPolarBear.Plugin.IntroSkipper</RootNamespace>
|
<RootNamespace>ConfusedPolarBear.Plugin.IntroSkipper</RootNamespace>
|
||||||
<AssemblyVersion>0.1.16.0</AssemblyVersion>
|
<AssemblyVersion>0.1.16.1</AssemblyVersion>
|
||||||
<FileVersion>0.1.16.0</FileVersion>
|
<FileVersion>0.1.16.1</FileVersion>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
@ -605,6 +605,25 @@ public static class FFmpegWrapper
|
|||||||
Encoding.UTF8).ConfigureAwait(false);
|
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>
|
/// <summary>
|
||||||
/// Determines the path an episode should be cached at.
|
/// Determines the path an episode should be cached at.
|
||||||
/// This function was created before the unified caching mechanism was introduced (in v0.1.7).
|
/// This function was created before the unified caching mechanism was introduced (in v0.1.7).
|
||||||
|
@ -8,6 +8,14 @@
|
|||||||
"category": "General",
|
"category": "General",
|
||||||
"imageUrl": "https://raw.githubusercontent.com/jumoog/intro-skipper/master/images/logo.png",
|
"imageUrl": "https://raw.githubusercontent.com/jumoog/intro-skipper/master/images/logo.png",
|
||||||
"versions": [
|
"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",
|
"version": "0.1.16.0",
|
||||||
"changelog": "- See the full changelog at [GitHub](https://github.com/jumoog/intro-skipper/blob/master/CHANGELOG.md)\n",
|
"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