using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading.Tasks; using FluentValidation; using NLog; using NzbDrone.Common; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Indexers.Settings; 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 AnimeBytes : TorrentIndexerBase { public override string Name => "AnimeBytes"; public override string[] IndexerUrls => new[] { "https://animebytes.tv/" }; public override string Description => "AnimeBytes (AB) is the largest private torrent tracker that specialises in anime and anime-related content."; public override string Language => "en-US"; public override Encoding Encoding => Encoding.UTF8; public override IndexerPrivacy Privacy => IndexerPrivacy.Private; public override IndexerCapabilities Capabilities => SetCapabilities(); public override TimeSpan RateLimit => TimeSpan.FromSeconds(4); private readonly ICached _queryResultCache; public AnimeBytes(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, ICacheManager cacheManager) : base(httpClient, eventAggregator, indexerStatusService, configService, logger) { _queryResultCache = cacheManager.GetCache(GetType(), "QueryResults"); } public override IIndexerRequestGenerator GetRequestGenerator() { return new AnimeBytesRequestGenerator(Settings, Capabilities); } public override IParseIndexerResponse GetParser() { return new AnimeBytesParser(Settings); } protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) { return false; } protected string BuildQueryResultCacheKey(IndexerRequest request) { return $"{request.HttpRequest.Url.FullUri}.{HashUtil.ComputeSha256Hash(Settings.ToJson())}"; } protected override async Task FetchPage(IndexerRequest request, IParseIndexerResponse parser) { var cacheKey = BuildQueryResultCacheKey(request); var queryResult = _queryResultCache.Find(cacheKey); if (queryResult != null) { queryResult.Cached = true; return queryResult; } _queryResultCache.ClearExpired(); queryResult = await base.FetchPage(request, parser); _queryResultCache.Set(cacheKey, queryResult, TimeSpan.FromMinutes(3)); return queryResult; } protected override IList CleanupReleases(IEnumerable releases, SearchCriteriaBase searchCriteria) { var cleanReleases = base.CleanupReleases(releases, searchCriteria); if (searchCriteria.IsRssSearch) { cleanReleases = cleanReleases.Where((r, index) => r.PublishDate > DateTime.UtcNow.AddDays(-1) || index < 20).ToList(); } return cleanReleases.Select(r => (ReleaseInfo)r.Clone()).ToList(); } private IndexerCapabilities SetCapabilities() { var caps = new IndexerCapabilities { TvSearchParams = new List { TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep }, MovieSearchParams = new List { MovieSearchParam.Q }, MusicSearchParams = new List { MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Year }, BookSearchParams = new List { BookSearchParam.Q } }; caps.Categories.AddCategoryMapping("anime[tv_series]", NewznabStandardCategory.TVAnime, "TV Series"); caps.Categories.AddCategoryMapping("anime[tv_special]", NewznabStandardCategory.TVAnime, "TV Special"); caps.Categories.AddCategoryMapping("anime[ova]", NewznabStandardCategory.TVAnime, "OVA"); caps.Categories.AddCategoryMapping("anime[ona]", NewznabStandardCategory.TVAnime, "ONA"); caps.Categories.AddCategoryMapping("anime[dvd_special]", NewznabStandardCategory.TVAnime, "DVD Special"); caps.Categories.AddCategoryMapping("anime[bd_special]", NewznabStandardCategory.TVAnime, "BD Special"); caps.Categories.AddCategoryMapping("anime[movie]", NewznabStandardCategory.Movies, "Movie"); caps.Categories.AddCategoryMapping("audio", NewznabStandardCategory.Audio, "Music"); caps.Categories.AddCategoryMapping("gamec[game]", NewznabStandardCategory.Console, "Game"); caps.Categories.AddCategoryMapping("gamec[game]", NewznabStandardCategory.PCGames, "Game"); caps.Categories.AddCategoryMapping("gamec[visual_novel]", NewznabStandardCategory.Console, "Game Visual Novel"); caps.Categories.AddCategoryMapping("gamec[visual_novel]", NewznabStandardCategory.PCGames, "Game Visual Novel"); caps.Categories.AddCategoryMapping("printedtype[manga]", NewznabStandardCategory.BooksComics, "Manga"); caps.Categories.AddCategoryMapping("printedtype[oneshot]", NewznabStandardCategory.BooksComics, "Oneshot"); caps.Categories.AddCategoryMapping("printedtype[anthology]", NewznabStandardCategory.BooksComics, "Anthology"); caps.Categories.AddCategoryMapping("printedtype[manhwa]", NewznabStandardCategory.BooksComics, "Manhwa"); caps.Categories.AddCategoryMapping("printedtype[light_novel]", NewznabStandardCategory.BooksComics, "Light Novel"); caps.Categories.AddCategoryMapping("printedtype[artbook]", NewznabStandardCategory.BooksComics, "Artbook"); return caps; } } public class AnimeBytesRequestGenerator : IIndexerRequestGenerator { private readonly AnimeBytesSettings _settings; private readonly IndexerCapabilities _capabilities; private static Regex YearRegex => new(@"\b((?:19|20)\d{2})$", RegexOptions.Compiled); public AnimeBytesRequestGenerator(AnimeBytesSettings settings, IndexerCapabilities capabilities) { _settings = settings; _capabilities = capabilities; } public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) => GetRequestWithSearchType(searchCriteria, "anime"); public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) => GetRequestWithSearchType(searchCriteria, "music"); public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) => GetRequestWithSearchType(searchCriteria, "anime"); public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) => GetRequestWithSearchType(searchCriteria, "anime"); public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) => GetRequestWithSearchType(searchCriteria, "anime"); private IndexerPageableRequestChain GetRequestWithSearchType(SearchCriteriaBase searchCriteria, string searchType) { var pageableRequests = new IndexerPageableRequestChain(); pageableRequests.Add(GetRequest(searchCriteria, searchType)); return pageableRequests; } private IEnumerable GetRequest(SearchCriteriaBase searchCriteria, string searchType) { var searchUrl = $"{_settings.BaseUrl.TrimEnd('/')}/scrape.php"; var term = searchCriteria.SanitizedSearchTerm.Trim(); var searchTerm = CleanSearchTerm(term); var parameters = new NameValueCollection { { "username", _settings.Username }, { "torrent_pass", _settings.Passkey }, { "sort", "grouptime" }, { "way", "desc" }, { "type", searchType }, { "searchstr", searchTerm }, { "limit", searchTerm.IsNotNullOrWhiteSpace() ? "50" : "20" } }; if (_settings.SearchByYear && searchType == "anime") { var searchYear = ParseYearFromSearchTerm(term); if (searchYear is > 0) { parameters.Set("year", searchYear.ToString()); } } if (searchType == "music" && searchCriteria is MusicSearchCriteria musicSearchCriteria) { if (musicSearchCriteria.Artist.IsNotNullOrWhiteSpace() && musicSearchCriteria.Artist != "VA") { parameters.Set("artistnames", musicSearchCriteria.Artist); } if (musicSearchCriteria.Album.IsNotNullOrWhiteSpace()) { parameters.Set("groupname", musicSearchCriteria.Album); } if (musicSearchCriteria.Year is > 0) { parameters.Set("year", musicSearchCriteria.Year.ToString()); } } var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList(); if (queryCats.Any() && searchCriteria is TvSearchCriteria { Season: > 0 }) { // Avoid searching for specials if it's a non-zero season search queryCats.RemoveAll(cat => cat is "anime[tv_special]" or "anime[ova]" or "anime[dvd_special]" or "anime[bd_special]"); } if (queryCats.Any()) { queryCats.ForEach(cat => parameters.Set(cat, "1")); } if (_settings.FreeleechOnly) { parameters.Set("freeleech", "1"); } if (_settings.ExcludeHentai && searchType == "anime") { parameters.Set("hentai", "0"); } searchUrl += "?" + parameters.GetQueryString(); yield return new IndexerRequest(searchUrl, HttpAccept.Json); } private static string CleanSearchTerm(string term) { // Tracer does not support searching with episode number so strip it if we have one term = Regex.Replace(term, @"\W(\dx)?\d?\d$", string.Empty, RegexOptions.Compiled); term = Regex.Replace(term, @"\W(S\d\d?E)?\d?\d$", string.Empty, RegexOptions.Compiled); term = Regex.Replace(term, @"\W\d+$", string.Empty, RegexOptions.Compiled); term = Regex.Replace(term.Trim(), @"\bThe Movie$", string.Empty, RegexOptions.Compiled | RegexOptions.IgnoreCase); return term.Trim(); } private static int? ParseYearFromSearchTerm(string term) { if (term.IsNullOrWhiteSpace()) { return null; } var yearMatch = YearRegex.Match(term); if (!yearMatch.Success) { return null; } return ParseUtil.CoerceInt(yearMatch.Groups[1].Value); } public Func> GetCookies { get; set; } public Action, DateTime?> CookiesUpdater { get; set; } } public class AnimeBytesParser : IParseIndexerResponse { private static readonly HashSet ExcludedProperties = new(StringComparer.OrdinalIgnoreCase) { "Freeleech" }; private static readonly HashSet RemuxResolutions = new(StringComparer.OrdinalIgnoreCase) { "1080i", "1080p", "2160p", "4K" }; private static readonly HashSet CommonReleaseGroupsProperties = new(StringComparer.OrdinalIgnoreCase) { "Softsubs", "Hardsubs", "RAW", "Translated" }; private static readonly HashSet ExcludedFileExtensions = new(StringComparer.OrdinalIgnoreCase) { ".mka", ".mds", ".md5", ".nfo", ".sfv", ".ass", ".mks", ".srt", ".ssa", ".sup", ".jpeg", ".jpg", ".png", ".otf", ".ttf" }; private static readonly string[] PropertiesSeparator = { " | ", " / " }; private readonly AnimeBytesSettings _settings; public AnimeBytesParser(AnimeBytesSettings settings) { _settings = settings; } public IList ParseResponse(IndexerResponse indexerResponse) { var releaseInfos = new List(); if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) { throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request"); } if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) { throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}"); } var response = STJson.Deserialize(indexerResponse.Content); if (response.Error.IsNotNullOrWhiteSpace()) { throw new IndexerException(indexerResponse, "Unexpected response from indexer request: {0}", response.Error); } if (response.Matches == 0) { return releaseInfos.ToArray(); } foreach (var group in response.Groups) { var categoryName = group.CategoryName; var description = group.Description; var year = group.Year; var groupName = group.GroupName; var seriesName = group.SeriesName; var mainTitle = WebUtility.HtmlDecode(group.FullName); if (seriesName.IsNotNullOrWhiteSpace()) { mainTitle = seriesName; } var synonyms = new HashSet(StringComparer.OrdinalIgnoreCase) { mainTitle }; if (group.Synonymns != null && group.Synonymns.Any()) { if (_settings.AddJapaneseTitle && group.Synonymns.TryGetValue("Japanese", out var japaneseTitle) && japaneseTitle.IsNotNullOrWhiteSpace()) { synonyms.Add(japaneseTitle.Trim()); } if (_settings.AddRomajiTitle && group.Synonymns.TryGetValue("Romaji", out var romajiTitle) && romajiTitle.IsNotNullOrWhiteSpace()) { synonyms.Add(romajiTitle.Trim()); } if (_settings.AddAlternativeTitle && group.Synonymns.TryGetValue("Alternative", out var alternativeTitle) && alternativeTitle.IsNotNullOrWhiteSpace()) { synonyms.UnionWith(alternativeTitle.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)); } } List categories = null; foreach (var torrent in group.Torrents) { // Skip non-freeleech results when freeleech only is set if (_settings.FreeleechOnly && torrent.RawDownMultiplier != 0) { continue; } var torrentId = torrent.Id; var link = torrent.Link; var publishDate = DateTime.ParseExact(torrent.UploadTime, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); var details = new Uri(_settings.BaseUrl + "torrent/" + torrentId + "/group"); var size = torrent.Size; var snatched = torrent.Snatched; var seeders = torrent.Seeders; var leechers = torrent.Leechers; var fileCount = torrent.FileCount; var peers = seeders + leechers; var rawDownMultiplier = torrent.RawDownMultiplier; var rawUpMultiplier = torrent.RawUpMultiplier; // MST with additional 5 hours per GB var minimumSeedTime = 259200 + (int)(size / (int)Math.Pow(1024, 3) * 18000); var propertyList = WebUtility.HtmlDecode(torrent.Property) .Split(PropertiesSeparator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) .ToList(); propertyList.RemoveAll(p => ExcludedProperties.Any(p.ContainsIgnoreCase)); var properties = propertyList.ToHashSet(); if (properties.Any(p => p.StartsWith("M2TS", StringComparison.Ordinal))) { properties.Add("BR-DISK"); } var isBluRayDisk = properties.Any(p => p.Equals("RAW", StringComparison.Ordinal) || p.StartsWith("M2TS", StringComparison.Ordinal) || p.StartsWith("ISO", StringComparison.Ordinal)); if (_settings.ExcludeRaw && isBluRayDisk) { continue; } properties = properties .Select(property => { if (isBluRayDisk) { property = Regex.Replace(property, @"\b(H\.?265)\b", "HEVC", RegexOptions.Compiled | RegexOptions.IgnoreCase); property = Regex.Replace(property, @"\b(H\.?264)\b", "AVC", RegexOptions.Compiled | RegexOptions.IgnoreCase); } if (torrent.Files.Any(f => f.FileName.ContainsIgnoreCase("Remux")) && RemuxResolutions.ContainsIgnoreCase(property)) { property += " Remux"; } return property; }) .ToHashSet(); int? season = null; int? episode = null; var releaseInfo = _settings.EnableSonarrCompatibility && categoryName == "Anime" ? "S01" : ""; var editionTitle = torrent.EditionData?.EditionTitle; if (editionTitle.IsNotNullOrWhiteSpace()) { releaseInfo = WebUtility.HtmlDecode(editionTitle); if (_settings.EnableSonarrCompatibility) { var simpleSeasonRegex = new Regex(@"\bSeason (\d+)\b", RegexOptions.Compiled); var simpleSeasonRegexMatch = simpleSeasonRegex.Match(releaseInfo); if (simpleSeasonRegexMatch.Success) { season = ParseUtil.CoerceInt(simpleSeasonRegexMatch.Groups[1].Value); } } var episodeRegex = new Regex(@"\bEpisode (\d+)\b", RegexOptions.Compiled); var episodeRegexMatch = episodeRegex.Match(releaseInfo); if (episodeRegexMatch.Success) { episode = ParseUtil.CoerceInt(episodeRegexMatch.Groups[1].Value); } } if (_settings.EnableSonarrCompatibility && categoryName == "Anime") { season ??= ParseSeasonFromTitles(synonyms); } if (episode is > 0 && season == null) { releaseInfo = $" - {episode:00}"; } else if (_settings.EnableSonarrCompatibility && season is > 0) { releaseInfo = $"S{season:00}"; if (episode is > 0) { releaseInfo += $"E{episode:00} - {episode:00}"; } } releaseInfo = releaseInfo.Trim(); // Ignore these categories as they'll cause hell with the matcher // TV Special, DVD Special, BD Special if (groupName is "TV Special" or "DVD Special" or "BD Special") { continue; } if (groupName is "TV Series" or "OVA" or "ONA") { categories = new List { NewznabStandardCategory.TVAnime }; } if (groupName is "Movie" or "Live Action Movie") { categories = new List { NewznabStandardCategory.Movies }; } if (categoryName is "Manga" or "Oneshot" or "Anthology" or "Manhwa" or "Manhua" or "Light Novel") { categories = new List { NewznabStandardCategory.BooksComics }; } if (categoryName is "Novel" or "Artbook") { categories = new List { NewznabStandardCategory.BooksComics }; } if (categoryName is "Game" or "Visual Novel") { if (properties.Contains("PSP")) { categories = new List { NewznabStandardCategory.Console, NewznabStandardCategory.ConsolePSP }; } if (properties.Contains("PS3")) { categories = new List { NewznabStandardCategory.Console, NewznabStandardCategory.ConsolePS3 }; } if (properties.Contains("PS Vita")) { categories = new List { NewznabStandardCategory.Console, NewznabStandardCategory.ConsolePSVita }; } if (properties.Contains("3DS")) { categories = new List { NewznabStandardCategory.Console, NewznabStandardCategory.Console3DS }; } if (properties.Contains("NDS")) { categories = new List { NewznabStandardCategory.Console, NewznabStandardCategory.ConsoleNDS }; } if (properties.Contains("PSX") || properties.Contains("PS2") || properties.Contains("SNES") || properties.Contains("NES") || properties.Contains("GBA") || properties.Contains("Switch") || properties.Contains("N64")) { categories = new List { NewznabStandardCategory.Console, NewznabStandardCategory.ConsoleOther }; } if (properties.Contains("PC")) { categories = new List { NewznabStandardCategory.PCGames }; } } if (categoryName is "Single" or "EP" or "Album" or "Compilation" or "Soundtrack" or "Remix CD" or "PV" or "Live Album" or "Image CD" or "Drama CD" or "Vocal CD") { if (properties.Any(p => p.Contains("Lossless"))) { categories = new List { NewznabStandardCategory.Audio, NewznabStandardCategory.AudioLossless }; } else if (properties.Any(p => p.Contains("MP3"))) { categories = new List { NewznabStandardCategory.Audio, NewznabStandardCategory.AudioMP3 }; } else { categories = new List { NewznabStandardCategory.Audio, NewznabStandardCategory.AudioOther }; } } // We don't actually have a release name >.> so try to create one var releaseGroup = properties.LastOrDefault(p => CommonReleaseGroupsProperties.Any(p.StartsWithIgnoreCase) && p.Contains('(') && p.Contains(')')); if (releaseGroup.IsNotNullOrWhiteSpace()) { var start = releaseGroup.IndexOf("(", StringComparison.Ordinal); releaseGroup = "[" + releaseGroup.Substring(start + 1, releaseGroup.IndexOf(")", StringComparison.Ordinal) - 1 - start) + "] "; } else { releaseGroup = string.Empty; } var infoString = properties.Select(p => "[" + p + "]").Join(string.Empty); if (_settings.UseFilenameForSingleEpisodes) { var files = torrent.Files.ToList(); if (files.Count > 1) { files = files.Where(f => !ExcludedFileExtensions.Contains(Path.GetExtension(f.FileName))).ToList(); } if (files.Count == 1) { var fileName = files.First().FileName; var guid = new Uri(details + "?nh=" + HashUtil.CalculateMd5(fileName)); var release = new TorrentInfo { MinimumRatio = 1, MinimumSeedTime = minimumSeedTime, Title = fileName, Year = year.GetValueOrDefault(), InfoUrl = details.AbsoluteUri, Guid = guid.AbsoluteUri, DownloadUrl = link.AbsoluteUri, PublishDate = publishDate, Categories = categories, Description = description, Size = size, Seeders = seeders, Peers = peers, Grabs = snatched, Files = fileCount, DownloadVolumeFactor = rawDownMultiplier, UploadVolumeFactor = rawUpMultiplier, }; releaseInfos.Add(release); } } var useYearInTitle = year is > 0 && torrent.Files.Any(f => f.FileName.Contains(year.Value.ToString())); foreach (var title in synonyms) { var releaseTitle = groupName is "Movie" or "Live Action Movie" ? $"{releaseGroup}{title} {year} {infoString}" : $"{releaseGroup}{title}{(useYearInTitle ? $" {year}" : string.Empty)} {releaseInfo} {infoString}"; var guid = new Uri(details + "?nh=" + HashUtil.CalculateMd5(title)); var release = new TorrentInfo { MinimumRatio = 1, MinimumSeedTime = minimumSeedTime, Title = releaseTitle.Trim(), Year = year.GetValueOrDefault(), InfoUrl = details.AbsoluteUri, Guid = guid.AbsoluteUri, DownloadUrl = link.AbsoluteUri, PublishDate = publishDate, Categories = categories, Description = description, Size = size, Seeders = seeders, Peers = peers, Grabs = snatched, Files = fileCount, DownloadVolumeFactor = rawDownMultiplier, UploadVolumeFactor = rawUpMultiplier, }; releaseInfos.Add(release); } } } return releaseInfos .OrderByDescending(o => o.PublishDate) .ToArray(); } private static int? ParseSeasonFromTitles(IReadOnlyCollection titles) { var advancedSeasonRegex = new Regex(@"\b(?:(?\d+)(?:st|nd|rd|th) Season|Season (?\d+))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); var seasonCharactersRegex = new Regex(@"(I{2,})$", RegexOptions.Compiled); var seasonNumberRegex = new Regex(@"\b(?[2-9])$", RegexOptions.Compiled); foreach (var title in titles) { var advancedSeasonRegexMatch = advancedSeasonRegex.Match(title); if (advancedSeasonRegexMatch.Success) { return ParseUtil.CoerceInt(advancedSeasonRegexMatch.Groups["season"].Value); } var seasonCharactersRegexMatch = seasonCharactersRegex.Match(title); if (seasonCharactersRegexMatch.Success) { return seasonCharactersRegexMatch.Groups[1].Value.Length; } var seasonNumberRegexMatch = seasonNumberRegex.Match(title); if (seasonNumberRegexMatch.Success) { return ParseUtil.CoerceInt(seasonNumberRegexMatch.Groups["season"].Value); } } return null; } public Action, DateTime?> CookiesUpdater { get; set; } } public class AnimeBytesSettingsValidator : NoAuthSettingsValidator { public AnimeBytesSettingsValidator() { RuleFor(c => c.Username).NotEmpty(); RuleFor(c => c.Passkey).NotEmpty() .Must(x => x.Length is 32 or 48) .WithMessage("Passkey length must be 32 or 48"); } } public class AnimeBytesSettings : NoAuthTorrentBaseSettings { private static readonly AnimeBytesSettingsValidator Validator = new(); public AnimeBytesSettings() { Username = ""; Passkey = ""; FreeleechOnly = false; ExcludeRaw = false; ExcludeHentai = false; SearchByYear = false; EnableSonarrCompatibility = true; UseFilenameForSingleEpisodes = true; AddJapaneseTitle = true; AddRomajiTitle = true; AddAlternativeTitle = true; } [FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)] public string Username { get; set; } [FieldDefinition(3, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)] public string Passkey { get; set; } [FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech torrents only")] public bool FreeleechOnly { get; set; } [FieldDefinition(5, Label = "Exclude RAW", Type = FieldType.Checkbox, HelpText = "Exclude RAW torrents from results")] public bool ExcludeRaw { get; set; } [FieldDefinition(6, Label = "Exclude Hentai", Type = FieldType.Checkbox, HelpText = "Exclude Hentai torrents from results")] public bool ExcludeHentai { get; set; } [FieldDefinition(7, Label = "Search By Year", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr to search by year as a different argument in the request.")] public bool SearchByYear { get; set; } [FieldDefinition(8, Label = "Enable Sonarr Compatibility", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr try to add Season information into Release names, without this Sonarr can't match any Seasons, but it has a lot of false positives as well")] public bool EnableSonarrCompatibility { get; set; } [FieldDefinition(9, Label = "Use Filenames for Single Episodes", Type = FieldType.Checkbox, HelpText = "Add a release using the actual filename, this currently only works for single episode releases")] public bool UseFilenameForSingleEpisodes { get; set; } [FieldDefinition(10, Label = "Add Japanese title as a synonym", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr add Japanese titles as synonyms, i.e kanji/hiragana/katakana.")] public bool AddJapaneseTitle { get; set; } [FieldDefinition(11, Label = "Add Romaji title as a synonym", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr add Romaji title as a synonym, i.e \"Shingeki no Kyojin\" with Attack on Titan")] public bool AddRomajiTitle { get; set; } [FieldDefinition(12, Label = "Add alternative title as a synonym", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr add alternative title as a synonym, i.e \"AoT\" with Attack on Titan, but also \"Attack on Titan Season 4\" Instead of \"Attack on Titan: The Final Season\"")] public bool AddAlternativeTitle { get; set; } public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } } public class AnimeBytesResponse { [JsonPropertyName("Matches")] public int Matches { get; set; } [JsonPropertyName("Groups")] public IReadOnlyCollection Groups { get; set; } public string Error { get; set; } } public class AnimeBytesGroup { [JsonPropertyName("ID")] public long Id { get; set; } [JsonPropertyName("CategoryName")] public string CategoryName { get; set; } [JsonPropertyName("FullName")] public string FullName { get; set; } [JsonPropertyName("GroupName")] public string GroupName { get; set; } [JsonPropertyName("SeriesName")] public string SeriesName { get; set; } [JsonPropertyName("Year")] [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public int? Year { get; set; } [JsonPropertyName("Image")] public string Image { get; set; } [JsonPropertyName("SynonymnsV2")] public IReadOnlyDictionary Synonymns { get; set; } [JsonPropertyName("Description")] public string Description { get; set; } [JsonPropertyName("Tags")] public IReadOnlyCollection Tags { get; set; } [JsonPropertyName("Torrents")] public IReadOnlyCollection Torrents { get; set; } } public class AnimeBytesTorrent { [JsonPropertyName("ID")] public long Id { get; set; } [JsonPropertyName("EditionData")] public AnimeBytesEditionData EditionData { get; set; } [JsonPropertyName("RawDownMultiplier")] public double RawDownMultiplier { get; set; } [JsonPropertyName("RawUpMultiplier")] public double RawUpMultiplier { get; set; } [JsonPropertyName("Link")] public Uri Link { get; set; } [JsonPropertyName("Property")] public string Property { get; set; } [JsonPropertyName("Snatched")] public int Snatched { get; set; } [JsonPropertyName("Seeders")] public int Seeders { get; set; } [JsonPropertyName("Leechers")] public int Leechers { get; set; } [JsonPropertyName("Size")] public long Size { get; set; } [JsonPropertyName("FileCount")] public int FileCount { get; set; } [JsonPropertyName("FileList")] public IReadOnlyCollection Files { get; set; } [JsonPropertyName("UploadTime")] public string UploadTime { get; set; } } public class AnimeBytesFile { [JsonPropertyName("filename")] public string FileName { get; set; } [JsonPropertyName("size")] public long FileSize { get; set; } } public class AnimeBytesEditionData { [JsonPropertyName("EditionTitle")] public string EditionTitle { get; set; } } }