Allow caching fpcalc results
Probably only useful during development, when the same files are being fingerprinted repeatedly
This commit is contained in:
parent
a444150e0b
commit
1c55d749a3
@ -16,38 +16,10 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public PluginConfiguration()
|
public PluginConfiguration()
|
||||||
{
|
{
|
||||||
AnalysisResults = new Collection<Intro>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save timestamps to disk.
|
/// If the output of fpcalc should be cached to the filesystem.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SaveTimestamps()
|
public bool CacheFingerprints { get; set; }
|
||||||
{
|
|
||||||
AnalysisResults.Clear();
|
|
||||||
|
|
||||||
foreach (var intro in Plugin.Instance!.Intros)
|
|
||||||
{
|
|
||||||
AnalysisResults.Add(intro.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Plugin.Instance!.SaveConfiguration();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Restore previous analysis results from disk.
|
|
||||||
/// </summary>
|
|
||||||
public void RestoreTimestamps()
|
|
||||||
{
|
|
||||||
// Since dictionaries can't be easily serialized, analysis results are stored on disk as a list.
|
|
||||||
foreach (var intro in AnalysisResults)
|
|
||||||
{
|
|
||||||
Plugin.Instance!.Intros[intro.EpisodeId] = intro;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Previous analysis results.
|
|
||||||
/// </summary>
|
|
||||||
public Collection<Intro> AnalysisResults { get; private set; }
|
|
||||||
}
|
}
|
||||||
|
@ -8,29 +8,17 @@
|
|||||||
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
||||||
<div data-role="content">
|
<div data-role="content">
|
||||||
<div class="content-primary">
|
<div class="content-primary">
|
||||||
<form id="TemplateConfigForm">
|
<form id="FingerprintConfigForm">
|
||||||
<div class="selectContainer">
|
|
||||||
<label class="selectLabel" for="Options">Several Options</label>
|
|
||||||
<select is="emby-select" id="Options" name="Options" class="emby-select-withcolor emby-select">
|
|
||||||
<option id="optOneOption" value="OneOption">One Option</option>
|
|
||||||
<option id="optAnotherOption" value="AnotherOption">Another Option</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="AnInteger">An Integer</label>
|
|
||||||
<input id="AnInteger" name="AnInteger" type="number" is="emby-input" min="0" />
|
|
||||||
<div class="fieldDescription">A Description</div>
|
|
||||||
</div>
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="TrueFalseSetting" name="TrueFalseCheckBox" type="checkbox" is="emby-checkbox" />
|
<input id="CacheFingerprints" type="checkbox" is="emby-checkbox" />
|
||||||
<span>A Checkbox</span>
|
<span>Cache fingerprints to disk</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<div class="fieldDescription">
|
||||||
|
If checked, will store the fingerprints for all subsequently scanned files to disk.
|
||||||
|
Caching fingerprints avoids having to re-run fpcalc on each file, at the expense of disk usage.
|
||||||
</div>
|
</div>
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputeLabel inputLabelUnfocused" for="AString">A String</label>
|
|
||||||
<input id="AString" name="AString" type="text" is="emby-input" />
|
|
||||||
<div class="fieldDescription">Another Description</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||||
@ -42,29 +30,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var TemplateConfig = {
|
var TemplateConfig = {
|
||||||
pluginUniqueId: 'eb5d7894-8eef-4b36-aa6f-5d124e828ce1'
|
pluginUniqueId: 'c83d86bb-a1e0-4c35-a113-e2101cf4ee6b'
|
||||||
};
|
};
|
||||||
|
|
||||||
document.querySelector('#TemplateConfigPage')
|
document.querySelector('#TemplateConfigPage')
|
||||||
.addEventListener('pageshow', function() {
|
.addEventListener('pageshow', function() {
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||||
document.querySelector('#Options').value = config.Options;
|
document.querySelector('#CacheFingerprints').checked = config.CacheFingerprints;
|
||||||
document.querySelector('#AnInteger').value = config.AnInteger;
|
|
||||||
document.querySelector('#TrueFalseSetting').checked = config.TrueFalseSetting;
|
|
||||||
document.querySelector('#AString').value = config.AString;
|
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector('#TemplateConfigForm')
|
document.querySelector('#FingerprintConfigForm')
|
||||||
.addEventListener('submit', function() {
|
.addEventListener('submit', function() {
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||||
config.Options = document.querySelector('#Options').value;
|
config.CacheFingerprints = document.querySelector('#CacheFingerprints').checked;
|
||||||
config.AnInteger = document.querySelector('#AnInteger').value;
|
|
||||||
config.TrueFalseSetting = document.querySelector('#TrueFalseSetting').checked;
|
|
||||||
config.AString = document.querySelector('#AString').value;
|
|
||||||
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
|
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
|
||||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
||||||
@ -39,6 +41,13 @@ public static class FPCalc {
|
|||||||
/// <param name="episode">Queued episode to fingerprint.</param>
|
/// <param name="episode">Queued episode to fingerprint.</param>
|
||||||
public static ReadOnlyCollection<uint> Fingerprint(QueuedEpisode episode)
|
public static ReadOnlyCollection<uint> Fingerprint(QueuedEpisode episode)
|
||||||
{
|
{
|
||||||
|
// Try to load this episode from cache before running fpcalc.
|
||||||
|
if (loadCachedFingerprint(episode, out ReadOnlyCollection<uint> cachedFingerprint))
|
||||||
|
{
|
||||||
|
Logger?.LogDebug("Fingerprint cache hit on {File}", episode.Path);
|
||||||
|
return cachedFingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
Logger?.LogDebug("Fingerprinting {Duration} seconds from {File}", episode.FingerprintDuration, episode.Path);
|
Logger?.LogDebug("Fingerprinting {Duration} seconds from {File}", episode.FingerprintDuration, episode.Path);
|
||||||
|
|
||||||
// FIXME: revisit escaping
|
// FIXME: revisit escaping
|
||||||
@ -69,9 +78,17 @@ public static class FPCalc {
|
|||||||
results.Add(Convert.ToUInt32(rawNumber, CultureInfo.InvariantCulture));
|
results.Add(Convert.ToUInt32(rawNumber, CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to cache this fingerprint.
|
||||||
|
cacheFingerprint(episode, results);
|
||||||
|
|
||||||
return results.AsReadOnly();
|
return results.AsReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs fpcalc and returns standard output.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Arguments to pass to fpcalc.</param>
|
||||||
|
/// <param name="timeout">Timeout (in seconds) to wait for fpcalc to exit.</param>
|
||||||
private static string getOutput(string args, int timeout = 60 * 1000)
|
private static string getOutput(string args, int timeout = 60 * 1000)
|
||||||
{
|
{
|
||||||
var info = new ProcessStartInfo("fpcalc", args);
|
var info = new ProcessStartInfo("fpcalc", args);
|
||||||
@ -86,4 +103,76 @@ public static class FPCalc {
|
|||||||
|
|
||||||
return fpcalc.StandardOutput.ReadToEnd();
|
return fpcalc.StandardOutput.ReadToEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to load an episode's fingerprint from cache. If caching is not enabled, calling this function is a no-op.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="episode">Episode to try to load from cache.</param>
|
||||||
|
/// <param name="fingerprint">ReadOnlyCollection to store the fingerprint in.</param>
|
||||||
|
/// <returns>true if the episode was successfully loaded from cache, false on any other error.</returns>
|
||||||
|
private static bool loadCachedFingerprint(QueuedEpisode episode, out ReadOnlyCollection<uint> fingerprint)
|
||||||
|
{
|
||||||
|
fingerprint = new List<uint>().AsReadOnly();
|
||||||
|
|
||||||
|
// If fingerprint caching isn't enabled, don't try to load anything.
|
||||||
|
if (!Plugin.Instance!.Configuration.CacheFingerprints)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = getFingerprintCachePath(episode);
|
||||||
|
|
||||||
|
// If this episode isn't cached, bail out.
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make async
|
||||||
|
var raw = File.ReadAllLines(path, Encoding.UTF8);
|
||||||
|
var result = new List<uint>();
|
||||||
|
|
||||||
|
// Read each stringified uint.
|
||||||
|
result.EnsureCapacity(raw.Length);
|
||||||
|
foreach (var rawNumber in raw)
|
||||||
|
{
|
||||||
|
result.Add(Convert.ToUInt32(rawNumber, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
fingerprint = result.AsReadOnly();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cache an episode's fingerprint to disk. If caching is not enabled, calling this function is a no-op.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="episode">Episode to store in cache.</param>
|
||||||
|
/// <param name="fingerprint">Fingerprint of the episode to store.</param>
|
||||||
|
private static void cacheFingerprint(QueuedEpisode episode, List<uint> fingerprint)
|
||||||
|
{
|
||||||
|
// Bail out if caching isn't enabled.
|
||||||
|
if (!Plugin.Instance!.Configuration.CacheFingerprints)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stringify each data point.
|
||||||
|
var lines = new List<string>();
|
||||||
|
foreach (var number in fingerprint)
|
||||||
|
{
|
||||||
|
lines.Add(number.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the episode.
|
||||||
|
File.WriteAllLinesAsync(getFingerprintCachePath(episode), lines, Encoding.UTF8).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the path an episode should be cached at.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="episode">Episode.</param>
|
||||||
|
private static string getFingerprintCachePath(QueuedEpisode episode)
|
||||||
|
{
|
||||||
|
return Path.Join(Plugin.Instance!.FingerprintCachePath, episode.EpisodeId.ToString("N"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
@ -14,6 +15,9 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||||
{
|
{
|
||||||
|
private IXmlSerializer _xmlSerializer;
|
||||||
|
private string _introPath;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -22,11 +26,69 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||||
: base(applicationPaths, xmlSerializer)
|
: base(applicationPaths, xmlSerializer)
|
||||||
{
|
{
|
||||||
|
_xmlSerializer = xmlSerializer;
|
||||||
|
|
||||||
|
// Create the base & cache directories (if needed).
|
||||||
|
FingerprintCachePath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "cache");
|
||||||
|
if (!Directory.Exists(FingerprintCachePath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(FingerprintCachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
_introPath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "intros.xml");
|
||||||
|
|
||||||
Intros = new Dictionary<Guid, Intro>();
|
Intros = new Dictionary<Guid, Intro>();
|
||||||
AnalysisQueue = new Dictionary<Guid, List<QueuedEpisode>>();
|
AnalysisQueue = new Dictionary<Guid, List<QueuedEpisode>>();
|
||||||
Instance = this;
|
Instance = this;
|
||||||
|
|
||||||
Configuration.RestoreTimestamps();
|
RestoreTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save timestamps to disk.
|
||||||
|
/// </summary>
|
||||||
|
public void SaveTimestamps()
|
||||||
|
{
|
||||||
|
var introList = new List<Intro>();
|
||||||
|
|
||||||
|
foreach (var intro in Plugin.Instance!.Intros)
|
||||||
|
{
|
||||||
|
introList.Add(intro.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_xmlSerializer.SerializeToFile(introList, _introPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restore previous analysis results from disk.
|
||||||
|
/// </summary>
|
||||||
|
public void RestoreTimestamps()
|
||||||
|
{
|
||||||
|
if (!File.Exists(_introPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since dictionaries can't be easily serialized, analysis results are stored on disk as a list.
|
||||||
|
var introList = (List<Intro>)_xmlSerializer.DeserializeFromFile(typeof(List<Intro>), _introPath);
|
||||||
|
|
||||||
|
foreach (var intro in introList)
|
||||||
|
{
|
||||||
|
Plugin.Instance!.Intros[intro.EpisodeId] = intro;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<PluginPageInfo> GetPages()
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
new PluginPageInfo
|
||||||
|
{
|
||||||
|
Name = this.Name,
|
||||||
|
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace)
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -44,6 +106,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int TotalQueued { get; set; }
|
public int TotalQueued { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Directory to cache fingerprints in.
|
||||||
|
/// </summary>
|
||||||
|
public string FingerprintCachePath { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Name => "Intro Skipper";
|
public override string Name => "Intro Skipper";
|
||||||
|
|
||||||
@ -52,17 +119,4 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public static Plugin? Instance { get; private set; }
|
public static Plugin? Instance { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IEnumerable<PluginPageInfo> GetPages()
|
|
||||||
{
|
|
||||||
return new[]
|
|
||||||
{
|
|
||||||
new PluginPageInfo
|
|
||||||
{
|
|
||||||
Name = this.Name,
|
|
||||||
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,7 @@ public class FingerprinterTask : IScheduledTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Instance!.Configuration.SaveTimestamps();
|
Plugin.Instance!.SaveTimestamps();
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user