From e9ae2d4cd8b590748ed41bbcc8ee78aa6bd42860 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Robert?= <sebastien.robert@intech.lu>
Date: Tue, 29 Dec 2015 11:17:18 +0100
Subject: [PATCH] WiHD Provider

---
 src/Jackett/Content/logos/wihd.png            | Bin 0 -> 24917 bytes
 src/Jackett/Indexers/WiHD.cs                  | 702 ++++++++++++++++++
 src/Jackett/Jackett.csproj                    |   5 +-
 .../Bespoke/ConfigurationDataWiHD.cs          |  55 ++
 4 files changed, 761 insertions(+), 1 deletion(-)
 create mode 100644 src/Jackett/Content/logos/wihd.png
 create mode 100644 src/Jackett/Indexers/WiHD.cs
 create mode 100644 src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataWiHD.cs

diff --git a/src/Jackett/Content/logos/wihd.png b/src/Jackett/Content/logos/wihd.png
new file mode 100644
index 0000000000000000000000000000000000000000..c6ccd81f4a9b7ed9ff2423ee4d55e00d622ae174
GIT binary patch
literal 24917
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
MUHx3vIVCg!0KoVHi2wiq

literal 0
HcmV?d00001

diff --git a/src/Jackett/Indexers/WiHD.cs b/src/Jackett/Indexers/WiHD.cs
new file mode 100644
index 00000000..09a61f17
--- /dev/null
+++ b/src/Jackett/Indexers/WiHD.cs
@@ -0,0 +1,702 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using CsQuery;
+using Jackett.Models;
+using Jackett.Models.IndexerConfig.Bespoke;
+using Jackett.Services;
+using Jackett.Utils;
+using Jackett.Utils.Clients;
+using Newtonsoft.Json.Linq;
+using NLog;
+
+namespace Jackett.Indexers
+{
+    /// <summary>
+    /// Provider for WiHD Private French Tracker
+    /// </summary>
+    public class WiHD : BaseIndexer, IIndexer
+    {
+        private string LoginUrl { get { return SiteLink + "login"; } }
+        private string LoginCheckUrl { get { return SiteLink + "login_check"; } }
+        private string SearchUrl { get { return SiteLink + "torrent/ajaxfiltertorrent/"; } }
+        private Dictionary<string, string> emulatedBrowserHeaders = new Dictionary<string, string>();
+        private CQ fDom = null;
+        private bool Latency { get { return ConfigData.Latency.Value; } }
+        private bool DevMode { get { return ConfigData.DevMode.Value; } }
+
+        private ConfigurationDataWiHD ConfigData
+        {
+            get { return (ConfigurationDataWiHD)configData; }
+            set { base.configData = value; }
+        }
+
+        public WiHD(IIndexerManagerService i, IWebClient w, Logger l, IProtectionService ps)
+            : base(
+                name: "WiHD",
+                description: "Your World in High Definition",
+                link: "http://world-in-hd.net/",
+                caps: new TorznabCapabilities(),
+                manager: i,
+                client: w,
+                logger: l,
+                p: ps,
+                downloadBase: "http://world-in-hd.net/torrents/download/",
+                configData: new ConfigurationDataWiHD())
+        {
+            // Clean capabilities
+            TorznabCaps.Categories.Clear();
+
+            // Movies
+            AddCategoryMapping("565af82b1fd35761568b4572", TorznabCatType.MoviesHD);        // 1080P
+            AddCategoryMapping("565af82b1fd35761568b4574", TorznabCatType.MoviesHD);        // 720P
+            AddCategoryMapping("565af82b1fd35761568b4576", TorznabCatType.MoviesHD);        // HDTV
+            AddCategoryMapping("565af82b1fd35761568b4578", TorznabCatType.MoviesBluRay);    // Bluray
+            AddCategoryMapping("565af82b1fd35761568b457a", TorznabCatType.MoviesBluRay);    // Bluray Remux
+            AddCategoryMapping("565af82b1fd35761568b457c", TorznabCatType.Movies3D);        // Bluray 3D
+
+            // TV
+            AddCategoryMapping("565af82d1fd35761568b4587", TorznabCatType.TVHD);            // 1080P
+            AddCategoryMapping("565af82d1fd35761568b4589", TorznabCatType.TVHD);            // 720P
+            AddCategoryMapping("565af82d1fd35761568b458b", TorznabCatType.TVHD);            // HDTV
+            AddCategoryMapping("565af82d1fd35761568b458d", TorznabCatType.TVHD);            // Bluray
+            AddCategoryMapping("565af82d1fd35761568b458f", TorznabCatType.TVHD);            // Bluray Remux
+            AddCategoryMapping("565af82d1fd35761568b4591", TorznabCatType.TVHD);            // Bluray 3D
+
+            // Anime
+            AddCategoryMapping("565af82d1fd35761568b459c", TorznabCatType.TVAnime);         // 1080P
+            AddCategoryMapping("565af82d1fd35761568b459e", TorznabCatType.TVAnime);         // 720P
+            AddCategoryMapping("565af82d1fd35761568b45a0", TorznabCatType.TVAnime);         // HDTV
+            AddCategoryMapping("565af82d1fd35761568b45a2", TorznabCatType.TVAnime);         // Bluray
+            AddCategoryMapping("565af82d1fd35761568b45a4", TorznabCatType.TVAnime);         // Bluray Remux
+            AddCategoryMapping("565af82d1fd35761568b45a6", TorznabCatType.TVAnime);         // Bluray 3D
+
+            // Other
+            AddCategoryMapping("565af82d1fd35761568b45af", TorznabCatType.PC);              // Apps
+            AddCategoryMapping("565af82d1fd35761568b45b1", TorznabCatType.AudioVideo);      // Clips
+            AddCategoryMapping("565af82d1fd35761568b45b3", TorznabCatType.AudioOther);      // Audios Tracks of Movies/TV/Anime
+            AddCategoryMapping("565af82d1fd35761568b45b5", TorznabCatType.TVDocumentary);   // Documentary
+            AddCategoryMapping("565af82d1fd35761568b45b7", TorznabCatType.MoviesBluRay);    // Bluray (ALL)
+        }
+
+        /// <summary>
+        /// Configure our WiHD Provider
+        /// </summary>
+        /// <param name="configJson">Our params in Json</param>
+        /// <returns>Configuration state</returns>
+        public async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
+        {
+            // Retrieve config values set by Jackett's user
+            ConfigData.LoadValuesFromJson(configJson);
+
+            // Check & Validate Config
+            validateConfig();
+
+            // Setting our data for a better emulated browser (maximum security)
+            // TODO: Encoded Content not supported by Jackett at this time
+            // emulatedBrowserHeaders.Add("Accept-Encoding", "gzip, deflate");
+
+            // If we want to simulate a browser
+            if (ConfigData.Browser.Value) {
+
+                // Clean headers
+                emulatedBrowserHeaders.Clear();
+
+                // Inject headers
+                emulatedBrowserHeaders.Add("Accept", ConfigData.HeaderAccept.Value);
+                emulatedBrowserHeaders.Add("Accept-Language", ConfigData.HeaderAcceptLang.Value);
+                emulatedBrowserHeaders.Add("DNT", Convert.ToInt32(ConfigData.HeaderDNT.Value).ToString());
+                emulatedBrowserHeaders.Add("Upgrade-Insecure-Requests", Convert.ToInt32(ConfigData.HeaderUpgradeInsecure.Value).ToString());
+                emulatedBrowserHeaders.Add("User-Agent", ConfigData.HeaderUserAgent.Value);
+            }
+
+
+            // Getting login form to retrieve CSRF token
+            var myRequest = new Utils.Clients.WebRequest()
+            {
+                Url = LoginUrl
+            };
+
+            // Add our headers to request
+            myRequest.Headers = emulatedBrowserHeaders;
+
+            // Get login page
+            var loginPage = await webclient.GetString(myRequest);
+
+            // Retrieving our CSRF token
+            CQ loginPageDom = loginPage.Content;
+            var csrfToken = loginPageDom["input[name=\"_csrf_token\"]"].Last();
+
+            // Building login form data
+            var pairs = new Dictionary<string, string> {
+                { "_csrf_token", csrfToken.Attr("value") },
+                { "_username", ConfigData.Username.Value },
+                { "_password", ConfigData.Password.Value },
+                { "_remember_me", "on" },
+                { "_submit", "" }
+            };
+
+            // Do the login
+            var request = new Utils.Clients.WebRequest(){
+                Cookies = loginPage.Cookies,
+                PostData = pairs,
+                Referer = LoginUrl,
+                Type = RequestType.POST,
+                Url = LoginUrl,
+                Headers = emulatedBrowserHeaders
+            };
+
+            // Perform loggin
+            latencyNow();
+            output("Perform loggin.. with " + LoginCheckUrl);
+            var response = await RequestLoginAndFollowRedirect(LoginCheckUrl, pairs, loginPage.Cookies, true, null, null);
+
+            // Test if we are logged in
+            await ConfigureIfOK(response.Cookies, response.Content != null && response.Content.Contains("/logout"), () => {
+                // Oops, unable to login
+                throw new ExceptionWithConfigData("Failed to login", configData);
+            });
+
+            return IndexerConfigurationStatus.RequiresTesting;
+        }
+
+        /// <summary>
+        /// Execute our search query
+        /// </summary>
+        /// <param name="query">Query</param>
+        /// <returns>Releases</returns>
+        public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
+        {
+            var releases = new List<ReleaseInfo>();
+            var torrentRowList = new List<CQ>();
+            var searchTerm = query.GetQueryString();
+            var searchUrl = SearchUrl;
+
+            // Check cache first so we don't query the server
+            if(!DevMode)
+            {
+                lock (cache)
+                {
+                    // Remove old cache items
+                    CleanCache();
+
+                    var cachedResult = cache.Where(i => i.Query == searchTerm).FirstOrDefault();
+                    if (cachedResult != null)
+                        return cachedResult.Results.Select(s => (ReleaseInfo)s.Clone()).ToArray();
+                }
+            }
+
+            // Add emulated XHR request
+            emulatedBrowserHeaders.Add("X-Requested-With", "XMLHttpRequest");
+
+            // Request our first page
+            latencyNow();
+            var results = await RequestStringWithCookiesAndRetry(buildQuery(searchTerm, query, searchUrl), null, null, emulatedBrowserHeaders);
+            fDom = results.Content;
+
+            try
+            {
+                // Find number of results
+                int nbResults = ParseUtil.CoerceInt(Regex.Match(fDom["div.ajaxtotaltorrentcount"].Text(), @"\d+").Value);
+                output("\nFound " + nbResults + " results for query !");
+
+                // Find torrent rows
+                var firstPageRows = findTorrentRows();
+                output("There are " + firstPageRows.Length + " results on the first page !");
+                torrentRowList.AddRange(firstPageRows.Select(fRow => fRow.Cq()));
+
+                // Calculate numbers of pages available for this search query
+                int pageLinkCount = (int)Math.Ceiling((double)nbResults / firstPageRows.Length);    // Based on number results and number of torrents on first page
+                output("--> Pages available for query: " + pageLinkCount);
+
+                // If we have a term used for search and pagination result superior to one
+                if (!string.IsNullOrWhiteSpace(query.GetQueryString()) && pageLinkCount > 1)
+                {
+                    // Starting with page #2
+                    for (int i = 2; i <= Math.Min(Int32.Parse(ConfigData.Pages.Value), pageLinkCount); i++)
+                    {
+                        output("Processing page #" + i);
+
+                        // Request our page
+                        latencyNow();
+                        results = await RequestStringWithCookiesAndRetry(buildQuery(searchTerm, query, searchUrl, i), null, null, emulatedBrowserHeaders);
+
+                        // Assign response
+                        fDom = results.Content;
+
+                        // Process page results
+                        var additionalPageRows = findTorrentRows();
+                        torrentRowList.AddRange(additionalPageRows.Select(fRow => fRow.Cq()));
+                    }
+                }
+
+                // Loop on results
+                foreach (CQ tRow in torrentRowList)
+                {
+                    output("\n=>> Torrent #" + (releases.Count + 1));
+
+                    // Release Name
+                    string name = tRow.Find(".torrent-h3 > h3 > a").Attr("title").ToString();
+                    output("Release: " + name);
+
+                    // Category
+                    string categoryID = tRow.Find(".category > img").Attr("src").Split('/').Last().ToString();
+                    string categoryName = tRow.Find(".category > img").Attr("title").ToString();
+                    output("Category: " + MapTrackerCatToNewznab(mediaToCategory(categoryID, categoryName)) + " (" + categoryName + ")");
+
+                    // Uploader
+                    string uploader = tRow.Find(".uploader > span > a").Attr("title").ToString();
+                    output("Uploader: " + uploader);
+
+                    // Seeders
+                    int seeders = ParseUtil.CoerceInt(Regex.Match(tRow.Find(".seeders")[0].LastChild.ToString(), @"\d+").Value);
+                    output("Seeders: " + seeders);
+
+                    // Leechers
+                    int leechers = ParseUtil.CoerceInt(Regex.Match(tRow.Find(".leechers")[0].LastChild.ToString(), @"\d+").Value);
+                    output("Leechers: " + leechers);
+
+                    // Completed
+                    int completed = ParseUtil.CoerceInt(Regex.Match(tRow.Find(".completed")[0].LastChild.ToString(), @"\d+").Value);
+                    output("Completed: " + completed);
+
+                    // Comments
+                    int comments = ParseUtil.CoerceInt(Regex.Match(tRow.Find(".comments")[0].LastChild.ToString(), @"\d+").Value);
+                    output("Comments: " + comments);
+
+                    // Size & Publish Date
+                    string infosData = tRow.Find(".torrent-h3 > span")[0].LastChild.ToString().Trim();
+                    IList<string> infosList = infosData.Split('-').Select(s => s.Trim()).Where(s => s != String.Empty).ToList();
+
+                    // --> Size
+                    var size = ReleaseInfo.GetBytes(infosList[1].Replace("Go", "gb").Replace("Mo", "mb").Replace("Ko", "kb"));
+                    output("Size: " + infosList[1] + " (" + size + " bytes)");
+
+                    // --> Publish Date
+                    IList<string> clockList = infosList[0].Replace("Il y a", "").Split(',').Select(s => s.Trim()).Where(s => s != String.Empty).ToList();
+                    var clock = agoToDate(clockList);
+                    output("Released on: " + clock.ToString());
+
+                    // Torrent Details URL
+                    string details = tRow.Find(".torrent-h3 > h3 > a").Attr("href").ToString().TrimStart('/');
+                    Uri detailsLink = new Uri(SiteLink + details);
+                    output("Details: " + detailsLink.AbsoluteUri);
+
+                    // Torrent Comments URL
+                    Uri commentsLink = new Uri(SiteLink + details + "#tab_2");
+                    output("Comments: " + commentsLink.AbsoluteUri);
+
+                    // Torrent Download URL
+                    string download = tRow.Find(".download-item > a").Attr("href").ToString().TrimStart('/');
+                    Uri downloadLink = new Uri(SiteLink + download);
+                    output("Download: " + downloadLink.AbsoluteUri);
+
+                    // Building release infos
+                    var release = new ReleaseInfo();
+                    release.Category = MapTrackerCatToNewznab(mediaToCategory(categoryID, categoryName));
+                    release.Title = name;
+                    release.Seeders = seeders;
+                    release.Peers = seeders + leechers;
+                    release.MinimumRatio = 1;
+                    release.MinimumSeedTime = 345600;
+                    release.PublishDate = clock;
+                    release.Size = size;
+                    release.Guid = detailsLink;
+                    release.Comments = commentsLink;
+                    release.Link = downloadLink;
+                    releases.Add(release);
+                }
+
+            }
+            catch (Exception ex)
+            {
+                OnParseError("Error, unable to parse result", ex);
+            }
+
+            // Remove our XHR request header
+            emulatedBrowserHeaders.Remove("X-Requested-With");
+
+            // Return found releases
+            return releases;
+        }
+
+        /// <summary>
+        /// Build query to process
+        /// </summary>
+        /// <param name="term">Term to search</param>
+        /// <param name="query">Torznab Query for categories mapping</param>
+        /// <param name="url">Search url for provider</param>
+        /// <param name="page">Page number to request</param>
+        /// <param name="exclu">Exclusive state</param>
+        /// <param name="freeleech">Freeleech state</param>
+        /// <param name="reseed">Reseed state</param>
+        /// <returns>URL to query for parsing and processing results</returns>
+        private string buildQuery(string term, TorznabQuery query, string url, int page = 1)
+        {
+            var parameters = new NameValueCollection();
+            List<string> categoriesList = MapTorznabCapsToTrackers(query);
+            string categories = null;
+
+            if (string.IsNullOrWhiteSpace(term))
+            {
+                // If no search string provided, use default (for testing purposes)
+                term = "the walking dead";
+            }
+
+            // Encode & Add search term to URL
+            url += Uri.EscapeDataString(term);
+
+            // Check if we are processing a new page
+            if (page > 1)
+            {
+                // Adding page number to query
+                url += "/" + page.ToString();
+            }
+
+            // Adding interrogation point
+            url += "?";
+
+            // Building our tracker query
+            parameters.Add("exclu", Convert.ToInt32(ConfigData.Exclusive.Value).ToString());
+            parameters.Add("freeleech", Convert.ToInt32(ConfigData.Freeleech.Value).ToString());
+            parameters.Add("reseed", Convert.ToInt32(ConfigData.Reseed.Value).ToString());
+
+            // Loop on Categories needed
+            foreach (string category in categoriesList)
+            {
+                // If last, build !
+                if(categoriesList.Last() == category)
+                {
+                    // Adding previous categories to URL with latest category
+                    parameters.Add(Uri.EscapeDataString("subcat[]"), category + categories);
+                }
+                else
+                {
+                    // Build categories parameter
+                    categories += "&" + Uri.EscapeDataString("subcat[]") + "=" + category;
+                }
+            }
+
+            // Add timestamp as a query param (for no caching)
+            parameters.Add("_", UnixTimeNow().ToString());
+
+            // Building our query -- Cannot use GetQueryString due to UrlEncode (generating wrong subcat[] param)
+            url += string.Join("&", parameters.AllKeys.Select(a => a + "=" + parameters[a]));
+
+            output("Builded query for \"" + term + "\"... with " + url);
+
+            // Return our search url
+            return url;
+        }
+
+        /// <summary>
+        /// Generate a random fake latency to avoid detection on tracker side
+        /// </summary>
+        private void latencyNow()
+        {
+            // Need latency ?
+            if(Latency)
+            {
+                var random = new Random(DateTime.Now.Millisecond);
+                int waiting = random.Next(Convert.ToInt32(ConfigData.LatencyStart.Value), Convert.ToInt32(ConfigData.LatencyEnd.Value));
+                output("Latency Faker => Sleeping for " + waiting + " ms...");
+                // Sleep now...
+                System.Threading.Thread.Sleep(waiting);
+            }
+        }
+
+        /// <summary>
+        /// Generate an UTC Unix TimeStamp
+        /// </summary>
+        /// <returns>Unix TimeStamp</returns>
+        private long UnixTimeNow()
+        {
+            var timeSpan = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0));
+            return (long)timeSpan.TotalSeconds;
+        }
+
+        /// <summary>
+        /// Find torrent rows in search pages
+        /// </summary>
+        /// <returns>JQuery Object</returns>
+        private CQ findTorrentRows()
+        {
+            // Return all occurencis of torrents found
+            return fDom[".torrent-item"];
+        }
+
+        /// <summary>
+        /// Convert Ago date to DateTime
+        /// </summary>
+        /// <param name="clockList"></param>
+        /// <returns>A DateTime</returns>
+        private DateTime agoToDate(IList<string> clockList)
+        {
+            DateTime release = DateTime.Now;
+            foreach(var ago in clockList)
+            {
+                // Check for years
+                if(ago.Contains("Années") || ago.Contains("Année"))
+                {
+                    // Number of years to remove
+                    int years = ParseUtil.CoerceInt(Regex.Match(ago.ToString(), @"\d+").Value);
+                    // Removing
+                    release = release.AddYears(-years);
+
+                    continue;
+                }
+                // Check for months
+                else if (ago.Contains("Mois"))
+                {
+                    // Number of months to remove
+                    int months = ParseUtil.CoerceInt(Regex.Match(ago.ToString(), @"\d+").Value);
+                    // Removing
+                    release = release.AddMonths(-months);
+
+                    continue;
+                }
+                // Check for days
+                else if (ago.Contains("Jours") || ago.Contains("Jour"))
+                {
+                    // Number of days to remove
+                    int days = ParseUtil.CoerceInt(Regex.Match(ago.ToString(), @"\d+").Value);
+                    // Removing
+                    release = release.AddDays(-days);
+
+                    continue;
+                }
+                // Check for hours
+                else if (ago.Contains("Heures") || ago.Contains("Heure"))
+                {
+                    // Number of hours to remove
+                    int hours = ParseUtil.CoerceInt(Regex.Match(ago.ToString(), @"\d+").Value);
+                    // Removing
+                    release = release.AddHours(-hours);
+
+                    continue;
+                }
+                // Check for minutes
+                else if (ago.Contains("Minutes") || ago.Contains("Minute"))
+                {
+                    // Number of minutes to remove
+                    int minutes = ParseUtil.CoerceInt(Regex.Match(ago.ToString(), @"\d+").Value);
+                    // Removing
+                    release = release.AddMinutes(-minutes);
+
+                    continue;
+                }
+                // Check for seconds
+                else if (ago.Contains("Secondes") || ago.Contains("Seconde"))
+                {
+                    // Number of seconds to remove
+                    int seconds = ParseUtil.CoerceInt(Regex.Match(ago.ToString(), @"\d+").Value);
+                    // Removing
+                    release = release.AddSeconds(-seconds);
+
+                    continue;
+                }
+                else
+                {
+                    output("Unable to detect release date of torrent", "error");
+                    //throw new Exception("Unable to detect release date of torrent");
+                }
+            }
+            return release;
+        }
+
+        /// <summary>
+        /// Retrieve category ID from media ID
+        /// </summary>
+        /// <param name="media">Media ID</param>
+        /// <returns>Category ID</returns>
+        private string mediaToCategory(string media, string name)
+        {
+            // Declare our Dictionnary -- Media ID (key) <-> Category ID (value)
+            Dictionary<string, string> dictionary = new Dictionary<string, string>();
+
+            // Movies
+            dictionary.Add("565af82b1fd35761568b4573", "565af82b1fd35761568b4572");         // 1080P
+            dictionary.Add("565af82b1fd35761568b4575", "565af82b1fd35761568b4574");         // 720P
+            dictionary.Add("565af82b1fd35761568b4577", "565af82b1fd35761568b4576");         // HDTV
+            dictionary.Add("565af82b1fd35761568b4579", "565af82b1fd35761568b4578");         // Bluray
+            dictionary.Add("565af82b1fd35761568b457b", "565af82b1fd35761568b457a");         // Bluray Remux
+            dictionary.Add("565af82b1fd35761568b457d", "565af82b1fd35761568b457c");         // Bluray 3D
+
+            // TV
+            dictionary.Add("565af82d1fd35761568b4588", "565af82d1fd35761568b4587");         // 1080P
+            dictionary.Add("565af82d1fd35761568b458a", "565af82d1fd35761568b4589");         // 720P
+            dictionary.Add("565af82d1fd35761568b458c", "565af82d1fd35761568b458b");         // HDTV
+            dictionary.Add("565af82d1fd35761568b458e", "565af82d1fd35761568b458d");         // Bluray
+            dictionary.Add("565af82d1fd35761568b4590", "565af82d1fd35761568b458f");         // Bluray Remux
+            dictionary.Add("565af82d1fd35761568b4592", "565af82d1fd35761568b4591");         // Bluray 3D
+
+            // Anime
+            dictionary.Add("565af82d1fd35761568b459d", "565af82d1fd35761568b459c");         // 1080P
+            dictionary.Add("565af82d1fd35761568b459f", "565af82d1fd35761568b459e");         // 720P
+            dictionary.Add("565af82d1fd35761568b45a1", "565af82d1fd35761568b45a0");         // HDTV
+            dictionary.Add("565af82d1fd35761568b45a3", "565af82d1fd35761568b45a2");         // Bluray
+            dictionary.Add("565af82d1fd35761568b45a5", "565af82d1fd35761568b45a4");         // Bluray Remux
+            // BUG ~~ Media ID for Anime BR 3D is same as TV BR 3D ~~
+            //dictionary.Add("565af82d1fd35761568b4592", "565af82d1fd35761568b45a6");       // Bluray 3D
+
+            // Other
+            dictionary.Add("565af82d1fd35761568b45b0", "565af82d1fd35761568b45af");         // Apps
+            dictionary.Add("565af82d1fd35761568b45b2", "565af82d1fd35761568b45b1");         // Clips
+            dictionary.Add("565af82d1fd35761568b45b4", "565af82d1fd35761568b45b3");         // Audios Tracks of Movies/TV/Anime
+            dictionary.Add("565af82d1fd35761568b45b6", "565af82d1fd35761568b45b5");         // Documentary
+            dictionary.Add("565af82d1fd35761568b45b8", "565af82d1fd35761568b45b7");         // Bluray (ALL)
+
+            // Check if we know this media ID
+            if (dictionary.ContainsKey(media))
+            {
+                // Due to a bug on tracker side, check for a specific id/name as image is same for TV/Anime BR 3D
+                if(media == "565af82d1fd35761568b4592" && name == "Animations - Bluray 3D")
+                {
+                    // If it's an Anime BR 3D
+                    return "565af82d1fd35761568b45a6";
+                }
+                else
+                {
+                    // Return category ID for media ID
+                    return dictionary[media];
+                }
+            }
+            else
+            {
+                // Media ID unknown
+                throw new Exception("Media ID Unknow !");
+            }
+        }
+
+        /// <summary>
+        /// Output message for logging or developpment (console)
+        /// </summary>
+        /// <param name="message">Message to output</param>
+        /// <param name="level">Level for Logger</param>
+        private void output(string message, string level = "debug")
+        {
+            // Check if we are in dev mode
+            if(DevMode)
+            {
+                // Output message to console
+                Console.WriteLine(message);
+            }
+            else
+            {
+                // Send message to logger with level
+                switch (level)
+                {
+                    default:
+                        goto case "debug";
+                    case "debug":
+                        logger.Debug(message);
+                        break;
+                    case "info":
+                        logger.Info(message);
+                        break;
+                    case "error":
+                        logger.Error(message);
+                        break;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Validate Config entered by user on Jackett
+        /// </summary>
+        private void validateConfig()
+        {
+            // Check Username Setting
+            if (string.IsNullOrEmpty(ConfigData.Username.Value))
+            {
+                throw new ExceptionWithConfigData("You must provide a username for this tracker to login !", ConfigData);
+            }
+
+            // Check Password Setting
+            if (string.IsNullOrEmpty(ConfigData.Password.Value))
+            {
+                throw new ExceptionWithConfigData("You must provide a password with your username for this tracker to login !", ConfigData);
+            }
+
+            // Check Max Page Setting
+            if (!string.IsNullOrEmpty(ConfigData.Pages.Value))
+            {
+                try
+                {
+                    output("Settings -- Max Pages => " + Convert.ToInt32(ConfigData.Pages.Value));
+                }
+                catch (Exception)
+                {
+                    throw new ExceptionWithConfigData("Please enter a numeric maximum number of pages to crawl !", ConfigData);
+                }
+            }
+            else
+            {
+                throw new ExceptionWithConfigData("Please enter a maximum number of pages to crawl !", ConfigData);
+            }
+
+            // Check Latency Setting
+            if (ConfigData.Latency.Value)
+            {
+                // Check Latency Start Setting
+                if (!string.IsNullOrEmpty(ConfigData.LatencyStart.Value))
+                {
+                    try
+                    {
+                        output("Settings -- Latency Start => " + Convert.ToInt32(ConfigData.LatencyStart.Value));
+                    }
+                    catch (Exception)
+                    {
+                        throw new ExceptionWithConfigData("Please enter a numeric latency start in ms !", ConfigData);
+                    }
+                }
+                else
+                {
+                    throw new ExceptionWithConfigData("Latency Simulation enabled, Please enter a start latency !", ConfigData);
+                }
+
+                // Check Latency End Setting
+                if (!string.IsNullOrEmpty(ConfigData.LatencyEnd.Value))
+                {
+                    try
+                    {
+                        output("Settings -- Latency End => " + Convert.ToInt32(ConfigData.LatencyEnd.Value));
+                    }
+                    catch (Exception)
+                    {
+                        throw new ExceptionWithConfigData("Please enter a numeric latency end in ms !", ConfigData);
+                    }
+                }
+                else
+                {
+                    throw new ExceptionWithConfigData("Latency Simulation enabled, Please enter a end latency !", ConfigData);
+                }
+            }
+
+            // Check Browser Setting
+            if (ConfigData.Browser.Value)
+            {
+                // Check ACCEPT header Setting
+                if (string.IsNullOrEmpty(ConfigData.HeaderAccept.Value))
+                {
+                    throw new ExceptionWithConfigData("Browser Simulation enabled, Please enter an ACCEPT header !", ConfigData);
+                }
+
+                // Check ACCEPT-LANG header Setting
+                if (string.IsNullOrEmpty(ConfigData.HeaderAcceptLang.Value))
+                {
+                    throw new ExceptionWithConfigData("Browser Simulation enabled, Please enter an ACCEPT-LANG header !", ConfigData);
+                }
+
+                // Check USER-AGENT header Setting
+                if (string.IsNullOrEmpty(ConfigData.HeaderUserAgent.Value))
+                {
+                    throw new ExceptionWithConfigData("Browser Simulation enabled, Please enter an USER-AGENT header !", ConfigData);
+                }
+            }
+        }
+    }
+}
diff --git a/src/Jackett/Jackett.csproj b/src/Jackett/Jackett.csproj
index 628b554d..b90f7b63 100644
--- a/src/Jackett/Jackett.csproj
+++ b/src/Jackett/Jackett.csproj
@@ -207,10 +207,12 @@
     <Compile Include="Indexers\ImmortalSeed.cs" />
     <Compile Include="Indexers\FileList.cs" />
     <Compile Include="Indexers\Abstract\AvistazTracker.cs" />
+    <Compile Include="Indexers\WiHD.cs" />
     <Compile Include="Indexers\XSpeeds.cs" />
     <Compile Include="Models\GitHub\Asset.cs" />
     <Compile Include="Models\GitHub\Release.cs" />
     <Compile Include="Models\IndexerConfig\Bespoke\ConfigurationDataBlueTigers.cs" />
+    <Compile Include="Models\IndexerConfig\Bespoke\ConfigurationDataWiHD.cs" />
     <Compile Include="Models\IndexerConfig\ConfigurationDataBasicLoginWithFilter.cs" />
     <Compile Include="Models\IndexerConfig\ConfigurationDataAPIKey.cs" />
     <Compile Include="Models\IndexerConfig\ConfigurationDataBasicLoginWithRSSAndDisplay.cs" />
@@ -562,6 +564,7 @@
     <Content Include="Content\logos\tvchaosuk.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="Content\logos\wihd.png" />
     <Content Include="Content\logos\xspeeds.png" />
     <Content Include="Content\setup_indexer.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -626,4 +629,4 @@
     </PropertyGroup>
     <Error Condition="!Exists('..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets'))" />
   </Target>
-</Project>
+</Project>
\ No newline at end of file
diff --git a/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataWiHD.cs b/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataWiHD.cs
new file mode 100644
index 00000000..95075e2f
--- /dev/null
+++ b/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataWiHD.cs
@@ -0,0 +1,55 @@
+namespace Jackett.Models.IndexerConfig.Bespoke
+{
+    class ConfigurationDataWiHD : ConfigurationData
+    {
+        public DisplayItem CredentialsWarning { get; private set; }
+        public StringItem Username { get; private set; }
+        public StringItem Password { get; private set; }
+        public DisplayItem PagesWarning { get; private set; }
+        public StringItem Pages { get; private set; }
+        public BoolItem Exclusive { get; private set; }
+        public BoolItem Freeleech { get; private set; }
+        public BoolItem Reseed { get; private set; }
+        public DisplayItem SecurityWarning { get; private set; }
+        public BoolItem Latency { get; private set; }
+        public BoolItem Browser { get; private set; }
+        public DisplayItem LatencyWarning { get; private set; }
+        public StringItem LatencyStart { get; private set; }
+        public StringItem LatencyEnd { get; private set; }
+        public DisplayItem HeadersWarning { get; private set; }
+        public StringItem HeaderAccept { get; private set; }
+        public StringItem HeaderAcceptLang { get; private set; }
+        public BoolItem HeaderDNT { get; private set; }
+        public BoolItem HeaderUpgradeInsecure { get; private set; }
+        public StringItem HeaderUserAgent { get; private set; }
+        public DisplayItem DevWarning { get; private set; }
+        public BoolItem DevMode { get; private set; }
+
+        public ConfigurationDataWiHD()
+            : base()
+        {
+            CredentialsWarning = new DisplayItem("<b>Credentials Configuration</b> (<i>Private Tracker</i>),<br /><br /> <ul><li><b>Username</b> is your account name on this tracker.</li><li><b>Password</b> is your password associated to your account name.</li></ul>") { Name = "Credentials" };
+            Username = new StringItem { Name = "Username (Required)", Value = "" };
+            Password = new StringItem { Name = "Password (Required)", Value = "" };
+            PagesWarning = new DisplayItem("<b>Preferences Configuration</b> (<i>Tweak your search settings</i>),<br /><br /> <ul><li><b>Max Pages to Process</b> let you specify how many page (max) Jackett can process when doing a search. Setting a value <b>higher than 4 is dangerous</b> for you account ! (<b>Result of too many requests to tracker...that <u>will be suspect</u></b>).</li><li><b>Exclusive Only</b> let you search <u>only</u> for torrents which are marked Exclusive.</li><li><b>Freeleech Only</b> let you search <u>only</u> for torrents which are marked Freeleech.</li><li><b>Reseed Only</b> let you search <u>only</u> for torrents which need to be seeded.</li></ul>") { Name  = "Preferences" };
+            Pages = new StringItem { Name = "Max Pages to Process (Required)", Value = "4" };
+            Exclusive = new BoolItem() { Name = "Exclusive Only (Optional)", Value = false };
+            Freeleech = new BoolItem() { Name = "Freeleech Only (Optional)", Value = false };
+            Reseed = new BoolItem() { Name = "Reseed Needed Only (Optional)", Value = false };
+            SecurityWarning = new DisplayItem("<b>Security Configuration</b> (<i>Read this area carefully !</i>),<br /><br /> <ul><li><b>Latency Simulation</b> will simulate human browsing with Jacket by pausing Jacket for an random time between each request, to fake a real content browsing.</li><li><b>Browser Simulation</b> will simulate a real human browser by injecting additionals headers when doing requests to tracker.</li></ul>") { Name = "Security" };
+            Latency = new BoolItem() { Name = "Latency Simulation (Optional)", Value = true };
+            Browser = new BoolItem() { Name = "Browser Simulation (Optional)", Value = true };
+            LatencyWarning = new DisplayItem("<b>Latency Configuration</b> (<i>Required if latency simulation enabled</i>),<br /><br/> <ul><li>By filling this range, <b>Jackett will make a random timed pause</b> <u>between requests</u> to tracker <u>to simulate a real browser</u>.</li><li>MilliSeconds <b>only</b></li></ul>") { Name = "Simulate Latency" };
+            LatencyStart = new StringItem { Name = "Minimum Latency (ms)", Value = "1589" };
+            LatencyEnd = new StringItem { Name = "Maximum Latency (ms)", Value = "3674" };
+            HeadersWarning = new DisplayItem("<b>Browser Headers Configuration</b> (<i>Required if browser simulation enabled</i>),<br /><br /> <ul><li>By filling these fields, <b>Jackett will inject headers</b> with your values <u>to simulate a real browser</u>.</li><li>You can get <b>your browser values</b> here: <a href='https://www.whatismybrowser.com/detect/what-http-headers-is-my-browser-sending' target='blank'>www.whatismybrowser.com</a></li></ul><br /><i><b>Note that</b> some headers are not necessary because they are injected automatically by this provider such as Accept_Encoding, Connection, Host or X-Requested-With</i>") { Name = "Injecting headers" };
+            HeaderAccept = new StringItem { Name = "Accept", Value = "" };
+            HeaderAcceptLang = new StringItem { Name = "Accept-Language", Value = "" };
+            HeaderDNT = new BoolItem { Name = "DNT", Value = false };
+            HeaderUpgradeInsecure = new BoolItem { Name = "Upgrade-Insecure-Requests", Value = false };
+            HeaderUserAgent = new StringItem { Name = "User-Agent", Value = "" };
+            DevWarning = new DisplayItem("<b>Devlopement Facility</b> (<i>For Developers ONLY</i>),<br /><br /> <ul><li>By enabling devlopement mode, <b>Jackett will bypass his cache</b> and will <u>output debug messages to console</u> instead of his log file.</li></ul>") { Name = "Devlopement" };
+            DevMode = new BoolItem { Name = "Enable DEV MODE (Developers ONLY)", Value = false };
+        }
+    }
+}
-- 
GitLab