Movie media sources in progress
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2024-11-27 21:03:09 +01:00
parent cfa94a89a6
commit 4b92508b10
12 changed files with 592 additions and 27 deletions

View File

@@ -0,0 +1,5 @@
namespace Jellyfin.Plugin.StreamCinema;
public interface IStreamCinemaInnerMediaSourceManager {
Type InnerType { get; }
}

View File

@@ -17,6 +17,7 @@ public abstract class StreamCinemaFilterFolder : Folder
{
private const string PreferredCulture = "cs";
private const string FallbackCulture = "en";
private const char VersionSeparator = '-';
/// <summary>
/// Filtering folders can never be deleted.
@@ -86,15 +87,18 @@ public abstract class StreamCinemaFilterFolder : Folder
// Filtered content items
FilterSortBy sortBy;
ItemOrder sortDir;
if (query.OrderBy.Count == 0) {
if (query.OrderBy.Count == 0)
{
sortBy = FilterSortBy.Title;
sortDir = ItemOrder.Ascending;
} else {
}
else
{
(ItemSortBy sortByJ, SortOrder sortDirJ) = query.OrderBy.First();
sortBy = ConvertSort(sortByJ);
sortDir = sortDirJ == SortOrder.Ascending ? ItemOrder.Ascending : ItemOrder.Descending;
}
FilterResponse? filterRes = Metadata.SearchAsync(query.SearchTerm ?? "", order: sortDir, sort: sortBy, type: ItemType, offset: offset, limit: limit).GetAwaiter().GetResult();
if (filterRes != null && filterRes.hits != null && filterRes.hits.hits != null)
{
@@ -112,7 +116,8 @@ public abstract class StreamCinemaFilterFolder : Folder
private static FilterSortBy ConvertSort(ItemSortBy sortByJ)
{
switch (sortByJ) {
switch (sortByJ)
{
default:
case ItemSortBy.Default: return FilterSortBy.Title;
case ItemSortBy.AiredEpisodeOrder: return FilterSortBy.Premiered;
@@ -250,27 +255,27 @@ public abstract class StreamCinemaFilterFolder : Folder
bool singleLevel = res.hits.hits[0]._source?.children_count == 0;
if (isAudio)
item = GetMediaItemById<MediaBrowser.Controller.Entities.Audio.MusicAlbum>(csId, out isNew);
item = GetMediaItemById<MediaBrowser.Controller.Entities.Audio.MusicAlbum>(csId, null, out isNew);
else if (singleLevel)
item = GetMediaItemById<MediaBrowser.Controller.Entities.TV.Season>(csId, out isNew);
item = GetMediaItemById<MediaBrowser.Controller.Entities.TV.Season>(csId, null, out isNew);
else
item = GetMediaItemById<MediaBrowser.Controller.Entities.TV.Series>(csId, out isNew);
item = GetMediaItemById<MediaBrowser.Controller.Entities.TV.Series>(csId, null, out isNew);
}
else if (isAudio)
{
// TODO determine if this is an AudioBook od Audio
item = GetMediaItemById<MediaBrowser.Controller.Entities.Audio.Audio>(csId, out isNew);
item = GetMediaItemById<MediaBrowser.Controller.Entities.Audio.Audio>(csId, null, out isNew);
}
else if (media.info_labels?.mediatype == "tvshow")
{
item = GetMediaItemById<MediaBrowser.Controller.Entities.TV.Episode>(csId, out isNew);
item = GetMediaItemById<MediaBrowser.Controller.Entities.TV.Episode>(csId, null, out isNew);
}
else
{
item = GetMediaItemById<MediaBrowser.Controller.Entities.Movies.Movie>(csId, out isNew);
item = GetMediaItemById<StreamCinemaMovie>(csId, null, out isNew);
}
if (isNew && media.info_labels != null)
@@ -291,7 +296,9 @@ public abstract class StreamCinemaFilterFolder : Folder
//item.ParentIndexNumber = info.ParentIndexNumber;
item.PremiereDate = media.info_labels?.premiered;
item.ProductionYear = media.info_labels?.year;
item.ProviderIds = media.services != null ? ConvertProviderIds(media.services) : null;
item.ProviderIds = new Dictionary<string, string>();
if (media.services != null)
ConvertProviderIds(media.services, item.ProviderIds);
//item.OfficialRating = info.OfficialRating;
item.DateCreated = media.info_labels?.dateadded ?? DateTime.UtcNow;
//item.Tags = info.Tags.ToArray();
@@ -308,6 +315,9 @@ public abstract class StreamCinemaFilterFolder : Folder
}
item.SetImagePath(ImageType.Primary, artUri.ToString());
}
// Identify the item as it originates from StreamCinema
item.ProviderIds.Add(StreamCinemaPlugin.StreamCinemaProviderName, "");
}
if (item is IHasArtist hasArtists)
@@ -412,16 +422,14 @@ public abstract class StreamCinemaFilterFolder : Folder
return preferred ?? fallback ?? first;
}
private static Dictionary<string, string> ConvertProviderIds(ServicesIds services)
private static void ConvertProviderIds(ServicesIds services, Dictionary<string, string> dest)
{
Dictionary<string, string> result = new Dictionary<string, string>();
if (services.imdb != null)
result.Add("Imdb", services.imdb);
dest.Add("Imdb", services.imdb);
if (services.tvdb != null)
result.Add("Tvdb", services.tvdb);
dest.Add("Tvdb", services.tvdb);
if (services.tmdb != null)
result.Add("Tmdb", services.tmdb);
return result;
dest.Add("Tmdb", services.tmdb);
//public string? csfd { get; set; }
//public string? trakt { get; set; }
@@ -434,10 +442,21 @@ public abstract class StreamCinemaFilterFolder : Folder
return System.IO.Path.Combine(basePath, "streamcinema", id.ToString("N", CultureInfo.InvariantCulture), "metadata");
}
private T GetMediaItemById<T>(string idString, out bool isNew)
where T : BaseItem, new()
/// <summary>
/// Calculates Jellyfin item identifier from CinemaStream identifier, optionally also with version identifier.
/// </summary>
internal static Guid GetMediaItemId(string csPrimaryId, string? csVersionId) {
string idS = csVersionId == null ? csPrimaryId : (csPrimaryId + VersionSeparator + csVersionId);
return StreamCinemaHost.LibraryManager.GetNewItemId(idS, typeof(StreamCinemaPlugin));
}
/// <summary>
/// Creates a new media item (non-folder) with a proper unique identifier.
/// </summary>
internal static T GetMediaItemById<T>(string csPrimaryId, string? csVersionId, out bool isNew)
where T : BaseItem, new()
{
Guid id = StreamCinemaHost.LibraryManager.GetNewItemId(idString, typeof(StreamCinemaPlugin));
Guid id = GetMediaItemId(csPrimaryId, csVersionId);
T? item = StreamCinemaHost.LibraryManager.GetItemById(id) as T;
if (item == null)

View File

@@ -0,0 +1,12 @@
namespace Jellyfin.Plugin.StreamCinema;
class StreamCinemaInnerMediaSourceManager : IStreamCinemaInnerMediaSourceManager
{
private readonly Type _innerType;
public StreamCinemaInnerMediaSourceManager(Type innerType) {
this._innerType = innerType;
}
public Type InnerType => _innerType;
}

View File

@@ -0,0 +1,417 @@
#pragma warning disable CA1002
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using StreamCinemaLib.API;
namespace Jellyfin.Plugin.StreamCinema;
public class StreamCinemaMediaSourceManager : IMediaSourceManager
{
private static readonly TimeSpan VersionValidityTimeout = TimeSpan.FromMinutes(60);
private static StreamCinemaMediaSourceManager? _instance;
private readonly IMediaSourceManager _inner;
private readonly ILibraryManager _libraryManager;
private readonly ConcurrentDictionary<Guid, VersionSetEntry> _videoVersions;
public StreamCinemaMediaSourceManager(IStreamCinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServiceProvider svc)
{
if (innerMediaSourceManager == null || svc == null)
throw new ArgumentNullException();
IMediaSourceManager? inner = svc.GetService(innerMediaSourceManager.InnerType) as IMediaSourceManager;
if (inner == null)
throw new InvalidOperationException("Original MediaSourceManager service not found.");
this._inner = inner;
this._libraryManager = libraryManager;
this._videoVersions = new ConcurrentDictionary<Guid, VersionSetEntry>();
_instance = this;
}
internal static bool TryGetInstance([NotNullWhen(true)] out StreamCinemaMediaSourceManager? instance)
{
instance = _instance;
return instance != null;
}
#region IMediaSourceManager Members
public Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken)
{
return _inner.AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, addProbeDelay, isLiveStream, cancellationToken);
}
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
{
_inner.AddParts(providers);
}
public Task CloseLiveStream(string id)
{
return _inner.CloseLiveStream(id);
}
public Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
{
return _inner.GetLiveStream(id, cancellationToken);
}
public ILiveStream GetLiveStreamInfo(string id)
{
return _inner.GetLiveStreamInfo(id);
}
public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId)
{
return _inner.GetLiveStreamInfoByUniqueId(uniqueId);
}
public Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken)
{
return _inner.GetLiveStreamMediaInfo(id, cancellationToken);
}
public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
{
return _inner.GetLiveStreamWithDirectStreamProvider(id, cancellationToken);
}
public List<MediaAttachment> GetMediaAttachments(Guid itemId)
{
return _inner.GetMediaAttachments(itemId);
}
public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
{
return _inner.GetMediaAttachments(query);
}
public Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken)
{
return _inner.GetMediaSource(item, mediaSourceId, liveStreamId, enablePathSubstitution, cancellationToken);
}
public List<MediaStream> GetMediaStreams(Guid itemId)
{
// Intercept for StreamCinemaItems
Video? item = _libraryManager.GetItemById<Video>(itemId);
if (item == null
|| item.ProviderIds == null
|| !item.ProviderIds.ContainsKey(StreamCinemaPlugin.StreamCinemaProviderName)
|| string.IsNullOrEmpty(item.ExternalId))
return _inner.GetMediaStreams(itemId);
StreamCinemaLib.API.Stream? stream = GetVersion(item, default).GetAwaiter().GetResult();
List<MediaStream> result = new List<MediaStream>();
if (stream != null)
{
// HACK: Those values are read after we return (not before)
item.Size = stream.size;
//stream_id;
//stream.name;
//stream.provider
//stream.ident;
//stream.media
//stream.date_added
if (stream.video != null)
{
foreach (StreamVideo j in stream.video)
{
MediaStream a = new MediaStream();
result.Add(a);
a.Type = MediaStreamType.Video;
a.Width = j.width;
a.Height = j.height;
a.Codec = j.codec;
a.AspectRatio = j.aspect.ToString();
ConvertHdr(j.hasHdr, j.hdrQuality, a);
// HACK: Those values are read after we return (not before)
item.RunTimeTicks = (long?)(j.duration * TimeSpan.TicksPerSecond);
if (j.is3d)
// Just a probability guess
item.Video3DFormat = Video3DFormat.HalfSideBySide;
}
}
if (stream.audio != null)
{
foreach (StreamAudio j in stream.audio)
{
MediaStream a = new MediaStream();
result.Add(a);
a.Type = MediaStreamType.Audio;
a.Language = j.language;
a.Codec = j.codec;
a.Channels = j.channels;
}
}
if (stream.subtitles != null)
{
foreach (StreamSubtitle j in stream.subtitles)
{
MediaStream a = new MediaStream();
result.Add(a);
a.Type = MediaStreamType.Subtitle;
a.Language = j.language;
a.IsForced = j.forced;
a.Path = j.src;
}
}
}
return result;
}
public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
{
return _inner.GetMediaStreams(query);
}
public MediaProtocol GetPathProtocol(string path)
{
return _inner.GetPathProtocol(path);
}
public Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
return _inner.GetPlaybackMediaSources(item, user, allowMediaProbe, enablePathSubstitution, cancellationToken);
}
public Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
{
return _inner.GetRecordingStreamMediaSources(info, cancellationToken);
}
public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User? user = null)
{
return _inner.GetStaticMediaSources(item, enablePathSubstitution, user);
}
public Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
{
return _inner.OpenLiveStream(request, cancellationToken);
}
public Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
{
return _inner.OpenLiveStreamInternal(request, cancellationToken);
}
public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
{
_inner.SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
}
public bool SupportsDirectStream(string path, MediaProtocol protocol)
{
return _inner.SupportsDirectStream(path, protocol);
}
#endregion
/// <summary>
/// Gets stream metadata for a video version (ie. for Movie or Episode).
/// </summary>
internal async Task<IEnumerable<T>?> GetVideoVersions<T>(T item, CancellationToken cancel)
where T : Video, new()
{
if (item == null)
throw new ArgumentNullException();
VersionSetEntry ver = await GetVersionSet(item, out BaseItem? primary, cancel);
return ver.Streams == null ? null : GetVideoVersionsEnumerate(item, primary!, ver.Streams);
}
private IEnumerable<T> GetVideoVersionsEnumerate<T>(T item, BaseItem primary, StreamCinemaLib.API.Stream[] versions)
where T : Video, new()
{
bool isFirst = true;
foreach (var i in versions)
{
T a;
if (isFirst)
{
// The primary item represents the first version
a = (T)primary;
isFirst = false;
}
else
{
a = StreamCinemaFilterFolder.GetMediaItemById<T>(item.ExternalId, i._id, out bool isNew);
if (isNew)
{
// Copy properties from the parent version
// Note: non-primary versions are never persisted so a new volatile instance got created
Guid aId = a.Id;
item.DeepCopy(a);
a.Id = aId;
a.SetPrimaryVersionId(item.Id.ToString("N", CultureInfo.InvariantCulture));
// Just a volatile item
_libraryManager.RegisterItem(a);
}
}
yield return a;
}
}
/// <summary>
/// Gets a version set for a video.
/// </summary>
private Task<VersionSetEntry> GetVersionSet(Video item, out BaseItem? primary, CancellationToken cancel)
{
if (item == null)
throw new ArgumentNullException();
if (item.ProviderIds == null
|| !item.ProviderIds.ContainsKey(StreamCinemaPlugin.StreamCinemaProviderName)
|| string.IsNullOrEmpty(item.ExternalId))
{
// Not a Stream Cinema video
primary = null;
return Task.FromResult<VersionSetEntry>(default);
}
Guid primaryId;
string csId;
if (!string.IsNullOrEmpty(item.PrimaryVersionId))
{
// Move to the primary item
primaryId = Guid.Parse(item.PrimaryVersionId, CultureInfo.InvariantCulture);
primary = _libraryManager.GetItemById(primaryId);
if (primary == null
|| primary.ProviderIds == null
|| !primary.ProviderIds.ContainsKey(StreamCinemaPlugin.StreamCinemaProviderName)
|| string.IsNullOrEmpty(primary.ExternalId))
// Not a Stream Cinema video
return Task.FromResult<VersionSetEntry>(default);
csId = primary.ExternalId;
}
else
{
primary = item;
primaryId = item.Id;
csId = item.ExternalId;
}
return GetVersionSet(primaryId, csId, cancel);
}
private async Task<VersionSetEntry> GetVersionSet(Guid primaryId, string csId, CancellationToken cancel)
{
DateTime now = DateTime.UtcNow;
VersionSetEntry result;
if (!_videoVersions.TryGetValue(primaryId, out result) || result.ValidUntil < now)
{
result.Streams = await Metadata.StreamsAsync(csId, cancel);
result.ValidUntil = now + VersionValidityTimeout;
_videoVersions[primaryId] = result;
}
return result;
}
private async Task<StreamCinemaLib.API.Stream?> GetVersion(Video item, CancellationToken cancel)
{
VersionSetEntry vers = await GetVersionSet(item, out BaseItem? primary, cancel);
if (vers.Streams == null)
return null;
bool isFirst = true;
foreach (var i in vers.Streams)
{
Guid id;
if (isFirst)
{
// The primary item represents the first version
isFirst = false;
id = primary!.Id;
}
else
id = StreamCinemaFilterFolder.GetMediaItemId(primary!.ExternalId, i._id);
if (id == item.Id)
return i;
}
return null;
}
private static void ConvertHdr(bool isHdr, string? hdr, MediaStream dst)
{
if (!isHdr)
return;
if (hdr != null)
{
// Examples:
// Dolby Vision
// Dolby Vision / SMPTE ST 2094 App 4
// Dolby Vision / SMPTE ST 2086
// SMPTE ST 2086
string[] sections = hdr.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (sections.Length != 0)
{
string lastValue = sections[sections.Length - 1];
if (lastValue != "Dolby Vision")
{
dst.ColorTransfer = lastValue;
switch (lastValue)
{
case "SMPTE ST 2094 App 4":
// DoVi Profile 5
dst.DvProfile = 5;
dst.RpuPresentFlag = 1;
dst.BlPresentFlag = 1;
dst.DvBlSignalCompatibilityId = 0;
dst.CodecTag = "dovi";
break;
case "SMPTE ST 2084":
// HDR10
dst.ColorTransfer = "smpte2084";
break;
case "SMPTE ST 2086":
// fall through for basic HDR level
break;
}
}
}
}
// Indicate the "lowest" HDR level
// See function GetVideoColorRange in MediaBrowser.Model/Entities/MediaStream.cs in Jellyfin for reasons
dst.DvProfile = 10;
dst.RpuPresentFlag = 1;
dst.BlPresentFlag = 1;
dst.DvBlSignalCompatibilityId = 0;
dst.CodecTag = "dovi";
return;
}
struct VersionSetEntry
{
internal DateTime ValidUntil;
internal StreamCinemaLib.API.Stream[]? Streams;
}
}

View File

@@ -0,0 +1,31 @@
/*
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using StreamCinemaLib.API;
namespace Jellyfin.Plugin.StreamCinema;
/// <summary>
/// Media source provider for for Stream Cinema media items.
/// </summary>
public class StreamCinemaMediaSourceProvider : 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();
}
}
*/

View File

@@ -0,0 +1,21 @@
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Model.Dto;
namespace Jellyfin.Plugin.StreamCinema;
/// <summary>
/// Movie media item from Stream Cinema.
/// </summary>
public class StreamCinemaMovie : Movie {
public sealed override string GetClientTypeName() => BaseItemKind.Movie.ToString();
protected override IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources() {
var result = this.VideoGetAllItemsForMediaSources();
if (result == null)
return base.GetAllItemsForMediaSources();
else
return result;
}
}

View File

@@ -15,6 +15,8 @@ namespace Jellyfin.Plugin.StreamCinema;
/// </summary>
public class StreamCinemaPlugin : BasePlugin<StreamCinemaPluginConfiguration>, IHasWebPages
{
internal const string StreamCinemaProviderName = "StreamCinema";
public StreamCinemaPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{

View File

@@ -1,5 +1,6 @@
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.DependencyInjection;
@@ -12,10 +13,31 @@ namespace Jellyfin.Plugin.StreamCinema;
///
public class StreamCinemaServiceRegistrator : IPluginServiceRegistrator
{
/// <inheritdoc />
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{
serviceCollection.AddSingleton<IImageProvider, StreamCinemaImageProvider>();
serviceCollection.AddHostedService<StreamCinemaHost>();
}
/// <inheritdoc />
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{
serviceCollection.AddSingleton<IImageProvider, StreamCinemaImageProvider>();
serviceCollection.AddHostedService<StreamCinemaHost>();
// HACK: Replace IMediaSourceManager as StreamCinemaMediaSourceProvider is not called soon enough while
// entering a media item page.
Type tIf = typeof(IMediaSourceManager);
int count = serviceCollection.Count;
for (int i = 0; i < count; i++)
{
ServiceDescriptor a = serviceCollection[i];
if (a.ServiceType == tIf && !a.IsKeyedService)
{
Type oldImplType = a.ImplementationType!;
serviceCollection.RemoveAt(i);
serviceCollection.AddSingleton<IStreamCinemaInnerMediaSourceManager>(new StreamCinemaInnerMediaSourceManager(oldImplType));
serviceCollection.AddSingleton(oldImplType, oldImplType);
// Replace IMediaSourceManager but also need direct access to the StreamCinemaMediaSourceManager if it itself
// also gets replaced in a longer chain
serviceCollection.AddSingleton<StreamCinemaMediaSourceManager>();
serviceCollection.AddSingleton<IMediaSourceManager>(x => x.GetRequiredService<StreamCinemaMediaSourceManager>());
break;
}
}
}
}

View File

@@ -0,0 +1,36 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
namespace Jellyfin.Plugin.StreamCinema;
static class StreamCinemaVideoExtensions
{
public static IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)>? VideoGetAllItemsForMediaSources<T>(this T @this)
where T : Video, new()
{
// Non-primary version items use default implementation
if (!string.IsNullOrEmpty(@this.PrimaryVersionId))
{
return null; // means call base.GetAllItemsForMediaSources
}
// Our media source manager must be present
StreamCinemaMediaSourceManager? msm;
if (!StreamCinemaMediaSourceManager.TryGetInstance(out msm))
return Enumerable.Empty<(BaseItem Item, MediaSourceType MediaSourceType)>();
// Generate item for each StreamCinemaLib.API.Stream. Obtaining the file sub-streams themselves
// is intercepted in StreamCinemaMediaSourceManager.GetMediaStreams
return GetAllItemsForMediaSourcesEnumerate(@this, msm);
}
private static IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSourcesEnumerate<T>(T @this, StreamCinemaMediaSourceManager msm)
where T : Video, new()
{
IEnumerable<T>? versions = msm.GetVideoVersions(@this, default).GetAwaiter().GetResult();
if (versions != null)
foreach (T i in versions)
yield return (i, MediaSourceType.Grouping);
}
}