Compare commits

6 Commits

Author SHA1 Message Date
6e80e621c0 External subtitles work, internal are broken
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-14 17:39:44 +00:00
c9fda519d8 Remove ILRepack.Lib.MSBuild.Task hack
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-14 06:40:14 +00:00
898a541c18 Embedded subtitles work. External not yet tested.
Some checks failed
continuous-integration/drone/push Build is failing
2025-04-14 06:16:48 +00:00
2033a30ddb Fix language code matching (ISO639-2 synonyms) 2025-04-14 06:16:33 +00:00
bfe40864d8 TV Series playback from episode list does not require mediaSourceId
Some checks failed
continuous-integration/drone/push Build is failing
2025-04-14 06:14:24 +00:00
5f027d0a66 Separate globals and initialize also from CinemaLibraryManager to allow migrations to run (backport)
Some checks failed
continuous-integration/drone/push Build is failing
2025-04-14 06:13:42 +00:00
10 changed files with 370 additions and 264 deletions

View File

@@ -5,10 +5,6 @@ steps:
- name: build
image: mcr.microsoft.com/dotnet/sdk:9.0
commands:
# HACK Waiting for "Assembly strong name Linux compatibility" (https://github.com/gluck/il-repack/pull/366) to propagate to the ILRepack.Lib.MSBuild.Task nuget package
- dotnet restore CinemaJellyfin/CinemaJellyfin.csproj
- wget https://www.ivasoft.cz/share/ILRepack.Lib.MSBuild.Task.dll -O /root/.nuget/packages/ilrepack.lib.msbuild.task/2.0.34.2/build/ILRepack.Lib.MSBuild.Task.dll
# END HACK
- dotnet build --configuration Release CinemaJellyfin/CinemaJellyfin.csproj
- dotnet publish -c Release -o out
@@ -16,8 +12,8 @@ steps:
image: git.ivasoft.cz/sw/docker-wine-dotnet
commands:
- wine Eazfuscator.NET.exe CinemaJellyfin/bin/Release/net8.0/CinemaJellyfin.dll -k key.snk -n --newline-flush
#when:
# event: tag
when:
event: tag
- name: gitea_release
image: plugins/gitea-release

View File

@@ -7,6 +7,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using CinemaLib.API;
using CinemaJellyfin;
namespace Jellyfin.Plugin.Cinema;
@@ -165,11 +166,11 @@ public abstract class CinemaFilterFolder : Folder
private static T CreateFilterFolderInternal<T>(CinemaFilterFolder? parent, string localizedName) where T : CinemaFilterFolder, new()
{
Guid folderId = CinemaHost.LibraryManager.GetNewItemId("folder", typeof(T));
string folderPath = GetInternalMetadataPath(CinemaHost.InternalMetadataPath!, folderId);
Guid folderId = CinemaGlobals.LibraryManager.GetNewItemId("folder", typeof(T));
string folderPath = GetInternalMetadataPath(CinemaGlobals.InternalMetadataPath!, folderId);
Directory.CreateDirectory(folderPath);
T? folder = CinemaHost.LibraryManager.GetItemById(folderId) as T;
T? folder = CinemaGlobals.LibraryManager.GetItemById(folderId) as T;
bool isNew;
bool forceUpdate = false;
if (isNew = folder == null)
@@ -177,8 +178,8 @@ public abstract class CinemaFilterFolder : Folder
folder = new T
{
Id = folderId,
DateCreated = CinemaHost.FileSystem.GetCreationTimeUtc(folderPath),
DateModified = CinemaHost.FileSystem.GetLastWriteTimeUtc(folderPath)
DateCreated = CinemaGlobals.FileSystem.GetCreationTimeUtc(folderPath),
DateModified = CinemaGlobals.FileSystem.GetLastWriteTimeUtc(folderPath)
};
}
@@ -200,11 +201,11 @@ public abstract class CinemaFilterFolder : Folder
if (isNew)
{
folder.OnMetadataChanged();
CinemaHost.LibraryManager.CreateItem(folder, parent);
CinemaGlobals.LibraryManager.CreateItem(folder, parent);
}
folder.RefreshMetadata(
new MetadataRefreshOptions(new DirectoryService(CinemaHost.FileSystem)) { ForceSave = !isNew && forceUpdate },
new MetadataRefreshOptions(new DirectoryService(CinemaGlobals.FileSystem)) { ForceSave = !isNew && forceUpdate },
default
).GetAwaiter().GetResult();

View File

@@ -0,0 +1,36 @@
using System;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
namespace CinemaJellyfin;
/// <summary>
/// Global services that are resolved at startup and are
/// otherwise inaccessible to some classes (mostly folders).
/// </summary>
static class CinemaGlobals
{
#pragma warning disable CS8618
private static ILibraryManager _libraryManager;
private static IServerConfigurationManager _config;
private static IFileSystem _fileSystem;
#pragma warning restore CS8618
internal static void Initialize(ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem)
{
_libraryManager = libraryManager;
_config = config;
_fileSystem = fileSystem;
}
public static ILibraryManager LibraryManager => _libraryManager;
public static IFileSystem FileSystem => _fileSystem;
public static string InternalMetadataPath => _config.ApplicationPaths.InternalMetadataPath;
public static string PreferredCulture => _config.Configuration.PreferredMetadataLanguage;
public static string FallbackCulture => "en";
}

View File

@@ -1,4 +1,5 @@
using System.Linq.Expressions;
using CinemaJellyfin;
using CinemaLib.Webshare;
using Jellyfin.Plugin.Cinema.Configuration;
using MediaBrowser.Controller.Configuration;
@@ -19,9 +20,6 @@ sealed class CinemaHost : IHostedService
#pragma warning disable CS8618
// This instance is specially registered and gets created before all classes
// except CinemaServiceRegistrator and CinemaPlugin.
private static ILibraryManager _libraryManager;
private static IServerConfigurationManager _config;
private static IFileSystem _fileSystem;
private static Session? _webshare;
#pragma warning restore CS8618
private readonly ILogger<CinemaHost> _logger;
@@ -33,22 +31,10 @@ sealed class CinemaHost : IHostedService
/// </summary>
public CinemaHost(ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem, ILogger<CinemaHost> logger)
{
_libraryManager = libraryManager;
_config = config;
_fileSystem = fileSystem;
CinemaGlobals.Initialize(libraryManager, config, fileSystem);
this._logger = logger;
}
public static ILibraryManager LibraryManager => _libraryManager;
public static string InternalMetadataPath => _config.ApplicationPaths.InternalMetadataPath;
public static IFileSystem FileSystem => _fileSystem;
public static string PreferredCulture => _config.Configuration.PreferredMetadataLanguage;
public static string FallbackCulture => "en";
public static Session? Webshare => _webshare;
public static async Task<bool> IsWebshareFreeAccount(CancellationToken cancel)

View File

@@ -16,7 +16,7 @@
<PackageReference Include="Jellyfin.Model" Version="10.10.3" />
<!--ProjectReference Include="..\..\..\jellyfin\MediaBrowser.Model\MediaBrowser.Model.csproj" /-->
<PackageReference Include="ILRepack.Lib.MSBuild.Task" Version="2.0.34.2" PrivateAssets="All"/>
<PackageReference Include="ILRepack.Lib.MSBuild.Task" Version="2.0.40" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>

View File

@@ -15,6 +15,9 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using CinemaJellyfin;
using Microsoft.Extensions.DependencyInjection;
using MediaBrowser.Controller.Configuration;
namespace Jellyfin.Plugin.Cinema;
@@ -33,6 +36,9 @@ sealed class CinemaLibraryManager : ILibraryManager
throw new InvalidOperationException("Original LibraryManager service not found.");
this._inner = inner;
this._userData = userData;
// We may run before CinemaHost if migrations are executed
CinemaGlobals.Initialize(this, svc.GetRequiredService<IServerConfigurationManager>(), svc.GetRequiredService<IFileSystem>());
}
#region ILibraryManager Members

View File

@@ -136,6 +136,11 @@ sealed class CinemaMediaAnalyzer
if (i.BitRate == null || i.BitRate < 16 * 1024)
i.BitRate = 16 * 1024;
break;
case MediaStreamType.Subtitle:
// Required for subtitle extraction
i.SupportsExternalStream = true;
break;
}
// Add unrecognized bogus streams so stream Index values are continous and sorted in the

View File

@@ -6,7 +6,13 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@@ -16,11 +22,9 @@ using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using CinemaLib.API;
using MediaBrowser.Controller;
using MediaBrowser.Controller.MediaEncoding;
using System.Runtime.CompilerServices;
using LinkGenerator = CinemaLib.Webshare.LinkGenerator;
using MediaBrowser.Controller.Configuration;
using Jellyfin.Extensions;
namespace Jellyfin.Plugin.Cinema;
@@ -54,12 +58,13 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IUserManager _userManager;
private readonly IApplicationPaths _pathManager;
private readonly ConcurrentDictionary<Guid, VersionSetEntry> _videoVersions;
private readonly ConcurrentDictionary<LinkKey, LinkEntry> _links;
public CinemaMediaSourceManager(CinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServerApplicationHost host,
IServiceProvider svc, IHttpContextAccessor http, IHttpClientFactory httpClientFactory, IMediaEncoder mediaEncoder,
IServerConfigurationManager serverConfigurationManager, IUserManager userManager)
IServerConfigurationManager serverConfigurationManager, IUserManager userManager, IApplicationPaths pathManager)
{
if (innerMediaSourceManager == null || svc == null)
throw new ArgumentNullException();
@@ -76,6 +81,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
this._mediaEncoder = mediaEncoder;
this._serverConfigurationManager = serverConfigurationManager;
this._userManager = userManager;
this._pathManager = pathManager;
this._videoVersions = new ConcurrentDictionary<Guid, VersionSetEntry>();
this._links = new ConcurrentDictionary<LinkKey, LinkEntry>();
@@ -241,25 +247,28 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
// We need to directly inspect the video file and therefore need to know which
// version to specifically return. For free accounts it is not possible to just
// read files at random, for VIP accounts that would slow playback startup also.
Guid mediaSourceId;
if (ctx == null
|| !ctx.Items.TryGetValue(ContextItemsMediaSourceIdKey, out object? mediaSourceIdO)
|| mediaSourceIdO is not string mediaSourceIdS
|| (!Guid.TryParse(mediaSourceIdS, out mediaSourceId)))
throw new InvalidOperationException("For precise stream indexing knowing mediaSourceId is required.");
// Warning: We assume GetVideoVersionsEnumerate keeps the order between its input and output collections
int idxVer = 0;
Video? videoVer = null;
foreach (Video i in items)
if ((!sortByPrefs && i.Id == mediaSourceId)
|| (sortByPrefs && idxVer == bestIdx))
{
videoVer = i;
break;
}
else
idxVer++;
Video? videoVer;
int idxVer;
if (!sortByPrefs) {
Guid mediaSourceId;
if (ctx == null
|| !ctx.Items.TryGetValue(ContextItemsMediaSourceIdKey, out object? mediaSourceIdO)
|| mediaSourceIdO is not string mediaSourceIdS
|| (!Guid.TryParse(mediaSourceIdS, out mediaSourceId)))
throw new InvalidOperationException("For precise stream indexing knowing mediaSourceId is required.");
idxVer = -1;
videoVer = items.Where((x, idx) => {
bool isMatch = x.Id == mediaSourceId;
if (isMatch)
idxVer = idx;
return isMatch;
}).FirstOrDefault();
} else {
idxVer = bestIdx;
videoVer = items.Skip(bestIdx).FirstOrDefault();
}
if (videoVer == null)
// Version not found, return empty set
break;
@@ -270,7 +279,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
Uri link = await GenerateLink(metaVer.Meta.provider, metaVer.Meta.ident, metaVer.Meta.name, cancellationToken);
if (metaVer.AnalyzedInfo == null)
metaVer.AnalyzedInfo = await CinemaMediaAnalyzer.AnalyzeResourceAsync(link, metaVer.Meta.size, metaVer.Meta, _httpClientFactory, _mediaEncoder, _serverConfigurationManager, cancellationToken);
result.Add(GetVersionInfo(videoVer, metaVer.Meta, metaVer.AnalyzedInfo.MediaStreams, isFreeAccount, link));
result.Add(await GetVersionInfo(videoVer, metaVer.Meta, metaVer.AnalyzedInfo.MediaStreams, isFreeAccount, link, cancellationToken));
}
else
{
@@ -278,7 +287,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
// Note: Also makes sure BaseItems exist for each version
int idx = 0;
foreach (Video i in items)
result.Add(GetVersionInfo(i, ver.Versions[idx++].Meta, null, isFreeAccount, FakeVideoUri));
result.Add(await GetVersionInfo(i, ver.Versions[idx++].Meta, null, isFreeAccount, FakeVideoUri, cancellationToken));
if (sortByPrefs && ver.Versions.Length != 0 && bestIdx >= 0)
{
@@ -356,7 +365,8 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
if (m.subtitles != null && subtitleLang != null)
foreach (StreamSubtitle j in m.subtitles)
if (ISO639_1ToISO639_2(j.language) == subtitleLang)
if (ISO639_1ToISO639_2(j.language).Primary == subtitleLang
|| ISO639_1ToISO639_2(j.language).Synonym == subtitleLang)
{
score += 4; // proper subtitles are preferred but much better resulution still rules
break;
@@ -364,7 +374,8 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
if (m.audio != null && audioLang != null)
foreach (StreamAudio j in m.audio)
if (ISO639_1ToISO639_2(j.language) == audioLang)
if (ISO639_1ToISO639_2(j.language).Primary == audioLang
|| ISO639_1ToISO639_2(j.language).Synonym == audioLang)
{
score += 25; // proper audio channel overrides resolution and other minor bonuses
break;
@@ -558,17 +569,17 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
bool isFreeAccount = await CinemaHost.IsWebshareFreeAccount(cancel);
return GetVersionInfo(item, ver.Meta, null, isFreeAccount, FakeVideoUri);
return await GetVersionInfo(item, ver.Meta, null, isFreeAccount, FakeVideoUri, cancel);
}
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver, IReadOnlyList<MediaStream>? analyzedStreams, bool isFreeAccount, Uri path)
private async ValueTask<MediaSourceInfo> GetVersionInfo(Video item, CinemaLib.API.Stream ver, IReadOnlyList<MediaStream>? analyzedStreams, bool isFreeAccount, Uri path, CancellationToken cancel)
{
MediaSourceInfo result = new MediaSourceInfo();
result.VideoType = VideoType.VideoFile;
result.Id = item.Id.ToString("N", CultureInfo.InvariantCulture);
result.Protocol = MediaProtocol.Http;
// Warning: We set some properties on item that get used below
result.MediaStreams = VersionToMediaStreams(item, ver, analyzedStreams);
result.MediaStreams = await VersionToMediaStreams(result.Id, item, ver, analyzedStreams, cancel);
double bitRate = (ver.size ?? 0.0) * 8 / (item.RunTimeTicks ?? 1.0) * TimeSpan.TicksPerSecond;
bool showBitrateWarning = isFreeAccount && bitRate / (1 + BitrateMargin) > WebshareFreeBitrate;
@@ -629,8 +640,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
return result;
}
private static List<MediaStream> VersionToMediaStreams(Video item, CinemaLib.API.Stream ver, IReadOnlyList<MediaStream>? analyzedStreams)
{
private async ValueTask<List<MediaStream>> VersionToMediaStreams(string mediaSourceId, Video item, CinemaLib.API.Stream ver, IReadOnlyList<MediaStream>? analyzedStreams, CancellationToken cancel) {
List<MediaStream> result = new List<MediaStream>();
if (analyzedStreams != null && analyzedStreams.Count != 0)
@@ -641,15 +651,40 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
{
foreach (StreamSubtitle j in ver.subtitles)
{
if (!string.IsNullOrEmpty(j.src))
if (!string.IsNullOrEmpty(j.src) && Uri.TryCreate(j.src, UriKind.Absolute, out Uri? src))
{
MediaStream a = new MediaStream();
a.Index = -1;
a.Index = result.Count;
a.Type = MediaStreamType.Subtitle;
a.Language = ISO639_1ToISO639_2(j.language);
a.Language = ISO639_1ToISO639_2(j.language).Primary;
a.IsForced = j.forced;
a.DeliveryUrl = j.src;
a.IsExternalUrl = true;
a.IsExternal = true;
// Note: This prevents subtitle burn-in and does not restart playback while activating
a.SupportsExternalStream = true;
a.Codec = "subrip";
#if NOT_BROKEN_GetStream
a.Path = (await GenerateLinkOrKeepUrl(src, cancel)).ToString();
#else
// BUG: SubtitleEncoder.GetStream is broken as it disposes the returned Stream
// before returning. Therefore we must cache the subtitle file locally
// HACK: We expect the ordering in ver.subtitles is stable as Jellyfin
// uses index to determine the filename
string subtitleCachePath = Path.Combine(_pathManager.DataPath, "subtitles");
string mediaSourceIdS = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture);
string folderPath = Path.Join(subtitleCachePath, mediaSourceIdS[..2], mediaSourceIdS);
string path = Path.Join(folderPath, a.Index.ToString(CultureInfo.InvariantCulture) + ".srt");
a.IsExternal = true;
a.Path = path;
if (!File.Exists(path)) {
// PERF: Subtitle files are expected not to be large (< 1 MB)
Uri link = await GenerateLinkOrKeepUrl(src, cancel);
using HttpResponseMessage response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(link, cancel);
byte[] contentB = await response.Content.ReadAsByteArrayAsync(cancel);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllBytesAsync(path, contentB, cancel);
}
#endif
result.Add(a);
}
}
@@ -702,7 +737,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
MediaStream a = new MediaStream();
a.Index = uniqueId--;
a.Type = MediaStreamType.Audio;
a.Language = ISO639_1ToISO639_2(j.language);
a.Language = ISO639_1ToISO639_2(j.language).Primary;
a.Codec = j.codec;
a.Channels = j.channels;
@@ -718,10 +753,12 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
MediaStream a = new MediaStream();
a.Index = uniqueId--;
a.Type = MediaStreamType.Subtitle;
a.Language = ISO639_1ToISO639_2(j.language);
a.Language = ISO639_1ToISO639_2(j.language).Primary;
a.IsForced = j.forced;
a.DeliveryUrl = j.src;
a.IsExternalUrl = true;
if (a.IsExternal = j.src != null) {
a.Path = j.src;
a.DeliveryMethod = MediaBrowser.Model.Dlna.SubtitleDeliveryMethod.External;
}
result.Add(a);
}
@@ -876,7 +913,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
return;
}
public async Task<Uri> GenerateLink(string provider, string ident, string? name, CancellationToken cancel)
public async ValueTask<Uri> GenerateLink(string provider, string ident, string? name, CancellationToken cancel)
{
name = name ?? "";
DateTime now = DateTime.UtcNow;
@@ -898,198 +935,212 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
return entry.Link;
}
private ValueTask<Uri> GenerateLinkOrKeepUrl(Uri uri, CancellationToken cancel) {
switch (uri.Host) {
case "webshare.cz":
const string WebsharePrefix = "#/file/";
if (uri.AbsolutePath == "/" && uri.Fragment.StartsWith(WebsharePrefix)) {
string ident = uri.Fragment.Substring(WebsharePrefix.Length);
return GenerateLink("webshare", ident, name: null, cancel);
}
break;
}
return new ValueTask<Uri>(uri);
}
/// <summary>
/// Converts two-letter 639-1 language code to three letter 639-2.
/// </summary>
private static string? ISO639_1ToISO639_2(string? twoLetter639_1)
private static (string? Primary, string? Synonym) ISO639_1ToISO639_2(string? twoLetter639_1)
{
// Based on https://www.loc.gov/standards/iso639-2/php/code_list-utf8.php
switch (twoLetter639_1)
{
case "aa": return "aar";
case "ab": return "abk";
case "af": return "afr";
case "ak": return "aka";
case "sq": return "alb";
case "am": return "amh";
case "ar": return "ara";
case "an": return "arg";
case "hy": return "arm";
case "as": return "asm";
case "av": return "ava";
case "ae": return "ave";
case "ay": return "aym";
case "az": return "aze";
case "ba": return "bak";
case "bm": return "bam";
case "eu": return "baq";
case "be": return "bel";
case "bn": return "ben";
case "bi": return "bis";
case "bo": return "tib";
case "bs": return "bos";
case "br": return "bre";
case "bg": return "bul";
case "my": return "bur";
case "ca": return "cat";
case "cs": return "cze";
case "ch": return "cha";
case "ce": return "che";
case "zh": return "chi";
case "cu": return "chu";
case "cv": return "chv";
case "kw": return "cor";
case "co": return "cos";
case "cr": return "cre";
case "cy": return "wel";
case "da": return "dan";
case "de": return "ger";
case "dv": return "div";
case "nl": return "dut";
case "dz": return "dzo";
case "el": return "gre";
case "en": return "eng";
case "eo": return "epo";
case "et": return "est";
case "ee": return "ewe";
case "fo": return "fao";
case "fa": return "per";
case "fj": return "fij";
case "fi": return "fin";
case "fr": return "fre";
case "fy": return "fry";
case "ff": return "ful";
case "ka": return "geo";
case "gd": return "gla";
case "ga": return "gle";
case "gl": return "glg";
case "gv": return "glv";
case "gn": return "grn";
case "gu": return "guj";
case "ht": return "hat";
case "ha": return "hau";
case "he": return "heb";
case "hz": return "her";
case "hi": return "hin";
case "ho": return "hmo";
case "hr": return "hrv";
case "hu": return "hun";
case "ig": return "ibo";
case "is": return "ice";
case "io": return "ido";
case "ii": return "iii";
case "iu": return "iku";
case "ie": return "ile";
case "ia": return "ina";
case "id": return "ind";
case "ik": return "ipk";
case "it": return "ita";
case "jv": return "jav";
case "ja": return "jpn";
case "kl": return "kal";
case "kn": return "kan";
case "ks": return "kas";
case "kr": return "kau";
case "kk": return "kaz";
case "km": return "khm";
case "ki": return "kik";
case "rw": return "kin";
case "ky": return "kir";
case "kv": return "kom";
case "kg": return "kon";
case "ko": return "kor";
case "kj": return "kua";
case "ku": return "kur";
case "lo": return "lao";
case "la": return "lat";
case "lv": return "lav";
case "li": return "lim";
case "ln": return "lin";
case "lt": return "lit";
case "lb": return "ltz";
case "lu": return "lub";
case "lg": return "lug";
case "mk": return "mac";
case "mh": return "mah";
case "ml": return "mal";
case "mi": return "mao";
case "mr": return "mar";
case "ms": return "may";
case "mg": return "mlg";
case "mt": return "mlt";
case "mn": return "mon";
case "na": return "nau";
case "nv": return "nav";
case "nr": return "nbl";
case "nd": return "nde";
case "ng": return "ndo";
case "ne": return "nep";
case "nn": return "nno";
case "nb": return "nob";
case "no": return "nor";
case "ny": return "nya";
case "oc": return "oci";
case "oj": return "oji";
case "or": return "ori";
case "om": return "orm";
case "os": return "oss";
case "pa": return "pan";
case "pi": return "pli";
case "pl": return "pol";
case "pt": return "por";
case "ps": return "pus";
case "qu": return "que";
case "rm": return "roh";
case "ro": return "rum";
case "rn": return "run";
case "ru": return "rus";
case "sg": return "sag";
case "sa": return "san";
case "si": return "sin";
case "sk": return "slo";
case "sl": return "slv";
case "se": return "sme";
case "sm": return "smo";
case "sn": return "sna";
case "sd": return "snd";
case "so": return "som";
case "st": return "sot";
case "es": return "spa";
case "sc": return "srd";
case "sr": return "srp";
case "ss": return "ssw";
case "su": return "sun";
case "sw": return "swa";
case "sv": return "swe";
case "ty": return "tah";
case "ta": return "tam";
case "tt": return "tat";
case "te": return "tel";
case "tg": return "tgk";
case "tl": return "tgl";
case "th": return "tha";
case "ti": return "tir";
case "to": return "ton";
case "tn": return "tsn";
case "ts": return "tso";
case "tk": return "tuk";
case "tr": return "tur";
case "tw": return "twi";
case "ug": return "uig";
case "uk": return "ukr";
case "ur": return "urd";
case "uz": return "uzb";
case "ve": return "ven";
case "vi": return "vie";
case "vo": return "vol";
case "wa": return "wln";
case "wo": return "wol";
case "xh": return "xho";
case "yi": return "yid";
case "yo": return "yor";
case "za": return "zha";
case "zu": return "zul";
default: return twoLetter639_1;
case "aa": return ("aar", null);
case "ab": return ("abk", null);
case "af": return ("afr", null);
case "ak": return ("aka", null);
case "sq": return ("alb", "sqi");
case "am": return ("amh", null);
case "ar": return ("ara", null);
case "an": return ("arg", null);
case "hy": return ("arm", "hye");
case "as": return ("asm", null);
case "av": return ("ava", null);
case "ae": return ("ave", null);
case "ay": return ("aym", null);
case "az": return ("aze", null);
case "ba": return ("bak", null);
case "bm": return ("bam", null);
case "eu": return ("baq", "eus");
case "be": return ("bel", null);
case "bn": return ("ben", null);
case "bi": return ("bis", null);
case "bo": return ("tib", "bod");
case "bs": return ("bos", null);
case "br": return ("bre", null);
case "bg": return ("bul", null);
case "my": return ("bur", "mya");
case "ca": return ("cat", null);
case "cs": return ("cze", "ces");
case "ch": return ("cha", null);
case "ce": return ("che", null);
case "zh": return ("chi", "zho");
case "cu": return ("chu", null);
case "cv": return ("chv", null);
case "kw": return ("cor", null);
case "co": return ("cos", null);
case "cr": return ("cre", null);
case "cy": return ("wel", "cym");
case "da": return ("dan", null);
case "de": return ("ger", "deu");
case "dv": return ("div", null);
case "nl": return ("dut", "nld");
case "dz": return ("dzo", null);
case "el": return ("gre", "ell");
case "en": return ("eng", null);
case "eo": return ("epo", null);
case "et": return ("est", null);
case "ee": return ("ewe", null);
case "fo": return ("fao", null);
case "fa": return ("per", "fas");
case "fj": return ("fij", null);
case "fi": return ("fin", null);
case "fr": return ("fre", "fra");
case "fy": return ("fry", null);
case "ff": return ("ful", null);
case "ka": return ("geo", "kat");
case "gd": return ("gla", null);
case "ga": return ("gle", null);
case "gl": return ("glg", null);
case "gv": return ("glv", null);
case "gn": return ("grn", null);
case "gu": return ("guj", null);
case "ht": return ("hat", null);
case "ha": return ("hau", null);
case "he": return ("heb", null);
case "hz": return ("her", null);
case "hi": return ("hin", null);
case "ho": return ("hmo", null);
case "hr": return ("hrv", null);
case "hu": return ("hun", null);
case "ig": return ("ibo", null);
case "is": return ("ice", "isl");
case "io": return ("ido", null);
case "ii": return ("iii", null);
case "iu": return ("iku", null);
case "ie": return ("ile", null);
case "ia": return ("ina", null);
case "id": return ("ind", null);
case "ik": return ("ipk", null);
case "it": return ("ita", null);
case "jv": return ("jav", null);
case "ja": return ("jpn", null);
case "kl": return ("kal", null);
case "kn": return ("kan", null);
case "ks": return ("kas", null);
case "kr": return ("kau", null);
case "kk": return ("kaz", null);
case "km": return ("khm", null);
case "ki": return ("kik", null);
case "rw": return ("kin", null);
case "ky": return ("kir", null);
case "kv": return ("kom", null);
case "kg": return ("kon", null);
case "ko": return ("kor", null);
case "kj": return ("kua", null);
case "ku": return ("kur", null);
case "lo": return ("lao", null);
case "la": return ("lat", null);
case "lv": return ("lav", null);
case "li": return ("lim", null);
case "ln": return ("lin", null);
case "lt": return ("lit", null);
case "lb": return ("ltz", null);
case "lu": return ("lub", null);
case "lg": return ("lug", null);
case "mk": return ("mac", "mkd");
case "mh": return ("mah", null);
case "ml": return ("mal", null);
case "mi": return ("mao", "mri");
case "mr": return ("mar", null);
case "ms": return ("may", "msa");
case "mg": return ("mlg", null);
case "mt": return ("mlt", null);
case "mn": return ("mon", null);
case "na": return ("nau", null);
case "nv": return ("nav", null);
case "nr": return ("nbl", null);
case "nd": return ("nde", null);
case "ng": return ("ndo", null);
case "ne": return ("nep", null);
case "nn": return ("nno", null);
case "nb": return ("nob", null);
case "no": return ("nor", null);
case "ny": return ("nya", null);
case "oc": return ("oci", null);
case "oj": return ("oji", null);
case "or": return ("ori", null);
case "om": return ("orm", null);
case "os": return ("oss", null);
case "pa": return ("pan", null);
case "pi": return ("pli", null);
case "pl": return ("pol", null);
case "pt": return ("por", null);
case "ps": return ("pus", null);
case "qu": return ("que", null);
case "rm": return ("roh", null);
case "ro": return ("rum", "ron");
case "rn": return ("run", null);
case "ru": return ("rus", null);
case "sg": return ("sag", null);
case "sa": return ("san", null);
case "si": return ("sin", null);
case "sk": return ("slo", "slk");
case "sl": return ("slv", null);
case "se": return ("sme", null);
case "sm": return ("smo", null);
case "sn": return ("sna", null);
case "sd": return ("snd", null);
case "so": return ("som", null);
case "st": return ("sot", null);
case "es": return ("spa", null);
case "sc": return ("srd", null);
case "sr": return ("srp", null);
case "ss": return ("ssw", null);
case "su": return ("sun", null);
case "sw": return ("swa", null);
case "sv": return ("swe", null);
case "ty": return ("tah", null);
case "ta": return ("tam", null);
case "tt": return ("tat", null);
case "te": return ("tel", null);
case "tg": return ("tgk", null);
case "tl": return ("tgl", null);
case "th": return ("tha", null);
case "ti": return ("tir", null);
case "to": return ("ton", null);
case "tn": return ("tsn", null);
case "ts": return ("tso", null);
case "tk": return ("tuk", null);
case "tr": return ("tur", null);
case "tw": return ("twi", null);
case "ug": return ("uig", null);
case "uk": return ("ukr", null);
case "ur": return ("urd", null);
case "uz": return ("uzb", null);
case "ve": return ("ven", null);
case "vi": return ("vie", null);
case "vo": return ("vol", null);
case "wa": return ("wln", null);
case "wo": return ("wol", null);
case "xh": return ("xho", null);
case "yi": return ("yid", null);
case "yo": return ("yor", null);
case "za": return ("zha", null);
case "zu": return ("zul", null);
default: return (twoLetter639_1, null);
}
}

View File

@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using CinemaJellyfin;
using CinemaLib.API;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
@@ -206,7 +207,7 @@ static class CinemaQueryExtensions
{
// HACK: We use RegisterItem that is volatile intead of CreateItem
//_libraryManager.CreateItem(item, parentFolder);
CinemaHost.LibraryManager.RegisterItem(item);
CinemaGlobals.LibraryManager.RegisterItem(item);
if (media.cast != null && media.cast.Count > 0)
{
@@ -264,9 +265,9 @@ static class CinemaQueryExtensions
InfoLabelI18n? preferred = null;
InfoLabelI18n? fallback = null;
foreach (InfoLabelI18n i in media.i18n_info_labels)
if (i.lang == CinemaHost.PreferredCulture)
if (i.lang == CinemaGlobals.PreferredCulture)
preferred = i;
else if (i.lang == CinemaHost.FallbackCulture)
else if (i.lang == CinemaGlobals.FallbackCulture)
fallback = i;
else if (first == null)
first = i;
@@ -280,7 +281,7 @@ static class CinemaQueryExtensions
internal static Guid GetMediaItemId(string csPrimaryId, string? csVersionId)
{
string idS = csVersionId == null ? csPrimaryId : (csPrimaryId + VersionSeparator + csVersionId);
return CinemaHost.LibraryManager.GetNewItemId(idS, typeof(CinemaPlugin));
return CinemaGlobals.LibraryManager.GetNewItemId(idS, typeof(CinemaPlugin));
}
/// <summary>
@@ -290,7 +291,7 @@ static class CinemaQueryExtensions
where T : BaseItem, new()
{
Guid id = GetMediaItemId(csPrimaryId, csVersionId);
T? item = CinemaHost.LibraryManager.GetItemById(id) as T;
T? item = CinemaGlobals.LibraryManager.GetItemById(id) as T;
if (item == null)
{

View File

@@ -75,13 +75,16 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
{
ControllerModel? mediaInfo = application.Controllers.Where(x => x.ControllerName == "MediaInfo").FirstOrDefault();
ControllerModel? dynamicHls = application.Controllers.Where(x => x.ControllerName == "DynamicHls").FirstOrDefault();
ActionModel? getPostedPlaybackInfo, getMasterHlsVideoPlaylist, getVariantHlsVideoPlaylist, getHlsVideoSegment;
ControllerModel? subtitle = application.Controllers.Where(x => x.ControllerName == "Subtitle").FirstOrDefault();
ActionModel? getPostedPlaybackInfo, getMasterHlsVideoPlaylist, getVariantHlsVideoPlaylist, getHlsVideoSegment, getSubtitleWithTicks;
if (mediaInfo == null
|| (getPostedPlaybackInfo = mediaInfo.Actions.Where(x => x.ActionName == "GetPostedPlaybackInfo").FirstOrDefault()) == null
|| dynamicHls == null
|| (getMasterHlsVideoPlaylist = dynamicHls.Actions.Where(x => x.ActionName == "GetMasterHlsVideoPlaylist").FirstOrDefault()) == null
|| (getVariantHlsVideoPlaylist = dynamicHls.Actions.Where(x => x.ActionName == "GetVariantHlsVideoPlaylist").FirstOrDefault()) == null
|| (getHlsVideoSegment = dynamicHls.Actions.Where(x => x.ActionName == "GetHlsVideoSegment").FirstOrDefault()) == null)
|| (getHlsVideoSegment = dynamicHls.Actions.Where(x => x.ActionName == "GetHlsVideoSegment").FirstOrDefault()) == null
|| subtitle == null
|| (getSubtitleWithTicks = subtitle.Actions.Where(x => x.ActionName == "GetSubtitleWithTicks").FirstOrDefault()) == null)
throw new InvalidOperationException("Failed to register for MediaSourceId extraction from the Jellyfin's MediaInfo controller.");
getPostedPlaybackInfo.Filters.Add(new GetPostedPlaybackInfoMediaSourceIdFilter());
@@ -90,6 +93,8 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
getMasterHlsVideoPlaylist.Filters.Add(new DynamicHlsMediaSourceIdFilter());
getVariantHlsVideoPlaylist.Filters.Add(new DynamicHlsMediaSourceIdFilter());
getHlsVideoSegment.Filters.Add(new DynamicHlsMediaSourceIdFilter());
// Slightly different parameter name
getSubtitleWithTicks.Filters.Add(new SubtitleMediaSourceIdFilter());
}
}
@@ -104,7 +109,9 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
// Fallback to the obsolete query argument
if (!context.ActionArguments.TryGetValue("mediaSourceId", out object? mediaSourceIdO)
|| (mediaSourceId = mediaSourceIdO as string) == null)
throw new InvalidOperationException("Cannot extract MediaSourceId from the Jellyfin's action MediaInfo.GetPostedPlaybackInfo.");
// It is optional in certain situations
return;
//throw new InvalidOperationException("Cannot extract MediaSourceId from the Jellyfin's action MediaInfo.GetPostedPlaybackInfo.");
}
context.HttpContext.Items.Add(CinemaMediaSourceManager.ContextItemsMediaSourceIdKey, mediaSourceId);
@@ -131,4 +138,21 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
{
}
}
sealed class SubtitleMediaSourceIdFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
string? mediaSourceId;
if (!context.ActionArguments.TryGetValue("routeMediaSourceId", out object? mediaSourceIdO)
|| (mediaSourceId = mediaSourceIdO as string) == null)
throw new InvalidOperationException("Cannot extract MediaSourceId from the Jellyfin's controller Subtitle.");
context.HttpContext.Items.Add(CinemaMediaSourceManager.ContextItemsMediaSourceIdKey, mediaSourceId);
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
}