[feature] migrated Anilibria yaml indexer to C# with new domain and API (#16043)

Thank you for your contribution :-)
This commit is contained in:
impr3ssi0n
2025-06-28 08:16:37 +03:00
committed by GitHub
parent 09f88b71ef
commit a4eca2ad51
6 changed files with 204 additions and 300 deletions

View File

@@ -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

View File

@@ -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> { 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<IndexerConfigurationStatus> 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<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
{
var releases = new List<ReleaseInfo>();
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<AnilibriaTorrentInfo>();
foreach (var id in ids)
{
var torrents = await RequestWithCookiesAsync(
$"{ApiBase}anime/torrents/release/{id}", cookieOverride: string.Empty);
torrentsInfo.AddRange(
JsonConvert.DeserializeObject<List<AnilibriaTorrentInfo>>(
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");
}
}

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,6 @@
namespace Jackett.Common.Models.IndexerConfig.Bespoke
{
public class ConfigurationDataAnilibria : ConfigurationData
{
}
}

View File

@@ -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<AnilibriaTorrentInfo>
{
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.");
}
}

View File

@@ -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",