Playback of correct version stream works
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:
@@ -330,6 +330,7 @@ public abstract class CinemaFilterFolder : Folder
|
||||
item.ProviderIds = new Dictionary<string, string>();
|
||||
item.ProviderIds[CinemaPlugin.CinemaProviderName] = "";
|
||||
|
||||
// Indicate just HTTP CinemaMediaSourceManager and CinemaMediaSourceController will handle the rest
|
||||
item.Path = "https://a/b";
|
||||
|
||||
if (item is IHasArtist hasArtists)
|
||||
|
||||
46
CinemaJellyfin/CinemaMediaSourceController.cs
Normal file
46
CinemaJellyfin/CinemaMediaSourceController.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
/// <summary>
|
||||
/// The artists controller.
|
||||
/// </summary>
|
||||
[Route("Cinema")]
|
||||
[ApiController]
|
||||
public class CinemaMediaSourceController : ControllerBase
|
||||
{
|
||||
private readonly CinemaMediaSourceManager _mediaManager;
|
||||
|
||||
public CinemaMediaSourceController(CinemaMediaSourceManager mediaManager)
|
||||
{
|
||||
_mediaManager = mediaManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a download link using the given provider and its identifier.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <response code="302">External resource redirect.</response>
|
||||
/// <response code="404">Link not found.</response>
|
||||
[HttpGet("{provider}/{ident}/{name}/link")]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> GetExternalIdInfos(
|
||||
[FromRoute, Required] string provider,
|
||||
[FromRoute, Required] string ident,
|
||||
[FromRoute, Optional] string? name,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Redirect((await _mediaManager.GenerateLink(provider, ident, name, cancel)).ToString());
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Cinema.Webshare;
|
||||
using CinemaLib.API;
|
||||
using MediaBrowser.Controller;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
@@ -28,15 +29,18 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
private const double WebshareFreeBitrate = 300000 * 8;
|
||||
|
||||
private static readonly TimeSpan VersionValidityTimeout = TimeSpan.FromMinutes(180);
|
||||
private static readonly TimeSpan LinkValidityTimeout = VersionValidityTimeout;
|
||||
|
||||
private static CinemaMediaSourceManager? _instance;
|
||||
|
||||
private readonly IMediaSourceManager _inner;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerApplicationHost _host;
|
||||
private readonly IHttpContextAccessor _http;
|
||||
private readonly ConcurrentDictionary<Guid, VersionSetEntry> _videoVersions;
|
||||
private readonly ConcurrentDictionary<LinkKey, LinkEntry> _links;
|
||||
|
||||
public CinemaMediaSourceManager(ICinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServiceProvider svc, IHttpContextAccessor http)
|
||||
public CinemaMediaSourceManager(ICinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServerApplicationHost host, IServiceProvider svc, IHttpContextAccessor http)
|
||||
{
|
||||
if (innerMediaSourceManager == null || svc == null)
|
||||
throw new ArgumentNullException();
|
||||
@@ -48,7 +52,9 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
|
||||
this._libraryManager = libraryManager;
|
||||
this._http = http;
|
||||
this._host = host;
|
||||
this._videoVersions = new ConcurrentDictionary<Guid, VersionSetEntry>();
|
||||
this._links = new ConcurrentDictionary<LinkKey, LinkEntry>();
|
||||
|
||||
_instance = this;
|
||||
}
|
||||
@@ -152,17 +158,40 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User? user = null)
|
||||
{
|
||||
// Intercept for CinemaItems
|
||||
if (item == null
|
||||
HttpContext? ctx = _http.HttpContext;
|
||||
if (ctx == null
|
||||
|| item == null
|
||||
|| item.ProviderIds == null
|
||||
|| !item.ProviderIds.ContainsKey(CinemaPlugin.CinemaProviderName)
|
||||
|| string.IsNullOrEmpty(item.ExternalId))
|
||||
return _inner.GetStaticMediaSources(item, enablePathSubstitution, user);
|
||||
|
||||
List<MediaSourceInfo> result = GetPlaybackMediaSources(item, user, false, enablePathSubstitution, default).GetAwaiter().GetResult();
|
||||
|
||||
// HACK Prevent crash
|
||||
if (result.Count == 0)
|
||||
result.Append(new MediaSourceInfo() { MediaStreams = new List<MediaStream>() });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
|
||||
{
|
||||
// Intercept for CinemaItems
|
||||
HttpContext? ctx = _http.HttpContext;
|
||||
if (ctx == null
|
||||
|| item == null
|
||||
|| item.ProviderIds == null
|
||||
|| !item.ProviderIds.ContainsKey(CinemaPlugin.CinemaProviderName)
|
||||
|| string.IsNullOrEmpty(item.ExternalId))
|
||||
return await _inner.GetPlaybackMediaSources(item, user, allowMediaProbe, enablePathSubstitution, cancellationToken);
|
||||
|
||||
List<MediaSourceInfo> result = new List<MediaSourceInfo>();
|
||||
Uri thisServerBaseUri = new Uri(_host.GetSmartApiUrl(ctx.Request));
|
||||
switch (item)
|
||||
{
|
||||
case Video video:
|
||||
VersionSetEntry ver = GetVersionSet(video, out BaseItem? videoPrimary, default).GetAwaiter().GetResult();
|
||||
VersionSetEntry ver = await GetVersionSet(video, out BaseItem? videoPrimary, default);
|
||||
if (ver.Versions != null)
|
||||
{
|
||||
IEnumerable<Video> items;
|
||||
@@ -176,73 +205,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
// Warning: We assume GetVideoVersionsEnumerate keeps the order between its input and output collections
|
||||
// Note: Also makes sure BaseItems exist for each version
|
||||
foreach (var i in items)
|
||||
result.Add(GetVersionInfo(i, ver.Versions[idx++].Meta));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// HACK Prevent crash
|
||||
if (result.Count == 0)
|
||||
result.Append(new MediaSourceInfo() { MediaStreams = new List<MediaStream>() });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
|
||||
{
|
||||
// Intercept for CinemaItems
|
||||
if (item == null
|
||||
|| item.ProviderIds == null
|
||||
|| !item.ProviderIds.ContainsKey(CinemaPlugin.CinemaProviderName)
|
||||
|| string.IsNullOrEmpty(item.ExternalId))
|
||||
return await _inner.GetPlaybackMediaSources(item, user, allowMediaProbe, enablePathSubstitution, cancellationToken);
|
||||
|
||||
// HACK: We do not want to generate links to all media sources - just the one going
|
||||
// to be played. Unfortunately our caller MediaInfoHelper.GetPlaybackInfo and indirectly
|
||||
// from MediaInfoController.GetPostedPlaybackInfo does not pass this info to us
|
||||
// so we will grab it directly from the HttpRequest
|
||||
HttpContext? ctx = _http.HttpContext;
|
||||
StringValues mediaSourceIdS;
|
||||
string? mediaSourceId;
|
||||
if (ctx == null
|
||||
|| !ctx.Request.Path.HasValue
|
||||
|| !ctx.Request.Path.Value.EndsWith("/PlaybackInfo")
|
||||
|| (mediaSourceIdS = ctx.Request.Query["mediaSourceId"]).Count != 1
|
||||
|| string.IsNullOrEmpty(mediaSourceId = mediaSourceIdS[0]))
|
||||
throw new NotSupportedException(string.Format("Unsupported caller '{0}' of CinemaMediaSourceManager", ctx != null && ctx.Request.Path.HasValue ? ctx.Request.Path.Value : "?"));
|
||||
|
||||
BaseItem? verItem = _libraryManager.GetItemById(Guid.Parse(mediaSourceId));
|
||||
|
||||
List<MediaSourceInfo> result = new List<MediaSourceInfo>();
|
||||
switch (verItem)
|
||||
{
|
||||
case Video video:
|
||||
VersionEntry? ver = await GetVersion(video, cancellationToken);
|
||||
if (ver != null)
|
||||
{
|
||||
Uri? path = ver.DownloadLink;
|
||||
if (path == null)
|
||||
{
|
||||
switch (ver.Meta.provider)
|
||||
{
|
||||
case "webshare": path = await LinkGenerator.GenerateDownloadLinkAsync(ver.Meta.ident, ver.Meta.name, cancellationToken); break;
|
||||
default: path = null; break;
|
||||
}
|
||||
ver.DownloadLink = path;
|
||||
}
|
||||
MediaSourceInfo a = GetVersionInfo(video, ver.Meta);
|
||||
if (path != null)
|
||||
a.Path = path.ToString();
|
||||
// Propagate "-seekable 0" to ffmpeg as free accounts on Webshare cannot use "Range: bytes" HTTP header
|
||||
// HACK: We misuse the user-agent as GetExtraArguments in MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
|
||||
// does not properly escape its value for command line
|
||||
if (IsWebshareFreeAccount)
|
||||
{
|
||||
if (a.RequiredHttpHeaders == null)
|
||||
a.RequiredHttpHeaders = new Dictionary<string, string>();
|
||||
a.RequiredHttpHeaders["User-Agent"] = "Lavf/60\" -seekable \"0";
|
||||
}
|
||||
result.Add(a);
|
||||
result.Add(GetVersionInfo(i, ver.Versions[idx++].Meta, thisServerBaseUri));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -370,11 +333,13 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
if (!_videoVersions.TryGetValue(primaryId, out result) || result.ValidUntil < now)
|
||||
{
|
||||
var a = await Metadata.StreamsAsync(csId, cancel);
|
||||
if (a != null) {
|
||||
if (a != null)
|
||||
{
|
||||
result.Versions = new VersionEntry[a.Length];
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
result.Versions[i] = new VersionEntry(a[i]);
|
||||
} else
|
||||
}
|
||||
else
|
||||
result.Versions = null;
|
||||
result.ValidUntil = now + VersionValidityTimeout;
|
||||
_videoVersions[primaryId] = result;
|
||||
@@ -412,14 +377,19 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
if (item == null)
|
||||
throw new ArgumentNullException();
|
||||
|
||||
HttpContext? ctx = _http.HttpContext;
|
||||
if (ctx == null)
|
||||
throw new InvalidOperationException("HttpContext required in GetVersionInfo");
|
||||
Uri thisServerBaseUri = new Uri(_host.GetSmartApiUrl(ctx.Request));
|
||||
|
||||
VersionEntry? ver = await GetVersion(item, cancel);
|
||||
if (ver == null)
|
||||
return null;
|
||||
|
||||
return GetVersionInfo(item, ver.Meta);
|
||||
return GetVersionInfo(item, ver.Meta, thisServerBaseUri);
|
||||
}
|
||||
|
||||
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver)
|
||||
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver, Uri thisServerBaseUri)
|
||||
{
|
||||
MediaSourceInfo result = new MediaSourceInfo();
|
||||
result.VideoType = VideoType.VideoFile;
|
||||
@@ -469,12 +439,21 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
}
|
||||
}
|
||||
result.Name = name;
|
||||
|
||||
//Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path,
|
||||
result.Path = $"{thisServerBaseUri.ToString()}Cinema/{Uri.EscapeDataString(ver.provider)}/{Uri.EscapeDataString(ver.ident)}/{Uri.EscapeDataString(ver.name)}/link";
|
||||
result.Container = item.Container;
|
||||
result.RunTimeTicks = item.RunTimeTicks;
|
||||
result.Size = item.Size;
|
||||
|
||||
// Propagate "-seekable 0" to ffmpeg as free accounts on Webshare cannot use "Range: bytes" HTTP header
|
||||
// HACK: We misuse the user-agent as GetExtraArguments in MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
|
||||
// does not properly escape its value for command line
|
||||
if (IsWebshareFreeAccount)
|
||||
{
|
||||
if (result.RequiredHttpHeaders == null)
|
||||
result.RequiredHttpHeaders = new Dictionary<string, string>();
|
||||
result.RequiredHttpHeaders["User-Agent"] = "Lavf/60\" -seekable \"0";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -593,6 +572,28 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task<Uri> GenerateLink(string provider, string ident, string? name, CancellationToken cancel)
|
||||
{
|
||||
name = name ?? "";
|
||||
DateTime now = DateTime.UtcNow;
|
||||
LinkKey key = new LinkKey(provider, ident, name);
|
||||
LinkEntry entry;
|
||||
if (!_links.TryGetValue(key, out entry) || !entry.IsValid(now))
|
||||
{
|
||||
Uri link;
|
||||
switch (provider)
|
||||
{
|
||||
case "webshare": link = await LinkGenerator.GenerateDownloadLinkAsync(ident, name, cancel); break;
|
||||
default: throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
entry = new LinkEntry(now + LinkValidityTimeout, link);
|
||||
_links[key] = entry;
|
||||
}
|
||||
|
||||
return entry.Link;
|
||||
}
|
||||
|
||||
struct VersionSetEntry
|
||||
{
|
||||
internal DateTime ValidUntil;
|
||||
@@ -612,4 +613,52 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
|
||||
internal Uri? DownloadLink { get; set; }
|
||||
}
|
||||
|
||||
struct LinkKey : IEquatable<LinkKey>
|
||||
{
|
||||
private readonly string _provider;
|
||||
private readonly string _ident;
|
||||
private readonly string _name;
|
||||
|
||||
internal LinkKey(string provider, string ident, string name)
|
||||
{
|
||||
this._provider = provider;
|
||||
this._ident = ident;
|
||||
this._name = name;
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is LinkKey && Equals((LinkKey)obj);
|
||||
}
|
||||
|
||||
public bool Equals(LinkKey other)
|
||||
{
|
||||
return _provider == other._provider && _ident == other._ident && _name == other._name;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _provider.GetHashCode() + 3 * _ident.GetHashCode() + 7 * _name.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
struct LinkEntry
|
||||
{
|
||||
private readonly DateTime _validUntil;
|
||||
private readonly Uri _link;
|
||||
|
||||
internal LinkEntry(DateTime validUntil, Uri link)
|
||||
{
|
||||
this._validUntil = validUntil;
|
||||
this._link = link;
|
||||
}
|
||||
|
||||
public bool IsValid(DateTime now)
|
||||
{
|
||||
return _validUntil > now;
|
||||
}
|
||||
|
||||
public Uri Link => _link;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
using System;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using CinemaLib.API;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
/// <summary>
|
||||
/// Media source provider for for Cinema media items.
|
||||
/// </summary>
|
||||
public class CinemaMediaSourceProvider : IMediaSourceProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetMediaSourcesInternal(item, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user