Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public PluginConfiguration()
}

/// <summary>
/// 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.
/// </summary>
public bool ExtractionDuringLibraryScan { get; set; } = false;
Expand Down
4 changes: 2 additions & 2 deletions Jellyfin.Plugin.SubtitleExtract/Configuration/configPage.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="chkEnableDuringScan" />
<span>Extract subtitles during library scan</span>
<span>Extract subtitles and attachments during library scan</span>
</label>
<div class="fieldDescription checkboxFieldDescription">This will make sure subtitles are extracted sooner but will result in longer library scans. Does not disable the scheduled task.</div>
<div class="fieldDescription checkboxFieldDescription">This will make sure subtitles and attachments are extracted sooner but will result in longer library scans. Does not disable the scheduled task.</div>
</div>

<br />
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extracts embedded attachments while library scanning for immediate access in web player.
/// </summary>
public class AttachmentExtractionProvider : ICustomMetadataProvider<Episode>,
ICustomMetadataProvider<Movie>,
ICustomMetadataProvider<Video>,
IHasItemChangeMonitor,
IHasOrder,
IForcedProvider
{
private readonly ILogger<SubtitleExtractionProvider> _logger;

private readonly IAttachmentExtractor _extractor;

/// <summary>
/// Initializes a new instance of the <see cref="AttachmentExtractionProvider"/> class.
/// </summary>
/// <param name="attachmentExtractor"><see cref="IAttachmentExtractor"/> instance.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
public AttachmentExtractionProvider(
IAttachmentExtractor attachmentExtractor,
ILogger<SubtitleExtractionProvider> logger)
{
_logger = logger;
_extractor = attachmentExtractor;
}

/// <inheritdoc />
public string Name => "AttachmentExtractionProvider";

/// <summary>
/// Gets the order in which the provider should be called. (Core provider is = 100).
/// </summary>
public int Order => 1000;

/// <inheritdoc/>
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;
}

/// <inheritdoc/>
public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
return FetchSubtitles(item, cancellationToken);
}

/// <inheritdoc/>
public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
return FetchSubtitles(item, cancellationToken);
}

/// <inheritdoc/>
public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
return FetchSubtitles(item, cancellationToken);
}

private async Task<ItemUpdateType> FetchSubtitles(BaseItem item, CancellationToken cancellationToken)
{
var config = SubtitleExtractPlugin.Current!.Configuration;

if (config.ExtractionDuringLibraryScan)
{
_logger.LogDebug("Extracting subtitles 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 subtitle extraction for: {Video}", item.Path);
}

return ItemUpdateType.None;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.SubtitleExtract.Tools;
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 Microsoft.Extensions.Logging;

Expand All @@ -22,23 +22,23 @@ public class SubtitleExtractionProvider : ICustomMetadataProvider<Episode>,
{
private readonly ILogger<SubtitleExtractionProvider> _logger;

private readonly SubtitleExtractor _extractor;
private readonly ISubtitleEncoder _encoder;

/// <summary>
/// Initializes a new instance of the <see cref="SubtitleExtractionProvider"/> class.
/// </summary>
/// <param name="subtitlesExtractor"><see cref="SubtitleExtractor"/> instance.</param>
/// <param name="subtitleEncoder"><see cref="ISubtitleEncoder"/> instance.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
public SubtitleExtractionProvider(
SubtitleExtractor subtitlesExtractor,
ISubtitleEncoder subtitleEncoder,
ILogger<SubtitleExtractionProvider> logger)
{
_logger = logger;
_extractor = subtitlesExtractor;
_encoder = subtitleEncoder;
}

/// <inheritdoc />
public string Name => SubtitleExtractPlugin.Current.Name;
public string Name => "SubtitleExtractionProvider";

/// <summary>
/// Gets the order in which the provider should be called. (Core provider is = 100).
Expand All @@ -51,7 +51,7 @@ public bool HasChanged(BaseItem item, IDirectoryService directoryService)
if (item.IsFileProtocol)
{
var file = directoryService.GetFile(item.Path);
if (file != null && item.DateModified != file.LastWriteTimeUtc)
if (file != null && (item.DateModified != file.LastWriteTimeUtc || item.Size != file.Length))
{
return true;
}
Expand Down Expand Up @@ -86,7 +86,10 @@ private async Task<ItemUpdateType> FetchSubtitles(BaseItem item, CancellationTok
{
_logger.LogDebug("Extracting subtitles for: {Video}", item.Path);

await _extractor.Run(item, cancellationToken).ConfigureAwait(false);
foreach (var mediaSource in item.GetMediaSources(false))
{
await _encoder.ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false);
}

_logger.LogDebug("Finished subtitle extraction for: {Video}", item.Path);
}
Expand Down
4 changes: 2 additions & 2 deletions Jellyfin.Plugin.SubtitleExtract/SubtitleExtractPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public SubtitleExtractPlugin(IApplicationPaths applicationPaths, IXmlSerializer
public override Guid Id => new("CD893C24-B59E-4060-87B2-184070E1BF68");

/// <inheritdoc />
public override string Description => "Extracts embedded subtitles";
public override string Description => "Extracts embedded subtitles and attachments";

/// <summary>
/// Gets the current plugin instance.
Expand All @@ -44,7 +44,7 @@ public IEnumerable<PluginPageInfo> GetPages()
return [
new PluginPageInfo
{
Name = "Jellyfin subtitle extractor",
Name = "Jellyfin subtitle and attachment extractor",
EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html"
}
];
Expand Down

This file was deleted.

124 changes: 124 additions & 0 deletions Jellyfin.Plugin.SubtitleExtract/Tasks/ExtractAttachmentsTask.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Scheduled task to extract embedded attachments for immediate access in web player.
/// </summary>
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);

/// <summary>
/// Initializes a new instance of the <see cref="ExtractAttachmentsTask" /> class.
/// </summary>
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
/// <param name="attachmentExtractor"><see cref="IAttachmentExtractor"/> instance.</param>
/// <param name="localization">Instance of <see cref="ILocalizationManager"/> interface.</param>
public ExtractAttachmentsTask(
ILibraryManager libraryManager,
IAttachmentExtractor attachmentExtractor,
ILocalizationManager localization)
{
_libraryManager = libraryManager;
_localization = localization;
_extractor = attachmentExtractor;
}

/// <inheritdoc />
public string Key => "ExtractAttachments";

/// <inheritdoc />
public string Name => "Extract Attachments";

/// <inheritdoc />
public string Description => "Extracts embedded attachments.";

/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");

/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return [];
}

/// <inheritdoc />
public async Task ExecuteAsync(IProgress<double> 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);
}
}
Loading