Files
stream-cinema/CinemaLib/API/Metadata.cs
Roman Vaníček 0bcb6d571f
All checks were successful
continuous-integration/drone/push Build is passing
Rename to Cinema
2024-11-30 00:50:10 +01:00

264 lines
9.4 KiB
C#

using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CinemaLib.API;
public class Metadata
{
private const string UserAgent = "User-Agent: XBMC/19 (Linux 6.1; http://www.xbmc.org)";
private const string AccessToken = "9ajdu4xyn1ig8nxsodr3";
private const int MaxPageLimit = 100; // thousand is too much for some search types
private static readonly Uri ApiRoot = new Uri("https://plugin.sc2.zone/api/");
private static readonly Uri ApiFilter = new Uri(ApiRoot, "media/filter/v2/");
/// <summary>
/// Searches for media using an expression.
/// </summary>
/// <param name="expression">Expression like part of title. Can be empty string to list all media.</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 async 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 || offset < 0)
throw new ArgumentOutOfRangeException();
if (limit == 0 || limit > MaxPageLimit)
limit = MaxPageLimit;
/*
bool noSearchTerms = expression.Trim().Length == 0;
UriBuilder uri = new UriBuilder(new Uri(ApiFilter, noSearchTerms ? "all" : "search"));
uri.Query = $"?access_token={AccessToken}&value={Uri.EscapeDataString(expression)}&order={ToString(order)}&sort={ToString(sort)}&type={ToString(type)}&from={offset.ToString()}&size={limit.ToString()}";
*/
bool noSearchTerms = expression.Trim().Length == 0;
string filterName;
string sortS = ToString(sort);
if (sort == FilterSortBy.Title) {
filterName = "startsWithSimple";
sortS = "";
} else if (noSearchTerms) {
filterName = "all";
} else {
filterName = "search";
}
UriBuilder uri = new UriBuilder(new Uri(ApiFilter, filterName));
string orderS = order == ItemOrder.Ascending ? "asc" : "desc";
uri.Query = $"?access_token={AccessToken}&value={Uri.EscapeDataString(expression)}&type={ToString(type)}&from={offset.ToString()}&size={limit.ToString()}"
+ (sortS.Length != 0 ? $"&order={ToString(order)}&sort={sortS}" : "");
FilterResponse? result = await Program._http.GetFromJsonAsync<FilterResponse>(uri.Uri, CreateFilterJsonOptions(), cancel);
if (result != null)
result = FixFilterResponse(result);
return result;
}
/// <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 async 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()}";
FilterResponse? result = await Program._http.GetFromJsonAsync<FilterResponse>(uri.Uri, CreateFilterJsonOptions(), cancel);
if (result != null)
result = FixFilterResponse(result);
return result;
}
/// <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 FilterResponse FixFilterResponse(FilterResponse res)
{
if (res == null)
throw new ArgumentNullException();
// Fix image URLs as they may miss the https scheme
if (res.hits != null && res.hits.hits != null)
foreach (MediaInfo i in res.hits.hits)
{
if (i._source == null)
continue;
if (i._source.cast != null)
foreach (Cast j in i._source.cast)
j.thumbnail = FixImageUrl(j.thumbnail);
if (i._source.i18n_info_labels != null)
foreach (InfoLabelI18n j in i._source.i18n_info_labels) {
if (j.art != null) {
j.art.poster = FixImageUrl(j.art.poster);
j.art.fanart = FixImageUrl(j.art.fanart);
}
}
}
return res;
}
private static string FixImageUrl(string url)
{
if (url != null && !url.StartsWith("http")) {
if (url.StartsWith("//"))
url = "https:" + url;
else
url = "https://" + url;
}
return url;
}
private static string ToString(ItemOrder value)
{
switch (value)
{
case ItemOrder.Ascending: return "asc";
case ItemOrder.Descending: return "desc";
default: throw new ArgumentOutOfRangeException();
}
}
private static string ToString(FilterSortBy value)
{
switch (value)
{
case FilterSortBy.Score: return "score";
case FilterSortBy.Year: return "year";
case FilterSortBy.Premiered: return "premiered";
case FilterSortBy.DateAdded: return "dateAdded";
case FilterSortBy.LastChildrenDateAdded: return "lastChildrenDateAdded";
case FilterSortBy.LastChildPremiered: return "lastChildPremiered";
case FilterSortBy.Title: return "title";
case FilterSortBy.PlayCount: return "playCount";
case FilterSortBy.LastSeen: return "lastSeen";
case FilterSortBy.Episode: return "episode";
case FilterSortBy.News: return "news";
case FilterSortBy.Popularity: return "popularity";
case FilterSortBy.Trending: return "trending";
case FilterSortBy.LangDateAdded: return "langDateAdded";
case FilterSortBy.Custom: return "custom";
default: throw new ArgumentOutOfRangeException();
}
}
private static string ToString(ItemType value)
{
switch (value)
{
case ItemType.TVShow: return "tvshow";
case ItemType.Concert: return "concert";
case ItemType.Anime: return "anime";
case ItemType.Movie: return "movie";
case ItemType.Season: return "season";
case ItemType.Episode: return "episode";
case ItemType.All: return "*";
default: throw new ArgumentOutOfRangeException();
}
}
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)
{
return DateTime.Parse(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
throw new NotSupportedException();
}
}
}