using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using Jackett.Common.Models; using Jackett.Common.Models.IndexerConfig; using Jackett.Common.Services.Interfaces; using Jackett.Common.Utils; using Jackett.Common.Utils.Clients; using Newtonsoft.Json.Linq; using NLog; namespace Jackett.Common.Indexers { [ExcludeFromCodeCoverage] public class YTS : BaseWebIndexer { public override string[] AlternativeSiteLinks { get; protected set; } = { "https://yts.mx/", "https://yts.unblockit.buzz/", "https://yts.unblockninja.com/" }; public override string[] LegacySiteLinks { get; protected set; } = { "https://yts.ag/", "https://yts.am/", "https://yts.lt/", "https://yts.unblockit.dev/", "https://yts.root.yt/", "https://yts.unblockit.ltd/", "https://yts.unblockit.link/" }; private string ApiEndpoint => SiteLink + "api/v2/list_movies.json"; public YTS(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, ICacheService cs) : base(id: "yts", name: "YTS", description: "YTS is a Public torrent site specialising in HD movies of small size", link: "https://yts.mx/", caps: new TorznabCapabilities { MovieSearchParams = new List { MovieSearchParam.Q, MovieSearchParam.ImdbId } }, configService: configService, client: wc, logger: l, p: ps, cacheService: cs, configData: new ConfigurationData()) { Encoding = Encoding.GetEncoding("windows-1252"); Language = "en-us"; Type = "public"; webclient.requestDelay = 2.5; // 0.5 requests per second (2 causes problems) // note: the API does not support searching with categories, so these are dummy ones for torznab compatibility // we map these newznab cats with the returned quality value in the releases routine. AddCategoryMapping(45, TorznabCatType.MoviesHD, "Movies/x264/720p"); AddCategoryMapping(44, TorznabCatType.MoviesHD, "Movies/x264/1080p"); AddCategoryMapping(46, TorznabCatType.MoviesUHD, "Movies/x264/2160p"); // added for #7010 AddCategoryMapping(47, TorznabCatType.Movies3D, "Movies/x264/3D"); } public override async Task ApplyConfiguration(JToken configJson) { LoadValuesFromJson(configJson); var releases = await PerformQuery(new TorznabQuery()); await ConfigureIfOK(string.Empty, releases.Count() > 0, () => throw new Exception("Could not find releases from this URL")); return IndexerConfigurationStatus.Completed; } protected override async Task> PerformQuery(TorznabQuery query) => await PerformQueryAsync(query); private async Task> PerformQueryAsync(TorznabQuery query) { var searchUrl = CreateSearchUrl(query); var response = await RequestWithCookiesAndRetryAsync(searchUrl); var releases = ParseWebResult(response); return releases; } private string CreateSearchUrl(TorznabQuery query) { var searchString = query.GetQueryString(); var queryCollection = new NameValueCollection { // without this the API sometimes returns nothing { "sort", "date_added" }, { "limit", "50" } }; if (query.ImdbID != null) { queryCollection.Add("query_term", query.ImdbID); } else if (!string.IsNullOrWhiteSpace(searchString)) { searchString = searchString.Replace("'", ""); // ignore ' (e.g. search for america's Next Top Model) queryCollection.Add("query_term", searchString); } var searchUrl = ApiEndpoint + "?" + queryCollection.GetQueryString(); return searchUrl; } private IEnumerable ParseWebResult(WebResult webResult) { var releases = new List(); var contentString = webResult.ContentString; try { // returned content might start with an html error message, remove it first var jsonStart = contentString.IndexOf('{'); var jsonContentStr = contentString.Remove(0, jsonStart); var jsonContent = JObject.Parse(jsonContentStr); var result = jsonContent.Value("status"); if (result != "ok") // query was not successful { return new List(); } var dataItems = jsonContent.Value("data"); var movieCount = dataItems.Value("movie_count"); if (movieCount < 1) // no results found in query { return new List(); } var movies = dataItems.Value("movies"); if (movies == null) { return new List(); } foreach (var movie in movies) { var torrents = movie.Value("torrents"); if (torrents == null) continue; foreach (var torrent in torrents) { var release = ParseJsonIntoReleaseInfo(movie, torrent); if (release == null) continue; releases.Add(release); } } } catch (Exception ex) { OnParseError(contentString, ex); } return releases; } private ReleaseInfo ParseJsonIntoReleaseInfo(JToken movie, JToken torrent) { //TODO change to initializer var release = new ReleaseInfo(); // append type: BRRip or WEBRip, resolves #3558 via #4577 var type = torrent.Value("type"); switch (type) { case "web": type = "WEBRip"; break; default: type = "BRRip"; break; } var quality = torrent.Value("quality"); var title = movie.Value("title").Replace(":", "").Replace(' ', '.'); var year = movie.Value("year"); release.Title = $"{title}.{year}.{quality}.{type}-YTS"; var imdb = movie.Value("imdb_code"); release.Imdb = ParseUtil.GetImdbID(imdb); release.InfoHash = torrent.Value("hash"); // magnet link is auto generated from infohash // ex: 2015-08-16 21:25:08 +0000 var dateStr = torrent.Value("date_uploaded"); var dateTime = DateTime.ParseExact(dateStr, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); release.PublishDate = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc).ToLocalTime(); release.Link = new Uri(torrent.Value("url")); release.Seeders = torrent.Value("seeds"); release.Peers = torrent.Value("peers") + release.Seeders; release.Size = torrent.Value("size_bytes"); release.DownloadVolumeFactor = 0; release.UploadVolumeFactor = 1; release.Details = new Uri(movie.Value("url")); release.Poster = new Uri(movie.Value("large_cover_image")); release.Guid = release.Link; // map the quality to a newznab category for torznab compatibility (for Radarr, etc) switch (quality) { case "720p": release.Category = MapTrackerCatToNewznab("45"); break; case "1080p": release.Category = MapTrackerCatToNewznab("44"); break; case "2160p": release.Category = MapTrackerCatToNewznab("46"); break; case "3D": release.Category = MapTrackerCatToNewznab("47"); break; default: release.Category = MapTrackerCatToNewznab("45"); break; } return release; } } }