StreamCinemaJellyfin initial commit
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2024-11-25 03:21:17 +01:00
parent 116dd2fa35
commit a52a756518
22 changed files with 1035 additions and 1 deletions

View File

@@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreamCinemaLib", "StreamCi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreamCinemaWeb", "StreamCinemaWeb\StreamCinemaWeb.csproj", "{6B2550AF-200C-40B3-95AE-892A604B9A76}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamCinemaJellyfin", "StreamCinemaJellyfin\StreamCinemaJellyfin.csproj", "{E106C0D2-788D-4401-A9F0-9D0A35E8BB63}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
{6B2550AF-200C-40B3-95AE-892A604B9A76}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B2550AF-200C-40B3-95AE-892A604B9A76}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B2550AF-200C-40B3-95AE-892A604B9A76}.Release|Any CPU.Build.0 = Release|Any CPU
{E106C0D2-788D-4401-A9F0-9D0A35E8BB63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E106C0D2-788D-4401-A9F0-9D0A35E8BB63}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E106C0D2-788D-4401-A9F0-9D0A35E8BB63}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E106C0D2-788D-4401-A9F0-9D0A35E8BB63}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Model.Plugins;
namespace Jellyfin.Plugin.StreamCinema.Configuration;
public class StreamCinemaPluginConfiguration : BasePluginConfiguration
{
public StreamCinemaPluginConfiguration()
{
Pin = "0000";
WebServiceUrl = "http://localhost:8866";
EnableDebugLogging = false;
NewEpisodes = false;
RecordingDefault = "2";
RecordingTransport = 1;
EnableInProgress = false;
PollInterval = 20;
BackendVersion = 0;
}
public string WebServiceUrl { get; set; }
public string CurrentWebServiceURL { get; set; }
public int BackendVersion { get; set; }
public string Pin { get; set; }
public string StoredSid { get; set; }
public bool EnableDebugLogging { get; set; }
public bool EnableInProgress { get; set; }
public int PollInterval { get; set; }
public bool NewEpisodes { get; set; }
public bool ShowRepeat { get; set; }
public bool GetEpisodeImage { get; set; }
public string RecordingDefault { get; set; }
public int RecordingTransport { get; set; }
public int PrePaddingSeconds { get; set; }
public int PostPaddingSeconds { get; set; }
public DateTime RecordingModificationTime { get; set; }
}

View File

@@ -0,0 +1,111 @@
using System;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.StreamCinema;
/// <summary>
/// Triggers start-up initialization for the entire Stream Cinema plugin.
/// </summary>
public sealed class Startup : IChannel
{
public Startup(ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem, ILogger<Startup> logger)
{
// Make sure the Stream Cinema root folders are created
CreateRoot<StreamCinemaMoviesFolder>("movies", "Movies", libraryManager, config, fileSystem);
/*
pluginItems.Add(CreateMenuItem("movies", Resources.Movies, GetResourceUrl("movies.png")));
pluginItems.Add(CreateMenuItem("shows", Resources.Shows, GetResourceUrl("tvshows.png")));
pluginItems.Add(CreateMenuItem("tv", Resources.TvProgram, GetResourceUrl("tv-program.png")));
pluginItems.Add(CreateMenuItem("anime", Resources.Anime, GetResourceUrl("anime.png")));
pluginItems.Add(CreateMenuItem("concerts", Resources.Concerts, GetResourceUrl("music.png")));*/
}
private static void CreateRoot<T>(string idPrefix, string localizedName, ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem) where T : StreamCinemaRootFolder, new()
{
string internalMetadataPath = config.ApplicationPaths.InternalMetadataPath;
Guid rootFolderId = libraryManager.GetNewItemId(StreamCinemaRootFolder.GetIdToHash("", idPrefix), typeof(StreamCinemaRootFolder));
string rootFolderPath = StreamCinemaRootFolder.GetInternalMetadataPath(internalMetadataPath, rootFolderId);
Directory.CreateDirectory(rootFolderPath);
StreamCinemaRootFolder? rootFolder = libraryManager.GetItemById(rootFolderId) as StreamCinemaRootFolder;
bool isNew;
bool forceUpdate = false;
if (isNew = rootFolder == null)
{
rootFolder = new T
{
Name = localizedName,
Id = rootFolderId,
DateCreated = fileSystem.GetCreationTimeUtc(rootFolderPath),
DateModified = fileSystem.GetLastWriteTimeUtc(rootFolderPath)
};
rootFolder.Initialize(libraryManager, idPrefix, internalMetadataPath);
}
rootFolder.Path = rootFolderPath;
rootFolder.ParentId = Guid.Empty;
if (isNew)
{
rootFolder.OnMetadataChanged();
libraryManager.CreateItem(rootFolder, null);
}
// We do not bother waiting for the task to finish
rootFolder.RefreshMetadata(
new MetadataRefreshOptions(new DirectoryService(fileSystem))
{
ForceSave = !isNew && forceUpdate
},
default).ConfigureAwait(false);
}
public string Name => "Stream Cinema startup";
public string Description => "";
public string DataVersion => "1";
public string HomePageUrl => "";
public ChannelParentalRating ParentalRating => ChannelParentalRating.GeneralAudience;
public InternalChannelFeatures GetChannelFeatures()
{
return new InternalChannelFeatures
{
// Anything that does not crash Jellyfin
ContentTypes = [ChannelMediaContentType.Movie],
MediaTypes = [ChannelMediaType.Video]
};
}
public Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken)
{
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
public Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
{
return Task.FromResult(new ChannelItemResult() { Items = new List<ChannelItemInfo>() });
}
public IEnumerable<ImageType> GetSupportedChannelImages()
{
return new List<ImageType> { ImageType.Primary };
}
public bool IsEnabledFor(string userId)
{
// Always hide us from user's view as there is no content here
return false;
}
}

View File

@@ -0,0 +1,13 @@
using MediaBrowser.Controller.Entities;
namespace Jellyfin.Plugin.StreamCinema;
public abstract class StreamCinemaFilterFolder : Folder
{
public sealed override bool CanDelete()
{
return false;
}
internal abstract string ImageName { get; }
}

View File

@@ -0,0 +1,36 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
namespace Jellyfin.Plugin.StreamCinema;
/// <summary>
/// An image provider for Stream Cinema icons.
/// </summary>
public class StreamCinemaImageProvider : IDynamicImageProvider
{
/// <inheritdoc />
public string Name => "Stream Cinema Image Provider";
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
yield return ImageType.Thumb;
}
/// <inheritdoc />
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
{
if (item is StreamCinemaFilterFolder filter)
return Task.FromResult(new DynamicImageResponse() { HasImage = true, Stream = typeof(StreamCinemaImageProvider).Assembly.GetManifestResourceStream(typeof(StreamCinemaImageProvider), filter.ImageName) });
else
return Task.FromResult(new DynamicImageResponse() { HasImage = false });
}
/// <inheritdoc />
public bool Supports(BaseItem item)
{
return item is StreamCinemaFilterFolder;
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../StreamCinemaLib/StreamCinemaLib.csproj" />
<!--PackageReference Include="Jellyfin.Controller" Version="10.10.3" /-->
<ProjectReference Include="../../../jellyfin/MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
<!--PackageReference Include="Jellyfin.Model" Version="10.10.3" /-->
<ProjectReference Include="../../../jellyfin/MediaBrowser.Model/MediaBrowser.Model.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,43 @@
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
namespace Jellyfin.Plugin.StreamCinema;
sealed class StreamCinemaMoviesFolder : StreamCinemaRootFolder
{
private BaseItem? _trending;
private BaseItem? _popular;
private BaseItem? _mostWatched;
private BaseItem? _newReleases;
internal override void Initialize(ILibraryManager libraryManager, string idPrefix, string internalMetadataPath)
{
base.Initialize(libraryManager, idPrefix, internalMetadataPath);
this._trending = CreateFilterFolder<StreamCinemaTrendingFolder>("trending", this, "Trending");
this._popular = CreateFilterFolder<StreamCinemaPopularFolder>("popular", this, "Popular");
//this._mostWatched = CreateFilterFolder("mostWatched", this, "Most Watched", "watched.png");
//this._newReleases = CreateFilterFolder("newReleases", this, "New Releases", "new.png");
}
public override CollectionType? CollectionType => Data.Enums.CollectionType.movies;
internal override string ImageName => "movies.png";
protected override IEnumerable<BaseItem> GetFilterItems(string path)
{
if (_trending == null)
// Not yet initialized
yield break;
if (path.Length == 0)
{
// Root items
yield return _trending;
yield return _popular!;
//yield return _mostWatched!;
//yield return _newReleases!;
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
using Jellyfin.Plugin.StreamCinema.Configuration;
namespace Jellyfin.Plugin.StreamCinema;
/// <summary>
/// Class Plugin.
/// </summary>
public class StreamCinemaPlugin : BasePlugin<StreamCinemaPluginConfiguration>, IHasWebPages
{
public StreamCinemaPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
}
/// <inheritdoc />
public override Guid Id => new Guid("253cce3e-aa5f-11ef-bb75-8f5ab4b2eee3");
/// <inheritdoc />
public override string Name => "StreamCinema";
/// <inheritdoc />
public override string Description => "Videodoplněk obsahující rozsáhlou databázi filmů a seriálů.";
/// <summary>
/// Gets the instance.
/// </summary>
/// <value>The instance.</value>
public static StreamCinemaPlugin Instance { get; private set; }
/// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages()
{
return new[]
{
new PluginPageInfo
{
Name = "streamcinema",
EmbeddedResourcePath = GetType().Namespace + ".Web.streamcinema.html",
},
new PluginPageInfo
{
Name = "streamcinemajs",
EmbeddedResourcePath = GetType().Namespace + ".Web.streamcinema.js"
}
};
}
}

View File

@@ -0,0 +1,10 @@
using StreamCinemaLib.API;
namespace Jellyfin.Plugin.StreamCinema;
public sealed class StreamCinemaPopularFolder : StreamCinemaSortFolder
{
internal override string ImageName => "popular.png";
internal override FilterSortBy SortBy { get { return FilterSortBy.Popularity; } }
}

View File

@@ -0,0 +1,648 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
using StreamCinemaLib.API;
namespace Jellyfin.Plugin.StreamCinema;
public abstract class StreamCinemaRootFolder : StreamCinemaFilterFolder, ICollectionFolder
{
private const int PageLimit = 30;
private const string PreferredCulture = "cs";
private const string FallbackCulture = "en";
private ILibraryManager? _libraryManager;
private string? _idPrefix;
private string? _internalMetadataPath;
internal virtual void Initialize(ILibraryManager libraryManager, string idPrefix, string internalMetadataPath)
{
this._libraryManager = libraryManager;
this._idPrefix = idPrefix;
this._internalMetadataPath = internalMetadataPath;
}
public abstract CollectionType? CollectionType { get; }
/// <summary>
/// Gets the static filter folders either directly from the root or nested filter folder.
/// </summary>
/// <param name="path">Path to folder whose filter items to get (empty string for root).</param>
protected abstract IEnumerable<BaseItem> GetFilterItems(string path);
protected sealed override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
if (_libraryManager == null)
// We were not properly initialized yet so refuse to work
return new QueryResult<BaseItem>() { Items = new List<BaseItem>() };
BaseItem? parentItem = query.ParentId.IsEmpty() ? this : _libraryManager.GetItemById(query.ParentId);
if (parentItem == null)
throw new InvalidOperationException("StreamCinema parent folder is missing.");
string folderId = parentItem?.ExternalId ?? "";
int offset = query.StartIndex ?? 0;
int limit = query.Limit ?? PageLimit;
List<BaseItem> items = new List<BaseItem>();
QueryResult<BaseItem> result = new QueryResult<BaseItem>() { Items = items, StartIndex = offset };
// Static items
if (string.IsNullOrEmpty(query.SearchTerm))
{
foreach (BaseItem i in GetFilterItems(folderId))
items.Add(i);
int staticCount = items.Count;
if (offset <= items.Count)
{
items.RemoveRange(0, offset);
limit -= items.Count;
}
else
{
items.Clear();
}
offset -= staticCount;
}
// Filtered content items
FilterResponse? filterRes = Metadata.SearchAsync(query.SearchTerm ?? "", 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 (TryCreateMediaItem(i._id, i._source, parentItem, out BaseItem? a))
items.Add(a);
}
}
return result;
}
protected BaseItem CreateFilterFolder<T>(string id, StreamCinemaFilterFolder parent, string localizedName) where T : StreamCinemaFilterFolder, new()
{
if (_libraryManager == null)
throw new InvalidOperationException();
Guid folderId = _libraryManager.GetNewItemId(GetIdToHash(id, _idPrefix!), typeof(StreamCinemaRootFolder));
string folderPath = GetInternalMetadataPath(_internalMetadataPath!, folderId);
T? folder = _libraryManager.GetItemById(folderId) as T;
bool isNew;
bool forceUpdate = false;
if (isNew = folder == null)
{
folder = new T
{
Id = folderId,
Name = localizedName,
ExternalId = id,
//DateCreated = fileSystem.GetCreationTimeUtc(rootFolderPath),
//DateModified = fileSystem.GetLastWriteTimeUtc(rootFolderPath)
};
}
folder.Path = folderPath;
folder.ParentId = parent.Id;
if (isNew)
{
folder.OnMetadataChanged();
_libraryManager.CreateItem(folder, parent);
}
return folder;
}
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 = GetItemById<MediaBrowser.Controller.Entities.Audio.MusicAlbum>(csId, out isNew);
else if (singleLevel)
item = GetItemById<MediaBrowser.Controller.Entities.TV.Season>(csId, out isNew);
else
item = GetItemById<MediaBrowser.Controller.Entities.TV.Series>(csId, out isNew);
}
else if (isAudio)
{
// TODO determine if this is an AudioBook od Audio
item = GetItemById<MediaBrowser.Controller.Entities.Audio.Audio>(csId, out isNew);
}
else if (media.info_labels?.mediatype == "tvshow")
{
item = GetItemById<MediaBrowser.Controller.Entities.TV.Episode>(csId, out isNew);
}
else
{
item = GetItemById<MediaBrowser.Controller.Entities.Movies.Movie>(csId, 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;
item.ProviderIds = media.services != null ? ConvertProviderIds(media.services) : null;
//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 ?? loc?.art?.fanart;
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());
}
}
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 = 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);
_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 Dictionary<string, string> ConvertProviderIds(ServicesIds services)
{
Dictionary<string, string> result = new Dictionary<string, string>();
if (services.imdb != null)
result.Add("Imdb", services.imdb);
if (services.tvdb != null)
result.Add("Tvdb", services.tvdb);
if (services.tmdb != null)
result.Add("Tmdb", services.tmdb);
return result;
//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, "streamcinema", id.ToString("N", CultureInfo.InvariantCulture), "metadata");
}
private T GetItemById<T>(string idString, out bool isNew)
where T : BaseItem, new()
{
if (_libraryManager == null)
throw new InvalidOperationException();
var id = _libraryManager.GetNewItemId(GetIdToHash(idString, _idPrefix!), typeof(T));
T? item = null;
//try
//{
item = _libraryManager.GetItemById(id) as T;
//}
//catch (Exception ex)
//{
// _logger.LogError(ex, "Error retrieving channel item from database");
//}
if (item == null)
{
item = new T();
isNew = true;
}
else
{
isNew = false;
}
item.Id = id;
return item;
}
internal static string GetIdToHash(string externalId, string idPrefix)
{
// Increment this as needed to force new downloads
return "StreamCinema-" + idPrefix + "-" + externalId;
}
/*
public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(query.FolderId))
{
// Main Menu
List<ChannelItemInfo> pluginItems = new List<ChannelItemInfo>();
return new ChannelItemResult() { Items = pluginItems };
}
/*
if (query.FolderId.StartsWith("series_", StringComparison.OrdinalIgnoreCase))
{
var hash = query.FolderId.Split('_')[1];
return await GetChannelItems(query, i => i.IsSeries && string.Equals(i.Name.GetMD5().ToString("N"), hash, StringComparison.Ordinal), cancellationToken);
}
if (string.Equals(query.FolderId, "kids", StringComparison.OrdinalIgnoreCase))
{
return await GetChannelItems(query, i => i.IsKids, cancellationToken);
}
if (string.Equals(query.FolderId, "movies", StringComparison.OrdinalIgnoreCase))
{
return await GetChannelItems(query, i => i.IsMovie, cancellationToken);
}
if (string.Equals(query.FolderId, "news", StringComparison.OrdinalIgnoreCase))
{
return await GetChannelItems(query, i => i.IsNews, cancellationToken);
}
if (string.Equals(query.FolderId, "sports", StringComparison.OrdinalIgnoreCase))
{
return await GetChannelItems(query, i => i.IsSports, cancellationToken);
}
if (string.Equals(query.FolderId, "others", StringComparison.OrdinalIgnoreCase))
{
return await GetChannelItems(query, i => !i.IsSports && !i.IsNews && !i.IsMovie && !i.IsKids && !i.IsSeries, cancellationToken);
}*/
/*
var result = new ChannelItemResult() { Items = new List<ChannelItemInfo>() };
return result;
}
public Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken)
{
if (type == ImageType.Primary)
{
return Task.FromResult(new DynamicImageResponse { Path = "https://streamcinema.cz/assets/logo-iqdz28nk.png", Protocol = MediaProtocol.Http, HasImage = true });
}
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
public IEnumerable<ImageType> GetSupportedChannelImages()
{
return new List<ImageType> { ImageType.Primary };
}
#region IHasCacheKey Members
public string GetCacheKey(string userId)
{
DateTimeOffset dto = LiveTvService.Instance.RecordingModificationTime;
return $"{dto.ToUnixTimeSeconds()}-{_cacheKeyBase}";
}
#endregion
#region ISupportsLatestMedia Members
public async Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken)
{
var result = await GetChannelItems(new InternalChannelItemQuery(), _ => true, cancellationToken).ConfigureAwait(false);
return result.Items.OrderByDescending(i => i.DateCreated ?? DateTime.MinValue);
}
#endregion
#region IHasFolderAttributes Members
#pragma warning disable CA1819
public string[] Attributes => ["Recordings"];
#pragma warning restore CA1819
#endregion
private static ChannelItemInfo CreateMenuItem(string id, string localizedName, string? imageUrl = null)
{
return new ChannelItemInfo
{
Id = id,
Name = localizedName,
FolderType = ChannelFolderType.Container,
Type = ChannelItemType.Folder,
ImageUrl = imageUrl
};
}
private static string GetResourceUrl(string fileName)
{
return "__plugin/" + fileName;
}
private void CleanCache(bool cleanAll = false)
{
if (!string.IsNullOrEmpty(_recordingCacheDirectory) && Directory.Exists(_recordingCacheDirectory))
{
string[] cachedJson = Directory.GetFiles(_recordingCacheDirectory, "*.json");
_logger.LogInformation("Cleaning JSON cache {CacheDirectory} {FileCount}", _recordingCacheDirectory, cachedJson.Length);
foreach (string fileName in cachedJson)
{
if (cleanAll || _fileSystem.GetLastWriteTimeUtc(fileName).Add(TimeSpan.FromHours(3)) <= DateTimeOffset.UtcNow)
{
_fileSystem.DeleteFile(fileName);
}
}
}
}
private LiveTvService GetService()
{
LiveTvService service = LiveTvService.Instance;
if (service is not null && (!service.IsActive || _cachedRecordingModificationTime != Plugin.Instance.Configuration.RecordingModificationTime || service.FlagRecordingChange))
{
try
{
CancellationToken cancellationToken = CancellationToken.None;
service.EnsureConnectionAsync(cancellationToken).Wait();
if (service.IsActive)
{
_useCachedRecordings = false;
if (_cachedRecordingModificationTime != Plugin.Instance.Configuration.RecordingModificationTime)
{
_cachedRecordingModificationTime = Plugin.Instance.Configuration.RecordingModificationTime;
}
}
}
catch (Exception)
{
}
}
return service;
}
public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, Func<MyRecordingInfo, bool> filter, CancellationToken cancellationToken)
{
await GetRecordingsAsync("GetChannelItems", cancellationToken);
List<ChannelItemInfo> pluginItems = new List<ChannelItemInfo>();
pluginItems.AddRange(_allRecordings.Where(filter).Select(ConvertToChannelItem));
var result = new ChannelItemResult() { Items = pluginItems };
return result;
}
private ChannelItemInfo ConvertToChannelItem(MyRecordingInfo item)
{
var path = string.IsNullOrEmpty(item.Path) ? item.Url : item.Path;
var channelItem = new ChannelItemInfo
{
Name = string.IsNullOrEmpty(item.EpisodeTitle) ? item.Name : item.EpisodeTitle,
SeriesName = !string.IsNullOrEmpty(item.EpisodeTitle) || item.IsSeries ? item.Name : null,
StartDate = item.StartDate,
EndDate = item.EndDate,
OfficialRating = item.OfficialRating,
CommunityRating = item.CommunityRating,
ContentType = item.IsMovie ? ChannelMediaContentType.Movie : ChannelMediaContentType.Episode,
Genres = item.Genres,
ImageUrl = item.ImageUrl,
Id = item.Id,
ParentIndexNumber = item.SeasonNumber,
IndexNumber = item.EpisodeNumber,
MediaType = item.ChannelType == ChannelType.TV ? ChannelMediaType.Video : ChannelMediaType.Audio,
MediaSources = new List<MediaSourceInfo>
{
new MediaSourceInfo
{
Path = path,
Container = item.Status == RecordingStatus.InProgress ? "ts" : null,
Protocol = path.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? MediaProtocol.Http : MediaProtocol.File,
BufferMs = 1000,
AnalyzeDurationMs = 0,
IsInfiniteStream = item.Status == RecordingStatus.InProgress,
TranscodingContainer = "ts",
RunTimeTicks = item.Status == RecordingStatus.InProgress ? null : (item.EndDate - item.StartDate).Ticks,
}
},
PremiereDate = item.OriginalAirDate,
ProductionYear = item.ProductionYear,
Type = ChannelItemType.Media,
DateModified = item.Status == RecordingStatus.InProgress ? DateTime.Now : Plugin.Instance.Configuration.RecordingModificationTime,
Overview = item.Overview,
IsLiveStream = item.Status != RecordingStatus.InProgress ? false : Plugin.Instance.Configuration.EnableInProgress,
Etag = item.Status.ToString()
};
return channelItem;
}
private async Task<bool> GetRecordingsAsync(string name, CancellationToken cancellationToken)
{
var service = GetService();
if (service is null || !service.IsActive)
{
return false;
}
if (_useCachedRecordings == false || service.FlagRecordingChange)
{
if (_pollInterval == -1)
{
var interval = TimeSpan.FromSeconds(Plugin.Instance.Configuration.PollInterval);
_updateTimer = new Timer(OnUpdateTimerCallbackAsync, null, TimeSpan.FromMinutes(2), interval);
if (_updateTimer != null)
{
_pollInterval = Plugin.Instance.Configuration.PollInterval;
}
}
if (await _semaphore.WaitAsync(30000, cancellationToken))
{
try
{
_logger.LogDebug("{0} Reload cache", name);
_allRecordings = await service.GetAllRecordingsAsync(cancellationToken).ConfigureAwait(false);
int maxId = _allRecordings.Max(r => int.Parse(r.Id, CultureInfo.InvariantCulture));
int inProcessCount = _allRecordings.Count(r => r.Status == RecordingStatus.InProgress);
string keyBase = $"{maxId}-{inProcessCount}-{_allRecordings.Count()}";
if (keyBase != _cacheKeyBase && !service.FlagRecordingChange)
{
_logger.LogDebug("External recording list change {0}", keyBase);
CleanCache(true);
}
_cacheKeyBase = keyBase;
_lastUpdate = DateTimeOffset.UtcNow;
service.FlagRecordingChange = false;
_useCachedRecordings = true;
}
catch (Exception)
{
}
_semaphore.Release();
}
}
return _useCachedRecordings;
}
private async void OnUpdateTimerCallbackAsync(object state)
{
LiveTvService service = LiveTvService.Instance;
if (service is not null && service.IsActive)
{
var backendUpdate = await service.GetLastUpdate(_cancellationToken.Token).ConfigureAwait(false);
if (backendUpdate > _lastUpdate)
{
_logger.LogDebug("Recordings reset {0}", backendUpdate);
_useCachedRecordings = false;
await GetRecordingsAsync("OnUpdateTimerCallbackAsync", _cancellationToken.Token);
}
}
}*/
}

View File

@@ -0,0 +1,21 @@
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Plugin.StreamCinema;
/// <summary>
/// Register StreamCinema services.
/// </summary>
///
public class StreamCinemaServiceRegistrator : IPluginServiceRegistrator
{
/// <inheritdoc />
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{
serviceCollection.AddSingleton<IChannel, Startup>();
serviceCollection.AddSingleton<IImageProvider, StreamCinemaImageProvider>();
}
}

View File

@@ -0,0 +1,9 @@
using MediaBrowser.Controller.Entities;
using StreamCinemaLib.API;
namespace Jellyfin.Plugin.StreamCinema;
public abstract class StreamCinemaSortFolder : StreamCinemaFilterFolder
{
internal abstract FilterSortBy SortBy { get; }
}

View File

@@ -0,0 +1,10 @@
using StreamCinemaLib.API;
namespace Jellyfin.Plugin.StreamCinema;
public sealed class StreamCinemaTrendingFolder : StreamCinemaSortFolder
{
internal override string ImageName => "trending.png";
internal override FilterSortBy SortBy { get { return FilterSortBy.Trending; } }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<!--OutputType>Exe</OutputType-->
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>