diff --git a/Jellyfin.Plugin.SubtitleExtract/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.SubtitleExtract/Configuration/PluginConfiguration.cs
index 13f1bd9..d3ffde6 100644
--- a/Jellyfin.Plugin.SubtitleExtract/Configuration/PluginConfiguration.cs
+++ b/Jellyfin.Plugin.SubtitleExtract/Configuration/PluginConfiguration.cs
@@ -15,7 +15,7 @@ public PluginConfiguration()
}
///
- /// Gets or sets a value indicating whether or not to extract subtitles as part of library scan.
+ /// Gets or sets a value indicating whether or not to extract subtitles and attachments as part of library scan.
/// default = false.
///
public bool ExtractionDuringLibraryScan { get; set; } = false;
diff --git a/Jellyfin.Plugin.SubtitleExtract/Configuration/configPage.html b/Jellyfin.Plugin.SubtitleExtract/Configuration/configPage.html
index 99843f7..0831581 100644
--- a/Jellyfin.Plugin.SubtitleExtract/Configuration/configPage.html
+++ b/Jellyfin.Plugin.SubtitleExtract/Configuration/configPage.html
@@ -15,9 +15,9 @@
- Extract subtitles during library scan
+ Extract subtitles and attachments during library scan
-
This will make sure subtitles are extracted sooner but will result in longer library scans. Does not disable the scheduled task.
+
This will make sure subtitles and attachments are extracted sooner but will result in longer library scans. Does not disable the scheduled task.
diff --git a/Jellyfin.Plugin.SubtitleExtract/Providers/AttachmentExtractionProvider.cs b/Jellyfin.Plugin.SubtitleExtract/Providers/AttachmentExtractionProvider.cs
new file mode 100644
index 0000000..7f0a299
--- /dev/null
+++ b/Jellyfin.Plugin.SubtitleExtract/Providers/AttachmentExtractionProvider.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Plugin.SubtitleExtract.Providers;
+
+///
+/// Extracts embedded attachments while library scanning for immediate access in web player.
+///
+public class AttachmentExtractionProvider : ICustomMetadataProvider,
+ ICustomMetadataProvider,
+ ICustomMetadataProvider,
+ IHasItemChangeMonitor,
+ IHasOrder,
+ IForcedProvider
+{
+ private readonly ILogger _logger;
+
+ private readonly IAttachmentExtractor _extractor;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// instance.
+ /// Instance of the interface.
+ public AttachmentExtractionProvider(
+ IAttachmentExtractor attachmentExtractor,
+ ILogger logger)
+ {
+ _logger = logger;
+ _extractor = attachmentExtractor;
+ }
+
+ ///
+ public string Name => "Attachment Extractor";
+
+ ///
+ /// Gets the order in which the provider should be called. (Core provider is = 100).
+ ///
+ public int Order => 1000;
+
+ ///
+ public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ {
+ if (item.IsFileProtocol)
+ {
+ var file = directoryService.GetFile(item.Path);
+ if (file != null && (item.DateModified != file.LastWriteTimeUtc || item.Size != file.Length))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ public Task FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchAttachments(item, cancellationToken);
+ }
+
+ ///
+ public Task FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchAttachments(item, cancellationToken);
+ }
+
+ ///
+ public Task FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchAttachments(item, cancellationToken);
+ }
+
+ private async Task FetchAttachments(BaseItem item, CancellationToken cancellationToken)
+ {
+ var config = SubtitleExtractPlugin.Current!.Configuration;
+
+ if (config.ExtractionDuringLibraryScan)
+ {
+ _logger.LogDebug("Extracting attachments for: {Video}", item.Path);
+ foreach (var mediaSource in item.GetMediaSources(false))
+ {
+ var streams = mediaSource.MediaStreams.Where(i => i.Type == MediaStreamType.Subtitle).ToList();
+ var mksStreams = streams.Where(i => !string.IsNullOrEmpty(i.Path) && i.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)).ToList();
+ var mksPaths = mksStreams.Select(i => i.Path).ToList();
+ if (mksPaths.Count > 0)
+ {
+ foreach (var path in mksPaths)
+ {
+ await _extractor.ExtractAllAttachments(path, mediaSource, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ if (streams.Count != mksStreams.Count)
+ {
+ await _extractor.ExtractAllAttachments(mediaSource.Path, mediaSource, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ _logger.LogDebug("Finished attachment extraction for: {Video}", item.Path);
+ }
+
+ return ItemUpdateType.None;
+ }
+}
diff --git a/Jellyfin.Plugin.SubtitleExtract/SubtitleExtractPlugin.cs b/Jellyfin.Plugin.SubtitleExtract/SubtitleExtractPlugin.cs
index f1a597b..f9ccc70 100644
--- a/Jellyfin.Plugin.SubtitleExtract/SubtitleExtractPlugin.cs
+++ b/Jellyfin.Plugin.SubtitleExtract/SubtitleExtractPlugin.cs
@@ -31,7 +31,7 @@ public SubtitleExtractPlugin(IApplicationPaths applicationPaths, IXmlSerializer
public override Guid Id => new("CD893C24-B59E-4060-87B2-184070E1BF68");
///
- public override string Description => "Extracts embedded subtitles";
+ public override string Description => "Extracts embedded subtitles and attachments";
///
/// Gets the current plugin instance.
@@ -44,7 +44,7 @@ public IEnumerable GetPages()
return [
new PluginPageInfo
{
- Name = "Jellyfin subtitle extractor",
+ Name = "Jellyfin subtitle and attachment extractor",
EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html"
}
];
diff --git a/Jellyfin.Plugin.SubtitleExtract/Tasks/ExtractAttachmentsTask.cs b/Jellyfin.Plugin.SubtitleExtract/Tasks/ExtractAttachmentsTask.cs
new file mode 100644
index 0000000..7e1be3d
--- /dev/null
+++ b/Jellyfin.Plugin.SubtitleExtract/Tasks/ExtractAttachmentsTask.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+
+namespace Jellyfin.Plugin.SubtitleExtract.Tasks;
+
+///
+/// Scheduled task to extract embedded attachments for immediate access in web player.
+///
+public class ExtractAttachmentsTask : IScheduledTask
+{
+ private const int QueryPageLimit = 250;
+
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+ private readonly IAttachmentExtractor _extractor;
+
+ private static readonly BaseItemKind[] _itemTypes = [BaseItemKind.Episode, BaseItemKind.Movie];
+ private static readonly MediaType[] _mediaTypes = [MediaType.Video];
+ private static readonly SourceType[] _sourceTypes = [SourceType.Library];
+ private static readonly DtoOptions _dtoOptions = new(false);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of interface.
+ /// instance.
+ /// Instance of interface.
+ public ExtractAttachmentsTask(
+ ILibraryManager libraryManager,
+ IAttachmentExtractor attachmentExtractor,
+ ILocalizationManager localization)
+ {
+ _libraryManager = libraryManager;
+ _localization = localization;
+ _extractor = attachmentExtractor;
+ }
+
+ ///
+ public string Key => "ExtractAttachments";
+
+ ///
+ public string Name => "Extract Attachments";
+
+ ///
+ public string Description => "Extracts embedded attachments.";
+
+ ///
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ ///
+ public IEnumerable GetDefaultTriggers()
+ {
+ return [];
+ }
+
+ ///
+ public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
+ {
+ var query = new InternalItemsQuery
+ {
+ Recursive = true,
+ HasSubtitles = true,
+ IsVirtualItem = false,
+ IncludeItemTypes = _itemTypes,
+ DtoOptions = _dtoOptions,
+ MediaTypes = _mediaTypes,
+ SourceTypes = _sourceTypes,
+ Limit = QueryPageLimit,
+ };
+
+ var numberOfVideos = _libraryManager.GetCount(query);
+
+ var startIndex = 0;
+ var completedVideos = 0;
+
+ while (startIndex < numberOfVideos)
+ {
+ query.StartIndex = startIndex;
+ var videos = _libraryManager.GetItemList(query);
+
+ foreach (var video in videos)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ foreach (var mediaSource in video.GetMediaSources(false))
+ {
+ var streams = mediaSource.MediaStreams.Where(i => i.Type == MediaStreamType.Subtitle).ToList();
+ var mksStreams = streams.Where(i => !string.IsNullOrEmpty(i.Path) && i.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)).ToList();
+ var mksPaths = mksStreams.Select(i => i.Path).ToList();
+ if (mksPaths.Count > 0)
+ {
+ foreach (var path in mksPaths)
+ {
+ await _extractor.ExtractAllAttachments(path, mediaSource, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ if (streams.Count != mksStreams.Count)
+ {
+ await _extractor.ExtractAllAttachments(mediaSource.Path, mediaSource, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ completedVideos++;
+ progress.Report(100d * completedVideos / numberOfVideos);
+ }
+
+ startIndex += QueryPageLimit;
+ }
+
+ progress.Report(100);
+ }
+}
diff --git a/Jellyfin.Plugin.SubtitleExtract/Tasks/ExtractSubtitlesTask.cs b/Jellyfin.Plugin.SubtitleExtract/Tasks/ExtractSubtitlesTask.cs
index 8b66432..376a702 100644
--- a/Jellyfin.Plugin.SubtitleExtract/Tasks/ExtractSubtitlesTask.cs
+++ b/Jellyfin.Plugin.SubtitleExtract/Tasks/ExtractSubtitlesTask.cs
@@ -1,17 +1,14 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
-using Jellyfin.Plugin.SubtitleExtract.Tools;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.SubtitleExtract.Tasks;
@@ -20,42 +17,41 @@ namespace Jellyfin.Plugin.SubtitleExtract.Tasks;
///
public class ExtractSubtitlesTask : IScheduledTask
{
- private const int QueryPageLimit = 100;
+ private const int QueryPageLimit = 250;
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
+ private readonly ISubtitleEncoder _encoder;
private static readonly BaseItemKind[] _itemTypes = [BaseItemKind.Episode, BaseItemKind.Movie];
private static readonly MediaType[] _mediaTypes = [MediaType.Video];
private static readonly SourceType[] _sourceTypes = [SourceType.Library];
private static readonly DtoOptions _dtoOptions = new(false);
- private readonly SubtitleExtractor _extractor;
-
///
/// Initializes a new instance of the class.
///
/// Instance of interface.
- /// instance.
+ /// instance.
/// Instance of interface.
public ExtractSubtitlesTask(
ILibraryManager libraryManager,
- SubtitleExtractor subtitleExtractor,
+ ISubtitleEncoder subtitleEncoder,
ILocalizationManager localization)
{
_libraryManager = libraryManager;
_localization = localization;
- _extractor = subtitleExtractor;
+ _encoder = subtitleEncoder;
}
///
public string Key => "ExtractSubtitles";
///
- public string Name => SubtitleExtractPlugin.Current!.Name;
+ public string Name => "Extract Subtitles";
///
- public string Description => SubtitleExtractPlugin.Current!.Description;
+ public string Description => "Extracts embedded subtitles.";
///
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
@@ -95,7 +91,10 @@ public async Task ExecuteAsync(IProgress progress, CancellationToken can
{
cancellationToken.ThrowIfCancellationRequested();
- await _extractor.Run(video, cancellationToken).ConfigureAwait(false);
+ foreach (var mediaSource in video.GetMediaSources(false))
+ {
+ await _encoder.ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false);
+ }
completedVideos++;
progress.Report(100d * completedVideos / numberOfVideos);