All checks were successful
continuous-integration/drone/push Build is passing
264 lines
9.4 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|