Funguje stahovani serii/epizod

This commit is contained in:
2024-11-10 02:55:32 +01:00
parent bfb76389cf
commit c2b2898452
15 changed files with 484 additions and 37 deletions

View File

@@ -5,6 +5,6 @@ namespace StreamCinemaLib.API;
public class FilterHits
{
public FilterTotalHits? total {get;set;}
public double max_score {get;set;}
public double? max_score {get;set;}
public List<MediaInfo>? hits {get; set;}
}

View File

@@ -5,8 +5,8 @@ namespace StreamCinemaLib.API;
public class MediaInfo
{
public string? _index { get; set; }
public string? _id { get; set; }
public double _score { get; set; }
public string _id { get; set; }
public double? _score { get; set; }
//"_ignored": [
// "i18n_info_labels.plot.keyword"
//],

View File

@@ -1,4 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -14,21 +16,111 @@ public class Metadata
private static readonly Uri ApiRoot = new Uri("https://plugin.sc2.zone/api/");
private static readonly Uri ApiFilter = new Uri(ApiRoot, "media/filter/v2/");
public static Task<FilterResponse?> SearchAsync(string expression, ItemOrder order = ItemOrder.Descending, FilterSortBy sort = FilterSortBy.Score, ItemType type = ItemType.All, int limit = 0, CancellationToken cancel = default)
/// <summary>
/// Searches for media using an expression.
/// </summary>
/// <param name="expression">Expression like part of title.</param>
/// <param name="order">Result ordering direction.</param>
/// <param name="sort">Result ordering column.</param>
/// <param name="type">Allowed result type.</param>
/// <param name="offset">Item offset.</param>
/// <param name="limit">Maximum returned items count.</param>
/// <param name="cancel">Asynchronous cancellation.</param>
/// <returns>Response.</returns>
public static Task<FilterResponse?> SearchAsync(string expression, ItemOrder order = ItemOrder.Descending, FilterSortBy sort = FilterSortBy.Score, ItemType type = ItemType.All, int offset = 0, int limit = 0, CancellationToken cancel = default)
{
if (expression == null)
throw new ArgumentNullException();
if (limit < 0)
if (limit < 0 || offset < 0)
throw new ArgumentOutOfRangeException();
if (limit == 0 || limit > MaxPageLimit)
limit = MaxPageLimit;
UriBuilder uri = new UriBuilder(new Uri(ApiFilter, "search"));
uri.Query = $"?access_token={AccessToken}&value={Uri.EscapeDataString(expression)}&order={ToString(order)}&sort={ToString(sort)}&type={ToString(type)}&size={limit.ToString()}";
uri.Query = $"?access_token={AccessToken}&value={Uri.EscapeDataString(expression)}&order={ToString(order)}&sort={ToString(sort)}&type={ToString(type)}&from={offset.ToString()}&size={limit.ToString()}";
JsonSerializerOptions options = new JsonSerializerOptions();
options.Converters.Add(new CustomDateTimeConverter());
return Program._http.GetFromJsonAsync<FilterResponse>(uri.Uri, options, cancel);
return Program._http.GetFromJsonAsync<FilterResponse>(uri.Uri, CreateFilterJsonOptions(), cancel);
}
/// <summary>
/// Search for child media.
/// </summary>
/// <param name="parentId">Identifier of the parent media.</param>
/// <param name="sort">Result ordering column.</param>
/// <param name="cancel">Asynchronous cancellation.</param>
/// <returns>Response.</returns>
public static Task<FilterResponse?> ChildrenAsync(string parentId, FilterSortBy sort = FilterSortBy.Episode, CancellationToken cancel = default)
{
if (parentId == null)
throw new ArgumentNullException();
UriBuilder uri = new UriBuilder(new Uri(ApiFilter, "parent"));
uri.Query = $"?access_token={AccessToken}&value={parentId}&sort={ToString(sort)}&size={MaxPageLimit.ToString()}";
return Program._http.GetFromJsonAsync<FilterResponse>(uri.Uri, CreateFilterJsonOptions(), cancel);
}
/// <summary>
/// Gets available streams for the given media.
/// </summary>
/// <param name="id">Media identifier.</param>
/// <param name="cancel">Asynchronous cancellation.</param>
/// <returns>Available streams for download.</returns>
public static Task<Stream[]?> StreamsAsync(string id, CancellationToken cancel = default)
{
if (id == null)
throw new ArgumentNullException();
UriBuilder uri = new UriBuilder(new Uri(ApiRoot, "media/" + id + "/streams"));
uri.Query = $"?access_token={AccessToken}";
return Program._http.GetFromJsonAsync<Stream[]>(uri.Uri, CreateFilterJsonOptions(), cancel);
}
/// <summary>
/// Tries to get a thumbnail image from a well-known image sources to speed-up loading.
/// </summary>
/// <param name="imageUri">Image url with probably full-sized image.</param>
/// <param name="suggestWidth">Requested image width that may however get rounded or completely ignored.</param>
/// <param name="suggestHeight">Requested image height that may however get rounded or completely ignored.</param>
/// <param name="thumbUri">On success the thumbnail address.</param>
/// <returns>True if thumbnail url got calculated, false otherwise.</returns>
public static bool TryGetThumbnail(Uri imageUri, int suggestWidth, int suggestHeight, [NotNullWhen(true)] out Uri? thumbUri) {
if (imageUri == null)
throw new ArgumentNullException();
switch (imageUri.Host) {
case "image.tmdb.org":
const string TmdbOrigPrefix = "/t/p/original/";
if (imageUri.AbsolutePath.StartsWith(TmdbOrigPrefix)) {
UriBuilder ub = new UriBuilder(imageUri);
ub.Path = "/t/p/w300_and_h450_bestv2/" + ub.Path.Substring(TmdbOrigPrefix.Length);
thumbUri = ub.Uri;
return true;
}
break;
case "img.csfd.cz":
if (imageUri.AbsolutePath.StartsWith("/files/images/film/posters/")) {
// 140px, 280px, 420px
int w;
if (suggestWidth < 160)
w = 140;
else if (suggestWidth < 320)
w = 280;
else
w = 420;
UriBuilder ub = new UriBuilder(imageUri);
ub.Host = "image.pmgstatic.com";
ub.Path = "/cache/resized/w" + w.ToString(CultureInfo.InvariantCulture) + ub.Path;
thumbUri = ub.Uri;
return true;
}
break;
}
thumbUri = null;
return false;
}
private static string ToString(ItemOrder value)
@@ -79,6 +171,12 @@ public class Metadata
}
}
private static JsonSerializerOptions CreateFilterJsonOptions() {
JsonSerializerOptions options = new JsonSerializerOptions();
options.Converters.Add(new CustomDateTimeConverter());
return options;
}
sealed class CustomDateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)

View File

@@ -0,0 +1,17 @@
using System;
namespace StreamCinemaLib.API;
public class Stream
{
public string _id {get; set;}
public string name {get;set;}
public string media {get;set;}
public string provider {get;set;}
public DateTime? date_added {get;set;}
public string ident {get;set;}
public long? size {get;set;}
public List<StreamAudio>? audio {get;set;}
public List<StreamVideo>? video {get;set;}
public List<StreamSubtitle>? subtitles {get;set;}
}

View File

@@ -0,0 +1,10 @@
using System;
namespace StreamCinemaLib.API;
public class StreamAudio
{
public string language { get; set; } // two letter ISO code
public string codec { get; set; } // ie. AAC
public int channels { get; set; }
}

View File

@@ -0,0 +1,9 @@
using System;
namespace StreamCinemaLib.API;
public class StreamSubtitle
{
public string language { get; set; } // two letter ISO code
public bool forced { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace StreamCinemaLib.API;
public class StreamVideo
{
public int width { get; set; } // horizontal resolution in pixels
public int height { get; set; } // vertical resolution in pixels
public string codec { get; set; } // ie. AVC
public double? aspect { get; set; } // aspect ratio, ie. 1.778
[JsonPropertyName("3d")]
public bool is3d { get; set; }
public double? duration { get; set; } // in seconds
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using System.Text;
using System.Xml;
@@ -9,7 +10,7 @@ public class LinkGenerator
{
private static readonly BaseEncoding UnixMD5 = new BaseEncoding("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", false);
private static readonly Uri WebshareApiUri = new Uri("https://webshare.cz/api/");
/// <summary>
/// Generates a download link for the given Webshare download.
/// </summary>
@@ -22,24 +23,38 @@ public class LinkGenerator
// 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>
Uri saltUri = new Uri(WebshareApiUri, "file_password_salt/");
HttpResponseMessage saltRes = await Program._http.PostAsync(saltUri, new FormUrlEncodedContent(new [] { new KeyValuePair<string, string>("ident", fileId) }), cancel);
HttpResponseMessage saltRes = await Program._http.PostAsync(saltUri, new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("ident", fileId) }), cancel);
if (!saltRes.IsSuccessStatusCode)
throw new IOException("Failed to get password salt from Webshare API.");
XmlReader saltR = XmlReader.Create(saltRes.Content.ReadAsStream());
saltR.ReadStartElement("response");
ThrowIfStatusNotOK(saltR);
string salt = saltR.ReadElementContentAsString("salt", "");
saltR.ReadElementContentAsString("app_version", "");
saltR.ReadEndElement(); // response
Exception? e = GetExceptionIfStatusNotOK(saltR, out string? code);
string password;
if (e != null)
{
if (code == "FILE_PASSWORD_SALT_FATAL_2")
{
// No password is set
password = "";
}
else
throw e;
}
else
{
// Calculate the download password
string salt = saltR.ReadElementContentAsString("salt", "");
saltR.ReadElementContentAsString("app_version", "");
saltR.ReadEndElement(); // response
// Calculate the download password
string password = CalculatePassword(fileId, fileName, salt);
password = CalculatePassword(fileId, fileName, salt);
}
// Obtain the download link
// Example response: <?xml version="1.0" encoding="UTF-8"?><response><status>OK</status><link>https://free.19.dl.wsfiles.cz/9209/...</link><app_version>30</app_version></response>
Uri linkUri = new Uri(WebshareApiUri, "file_link/");
HttpResponseMessage linkRes = await Program._http.PostAsync(linkUri, new FormUrlEncodedContent(new [] {
HttpResponseMessage linkRes = await Program._http.PostAsync(linkUri, new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("ident", fileId),
new KeyValuePair<string, string>("download_type", "video_stream"),
new KeyValuePair<string, string>("device_uuid", Guid.NewGuid().ToString("X")),
@@ -61,14 +76,24 @@ public class LinkGenerator
return new Uri(link);
}
private static void ThrowIfStatusNotOK(XmlReader r) {
private static Exception? GetExceptionIfStatusNotOK(XmlReader r, out string? code)
{
string status = r.ReadElementContentAsString("status", "");
if (status == "OK")
return;
if (status == "OK") {
code = null;
return null;
}
string code = r.ReadElementContentAsString("code", "");
code = r.ReadElementContentAsString("code", "");
string message = r.ReadElementContentAsString("message", "");
throw new IOException(code + ": " + message);
return new IOException(code + ": " + message);
}
private static void ThrowIfStatusNotOK(XmlReader r)
{
Exception? e = GetExceptionIfStatusNotOK(r, out _);
if (e != null)
throw e;
}
/// <summary>

View File

@@ -40,7 +40,17 @@ public abstract class BasicLayout
w.WriteLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
w.WriteLine("<title>" + HttpUtility.HtmlEncode(title) + "</title>");
w.WriteLine("<style>");
// style go here
w.WriteLine(".search-results { display: flex; flex-wrap: wrap; }");
w.WriteLine(".media-episodes { display: flex; flex-direction: column; gap: 1em; }");
w.WriteLine(".search-results > .media-info { width: 25%; }");
w.WriteLine(".media-episodes > .media-info { height: 12vh; display: flex; align-items: center; }");
w.WriteLine(".search-results .media-art { margin: 1em 1em 0em 1em }");
w.WriteLine(".media-episodes .media-art { margin: 1em; width: 12vh; height: 12vh; }");
w.WriteLine(".search-results .media-art img { width: 100%; }");
w.WriteLine(".media-episodes .media-art img { height: 100%; }");
w.WriteLine(".media-title { margin: 0em 1em 0em 1em; font-weight: bold; font-size: larger;}");
w.WriteLine(".search-results .media-plot { margin: 0em 1em 1em 1em; height: 4.5em; overflow-y: hidden; color: #999;}");
w.WriteLine(".media-episodes .media-plot { margin: 1em; overflow-y: hidden; color: #999;}");
w.WriteLine("</style>");
w.WriteLine("</head><body>");
}

View File

@@ -0,0 +1,99 @@
using System;
using System.Web;
using StreamCinemaLib.API;
using StreamCinemaWeb.Layouts;
namespace StreamCinemaWeb.Pages;
public class EpisodesPage : BasicLayout
{
private readonly string _parentId;
private readonly string _title;
public EpisodesPage(string parentId, string title)
{
this._parentId = parentId;
this._title = title;
}
protected override string Title => _title;
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w)
{
w.WriteLine("<h1>" + HttpUtility.HtmlEncode(_title) + "</h1>");
FilterResponse? res = await Metadata.ChildrenAsync(_parentId, sort: FilterSortBy.Episode, cancel: req.HttpContext.RequestAborted);
if (res == null || res.hits == null || res.hits.hits == null || res.hits.hits.Count == 0)
{
w.WriteLine("<em>Nic nenalezeno</em>");
}
else
{
if (res.hits.hits[0]._source?.children_count == 0)
{
// Just episodes
w.WriteLine("<h2>Epizody</h2>");
RenderEpisodes(w, res.hits.hits, _title);
}
else
{
// Seasons and episodes
foreach (MediaInfo i in res.hits.hits)
{
if (i._source == null)
continue;
InfoLabelI18n? label = i._source.i18n_info_labels?.Where(x => x.lang == "cs").FirstOrDefault();
if (label == null)
label = i._source.i18n_info_labels?.FirstOrDefault();
if (label == null)
continue;
w.WriteLine("<h2>Sezóna " + label.title + "</h2>");
FilterResponse? resEp = await Metadata.ChildrenAsync(i._id, sort: FilterSortBy.Episode, cancel: req.HttpContext.RequestAborted);
if (resEp == null || resEp.hits == null || resEp.hits.hits == null || resEp.hits.hits.Count == 0)
{
w.WriteLine("<em>Nic nenalezeno</em>");
}
else
{
RenderEpisodes(w, resEp.hits.hits, _title + " - " + label.title);
}
}
}
}
}
private void RenderEpisodes(TextWriter w, IEnumerable<MediaInfo> episodes, string parentTitles)
{
w.WriteLine("<div class=\"media-episodes\">");
foreach (MediaInfo i in episodes)
{
if (i._source == null)
continue;
InfoLabelI18n? label = i._source.i18n_info_labels?.Where(x => x.lang == "cs").FirstOrDefault();
if (label == null)
label = i._source.i18n_info_labels?.FirstOrDefault();
if (label == null)
continue;
string? artUriS = label?.art?.poster ?? label?.art?.fanart;
Uri? artUri;
if (artUriS == null)
artUri = null;
else if (Metadata.TryGetThumbnail(artUri = new Uri(artUriS), 128, 128, out Uri? artThumbUri))
artUri = artThumbUri;
string streamsUri = "/streams/" + i._id + "/" + HttpUtility.UrlEncode(parentTitles + " - " + label?.title ?? "neznamy");
w.WriteLine("<div class=\"media-info\">");
w.WriteLine("<div class=\"media-art\"><a href=\"" + streamsUri + "\"><img src=\"" + artUri?.ToString() + "\"></a></div>");
w.WriteLine("<div class=\"media-title\"><a href=\"" + streamsUri + "\">" + HttpUtility.HtmlEncode(label?.title) + "</a></div>");
w.WriteLine("<div class=\"media-plot\">" + HttpUtility.HtmlEncode(label?.plot) + "</div>");
w.WriteLine("</div>"); // media-info
}
w.WriteLine("</div>");
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Web;
using StreamCinema.Webshare;
using StreamCinemaLib.API;
using StreamCinemaWeb.Layouts;
using LinkGenerator = StreamCinema.Webshare.LinkGenerator;
namespace StreamCinemaWeb.Pages;
public class LinkPage : BasicLayout
{
private readonly string _provider;
private readonly string _ident;
private readonly string _name;
private readonly string _title;
public LinkPage(string provider, string ident, string name, string title)
{
this._provider = provider;
this._ident = ident;
this._name = name;
this._title = title;
}
protected override string Title => _title;
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w)
{
w.WriteLine("<h1>" + HttpUtility.HtmlEncode(_title) + "</h1>");
Uri link;
switch (_provider)
{
case "webshare": link = await LinkGenerator.GenerateDownloadLinkAsync(_ident, _name, req.HttpContext.RequestAborted); break;
default:
w.WriteLine("<em>Provider '" + HttpUtility.HtmlEncode(_provider) + "' není aktuálně podporován.</em>");
return;
}
w.WriteLine("<div><a href=\"" + link.ToString() + "\">" + HttpUtility.HtmlEncode(link.ToString()) + "</a></div>");
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Web;
using StreamCinemaWeb.Layouts;
namespace StreamCinemaWeb.Pages;
public class MediaPage : BasicLayout
{
private readonly string _id;
private readonly string _title;
public MediaPage(string id, string title)
{
this._id = id;
this._title = title;
}
protected override string Title => _title;
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w)
{
w.WriteLine("<h1>" + HttpUtility.HtmlEncode(_title) + "</h1>");
}
}

View File

@@ -1,24 +1,30 @@
using System;
using StreamCinemaWeb.Layouts;
using StreamCinemaLib.API;
using System.Globalization;
using System.Web;
using StreamCinemaWeb.Layouts;
using StreamCinemaLib.API;
using System.Runtime.InteropServices;
namespace StreamCinemaWeb.Pages;
public class SearchPage : BasicLayout
{
private const int PageSize = 30;
private const string ParamExpression = "expr";
private const string ParamPageIdx = "page";
protected override string Title => "Hledat filmy a seriály";
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w)
{
string expr;
if (req.Method != "POST" || (expr = req.Form["expr"]) == null || expr.Trim().Length == 0)
string? expr;
if ((expr = req.Query["expr"]) == null || expr.Trim().Length == 0)
{
// Show search
w.WriteLine("<h1>Hledat filmy a seriály</h1>");
w.WriteLine("<form method=\"POST\" action=\"/\">");
w.WriteLine("<input type=\"text\" name=\"expr\" style=\"width:100%;margin:1em\">");
w.WriteLine("<form method=\"GET\" action=\"/\">");
w.WriteLine("<input type=\"text\" name=\"" + ParamExpression + "\" style=\"width:100%;margin:1em\">");
w.WriteLine("<input type=\"submit\" value=\"Hledat\">");
w.WriteLine("</form>");
@@ -26,14 +32,18 @@ public class SearchPage : BasicLayout
else
{
// Execute search
FilterResponse? res = await Metadata.SearchAsync(expr, cancel: req.HttpContext.RequestAborted);
w.WriteLine("<h1>Hledání:" + HttpUtility.HtmlEncode(expr) + "</h1>");
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);
w.WriteLine("<h1>Hledání: " + HttpUtility.HtmlEncode(expr) + "</h1>");
if (res == null || res.hits == null || res.hits.hits == null || res.hits.hits.Count == 0)
{
w.WriteLine("<em>Nic nenalezeno</em>");
}
else
{
w.WriteLine("<div class=\"search-results\">");
foreach (MediaInfo i in res.hits.hits)
{
if (i._source == null)
@@ -44,12 +54,23 @@ public class SearchPage : BasicLayout
if (label == null)
continue;
string? artUriS = label?.art?.poster ?? label?.art?.fanart;
Uri? artUri;
if (artUriS == null)
artUri = null;
else if (Metadata.TryGetThumbnail(artUri = new Uri(artUriS), 300, 300, out Uri? artThumbUri))
artUri = artThumbUri;
string detailUri = (i._source.children_count == 0 ? "media" : "episodes") + "/" + i._id + "/" + HttpUtility.UrlEncode(label?.title ?? "neznamy");
w.WriteLine("<div class=\"media-info\">");
w.WriteLine("<div class=\"media-art\"><img src=\"" + label?.art?.poster ?? label?.art?.fanart + "\"></div>");
w.WriteLine("<div class=\"media-title\">" + HttpUtility.HtmlEncode(label?.title) + "</div");
w.WriteLine("<div class=\"media-plot\">" + HttpUtility.HtmlEncode(label?.plot) + "</div");
w.WriteLine("<div class=\"media-art\"><a href=\"" + detailUri + "\"><img src=\"" + artUri?.ToString() + "\"></a></div>");
w.WriteLine("<div class=\"media-title\"><a href=\"" + detailUri + "\">" + HttpUtility.HtmlEncode(label?.title) + "</a></div>");
w.WriteLine("<div class=\"media-plot\">" + HttpUtility.HtmlEncode(label?.plot) + "</div>");
w.WriteLine("</div>"); // media-info
}
w.WriteLine("</div>");
w.WriteLine("<div class=\"btn\"><a href=\"?" + ParamExpression + "=" + HttpUtility.UrlEncode(expr) + "&" + ParamPageIdx + "=" + (page + 1).ToString() + "\">Další</a></div>");
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Web;
using StreamCinemaLib.API;
using StreamCinemaWeb.Layouts;
namespace StreamCinemaWeb.Pages;
public class StreamsPage : BasicLayout
{
private readonly string _id;
private readonly string _title;
public StreamsPage(string id, string title)
{
this._id = id;
this._title = title;
}
protected override string Title => _title;
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w)
{
w.WriteLine("<h1>" + HttpUtility.HtmlEncode(_title) + "</h1>");
w.WriteLine("<div><b>Zvolte kvalitu</b></div>");
StreamCinemaLib.API.Stream[]? res = await Metadata.StreamsAsync(_id, cancel: req.HttpContext.RequestAborted);
if (res == null || res.Length == 0)
{
w.WriteLine("<em>Nic nenalezeno</em>");
}
else
{
w.WriteLine("<div class=\"media-streams\">");
foreach (StreamCinemaLib.API.Stream i in res)
{
w.WriteLine("<div class=\"media-stream\"><a href=\"/link/" + i.provider + "/" + HttpUtility.UrlEncode(i.ident) + "/" + HttpUtility.UrlEncode(i.name) + "/" + HttpUtility.UrlEncode(_title) + "\">");
w.WriteLine($"<div class=\"stream-basic\">{Math.Round((double)(i.size ?? 0) / (1024 * 1024 * 1024), 2)} GB ({i.date_added})</div>");
if (i.video != null && i.video.Count != 0)
{
StreamVideo a = i.video.First();
string is3D = a.is3d ? " (3D)" : "";
w.WriteLine($"<div class=\"video-channel\">{a.width}x{a.height} {a.codec}{is3D} {Math.Round((a.duration ?? 0) / 60)} min</div>");
}
if (i.audio != null && i.audio.Count != 0)
{
w.WriteLine("<div class=\"audio-channels\">");
foreach (StreamAudio j in i.audio)
{
w.WriteLine($"<div class=\"audio-channel\">{j.language} {j.codec} {j.channels}</div>");
}
w.WriteLine("</div>");
}
if (i.subtitles != null && i.subtitles.Count != 0)
{
w.WriteLine("<div class=\"subtitle-channels\">");
foreach (StreamSubtitle j in i.subtitles)
{
w.WriteLine($"<div class=\"subtitle-channel\">{j.language}</div>");
}
w.WriteLine("</div>");
}
w.WriteLine("</a></div>");
}
w.WriteLine("</div>");
}
}
}

View File

@@ -1,9 +1,13 @@
using System.Web;
using StreamCinemaWeb.Pages;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", new SearchPage().ExecuteAsync);
app.MapPost("/", 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));
app.Run();