Add initial support for writing EDL files
This commit is contained in:
parent
cf1dc66970
commit
9dbe684790
@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## v0.1.6.0
|
||||
## v0.1.6.0 (unreleased)
|
||||
* Write EDL files with intro timestamps
|
||||
* Restore per season status updates
|
||||
|
||||
|
44
ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestEdl.cs
Normal file
44
ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestEdl.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
|
||||
|
||||
public class TestEdl
|
||||
{
|
||||
// Test data is from https://kodi.wiki/view/Edit_decision_list#MPlayer_EDL
|
||||
[Theory]
|
||||
[InlineData(5.3, 7.1, EdlAction.Cut, "5.3 7.1 0")]
|
||||
[InlineData(15, 16.7, EdlAction.Mute, "15 16.7 1")]
|
||||
[InlineData(420, 822, EdlAction.CommercialBreak, "420 822 3")]
|
||||
[InlineData(1, 255.3, EdlAction.SceneMarker, "1 255.3 2")]
|
||||
[InlineData(1.123456789, 5.654647987, EdlAction.CommercialBreak, "1.12 5.65 3")]
|
||||
public void TestEdlSerialization(double start, double end, EdlAction action, string expected)
|
||||
{
|
||||
var intro = MakeIntro(start, end);
|
||||
var actual = intro.ToEdl(action);
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestEdlInvalidSerialization()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => {
|
||||
var intro = MakeIntro(0, 5);
|
||||
intro.ToEdl(EdlAction.None);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Death Note - S01E12 - Love.mkv", "Death Note - S01E12 - Love.edl")]
|
||||
[InlineData("/full/path/to/file.rm", "/full/path/to/file.edl")]
|
||||
public void TestEdlPath(string mediaPath, string edlPath)
|
||||
{
|
||||
Assert.Equal(edlPath, EdlManager.GetEdlPath(mediaPath));
|
||||
}
|
||||
|
||||
private Intro MakeIntro(double start, double end)
|
||||
{
|
||||
return new Intro(Guid.Empty, new TimeRange(start, end));
|
||||
}
|
||||
}
|
@ -14,6 +14,8 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
{
|
||||
}
|
||||
|
||||
// ===== Analysis settings =====
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the episode's fingerprint should be cached to the filesystem.
|
||||
/// </summary>
|
||||
@ -24,6 +26,13 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// </summary>
|
||||
public int MaxParallelism { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the action to write to created EDL files.
|
||||
/// </summary>
|
||||
public EdlAction EdlAction { get; set; } = EdlAction.None;
|
||||
|
||||
// ===== Playback settings =====
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether introductions should be automatically skipped.
|
||||
/// </summary>
|
||||
|
@ -37,6 +37,45 @@
|
||||
Maximum degree of parallelism to use when analyzing episodes.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<label class="selectLabel" for="Options">EDL Action</label>
|
||||
<select is="emby-select" id="EdlAction" class="emby-select-withcolor emby-select">
|
||||
<option value="None">
|
||||
None (do not create or modify EDL files)
|
||||
</option>
|
||||
|
||||
<option value="CommercialBreak">
|
||||
Commercial Break (recommended, skips past the intro once)
|
||||
</option>
|
||||
|
||||
<option value="Cut">
|
||||
Cut (player will remove the intro from the video)
|
||||
</option>
|
||||
|
||||
<option value="Intro">
|
||||
Intro (show a skip button, *experimental*)
|
||||
</option>
|
||||
|
||||
<option value="Mute">
|
||||
Mute (audio will be muted)
|
||||
</option>
|
||||
|
||||
<option value="SceneMarker">
|
||||
Scene Marker (create a chapter marker)
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="fieldDescription">
|
||||
If set to a value other than None, specifies which action to write to
|
||||
<a href="https://kodi.wiki/view/Edit_decision_list">MPlayer compatible EDL files</a>
|
||||
alongside your episode files. <br />
|
||||
<strong>
|
||||
If this value is changed from None, the plugin will overwrite any EDL files
|
||||
associated with your episode files.
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="verticalSection-extrabottompadding">
|
||||
@ -438,6 +477,7 @@
|
||||
ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) {
|
||||
document.querySelector('#AutoSkip').checked = config.AutoSkip;
|
||||
document.querySelector('#MaxParallelism').value = config.MaxParallelism;
|
||||
document.querySelector('#EdlAction').value = config.EdlAction;
|
||||
|
||||
document.querySelector('#CacheFingerprints').checked = config.CacheFingerprints;
|
||||
document.querySelector('#ShowPromptAdjustment').value = config.ShowPromptAdjustment;
|
||||
@ -453,6 +493,7 @@
|
||||
ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) {
|
||||
config.AutoSkip = document.querySelector('#AutoSkip').checked;
|
||||
config.MaxParallelism = document.querySelector('#MaxParallelism').value;
|
||||
config.EdlAction = document.querySelector('#EdlAction').value;
|
||||
|
||||
config.CacheFingerprints = document.querySelector('#CacheFingerprints').checked;
|
||||
config.ShowPromptAdjustment = document.querySelector("#ShowPromptAdjustment").value;
|
||||
|
37
ConfusedPolarBear.Plugin.IntroSkipper/Data/EdlAction.cs
Normal file
37
ConfusedPolarBear.Plugin.IntroSkipper/Data/EdlAction.cs
Normal file
@ -0,0 +1,37 @@
|
||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
||||
|
||||
/// <summary>
|
||||
/// Taken from https://kodi.wiki/view/Edit_decision_list#MPlayer_EDL.
|
||||
/// </summary>
|
||||
public enum EdlAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Do not create EDL files.
|
||||
/// </summary>
|
||||
None = -1,
|
||||
|
||||
/// <summary>
|
||||
/// Completely remove the intro from playback as if it was never in the original video.
|
||||
/// </summary>
|
||||
Cut,
|
||||
|
||||
/// <summary>
|
||||
/// Mute audio, continue playback.
|
||||
/// </summary>
|
||||
Mute,
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a new scene marker.
|
||||
/// </summary>
|
||||
SceneMarker,
|
||||
|
||||
/// <summary>
|
||||
/// Automatically skip the intro once during playback.
|
||||
/// </summary>
|
||||
CommercialBreak,
|
||||
|
||||
/// <summary>
|
||||
/// Show a skip button.
|
||||
/// </summary>
|
||||
Intro,
|
||||
}
|
@ -68,4 +68,22 @@ public class Intro
|
||||
/// Gets or sets the recommended time to hide the skip intro prompt.
|
||||
/// </summary>
|
||||
public double HideSkipPromptAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert this Intro object to a Kodi compatible EDL entry.
|
||||
/// </summary>
|
||||
/// <param name="action">User specified configuration EDL action.</param>
|
||||
/// <returns>String.</returns>
|
||||
public string ToEdl(EdlAction action)
|
||||
{
|
||||
if (action == EdlAction.None)
|
||||
{
|
||||
throw new ArgumentException("Cannot serialize an EdlAction of None");
|
||||
}
|
||||
|
||||
var start = Math.Round(IntroStart, 2);
|
||||
var end = Math.Round(IntroEnd, 2);
|
||||
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0} {1} {2}", start, end, (int)action);
|
||||
}
|
||||
}
|
||||
|
59
ConfusedPolarBear.Plugin.IntroSkipper/EdlManager.cs
Normal file
59
ConfusedPolarBear.Plugin.IntroSkipper/EdlManager.cs
Normal file
@ -0,0 +1,59 @@
|
||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Update EDL files associated with a list of episodes.
|
||||
/// </summary>
|
||||
public static class EdlManager
|
||||
{
|
||||
private static ILogger? _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize EDLManager with a logger.
|
||||
/// </summary>
|
||||
/// <param name="logger">ILogger.</param>
|
||||
public static void Initialize(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the EDL action is set to a value other than None, update EDL files for the provided episodes.
|
||||
/// </summary>
|
||||
/// <param name="episodes">Episodes to update EDL files for.</param>
|
||||
public static void UpdateEDLFiles(ReadOnlyCollection<QueuedEpisode> episodes)
|
||||
{
|
||||
var action = Plugin.Instance!.Configuration.EdlAction;
|
||||
if (action == EdlAction.None)
|
||||
{
|
||||
_logger?.LogDebug("EDL action is set to none, not updating EDL files");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger?.LogDebug("Updating EDL files with action {Action}", action);
|
||||
|
||||
foreach (var episode in episodes)
|
||||
{
|
||||
var id = episode.EpisodeId;
|
||||
var intro = Plugin.Instance!.Intros[id];
|
||||
var edlPath = GetEdlPath(Plugin.Instance!.GetItemPath(id));
|
||||
|
||||
_logger?.LogTrace("Episode {Id} has EDL path {Path}", id, edlPath);
|
||||
|
||||
File.WriteAllText(edlPath, intro.ToEdl(action));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given the path to an episode, return the path to the associated EDL file.
|
||||
/// </summary>
|
||||
/// <param name="mediaPath">Full path to episode.</param>
|
||||
/// <returns>Full path to EDL file.</returns>
|
||||
public static string GetEdlPath(string mediaPath)
|
||||
{
|
||||
return Path.ChangeExtension(mediaPath, "edl");
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
@ -17,6 +18,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
{
|
||||
private IXmlSerializer _xmlSerializer;
|
||||
private ILibraryManager _libraryManager;
|
||||
private string _introPath;
|
||||
|
||||
/// <summary>
|
||||
@ -25,13 +27,16 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||
/// <param name="serverConfiguration">Server configuration manager.</param>
|
||||
/// <param name="libraryManager">Library manager.</param>
|
||||
public Plugin(
|
||||
IApplicationPaths applicationPaths,
|
||||
IXmlSerializer xmlSerializer,
|
||||
IServerConfigurationManager serverConfiguration)
|
||||
IServerConfigurationManager serverConfiguration,
|
||||
ILibraryManager libraryManager)
|
||||
: base(applicationPaths, xmlSerializer)
|
||||
{
|
||||
_xmlSerializer = xmlSerializer;
|
||||
_libraryManager = libraryManager;
|
||||
|
||||
// Create the base & cache directories (if needed).
|
||||
FingerprintCachePath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "cache");
|
||||
@ -129,6 +134,16 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full path for an item.
|
||||
/// </summary>
|
||||
/// <param name="id">Item id.</param>
|
||||
/// <returns>Full path to item.</returns>
|
||||
internal string GetItemPath(Guid id)
|
||||
{
|
||||
return _libraryManager.GetItemById(id).Path;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PluginPageInfo> GetPages()
|
||||
{
|
||||
|
@ -88,6 +88,8 @@ public class FingerprinterTask : IScheduledTask
|
||||
_queueLogger = loggerFactory.CreateLogger<QueueManager>();
|
||||
|
||||
_fingerprintCache = new Dictionary<Guid, ReadOnlyCollection<uint>>();
|
||||
|
||||
EdlManager.Initialize(_logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -146,6 +148,7 @@ public class FingerprinterTask : IScheduledTask
|
||||
Parallel.ForEach(queue, options, (season) =>
|
||||
{
|
||||
var first = season.Value[0];
|
||||
var writeEdl = false;
|
||||
|
||||
try
|
||||
{
|
||||
@ -153,6 +156,7 @@ public class FingerprinterTask : IScheduledTask
|
||||
// (instead of just using the number of episodes in the current season).
|
||||
var analyzed = AnalyzeSeason(season, cancellationToken);
|
||||
Interlocked.Add(ref totalProcessed, analyzed);
|
||||
writeEdl = analyzed > 0;
|
||||
}
|
||||
catch (FingerprintException ex)
|
||||
{
|
||||
@ -180,6 +184,12 @@ public class FingerprinterTask : IScheduledTask
|
||||
}
|
||||
}
|
||||
|
||||
if (writeEdl && Plugin.Instance!.Configuration.EdlAction != EdlAction.None)
|
||||
{
|
||||
EdlManager.UpdateEDLFiles(season.Value.AsReadOnly());
|
||||
}
|
||||
|
||||
totalProcessed += season.Value.Count;
|
||||
progress.Report((totalProcessed * 100) / Plugin.Instance!.TotalQueued);
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user