diff --git a/src/NzbDrone.Core/Indexers/Definitions/Shizaproject.cs b/src/NzbDrone.Core/Indexers/Definitions/Shizaproject.cs new file mode 100644 index 000000000..3c329afbf --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Shizaproject.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; +using System.Text.RegularExpressions; +using FluentValidation; +using Newtonsoft.Json; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Definitions +{ + public class Shizaproject : TorrentIndexerBase + { + public override string Name => "ShizaProject"; + public override string[] IndexerUrls => new string[] { "https://shiza-project.com/" }; + public override string Description => "Shizaproject is russian anime voiceover group and eponymous anime tracker."; + public override string Language => "ru-ru"; + public override Encoding Encoding => Encoding.UTF8; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override IndexerPrivacy Privacy => IndexerPrivacy.Public; + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public Shizaproject(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) + : base(httpClient, eventAggregator, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new ShizaprojectRequestGenerator() { Settings = Settings, Capabilities = Capabilities }; + } + + public override IParseIndexerResponse GetParser() + { + return new ShizaprojectParser(Settings, Capabilities.Categories); + } + + private IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep + }, + MovieSearchParams = new List + { + MovieSearchParam.Q + } + }; + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "TV"); + caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "TV_SPECIAL"); + caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TVAnime, "ONA"); + caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.TVAnime, "OVA"); + caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Movies, "MOVIE"); + caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Movies, "SHORT_MOVIE"); + return caps; + } + } + + public class ShizaprojectRequestGenerator : IIndexerRequestGenerator + { + public ShizaprojectSettings Settings { get; set; } + public IndexerCapabilities Capabilities { get; set; } + + public ShizaprojectRequestGenerator() + { + } + + private IEnumerable GetPagedRequests(string term, int[] categories) + { + var variables = new + { + // Number of fetched releases (required parameter) TODO: consider adding pagination + first = 50, + + // Remove season and episode info from search term cause it breaks search + query = Regex.Replace(term, @"(?:[SsEe]?\d{1,4}){1,2}$", "").TrimEnd() + }; + + var query = @" + query fetchReleases($first: Int, $query: String) { + releases(first: $first, query: $query) { + edges { + node { + name + type + originalName + alternativeNames + publishedAt + slug + torrents { + downloaded + seeders + leechers + size + magnetUri + updatedAt + file { + url + } + videoQualities + } + } + } + } + }"; + + var queryCollection = new NameValueCollection + { + { "query", query.Replace('\n', ' ').Trim() }, + { "variables", Newtonsoft.Json.JsonConvert.SerializeObject(variables) } + }; + + var requestUrl = string.Format("{0}/graphql?", Settings.BaseUrl.TrimEnd('/')) + queryCollection.GetQueryString(); + + var request = new IndexerRequest(requestUrl, HttpAccept.Json); + yield return request; + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); + + return pageableRequests; + } + + // Shizaproject doesn't support music, but this function required by interface + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + // Shizaproject doesn't support books, but this function required by interface + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public Func> GetCookies { get; set; } + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class ShizaprojectParser : IParseIndexerResponse + { + private readonly ShizaprojectSettings _settings; + private readonly IndexerCapabilitiesCategories _categories; + + public ShizaprojectParser(ShizaprojectSettings settings, IndexerCapabilitiesCategories categories) + { + _settings = settings; + _categories = categories; + } + + private string composeTitle(ShizaprojectNode n, ShizaprojectTorrent tr) + { + var title = string.Format("{0} / {1}", n.Name, n.OriginalName); + foreach (var tl in n.AlternativeNames) + { + title += " / " + tl; + } + + title += " ["; + foreach (var q in tr.VideoQualities) + { + title += " " + q; + } + + title += " ]"; + return title; + } + + private DateTime getActualPublishDate(ShizaprojectNode n, ShizaprojectTorrent t) + { + if (n.PublishedAt == null) + { + return t.UpdatedAt; + } + else + { + return (t.UpdatedAt > n.PublishedAt) ? t.UpdatedAt : n.PublishedAt.Value; + } + } + + private string getResolution(string[] qualities) + { + var resPrefix = "RESOLUTION_"; + var res = Array.Find(qualities, s => s.StartsWith(resPrefix)); + return res != null ? res.Replace(resPrefix, "") : "Unknown"; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + var response = JsonConvert.DeserializeObject(indexerResponse.Content); + + if (response.Errors != null && response.Errors.Length > 0) + { + var message = "Errors:\n"; + foreach (var e in response.Errors) + { + message += string.Format("{0} - {1}\n", e.Extensions.Code, e.Message); + } + + throw new IndexerException(indexerResponse, message); + } + + foreach (var e in response.Data.Releases.Edges) + { + foreach (var tr in e.Node.Torrents) + { + var torrentInfo = new TorrentInfo + { + Title = composeTitle(e.Node, tr), + InfoUrl = string.Format("{0}/releases/{1}/", _settings.BaseUrl.TrimEnd('/'), e.Node.Slug), + DownloadVolumeFactor = 0, + UploadVolumeFactor = 1, + Seeders = tr.Seeders, + Peers = tr.Leechers + tr.Seeders, + Grabs = tr.Downloaded, + Categories = _categories.MapTrackerCatDescToNewznab(e.Node.Type), + PublishDate = getActualPublishDate(e.Node, tr), + Guid = tr.File.Url, + DownloadUrl = tr.File.Url, + MagnetUrl = tr.MagnetUri, + Size = tr.Size, + Resolution = getResolution(tr.VideoQualities) + }; + + torrentInfos.Add(torrentInfo); + } + } + + return torrentInfos.ToArray(); + } + + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class ShizaprojectSettingsValidator : AbstractValidator + { + public ShizaprojectSettingsValidator() + { + } + } + + public class ShizaprojectSettings : IIndexerSettings + { + private static readonly ShizaprojectSettingsValidator Validator = new ShizaprojectSettingsValidator(); + + [FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")] + public string BaseUrl { get; set; } + + [FieldDefinition(2)] + public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings(); + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } + + public class ShizaprojectReleasesResponse + { + public ShizaprojectData Data { get; set; } + public ShizaprojectError[] Errors { get; set; } + } + + public class ShizaprojectError + { + public ShizaprojectErrorExtensions Extensions { get; set; } + public string Message { get; set; } + } + + public class ShizaprojectErrorExtensions + { + public string Code { get; set; } + } + + public class ShizaprojectData + { + public ShizaprojectReleases Releases { get; set; } + } + + public class ShizaprojectReleases + { + public ShizaprojectEdge[] Edges { get; set; } + } + + public class ShizaprojectEdge + { + public ShizaprojectNode Node { get; set; } + } + + public class ShizaprojectNode + { + public string Name { get; set; } + public string OriginalName { get; set; } + public string[] AlternativeNames { get; set; } + public DateTime? PublishedAt { get; set; } + public string Slug { get; set; } + public ShizaprojectTorrent[] Torrents { get; set; } + public string Type { get; set; } + } + + public class ShizaprojectFile + { + public string Url { get; set; } + } + + public class ShizaprojectTorrent + { + public int Downloaded { get; set; } + public int Seeders { get; set; } + public int Leechers { get; set; } + public long Size { get; set; } + public string MagnetUri { get; set; } + public DateTime UpdatedAt { get; set; } + public string[] VideoQualities { get; set; } + public ShizaprojectFile File { get; set; } + } +}