diff --git a/src/Jackett.Common/Definitions/anilibria.yml b/src/Jackett.Common/Definitions/anilibria.yml deleted file mode 100644 index 28a1b3a03..000000000 --- a/src/Jackett.Common/Definitions/anilibria.yml +++ /dev/null @@ -1,300 +0,0 @@ ---- -id: anilibria -name: AniLibria -description: "AniLibria is a Public torrent tracker for anime, voiced in Russian by AniLibria team" -language: ru-RU -type: public -encoding: UTF-8 -links: - - https://www.anilibria.tv/ - -caps: - categories: - Anime: TV/Anime - Movies: Movies/Other - - modes: - search: [q] - tv-search: [q, season, ep] - movie-search: [q] - -settings: - - name: stripcyrillic - type: checkbox - label: Strip Cyrillic Letters - default: false - - name: sonarr_compatibility - type: checkbox - label: Improve Sonarr compatibility by trying to better parse Season information in release titles. - default: false - - name: addrussiantotitle - type: checkbox - label: Add RUS to end of all titles to improve language detection by Sonarr and Radarr. Will cause English-only results to be misidentified. - default: false - -search: - paths: - # https://github.com/anilibria/docs/blob/master/api_v3.md - - path: "https://api.anilibria.tv/v3/title/{{ if .Keywords }}search{{ else }}updates{{ end }}" - response: - type: json - - inputs: - search: "{{ .Keywords }}" - filter: "names,posters.small.url,code,torrents.list,season.year,description" - limit: 100 - - keywordsfilters: - # strip season and/or ep - - name: re_replace - args: ["(?i)\\b(?:[SE]\\d{1,4}){1,2}\\b\\s?", ""] - - rows: - selector: list - attribute: torrents.list - multiple: true - - fields: - _episodes: - selector: episodes.string - optional: true - category: - text: "{{ if eq .Result._episodes \"Фильм\" }}Movies{{ else }}Anime{{ end }}" - title_ru: - selector: ..names.ru - title_en: - selector: ..names.en - title_en_parsed: - selector: ..names.en - filters: - - name: re_replace - args: ["(?i)\\bPart\\s*1\\b", "Part One"] - - name: re_replace - args: ["(?i)\\bPart\\s*2\\b", "Part Two"] - - name: re_replace - args: ["(?i)\\bPart\\s*3\\b", "Part Three"] - - name: re_replace - args: ["(?i)\\bPart\\s*4\\b", "Part Four"] - - name: re_replace - args: ["(?i)\\bPart\\s*5\\b", "Part Five"] - - name: re_replace - args: ["(?i)\\bPart\\s*6\\b", "Part Six"] - - name: re_replace - args: ["(?i)\\bPart\\s*7\\b", "Part Seven"] - - name: re_replace - args: ["(?i)\\bPart\\s*8\\b", "Part Eight"] - - name: re_replace - args: ["(?i)\\bPart\\s*9\\b", "Part Nine"] - - name: re_replace - args: ["(?i)\\bseason\\s*(\\d+)\\b", ""] - - name: re_replace - args: ["(?i)\\b(\\d+)(st|nd|rd|th)\\s*season[\\s\\.]*", ""] - - name: re_replace - args: ["(?i)\\b(\\d+)\\s*season\\b[\\s\\.]*", ""] - - name: re_replace - args: ["(?i)\\bseason\\s*([IVXLCDM]+)\\b", ""] - - name: re_replace - args: ["\\bI$", ""] - - name: re_replace - args: ["\\bII$", ""] - - name: re_replace - args: ["\\bIII$", ""] - - name: re_replace - args: ["\\bIV$", ""] - - name: re_replace - args: ["\\bV$", ""] - - name: re_replace - args: ["\\bVI$", ""] - - name: re_replace - args: ["\\bVII$", ""] - - name: re_replace - args: ["\\bVIII$", ""] - - name: re_replace - args: ["\\bIX$", ""] - - name: re_replace - args: ["\\bX$", ""] - - name: re_replace - args: ["(?i)\\b(\\d+)(?:st|nd|rd|th)?\\b", ""] - - name: trim - title_alternative: - selector: ..names.alternative - optional: true - filters: - - name: re_replace - args: ["(\\([\\p{IsCyrillic}\\W]+\\))|(^[\\p{IsCyrillic}\\W\\d]+\\/ )|([\\p{IsCyrillic} \\-]+,+)|([\\p{IsCyrillic}]+)", "{{ if .Config.stripcyrillic }}{{ else }}$1$2$3$4{{ end }}"] - - name: re_replace - args: ["[\\[\\(\\{<«][\\s\\W]*[\\]\\)\\}>»]", ""] - - name: re_replace - args: ["^[\\s&,\\.!\\?\\+\\-_\\|\\/':]+", ""] - - name: re_replace - args: ["^OVA$", ""] - _season_number_en: - selector: ..names.en - filters: - - name: re_replace - args: ["(?i)\\bPart\\s*\\d+\\s*$", ""] - - name: re_replace - args: ["(?i)(^.*\\bseason\\s*(\\d+)\\b\\s*$)", "S$2"] - - name: re_replace - args: ["(?i)(^.*\\b(\\d+)(st|nd|rd|th)\\s*season\\b.*$)", "S$2"] - - name: re_replace - args: ["(?i)(^.*\\b(\\d+)\\s*season\\b.*$)", "S$2"] - - name: re_replace - args: ["(?i)(^.*\\bseason\\s*([IVXLCDM]+)\\b\\s*$)", "$1"] - - name: re_replace - args: ["(^.*X$)", "S10"] - - name: re_replace - args: ["(^.*IX$)", "S9"] - - name: re_replace - args: ["(^.*VIII$)", "S8"] - - name: re_replace - args: ["(^.*VII$)", "S7"] - - name: re_replace - args: ["(^.*VI$)", "S6"] - - name: re_replace - args: ["(^.*V$)", "S5"] - - name: re_replace - args: ["(^.*IV$)", "S4"] - - name: re_replace - args: ["(^.*III$)", "S3"] - - name: re_replace - args: ["(^.*II$)", "S2"] - - name: re_replace - args: ["(^.*I$)", "S1"] - - name: re_replace - args: ["(?i)(^.*\\b(\\d+)(?:st|nd|rd|th)?\\b\\s*$)", "S$2"] - - name: re_replace - args: ["(?i)^(?!S\\d+).*", ""] - _season_number_alternative: - selector: ..names.alternative - optional: true - filters: - - name: re_replace - args: ["(?i)\\bPart\\s*\\d+\\s*$", ""] - - name: re_replace - args: ["(?i)(^.*\\bseason\\s*(\\d+)\\b\\s*$)", "S$2"] - - name: re_replace - args: ["(?i)(^.*\\b(\\d+)(st|nd|rd|th)\\s*season\\b\\s*$)", "S$2"] - - name: re_replace - args: ["(?i)(^.*\\b(\\d+)\\s*season\\b\\s*$)", "S$2"] - - name: re_replace - args: ["(?i)(^.*\\bseason\\s*([IVXLCDM]+)\\b\\s*$)", "$1"] - - name: re_replace - args: ["(^.*X$)", "S10"] - - name: re_replace - args: ["(^.*IX$)", "S9"] - - name: re_replace - args: ["(^.*VIII$)", "S8"] - - name: re_replace - args: ["(^.*VII$)", "S7"] - - name: re_replace - args: ["(^.*VI$)", "S6"] - - name: re_replace - args: ["(^.*V$)", "S5"] - - name: re_replace - args: ["(^.*IV$)", "S4"] - - name: re_replace - args: ["(^.*III$)", "S3"] - - name: re_replace - args: ["(^.*II$)", "S2"] - - name: re_replace - args: ["(^.*I$)", "S1"] - - name: re_replace - args: ["(?i)(^.*\\b(\\d+)(?:st|nd|rd|th)?\\b\\s*$)", "S$2"] - - name: re_replace - args: ["(?i)^(?!S\\d+).*", ""] - _season_number: - text: "{{ .Result._season_number_en }}" - filters: - - name: append - args: "{{ .Result._season_number_alternative }}" - - name: re_replace - args: ["^S1S1$", "S1"] - - name: re_replace - args: ["^S1(.+)$", "$1"] - - name: re_replace - args: ["^(S\\d+).*$", "$1"] - - name: re_replace - args: ["^$", "S1"] - year: - selector: ..season.year - _quality: - selector: quality.string - _quality_type: - selector: quality.type - _quality_resolution: - selector: quality.resolution - _quality_encoder: - selector: quality.encoder - filters: - - name: re_replace - args: ["(?i)^h", "x"] - title_parsed: - text: "{{ if .Config.stripcyrillic }}{{ else }}{{ .Result.title_ru }} / {{ end }}{{ .Result.title_en_parsed }} {{ .Result._season_number}}E{{ .Result._episodes }} [{{ .Result._quality_type }} {{ .Result._quality_resolution }} {{ .Result._quality_encoder }}]" - filters: - - name: re_replace - args: ["\\bS\\d+EФильм\\b", "({{ .Result.year }}) MOVIE"] - - name: re_replace - args: ["\\bS\\d+EOVA\\b", "({{ .Result.year }}) OVA"] - - name: re_replace - args: ["\\bS\\d+EONA\\b", "({{ .Result.year }}) ONA"] - - name: re_replace - args: ["\\bS\\d+EMovie\\b", "({{ .Result.year }}) MOVIE"] - - name: re_replace - args: ["\\bS\\d+EП/м фильм\\b", "({{ .Result.year }}) MOVIE"] - - name: re_replace - args: ["\\bS\\d+EРекап\\b", "({{ .Result.year }}) RECAP"] - - name: re_replace - args: ["\\bS\\d+ETV-Special\\b", "({{ .Result.year }}) SPECIAL"] - - name: append - args: "{{ if .Config.addrussiantotitle }} - RUS{{ else }}{{ end }}" - title_original: - text: "{{ if .Config.stripcyrillic }}{{ else }}{{ .Result.title_ru }} / {{ end }}{{ .Result.title_en }}{{ if .Result.title_alternative }} / AKA {{ .Result.title_alternative }}{{ else }}{{ end }} ({{ .Result.year }}) [{{ .Result._quality }}]{{ if .Result._episodes }} - E{{ .Result._episodes }}{{ else }}{{ end }}" - filters: - - name: re_replace - args: [" - \\bEФильм\\b", " - MOVIE"] - - name: re_replace - args: [" - \\bEMovie\\b", " - MOVIE"] - - name: re_replace - args: [" - \\bEП/м фильм\\b", " - MOVIE"] - - name: re_replace - args: [" - \\bEOVA\\b", " - OVA"] - - name: re_replace - args: [" - \\bEONA\\b", " - ONA"] - - name: append - args: "{{ if .Config.addrussiantotitle }} - RUS{{ else }}{{ end }}" - title: - text: "{{ if .Config.sonarr_compatibility }}{{ .Result.title_parsed }}{{ else }}{{ .Result.title_original }}{{ end }}" - _code: - selector: ..code - details: - text: "{{ .Config.sitelink }}release/{{ .Result._code }}.html" - download_url: - selector: url - download: - text: "{{ .Config.sitelink }}{{ .Result.download_url }}" - magnet: - selector: magnet - poster: - selector: ..posters.small.url - filters: - - name: prepend - args: "https://static.anilibria.tv" - seeders: - selector: seeders - leechers: - selector: leechers - grabs: - selector: downloads - date: - # unix - selector: uploaded_timestamp - size: - selector: total_size - downloadvolumefactor: - text: 0 - uploadvolumefactor: - text: 1 - description: - selector: ..description -# json api v3 diff --git a/src/Jackett.Common/Indexers/Definitions/Anilibria.cs b/src/Jackett.Common/Indexers/Definitions/Anilibria.cs new file mode 100644 index 000000000..22fb6f1a0 --- /dev/null +++ b/src/Jackett.Common/Indexers/Definitions/Anilibria.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using AngleSharp; +using Jackett.Common.Helpers; +using Jackett.Common.Models; +using Jackett.Common.Models.DTO.Anilibria; +using Jackett.Common.Models.IndexerConfig.Bespoke; +using Jackett.Common.Serializer; +using Jackett.Common.Services.Interfaces; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NLog; +using WebClient = Jackett.Common.Utils.Clients.WebClient; + +namespace Jackett.Common.Indexers.Definitions +{ + [ExcludeFromCodeCoverage] + public class Anilibria : IndexerBase + { + public override string Id => "anilibria"; + public override string Name => "Anilibria"; + public override string Description => "Anilibria is a russian-language anime distribution platform"; + public override string SiteLink { get; protected set; } = "https://anilibria.top/"; + public override string[] LegacySiteLinks => new[] + { + "https://www.anilibria.tv/", + }; private string ApiBase => $"{SiteLink}api/v1/"; + public override string Language => "ru-RU"; + public override string Type => "public"; + public override TorznabCapabilities TorznabCaps => SetCapabilities(); + + public Anilibria(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, + ICacheService cs) : base( + configService: configService, client: wc, logger: l, p: ps, cacheService: cs, + configData: new ConfigurationDataAnilibria()) + { + } + + private static TorznabCapabilities SetCapabilities() + { + var caps = new TorznabCapabilities + { + TvSearchParams = new List { TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep } + }; + caps.Categories.AddCategoryMapping("TV", TorznabCatType.TVAnime, "Аниме TV"); + caps.Categories.AddCategoryMapping("MOVIE", TorznabCatType.TVAnime, "Аниме Фильмы"); + caps.Categories.AddCategoryMapping("OVA", TorznabCatType.TVAnime, "Аниме OVA"); + caps.Categories.AddCategoryMapping("ONA", TorznabCatType.TVAnime, "Аниме ONA"); + caps.Categories.AddCategoryMapping("SPECIAL", TorznabCatType.TVAnime, "Аниме Спешл"); + caps.Categories.AddCategoryMapping("WEB", TorznabCatType.TVAnime, "Аниме WEB"); + caps.Categories.AddCategoryMapping("OAD", TorznabCatType.TVAnime, "Аниме OAD"); + caps.Categories.AddCategoryMapping("DORAMA", TorznabCatType.TV, "Дорамы"); + return caps; + } + + public override async Task ApplyConfiguration(JToken configJson) + { + LoadValuesFromJson(configJson); + IsConfigured = false; + + try + { + var results = await PerformQuery(new TorznabQuery()); + + if (!results.Any()) + { + throw new Exception("API unavailable or unknown error"); + } + + IsConfigured = true; + SaveConfig(); + } + catch (Exception e) + { + throw new ExceptionWithConfigData(e.Message, configData); + } + + return IndexerConfigurationStatus.Completed; + } + + protected override async Task> PerformQuery(TorznabQuery query) + { + var releases = new List(); + var template = Uri.EscapeDataString(query.GetQueryString()); + + if (string.IsNullOrEmpty(template)) + { + template = "*"; + } + + var responseReleases = await RequestWithCookiesAsync( + $"{ApiBase}app/search/releases?query={template}", cookieOverride: string.Empty); + var ids = JArray.Parse(responseReleases.ContentString).Select(o => (long?)o["id"]).Where(id => id.HasValue) + .Select(id => id.Value).ToList(); + var torrentsInfo = new List(); + + foreach (var id in ids) + { + var torrents = await RequestWithCookiesAsync( + $"{ApiBase}anime/torrents/release/{id}", cookieOverride: string.Empty); + torrentsInfo.AddRange( + JsonConvert.DeserializeObject>( + torrents.ContentString, new AnilibriaTopTorrentInfoConverter())); + } + + releases.AddRange( + torrentsInfo.Select( + torrentInfo => new ReleaseInfo + { + Guid = GetReleaseLink(torrentInfo.Alias), + Title = $"{torrentInfo.NameMain} / {torrentInfo.Label}", + Details = GetReleaseLink(torrentInfo.Alias), + Poster = GetPosterLink(torrentInfo.PosterSrc), + Year = torrentInfo.Year, + Link = GetDownloadLink(torrentInfo.Hash), + Size = torrentInfo.Size, + Seeders = torrentInfo.Seeders, + Peers = torrentInfo.Seeders + torrentInfo.Leechers, + PublishDate = torrentInfo.CreatedAt, + InfoHash = torrentInfo.Hash, + Grabs = torrentInfo.Grabs, + DownloadVolumeFactor = 0, + UploadVolumeFactor = 1, + Category = MapTrackerCatToNewznab(torrentInfo.Category) + })); + return releases; + } + + private Uri GetReleaseLink(string alias) => new($"{SiteLink}anime/releases/release/{alias}"); + private Uri GetPosterLink(string posterSrc) => new($"{SiteLink}{posterSrc.TrimStart('/')}"); + private Uri GetDownloadLink(string hash) => new($"{ApiBase}anime/torrents/{hash}/file"); + } +} diff --git a/src/Jackett.Common/Models/DTO/Anilibria/AnilibriaTorrentInfo.cs b/src/Jackett.Common/Models/DTO/Anilibria/AnilibriaTorrentInfo.cs new file mode 100644 index 000000000..e7280dd16 --- /dev/null +++ b/src/Jackett.Common/Models/DTO/Anilibria/AnilibriaTorrentInfo.cs @@ -0,0 +1,23 @@ +using System; + +namespace Jackett.Common.Models.DTO.Anilibria +{ + public class AnilibriaTorrentInfo + { + public long Id { get; set; } + public string Hash { get; set; } + public long Size { get; set; } + public string Magnet { get; set; } + public long Seeders { get; set; } + public long Leechers { get; set; } + public string Label { get; set; } + public string NameMain { get; set; } + public string NameEnglish { get; set; } + public string Alias { get; set; } + public string PosterSrc { get; set; } + public DateTime CreatedAt { get; set; } + public int Year { get; set; } + public long Grabs { get; set; } + public string Category { get; set; } + } +} diff --git a/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataAnilibria.cs b/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataAnilibria.cs new file mode 100644 index 000000000..d50d6a118 --- /dev/null +++ b/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataAnilibria.cs @@ -0,0 +1,6 @@ +namespace Jackett.Common.Models.IndexerConfig.Bespoke +{ + public class ConfigurationDataAnilibria : ConfigurationData + { + } +} diff --git a/src/Jackett.Common/Serializer/AnilibriaTorrentInfoConverter.cs b/src/Jackett.Common/Serializer/AnilibriaTorrentInfoConverter.cs new file mode 100644 index 000000000..dabb8ef57 --- /dev/null +++ b/src/Jackett.Common/Serializer/AnilibriaTorrentInfoConverter.cs @@ -0,0 +1,36 @@ +using System; +using Jackett.Common.Models.DTO.Anilibria; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Jackett.Common.Serializer +{ + public class AnilibriaTopTorrentInfoConverter : JsonConverter + { + public override AnilibriaTorrentInfo ReadJson(JsonReader reader, Type objectType, AnilibriaTorrentInfo existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var obj = JObject.Load(reader); + + return new AnilibriaTorrentInfo + { + Id = (long?)obj["id"] ?? 0, + Hash = (string)obj["hash"], + Size = (long?)obj["size"] ?? 0, + Magnet = (string)obj["magnet"], + Seeders = (long?)obj["seeders"] ?? 0, + Leechers = (long?)obj["leechers"] ?? 0, + Label = (string)obj["label"], + NameMain = (string)obj["release"]?["name"]?["main"], + NameEnglish = (string)obj["release"]?["name"]?["english"], + Alias = (string)obj["release"]?["alias"], + PosterSrc = (string)obj["release"]?["poster"]?["src"], + CreatedAt = (DateTime?)obj["created_at"] ?? DateTime.MinValue, + Year = (int?)obj["year"] ?? 0, + Grabs = (long?)obj["completed_times"] ?? 0, + Category = (string)obj["release"]?["type"]?["value"], + }; + } + + public override void WriteJson(JsonWriter writer, AnilibriaTorrentInfo value, JsonSerializer serializer) => throw new NotImplementedException("Serialization not implemented."); + } +} diff --git a/src/Jackett.Updater/Program.cs b/src/Jackett.Updater/Program.cs index 2e4d19b8e..569c444ab 100644 --- a/src/Jackett.Updater/Program.cs +++ b/src/Jackett.Updater/Program.cs @@ -275,6 +275,7 @@ namespace Jackett.Updater "Definitions/anaschcc.yml", "Definitions/angietorrents.yml", "Definitions/anidex.yml", // migrated to C# + "Definitions/anilibria.yml", // migrated to c# #5762 "Definitions/anime-free.yml", "Definitions/animeclipse.yml", "Definitions/animeitalia.yml",