External subtitles work, internal are broken
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -6,7 +6,13 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
@@ -16,11 +22,9 @@ using MediaBrowser.Model.MediaInfo;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using CinemaLib.API;
|
using CinemaLib.API;
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using LinkGenerator = CinemaLib.Webshare.LinkGenerator;
|
using LinkGenerator = CinemaLib.Webshare.LinkGenerator;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Cinema;
|
namespace Jellyfin.Plugin.Cinema;
|
||||||
|
|
||||||
@@ -54,12 +58,13 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
|||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IApplicationPaths _pathManager;
|
||||||
private readonly ConcurrentDictionary<Guid, VersionSetEntry> _videoVersions;
|
private readonly ConcurrentDictionary<Guid, VersionSetEntry> _videoVersions;
|
||||||
private readonly ConcurrentDictionary<LinkKey, LinkEntry> _links;
|
private readonly ConcurrentDictionary<LinkKey, LinkEntry> _links;
|
||||||
|
|
||||||
public CinemaMediaSourceManager(CinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServerApplicationHost host,
|
public CinemaMediaSourceManager(CinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServerApplicationHost host,
|
||||||
IServiceProvider svc, IHttpContextAccessor http, IHttpClientFactory httpClientFactory, IMediaEncoder mediaEncoder,
|
IServiceProvider svc, IHttpContextAccessor http, IHttpClientFactory httpClientFactory, IMediaEncoder mediaEncoder,
|
||||||
IServerConfigurationManager serverConfigurationManager, IUserManager userManager)
|
IServerConfigurationManager serverConfigurationManager, IUserManager userManager, IApplicationPaths pathManager)
|
||||||
{
|
{
|
||||||
if (innerMediaSourceManager == null || svc == null)
|
if (innerMediaSourceManager == null || svc == null)
|
||||||
throw new ArgumentNullException();
|
throw new ArgumentNullException();
|
||||||
@@ -76,6 +81,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
|||||||
this._mediaEncoder = mediaEncoder;
|
this._mediaEncoder = mediaEncoder;
|
||||||
this._serverConfigurationManager = serverConfigurationManager;
|
this._serverConfigurationManager = serverConfigurationManager;
|
||||||
this._userManager = userManager;
|
this._userManager = userManager;
|
||||||
|
this._pathManager = pathManager;
|
||||||
this._videoVersions = new ConcurrentDictionary<Guid, VersionSetEntry>();
|
this._videoVersions = new ConcurrentDictionary<Guid, VersionSetEntry>();
|
||||||
this._links = new ConcurrentDictionary<LinkKey, LinkEntry>();
|
this._links = new ConcurrentDictionary<LinkKey, LinkEntry>();
|
||||||
|
|
||||||
@@ -273,7 +279,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
|||||||
Uri link = await GenerateLink(metaVer.Meta.provider, metaVer.Meta.ident, metaVer.Meta.name, cancellationToken);
|
Uri link = await GenerateLink(metaVer.Meta.provider, metaVer.Meta.ident, metaVer.Meta.name, cancellationToken);
|
||||||
if (metaVer.AnalyzedInfo == null)
|
if (metaVer.AnalyzedInfo == null)
|
||||||
metaVer.AnalyzedInfo = await CinemaMediaAnalyzer.AnalyzeResourceAsync(link, metaVer.Meta.size, metaVer.Meta, _httpClientFactory, _mediaEncoder, _serverConfigurationManager, cancellationToken);
|
metaVer.AnalyzedInfo = await CinemaMediaAnalyzer.AnalyzeResourceAsync(link, metaVer.Meta.size, metaVer.Meta, _httpClientFactory, _mediaEncoder, _serverConfigurationManager, cancellationToken);
|
||||||
result.Add(GetVersionInfo(videoVer, metaVer.Meta, metaVer.AnalyzedInfo.MediaStreams, isFreeAccount, link));
|
result.Add(await GetVersionInfo(videoVer, metaVer.Meta, metaVer.AnalyzedInfo.MediaStreams, isFreeAccount, link, cancellationToken));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -281,7 +287,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
|||||||
// Note: Also makes sure BaseItems exist for each version
|
// Note: Also makes sure BaseItems exist for each version
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
foreach (Video i in items)
|
foreach (Video i in items)
|
||||||
result.Add(GetVersionInfo(i, ver.Versions[idx++].Meta, null, isFreeAccount, FakeVideoUri));
|
result.Add(await GetVersionInfo(i, ver.Versions[idx++].Meta, null, isFreeAccount, FakeVideoUri, cancellationToken));
|
||||||
|
|
||||||
if (sortByPrefs && ver.Versions.Length != 0 && bestIdx >= 0)
|
if (sortByPrefs && ver.Versions.Length != 0 && bestIdx >= 0)
|
||||||
{
|
{
|
||||||
@@ -563,17 +569,17 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
|||||||
|
|
||||||
bool isFreeAccount = await CinemaHost.IsWebshareFreeAccount(cancel);
|
bool isFreeAccount = await CinemaHost.IsWebshareFreeAccount(cancel);
|
||||||
|
|
||||||
return GetVersionInfo(item, ver.Meta, null, isFreeAccount, FakeVideoUri);
|
return await GetVersionInfo(item, ver.Meta, null, isFreeAccount, FakeVideoUri, cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver, IReadOnlyList<MediaStream>? analyzedStreams, bool isFreeAccount, Uri path)
|
private async ValueTask<MediaSourceInfo> GetVersionInfo(Video item, CinemaLib.API.Stream ver, IReadOnlyList<MediaStream>? analyzedStreams, bool isFreeAccount, Uri path, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
MediaSourceInfo result = new MediaSourceInfo();
|
MediaSourceInfo result = new MediaSourceInfo();
|
||||||
result.VideoType = VideoType.VideoFile;
|
result.VideoType = VideoType.VideoFile;
|
||||||
result.Id = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
result.Id = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||||
result.Protocol = MediaProtocol.Http;
|
result.Protocol = MediaProtocol.Http;
|
||||||
// Warning: We set some properties on item that get used below
|
// Warning: We set some properties on item that get used below
|
||||||
result.MediaStreams = VersionToMediaStreams(item, ver, analyzedStreams);
|
result.MediaStreams = await VersionToMediaStreams(result.Id, item, ver, analyzedStreams, cancel);
|
||||||
|
|
||||||
double bitRate = (ver.size ?? 0.0) * 8 / (item.RunTimeTicks ?? 1.0) * TimeSpan.TicksPerSecond;
|
double bitRate = (ver.size ?? 0.0) * 8 / (item.RunTimeTicks ?? 1.0) * TimeSpan.TicksPerSecond;
|
||||||
bool showBitrateWarning = isFreeAccount && bitRate / (1 + BitrateMargin) > WebshareFreeBitrate;
|
bool showBitrateWarning = isFreeAccount && bitRate / (1 + BitrateMargin) > WebshareFreeBitrate;
|
||||||
@@ -634,8 +640,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<MediaStream> VersionToMediaStreams(Video item, CinemaLib.API.Stream ver, IReadOnlyList<MediaStream>? analyzedStreams)
|
private async ValueTask<List<MediaStream>> VersionToMediaStreams(string mediaSourceId, Video item, CinemaLib.API.Stream ver, IReadOnlyList<MediaStream>? analyzedStreams, CancellationToken cancel) {
|
||||||
{
|
|
||||||
List<MediaStream> result = new List<MediaStream>();
|
List<MediaStream> result = new List<MediaStream>();
|
||||||
|
|
||||||
if (analyzedStreams != null && analyzedStreams.Count != 0)
|
if (analyzedStreams != null && analyzedStreams.Count != 0)
|
||||||
@@ -646,15 +651,40 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
|||||||
{
|
{
|
||||||
foreach (StreamSubtitle j in ver.subtitles)
|
foreach (StreamSubtitle j in ver.subtitles)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(j.src))
|
if (!string.IsNullOrEmpty(j.src) && Uri.TryCreate(j.src, UriKind.Absolute, out Uri? src))
|
||||||
{
|
{
|
||||||
MediaStream a = new MediaStream();
|
MediaStream a = new MediaStream();
|
||||||
a.Index = -1;
|
a.Index = result.Count;
|
||||||
a.Type = MediaStreamType.Subtitle;
|
a.Type = MediaStreamType.Subtitle;
|
||||||
a.Language = ISO639_1ToISO639_2(j.language).Primary;
|
a.Language = ISO639_1ToISO639_2(j.language).Primary;
|
||||||
a.IsForced = j.forced;
|
a.IsForced = j.forced;
|
||||||
a.DeliveryUrl = j.src;
|
|
||||||
a.IsExternal = true;
|
a.IsExternal = true;
|
||||||
|
// Note: This prevents subtitle burn-in and does not restart playback while activating
|
||||||
|
a.SupportsExternalStream = true;
|
||||||
|
a.Codec = "subrip";
|
||||||
|
#if NOT_BROKEN_GetStream
|
||||||
|
a.Path = (await GenerateLinkOrKeepUrl(src, cancel)).ToString();
|
||||||
|
#else
|
||||||
|
// BUG: SubtitleEncoder.GetStream is broken as it disposes the returned Stream
|
||||||
|
// before returning. Therefore we must cache the subtitle file locally
|
||||||
|
// HACK: We expect the ordering in ver.subtitles is stable as Jellyfin
|
||||||
|
// uses index to determine the filename
|
||||||
|
string subtitleCachePath = Path.Combine(_pathManager.DataPath, "subtitles");
|
||||||
|
string mediaSourceIdS = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture);
|
||||||
|
string folderPath = Path.Join(subtitleCachePath, mediaSourceIdS[..2], mediaSourceIdS);
|
||||||
|
string path = Path.Join(folderPath, a.Index.ToString(CultureInfo.InvariantCulture) + ".srt");
|
||||||
|
a.IsExternal = true;
|
||||||
|
a.Path = path;
|
||||||
|
|
||||||
|
if (!File.Exists(path)) {
|
||||||
|
// PERF: Subtitle files are expected not to be large (< 1 MB)
|
||||||
|
Uri link = await GenerateLinkOrKeepUrl(src, cancel);
|
||||||
|
using HttpResponseMessage response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(link, cancel);
|
||||||
|
byte[] contentB = await response.Content.ReadAsByteArrayAsync(cancel);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
await File.WriteAllBytesAsync(path, contentB, cancel);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
result.Add(a);
|
result.Add(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -725,8 +755,10 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
|||||||
a.Type = MediaStreamType.Subtitle;
|
a.Type = MediaStreamType.Subtitle;
|
||||||
a.Language = ISO639_1ToISO639_2(j.language).Primary;
|
a.Language = ISO639_1ToISO639_2(j.language).Primary;
|
||||||
a.IsForced = j.forced;
|
a.IsForced = j.forced;
|
||||||
a.DeliveryUrl = j.src;
|
if (a.IsExternal = j.src != null) {
|
||||||
a.IsExternal = j.src != null;
|
a.Path = j.src;
|
||||||
|
a.DeliveryMethod = MediaBrowser.Model.Dlna.SubtitleDeliveryMethod.External;
|
||||||
|
}
|
||||||
|
|
||||||
result.Add(a);
|
result.Add(a);
|
||||||
}
|
}
|
||||||
@@ -881,7 +913,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Uri> GenerateLink(string provider, string ident, string? name, CancellationToken cancel)
|
public async ValueTask<Uri> GenerateLink(string provider, string ident, string? name, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
name = name ?? "";
|
name = name ?? "";
|
||||||
DateTime now = DateTime.UtcNow;
|
DateTime now = DateTime.UtcNow;
|
||||||
@@ -903,6 +935,20 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
|||||||
return entry.Link;
|
return entry.Link;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ValueTask<Uri> GenerateLinkOrKeepUrl(Uri uri, CancellationToken cancel) {
|
||||||
|
switch (uri.Host) {
|
||||||
|
case "webshare.cz":
|
||||||
|
const string WebsharePrefix = "#/file/";
|
||||||
|
if (uri.AbsolutePath == "/" && uri.Fragment.StartsWith(WebsharePrefix)) {
|
||||||
|
string ident = uri.Fragment.Substring(WebsharePrefix.Length);
|
||||||
|
return GenerateLink("webshare", ident, name: null, cancel);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ValueTask<Uri>(uri);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts two-letter 639-1 language code to three letter 639-2.
|
/// Converts two-letter 639-1 language code to three letter 639-2.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -75,13 +75,16 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
|
|||||||
{
|
{
|
||||||
ControllerModel? mediaInfo = application.Controllers.Where(x => x.ControllerName == "MediaInfo").FirstOrDefault();
|
ControllerModel? mediaInfo = application.Controllers.Where(x => x.ControllerName == "MediaInfo").FirstOrDefault();
|
||||||
ControllerModel? dynamicHls = application.Controllers.Where(x => x.ControllerName == "DynamicHls").FirstOrDefault();
|
ControllerModel? dynamicHls = application.Controllers.Where(x => x.ControllerName == "DynamicHls").FirstOrDefault();
|
||||||
ActionModel? getPostedPlaybackInfo, getMasterHlsVideoPlaylist, getVariantHlsVideoPlaylist, getHlsVideoSegment;
|
ControllerModel? subtitle = application.Controllers.Where(x => x.ControllerName == "Subtitle").FirstOrDefault();
|
||||||
|
ActionModel? getPostedPlaybackInfo, getMasterHlsVideoPlaylist, getVariantHlsVideoPlaylist, getHlsVideoSegment, getSubtitleWithTicks;
|
||||||
if (mediaInfo == null
|
if (mediaInfo == null
|
||||||
|| (getPostedPlaybackInfo = mediaInfo.Actions.Where(x => x.ActionName == "GetPostedPlaybackInfo").FirstOrDefault()) == null
|
|| (getPostedPlaybackInfo = mediaInfo.Actions.Where(x => x.ActionName == "GetPostedPlaybackInfo").FirstOrDefault()) == null
|
||||||
|| dynamicHls == null
|
|| dynamicHls == null
|
||||||
|| (getMasterHlsVideoPlaylist = dynamicHls.Actions.Where(x => x.ActionName == "GetMasterHlsVideoPlaylist").FirstOrDefault()) == null
|
|| (getMasterHlsVideoPlaylist = dynamicHls.Actions.Where(x => x.ActionName == "GetMasterHlsVideoPlaylist").FirstOrDefault()) == null
|
||||||
|| (getVariantHlsVideoPlaylist = dynamicHls.Actions.Where(x => x.ActionName == "GetVariantHlsVideoPlaylist").FirstOrDefault()) == null
|
|| (getVariantHlsVideoPlaylist = dynamicHls.Actions.Where(x => x.ActionName == "GetVariantHlsVideoPlaylist").FirstOrDefault()) == null
|
||||||
|| (getHlsVideoSegment = dynamicHls.Actions.Where(x => x.ActionName == "GetHlsVideoSegment").FirstOrDefault()) == null)
|
|| (getHlsVideoSegment = dynamicHls.Actions.Where(x => x.ActionName == "GetHlsVideoSegment").FirstOrDefault()) == null
|
||||||
|
|| subtitle == null
|
||||||
|
|| (getSubtitleWithTicks = subtitle.Actions.Where(x => x.ActionName == "GetSubtitleWithTicks").FirstOrDefault()) == null)
|
||||||
throw new InvalidOperationException("Failed to register for MediaSourceId extraction from the Jellyfin's MediaInfo controller.");
|
throw new InvalidOperationException("Failed to register for MediaSourceId extraction from the Jellyfin's MediaInfo controller.");
|
||||||
|
|
||||||
getPostedPlaybackInfo.Filters.Add(new GetPostedPlaybackInfoMediaSourceIdFilter());
|
getPostedPlaybackInfo.Filters.Add(new GetPostedPlaybackInfoMediaSourceIdFilter());
|
||||||
@@ -90,6 +93,8 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
|
|||||||
getMasterHlsVideoPlaylist.Filters.Add(new DynamicHlsMediaSourceIdFilter());
|
getMasterHlsVideoPlaylist.Filters.Add(new DynamicHlsMediaSourceIdFilter());
|
||||||
getVariantHlsVideoPlaylist.Filters.Add(new DynamicHlsMediaSourceIdFilter());
|
getVariantHlsVideoPlaylist.Filters.Add(new DynamicHlsMediaSourceIdFilter());
|
||||||
getHlsVideoSegment.Filters.Add(new DynamicHlsMediaSourceIdFilter());
|
getHlsVideoSegment.Filters.Add(new DynamicHlsMediaSourceIdFilter());
|
||||||
|
// Slightly different parameter name
|
||||||
|
getSubtitleWithTicks.Filters.Add(new SubtitleMediaSourceIdFilter());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,4 +138,21 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class SubtitleMediaSourceIdFilter : IActionFilter
|
||||||
|
{
|
||||||
|
public void OnActionExecuting(ActionExecutingContext context)
|
||||||
|
{
|
||||||
|
string? mediaSourceId;
|
||||||
|
if (!context.ActionArguments.TryGetValue("routeMediaSourceId", out object? mediaSourceIdO)
|
||||||
|
|| (mediaSourceId = mediaSourceIdO as string) == null)
|
||||||
|
throw new InvalidOperationException("Cannot extract MediaSourceId from the Jellyfin's controller Subtitle.");
|
||||||
|
|
||||||
|
context.HttpContext.Items.Add(CinemaMediaSourceManager.ContextItemsMediaSourceIdKey, mediaSourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnActionExecuted(ActionExecutedContext context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user