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 StreamCinemaLib.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/"); /// /// Searches for media using an expression. /// /// Expression like part of title. Can be empty string to list all media. /// Result ordering direction. /// Result ordering column. /// Allowed result type. /// Item offset. /// Maximum returned items count. /// Asynchronous cancellation. /// Response. public async static Task 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(uri.Uri, CreateFilterJsonOptions(), cancel); if (result != null) result = FixFilterResponse(result); return result; } /// /// Search for child media. /// /// Identifier of the parent media. /// Result ordering column. /// Asynchronous cancellation. /// Response. public async static Task 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(uri.Uri, CreateFilterJsonOptions(), cancel); if (result != null) result = FixFilterResponse(result); return result; } /// /// Gets available streams for the given media. /// /// Media identifier. /// Asynchronous cancellation. /// Available streams for download. public static Task 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(uri.Uri, CreateFilterJsonOptions(), cancel); } /// /// Tries to get a thumbnail image from a well-known image sources to speed-up loading. /// /// Image url with probably full-sized image. /// Requested image width that may however get rounded or completely ignored. /// Requested image height that may however get rounded or completely ignored. /// On success the thumbnail address. /// True if thumbnail url got calculated, false otherwise. 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 { 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(); } } }