More efficient session validity checks. Jellyfin session detection.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user