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.Text;
|
||||
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.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
@@ -16,11 +22,9 @@ using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using CinemaLib.API;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using System.Runtime.CompilerServices;
|
||||
using LinkGenerator = CinemaLib.Webshare.LinkGenerator;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
@@ -54,12 +58,13 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IApplicationPaths _pathManager;
|
||||
private readonly ConcurrentDictionary<Guid, VersionSetEntry> _videoVersions;
|
||||
private readonly ConcurrentDictionary<LinkKey, LinkEntry> _links;
|
||||
|
||||
public CinemaMediaSourceManager(CinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServerApplicationHost host,
|
||||
IServiceProvider svc, IHttpContextAccessor http, IHttpClientFactory httpClientFactory, IMediaEncoder mediaEncoder,
|
||||
IServerConfigurationManager serverConfigurationManager, IUserManager userManager)
|
||||
IServerConfigurationManager serverConfigurationManager, IUserManager userManager, IApplicationPaths pathManager)
|
||||
{
|
||||
if (innerMediaSourceManager == null || svc == null)
|
||||
throw new ArgumentNullException();
|
||||
@@ -76,6 +81,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
this._mediaEncoder = mediaEncoder;
|
||||
this._serverConfigurationManager = serverConfigurationManager;
|
||||
this._userManager = userManager;
|
||||
this._pathManager = pathManager;
|
||||
this._videoVersions = new ConcurrentDictionary<Guid, VersionSetEntry>();
|
||||
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);
|
||||
if (metaVer.AnalyzedInfo == null)
|
||||
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
|
||||
{
|
||||
@@ -281,7 +287,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
// Note: Also makes sure BaseItems exist for each version
|
||||
int idx = 0;
|
||||
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)
|
||||
{
|
||||
@@ -563,17 +569,17 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
|
||||
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();
|
||||
result.VideoType = VideoType.VideoFile;
|
||||
result.Id = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
result.Protocol = MediaProtocol.Http;
|
||||
// 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;
|
||||
bool showBitrateWarning = isFreeAccount && bitRate / (1 + BitrateMargin) > WebshareFreeBitrate;
|
||||
@@ -634,8 +640,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
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>();
|
||||
|
||||
if (analyzedStreams != null && analyzedStreams.Count != 0)
|
||||
@@ -646,15 +651,40 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
{
|
||||
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();
|
||||
a.Index = -1;
|
||||
a.Index = result.Count;
|
||||
a.Type = MediaStreamType.Subtitle;
|
||||
a.Language = ISO639_1ToISO639_2(j.language).Primary;
|
||||
a.IsForced = j.forced;
|
||||
a.DeliveryUrl = j.src;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -725,8 +755,10 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
a.Type = MediaStreamType.Subtitle;
|
||||
a.Language = ISO639_1ToISO639_2(j.language).Primary;
|
||||
a.IsForced = j.forced;
|
||||
a.DeliveryUrl = j.src;
|
||||
a.IsExternal = j.src != null;
|
||||
if (a.IsExternal = j.src != null) {
|
||||
a.Path = j.src;
|
||||
a.DeliveryMethod = MediaBrowser.Model.Dlna.SubtitleDeliveryMethod.External;
|
||||
}
|
||||
|
||||
result.Add(a);
|
||||
}
|
||||
@@ -881,7 +913,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
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 ?? "";
|
||||
DateTime now = DateTime.UtcNow;
|
||||
@@ -903,6 +935,20 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
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>
|
||||
/// Converts two-letter 639-1 language code to three letter 639-2.
|
||||
/// </summary>
|
||||
|
||||
@@ -75,13 +75,16 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
|
||||
{
|
||||
ControllerModel? mediaInfo = application.Controllers.Where(x => x.ControllerName == "MediaInfo").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
|
||||
|| (getPostedPlaybackInfo = mediaInfo.Actions.Where(x => x.ActionName == "GetPostedPlaybackInfo").FirstOrDefault()) == null
|
||||
|| dynamicHls == null
|
||||
|| (getMasterHlsVideoPlaylist = dynamicHls.Actions.Where(x => x.ActionName == "GetMasterHlsVideoPlaylist").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.");
|
||||
|
||||
getPostedPlaybackInfo.Filters.Add(new GetPostedPlaybackInfoMediaSourceIdFilter());
|
||||
@@ -90,6 +93,8 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
|
||||
getMasterHlsVideoPlaylist.Filters.Add(new DynamicHlsMediaSourceIdFilter());
|
||||
getVariantHlsVideoPlaylist.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