From 63025900b6d77982096d48aa5f2a214852e5ceb8 Mon Sep 17 00:00:00 2001 From: xfouloux Date: Wed, 17 Mar 2021 14:26:33 +1100 Subject: [PATCH] add sharewood api with C# (#11327) for #10269 --- src/Jackett.Common/Indexers/Sharewood.cs | 331 +++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 src/Jackett.Common/Indexers/Sharewood.cs diff --git a/src/Jackett.Common/Indexers/Sharewood.cs b/src/Jackett.Common/Indexers/Sharewood.cs new file mode 100644 index 000000000..e57e91175 --- /dev/null +++ b/src/Jackett.Common/Indexers/Sharewood.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Jackett.Common.Models; +using Jackett.Common.Models.IndexerConfig; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; +using Newtonsoft.Json.Linq; +using NLog; +using static Jackett.Common.Models.IndexerConfig.ConfigurationData; +using WebClient = Jackett.Common.Utils.Clients.WebClient; + + + +namespace Jackett.Common.Indexers +{ + [ExcludeFromCodeCoverage] + public class ShareWood : BaseWebIndexer + { + private readonly Dictionary _apiHeaders = new Dictionary + { + {"Accept", "application/json"}, + {"Content-Type", "application/json"} + }; + // API DOC: https://github.com/Jackett/Jackett/issues/10269 + private string SearchUrl => SiteLink + "api/" + configData.Passkey.Value; + private new ConfigurationDataPasskey configData => (ConfigurationDataPasskey)base.configData; + + public ShareWood(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, + ICacheService cs) + : base( + id: "sharewoodapi", + name: "Sharewood API", + description: "Sharewood is a Semi-Private FRENCH Torrent Tracker for GENERAL", + link: "https://www.sharewood.tv/", + caps: new TorznabCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep + }, + MovieSearchParams = new List + { + MovieSearchParam.Q + }, + MusicSearchParams = new List + { + MusicSearchParam.Q + }, + BookSearchParams = new List + { + BookSearchParam.Q + } + }, + configService: configService, + client: wc, + logger: l, + p: ps, + cacheService: cs, + configData: new ConfigurationDataPasskey() + ) + { + Encoding = Encoding.UTF8; + Language = "fr-fr"; + Type = "semi-private"; + + // requestDelay for API Limit (1 request per 2 seconds) + webclient.requestDelay = 2.1; + + //CATEGORIES + //AddCategoryMapping(1, TorznabCatType.Movies, "Vidéos"); + //AddCategoryMapping(1, TorznabCatType.TV, "Vidéos"); + //AddCategoryMapping(2, TorznabCatType.Audio, "Audio"); + //AddCategoryMapping(3, TorznabCatType.PC, "Application"); + //AddCategoryMapping(4, TorznabCatType.Books, "Ebooks"); + //AddCategoryMapping(5, TorznabCatType.PCGames, "Jeu-Vidéo"); + //AddCategoryMapping(6, TorznabCatType.OtherMisc, "Formation"); + //AddCategoryMapping(7, TorznabCatType.XXX, "XXX"); + + //SUBCATEGORIES + AddCategoryMapping(9, TorznabCatType.Movies, "Films"); + AddCategoryMapping(10, TorznabCatType.TV, "Série"); + AddCategoryMapping(11, TorznabCatType.MoviesOther, "Film Animation"); + AddCategoryMapping(12, TorznabCatType.TVAnime, "Série Animation"); + AddCategoryMapping(13, TorznabCatType.TVDocumentary, "Documentaire"); + AddCategoryMapping(14, TorznabCatType.TVOther, "Emission TV"); + AddCategoryMapping(15, TorznabCatType.TVOther, "Spectacle/Concert"); + AddCategoryMapping(16, TorznabCatType.TVSport, "Sport"); + AddCategoryMapping(17, TorznabCatType.AudioOther, "Karaoké Vidéo"); + AddCategoryMapping(18, TorznabCatType.AudioOther, "Karaoké"); + AddCategoryMapping(20, TorznabCatType.Audio, "Musique"); + AddCategoryMapping(21, TorznabCatType.AudioOther, "Podcast"); + AddCategoryMapping(22, TorznabCatType.Audio, "Sample"); + AddCategoryMapping(23, TorznabCatType.AudioAudiobook, "Ebook Audio"); + AddCategoryMapping(24, TorznabCatType.Books, "BD"); + AddCategoryMapping(25, TorznabCatType.BooksComics, "Comic"); + AddCategoryMapping(26, TorznabCatType.BooksOther, "Manga"); + AddCategoryMapping(27, TorznabCatType.Books, "Livre"); + AddCategoryMapping(28, TorznabCatType.BooksMags, "Presse"); + AddCategoryMapping(29, TorznabCatType.Audio, "Application Linux"); + AddCategoryMapping(30, TorznabCatType.PC, "Application Window"); + AddCategoryMapping(31, TorznabCatType.PCMac, "Application Mac"); + AddCategoryMapping(34, TorznabCatType.PCMobileiOS, "Application Smartphone/Tablette"); + AddCategoryMapping(34, TorznabCatType.PCMobileAndroid, "Application Smartphone/Tablette"); + AddCategoryMapping(35, TorznabCatType.Other, "GPS"); + AddCategoryMapping(36, TorznabCatType.Audio, "Jeux Linux"); + AddCategoryMapping(37, TorznabCatType.PCGames, "Jeux Windows"); + AddCategoryMapping(39, TorznabCatType.ConsoleNDS, "Jeux Nintendo"); + AddCategoryMapping(39, TorznabCatType.ConsoleWii, "Jeux Nintendo"); + AddCategoryMapping(39, TorznabCatType.ConsoleWiiware, "Jeux Nintendo"); + AddCategoryMapping(39, TorznabCatType.Console3DS, "Jeux Nintendo"); + AddCategoryMapping(39, TorznabCatType.ConsoleWiiU, "Jeux Nintendo"); + AddCategoryMapping(41, TorznabCatType.PCMobileAndroid, "PC/Mobile-Android"); + AddCategoryMapping(42, TorznabCatType.PCGames, "Jeux Microsoft"); + AddCategoryMapping(44, TorznabCatType.XXX, "XXX Films"); + AddCategoryMapping(45, TorznabCatType.XXXOther, "XXX Hentai"); + AddCategoryMapping(47, TorznabCatType.XXXImageSet, "XXX Images"); + AddCategoryMapping(48, TorznabCatType.XXXOther, "XXX Jeu-Vidéo"); + AddCategoryMapping(50, TorznabCatType.OtherMisc, "Formation Logiciels"); + AddCategoryMapping(49, TorznabCatType.OtherMisc, "Formations Vidéos"); + AddCategoryMapping(51, TorznabCatType.XXXOther, "XXX Ebooks"); + AddCategoryMapping(52, TorznabCatType.AudioVideo, "Vidéos-Clips"); + AddCategoryMapping(51, TorznabCatType.XXXOther, "XXX Ebooks"); + AddCategoryMapping(51, TorznabCatType.XXXOther, "XXX Ebooks"); + + var FreeLeechOnly = new BoolConfigurationItem("Search freeleech only"); + configData.AddDynamic("freeleechonly", FreeLeechOnly); + + var ReplaceMulti = new BoolConfigurationItem("Replace MULTI by another language in release name"); + configData.AddDynamic("replacemulti", ReplaceMulti); + + // Configure the language select option for MULTI + var languageSelect = new SingleSelectConfigurationItem("Replace MULTI by this language", new Dictionary + { + {"FRENCH", "FRENCH"}, + {"MULTI.FRENCH", "MULTI.FRENCH"}, + {"ENGLISH", "ENGLISH"}, + {"MULTI.ENGLISH", "MULTI.ENGLISH" }, + {"VOSTFR", "VOSTFR"}, + {"MULTI.VOSTFR", "MULTI.VOSTFR"} + }) + { Value = "FRENCH" }; + ; + configData.AddDynamic("languageid", languageSelect); + + var ReplaceVostfr = new BoolConfigurationItem("Replace VOSTFR with ENGLISH"); + configData.AddDynamic("replacevostfr", ReplaceVostfr); + + EnableConfigurableRetryAttempts(); + } + + private string MultiRename(string term, string replacement) + { + replacement = " " + replacement + " "; + term = Regex.Replace(term, @"(?i)(\smulti\s)", replacement); + term = Regex.Replace(term, @"(?i)(\smulti$)", replacement); + return term; + } + + private string VostfrRename(string term, string replacement) + { + term = Regex.Replace(term, @"(?i)(vostfr)", replacement); + term = Regex.Replace(term, @"(?i)(subfrench)", replacement); + return term; + } + + private bool GetFreeLeech => ((BoolConfigurationItem)configData.GetDynamic("freeleechonly")).Value; + private bool GetReplaceMulti => ((BoolConfigurationItem)configData.GetDynamic("replacemulti")).Value; + private string GetLang => ((SingleSelectConfigurationItem)configData.GetDynamic("languageid")).Value; + private bool GetReplaceVostfr => ((BoolConfigurationItem)configData.GetDynamic("replacevostfr")).Value; + public override async Task ApplyConfiguration(JToken configJson) + { + LoadValuesFromJson(configJson); + + if (configData.Passkey.Value.Length != 32) + throw new Exception("Invalid Passkey configured. Expected length: 32"); + + var releases = await PerformQuery(new TorznabQuery()); + + await ConfigureIfOK(string.Empty, releases.Any(), + () => throw new Exception("Could not find releases.")); + + return IndexerConfigurationStatus.Completed; + } + + protected override async Task> PerformQuery(TorznabQuery query) + { + + var releases = new List(); + var Mapcats = MapTorznabCapsToTrackers(query); + + if (Mapcats.Count == 0) + { // NO CATEGORIES ==> RSS SEARCH + Mapcats.Add("1000"); + } + foreach (var cats in Mapcats) + { + + var searchUrl = SearchUrl; + + var qc = new List> // NameValueCollection don't support cat[]=19&cat[]=6 + { + {"limit", "25"} + }; + + if (Convert.ToInt32(cats) != 1000) + { + qc.Add("subcategory", cats); + } + + if (query.GetQueryString() != "") + { + qc.Add("name", query.GetQueryString()); + searchUrl = searchUrl + "/search"; + } + else + { + searchUrl = searchUrl + "/last-torrents"; + } + + searchUrl = searchUrl + "?" + qc.GetQueryString(); + + var response = await RequestWithCookiesAsync(searchUrl); + if (response.Status == HttpStatusCode.Unauthorized) + { + response = await RequestWithCookiesAsync(searchUrl); + } + else if (response.Status != HttpStatusCode.OK) + throw new Exception($"Unknown error in search: {response.ContentString}"); + + try + { + var rows = JArray.Parse(response.ContentString); + foreach (var row in rows) + { + var id = row.Value("id"); + var link = new Uri($"{SearchUrl}/{id}/download"); + var urlStr = row.Value("slug"); + var details = new Uri($"{SiteLink}torrents/{urlStr}.{id}"); + DateTime publishDate = DateTime.Parse(row.Value("created_at"), CultureInfo.InvariantCulture); + var cat = row.Value("subcategory_id"); + + if (Convert.ToInt32(cats) != 1000) + { + if (Convert.ToInt32(cats) < 8) //USE CATEGORIES + { + cat = row.Value("category_id"); + } + else //USE SUBCATEGORIES + { + cat = row.Value("subcategory_id"); + } + } + + long dlVolumeFactor = 1; + long ulVolumeFactor = 1; + if (row.Value("free") == true) + { + dlVolumeFactor = 0; + } + if (row.Value("doubleup") == true) + { + ulVolumeFactor = 2; + } + var sizeString = row.Value("size"); + var title = row.Value("name"); + + //SPECIAL CASES + + if (GetFreeLeech == true && dlVolumeFactor == 1) + continue; + + if (GetReplaceMulti == true) + { + title = MultiRename(title, GetLang); + } + + if (GetReplaceVostfr == true) + { + title = VostfrRename(title, "ENGLISH"); + } + + var release = new ReleaseInfo + { + + Title = title, + Link = link, + Details = details, + Guid = details, + Category = MapTrackerCatToNewznab(cat), + PublishDate = publishDate, + Size = ReleaseInfo.GetBytes(sizeString), + Grabs = row.Value("times_completed"), + Seeders = row.Value("seeders"), + Peers = row.Value("leechers") + row.Value("seeders"), + DownloadVolumeFactor = dlVolumeFactor, + UploadVolumeFactor = ulVolumeFactor, + MinimumRatio = 1, + MinimumSeedTime = 259200 // 72 hours + }; + + releases.Add(release); + } + } + catch (Exception ex) + { + OnParseError(response.ContentString, ex); + } + } + return releases; + } + + public override async Task Download(Uri link) + { + var response = await RequestWithCookiesAsync(link.ToString()); + if (response.Status == HttpStatusCode.Unauthorized) + { + response = await RequestWithCookiesAsync(link.ToString()); + } + else if (response.Status != HttpStatusCode.OK) + throw new Exception($"Unknown error in download: {response.ContentBytes}"); + return response.ContentBytes; + } + } +}