Clean stratup through IHostedService, debugging plugin dir, root folder shows up and basic search works
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2024-11-25 14:10:11 +01:00
parent dcf2895491
commit e108bb0ae7
7 changed files with 313 additions and 118 deletions

View File

@@ -1,111 +0,0 @@
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,93 @@
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.StreamCinema;
/// <summary>
/// <see cref="IHostedService"/> responsible for Live TV recordings.
/// </summary>
public sealed class StreamCinemaHost : IHostedService
{
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly ILogger<StreamCinemaHost> _logger;
/// <summary>
/// Initializes a the Stream Cinema plugin.
/// </summary>
public StreamCinemaHost(ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem, ILogger<StreamCinemaHost> logger)
{
this._libraryManager = libraryManager;
this._config = config;
this._fileSystem = fileSystem;
this._logger = logger;
}
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
// 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")));*/
return Task.CompletedTask;
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
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.Id = rootFolderId; // seems to get lost on existing item
rootFolder.Path = rootFolderPath;
rootFolder.ParentId = Guid.Empty;
rootFolder.IsRoot = true;
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);
libraryManager.RootFolder.AddVirtualChild(rootFolder);
}
}

View File

@@ -7,11 +7,18 @@
</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" /-->
<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>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Debug'">
<ItemGroup>
<DebugCopyFiles Include="$(TargetDir)\StreamCinema*.dll" />
</ItemGroup>
<MakeDir Directories="\home\code\.local\share\jellyfin\plugins\StreamCinema" />
<Copy SourceFiles="@(DebugCopyFiles)" DestinationFolder="\home\code\.local\share\jellyfin\plugins\StreamCinema" />
</Target>
</Project>

View File

@@ -0,0 +1,196 @@
#pragma warning disable CS1591
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.StreamCinema;
/// <summary>
/// Metadata service that recognizes the StreamCinemaFilterFolder folders.
/// </summary>
public class StreamCinemaMetadataService : IMetadataService
{
public StreamCinemaMetadataService(
IServerConfigurationManager serverConfigurationManager,
ILogger<StreamCinemaMetadataService> logger,
IProviderManager providerManager,
IFileSystem fileSystem,
ILibraryManager libraryManager)
{
}
public int Order => 0;
public bool CanRefresh(BaseItem item)
{
return item is StreamCinemaFilterFolder;
}
public bool CanRefreshPrimary(Type type)
{
return typeof(StreamCinemaFilterFolder).IsAssignableFrom(type);
}
public Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
{
var itemOfType = (StreamCinemaFilterFolder)item;
var updateType = ItemUpdateType.None;
/*
var libraryOptions = LibraryManager.GetLibraryOptions(item);
var requiresRefresh = libraryOptions.AutomaticRefreshIntervalDays > 0 && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= libraryOptions.AutomaticRefreshIntervalDays;
if (!requiresRefresh && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
{
// TODO: If this returns true, should we instead just change metadata refresh mode to Full?
requiresRefresh = item.RequiresRefresh();
if (requiresRefresh)
{
Logger.LogDebug("Refreshing {Type} {Item} because item.RequiresRefresh() returned true", typeof(TItemType).Name, item.Path ?? item.Name);
}
}
if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
{
if (ImageProvider.RemoveImages(item))
{
updateType |= ItemUpdateType.ImageUpdate;
}
}
var localImagesFailed = false;
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
// Only validate already registered images if we are replacing and saving locally
if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
{
item.ValidateImages();
}
else
{
// Run full image validation and register new local images
try
{
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
{
updateType |= ItemUpdateType.ImageUpdate;
}
}
catch (Exception ex)
{
localImagesFailed = true;
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
}
}
var metadataResult = new MetadataResult<StreamCinemaFilterFolder>
{
Item = itemOfType,
//People = LibraryManager.GetPeople(item)
};
bool hasRefreshedMetadata = true;
bool hasRefreshedImages = true;
var isFirstRefresh = item.DateLastRefreshed == default;
// Next run metadata providers
if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
{
var providers = GetProviders(item, libraryOptions, refreshOptions, isFirstRefresh, requiresRefresh)
.ToList();
if (providers.Count > 0 || isFirstRefresh || requiresRefresh)
{
if (item.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata))
{
updateType |= ItemUpdateType.MetadataImport;
}
}
if (providers.Count > 0)
{
var id = itemOfType.GetLookupInfo();
if (refreshOptions.SearchResult is not null)
{
ApplySearchResult(id, refreshOptions.SearchResult);
}
id.IsAutomated = refreshOptions.IsAutomated;
var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, hasMetadataSavers, cancellationToken).ConfigureAwait(false);
updateType |= result.UpdateType;
if (result.Failures > 0)
{
hasRefreshedMetadata = false;
}
}
}
// Next run remote image providers, but only if local image providers didn't throw an exception
if (!localImagesFailed && refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly)
{
var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList();
if (providers.Count > 0)
{
var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false);
updateType |= result.UpdateType;
if (result.Failures > 0)
{
hasRefreshedImages = false;
}
}
}
var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType);
updateType |= beforeSaveResult;
// Save if changes were made, or it's never been saved before
if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh)
{
if (item.IsFileProtocol)
{
var file = TryGetFile(item.Path, refreshOptions.DirectoryService);
if (file is not null)
{
item.DateModified = file.LastWriteTimeUtc;
}
}
// If any of these properties are set then make sure the updateType is not None, just to force everything to save
if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata)
{
updateType |= ItemUpdateType.MetadataDownload;
}
if (hasRefreshedMetadata && hasRefreshedImages)
{
item.DateLastRefreshed = DateTime.UtcNow;
}
else
{
item.DateLastRefreshed = default;
}
// Save to database
await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
}*/
itemOfType.AfterMetadataRefresh();
updateType |= ItemUpdateType.MetadataImport | ItemUpdateType.MetadataDownload;
return Task.FromResult(updateType);
}
}

View File

@@ -23,6 +23,8 @@ sealed class StreamCinemaMoviesFolder : StreamCinemaRootFolder
public override CollectionType? CollectionType => Data.Enums.CollectionType.movies;
public override BaseItemKind ClientType => BaseItemKind.Movie;
internal override string ImageName => "movies.png";
protected override IEnumerable<BaseItem> GetFilterItems(string path)

View File

@@ -32,12 +32,19 @@ public abstract class StreamCinemaRootFolder : StreamCinemaFilterFolder, ICollec
public abstract CollectionType? CollectionType { get; }
public abstract BaseItemKind ClientType { 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);
public sealed override string GetClientTypeName()
{
return ClientType.ToString();
}
protected sealed override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
if (_libraryManager == null)
@@ -65,13 +72,14 @@ public abstract class StreamCinemaRootFolder : StreamCinemaFilterFolder, ICollec
{
items.RemoveRange(0, offset);
limit -= items.Count;
offset = 0;
}
else
{
items.Clear();
}
offset -= staticCount;
}
}
// Filtered content items
FilterResponse? filterRes = Metadata.SearchAsync(query.SearchTerm ?? "", offset: offset, limit: limit).GetAwaiter().GetResult();

View File

@@ -15,7 +15,7 @@ public class StreamCinemaServiceRegistrator : IPluginServiceRegistrator
/// <inheritdoc />
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{
serviceCollection.AddSingleton<IChannel, Startup>();
serviceCollection.AddSingleton<IImageProvider, StreamCinemaImageProvider>();
serviceCollection.AddHostedService<StreamCinemaHost>();
}
}