TV Series playback 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:
33
CinemaJellyfin/CinemaAnimeFolder.cs
Normal file
33
CinemaJellyfin/CinemaAnimeFolder.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using CinemaLib.API;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
sealed class CinemaAnimeFolder : CinemaRootFolder
|
||||
{
|
||||
public CinemaAnimeFolder()
|
||||
{
|
||||
}
|
||||
|
||||
public override CollectionType? CollectionType => Data.Enums.CollectionType.movies;
|
||||
|
||||
public override BaseItemKind ClientType => BaseItemKind.Movie;
|
||||
|
||||
public override ItemType ItemType => ItemType.Anime;
|
||||
|
||||
internal override string ImageName => "anime.png";
|
||||
|
||||
protected override IEnumerable<BaseItem> GetFilterItems()
|
||||
{
|
||||
// Root items
|
||||
// none
|
||||
yield break;
|
||||
}
|
||||
|
||||
public override bool TryCreateMediaItem(MediaSource? media, string csId, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item)
|
||||
{
|
||||
return media.TryCreateMediaItem<CinemaTvSeries>(csId, parentFolder, false, out item);
|
||||
}
|
||||
}
|
||||
0
CinemaJellyfin/CinemaAudio.cs
Normal file
0
CinemaJellyfin/CinemaAudio.cs
Normal file
33
CinemaJellyfin/CinemaConcertFolder.cs
Normal file
33
CinemaJellyfin/CinemaConcertFolder.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using CinemaLib.API;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
sealed class CinemaConcertFolder : CinemaRootFolder
|
||||
{
|
||||
public CinemaConcertFolder()
|
||||
{
|
||||
}
|
||||
|
||||
public override CollectionType? CollectionType => Data.Enums.CollectionType.music;
|
||||
|
||||
public override BaseItemKind ClientType => BaseItemKind.MusicAlbum;
|
||||
|
||||
public override ItemType ItemType => ItemType.Concert;
|
||||
|
||||
internal override string ImageName => "music.png";
|
||||
|
||||
protected override IEnumerable<BaseItem> GetFilterItems()
|
||||
{
|
||||
// Root items
|
||||
// none
|
||||
yield break;
|
||||
}
|
||||
|
||||
public override bool TryCreateMediaItem(MediaSource? media, string csId, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item)
|
||||
{
|
||||
return media.TryCreateMediaItem<CinemaMusicAlbum>(csId, parentFolder, true, out item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
/// <summary>
|
||||
/// Episode media item from Cinema.
|
||||
/// </summary>
|
||||
public class CinemaEpisode : Episode
|
||||
{
|
||||
public sealed override string GetClientTypeName() => BaseItemKind.Episode.ToString();
|
||||
|
||||
protected override IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
|
||||
{
|
||||
var result = this.VideoGetAllItemsForMediaSources();
|
||||
if (result == null)
|
||||
return base.GetAllItemsForMediaSources();
|
||||
else
|
||||
return result;
|
||||
}
|
||||
|
||||
public override List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
|
||||
{
|
||||
var result = this.VideoGetMediaSources(enablePathSubstitution);
|
||||
if (result == null)
|
||||
return base.GetMediaSources(enablePathSubstitution);
|
||||
else
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,6 @@ namespace Jellyfin.Plugin.Cinema;
|
||||
/// </summary>
|
||||
public abstract class CinemaFilterFolder : Folder
|
||||
{
|
||||
private const string PreferredCulture = "cs";
|
||||
private const string FallbackCulture = "en";
|
||||
private const char VersionSeparator = '-';
|
||||
|
||||
/// <summary>
|
||||
/// Filtering folders can never be deleted.
|
||||
/// </summary>
|
||||
@@ -54,6 +50,17 @@ public abstract class CinemaFilterFolder : Folder
|
||||
/// </summary>
|
||||
protected abstract IEnumerable<BaseItem> GetFilterItems();
|
||||
|
||||
/// <summary>
|
||||
/// Tries to create a Jellyfin media item from the provided Cinema media item.
|
||||
/// </summary>
|
||||
/// <param name="csId">Cinema media identifier.</param>
|
||||
/// <param name="media">Cinema metadata.</param>
|
||||
/// <param name="parentFolder">Jellyfin parent folder.</param>
|
||||
/// <param name="item">On success the created item.</param>
|
||||
/// <returns>True on success, false otherwise.</returns>
|
||||
/// <remarks>Implementation shall just choose proper container type and call <see cref="CinemaQueryExtensions.TryCreateMediaItem"/></remarks>
|
||||
public abstract bool TryCreateMediaItem(MediaSource? media, string csId, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item);
|
||||
|
||||
/// <summary>
|
||||
/// Gets static as well as dynamic items from Stream Cinema database.
|
||||
/// </summary>
|
||||
@@ -95,7 +102,7 @@ public abstract class CinemaFilterFolder : Folder
|
||||
else
|
||||
{
|
||||
(ItemSortBy sortByJ, SortOrder sortDirJ) = query.OrderBy.First();
|
||||
sortBy = ConvertSort(sortByJ);
|
||||
sortBy = sortByJ.ToCinema();
|
||||
sortDir = sortDirJ == SortOrder.Ascending ? ItemOrder.Ascending : ItemOrder.Descending;
|
||||
}
|
||||
|
||||
@@ -106,7 +113,7 @@ public abstract class CinemaFilterFolder : Folder
|
||||
result.TotalRecordCount = (int)filterRes.hits.total.value;
|
||||
foreach (var i in filterRes.hits.hits)
|
||||
{
|
||||
if (TryCreateMediaItem(i._id, i._source, this, out BaseItem? a))
|
||||
if (TryCreateMediaItem(i._source, i._id, this, out BaseItem? a))
|
||||
items.Add(a);
|
||||
}
|
||||
}
|
||||
@@ -114,46 +121,6 @@ public abstract class CinemaFilterFolder : Folder
|
||||
return result;
|
||||
}
|
||||
|
||||
private static FilterSortBy ConvertSort(ItemSortBy sortByJ)
|
||||
{
|
||||
switch (sortByJ)
|
||||
{
|
||||
default:
|
||||
case ItemSortBy.Default: return FilterSortBy.Title;
|
||||
case ItemSortBy.AiredEpisodeOrder: return FilterSortBy.Premiered;
|
||||
//case ItemSortBy.Album: return
|
||||
//case ItemSortBy.AlbumArtist: return
|
||||
//case ItemSortBy.Artist: return
|
||||
case ItemSortBy.DateCreated: return FilterSortBy.DateAdded;
|
||||
case ItemSortBy.OfficialRating: return FilterSortBy.Score;
|
||||
case ItemSortBy.DatePlayed: return FilterSortBy.LastSeen;
|
||||
case ItemSortBy.PremiereDate: return FilterSortBy.Premiered;
|
||||
case ItemSortBy.StartDate: return FilterSortBy.Premiered;
|
||||
case ItemSortBy.SortName: return FilterSortBy.Title;
|
||||
case ItemSortBy.Name: return FilterSortBy.Title;
|
||||
case ItemSortBy.Random: return FilterSortBy.Trending;
|
||||
//case ItemSortBy.Runtime: return
|
||||
case ItemSortBy.CommunityRating: return FilterSortBy.Popularity;
|
||||
case ItemSortBy.ProductionYear: return FilterSortBy.Year;
|
||||
case ItemSortBy.PlayCount: return FilterSortBy.PlayCount;
|
||||
case ItemSortBy.CriticRating: return FilterSortBy.Score;
|
||||
//case ItemSortBy.IsFolder:
|
||||
//case ItemSortBy.IsUnplayed:
|
||||
//case ItemSortBy.IsPlayed:
|
||||
case ItemSortBy.SeriesSortName: return FilterSortBy.Title;
|
||||
//case ItemSortBy.VideoBitRate:
|
||||
//case ItemSortBy.AirTime:
|
||||
//case ItemSortBy.Studio:
|
||||
//case ItemSortBy.IsFavoriteOrLiked:
|
||||
case ItemSortBy.DateLastContentAdded: return FilterSortBy.LastChildrenDateAdded;
|
||||
case ItemSortBy.SeriesDatePlayed: return FilterSortBy.LastChildPremiered;
|
||||
//case ItemSortBy.ParentIndexNumber:
|
||||
//case ItemSortBy.IndexNumber:
|
||||
case ItemSortBy.SimilarityScore: return FilterSortBy.Score;
|
||||
case ItemSortBy.SearchScore: return FilterSortBy.Score;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an already persisted instance or creates a new instance of the given child filter folder.
|
||||
/// </summary>
|
||||
@@ -221,281 +188,10 @@ public abstract class CinemaFilterFolder : Folder
|
||||
return folder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to create a Jellyfin media item from the provided Cinema Stream media item.
|
||||
/// </summary>
|
||||
/// <param name="csId">Cinema Stream media identifier.</param>
|
||||
/// <param name="media">Cinema Stream metadata.</param>
|
||||
/// <param name="parentFolder">Jellyfin parent folder.</param>
|
||||
/// <param name="item">On success the created item.</param>
|
||||
/// <returns>True on success, false otherwise.</returns>
|
||||
private bool TryCreateMediaItem(string csId, MediaSource? media, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item)
|
||||
{
|
||||
if (media == null)
|
||||
{
|
||||
item = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var parentFolderId = parentFolder.Id;
|
||||
|
||||
bool isNew;
|
||||
bool forceUpdate = false;
|
||||
|
||||
bool isAudio = media.is_concert ?? false;
|
||||
if (media.children_count != 0)
|
||||
{
|
||||
// Series, season or a music album
|
||||
FilterResponse? res = Metadata.ChildrenAsync(csId, sort: FilterSortBy.Episode).GetAwaiter().GetResult();
|
||||
if (res == null || res.hits == null || res.hits.hits == null || res.hits.hits.Count == 0)
|
||||
{
|
||||
item = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool singleLevel = res.hits.hits[0]._source?.children_count == 0;
|
||||
if (isAudio)
|
||||
item = GetMediaItemById<MediaBrowser.Controller.Entities.Audio.MusicAlbum>(csId, null, out isNew);
|
||||
else if (singleLevel)
|
||||
item = GetMediaItemById<MediaBrowser.Controller.Entities.TV.Season>(csId, null, out isNew);
|
||||
else
|
||||
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, null, out isNew);
|
||||
|
||||
}
|
||||
else if (media.info_labels?.mediatype == "tvshow")
|
||||
{
|
||||
item = GetMediaItemById<MediaBrowser.Controller.Entities.TV.Episode>(csId, null, out isNew);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
item = GetMediaItemById<CinemaMovie>(csId, null, out isNew);
|
||||
}
|
||||
|
||||
if (isNew && media.info_labels != null)
|
||||
{
|
||||
item.RunTimeTicks = (long?)(media.info_labels.duration * TimeSpan.TicksPerSecond);
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
InfoLabelI18n? loc = GetLocalized(media);
|
||||
item.Name = loc?.title ?? media.info_labels?.originaltitle;
|
||||
item.Genres = media.info_labels?.genre?.ToArray();
|
||||
item.Studios = media.info_labels?.studio?.ToArray();
|
||||
item.CommunityRating = (float?)media.ratings?.overall?.rating;
|
||||
item.Overview = loc?.plot;
|
||||
// TODO
|
||||
// item.IndexNumber = info.IndexNumber;
|
||||
//item.ParentIndexNumber = info.ParentIndexNumber;
|
||||
item.PremiereDate = media.info_labels?.premiered;
|
||||
item.ProductionYear = media.info_labels?.year;
|
||||
if (media.services != null) {
|
||||
item.ProviderIds = new Dictionary<string, string>();
|
||||
ConvertProviderIds(media.services, item.ProviderIds);
|
||||
}
|
||||
//item.OfficialRating = info.OfficialRating;
|
||||
item.DateCreated = media.info_labels?.dateadded ?? DateTime.UtcNow;
|
||||
//item.Tags = info.Tags.ToArray();
|
||||
item.OriginalTitle = media.info_labels?.originaltitle;
|
||||
|
||||
string? artUriS = loc?.art?.poster;
|
||||
Uri? artUri;
|
||||
if (artUriS != null)
|
||||
{
|
||||
artUri = new Uri(artUriS);
|
||||
if (Metadata.TryGetThumbnail(artUri, 128, 128, out Uri? artThumbUri))
|
||||
{
|
||||
item.SetImagePath(ImageType.Thumb, artThumbUri.ToString());
|
||||
}
|
||||
item.SetImagePath(ImageType.Primary, artUri.ToString());
|
||||
}
|
||||
|
||||
artUriS = loc?.art?.fanart;
|
||||
if (artUriS != null)
|
||||
{
|
||||
artUri = new Uri(artUriS);
|
||||
item.SetImagePath(ImageType.Backdrop, artUri.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// Indicate just HTTP CinemaMediaSourceManager and CinemaMediaSourceController will handle the rest
|
||||
item.Path = "https://a/b";
|
||||
|
||||
if (item is IHasArtist hasArtists)
|
||||
{
|
||||
hasArtists.Artists = media.info_labels?.writer;
|
||||
}
|
||||
|
||||
if (item is IHasAlbumArtist hasAlbumArtists)
|
||||
{
|
||||
hasAlbumArtists.AlbumArtists = media.info_labels?.director;
|
||||
}
|
||||
|
||||
item.ParentId = parentFolderId;
|
||||
|
||||
/*
|
||||
if (item is IHasSeries hasSeries)
|
||||
{
|
||||
if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
forceUpdate = true;
|
||||
_logger.LogDebug("Forcing update due to SeriesName {0}", item.Name);
|
||||
}
|
||||
|
||||
hasSeries.SeriesName = info.SeriesName;
|
||||
}*/
|
||||
|
||||
item.ExternalId = CinemaIdToExternalId(csId);
|
||||
|
||||
/*
|
||||
if (item is Audio channelAudioItem)
|
||||
{
|
||||
channelAudioItem.ExtraType = info.ExtraType;
|
||||
|
||||
var mediaSource = info.MediaSources.FirstOrDefault();
|
||||
item.Path = mediaSource?.Path;
|
||||
}
|
||||
|
||||
if (item is Video channelVideoItem)
|
||||
{
|
||||
channelVideoItem.ExtraType = info.ExtraType;
|
||||
|
||||
var mediaSource = info.MediaSources.FirstOrDefault();
|
||||
item.Path = mediaSource?.Path;
|
||||
}*/
|
||||
|
||||
item.OnMetadataChanged();
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
// HACK: We use RegisterItem that is volatile intead of CreateItem
|
||||
//_libraryManager.CreateItem(item, parentFolder);
|
||||
CinemaHost.LibraryManager.RegisterItem(item);
|
||||
|
||||
if (media.cast != null && media.cast.Count > 0)
|
||||
{
|
||||
//await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (forceUpdate)
|
||||
{
|
||||
// HACK our items are volatile
|
||||
//item.UpdateToRepositoryAsync(ItemUpdateType.None, default).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/*
|
||||
if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media)
|
||||
{
|
||||
if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
|
||||
{
|
||||
await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
|
||||
}
|
||||
}*/
|
||||
|
||||
/*
|
||||
if (isNew || forceUpdate || item.DateLastRefreshed == default)
|
||||
{
|
||||
_providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
|
||||
}*/
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private InfoLabelI18n? GetLocalized(MediaSource media)
|
||||
{
|
||||
if (media.i18n_info_labels == null)
|
||||
return null;
|
||||
InfoLabelI18n? first = null;
|
||||
InfoLabelI18n? preferred = null;
|
||||
InfoLabelI18n? fallback = null;
|
||||
foreach (InfoLabelI18n i in media.i18n_info_labels)
|
||||
if (i.lang == PreferredCulture)
|
||||
preferred = i;
|
||||
else if (i.lang == FallbackCulture)
|
||||
fallback = i;
|
||||
else if (first == null)
|
||||
first = i;
|
||||
|
||||
return preferred ?? fallback ?? first;
|
||||
}
|
||||
|
||||
private static void ConvertProviderIds(ServicesIds services, Dictionary<string, string> dest)
|
||||
{
|
||||
if (services.imdb != null)
|
||||
dest.Add("Imdb", services.imdb);
|
||||
if (services.tvdb != null)
|
||||
dest.Add("Tvdb", services.tvdb);
|
||||
if (services.tmdb != null)
|
||||
dest.Add("Tmdb", services.tmdb);
|
||||
|
||||
//public string? csfd { get; set; }
|
||||
//public string? trakt { get; set; }
|
||||
//public string? trakt_with_type { get; set; }
|
||||
//public string? slug { get; set; }
|
||||
}
|
||||
|
||||
internal static string GetInternalMetadataPath(string basePath, Guid id)
|
||||
{
|
||||
return System.IO.Path.Combine(basePath, "cinema", id.ToString("N", CultureInfo.InvariantCulture), "metadata");
|
||||
}
|
||||
|
||||
/// <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 CinemaHost.LibraryManager.GetNewItemId(idS, typeof(CinemaPlugin));
|
||||
}
|
||||
|
||||
/// <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 = GetMediaItemId(csPrimaryId, csVersionId);
|
||||
T? item = CinemaHost.LibraryManager.GetItemById(id) as T;
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
item = new T();
|
||||
isNew = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isNew = false;
|
||||
}
|
||||
|
||||
item.Id = id;
|
||||
return item;
|
||||
}
|
||||
|
||||
internal static string CinemaIdToExternalId(string csId) {
|
||||
return CinemaPlugin.CinemaExtIdPrefix + csId;
|
||||
}
|
||||
|
||||
internal static bool IsCinemaExternalId(string? externalId) {
|
||||
return externalId != null && externalId.StartsWith(CinemaPlugin.CinemaExtIdPrefix);
|
||||
}
|
||||
|
||||
internal static bool TryGetCinemaIdFromExternalId(string? externalId, [NotNullWhen(true)] out string? csId) {
|
||||
if (IsCinemaExternalId(externalId)) {
|
||||
csId = externalId!.Substring(CinemaPlugin.CinemaExtIdPrefix.Length);
|
||||
return true;
|
||||
} else {
|
||||
csId = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,18 @@ public sealed class CinemaHost : IHostedService
|
||||
|
||||
public static IFileSystem FileSystem => _fileSystem;
|
||||
|
||||
public static string PreferredCulture => _config.Configuration.PreferredMetadataLanguage;
|
||||
|
||||
public static string FallbackCulture => "en";
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Make sure the Stream Cinema root folders are created
|
||||
CinemaFilterFolder.CreateRootFilterFolder<CinemaMoviesFolder>("Movies");
|
||||
CinemaFilterFolder.CreateRootFilterFolder<CinemaTvShowsFolder>("TV Shows");
|
||||
CinemaFilterFolder.CreateRootFilterFolder<CinemaAnimeFolder>("Anime");
|
||||
CinemaFilterFolder.CreateRootFilterFolder<CinemaConcertFolder>("Music");
|
||||
|
||||
/*
|
||||
pluginItems.Add(CreateMenuItem("movies", Resources.Movies, GetResourceUrl("movies.png")));
|
||||
|
||||
12
CinemaJellyfin/CinemaInnerLibraryManager.cs
Normal file
12
CinemaJellyfin/CinemaInnerLibraryManager.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
class CinemaInnerLibraryManager
|
||||
{
|
||||
private readonly Type _innerType;
|
||||
|
||||
public CinemaInnerLibraryManager(Type innerType) {
|
||||
this._innerType = innerType;
|
||||
}
|
||||
|
||||
public Type InnerType => _innerType;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
class CinemaInnerMediaSourceManager : ICinemaInnerMediaSourceManager
|
||||
public class CinemaInnerMediaSourceManager
|
||||
{
|
||||
private readonly Type _innerType;
|
||||
|
||||
|
||||
505
CinemaJellyfin/CinemaLibraryManager.cs
Normal file
505
CinemaJellyfin/CinemaLibraryManager.cs
Normal file
@@ -0,0 +1,505 @@
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Controller.Sorting;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
sealed class CinemaLibraryManager : ILibraryManager
|
||||
{
|
||||
private readonly ILibraryManager _inner;
|
||||
|
||||
public CinemaLibraryManager(CinemaInnerLibraryManager innerLibraryManager, IServiceProvider svc)
|
||||
{
|
||||
if (innerLibraryManager == null || svc == null)
|
||||
throw new ArgumentNullException();
|
||||
|
||||
ILibraryManager? inner = svc.GetService(innerLibraryManager.InnerType) as ILibraryManager;
|
||||
if (inner == null)
|
||||
throw new InvalidOperationException("Original LibraryManager service not found.");
|
||||
this._inner = inner;
|
||||
|
||||
}
|
||||
|
||||
#region ILibraryManager Members
|
||||
|
||||
public AggregateFolder RootFolder => _inner.RootFolder;
|
||||
|
||||
public bool IsScanRunning => _inner.IsScanRunning;
|
||||
|
||||
public event EventHandler<ItemChangeEventArgs>? ItemAdded {
|
||||
add => _inner.ItemAdded += value;
|
||||
remove => _inner.ItemAdded -= value;
|
||||
}
|
||||
public event EventHandler<ItemChangeEventArgs>? ItemUpdated {
|
||||
add => _inner.ItemUpdated += value;
|
||||
remove => _inner.ItemUpdated -= value;
|
||||
}
|
||||
public event EventHandler<ItemChangeEventArgs>? ItemRemoved {
|
||||
add => _inner.ItemRemoved += value;
|
||||
remove => _inner.ItemRemoved -= value;
|
||||
}
|
||||
|
||||
public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
|
||||
{
|
||||
_inner.AddMediaPath(virtualFolderName, mediaPath);
|
||||
}
|
||||
|
||||
public void AddParts(IEnumerable<IResolverIgnoreRule> rules, IEnumerable<IItemResolver> resolvers, IEnumerable<IIntroProvider> introProviders, IEnumerable<IBaseItemComparer> itemComparers, IEnumerable<ILibraryPostScanTask> postscanTasks)
|
||||
{
|
||||
_inner.AddParts(rules, resolvers, introProviders, itemComparers, postscanTasks);
|
||||
}
|
||||
|
||||
public Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
|
||||
{
|
||||
return _inner.AddVirtualFolder(name, collectionType, options, refreshLibrary);
|
||||
}
|
||||
|
||||
public Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool removeOnFailure = true)
|
||||
{
|
||||
return _inner.ConvertImageToLocal(item, image, imageIndex, removeOnFailure);
|
||||
}
|
||||
|
||||
public void CreateItem(BaseItem item, BaseItem? parent)
|
||||
{
|
||||
_inner.CreateItem(item, parent);
|
||||
}
|
||||
|
||||
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
|
||||
{
|
||||
_inner.CreateItems(items, parent, cancellationToken);
|
||||
}
|
||||
|
||||
public void DeleteItem(BaseItem item, DeleteOptions options)
|
||||
{
|
||||
_inner.DeleteItem(item, options);
|
||||
}
|
||||
|
||||
public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem)
|
||||
{
|
||||
_inner.DeleteItem(item, options, notifyParentItem);
|
||||
}
|
||||
|
||||
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
|
||||
{
|
||||
_inner.DeleteItem(item, options, parent, notifyParentItem);
|
||||
}
|
||||
|
||||
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
|
||||
{
|
||||
return _inner.FillMissingEpisodeNumbersFromPath(episode, forceRefresh);
|
||||
}
|
||||
|
||||
public BaseItem? FindByPath(string path, bool? isFolder)
|
||||
{
|
||||
return _inner.FindByPath(path, isFolder);
|
||||
}
|
||||
|
||||
public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||
{
|
||||
return _inner.FindExtras(owner, fileSystemChildren, directoryService);
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
|
||||
{
|
||||
return _inner.GetAlbumArtists(query);
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
|
||||
{
|
||||
return _inner.GetAllArtists(query);
|
||||
}
|
||||
|
||||
public MusicArtist GetArtist(string name)
|
||||
{
|
||||
return _inner.GetArtist(name);
|
||||
}
|
||||
|
||||
public MusicArtist GetArtist(string name, DtoOptions options)
|
||||
{
|
||||
return _inner.GetArtist(name, options);
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
|
||||
{
|
||||
return _inner.GetArtists(query);
|
||||
}
|
||||
|
||||
public List<Folder> GetCollectionFolders(BaseItem item)
|
||||
{
|
||||
return _inner.GetCollectionFolders(item);
|
||||
}
|
||||
|
||||
public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
|
||||
{
|
||||
return _inner.GetCollectionFolders(item, allUserRootChildren);
|
||||
}
|
||||
|
||||
public CollectionType? GetConfiguredContentType(BaseItem item)
|
||||
{
|
||||
return _inner.GetConfiguredContentType(item);
|
||||
}
|
||||
|
||||
public CollectionType? GetConfiguredContentType(string path)
|
||||
{
|
||||
return _inner.GetConfiguredContentType(path);
|
||||
}
|
||||
|
||||
public CollectionType? GetContentType(BaseItem item)
|
||||
{
|
||||
return _inner.GetContentType(item);
|
||||
}
|
||||
|
||||
public int GetCount(InternalItemsQuery query)
|
||||
{
|
||||
return _inner.GetCount(query);
|
||||
}
|
||||
|
||||
public Genre GetGenre(string name)
|
||||
{
|
||||
return _inner.GetGenre(name);
|
||||
}
|
||||
|
||||
public Guid GetGenreId(string name)
|
||||
{
|
||||
return _inner.GetGenreId(name);
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
|
||||
{
|
||||
return _inner.GetGenres(query);
|
||||
}
|
||||
|
||||
public CollectionType? GetInheritedContentType(BaseItem item)
|
||||
{
|
||||
return _inner.GetInheritedContentType(item);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
|
||||
{
|
||||
return _inner.GetIntros(item, user);
|
||||
}
|
||||
|
||||
public BaseItem? GetItemById(Guid id)
|
||||
{
|
||||
return _inner.GetItemById(id);
|
||||
}
|
||||
|
||||
public T? GetItemById<T>(Guid id) where T : BaseItem
|
||||
{
|
||||
return _inner.GetItemById<T>(id);
|
||||
}
|
||||
|
||||
public T? GetItemById<T>(Guid id, Guid userId) where T : BaseItem
|
||||
{
|
||||
return _inner.GetItemById<T>(id, userId);
|
||||
}
|
||||
|
||||
public T? GetItemById<T>(Guid id, User? user) where T : BaseItem
|
||||
{
|
||||
return _inner.GetItemById<T>(id, user);
|
||||
}
|
||||
|
||||
public List<Guid> GetItemIds(InternalItemsQuery query)
|
||||
{
|
||||
return _inner.GetItemIds(query);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetItemList(InternalItemsQuery query)
|
||||
{
|
||||
string? serieOrSeasonIdS;
|
||||
Guid serieOrSeasonId;
|
||||
BaseItem? serieOrSeason;
|
||||
if (query.IncludeItemTypes == null
|
||||
|| query.IncludeItemTypes.Length != 1
|
||||
|| query.IncludeItemTypes[0] != BaseItemKind.Episode
|
||||
|| (serieOrSeasonIdS = query.AncestorWithPresentationUniqueKey ?? query.SeriesPresentationUniqueKey) == null
|
||||
|| !Guid.TryParse(serieOrSeasonIdS, out serieOrSeasonId)
|
||||
|| (serieOrSeason = _inner.GetItemById(serieOrSeasonId)) == null
|
||||
|| !CinemaQueryExtensions.IsCinemaExternalId(serieOrSeason.ExternalId))
|
||||
return _inner.GetItemList(query);
|
||||
|
||||
// HACK: Necessary until GetEpisodes is virtual
|
||||
switch (serieOrSeason) {
|
||||
case CinemaSeason season: return new List<BaseItem>(season.GetItemList(query));
|
||||
case CinemaTvSeries series:
|
||||
List<BaseItem> result = new List<BaseItem>();
|
||||
foreach (BaseItem i in series.GetItemList(new InternalItemsQuery())) {
|
||||
switch (i) {
|
||||
case CinemaEpisode episode: result.Add(episode); break;
|
||||
case CinemaSeason season: result.AddRange(season.GetItemList(query)); break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
default: return _inner.GetItemList(query);
|
||||
}
|
||||
}
|
||||
|
||||
public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
|
||||
{
|
||||
return _inner.GetItemList(query, allowExternalContent);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
|
||||
{
|
||||
return _inner.GetItemList(query, parents);
|
||||
}
|
||||
|
||||
public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
|
||||
{
|
||||
return _inner.GetItemsResult(query);
|
||||
}
|
||||
|
||||
public LibraryOptions GetLibraryOptions(BaseItem item)
|
||||
{
|
||||
return _inner.GetLibraryOptions(item);
|
||||
}
|
||||
|
||||
public MusicGenre GetMusicGenre(string name)
|
||||
{
|
||||
return _inner.GetMusicGenre(name);
|
||||
}
|
||||
|
||||
public Guid GetMusicGenreId(string name)
|
||||
{
|
||||
return _inner.GetMusicGenreId(name);
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
|
||||
{
|
||||
return _inner.GetMusicGenres(query);
|
||||
}
|
||||
|
||||
public UserView GetNamedView(User user, string name, Guid parentId, CollectionType? viewType, string sortName)
|
||||
{
|
||||
return _inner.GetNamedView(user, name, parentId, viewType, sortName);
|
||||
}
|
||||
|
||||
public UserView GetNamedView(User user, string name, CollectionType? viewType, string sortName)
|
||||
{
|
||||
return _inner.GetNamedView(user, name, viewType, sortName);
|
||||
}
|
||||
|
||||
public UserView GetNamedView(string name, CollectionType viewType, string sortName)
|
||||
{
|
||||
return _inner.GetNamedView(name, viewType, sortName);
|
||||
}
|
||||
|
||||
public UserView GetNamedView(string name, Guid parentId, CollectionType? viewType, string sortName, string uniqueId)
|
||||
{
|
||||
return _inner.GetNamedView(name, parentId, viewType, sortName, uniqueId);
|
||||
}
|
||||
|
||||
public Guid GetNewItemId(string key, Type type)
|
||||
{
|
||||
return _inner.GetNewItemId(key, type);
|
||||
}
|
||||
|
||||
public BaseItem GetParentItem(Guid? parentId, Guid? userId)
|
||||
{
|
||||
return _inner.GetParentItem(parentId, userId);
|
||||
}
|
||||
|
||||
public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem = null)
|
||||
{
|
||||
return _inner.GetPathAfterNetworkSubstitution(path, ownerItem);
|
||||
}
|
||||
|
||||
public List<PersonInfo> GetPeople(BaseItem item)
|
||||
{
|
||||
return _inner.GetPeople(item);
|
||||
}
|
||||
|
||||
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
|
||||
{
|
||||
return _inner.GetPeople(query);
|
||||
}
|
||||
|
||||
public List<Person> GetPeopleItems(InternalPeopleQuery query)
|
||||
{
|
||||
return _inner.GetPeopleItems(query);
|
||||
}
|
||||
|
||||
public List<string> GetPeopleNames(InternalPeopleQuery query)
|
||||
{
|
||||
return _inner.GetPeopleNames(query);
|
||||
}
|
||||
|
||||
public Person? GetPerson(string name)
|
||||
{
|
||||
return _inner.GetPerson(name);
|
||||
}
|
||||
|
||||
public int? GetSeasonNumberFromPath(string path)
|
||||
{
|
||||
return _inner.GetSeasonNumberFromPath(path);
|
||||
}
|
||||
|
||||
public UserView GetShadowView(BaseItem parent, CollectionType? viewType, string sortName)
|
||||
{
|
||||
return _inner.GetShadowView(parent, viewType, sortName);
|
||||
}
|
||||
|
||||
public Studio GetStudio(string name)
|
||||
{
|
||||
return _inner.GetStudio(name);
|
||||
}
|
||||
|
||||
public Guid GetStudioId(string name)
|
||||
{
|
||||
return _inner.GetStudioId(name);
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
|
||||
{
|
||||
return GetStudios(query);
|
||||
}
|
||||
|
||||
public Folder GetUserRootFolder()
|
||||
{
|
||||
return _inner.GetUserRootFolder();
|
||||
}
|
||||
|
||||
public List<VirtualFolderInfo> GetVirtualFolders()
|
||||
{
|
||||
return _inner.GetVirtualFolders();
|
||||
}
|
||||
|
||||
public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
|
||||
{
|
||||
return _inner.GetVirtualFolders(includeRefreshState);
|
||||
}
|
||||
|
||||
public Year GetYear(int value)
|
||||
{
|
||||
return _inner.GetYear(value);
|
||||
}
|
||||
|
||||
public bool IgnoreFile(FileSystemMetadata file, BaseItem parent)
|
||||
{
|
||||
return _inner.IgnoreFile(file, parent);
|
||||
}
|
||||
|
||||
public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
|
||||
{
|
||||
return _inner.NormalizeRootPathList(paths);
|
||||
}
|
||||
|
||||
public ItemLookupInfo ParseName(string name)
|
||||
{
|
||||
return _inner.ParseName(name);
|
||||
}
|
||||
|
||||
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
|
||||
{
|
||||
return _inner.QueryItems(query);
|
||||
}
|
||||
|
||||
public void QueueLibraryScan()
|
||||
{
|
||||
_inner.QueueLibraryScan();
|
||||
}
|
||||
|
||||
public void RegisterItem(BaseItem item)
|
||||
{
|
||||
_inner.RegisterItem(item);
|
||||
}
|
||||
|
||||
public void RemoveMediaPath(string virtualFolderName, string mediaPath)
|
||||
{
|
||||
_inner.RemoveMediaPath(virtualFolderName, mediaPath);
|
||||
}
|
||||
|
||||
public Task RemoveVirtualFolder(string name, bool refreshLibrary)
|
||||
{
|
||||
return _inner.RemoveVirtualFolder(name, refreshLibrary);
|
||||
}
|
||||
|
||||
public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null)
|
||||
{
|
||||
return _inner.ResolvePath(fileInfo, parent, directoryService);
|
||||
}
|
||||
|
||||
public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null)
|
||||
{
|
||||
return _inner.ResolvePaths(files, directoryService, parent, libraryOptions, collectionType);
|
||||
}
|
||||
|
||||
public BaseItem RetrieveItem(Guid id)
|
||||
{
|
||||
return _inner.RetrieveItem(id);
|
||||
}
|
||||
|
||||
public Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
|
||||
{
|
||||
return _inner.RunMetadataSavers(item, updateReason);
|
||||
}
|
||||
|
||||
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
|
||||
{
|
||||
return _inner.Sort(items, user, sortBy, sortOrder);
|
||||
}
|
||||
|
||||
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
|
||||
{
|
||||
return _inner.Sort(items, user, orderBy);
|
||||
}
|
||||
|
||||
public Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
|
||||
{
|
||||
return _inner.UpdateImagesAsync(item, forceUpdate);
|
||||
}
|
||||
|
||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
{
|
||||
return _inner.UpdateItemAsync(item, parent, updateReason, cancellationToken);
|
||||
}
|
||||
|
||||
public Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
{
|
||||
return _inner.UpdateItemsAsync(items, parent, updateReason, cancellationToken);
|
||||
}
|
||||
|
||||
public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
|
||||
{
|
||||
_inner.UpdateMediaPath(virtualFolderName, mediaPath);
|
||||
}
|
||||
|
||||
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
|
||||
{
|
||||
_inner.UpdatePeople(item, people);
|
||||
}
|
||||
|
||||
public Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
|
||||
{
|
||||
return _inner.UpdatePeopleAsync(item, people, cancellationToken);
|
||||
}
|
||||
|
||||
public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return _inner.ValidateMediaLibrary(progress, cancellationToken);
|
||||
}
|
||||
|
||||
public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return _inner.ValidatePeopleAsync(progress, cancellationToken);
|
||||
}
|
||||
|
||||
public Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
||||
{
|
||||
return _inner.ValidateTopLibraryFolders(cancellationToken, removeRoot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -7,7 +7,6 @@ using System.Globalization;
|
||||
using System.Text;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
@@ -22,7 +21,7 @@ using MediaBrowser.Controller;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
public sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
{
|
||||
private const bool IsWebshareFreeAccount = true;
|
||||
private const double BitrateMargin = 0.1; // 10 %
|
||||
@@ -30,6 +29,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
|
||||
private static readonly TimeSpan VersionValidityTimeout = TimeSpan.FromMinutes(180);
|
||||
private static readonly TimeSpan LinkValidityTimeout = VersionValidityTimeout;
|
||||
internal static readonly TimeSpan SubfolderValidityTimeout = VersionValidityTimeout;
|
||||
|
||||
private static CinemaMediaSourceManager? _instance;
|
||||
|
||||
@@ -40,7 +40,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
private readonly ConcurrentDictionary<Guid, VersionSetEntry> _videoVersions;
|
||||
private readonly ConcurrentDictionary<LinkKey, LinkEntry> _links;
|
||||
|
||||
public CinemaMediaSourceManager(ICinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServerApplicationHost host, IServiceProvider svc, IHttpContextAccessor http)
|
||||
public CinemaMediaSourceManager(CinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServerApplicationHost host, IServiceProvider svc, IHttpContextAccessor http)
|
||||
{
|
||||
if (innerMediaSourceManager == null || svc == null)
|
||||
throw new ArgumentNullException();
|
||||
@@ -148,7 +148,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
HttpContext? ctx = _http.HttpContext;
|
||||
if (ctx == null
|
||||
|| item == null
|
||||
|| !CinemaFilterFolder.IsCinemaExternalId(item.ExternalId)
|
||||
|| !CinemaQueryExtensions.IsCinemaExternalId(item.ExternalId)
|
||||
|| string.IsNullOrEmpty(item.ExternalId))
|
||||
return _inner.GetStaticMediaSources(item, enablePathSubstitution, user);
|
||||
|
||||
@@ -167,7 +167,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
HttpContext? ctx = _http.HttpContext;
|
||||
if (ctx == null
|
||||
|| item == null
|
||||
|| !CinemaFilterFolder.IsCinemaExternalId(item.ExternalId)
|
||||
|| !CinemaQueryExtensions.IsCinemaExternalId(item.ExternalId)
|
||||
|| string.IsNullOrEmpty(item.ExternalId))
|
||||
return await _inner.GetPlaybackMediaSources(item, user, allowMediaProbe, enablePathSubstitution, cancellationToken);
|
||||
|
||||
@@ -183,6 +183,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
switch (video)
|
||||
{
|
||||
case CinemaMovie movie: items = GetVideoVersionsEnumerate<CinemaMovie>(csId!, movie, videoPrimary!, ver.Versions); break;
|
||||
case CinemaEpisode episode: items = GetVideoVersionsEnumerate<CinemaEpisode>(csId!, episode, videoPrimary!, ver.Versions); break;
|
||||
default: throw new NotSupportedException(string.Format("BaseItem type '{0}' not supported in CinemaMediaSources.", video.GetType().Name));
|
||||
}
|
||||
|
||||
@@ -249,7 +250,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
}
|
||||
else
|
||||
{
|
||||
a = CinemaFilterFolder.GetMediaItemById<T>(csId, i.Meta._id, out bool isNew);
|
||||
a = CinemaQueryExtensions.GetMediaItemById<T>(csId, i.Meta._id, out bool isNew);
|
||||
if (isNew)
|
||||
{
|
||||
// Copy properties from the parent version
|
||||
@@ -275,7 +276,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
if (item == null)
|
||||
throw new ArgumentNullException();
|
||||
|
||||
if (!CinemaFilterFolder.IsCinemaExternalId(item.ExternalId)
|
||||
if (!CinemaQueryExtensions.IsCinemaExternalId(item.ExternalId)
|
||||
|| string.IsNullOrEmpty(item.ExternalId))
|
||||
{
|
||||
// Not a Stream Cinema video
|
||||
@@ -292,7 +293,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
primaryId = Guid.Parse(item.PrimaryVersionId, CultureInfo.InvariantCulture);
|
||||
primary = _libraryManager.GetItemById(primaryId);
|
||||
if (primary == null
|
||||
|| !CinemaFilterFolder.IsCinemaExternalId(primary.ExternalId)
|
||||
|| !CinemaQueryExtensions.IsCinemaExternalId(primary.ExternalId)
|
||||
|| string.IsNullOrEmpty(primary.ExternalId)) {
|
||||
// Not a Stream Cinema video
|
||||
csId = null;
|
||||
@@ -308,7 +309,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
externalId = item.ExternalId;
|
||||
}
|
||||
|
||||
if (!CinemaFilterFolder.TryGetCinemaIdFromExternalId(externalId, out csId))
|
||||
if (!CinemaQueryExtensions.TryGetCinemaIdFromExternalId(externalId, out csId))
|
||||
throw new InvalidOperationException("Cannot parse Cinema identifier.");
|
||||
|
||||
return GetVersionSet(primaryId, csId, cancel);
|
||||
@@ -352,7 +353,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
|
||||
id = primary!.Id;
|
||||
}
|
||||
else
|
||||
id = CinemaFilterFolder.GetMediaItemId(csId!, i.Meta._id);
|
||||
id = CinemaQueryExtensions.GetMediaItemId(csId!, i.Meta._id);
|
||||
|
||||
if (id == item.Id)
|
||||
return i;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using CinemaLib.API;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
@@ -9,8 +9,8 @@ sealed class CinemaMoviesFolder : CinemaRootFolder
|
||||
{
|
||||
private readonly BaseItem _trending;
|
||||
private readonly BaseItem _popular;
|
||||
private readonly BaseItem _mostWatched;
|
||||
private readonly BaseItem _newReleases;
|
||||
//private readonly BaseItem _mostWatched;
|
||||
//private readonly BaseItem _newReleases;
|
||||
|
||||
public CinemaMoviesFolder()
|
||||
{
|
||||
@@ -36,4 +36,9 @@ sealed class CinemaMoviesFolder : CinemaRootFolder
|
||||
//yield return _mostWatched!;
|
||||
//yield return _newReleases!;
|
||||
}
|
||||
|
||||
public override bool TryCreateMediaItem(MediaSource? media, string csId, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item)
|
||||
{
|
||||
return media.TryCreateMediaItem<Folder>(csId, parentFolder, false, out item);
|
||||
}
|
||||
}
|
||||
68
CinemaJellyfin/CinemaMusicAlbum.cs
Normal file
68
CinemaJellyfin/CinemaMusicAlbum.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using CinemaLib.API;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
/// <summary>
|
||||
/// Music album folder item from Cinema.
|
||||
/// </summary>
|
||||
public sealed class CinemaMusicAlbum : MusicAlbum
|
||||
{
|
||||
public sealed override string GetClientTypeName() => BaseItemKind.Series.ToString();
|
||||
|
||||
public override int GetChildCount(User user)
|
||||
{
|
||||
return base.GetChildCount(user);
|
||||
}
|
||||
|
||||
public override int GetRecursiveChildCount(User user)
|
||||
{
|
||||
return base.GetRecursiveChildCount(user);
|
||||
}
|
||||
|
||||
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||
{
|
||||
return (List<BaseItem>)GetItemsInternal(query).Items;
|
||||
}
|
||||
|
||||
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
||||
{
|
||||
int offset = query.StartIndex ?? 0;
|
||||
int limit = query.Limit ?? 0;
|
||||
|
||||
List<BaseItem> items = new List<BaseItem>();
|
||||
QueryResult<BaseItem> result = new QueryResult<BaseItem>() { Items = items, StartIndex = offset };
|
||||
|
||||
FilterSortBy sortBy;
|
||||
ItemOrder sortDir;
|
||||
if (query.OrderBy.Count == 0)
|
||||
{
|
||||
sortBy = FilterSortBy.Episode;
|
||||
sortDir = ItemOrder.Ascending;
|
||||
}
|
||||
else
|
||||
{
|
||||
(ItemSortBy sortByJ, SortOrder sortDirJ) = query.OrderBy.First();
|
||||
sortBy = sortByJ.ToCinema();
|
||||
sortDir = sortDirJ == SortOrder.Ascending ? ItemOrder.Ascending : ItemOrder.Descending;
|
||||
}
|
||||
|
||||
FilterResponse? filterRes = Metadata.ChildrenAsync(query.SearchTerm ?? "", order: sortDir, sort: sortBy, offset: offset, limit: limit).GetAwaiter().GetResult();
|
||||
if (filterRes != null && filterRes.hits != null && filterRes.hits.hits != null)
|
||||
{
|
||||
if (filterRes.hits.total != null)
|
||||
result.TotalRecordCount = (int)filterRes.hits.total.value;
|
||||
foreach (var i in filterRes.hits.hits)
|
||||
{
|
||||
if (i._source.TryCreateMediaItem<Folder>(i._id, this, false, out BaseItem? a))
|
||||
items.Add(a);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using CinemaLib.API;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
@@ -18,4 +19,9 @@ public sealed class CinemaPopularFolder : CinemaSortFolder
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
public override bool TryCreateMediaItem(MediaSource? media, string csId, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item)
|
||||
{
|
||||
return media.TryCreateMediaItem<Folder>(csId, parentFolder, false, out item);
|
||||
}
|
||||
}
|
||||
334
CinemaJellyfin/CinemaQueryExtensions.cs
Normal file
334
CinemaJellyfin/CinemaQueryExtensions.cs
Normal file
@@ -0,0 +1,334 @@
|
||||
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using CinemaLib.API;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
static class CinemaQueryExtensions
|
||||
{
|
||||
private const char VersionSeparator = '-';
|
||||
|
||||
public static FilterSortBy ToCinema(this ItemSortBy sortByJ)
|
||||
{
|
||||
switch (sortByJ)
|
||||
{
|
||||
default:
|
||||
case ItemSortBy.Default: return FilterSortBy.Title;
|
||||
case ItemSortBy.AiredEpisodeOrder: return FilterSortBy.Premiered;
|
||||
//case ItemSortBy.Album: return
|
||||
//case ItemSortBy.AlbumArtist: return
|
||||
//case ItemSortBy.Artist: return
|
||||
case ItemSortBy.DateCreated: return FilterSortBy.DateAdded;
|
||||
case ItemSortBy.OfficialRating: return FilterSortBy.Score;
|
||||
case ItemSortBy.DatePlayed: return FilterSortBy.LastSeen;
|
||||
case ItemSortBy.PremiereDate: return FilterSortBy.Premiered;
|
||||
case ItemSortBy.StartDate: return FilterSortBy.Premiered;
|
||||
case ItemSortBy.SortName: return FilterSortBy.Title;
|
||||
case ItemSortBy.Name: return FilterSortBy.Title;
|
||||
case ItemSortBy.Random: return FilterSortBy.Trending;
|
||||
//case ItemSortBy.Runtime: return
|
||||
case ItemSortBy.CommunityRating: return FilterSortBy.Popularity;
|
||||
case ItemSortBy.ProductionYear: return FilterSortBy.Year;
|
||||
case ItemSortBy.PlayCount: return FilterSortBy.PlayCount;
|
||||
case ItemSortBy.CriticRating: return FilterSortBy.Score;
|
||||
//case ItemSortBy.IsFolder:
|
||||
//case ItemSortBy.IsUnplayed:
|
||||
//case ItemSortBy.IsPlayed:
|
||||
case ItemSortBy.SeriesSortName: return FilterSortBy.Title;
|
||||
//case ItemSortBy.VideoBitRate:
|
||||
//case ItemSortBy.AirTime:
|
||||
//case ItemSortBy.Studio:
|
||||
//case ItemSortBy.IsFavoriteOrLiked:
|
||||
case ItemSortBy.DateLastContentAdded: return FilterSortBy.LastChildrenDateAdded;
|
||||
case ItemSortBy.SeriesDatePlayed: return FilterSortBy.LastChildPremiered;
|
||||
//case ItemSortBy.ParentIndexNumber:
|
||||
//case ItemSortBy.IndexNumber:
|
||||
case ItemSortBy.SimilarityScore: return FilterSortBy.Score;
|
||||
case ItemSortBy.SearchScore: return FilterSortBy.Score;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to create a Jellyfin media item from the provided Cinema media item.
|
||||
/// </summary>
|
||||
/// <param name="csId">Cinema media identifier.</param>
|
||||
/// <param name="media">Cinema metadata.</param>
|
||||
/// <param name="parentFolder">Jellyfin parent folder.</param>
|
||||
/// <param name="item">On success the created item.</param>
|
||||
/// <returns>True on success, false otherwise.</returns>
|
||||
public static bool TryCreateMediaItem<TContainerType>(this MediaSource? media, string csId, BaseItem parentFolder, bool allowContainer, [NotNullWhen(true)] out BaseItem? item)
|
||||
where TContainerType : Folder, new()
|
||||
{
|
||||
if (media == null)
|
||||
{
|
||||
item = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var parentFolderId = parentFolder.Id;
|
||||
|
||||
bool isNew;
|
||||
bool forceUpdate = false;
|
||||
|
||||
bool isAudio = media.is_concert ?? false;
|
||||
if (media.children_count != 0)
|
||||
{
|
||||
// Container
|
||||
if (!allowContainer) {
|
||||
item = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
item = GetMediaItemById<TContainerType>(csId, null, out isNew);
|
||||
|
||||
}
|
||||
else if (isAudio)
|
||||
{
|
||||
// TODO determine if this is an AudioBook od Audio
|
||||
item = GetMediaItemById<MediaBrowser.Controller.Entities.Audio.Audio>(csId, null, out isNew);
|
||||
|
||||
}
|
||||
else if (media.info_labels?.mediatype == "episode")
|
||||
{
|
||||
item = GetMediaItemById<CinemaEpisode>(csId, null, out isNew);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
item = GetMediaItemById<CinemaMovie>(csId, null, out isNew);
|
||||
}
|
||||
|
||||
if (isNew && media.info_labels != null)
|
||||
{
|
||||
item.RunTimeTicks = (long?)(media.info_labels.duration * TimeSpan.TicksPerSecond);
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
InfoLabelI18n? loc = GetLocalized(media);
|
||||
item.Name = loc?.title ?? media.info_labels?.originaltitle;
|
||||
item.Genres = media.info_labels?.genre?.ToArray();
|
||||
item.Studios = media.info_labels?.studio?.ToArray();
|
||||
item.CommunityRating = (float?)media.ratings?.overall?.rating;
|
||||
item.Overview = loc?.plot;
|
||||
// TODO
|
||||
// item.IndexNumber = info.IndexNumber;
|
||||
//item.ParentIndexNumber = info.ParentIndexNumber;
|
||||
item.PremiereDate = media.info_labels?.premiered;
|
||||
item.ProductionYear = media.info_labels?.year;
|
||||
if (media.services != null)
|
||||
{
|
||||
item.ProviderIds = new Dictionary<string, string>();
|
||||
ConvertProviderIds(media.services, item.ProviderIds);
|
||||
}
|
||||
//item.OfficialRating = info.OfficialRating;
|
||||
item.DateCreated = media.info_labels?.dateadded ?? DateTime.UtcNow;
|
||||
//item.Tags = info.Tags.ToArray();
|
||||
item.OriginalTitle = media.info_labels?.originaltitle;
|
||||
|
||||
string? artUriS = loc?.art?.poster;
|
||||
Uri? artUri;
|
||||
if (artUriS != null)
|
||||
{
|
||||
artUri = new Uri(artUriS);
|
||||
if (Metadata.TryGetThumbnail(artUri, 128, 128, out Uri? artThumbUri))
|
||||
{
|
||||
item.SetImagePath(ImageType.Thumb, artThumbUri.ToString());
|
||||
}
|
||||
item.SetImagePath(ImageType.Primary, artUri.ToString());
|
||||
}
|
||||
|
||||
artUriS = loc?.art?.fanart;
|
||||
if (artUriS != null)
|
||||
{
|
||||
artUri = new Uri(artUriS);
|
||||
item.SetImagePath(ImageType.Backdrop, artUri.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// Indicate just HTTP CinemaMediaSourceManager and CinemaMediaSourceController will handle the rest
|
||||
item.Path = "https://a/b";
|
||||
|
||||
if (item is IHasArtist hasArtists)
|
||||
{
|
||||
hasArtists.Artists = media.info_labels?.writer;
|
||||
}
|
||||
|
||||
if (item is IHasAlbumArtist hasAlbumArtists)
|
||||
{
|
||||
hasAlbumArtists.AlbumArtists = media.info_labels?.director;
|
||||
}
|
||||
|
||||
item.ParentId = parentFolderId;
|
||||
|
||||
/*
|
||||
if (item is IHasSeries hasSeries)
|
||||
{
|
||||
if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
forceUpdate = true;
|
||||
_logger.LogDebug("Forcing update due to SeriesName {0}", item.Name);
|
||||
}
|
||||
|
||||
hasSeries.SeriesName = info.SeriesName;
|
||||
}*/
|
||||
|
||||
item.ExternalId = CinemaIdToExternalId(csId);
|
||||
|
||||
if (item is ICinemaChildrenCount childrenCount) {
|
||||
childrenCount.ChildrenCount = media.children_count;
|
||||
childrenCount.TotalChildrenCount = media.total_children_count;
|
||||
}
|
||||
|
||||
/*
|
||||
if (item is Audio channelAudioItem)
|
||||
{
|
||||
channelAudioItem.ExtraType = info.ExtraType;
|
||||
|
||||
var mediaSource = info.MediaSources.FirstOrDefault();
|
||||
item.Path = mediaSource?.Path;
|
||||
}
|
||||
|
||||
if (item is Video channelVideoItem)
|
||||
{
|
||||
channelVideoItem.ExtraType = info.ExtraType;
|
||||
|
||||
var mediaSource = info.MediaSources.FirstOrDefault();
|
||||
item.Path = mediaSource?.Path;
|
||||
}*/
|
||||
|
||||
item.OnMetadataChanged();
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
// HACK: We use RegisterItem that is volatile intead of CreateItem
|
||||
//_libraryManager.CreateItem(item, parentFolder);
|
||||
CinemaHost.LibraryManager.RegisterItem(item);
|
||||
|
||||
if (media.cast != null && media.cast.Count > 0)
|
||||
{
|
||||
//await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (forceUpdate)
|
||||
{
|
||||
// HACK our items are volatile
|
||||
//item.UpdateToRepositoryAsync(ItemUpdateType.None, default).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/*
|
||||
if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media)
|
||||
{
|
||||
if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
|
||||
{
|
||||
await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
|
||||
}
|
||||
}*/
|
||||
|
||||
/*
|
||||
if (isNew || forceUpdate || item.DateLastRefreshed == default)
|
||||
{
|
||||
_providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
|
||||
}*/
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ConvertProviderIds(ServicesIds services, Dictionary<string, string> dest)
|
||||
{
|
||||
if (services.imdb != null)
|
||||
dest.Add("Imdb", services.imdb);
|
||||
if (services.tvdb != null)
|
||||
dest.Add("Tvdb", services.tvdb);
|
||||
if (services.tmdb != null)
|
||||
dest.Add("Tmdb", services.tmdb);
|
||||
|
||||
//public string? csfd { get; set; }
|
||||
//public string? trakt { get; set; }
|
||||
//public string? trakt_with_type { get; set; }
|
||||
//public string? slug { get; set; }
|
||||
}
|
||||
|
||||
private static InfoLabelI18n? GetLocalized(MediaSource media)
|
||||
{
|
||||
if (media.i18n_info_labels == null)
|
||||
return null;
|
||||
InfoLabelI18n? first = null;
|
||||
InfoLabelI18n? preferred = null;
|
||||
InfoLabelI18n? fallback = null;
|
||||
foreach (InfoLabelI18n i in media.i18n_info_labels)
|
||||
if (i.lang == CinemaHost.PreferredCulture)
|
||||
preferred = i;
|
||||
else if (i.lang == CinemaHost.FallbackCulture)
|
||||
fallback = i;
|
||||
else if (first == null)
|
||||
first = i;
|
||||
|
||||
return preferred ?? fallback ?? first;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Jellyfin item identifier from Cinema identifier, optionally also with version identifier.
|
||||
/// </summary>
|
||||
internal static Guid GetMediaItemId(string csPrimaryId, string? csVersionId)
|
||||
{
|
||||
string idS = csVersionId == null ? csPrimaryId : (csPrimaryId + VersionSeparator + csVersionId);
|
||||
return CinemaHost.LibraryManager.GetNewItemId(idS, typeof(CinemaPlugin));
|
||||
}
|
||||
|
||||
/// <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 = GetMediaItemId(csPrimaryId, csVersionId);
|
||||
T? item = CinemaHost.LibraryManager.GetItemById(id) as T;
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
item = new T();
|
||||
isNew = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isNew = false;
|
||||
}
|
||||
|
||||
item.Id = id;
|
||||
return item;
|
||||
}
|
||||
|
||||
internal static string CinemaIdToExternalId(string csId)
|
||||
{
|
||||
return CinemaPlugin.CinemaExtIdPrefix + csId;
|
||||
}
|
||||
|
||||
internal static bool IsCinemaExternalId(string? externalId)
|
||||
{
|
||||
return externalId != null && externalId.StartsWith(CinemaPlugin.CinemaExtIdPrefix);
|
||||
}
|
||||
|
||||
internal static bool TryGetCinemaIdFromExternalId(string? externalId, [NotNullWhen(true)] out string? csId)
|
||||
{
|
||||
if (IsCinemaExternalId(externalId))
|
||||
{
|
||||
csId = externalId!.Substring(CinemaPlugin.CinemaExtIdPrefix.Length);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
csId = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
CinemaJellyfin/CinemaSeason.cs
Normal file
69
CinemaJellyfin/CinemaSeason.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Globalization;
|
||||
using CinemaLib.API;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
/// <summary>
|
||||
/// Season folder item from Cinema.
|
||||
/// </summary>
|
||||
public sealed class CinemaSeason : Season, ICinemaChildrenCount
|
||||
{
|
||||
private CinemaSubfolderHelper _helper;
|
||||
|
||||
public CinemaSeason() {
|
||||
this._helper = new CinemaSubfolderHelper(this, FilterSortBy.Episode);
|
||||
}
|
||||
|
||||
#region ICinemaChildrenCountMembers
|
||||
|
||||
public int ChildrenCount
|
||||
{
|
||||
get => _helper.ChildrenCount;
|
||||
set => _helper.ChildrenCount = value;
|
||||
}
|
||||
|
||||
public int TotalChildrenCount {
|
||||
get => _helper.TotalChildrenCount;
|
||||
set => _helper.TotalChildrenCount = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public sealed override string GetClientTypeName() => BaseItemKind.Season.ToString();
|
||||
|
||||
public override int GetChildCount(User user)
|
||||
{
|
||||
return _helper.ChildrenCount;
|
||||
}
|
||||
|
||||
public override int GetRecursiveChildCount(User user)
|
||||
{
|
||||
return _helper.TotalChildrenCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Always false as it causes iteration of all our items that degrades performance
|
||||
/// in the CinemaTvShowsFolder.
|
||||
/// </summary>
|
||||
public override bool SupportsPlayedStatus => false;
|
||||
|
||||
/// <summary>
|
||||
/// Let the hack in <see cref="CinemaLibraryManager"/> find us.
|
||||
/// </summary>
|
||||
public override string CreatePresentationUniqueKey() => Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||
{
|
||||
return (List<BaseItem>)_helper.GetItemsInternal<Folder>(query, false).Items;
|
||||
}
|
||||
|
||||
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
||||
{
|
||||
return _helper.GetItemsInternal<Folder>(query, false);
|
||||
}
|
||||
}
|
||||
@@ -21,16 +21,16 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
|
||||
|
||||
// HACK: Replace IMediaSourceManager as CinemaMediaSourceProvider is not called soon enough while
|
||||
// entering a media item page.
|
||||
Type tIf = typeof(IMediaSourceManager);
|
||||
Type tMsm = typeof(IMediaSourceManager);
|
||||
int count = serviceCollection.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ServiceDescriptor a = serviceCollection[i];
|
||||
if (a.ServiceType == tIf && !a.IsKeyedService)
|
||||
if (a.ServiceType == tMsm && !a.IsKeyedService)
|
||||
{
|
||||
Type oldImplType = a.ImplementationType!;
|
||||
serviceCollection.RemoveAt(i);
|
||||
serviceCollection.AddSingleton<ICinemaInnerMediaSourceManager>(new CinemaInnerMediaSourceManager(oldImplType));
|
||||
serviceCollection.AddSingleton(new CinemaInnerMediaSourceManager(oldImplType));
|
||||
serviceCollection.AddSingleton(oldImplType, oldImplType);
|
||||
// Replace IMediaSourceManager but also need direct access to the CinemaMediaSourceManager if it itself
|
||||
// also gets replaced in a longer chain
|
||||
@@ -39,5 +39,26 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: Replace ILibraryManager as Series.GetEpisodes is not virtual
|
||||
Type tLm = typeof(ILibraryManager);
|
||||
int count2 = serviceCollection.Count;
|
||||
for (int i = 0; i < count2; i++)
|
||||
{
|
||||
ServiceDescriptor a = serviceCollection[i];
|
||||
if (a.ServiceType == tLm && !a.IsKeyedService)
|
||||
{
|
||||
Type oldImplType = a.ImplementationType!;
|
||||
serviceCollection.RemoveAt(i);
|
||||
serviceCollection.AddSingleton(new CinemaInnerLibraryManager(oldImplType));
|
||||
serviceCollection.AddSingleton(oldImplType, oldImplType);
|
||||
// Replace IMediaSourceManager but also need direct access to the CinemaLibraryManager if it itself
|
||||
// also gets replaced in a longer chain
|
||||
serviceCollection.AddSingleton<CinemaLibraryManager>();
|
||||
serviceCollection.AddSingleton<ILibraryManager>(x => x.GetRequiredService<CinemaLibraryManager>());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
100
CinemaJellyfin/CinemaSubfolderHelper.cs
Normal file
100
CinemaJellyfin/CinemaSubfolderHelper.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using CinemaLib.API;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
/// <summary>
|
||||
/// Helper data for Cinema items that inherit from a <see cref="Folder"/>.
|
||||
/// </summary>
|
||||
struct CinemaSubfolderHelper
|
||||
{
|
||||
private readonly Folder @this;
|
||||
private readonly FilterSortBy _sortBy;
|
||||
|
||||
private int? _childrenCount;
|
||||
private int? _totalChildrenCount;
|
||||
private FilterResponse? _cached;
|
||||
private DateTime _cacheValidTo;
|
||||
|
||||
internal CinemaSubfolderHelper(Folder @this, FilterSortBy sortBy)
|
||||
{
|
||||
this.@this = @this;
|
||||
this._sortBy = sortBy;
|
||||
}
|
||||
|
||||
public int ChildrenCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_childrenCount == 0)
|
||||
EnsureCached();
|
||||
return _childrenCount ?? 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
_childrenCount = value;
|
||||
}
|
||||
}
|
||||
|
||||
public int TotalChildrenCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_totalChildrenCount == 0)
|
||||
EnsureCached();
|
||||
return _totalChildrenCount ?? 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
_totalChildrenCount = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal QueryResult<BaseItem> GetItemsInternal<TContainerType>(InternalItemsQuery query, bool allowSubfolders)
|
||||
where TContainerType : Folder, new()
|
||||
{
|
||||
FilterResponse filterRes = EnsureCached();
|
||||
List<BaseItem> items = new List<BaseItem>();
|
||||
QueryResult<BaseItem> result = new QueryResult<BaseItem>() { Items = items };
|
||||
if (filterRes != null && filterRes.hits != null && filterRes.hits.hits != null)
|
||||
{
|
||||
if (filterRes.hits.total != null)
|
||||
result.TotalRecordCount = (int)filterRes.hits.total.value;
|
||||
|
||||
foreach (var i in filterRes.hits.hits)
|
||||
{
|
||||
if (i._source.TryCreateMediaItem<TContainerType>(i._id, @this, allowSubfolders, out BaseItem? a))
|
||||
items.Add(a);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private FilterResponse EnsureCached()
|
||||
{
|
||||
DateTime now = DateTime.UtcNow;
|
||||
if (_cached != null && _cacheValidTo > now)
|
||||
return _cached;
|
||||
|
||||
string? csId;
|
||||
if (!CinemaQueryExtensions.TryGetCinemaIdFromExternalId(@this.ExternalId, out csId))
|
||||
throw new InvalidOperationException("Expected Cinema ID to be present.");
|
||||
|
||||
FilterResponse? filterRes = Metadata.ChildrenAsync(csId, order: ItemOrder.Ascending, sort: _sortBy).GetAwaiter().GetResult();
|
||||
if (filterRes != null && filterRes.hits != null && filterRes.hits.hits != null)
|
||||
{
|
||||
_childrenCount = filterRes.hits.hits.Count;
|
||||
_totalChildrenCount = filterRes.hits.total != null ? (int)filterRes.hits.total.value : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_childrenCount = 0;
|
||||
_totalChildrenCount = 0;
|
||||
}
|
||||
|
||||
_cached = filterRes ?? new FilterResponse();
|
||||
_cacheValidTo = now + CinemaMediaSourceManager.SubfolderValidityTimeout;
|
||||
return _cached;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using CinemaLib.API;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
@@ -18,4 +19,9 @@ public sealed class CinemaTrendingFolder : CinemaSortFolder
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
public override bool TryCreateMediaItem(MediaSource? media, string csId, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item)
|
||||
{
|
||||
return media.TryCreateMediaItem<Folder>(csId, parentFolder, false, out item);
|
||||
}
|
||||
}
|
||||
78
CinemaJellyfin/CinemaTvSeries.cs
Normal file
78
CinemaJellyfin/CinemaTvSeries.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Globalization;
|
||||
using CinemaLib.API;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
/// <summary>
|
||||
/// TV Series folder item from Cinema.
|
||||
/// </summary>
|
||||
public sealed class CinemaTvSeries : Series, ICinemaChildrenCount
|
||||
{
|
||||
private CinemaSubfolderHelper _helper;
|
||||
|
||||
public CinemaTvSeries() {
|
||||
this._helper = new CinemaSubfolderHelper(this, FilterSortBy.Episode);
|
||||
}
|
||||
|
||||
#region ICinemaChildrenCountMembers
|
||||
|
||||
public int ChildrenCount
|
||||
{
|
||||
get => _helper.ChildrenCount;
|
||||
set => _helper.ChildrenCount = value;
|
||||
}
|
||||
|
||||
public int TotalChildrenCount {
|
||||
get => _helper.TotalChildrenCount;
|
||||
set => _helper.TotalChildrenCount = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public sealed override string GetClientTypeName() => BaseItemKind.Series.ToString();
|
||||
|
||||
public override int GetChildCount(User user)
|
||||
{
|
||||
return _helper.ChildrenCount;
|
||||
}
|
||||
|
||||
public override int GetRecursiveChildCount(User user)
|
||||
{
|
||||
return _helper.TotalChildrenCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Always false as it causes iteration of all our items that degrades performance
|
||||
/// in the CinemaTvShowsFolder.
|
||||
/// </summary>
|
||||
public override bool SupportsPlayedStatus => false;
|
||||
|
||||
/// <summary>
|
||||
/// Let the hack in <see cref="CinemaLibraryManager"/> find us.
|
||||
/// </summary>
|
||||
public override string CreatePresentationUniqueKey() => Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||
{
|
||||
return (List<BaseItem>)GetItemsInternal(query).Items;
|
||||
}
|
||||
|
||||
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
||||
{
|
||||
QueryResult<BaseItem> result = _helper.GetItemsInternal<CinemaSeason>(query, true);
|
||||
|
||||
// Set series identifier on all seasons
|
||||
foreach (var i in result.Items)
|
||||
if (i is CinemaSeason season) {
|
||||
season.SeriesId = Id;
|
||||
season.SeriesName = Name;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
33
CinemaJellyfin/CinemaTvShowsFolder.cs
Normal file
33
CinemaJellyfin/CinemaTvShowsFolder.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using CinemaLib.API;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
sealed class CinemaTvShowsFolder : CinemaRootFolder
|
||||
{
|
||||
public CinemaTvShowsFolder()
|
||||
{
|
||||
}
|
||||
|
||||
public override CollectionType? CollectionType => Data.Enums.CollectionType.tvshows;
|
||||
|
||||
public override BaseItemKind ClientType => BaseItemKind.Series;
|
||||
|
||||
public override ItemType ItemType => ItemType.TVShow;
|
||||
|
||||
internal override string ImageName => "tvshow.png";
|
||||
|
||||
protected override IEnumerable<BaseItem> GetFilterItems()
|
||||
{
|
||||
// Root items
|
||||
// none
|
||||
yield break;
|
||||
}
|
||||
|
||||
public override bool TryCreateMediaItem(MediaSource? media, string csId, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item)
|
||||
{
|
||||
return media.TryCreateMediaItem<CinemaTvSeries>(csId, parentFolder, true, out item);
|
||||
}
|
||||
}
|
||||
6
CinemaJellyfin/ICinemaChildrenCount.cs
Normal file
6
CinemaJellyfin/ICinemaChildrenCount.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
public interface ICinemaChildrenCount {
|
||||
int ChildrenCount { get; set; }
|
||||
int TotalChildrenCount { get; set; }
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
public interface ICinemaInnerMediaSourceManager {
|
||||
Type InnerType { get; }
|
||||
}
|
||||
@@ -37,12 +37,6 @@ public class Metadata
|
||||
if (limit == 0 || limit > MaxPageLimit)
|
||||
limit = MaxPageLimit;
|
||||
|
||||
/*
|
||||
bool noSearchTerms = expression.Trim().Length == 0;
|
||||
UriBuilder uri = new UriBuilder(new Uri(ApiFilter, noSearchTerms ? "all" : "search"));
|
||||
uri.Query = $"?access_token={AccessToken}&value={Uri.EscapeDataString(expression)}&order={ToString(order)}&sort={ToString(sort)}&type={ToString(type)}&from={offset.ToString()}&size={limit.ToString()}";
|
||||
*/
|
||||
|
||||
bool noSearchTerms = expression.Trim().Length == 0;
|
||||
string filterName;
|
||||
string sortS = ToString(sort);
|
||||
@@ -73,13 +67,13 @@ public class Metadata
|
||||
/// <param name="sort">Result ordering column.</param>
|
||||
/// <param name="cancel">Asynchronous cancellation.</param>
|
||||
/// <returns>Response.</returns>
|
||||
public async static Task<FilterResponse?> ChildrenAsync(string parentId, FilterSortBy sort = FilterSortBy.Episode, CancellationToken cancel = default)
|
||||
public async static Task<FilterResponse?> ChildrenAsync(string parentId, ItemOrder order = ItemOrder.Descending, FilterSortBy sort = FilterSortBy.Episode, int offset = 0, int limit = 0, CancellationToken cancel = default)
|
||||
{
|
||||
if (parentId == null)
|
||||
throw new ArgumentNullException();
|
||||
|
||||
UriBuilder uri = new UriBuilder(new Uri(ApiFilter, "parent"));
|
||||
uri.Query = $"?access_token={AccessToken}&value={parentId}&sort={ToString(sort)}&size={MaxPageLimit.ToString()}";
|
||||
uri.Query = $"?access_token={AccessToken}&value={parentId}&order={ToString(order)}&sort={ToString(sort)}&from={offset.ToString()}&size={limit.ToString()}";
|
||||
|
||||
FilterResponse? result = await Program._http.GetFromJsonAsync<FilterResponse>(uri.Uri, CreateFilterJsonOptions(), cancel);
|
||||
if (result != null)
|
||||
|
||||
Reference in New Issue
Block a user