UserData bound to ExternalId, Resume play seems still not working. Rename music to concerts.
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2024-12-07 04:07:06 +01:00
parent 1bf5fc675e
commit f973f23443
16 changed files with 194 additions and 143 deletions

1
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -11,13 +11,13 @@ public sealed class CinemaConcertFolder : CinemaRootFolder
{
}
public override CollectionType? CollectionType => Data.Enums.CollectionType.music;
public override CollectionType? CollectionType => Data.Enums.CollectionType.musicvideos;
public override BaseItemKind ClientType => BaseItemKind.MusicAlbum;
public override BaseItemKind ClientType => BaseItemKind.MusicVideo;
public override ItemType ItemType => ItemType.Concert;
internal override string ImageName => "music.png";
internal override string ImageName => "concert.png";
protected override IEnumerable<BaseItem> GetFilterItems()
{
@@ -28,6 +28,6 @@ public sealed class CinemaConcertFolder : CinemaRootFolder
public override bool TryCreateMediaItem(MediaSource? media, string csId, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item)
{
return media.TryCreateMediaItem<CinemaMusicAlbum>(csId, parentFolder, true, out item);
return media.TryCreateMediaItem<Folder>(csId, parentFolder, true, out item);
}
}

View File

@@ -12,6 +12,11 @@ public class CinemaEpisode : Episode
{
public sealed override string GetClientTypeName() => BaseItemKind.Episode.ToString();
public override List<string> GetUserDataKeys()
{
return new List<string>() { ExternalId };
}
protected override IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
{
var result = this.VideoGetAllItemsForMediaSources();

View File

@@ -172,9 +172,6 @@ public abstract class CinemaFilterFolder : Folder
{
Folder parentItem = LibraryManager.GetUserRootFolder();
folder.ParentId = parentItem.Id;
//folder.IsRoot = true;
//if (!CinemaHost.LibraryManager.RootFolder.VirtualChildren.Contains(folder))
// CinemaHost.LibraryManager.RootFolder.AddVirtualChild(folder);
if (!parentItem.Children.Contains(folder))
parentItem.AddChild(folder);
}

View File

@@ -86,8 +86,8 @@ sealed class CinemaHost : IHostedService
.Hide = config.HideSeriesFolder;
CinemaFilterFolder.CreateRootFilterFolder<CinemaAnimeFolder>(string.IsNullOrWhiteSpace(config.AnimeFolderName) ? "Anime" : config.AnimeFolderName)
.Hide = config.HideAnimeFolder;
CinemaFilterFolder.CreateRootFilterFolder<CinemaConcertFolder>(string.IsNullOrWhiteSpace(config.MusicFolderName) ? "Music" : config.MusicFolderName)
.Hide = config.HideMusicFolder;
CinemaFilterFolder.CreateRootFilterFolder<CinemaConcertFolder>(string.IsNullOrWhiteSpace(config.ConcertFolderName) ? "Concerts" : config.ConcertFolderName)
.Hide = config.HideConcertFolder;
}
private void EnsureWebshareSession(CinemaPluginConfiguration config)

View File

@@ -1,3 +1,4 @@
using CinemaLib.API;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
@@ -5,6 +6,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Sorting;
@@ -19,8 +21,9 @@ namespace Jellyfin.Plugin.Cinema;
sealed class CinemaLibraryManager : ILibraryManager
{
private readonly ILibraryManager _inner;
private readonly IUserDataRepository _userData;
public CinemaLibraryManager(CinemaInnerLibraryManager innerLibraryManager, IServiceProvider svc)
public CinemaLibraryManager(CinemaInnerLibraryManager innerLibraryManager, IUserDataRepository userData, IServiceProvider svc)
{
if (innerLibraryManager == null || svc == null)
throw new ArgumentNullException();
@@ -29,7 +32,7 @@ sealed class CinemaLibraryManager : ILibraryManager
if (inner == null)
throw new InvalidOperationException("Original LibraryManager service not found.");
this._inner = inner;
this._userData = userData;
}
#region ILibraryManager Members
@@ -265,20 +268,46 @@ sealed class CinemaLibraryManager : ILibraryManager
public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
{
QueryResult<BaseItem> result = _inner.GetItemsResult(query);
if (query.ParentId != Guid.Empty || query.OrderBy.FirstOrDefault().OrderBy == ItemSortBy.DatePlayed)
if (query.ParentId != Guid.Empty)
// Not a search at the root so do not involve our root folders
return result;
List<BaseItem> resultL = new List<BaseItem>(result.Items);
List<BaseItem> a = new List<BaseItem>(result.Items);
if (query.OrderBy.FirstOrDefault().OrderBy == ItemSortBy.DatePlayed && query.User != null)
{
// Get Resume play items
// PERF: This may quickly become very slow
var resumePlay = _userData.GetAllUserData(query.User.InternalId)
.OrderByDescending(x => x.Played)
.Skip(query.StartIndex ?? 0)
.Take(query.Limit ?? 20);
foreach (var i in resumePlay)
{
// Note: All Cinema items override GetUserDataKeys and return ExternalId
string? csId;
if (!CinemaQueryExtensions.TryGetCinemaIdFromExternalId(i.Key, out csId))
continue;
MediaSource? ms = Metadata.DetailAsync(csId, default).GetAwaiter().GetResult();
if (CinemaQueryExtensions.TryCreateMediaItem<Folder>(ms, csId, null, false, out BaseItem? a))
resultL.Add(a);
}
}
else
{
// Add our root folders to the search
foreach (BaseItem i in GetUserRootFolder().Children)
if (i is CinemaRootFolder root)
// Prevent duplicates as content in Anime is also elsewhere
if (i is CinemaRootFolder root && i is not CinemaAnimeFolder)
{
var b = root.GetItemList(query);
if (b != null)
a.AddRange(b);
resultL.AddRange(b);
}
}
return new QueryResult<BaseItem>() { Items = a };
return new QueryResult<BaseItem>() { Items = resultL };
}
public LibraryOptions GetLibraryOptions(BaseItem item)

View File

@@ -12,6 +12,11 @@ public class CinemaMovie : Movie
{
public sealed override string GetClientTypeName() => BaseItemKind.Movie.ToString();
public override List<string> GetUserDataKeys()
{
return new List<string>() { ExternalId };
}
protected override IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
{
var result = this.VideoGetAllItemsForMediaSources();

View File

@@ -1,68 +0,0 @@
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;
}
}

View File

@@ -0,0 +1,36 @@
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
namespace Jellyfin.Plugin.Cinema;
/// <summary>
/// Concerts folder item from Cinema.
/// </summary>
public sealed class CinemaMusicVideo : MusicVideo
{
public sealed override string GetClientTypeName() => BaseItemKind.MusicVideo.ToString();
public override List<string> GetUserDataKeys()
{
return new List<string>() { ExternalId };
}
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;
}
}

View File

@@ -61,7 +61,7 @@ static class CinemaQueryExtensions
/// <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)
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)
@@ -70,12 +70,10 @@ static class CinemaQueryExtensions
return false;
}
var parentFolderId = parentFolder.Id;
bool isNew;
bool forceUpdate = false;
bool isAudio = media.is_concert ?? false;
bool isConcert = media.is_concert ?? false;
if (media.children_count != 0)
{
// Container
@@ -87,10 +85,9 @@ static class CinemaQueryExtensions
item = GetMediaItemById<TContainerType>(csId, null, out isNew);
}
else if (isAudio)
else if (isConcert)
{
// TODO determine if this is an AudioBook od Audio
item = GetMediaItemById<MediaBrowser.Controller.Entities.Audio.Audio>(csId, null, out isNew);
item = GetMediaItemById<CinemaMusicVideo>(csId, null, out isNew);
}
else if (media.info_labels?.mediatype == "episode")
@@ -164,7 +161,8 @@ static class CinemaQueryExtensions
hasAlbumArtists.AlbumArtists = media.info_labels?.director;
}
item.ParentId = parentFolderId;
if (parentFolder != null)
item.ParentId = parentFolder.Id;
/*
if (item is IHasSeries hasSeries)

View File

@@ -15,10 +15,16 @@ public sealed class CinemaSeason : Season, ICinemaChildrenCount
{
private CinemaSubfolderHelper _helper;
public CinemaSeason() {
public CinemaSeason()
{
this._helper = new CinemaSubfolderHelper(this, FilterSortBy.Episode);
}
public override List<string> GetUserDataKeys()
{
return new List<string>() { ExternalId };
}
#region ICinemaChildrenCountMembers
public int ChildrenCount
@@ -27,7 +33,8 @@ public sealed class CinemaSeason : Season, ICinemaChildrenCount
set => _helper.ChildrenCount = value;
}
public int TotalChildrenCount {
public int TotalChildrenCount
{
get => _helper.TotalChildrenCount;
set => _helper.TotalChildrenCount = value;
}

View File

@@ -15,10 +15,16 @@ public sealed class CinemaTvSeries : Series, ICinemaChildrenCount
{
private CinemaSubfolderHelper _helper;
public CinemaTvSeries() {
public CinemaTvSeries()
{
this._helper = new CinemaSubfolderHelper(this, FilterSortBy.Episode);
}
public override List<string> GetUserDataKeys()
{
return new List<string>() { ExternalId };
}
#region ICinemaChildrenCountMembers
public int ChildrenCount
@@ -27,7 +33,8 @@ public sealed class CinemaTvSeries : Series, ICinemaChildrenCount
set => _helper.ChildrenCount = value;
}
public int TotalChildrenCount {
public int TotalChildrenCount
{
get => _helper.TotalChildrenCount;
set => _helper.TotalChildrenCount = value;
}
@@ -68,7 +75,8 @@ public sealed class CinemaTvSeries : Series, ICinemaChildrenCount
// Set series identifier on all seasons
foreach (var i in result.Items)
if (i is CinemaSeason season) {
if (i is CinemaSeason season)
{
season.SeriesId = Id;
season.SeriesName = Name;
}

View File

@@ -20,8 +20,8 @@ public class CinemaPluginConfiguration : BasePluginConfiguration
public string AnimeFolderName { get; set; }
public bool HideAnimeFolder { get; set; }
public string MusicFolderName { get; set; }
public bool HideMusicFolder { get; set; }
public string ConcertFolderName { get; set; }
public bool HideConcertFolder { get; set; }
public string? WebshareUser { get; set; }
public string? WebsharePassword { get; set; }

View File

@@ -40,12 +40,12 @@
<br />
<div class="inputContainer">
<input is="emby-input" type="text" id="musicFolderName" label="Music Folder Name" />
<div class="fieldDescription">Custom name for the Cinema Music root folder.</div>
<input is="emby-input" type="text" id="concertFolderName" label="Concerts Folder Name" />
<div class="fieldDescription">Custom name for the Cinema Concerts root folder.</div>
</div>
<label class="checkboxContainer">
<input is="emby-checkbox" type="checkbox" id="hideMusicFolder" />
<span>Hide the Cinema Music root folder.</span>
<input is="emby-checkbox" type="checkbox" id="hideConcertFolder" />
<span>Hide the Cinema Concerts root folder.</span>
</label>
<div class="verticalSection verticalSection-extrabottompadding">
<h2>Webshare Account</h2>
@@ -101,14 +101,14 @@
}));
document.querySelector('#hideAnimeFolder').checked = config.HideAnimeFolder;
var musicFolderName = document.querySelector('#musicFolderName');
if (config.MusicFolderName)
musicFolderName.value = config.MusicFolderName;
musicFolderName.dispatchEvent(new Event('change', {
var concertFolderName = document.querySelector('#concertFolderName');
if (config.ConcertFolderName)
concertFolderName.value = config.ConcertFolderName;
concertFolderName.dispatchEvent(new Event('change', {
bubbles: true,
cancelable: false
}));
document.querySelector('#hideMusicFolder').checked = config.HideMusicFolder;
document.querySelector('#hideConcertFolder').checked = config.HideConcertFolder;
var webshareUser = document.querySelector('#webshareUser');
if (config.WebshareUser)
@@ -140,8 +140,8 @@
config.HideSeriesFolder = document.querySelector('#hideSeriesFolder').checked;
config.AnimeFolderName = document.querySelector('#animeFolderName').value;
config.HideAnimeFolder = document.querySelector('#hideAnimeFolder').checked;
config.MusicFolderName = document.querySelector('#musicFolderName').value;
config.HideMusicFolder = document.querySelector('#hideMusicFolder').checked;
config.ConcertFolderName = document.querySelector('#concertFolderName').value;
config.HideConcertFolder = document.querySelector('#hideConcertFolder').checked;
config.WebshareUser = document.querySelector('#webshareUser').value;
config.WebsharePassword = document.querySelector('#websharePassword').value;

View File

@@ -40,12 +40,17 @@ public class Metadata
bool noSearchTerms = expression.Trim().Length == 0;
string filterName;
string sortS = ToString(sort);
if (sort == FilterSortBy.Title) {
if (sort == FilterSortBy.Title)
{
filterName = "startsWithSimple";
sortS = "";
} else if (noSearchTerms) {
}
else if (noSearchTerms)
{
filterName = "all";
} else {
}
else
{
filterName = "search";
}
@@ -81,6 +86,24 @@ public class Metadata
return result;
}
/// <summary>
/// Gets detail info about in item.
/// </summary>
/// <param name="id">Item identifier.</param>
/// <param name="cancel">Asynchronous cancellation.</param>
/// <returns>Media info.</returns>
public async static Task<MediaSource?> DetailAsync(string id, CancellationToken cancel)
{
if (id == null || id.Length == 0)
throw new ArgumentNullException();
Uri uri = new Uri(new Uri(ApiRoot, "media/"), id);
MediaSource? result = await Program._http.GetFromJsonAsync<MediaSource>(uri, CreateFilterJsonOptions(), cancel);
if (result != null)
result = FixDetailResponse(result);
return result;
}
/// <summary>
/// Gets available streams for the given media.
/// </summary>
@@ -160,25 +183,35 @@ public class Metadata
if (i._source == null)
continue;
if (i._source.cast != null)
foreach (Cast j in i._source.cast)
FixDetailResponse(i._source);
}
return res;
}
private static MediaSource FixDetailResponse(MediaSource res)
{
if (res.cast != null)
foreach (Cast j in res.cast)
j.thumbnail = FixImageUrl(j.thumbnail);
if (i._source.i18n_info_labels != null)
foreach (InfoLabelI18n j in i._source.i18n_info_labels) {
if (j.art != null) {
if (res.i18n_info_labels != null)
foreach (InfoLabelI18n j in res.i18n_info_labels)
{
if (j.art != null)
{
j.art.poster = FixImageUrl(j.art.poster);
j.art.fanart = FixImageUrl(j.art.fanart);
}
}
}
return res;
}
private static string FixImageUrl(string url)
{
if (url != null && !url.StartsWith("http")) {
if (url != null && !url.StartsWith("http"))
{
if (url.StartsWith("//"))
url = "https:" + url;
else