More efficient session validity checks. Jellyfin session detection.

This commit is contained in:
2024-12-06 21:49:43 +01:00
parent a0f6870493
commit 9b6a93cda3
5 changed files with 90 additions and 67 deletions

View File

@@ -51,6 +51,12 @@ public sealed class CinemaHost : IHostedService
public static Session? Webshare => _webshare;
public static async Task<bool> IsWebshareFreeAccount(CancellationToken cancel)
{
var webshare = _webshare;
return webshare == null || await webshare.GetTokenAsync(cancel) == null;
}
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
@@ -91,9 +97,15 @@ public sealed class CinemaHost : IHostedService
|| config.WebshareToken != _lastWebshareToken)
{
// Re-create the Webshare session as the credentials have changed
if (config.WebshareUser != null && config.WebsharePassword != null)
if (config.WebshareUser != null && config.WebsharePassword != null) {
_webshare = new Session(config.WebshareUser, config.WebsharePassword, config.WebshareToken);
else
string? token = _webshare.GetTokenAsync(default).GetAwaiter().GetResult();
if (token != config.WebshareToken) {
config.WebshareToken = token;
CinemaPlugin.Instance!.SaveConfiguration(config);
}
} else
_webshare = null;
_lastWebsharePassword = config.WebsharePassword;

View File

@@ -25,7 +25,6 @@ namespace Jellyfin.Plugin.Cinema;
public sealed class CinemaMediaSourceManager : IMediaSourceManager
{
private const bool IsWebshareFreeAccount = true;
private const double BitrateMargin = 0.1; // 10 %
private const double WebshareFreeBitrate = 300000 * 8;
@@ -185,11 +184,13 @@ public sealed class CinemaMediaSourceManager : IMediaSourceManager
VersionSetEntry ver = await GetVersionSet(video, out BaseItem? videoPrimary, out string? csId, default);
if (ver.Versions != null)
{
bool isFreeAccount = await CinemaHost.IsWebshareFreeAccount(cancellationToken);
IEnumerable<Video> items;
switch (video)
{
case CinemaMovie movie: items = GetVideoVersionsEnumerate<CinemaMovie>(csId!, movie, videoPrimary!, ver.Versions); break;
case CinemaEpisode episode: items = GetVideoVersionsEnumerate<CinemaEpisode>(csId!, episode, videoPrimary!, SortVersionsByPreferences(user, ver.Versions)); break;
case CinemaEpisode episode: items = GetVideoVersionsEnumerate<CinemaEpisode>(csId!, episode, videoPrimary!, SortVersionsByPreferences(user, ver.Versions, isFreeAccount)); break;
default: throw new NotSupportedException(string.Format("BaseItem type '{0}' not supported in CinemaMediaSources.", video.GetType().Name));
}
@@ -197,14 +198,14 @@ public sealed class CinemaMediaSourceManager : IMediaSourceManager
// Warning: We assume GetVideoVersionsEnumerate keeps the order between its input and output collections
// Note: Also makes sure BaseItems exist for each version
foreach (var i in items)
result.Add(GetVersionInfo(i, ver.Versions[idx++].Meta, thisServerBaseUri));
result.Add(GetVersionInfo(i, ver.Versions[idx++].Meta, thisServerBaseUri, isFreeAccount));
// For episodes we must choose the audio/subtitle automatically as there is no UI for it
// and MediaInfoHelper.SetDeviceSpecificData and deeper StreamBuilder.BuildVideoItem does
// a poor job at selecting the proper streams
if (video is CinemaEpisode)
foreach (MediaSourceInfo i in result)
ForceByPreferences(user, i);
ForceByPreferences(user, i, isFreeAccount);
}
break;
}
@@ -238,13 +239,13 @@ public sealed class CinemaMediaSourceManager : IMediaSourceManager
/// Sorts media verions by preferences as episodes currently do not support multiple versions
/// and the first one gets played.
/// </summary>
private static VersionEntry[] SortVersionsByPreferences(User user, VersionEntry[] items)
private static VersionEntry[] SortVersionsByPreferences(User user, VersionEntry[] items, bool isFreeAccount)
{
if (user == null)
// We have no info about the user so nothing to sort
return items;
double? bitrateLimit = IsWebshareFreeAccount ? WebshareFreeBitrate : null;
double? bitrateLimit = isFreeAccount ? WebshareFreeBitrate : null;
string? audioLang = user.AudioLanguagePreference;
string? subtitleLang = user.SubtitleLanguagePreference;
int[] scores = new int[items.Length];
@@ -290,7 +291,7 @@ public sealed class CinemaMediaSourceManager : IMediaSourceManager
return items;
}
private static void ForceByPreferences(User user, MediaSourceInfo item)
private static void ForceByPreferences(User user, MediaSourceInfo item, bool isFreeAccount)
{
if (user == null)
// We have no info about the user so nothing to force
@@ -298,7 +299,7 @@ public sealed class CinemaMediaSourceManager : IMediaSourceManager
string? audioLang = user.AudioLanguagePreference;
// Free account prevents seeking and thus subtitle exctraction
string? subtitleLang = IsWebshareFreeAccount ? null : user.SubtitleLanguagePreference;
string? subtitleLang = isFreeAccount ? null : user.SubtitleLanguagePreference;
foreach (MediaStream i in item.MediaStreams)
{
@@ -469,10 +470,12 @@ public sealed class CinemaMediaSourceManager : IMediaSourceManager
if (ver == null)
return null;
return GetVersionInfo(item, ver.Meta, thisServerBaseUri);
bool isFreeAccount = await CinemaHost.IsWebshareFreeAccount(cancel);
return GetVersionInfo(item, ver.Meta, thisServerBaseUri, isFreeAccount);
}
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver, Uri thisServerBaseUri)
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver, Uri thisServerBaseUri, bool isFreeAccount)
{
MediaSourceInfo result = new MediaSourceInfo();
result.VideoType = VideoType.VideoFile;
@@ -482,7 +485,7 @@ public sealed class CinemaMediaSourceManager : IMediaSourceManager
result.MediaStreams = VersionToMediaStreams(item, ver);
double bitRate = (ver.size ?? 0.0) * 8 / (item.RunTimeTicks ?? 1.0) * TimeSpan.TicksPerSecond;
bool showBitrateWarning = IsWebshareFreeAccount && bitRate / (1 + BitrateMargin) > WebshareFreeBitrate;
bool showBitrateWarning = isFreeAccount && bitRate / (1 + BitrateMargin) > WebshareFreeBitrate;
string name = showBitrateWarning ? "LOGIN Webshare! " : "";
name += $"{Math.Round((double)(ver.size ?? 0) / (1024 * 1024 * 1024), 2)} GB ({ver.date_added?.ToShortDateString()})"
@@ -532,7 +535,7 @@ public sealed class CinemaMediaSourceManager : IMediaSourceManager
// Propagate "-seekable 0" to ffmpeg as free accounts on Webshare cannot use "Range: bytes" HTTP header
// HACK: We misuse the user-agent as GetExtraArguments in MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
// does not properly escape its value for command line
if (IsWebshareFreeAccount)
if (isFreeAccount)
{
if (result.RequiredHttpHeaders == null)
result.RequiredHttpHeaders = new Dictionary<string, string>();

View File

@@ -23,7 +23,7 @@ public class CinemaPluginConfiguration : BasePluginConfiguration
public string MusicFolderName { get; set; }
public bool HideMusicFolder { get; set; }
public string WebshareUser { get; set; }
public string WebsharePassword { get; set; }
public string WebshareToken { get; set; }
public string? WebshareUser { get; set; }
public string? WebsharePassword { get; set; }
public string? WebshareToken { get; set; }
}

View File

@@ -1,7 +1,5 @@
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Xml;
using Cinema.Webshare;
@@ -17,6 +15,7 @@ public sealed class Session
private static readonly Uri WebshareApiUri = LinkGenerator.WebshareApiUri;
private static readonly TimeSpan InitialLoginBackoff = new TimeSpan(4 * TimeSpan.TicksPerSecond);
private static readonly TimeSpan MaxLoginBackoff = new TimeSpan(8 * TimeSpan.TicksPerMinute);
private static readonly TimeSpan TokenValidPeriod = new TimeSpan(120 * TimeSpan.TicksPerSecond);
private readonly string _userName;
private readonly string _password;
@@ -26,6 +25,7 @@ public sealed class Session
private DateTime _nextLoginTrial;
private TimeSpan _nextLoginBackoff;
private DateTime _nextValidTrial;
/// <summary>
/// Constructs a session from credentials and optionally a possibly live token.
@@ -74,52 +74,62 @@ public sealed class Session
/// <returns>True if the local instance data are valid. -or- False otherwise.</returns>
private async Task<bool> EnsureValidAsync(CancellationToken cancel)
{
if (!await RefreshUserDataAsync(cancel))
DateTime now = DateTime.Now;
if (_token != null && (_isVip ?? false) && now < _nextValidTrial)
// Fast track positive check
return true;
if (await RefreshUserDataAsync(cancel) && (_isVip ?? false))
{
DateTime now = DateTime.Now;
if (now > _nextLoginTrial)
// Valid VIP (slower with API check)
_nextValidTrial = now + TokenValidPeriod;
return true;
}
if (now > _nextLoginTrial)
{
// Try to log in again
string? token;
try
{
// Try to login again
string? token;
try
{
string? salt;
if ((salt = await GetSaltAsync(_userName, cancel)) == null
|| (token = await GetTokenAsync(_userName, _password.HashPassword(salt), cancel)) == null)
{
// Login failed
token = null;
}
}
catch
string? salt;
if ((salt = await GetSaltAsync(_userName, cancel)) == null
|| (token = await GetTokenAsync(_userName, _password.HashPassword(salt), cancel)) == null)
{
// Login failed
token = null;
}
if (token != null)
{
_nextLoginBackoff = InitialLoginBackoff;
_token = token;
if (await RefreshUserDataAsync(cancel))
return true;
}
else
{
TimeSpan nextBackoff = new TimeSpan(2 * _nextLoginBackoff.Ticks);
if (nextBackoff > MaxLoginBackoff)
nextBackoff = MaxLoginBackoff;
_nextLoginTrial = now + nextBackoff;
}
}
catch
{
token = null;
}
_token = null;
_isVip = null;
_vipDaysLeft = null;
return false;
if (token != null && await RefreshUserDataAsync(cancel) && (_isVip ?? false))
{
// Valid token with VIP
_nextLoginBackoff = InitialLoginBackoff;
_token = token;
_nextValidTrial = now + TokenValidPeriod;
return true;
}
else
{
// Invalid, no token or not VIP ...
TimeSpan nextBackoff = new TimeSpan(2 * _nextLoginBackoff.Ticks);
if (nextBackoff > MaxLoginBackoff)
nextBackoff = MaxLoginBackoff;
_nextLoginTrial = now + nextBackoff;
}
}
else
return true;
// ... still invalid, no token or not VIP
_token = null;
_isVip = null;
_vipDaysLeft = null;
return false;
}
/// <summary>

View File

@@ -7,8 +7,6 @@ namespace CinemaWeb.Layouts;
public abstract class BasicLayout
{
private static DateTime _nextLoginCheck;
protected abstract string Title { get; }
protected abstract Task RenderContentAsync(HttpRequest req, TextWriter w, CancellationToken cancel);
@@ -22,12 +20,10 @@ public abstract class BasicLayout
try
{
DateTime now = DateTime.UtcNow;
if (Program._webshare != null && _nextLoginCheck < now) {
if (await Program._webshare.GetTokenAsync(cancel) == null)
// Automatic logout
Program._webshare = null;
_nextLoginCheck = now + TimeSpan.FromMinutes(10);
}
var webshare = Program._webshare;
if (webshare != null && await webshare.GetTokenAsync(cancel) == null)
// Automatic logout
Program._webshare = null;
string? userName = Program._webshare != null ? Program._webshare.UserName : null;
RenderHeader(Title, userName, w);
@@ -46,7 +42,8 @@ public abstract class BasicLayout
return Results.Content(content.ToString(), "text/html; charset=utf-8", null, statusCode);
}
private static void RenderHeader(string title, string? userName, TextWriter w) {
private static void RenderHeader(string title, string? userName, TextWriter w)
{
w.WriteLine("<!doctype html>");
w.WriteLine("<html><head>");
w.WriteLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
@@ -71,7 +68,8 @@ public abstract class BasicLayout
w.WriteLine("<div style=\"font-weight:bold\">Menu: <a href=\"/\">Hledání</a></div>");
}
private static void RenderFooter(TextWriter w) {
private static void RenderFooter(TextWriter w)
{
w.WriteLine("</body></html>");
}
}