User authentication. Jellyfin configuration incomplete.
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using System.Linq.Expressions;
|
||||
using CinemaLib.Webshare;
|
||||
using Jellyfin.Plugin.Cinema.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -20,8 +22,11 @@ public sealed class CinemaHost : IHostedService
|
||||
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;
|
||||
private string? _lastWebsharePasswordHash;
|
||||
private string? _lastWebshareToken;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a the Stream Cinema plugin.
|
||||
@@ -44,12 +49,16 @@ public sealed class CinemaHost : IHostedService
|
||||
|
||||
public static string FallbackCulture => "en";
|
||||
|
||||
public static Session? Webshare => _webshare;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
CinemaPluginConfiguration config = CinemaPlugin.Instance!.Configuration;
|
||||
CinemaPlugin.Instance!.ConfigurationChanged += ReloadConfig;
|
||||
|
||||
EnsureRootFolders(CinemaPlugin.Instance!.Configuration);
|
||||
EnsureWebshareSession(config);
|
||||
EnsureRootFolders(config);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -58,10 +67,13 @@ public sealed class CinemaHost : IHostedService
|
||||
|
||||
private void ReloadConfig(object? sender, BasePluginConfiguration e)
|
||||
{
|
||||
EnsureRootFolders((CinemaPluginConfiguration)e);
|
||||
CinemaPluginConfiguration config = (CinemaPluginConfiguration)e;
|
||||
EnsureWebshareSession(config);
|
||||
EnsureRootFolders(config);
|
||||
}
|
||||
|
||||
private void EnsureRootFolders(CinemaPluginConfiguration config) {
|
||||
private void EnsureRootFolders(CinemaPluginConfiguration config)
|
||||
{
|
||||
CinemaFilterFolder.CreateRootFilterFolder<CinemaMoviesFolder>(string.IsNullOrWhiteSpace(config.MoviesFolderName) ? "Movies" : config.MoviesFolderName)
|
||||
.Hide = config.HideMoviesFolder;
|
||||
CinemaFilterFolder.CreateRootFilterFolder<CinemaTvShowsFolder>(string.IsNullOrWhiteSpace(config.SeriesFolderName) ? "TV Shows" : config.SeriesFolderName)
|
||||
@@ -71,4 +83,21 @@ public sealed class CinemaHost : IHostedService
|
||||
CinemaFilterFolder.CreateRootFilterFolder<CinemaConcertFolder>(string.IsNullOrWhiteSpace(config.MusicFolderName) ? "Music" : config.MusicFolderName)
|
||||
.Hide = config.HideMusicFolder;
|
||||
}
|
||||
|
||||
private void EnsureWebshareSession(CinemaPluginConfiguration config)
|
||||
{
|
||||
if (config.WebshareUser != _webshare?.UserName
|
||||
|| config.WebsharePasswordHash != _lastWebsharePasswordHash
|
||||
|| config.WebshareToken != _lastWebshareToken)
|
||||
{
|
||||
// Re-create the Webshare session as the credentials have changed
|
||||
if (config.WebshareUser != null && config.WebsharePasswordHash != null)
|
||||
_webshare = new Session(config.WebshareUser, config.WebsharePasswordHash, config.WebshareToken);
|
||||
else
|
||||
_webshare = null;
|
||||
|
||||
_lastWebsharePasswordHash = config.WebsharePasswordHash;
|
||||
_lastWebshareToken = config.WebshareToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -772,7 +772,7 @@ public sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
Uri link;
|
||||
switch (provider)
|
||||
{
|
||||
case "webshare": link = await LinkGenerator.GenerateDownloadLinkAsync(ident, name, cancel); break;
|
||||
case "webshare": link = await LinkGenerator.GenerateDownloadLinkAsync(ident, name, CinemaHost.Webshare, cancel); break;
|
||||
default: throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,5 +24,6 @@ public class CinemaPluginConfiguration : BasePluginConfiguration
|
||||
public bool HideMusicFolder { get; set; }
|
||||
|
||||
public string WebshareUser { get; set; }
|
||||
public string WebsharePassword { get; set; }
|
||||
public string WebsharePasswordHash { get; set; }
|
||||
public string WebshareToken { get; set; }
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="password" id="websharePassword" label="Webshare password" />
|
||||
<input type="hidden" id="websharePasswordHash" />
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
@@ -117,10 +118,10 @@
|
||||
bubbles: true,
|
||||
cancelable: false
|
||||
}));
|
||||
var websharePassword = document.querySelector('#websharePassword');
|
||||
if (config.WebsharePassword)
|
||||
websharePassword.value = config.WebsharePassword;
|
||||
websharePassword.dispatchEvent(new Event('change', {
|
||||
var websharePasswordHash = document.querySelector('#websharePasswordHash');
|
||||
if (config.WebsharePasswordHash && config.WebsharePasswordHash.length != 0)
|
||||
websharePasswordHash.value = "";
|
||||
websharePasswordHash.dispatchEvent(new Event('change', {
|
||||
bubbles: true,
|
||||
cancelable: false
|
||||
}));
|
||||
@@ -144,7 +145,7 @@
|
||||
config.HideMusicFolder = document.querySelector('#hideMusicFolder').checked;
|
||||
|
||||
config.WebshareUser = document.querySelector('#webshareUser').value;
|
||||
config.WebsharePassword = document.querySelector('#websharePassword').value;
|
||||
config.WebsharePasswordHash = document.querySelector('#websharePasswordHash').value;
|
||||
|
||||
ApiClient.updatePluginConfiguration(CinemaPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ class Program
|
||||
{
|
||||
CancellationToken cancel = default;
|
||||
//string a = LinkGenerator.CalculatePassword(fileId: "2kRBqAYQS2", fileName: "bIaFGdkvKiiTBo2_S3E1", salt: "UX8y8Fpa");
|
||||
Uri a = await LinkGenerator.GenerateDownloadLinkAsync(fileId: "2kRBqAYQS2", fileName: "bIaFGdkvKiiTBo2_S3E1", cancel);
|
||||
Uri a = await LinkGenerator.GenerateDownloadLinkAsync(fileId: "2kRBqAYQS2", fileName: "bIaFGdkvKiiTBo2_S3E1", null, cancel);
|
||||
Console.WriteLine(a);
|
||||
}
|
||||
}
|
||||
25
CinemaLib/Webshare/ApiExtensions.cs
Normal file
25
CinemaLib/Webshare/ApiExtensions.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Xml;
|
||||
|
||||
namespace Cinema.Webshare;
|
||||
|
||||
static class ApiExtensions {
|
||||
public static Exception? GetExceptionIfStatusNotOK(this XmlReader r, out string? code)
|
||||
{
|
||||
string status = r.ReadElementContentAsString("status", "");
|
||||
if (status == "OK") {
|
||||
code = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
code = r.ReadElementContentAsString("code", "");
|
||||
string message = r.ReadElementContentAsString("message", "");
|
||||
return new IOException(code + ": " + message);
|
||||
}
|
||||
|
||||
public static void ThrowIfStatusNotOK(this XmlReader r)
|
||||
{
|
||||
Exception? e = GetExceptionIfStatusNotOK(r, out _);
|
||||
if (e != null)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
24
CinemaLib/Webshare/CredentialCheck.cs
Normal file
24
CinemaLib/Webshare/CredentialCheck.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace CinemaLib.Webshare;
|
||||
|
||||
/// <summary>
|
||||
/// Context for checking user name and password step-by-step.
|
||||
/// </summary>
|
||||
public struct CredentialCheck
|
||||
{
|
||||
private readonly string _userName;
|
||||
private readonly string _salt;
|
||||
|
||||
internal CredentialCheck(string userName, string salt)
|
||||
{
|
||||
this._userName = userName;
|
||||
this._salt = salt;
|
||||
}
|
||||
|
||||
public bool IsNull => _userName == null;
|
||||
|
||||
public string UserName => _userName;
|
||||
|
||||
internal string Salt => _salt;
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using CinemaLib.Webshare;
|
||||
|
||||
namespace Cinema.Webshare;
|
||||
|
||||
public class LinkGenerator
|
||||
public static class LinkGenerator
|
||||
{
|
||||
private static readonly BaseEncoding UnixMD5 = new BaseEncoding("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", false);
|
||||
private static readonly Uri WebshareApiUri = new Uri("https://webshare.cz/api/");
|
||||
internal static readonly Uri WebshareApiUri = new Uri("https://webshare.cz/api/");
|
||||
|
||||
/// <summary>
|
||||
/// Generates a download link for the given Webshare download.
|
||||
/// </summary>
|
||||
/// <param name="fileId">Webshare file identifier.</param>
|
||||
/// <param name="fileName">Webshare file name.</param>
|
||||
/// <param name="session">Optional session to get a VIP link.</param>
|
||||
/// <param name="cancel">Asynchronous cancellation.</param>
|
||||
/// <returns>Time-limited download link.</returns>
|
||||
public async static Task<Uri> GenerateDownloadLinkAsync(string fileId, string fileName, CancellationToken cancel)
|
||||
public async static Task<Uri> GenerateDownloadLinkAsync(string fileId, string fileName, Session? session, CancellationToken cancel)
|
||||
{
|
||||
// Obtain the download password salt
|
||||
// Example response: <?xml version="1.0" encoding="UTF-8"?><response><status>OK</status><salt>UX8y8Fpa</salt><app_version>30</app_version></response>
|
||||
@@ -29,7 +29,7 @@ public class LinkGenerator
|
||||
|
||||
XmlReader saltR = XmlReader.Create(saltRes.Content.ReadAsStream());
|
||||
saltR.ReadStartElement("response");
|
||||
Exception? e = GetExceptionIfStatusNotOK(saltR, out string? code);
|
||||
Exception? e = saltR.GetExceptionIfStatusNotOK(out string? code);
|
||||
string password;
|
||||
if (e != null)
|
||||
{
|
||||
@@ -61,6 +61,7 @@ public class LinkGenerator
|
||||
new KeyValuePair<string, string>("device_res_x", "3840"),
|
||||
new KeyValuePair<string, string>("device_res_y", "2160"),
|
||||
new KeyValuePair<string, string>("password", password),
|
||||
new KeyValuePair<string, string>(Session.TokenKeyName, (session != null ? await session.GetTokenAsync(cancel) : null) ?? ""),
|
||||
}), cancel
|
||||
);
|
||||
if (!linkRes.IsSuccessStatusCode)
|
||||
@@ -68,7 +69,7 @@ public class LinkGenerator
|
||||
|
||||
XmlReader linkR = XmlReader.Create(linkRes.Content.ReadAsStream());
|
||||
linkR.ReadStartElement("response");
|
||||
ThrowIfStatusNotOK(linkR);
|
||||
linkR.ThrowIfStatusNotOK();
|
||||
string link = linkR.ReadElementContentAsString("link", "");
|
||||
linkR.ReadElementContentAsString("app_version", "");
|
||||
linkR.ReadEndElement(); // response
|
||||
@@ -76,26 +77,6 @@ public class LinkGenerator
|
||||
return new Uri(link);
|
||||
}
|
||||
|
||||
private static Exception? GetExceptionIfStatusNotOK(XmlReader r, out string? code)
|
||||
{
|
||||
string status = r.ReadElementContentAsString("status", "");
|
||||
if (status == "OK") {
|
||||
code = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
code = r.ReadElementContentAsString("code", "");
|
||||
string message = r.ReadElementContentAsString("message", "");
|
||||
return new IOException(code + ": " + message);
|
||||
}
|
||||
|
||||
private static void ThrowIfStatusNotOK(XmlReader r)
|
||||
{
|
||||
Exception? e = GetExceptionIfStatusNotOK(r, out _);
|
||||
if (e != null)
|
||||
throw e;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the Webshare download password from file identifier, name and salt.
|
||||
/// </summary>
|
||||
@@ -117,110 +98,8 @@ public class LinkGenerator
|
||||
string toHash = fileName + fileId;
|
||||
byte[] password = SHA256.HashData(Encoding.UTF8.GetBytes(toHash));
|
||||
string passwordS = Convert.ToHexString(password).ToLowerInvariant();
|
||||
string crypt = MD5Crypt(passwordS, salt);
|
||||
string crypt = Session.MD5Crypt(passwordS, salt);
|
||||
byte[] result = SHA1.HashData(Encoding.ASCII.GetBytes(crypt));
|
||||
return Convert.ToHexString(result).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string MD5Crypt(string password, string salt)
|
||||
{
|
||||
string prefixString = "$1$";
|
||||
byte[] prefixBytes = Encoding.ASCII.GetBytes(prefixString);
|
||||
byte[] saltBytes = Encoding.ASCII.GetBytes(salt);
|
||||
|
||||
byte[] key = Encoding.ASCII.GetBytes(password);
|
||||
byte[] truncatedSalt = TruncateAndCopy(saltBytes, 8);
|
||||
byte[] crypt = Crypt(key, truncatedSalt, prefixBytes, MD5.Create());
|
||||
|
||||
string result = prefixString
|
||||
+ salt + '$'
|
||||
+ UnixMD5.GetString(crypt);
|
||||
|
||||
return result.TrimEnd('=');
|
||||
}
|
||||
|
||||
private static byte[] Crypt(byte[] key, byte[] salt, byte[] prefix, HashAlgorithm A)
|
||||
{
|
||||
byte[] H = null, I = null;
|
||||
|
||||
A.Initialize();
|
||||
AddToDigest(A, key);
|
||||
AddToDigest(A, salt);
|
||||
AddToDigest(A, key);
|
||||
FinishDigest(A);
|
||||
|
||||
I = (byte[])A.Hash.Clone();
|
||||
|
||||
A.Initialize();
|
||||
AddToDigest(A, key);
|
||||
AddToDigest(A, prefix);
|
||||
AddToDigest(A, salt);
|
||||
|
||||
AddToDigestRolling(A, I, 0, I.Length, key.Length);
|
||||
|
||||
int length = key.Length;
|
||||
for (int i = 0; i < 31 && length != 0; i++)
|
||||
{
|
||||
AddToDigest(A, new[] { (length & (1 << i)) != 0 ? (byte)0 : key[0] });
|
||||
length &= ~(1 << i);
|
||||
}
|
||||
FinishDigest(A);
|
||||
|
||||
H = (byte[])A.Hash.Clone();
|
||||
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
A.Initialize();
|
||||
if ((i & 1) != 0) { AddToDigest(A, key); }
|
||||
if ((i & 1) == 0) { AddToDigest(A, H); }
|
||||
if ((i % 3) != 0) { AddToDigest(A, salt); }
|
||||
if ((i % 7) != 0) { AddToDigest(A, key); }
|
||||
if ((i & 1) != 0) { AddToDigest(A, H); }
|
||||
if ((i & 1) == 0) { AddToDigest(A, key); }
|
||||
FinishDigest(A);
|
||||
|
||||
Array.Copy(A.Hash, H, H.Length);
|
||||
}
|
||||
|
||||
byte[] crypt = new byte[H.Length];
|
||||
int[] permutation = new[] { 11, 4, 10, 5, 3, 9, 15, 2, 8, 14, 1, 7, 13, 0, 6, 12 };
|
||||
Array.Reverse(permutation);
|
||||
for (int i = 0; i < crypt.Length; i++)
|
||||
{
|
||||
crypt[i] = H[permutation[i]];
|
||||
}
|
||||
|
||||
return crypt;
|
||||
}
|
||||
|
||||
private static void AddToDigest(HashAlgorithm algorithm, byte[] buffer)
|
||||
{
|
||||
AddToDigest(algorithm, buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
private static void AddToDigest(HashAlgorithm algorithm, byte[] buffer, int offset, int count)
|
||||
{
|
||||
algorithm.TransformBlock(buffer, offset, count, buffer, offset);
|
||||
}
|
||||
|
||||
private static void AddToDigestRolling(HashAlgorithm algorithm, byte[] buffer, int offset, int inputCount, int outputCount)
|
||||
{
|
||||
int count;
|
||||
for (count = 0; count < outputCount; count += inputCount)
|
||||
{
|
||||
AddToDigest(algorithm, buffer, offset, Math.Min(outputCount - count, inputCount));
|
||||
}
|
||||
}
|
||||
|
||||
private static void FinishDigest(HashAlgorithm algorithm)
|
||||
{
|
||||
algorithm.TransformFinalBlock(new byte[0], 0, 0);
|
||||
}
|
||||
|
||||
private static byte[] TruncateAndCopy(byte[] buffer, int maxLength)
|
||||
{
|
||||
byte[] truncatedBuffer = new byte[Math.Min(buffer.Length, maxLength)];
|
||||
Array.Copy(buffer, truncatedBuffer, truncatedBuffer.Length);
|
||||
return truncatedBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
343
CinemaLib/Webshare/Session.cs
Normal file
343
CinemaLib/Webshare/Session.cs
Normal file
@@ -0,0 +1,343 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Cinema.Webshare;
|
||||
|
||||
namespace CinemaLib.Webshare;
|
||||
|
||||
/// <summary>
|
||||
/// Webshare user authentication context.
|
||||
/// </summary>
|
||||
public sealed class Session
|
||||
{
|
||||
internal const string TokenKeyName = "wst";
|
||||
|
||||
private static readonly BaseEncoding UnixMD5 = new BaseEncoding("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", false);
|
||||
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 readonly string _userName;
|
||||
private readonly string _passwordHash;
|
||||
private string? _token;
|
||||
private bool? _isVip;
|
||||
private int? _vipDaysLeft;
|
||||
|
||||
private DateTime _nextLoginTrial;
|
||||
private TimeSpan _nextLoginBackoff;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a session from credentials and optionally a possibly live token.
|
||||
/// </summary>
|
||||
public Session(string userName, string passwordHash, string? token = null)
|
||||
{
|
||||
if (userName == null || passwordHash == null)
|
||||
throw new ArgumentNullException();
|
||||
|
||||
this._userName = userName;
|
||||
this._passwordHash = passwordHash;
|
||||
this._token = token;
|
||||
this._nextLoginTrial = default;
|
||||
this._nextLoginBackoff = InitialLoginBackoff;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user name under which the session is authenticated.
|
||||
/// </summary>
|
||||
public string UserName => _userName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an existing VIP session token or creates a new one.
|
||||
/// </summary>
|
||||
/// <param name="cancel">Asynchronous cancellation.</param>
|
||||
/// <returns>Valid VIP token. -or- Null if account credentials are invalid or not with VIP active.</returns>
|
||||
public async Task<string?> GetTokenAsync(CancellationToken cancel)
|
||||
{
|
||||
return (await EnsureValidAsync(cancel)) && (_isVip ?? false) ? _token : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of days left in the VIP account.
|
||||
/// </summary>
|
||||
/// <param name="cancel">Asynchronous cancellation.</param>
|
||||
/// <returns>Days left. -or- Null if account credentials are invalid or not with VIP active.</returns>
|
||||
public async Task<int?> GetVipDaysLeftAsync(CancellationToken cancel)
|
||||
{
|
||||
return await EnsureValidAsync(cancel) ? _vipDaysLeft : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes sure the cached instance data are valid. The account still may not be VIP.
|
||||
/// </summary>
|
||||
/// <param name="cancel"></param>
|
||||
/// <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 (now > _nextLoginTrial)
|
||||
{
|
||||
// Try to login again
|
||||
string? token;
|
||||
try
|
||||
{
|
||||
string? salt = await GetSaltAsync(_userName, cancel);
|
||||
token = salt != null ? await GetTokenAsync(new CredentialCheck(_userName, salt), _passwordHash, cancel) : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
_token = null;
|
||||
_isVip = null;
|
||||
_vipDaysLeft = null;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether user name is valid.
|
||||
/// </summary>
|
||||
/// <param name="userName">User name to check.</param>
|
||||
/// <param name="cancel">Asynchronous cancellation.</param>
|
||||
/// <returns>Intermedeiate result usable for password check. -or- <see cref="CredentialCheck.IsNull"/> for invalid user name.</returns>
|
||||
public static async Task<CredentialCheck> CheckUserNameAsync(string userName, CancellationToken cancel)
|
||||
{
|
||||
if (userName == null || userName.Length == 0)
|
||||
throw new ArgumentNullException();
|
||||
|
||||
string? salt = await GetSaltAsync(userName, cancel);
|
||||
return salt != null ? new CredentialCheck(userName, salt) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if password is valid for the given user name.
|
||||
/// </summary>
|
||||
/// <param name="userName">User name or email.</param>
|
||||
/// <param name="password">Password in plain-text.</param>
|
||||
/// <returns>True if password is valid. -or- False otherwise.</returns>
|
||||
public static async Task<bool> CheckPasswordAsync(CredentialCheck userName, string password, CancellationToken cancel)
|
||||
{
|
||||
if (userName.IsNull || password == null)
|
||||
throw new ArgumentNullException();
|
||||
|
||||
string passwordHash = MD5Crypt(password, userName.Salt);
|
||||
return null != await GetTokenAsync(userName, passwordHash, cancel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a user name salt.
|
||||
/// </summary>
|
||||
/// <returns>Non-null if user name is valid. -or- Null otherwise.</returns>
|
||||
private static async Task<string?> GetSaltAsync(string userName, CancellationToken cancel)
|
||||
{
|
||||
// Example response: <?xml version="1.0" encoding="UTF-8"?><response><status>OK</status><salt>somesaltvalue</salt><app_version>30</app_version></response>
|
||||
Uri saltUri = new Uri(WebshareApiUri, "salt/");
|
||||
HttpResponseMessage saltRes = await Program._http.PostAsync(saltUri, new FormUrlEncodedContent(new[] {
|
||||
new KeyValuePair<string, string>("username_or_email", userName),
|
||||
}), cancel
|
||||
);
|
||||
if (!saltRes.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
XmlReader saltR = XmlReader.Create(saltRes.Content.ReadAsStream());
|
||||
saltR.ReadStartElement("response");
|
||||
Exception? e = saltR.GetExceptionIfStatusNotOK(out string? code);
|
||||
if (e != null)
|
||||
return null;
|
||||
|
||||
// Calculate the download password
|
||||
string salt = saltR.ReadElementContentAsString("salt", "");
|
||||
saltR.ReadElementContentAsString("app_version", "");
|
||||
saltR.ReadEndElement(); // response
|
||||
|
||||
return salt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the session token.
|
||||
/// </summary>
|
||||
/// <returns>Non-null if user name is valid. -or- Null otherwise.</returns>
|
||||
private static async Task<string?> GetTokenAsync(CredentialCheck userName, string passwordHash, CancellationToken cancel)
|
||||
{
|
||||
// Example response: <?xml version="1.0" encoding="UTF-8"?><response><status>OK</status><token>sometokenvalue</token><app_version>30</app_version></response>
|
||||
Uri tokenUri = new Uri(WebshareApiUri, "login/");
|
||||
HttpResponseMessage tokenRes = await Program._http.PostAsync(tokenUri, new FormUrlEncodedContent(new[] {
|
||||
new KeyValuePair<string, string>("username_or_email", userName.UserName),
|
||||
new KeyValuePair<string, string>("password", passwordHash),
|
||||
new KeyValuePair<string, string>("keep_logged_in", "1"),
|
||||
}), cancel
|
||||
);
|
||||
if (!tokenRes.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
XmlReader tokenR = XmlReader.Create(tokenRes.Content.ReadAsStream());
|
||||
tokenR.ReadStartElement("response");
|
||||
Exception? e = tokenR.GetExceptionIfStatusNotOK(out string? code);
|
||||
if (e != null)
|
||||
return null;
|
||||
|
||||
// Calculate the download password
|
||||
string token = tokenR.ReadElementContentAsString("token", "");
|
||||
tokenR.ReadElementContentAsString("app_version", "");
|
||||
tokenR.ReadEndElement(); // response
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets user data and indirectly validates the session token.
|
||||
/// </summary>
|
||||
/// <returns>True if refresh has been successful and session token is valid. -or- False otherwise.</returns>
|
||||
private async Task<bool> RefreshUserDataAsync(CancellationToken cancel)
|
||||
{
|
||||
_isVip = null;
|
||||
_vipDaysLeft = null;
|
||||
|
||||
// Example response: <?xml version="1.0" encoding="UTF-8"?><response><status>OK</status>...??...<app_version>30</app_version></response>
|
||||
Uri dataUri = new Uri(WebshareApiUri, "user_data/");
|
||||
HttpResponseMessage dataRes = await Program._http.PostAsync(dataUri, new FormUrlEncodedContent(new[] {
|
||||
new KeyValuePair<string, string>(TokenKeyName, _token ?? "")
|
||||
}), cancel
|
||||
);
|
||||
if (!dataRes.IsSuccessStatusCode)
|
||||
return false;
|
||||
|
||||
XmlReader tokenR = XmlReader.Create(dataRes.Content.ReadAsStream());
|
||||
tokenR.ReadStartElement("response");
|
||||
Exception? e = tokenR.GetExceptionIfStatusNotOK(out string? code);
|
||||
if (e != null)
|
||||
return false;
|
||||
|
||||
// Calculate the download password
|
||||
_isVip = tokenR.ReadElementContentAsString("vip", "") == "1";
|
||||
if (int.TryParse(tokenR.ReadElementContentAsString("vip_days", ""), CultureInfo.InvariantCulture, out int vipDays))
|
||||
_vipDaysLeft = vipDays;
|
||||
tokenR.ReadElementContentAsString("app_version", "");
|
||||
tokenR.ReadEndElement(); // response
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static string MD5Crypt(string password, string salt)
|
||||
{
|
||||
string prefixString = "$1$";
|
||||
byte[] prefixBytes = Encoding.ASCII.GetBytes(prefixString);
|
||||
byte[] saltBytes = Encoding.ASCII.GetBytes(salt);
|
||||
|
||||
byte[] key = Encoding.ASCII.GetBytes(password);
|
||||
byte[] truncatedSalt = TruncateAndCopy(saltBytes, 8);
|
||||
byte[] crypt = Crypt(key, truncatedSalt, prefixBytes, MD5.Create());
|
||||
|
||||
string result = prefixString
|
||||
+ salt + '$'
|
||||
+ UnixMD5.GetString(crypt);
|
||||
|
||||
return result.TrimEnd('=');
|
||||
}
|
||||
|
||||
private static byte[] Crypt(byte[] key, byte[] salt, byte[] prefix, HashAlgorithm A)
|
||||
{
|
||||
byte[] H = null, I = null;
|
||||
|
||||
A.Initialize();
|
||||
AddToDigest(A, key);
|
||||
AddToDigest(A, salt);
|
||||
AddToDigest(A, key);
|
||||
FinishDigest(A);
|
||||
|
||||
I = (byte[])A.Hash.Clone();
|
||||
|
||||
A.Initialize();
|
||||
AddToDigest(A, key);
|
||||
AddToDigest(A, prefix);
|
||||
AddToDigest(A, salt);
|
||||
|
||||
AddToDigestRolling(A, I, 0, I.Length, key.Length);
|
||||
|
||||
int length = key.Length;
|
||||
for (int i = 0; i < 31 && length != 0; i++)
|
||||
{
|
||||
AddToDigest(A, new[] { (length & (1 << i)) != 0 ? (byte)0 : key[0] });
|
||||
length &= ~(1 << i);
|
||||
}
|
||||
FinishDigest(A);
|
||||
|
||||
H = (byte[])A.Hash.Clone();
|
||||
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
A.Initialize();
|
||||
if ((i & 1) != 0) { AddToDigest(A, key); }
|
||||
if ((i & 1) == 0) { AddToDigest(A, H); }
|
||||
if ((i % 3) != 0) { AddToDigest(A, salt); }
|
||||
if ((i % 7) != 0) { AddToDigest(A, key); }
|
||||
if ((i & 1) != 0) { AddToDigest(A, H); }
|
||||
if ((i & 1) == 0) { AddToDigest(A, key); }
|
||||
FinishDigest(A);
|
||||
|
||||
Array.Copy(A.Hash, H, H.Length);
|
||||
}
|
||||
|
||||
byte[] crypt = new byte[H.Length];
|
||||
int[] permutation = new[] { 11, 4, 10, 5, 3, 9, 15, 2, 8, 14, 1, 7, 13, 0, 6, 12 };
|
||||
Array.Reverse(permutation);
|
||||
for (int i = 0; i < crypt.Length; i++)
|
||||
{
|
||||
crypt[i] = H[permutation[i]];
|
||||
}
|
||||
|
||||
return crypt;
|
||||
}
|
||||
|
||||
private static void AddToDigest(HashAlgorithm algorithm, byte[] buffer)
|
||||
{
|
||||
AddToDigest(algorithm, buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
private static void AddToDigest(HashAlgorithm algorithm, byte[] buffer, int offset, int count)
|
||||
{
|
||||
algorithm.TransformBlock(buffer, offset, count, buffer, offset);
|
||||
}
|
||||
|
||||
private static void AddToDigestRolling(HashAlgorithm algorithm, byte[] buffer, int offset, int inputCount, int outputCount)
|
||||
{
|
||||
int count;
|
||||
for (count = 0; count < outputCount; count += inputCount)
|
||||
{
|
||||
AddToDigest(algorithm, buffer, offset, Math.Min(outputCount - count, inputCount));
|
||||
}
|
||||
}
|
||||
|
||||
private static void FinishDigest(HashAlgorithm algorithm)
|
||||
{
|
||||
algorithm.TransformFinalBlock(new byte[0], 0, 0);
|
||||
}
|
||||
|
||||
private static byte[] TruncateAndCopy(byte[] buffer, int maxLength)
|
||||
{
|
||||
byte[] truncatedBuffer = new byte[Math.Min(buffer.Length, maxLength)];
|
||||
Array.Copy(buffer, truncatedBuffer, truncatedBuffer.Length);
|
||||
return truncatedBuffer;
|
||||
}
|
||||
}
|
||||
@@ -7,25 +7,37 @@ namespace CinemaWeb.Layouts;
|
||||
|
||||
public abstract class BasicLayout
|
||||
{
|
||||
private static DateTime _nextLoginCheck;
|
||||
|
||||
protected abstract string Title { get; }
|
||||
|
||||
protected abstract Task RenderContentAsync(HttpRequest req, TextWriter w);
|
||||
protected abstract Task RenderContentAsync(HttpRequest req, TextWriter w, CancellationToken cancel);
|
||||
|
||||
public async Task<IResult> ExecuteAsync(HttpRequest req)
|
||||
{
|
||||
int statusCode = 200;
|
||||
CancellationToken cancel = req.HttpContext.RequestAborted;
|
||||
StringBuilder content = new StringBuilder();
|
||||
TextWriter w = new StringWriter(content);
|
||||
try
|
||||
{
|
||||
RenderHeader(Title, w);
|
||||
await RenderContentAsync(req, w);
|
||||
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);
|
||||
}
|
||||
|
||||
string? userName = Program._webshare != null ? Program._webshare.UserName : null;
|
||||
RenderHeader(Title, userName, w);
|
||||
await RenderContentAsync(req, w, cancel);
|
||||
RenderFooter(w);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
content.Clear();
|
||||
RenderHeader(Title + " - Error " + e.GetType().Name, w);
|
||||
RenderHeader(Title + " - Error " + e.GetType().Name, null, w);
|
||||
w.WriteLine("<h1>Error " + e.GetType().Name + "</h1><div><pre>" + HttpUtility.HtmlEncode(e.Message) + "</pre></div>");
|
||||
RenderFooter(w);
|
||||
statusCode = 500;
|
||||
@@ -34,7 +46,7 @@ public abstract class BasicLayout
|
||||
return Results.Content(content.ToString(), "text/html; charset=utf-8", null, statusCode);
|
||||
}
|
||||
|
||||
private static void RenderHeader(string title, 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\">");
|
||||
@@ -55,6 +67,8 @@ public abstract class BasicLayout
|
||||
w.WriteLine(".media-episodes .media-plot { margin: 1em; overflow-y: hidden; color: #999;}");
|
||||
w.WriteLine("</style>");
|
||||
w.WriteLine("</head><body>");
|
||||
w.WriteLine("<div style=\"float:right\">Uživatel: [<a href=\"/login\">" + (userName != null ? userName : "<em>přihlásit</em>") + "</a>]</div>");
|
||||
w.WriteLine("<div style=\"font-weight:bold\">Menu: <a href=\"/\">Hledání</a></div>");
|
||||
}
|
||||
|
||||
private static void RenderFooter(TextWriter w) {
|
||||
|
||||
@@ -18,11 +18,11 @@ public class EpisodesPage : BasicLayout
|
||||
|
||||
protected override string Title => _title;
|
||||
|
||||
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w)
|
||||
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w, CancellationToken cancel)
|
||||
{
|
||||
w.WriteLine("<h1>" + HttpUtility.HtmlEncode(_title) + "</h1>");
|
||||
|
||||
FilterResponse? res = await Metadata.ChildrenAsync(_parentId, sort: FilterSortBy.Episode, cancel: req.HttpContext.RequestAborted);
|
||||
FilterResponse? res = await Metadata.ChildrenAsync(_parentId, sort: FilterSortBy.Episode, cancel: cancel);
|
||||
|
||||
if (res == null || res.hits == null || res.hits.hits == null || res.hits.hits.Count == 0)
|
||||
{
|
||||
@@ -52,7 +52,7 @@ public class EpisodesPage : BasicLayout
|
||||
|
||||
w.WriteLine("<h2>Sezóna " + label.title + "</h2>");
|
||||
|
||||
FilterResponse? resEp = await Metadata.ChildrenAsync(i._id, sort: FilterSortBy.Episode, cancel: req.HttpContext.RequestAborted);
|
||||
FilterResponse? resEp = await Metadata.ChildrenAsync(i._id, sort: FilterSortBy.Episode, cancel: cancel);
|
||||
if (resEp == null || resEp.hits == null || resEp.hits.hits == null || resEp.hits.hits.Count == 0)
|
||||
{
|
||||
w.WriteLine("<em>Nic nenalezeno</em>");
|
||||
|
||||
@@ -25,14 +25,14 @@ public class LinkPage : BasicLayout
|
||||
|
||||
protected override string Title => _title;
|
||||
|
||||
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w)
|
||||
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w, CancellationToken cancel)
|
||||
{
|
||||
w.WriteLine("<h1>" + HttpUtility.HtmlEncode(_title) + "</h1>");
|
||||
|
||||
Uri link;
|
||||
switch (_provider)
|
||||
{
|
||||
case "webshare": link = await LinkGenerator.GenerateDownloadLinkAsync(_ident, _name, req.HttpContext.RequestAborted); break;
|
||||
case "webshare": link = await LinkGenerator.GenerateDownloadLinkAsync(_ident, _name, Program._webshare, cancel); break;
|
||||
default:
|
||||
w.WriteLine("<em>Provider '" + HttpUtility.HtmlEncode(_provider) + "' není aktuálně podporován.</em>");
|
||||
return;
|
||||
@@ -45,7 +45,7 @@ public class LinkPage : BasicLayout
|
||||
if (subs.Count != 0) {
|
||||
w.WriteLine("<h2>Externí titulky</h2>");
|
||||
foreach (string i in subs) {
|
||||
(string lang, Uri uri) = await ProcessSubtitleLinkAsync(i, req.HttpContext.RequestAborted);
|
||||
(string lang, Uri uri) = await ProcessSubtitleLinkAsync(i, cancel);
|
||||
w.WriteLine("<div><a href=\"" + uri.ToString() + "\">" + HttpUtility.HtmlEncode(lang) + "</a></div>");
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public class LinkPage : BasicLayout
|
||||
Uri result;
|
||||
switch (provider) {
|
||||
case "webshare":
|
||||
result = await LinkGenerator.GenerateDownloadLinkAsync(rawId, "", cancel);
|
||||
result = await LinkGenerator.GenerateDownloadLinkAsync(rawId, "", Program._webshare, cancel);
|
||||
break;
|
||||
case "http":
|
||||
result = new Uri(rawId);
|
||||
|
||||
75
CinemaWeb/Pages/LoginPage.cs
Normal file
75
CinemaWeb/Pages/LoginPage.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using CinemaWeb.Layouts;
|
||||
using CinemaLib.Webshare;
|
||||
|
||||
namespace CinemaWeb.Pages;
|
||||
|
||||
public class LoginPage : BasicLayout
|
||||
{
|
||||
private const string ParamUserName = "user";
|
||||
private const string ParamPassword = "pw";
|
||||
private const string ParamToken = "token";
|
||||
private const string ParamLogout = "logout";
|
||||
|
||||
protected override string Title => "Přihlášení uživatele";
|
||||
|
||||
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w, CancellationToken cancel)
|
||||
{
|
||||
bool justLoggedIn = false;
|
||||
if (req.Method == "POST")
|
||||
{
|
||||
//
|
||||
// Execute an action
|
||||
//
|
||||
StringValues usernameS;
|
||||
string? username;
|
||||
StringValues passwordS;
|
||||
string? password;
|
||||
StringValues tokenS;
|
||||
if ((usernameS = req.Form[ParamUserName]).Count == 1 && (username = usernameS[0]) != null
|
||||
&& (passwordS = req.Form[ParamPassword]).Count == 1 && (password = passwordS[0]) != null
|
||||
&& (tokenS = req.Form[ParamToken]).Count == 1)
|
||||
{
|
||||
// Log in
|
||||
justLoggedIn = true;
|
||||
Program._webshare = new Session(username, password, tokenS[0]);
|
||||
|
||||
}
|
||||
else if (req.Form[ParamLogout].Count != 0)
|
||||
{
|
||||
// Log out
|
||||
Program._webshare = null;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Show state
|
||||
//
|
||||
string? token;
|
||||
if (Program._webshare != null && (token = await Program._webshare.GetTokenAsync(cancel)) != null)
|
||||
{
|
||||
// Logged-in user
|
||||
w.WriteLine("<h1>Přihlášený uživatel</h1>");
|
||||
w.WriteLine("<form method=\"POST\" action=\"/login\">");
|
||||
w.WriteLine("<div>Uživatel: <b>" + HttpUtility.HtmlEncode(Program._webshare.UserName) + "</b></div>");
|
||||
w.WriteLine("<div>Token: <b>" + HttpUtility.HtmlEncode(token) + "</b></div>");
|
||||
w.WriteLine("<input type=\"hidden\" name=\"" + ParamLogout + "\">");
|
||||
w.WriteLine("<input type=\"submit\" value=\"Odhlásit\">");
|
||||
w.WriteLine("</form>");
|
||||
}
|
||||
else
|
||||
{
|
||||
w.WriteLine("<h1>Přihlášení uživatele</h1>");
|
||||
w.WriteLine("<form method=\"POST\" action=\"/login\">");
|
||||
w.WriteLine("<div>Uživatel: <input type=\"text\" name=\"" + ParamUserName + "\" style=\"width:15em;margin:1em\"></div>");
|
||||
w.WriteLine("<div>Heslo: <input type=\"password\" name=\"" + ParamPassword + "\" style=\"width:15em;margin:1em\"></div>");
|
||||
w.WriteLine("<div>Token: <input type=\"text\" name=\"" + ParamToken + "\" style=\"width:15em;margin:1em\"> (nepovinné)</div>");
|
||||
w.WriteLine("<input type=\"submit\" value=\"Přihlásit\">");
|
||||
if (justLoggedIn)
|
||||
w.WriteLine("<div style=\"color:red;font-weight:bold\">Přihlašovací údaje nejsou platné</div>");
|
||||
w.WriteLine("</form>");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,13 @@ public class MediaPage : BasicLayout
|
||||
|
||||
protected override string Title => _title;
|
||||
|
||||
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w)
|
||||
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w, CancellationToken cancel)
|
||||
{
|
||||
w.WriteLine("<h1>" + HttpUtility.HtmlEncode(_title) + "</h1>");
|
||||
w.WriteLine("<div>TODO</div>");
|
||||
w.WriteLine("<h2>Zvolte kvalitu</h2>");
|
||||
|
||||
CinemaLib.API.Stream[]? res = await Metadata.StreamsAsync(_id, cancel: req.HttpContext.RequestAborted);
|
||||
CinemaLib.API.Stream[]? res = await Metadata.StreamsAsync(_id, cancel: cancel);
|
||||
|
||||
if (res == null || res.Length == 0)
|
||||
{
|
||||
|
||||
@@ -16,15 +16,15 @@ public class SearchPage : BasicLayout
|
||||
|
||||
protected override string Title => "Hledat filmy a seriály";
|
||||
|
||||
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w)
|
||||
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w, CancellationToken cancel)
|
||||
{
|
||||
string? expr;
|
||||
if ((expr = req.Query["expr"]) == null || expr.Trim().Length == 0)
|
||||
if ((expr = req.Query[ParamExpression]) == null || expr.Trim().Length == 0)
|
||||
{
|
||||
// Show search
|
||||
w.WriteLine("<h1>Hledat filmy a seriály</h1>");
|
||||
w.WriteLine("<form method=\"GET\" action=\"/\">");
|
||||
w.WriteLine("<input type=\"text\" name=\"" + ParamExpression + "\" style=\"width:100%;margin:1em\">");
|
||||
w.WriteLine("<input type=\"text\" name=\"" + ParamExpression + "\" style=\"width:100%;margin:1em 0em 1em 0em\">");
|
||||
w.WriteLine("<input type=\"submit\" value=\"Hledat\">");
|
||||
w.WriteLine("</form>");
|
||||
|
||||
@@ -34,7 +34,7 @@ public class SearchPage : BasicLayout
|
||||
// Execute search
|
||||
string? pageS = req.Query[ParamPageIdx];
|
||||
int page = pageS != null && int.TryParse(pageS, out int value) ? value : 0;
|
||||
FilterResponse? res = await Metadata.SearchAsync(expr, offset: page * PageSize, limit: PageSize, cancel: req.HttpContext.RequestAborted);
|
||||
FilterResponse? res = await Metadata.SearchAsync(expr, offset: page * PageSize, limit: PageSize, cancel: cancel);
|
||||
|
||||
w.WriteLine("<h1>Hledání: " + HttpUtility.HtmlEncode(expr) + "</h1>");
|
||||
if (res == null || res.hits == null || res.hits.hits == null || res.hits.hits.Count == 0)
|
||||
|
||||
@@ -19,12 +19,12 @@ public class StreamsPage : BasicLayout
|
||||
|
||||
protected override string Title => _title;
|
||||
|
||||
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w)
|
||||
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w, CancellationToken cancel)
|
||||
{
|
||||
w.WriteLine("<h1>" + HttpUtility.HtmlEncode(_title) + "</h1>");
|
||||
w.WriteLine("<div><b>Zvolte kvalitu</b></div>");
|
||||
|
||||
CinemaLib.API.Stream[]? res = await Metadata.StreamsAsync(_id, cancel: req.HttpContext.RequestAborted);
|
||||
CinemaLib.API.Stream[]? res = await Metadata.StreamsAsync(_id, cancel: cancel);
|
||||
|
||||
if (res == null || res.Length == 0)
|
||||
{
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
using System.Web;
|
||||
using CinemaLib.Webshare;
|
||||
using CinemaWeb.Pages;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.Build();
|
||||
class Program
|
||||
{
|
||||
internal static Session? _webshare;
|
||||
|
||||
app.MapGet("/", new SearchPage().ExecuteAsync);
|
||||
app.MapGet("/episodes/{id}/{title}", (string id, string title, HttpRequest req) => new EpisodesPage(id, HttpUtility.UrlDecode(title)).ExecuteAsync(req));
|
||||
app.MapGet("/streams/{id}/{title}", (string id, string title, HttpRequest req) => new StreamsPage(id, HttpUtility.UrlDecode(title)).ExecuteAsync(req));
|
||||
app.MapGet("/media/{id}/{title}", (string id, string title, HttpRequest req) => new MediaPage(id, HttpUtility.UrlDecode(title)).ExecuteAsync(req));
|
||||
app.MapGet("/link/{provider}/{ident}/{name}/{title}", (string provider, string ident, string name, string title, HttpRequest req) => new LinkPage(provider, HttpUtility.UrlDecode(ident), HttpUtility.UrlDecode(name), HttpUtility.UrlDecode(title)).ExecuteAsync(req));
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.Build();
|
||||
|
||||
app.Run();
|
||||
app.MapGet("/", new SearchPage().ExecuteAsync);
|
||||
app.MapGet("/login", new LoginPage().ExecuteAsync);
|
||||
app.MapPost("/login", new LoginPage().ExecuteAsync);
|
||||
app.MapGet("/episodes/{id}/{title}", (string id, string title, HttpRequest req) => new EpisodesPage(id, HttpUtility.UrlDecode(title)).ExecuteAsync(req));
|
||||
app.MapGet("/streams/{id}/{title}", (string id, string title, HttpRequest req) => new StreamsPage(id, HttpUtility.UrlDecode(title)).ExecuteAsync(req));
|
||||
app.MapGet("/media/{id}/{title}", (string id, string title, HttpRequest req) => new MediaPage(id, HttpUtility.UrlDecode(title)).ExecuteAsync(req));
|
||||
app.MapGet("/link/{provider}/{ident}/{name}/{title}", (string provider, string ident, string name, string title, HttpRequest req) => new LinkPage(provider, HttpUtility.UrlDecode(ident), HttpUtility.UrlDecode(name), HttpUtility.UrlDecode(title)).ExecuteAsync(req));
|
||||
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user