2020-02-09 02:35:16 +00:00
using System ;
2017-04-15 08:45:10 +00:00
using System.Collections.Generic ;
using System.Collections.Specialized ;
2020-05-03 23:35:52 +00:00
using System.Diagnostics.CodeAnalysis ;
2018-03-01 11:38:27 +00:00
using System.Globalization ;
2017-08-13 22:15:11 +00:00
using System.IO ;
2017-04-15 08:45:10 +00:00
using System.Linq ;
2017-11-05 09:42:03 +00:00
using System.Net ;
2017-04-15 08:45:10 +00:00
using System.Reflection ;
using System.Text ;
using System.Text.RegularExpressions ;
using System.Threading.Tasks ;
2020-03-01 01:46:34 +00:00
using AngleSharp.Dom ;
using AngleSharp.Html.Dom ;
using AngleSharp.Html.Parser ;
2018-03-10 08:05:56 +00:00
using Jackett.Common.Models ;
using Jackett.Common.Models.IndexerConfig.Bespoke ;
using Jackett.Common.Services.Interfaces ;
using Jackett.Common.Utils ;
using Jackett.Common.Utils.Clients ;
2017-04-15 08:45:10 +00:00
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
using NLog ;
2020-10-19 20:50:19 +00:00
using WebClient = Jackett . Common . Utils . Clients . WebClient ;
2017-04-15 08:45:10 +00:00
2018-03-10 08:05:56 +00:00
namespace Jackett.Common.Indexers
2017-04-15 08:45:10 +00:00
{
/// <summary>
/// Provider for Abnormal Private French Tracker
2018-03-01 11:38:27 +00:00
/// gazelle based but the ajax.php API seems to be broken (always returning failure)
2017-04-15 08:45:10 +00:00
/// </summary>
2020-05-03 23:35:52 +00:00
[ExcludeFromCodeCoverage]
2017-07-10 20:58:44 +00:00
public class Abnormal : BaseCachingWebIndexer
2017-04-15 08:45:10 +00:00
{
2020-02-25 16:08:03 +00:00
private string LoginUrl = > SiteLink + "login.php" ;
private string SearchUrl = > SiteLink + "torrents.php" ;
2020-11-08 02:11:27 +00:00
private string DetailsUrl = > SiteLink + "torrents.php?id=" ;
2020-02-25 16:08:03 +00:00
private string ReplaceMulti = > ConfigData . ReplaceMulti . Value ;
private bool Latency = > ConfigData . Latency . Value ;
private bool DevMode = > ConfigData . DevMode . Value ;
private bool CacheMode = > ConfigData . HardDriveCache . Value ;
2017-08-13 22:15:11 +00:00
private static string Directory = > Path . Combine ( Path . GetTempPath ( ) , Assembly . GetExecutingAssembly ( ) . GetName ( ) . Name . ToLower ( ) , MethodBase . GetCurrentMethod ( ) . DeclaringType ? . Name . ToLower ( ) ) ;
2017-04-15 08:45:10 +00:00
2020-02-10 22:16:19 +00:00
private readonly Dictionary < string , string > emulatedBrowserHeaders = new Dictionary < string , string > ( ) ;
2017-04-15 08:45:10 +00:00
private ConfigurationDataAbnormal ConfigData
{
2020-02-25 16:08:03 +00:00
get = > ( ConfigurationDataAbnormal ) configData ;
2020-10-19 20:50:19 +00:00
set = > configData = value ;
2017-04-15 08:45:10 +00:00
}
2020-12-11 22:14:21 +00:00
public Abnormal ( IIndexerConfigurationService configService , WebClient w , Logger l , IProtectionService ps ,
ICacheService cs )
2020-05-11 19:59:28 +00:00
: base ( id : "abnormal" ,
name : "Abnormal" ,
description : "General French Private Tracker" ,
link : "https://abnormal.ws/" ,
2020-10-18 17:26:22 +00:00
caps : new TorznabCapabilities {
2020-10-18 20:47:36 +00:00
TvSearchParams = new List < TvSearchParam >
{
TvSearchParam . Q , TvSearchParam . Season , TvSearchParam . Ep
} ,
MovieSearchParams = new List < MovieSearchParam >
{
MovieSearchParam . Q
}
2020-10-18 17:26:22 +00:00
} ,
2020-05-11 19:59:28 +00:00
configService : configService ,
client : w ,
logger : l ,
p : ps ,
2020-12-11 22:14:21 +00:00
cacheService : cs ,
2020-05-11 19:59:28 +00:00
downloadBase : "https://abnormal.ws/torrents.php?action=download&id=" ,
configData : new ConfigurationDataAbnormal ( ) )
2017-04-15 08:45:10 +00:00
{
Language = "fr-fr" ;
Encoding = Encoding . UTF8 ;
Type = "private" ;
2020-08-03 19:26:24 +00:00
// NET::ERR_CERT_DATE_INVALID expired 29 July 2020
2020-08-03 19:27:26 +00:00
w . AddTrustedCertificate ( new Uri ( SiteLink ) . Host , "9cb32582b564256146616afddbdb8e7c94c428ed" ) ;
2017-04-15 08:45:10 +00:00
2020-11-06 03:09:57 +00:00
AddCategoryMapping ( "MOVIE|DVDR" , TorznabCatType . MoviesDVD , "DVDR" ) ;
AddCategoryMapping ( "MOVIE|DVDRIP" , TorznabCatType . MoviesSD , "DVDRIP" ) ;
AddCategoryMapping ( "MOVIE|BDRIP" , TorznabCatType . MoviesSD , "BDRIP" ) ;
AddCategoryMapping ( "MOVIE|VOSTFR" , TorznabCatType . MoviesOther , "VOSTFR" ) ;
AddCategoryMapping ( "MOVIE|HD|720p" , TorznabCatType . MoviesHD , "HD 720P" ) ;
AddCategoryMapping ( "MOVIE|HD|1080p" , TorznabCatType . MoviesHD , "HD 1080P" ) ;
AddCategoryMapping ( "MOVIE|REMUXBR" , TorznabCatType . MoviesBluRay , "REMUX BLURAY" ) ;
AddCategoryMapping ( "MOVIE|FULLBR" , TorznabCatType . MoviesBluRay , "FULL BLURAY" ) ;
AddCategoryMapping ( "TV|SD|VOSTFR" , TorznabCatType . TV , "TV SD VOSTFR" ) ;
AddCategoryMapping ( "TV|HD|VOSTFR" , TorznabCatType . TVHD , "TV HD VOSTFR" ) ;
AddCategoryMapping ( "TV|SD|VF" , TorznabCatType . TVSD , "TV SD VF" ) ;
AddCategoryMapping ( "TV|HD|VF" , TorznabCatType . TVHD , "TV HD VF" ) ;
AddCategoryMapping ( "TV|PACK|FR" , TorznabCatType . TVOther , "TV PACK FR" ) ;
AddCategoryMapping ( "TV|PACK|VOSTFR" , TorznabCatType . TVOther , "TV PACK VOSTFR" ) ;
AddCategoryMapping ( "TV|EMISSIONS" , TorznabCatType . TVOther , "TV EMISSIONS" ) ;
AddCategoryMapping ( "ANIME" , TorznabCatType . TVAnime , "ANIME" ) ;
AddCategoryMapping ( "DOCS" , TorznabCatType . TVDocumentary , "TV DOCS" ) ;
AddCategoryMapping ( "MUSIC|FLAC" , TorznabCatType . AudioLossless , "FLAC" ) ;
AddCategoryMapping ( "MUSIC|MP3" , TorznabCatType . AudioMP3 , "MP3" ) ;
AddCategoryMapping ( "MUSIC|CONCERT" , TorznabCatType . AudioVideo , "CONCERT" ) ;
AddCategoryMapping ( "PC|APP" , TorznabCatType . PC , "PC" ) ;
AddCategoryMapping ( "PC|GAMES" , TorznabCatType . PCGames , "GAMES" ) ;
AddCategoryMapping ( "EBOOKS" , TorznabCatType . BooksEBook , "EBOOKS" ) ;
2017-04-15 08:45:10 +00:00
}
/// <summary>
/// Configure our WiHD Provider
/// </summary>
/// <param name="configJson">Our params in Json</param>
/// <returns>Configuration state</returns>
2017-06-28 05:31:38 +00:00
public override async Task < IndexerConfigurationStatus > ApplyConfiguration ( JToken configJson )
2017-04-15 08:45:10 +00:00
{
// Retrieve config values set by Jackett's user
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
2017-07-10 20:58:44 +00:00
if ( ConfigData . Browser . Value )
{
2017-04-15 08:45:10 +00:00
// 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
2020-10-19 21:19:10 +00:00
var myRequest = new Utils . Clients . WebRequest
2017-04-15 08:45:10 +00:00
{
Url = LoginUrl
} ;
// Add our headers to request
myRequest . Headers = emulatedBrowserHeaders ;
// Building login form data
var pairs = new Dictionary < string , string > {
{ "username" , ConfigData . Username . Value } ,
{ "password" , ConfigData . Password . Value } ,
{ "keeplogged" , "1" } ,
{ "login" , "Connexion" }
} ;
// Do the login
2020-10-19 21:19:10 +00:00
var request = new Utils . Clients . WebRequest
2017-07-10 20:58:44 +00:00
{
2017-04-15 08:45:10 +00:00
PostData = pairs ,
Referer = LoginUrl ,
Type = RequestType . POST ,
Url = LoginUrl ,
Headers = emulatedBrowserHeaders
} ;
// Perform loggin
latencyNow ( ) ;
output ( "\nPerform loggin.. with " + LoginUrl ) ;
2020-06-11 15:09:27 +00:00
var response = await webclient . GetResultAsync ( request ) ;
2017-04-15 08:45:10 +00:00
// Test if we are logged in
await ConfigureIfOK ( response . Cookies , response . Cookies . Contains ( "session=" ) , ( ) = >
{
// Parse error page
2020-03-01 01:46:34 +00:00
var parser = new HtmlParser ( ) ;
2020-06-09 17:36:57 +00:00
var dom = parser . ParseDocument ( response . ContentString ) ;
2020-03-01 01:46:34 +00:00
var message = dom . QuerySelector ( ".warning" ) . TextContent . Split ( '.' ) . Reverse ( ) . Skip ( 1 ) . First ( ) ;
2017-04-15 08:45:10 +00:00
// Try left
2020-03-01 01:46:34 +00:00
var left = dom . QuerySelector ( ".info" ) . TextContent . Trim ( ) ;
2017-04-15 08:45:10 +00:00
// Oops, unable to login
output ( "-> Login failed: \"" + message + "\" and " + left + " tries left before being banned for 6 hours !" , "error" ) ;
throw new ExceptionWithConfigData ( "Login failed: " + message , configData ) ;
} ) ;
output ( "-> Login Success" ) ;
return IndexerConfigurationStatus . RequiresTesting ;
}
/// <summary>
/// Execute our search query
/// </summary>
/// <param name="query">Query</param>
/// <returns>Releases</returns>
2017-07-03 05:15:47 +00:00
protected override async Task < IEnumerable < ReleaseInfo > > PerformQuery ( TorznabQuery query )
2017-04-15 08:45:10 +00:00
{
2020-02-10 22:16:19 +00:00
var startTransition = TimeZoneInfo . TransitionTime . CreateFloatingDateRule ( new DateTime ( 1 , 1 , 1 , 3 , 0 , 0 ) , 3 , 5 , DayOfWeek . Sunday ) ;
var endTransition = TimeZoneInfo . TransitionTime . CreateFloatingDateRule ( new DateTime ( 1 , 1 , 1 , 4 , 0 , 0 ) , 10 , 5 , DayOfWeek . Sunday ) ;
var delta = new TimeSpan ( 1 , 0 , 0 ) ;
var adjustment = TimeZoneInfo . AdjustmentRule . CreateAdjustmentRule ( new DateTime ( 1999 , 10 , 1 ) , DateTime . MaxValue . Date , delta , startTransition , endTransition ) ;
2018-03-01 11:38:27 +00:00
TimeZoneInfo . AdjustmentRule [ ] adjustments = { adjustment } ;
2020-02-10 22:16:19 +00:00
var FranceTz = TimeZoneInfo . CreateCustomTimeZone ( "W. Europe Standard Time" , new TimeSpan ( 1 , 0 , 0 ) , "(GMT+01:00) W. Europe Standard Time" , "W. Europe Standard Time" , "W. Europe DST Time" , adjustments ) ;
2018-03-01 11:38:27 +00:00
2017-04-15 08:45:10 +00:00
var releases = new List < ReleaseInfo > ( ) ;
2020-03-01 01:46:34 +00:00
var qRowList = new List < IElement > ( ) ;
2017-04-15 08:45:10 +00:00
var searchTerm = query . GetQueryString ( ) ;
var searchUrl = SearchUrl ;
2020-02-10 22:16:19 +00:00
var nbResults = 0 ;
var pageLinkCount = 0 ;
2017-04-15 08:45:10 +00:00
// Check cache first so we don't query the server (if search term used or not in dev mode)
2017-07-10 20:58:44 +00:00
if ( ! DevMode & & ! string . IsNullOrEmpty ( searchTerm ) )
2017-04-15 08:45:10 +00:00
{
lock ( cache )
{
// Remove old cache items
CleanCache ( ) ;
// Search in cache
var cachedResult = cache . Where ( i = > i . Query = = searchTerm ) . FirstOrDefault ( ) ;
if ( cachedResult ! = null )
return cachedResult . Results . Select ( s = > ( ReleaseInfo ) s . Clone ( ) ) . ToArray ( ) ;
}
}
// Build our query
var request = buildQuery ( searchTerm , query , searchUrl ) ;
// Getting results & Store content
2020-03-01 01:46:34 +00:00
var parser = new HtmlParser ( ) ;
var dom = parser . ParseDocument ( await queryExec ( request ) ) ;
2017-04-15 08:45:10 +00:00
try
{
// Find torrent rows
2020-03-01 01:46:34 +00:00
var firstPageRows = findTorrentRows ( dom ) ;
2017-04-15 08:45:10 +00:00
// Add them to torrents list
2020-03-01 01:46:34 +00:00
qRowList . AddRange ( firstPageRows ) ;
2017-04-15 08:45:10 +00:00
// Check if there are pagination links at bottom
2020-03-01 01:46:34 +00:00
var qPagination = dom . QuerySelectorAll ( ".linkbox > a" ) ;
if ( qPagination . Length > 0 )
2017-07-10 20:58:44 +00:00
{
2017-04-15 08:45:10 +00:00
// Calculate numbers of pages available for this search query (Based on number results and number of torrents on first page)
2020-03-01 01:46:34 +00:00
pageLinkCount = ParseUtil . CoerceInt ( Regex . Match ( qPagination . Last ( ) . GetAttribute ( "href" ) . ToString ( ) , @"\d+" ) . Value ) ;
2017-04-15 08:45:10 +00:00
// Calculate average number of results (based on torrents rows lenght on first page)
2020-03-01 01:46:34 +00:00
nbResults = firstPageRows . Length * pageLinkCount ;
2017-04-15 08:45:10 +00:00
}
2017-07-10 20:58:44 +00:00
else
{
2017-04-15 08:45:10 +00:00
// Check if we have a minimum of one result
if ( firstPageRows . Length > = 1 )
{
// Retrieve total count on our alone page
2020-03-01 01:46:34 +00:00
nbResults = firstPageRows . Length ;
2017-04-15 08:45:10 +00:00
pageLinkCount = 1 ;
}
else
{
output ( "\nNo result found for your query, please try another search term ...\n" , "info" ) ;
// No result found for this query
return releases ;
}
}
output ( "\nFound " + nbResults + " result(s) (+/- " + firstPageRows . Length + ") in " + pageLinkCount + " page(s) for this query !" ) ;
output ( "\nThere are " + firstPageRows . Length + " results on the first page !" ) ;
// 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
2020-02-10 22:16:19 +00:00
for ( var i = 2 ; i < = Math . Min ( int . Parse ( ConfigData . Pages . Value ) , pageLinkCount ) ; i + + )
2017-04-15 08:45:10 +00:00
{
output ( "\nProcessing page #" + i ) ;
// Request our page
latencyNow ( ) ;
// Build our query
var pageRequest = buildQuery ( searchTerm , query , searchUrl , i ) ;
// Getting results & Store content
2020-03-01 01:46:34 +00:00
parser = new HtmlParser ( ) ;
dom = parser . ParseDocument ( await queryExec ( pageRequest ) ) ;
2017-04-15 08:45:10 +00:00
// Process page results
2020-03-01 01:46:34 +00:00
var additionalPageRows = findTorrentRows ( dom ) ;
2017-04-15 08:45:10 +00:00
// Add them to torrents list
2020-03-01 01:46:34 +00:00
qRowList . AddRange ( additionalPageRows ) ;
2017-04-15 08:45:10 +00:00
}
}
// Loop on results
2020-03-01 01:46:34 +00:00
foreach ( var row in qRowList )
2017-04-15 08:45:10 +00:00
{
output ( "\n=>> Torrent #" + ( releases . Count + 1 ) ) ;
// ID
2020-03-01 01:46:34 +00:00
var id = ParseUtil . CoerceInt ( Regex . Match ( row . QuerySelector ( "td:nth-of-type(2) > a" ) . GetAttribute ( "href" ) , @"\d+" ) . Value ) ;
2017-04-15 08:45:10 +00:00
output ( "ID: " + id ) ;
// Release Name
2020-03-01 01:46:34 +00:00
var name = row . QuerySelector ( "td:nth-of-type(2) > a" ) . TextContent ;
2019-05-05 21:13:10 +00:00
//issue #3847 replace multi keyword
2020-02-09 02:35:16 +00:00
if ( ! string . IsNullOrEmpty ( ReplaceMulti ) )
{
2020-02-10 22:16:19 +00:00
var regex = new Regex ( "(?i)([\\.\\- ])MULTI([\\.\\- ])" ) ;
2020-02-09 02:35:16 +00:00
name = regex . Replace ( name , "$1" + ReplaceMulti + "$2" ) ;
2019-05-05 21:13:10 +00:00
}
2017-04-15 08:45:10 +00:00
output ( "Release: " + name ) ;
// Category
2020-03-01 01:46:34 +00:00
var categoryId = row . QuerySelector ( "td:nth-of-type(1) > a" ) . GetAttribute ( "href" ) . Replace ( "torrents.php?cat[]=" , string . Empty ) ;
var newznab = MapTrackerCatToNewznab ( categoryId ) ;
output ( "Category: " + MapTrackerCatToNewznab ( categoryId ) . First ( ) . ToString ( ) + " (" + categoryId + ")" ) ;
2017-04-15 08:45:10 +00:00
// Seeders
2020-03-01 01:46:34 +00:00
var seeders = ParseUtil . CoerceInt ( Regex . Match ( row . QuerySelector ( "td:nth-of-type(6)" ) . TextContent , @"\d+" ) . Value ) ;
2017-04-15 08:45:10 +00:00
output ( "Seeders: " + seeders ) ;
// Leechers
2020-03-01 01:46:34 +00:00
var leechers = ParseUtil . CoerceInt ( Regex . Match ( row . QuerySelector ( "td:nth-of-type(7)" ) . TextContent , @"\d+" ) . Value ) ;
2017-04-15 08:45:10 +00:00
output ( "Leechers: " + leechers ) ;
// Completed
2020-03-01 01:46:34 +00:00
var completed = ParseUtil . CoerceInt ( Regex . Match ( row . QuerySelector ( "td:nth-of-type(6)" ) . TextContent , @"\d+" ) . Value ) ;
2017-04-15 08:45:10 +00:00
output ( "Completed: " + completed ) ;
// Size
2020-03-01 01:46:34 +00:00
var sizeStr = row . QuerySelector ( "td:nth-of-type(5)" ) . TextContent . Replace ( "Go" , "gb" ) . Replace ( "Mo" , "mb" ) . Replace ( "Ko" , "kb" ) ;
2020-02-10 22:16:19 +00:00
var size = ReleaseInfo . GetBytes ( sizeStr ) ;
2017-04-15 08:45:10 +00:00
output ( "Size: " + sizeStr + " (" + size + " bytes)" ) ;
// Publish DateToString
2020-03-01 01:46:34 +00:00
var datestr = row . QuerySelector ( "span.time" ) . GetAttribute ( "title" ) ;
2018-03-01 11:38:27 +00:00
var dateLocal = DateTime . SpecifyKind ( DateTime . ParseExact ( datestr , "MMM dd yyyy, HH:mm" , CultureInfo . InvariantCulture ) , DateTimeKind . Unspecified ) ;
var date = TimeZoneInfo . ConvertTimeToUtc ( dateLocal , FranceTz ) ;
output ( "Released on: " + date ) ;
2017-04-15 08:45:10 +00:00
// Torrent Details URL
2020-11-08 02:11:27 +00:00
var details = new Uri ( DetailsUrl + id ) ;
output ( "Details: " + details . AbsoluteUri ) ;
2017-04-15 08:45:10 +00:00
// Torrent Download URL
2017-08-13 22:15:11 +00:00
Uri downloadLink = null ;
2020-03-01 01:46:34 +00:00
var link = row . QuerySelector ( "td:nth-of-type(4) > a" ) . GetAttribute ( "href" ) ;
2020-02-10 22:16:19 +00:00
if ( ! string . IsNullOrEmpty ( link ) )
2017-08-13 22:15:11 +00:00
{
// Download link available
downloadLink = new Uri ( SiteLink + link ) ;
output ( "Download Link: " + downloadLink . AbsoluteUri ) ;
}
else
{
// No download link available -- Must be on pending ( can't be downloaded now...)
output ( "Download Link: Not available, torrent pending ? Skipping ..." ) ;
continue ;
}
2017-04-15 08:45:10 +00:00
2017-08-13 22:15:11 +00:00
// Freeleech
2020-02-10 22:16:19 +00:00
var downloadVolumeFactor = 1 ;
2020-03-01 01:46:34 +00:00
if ( row . QuerySelector ( "img[alt=\"Freeleech\"]" ) ! = null )
2017-08-13 22:15:11 +00:00
{
downloadVolumeFactor = 0 ;
output ( "FreeLeech =)" ) ;
}
2017-04-15 08:45:10 +00:00
2017-08-13 22:15:11 +00:00
// Building release infos
2020-03-01 01:46:34 +00:00
var release = new ReleaseInfo
2017-08-13 22:15:11 +00:00
{
2020-03-01 01:46:34 +00:00
Category = MapTrackerCatToNewznab ( categoryId ) ,
2017-08-13 22:15:11 +00:00
Title = name ,
Seeders = seeders ,
Peers = seeders + leechers ,
PublishDate = date ,
Size = size ,
2020-11-08 02:11:27 +00:00
Guid = details ,
Details = details ,
2017-08-13 22:15:11 +00:00
Link = downloadLink ,
2020-01-11 18:07:19 +00:00
MinimumRatio = 1 ,
MinimumSeedTime = 172800 , // 48 hours
2017-08-13 22:15:11 +00:00
UploadVolumeFactor = 1 ,
DownloadVolumeFactor = downloadVolumeFactor
} ;
2017-04-15 08:45:10 +00:00
releases . Add ( release ) ;
}
}
catch ( Exception ex )
{
OnParseError ( "Error, unable to parse result \n" + ex . StackTrace , ex ) ;
}
// 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>
/// <returns>URL to query for parsing and processing results</returns>
private string buildQuery ( string term , TorznabQuery query , string url , int page = 0 )
{
var parameters = new NameValueCollection ( ) ;
2020-02-10 22:16:19 +00:00
var categoriesList = MapTorznabCapsToTrackers ( query ) ;
2017-04-15 08:45:10 +00:00
string categories = null ;
// Check if we are processing a new page
if ( page > 0 )
{
// Adding page number to query
parameters . Add ( "page" , page . ToString ( ) ) ;
}
// Loop on Categories needed
2020-02-10 22:16:19 +00:00
foreach ( var category in categoriesList )
2017-04-15 08:45:10 +00:00
{
// If last, build !
if ( categoriesList . Last ( ) = = category )
{
// Adding previous categories to URL with latest category
2017-11-05 09:42:03 +00:00
parameters . Add ( Uri . EscapeDataString ( "cat[]" ) , WebUtility . UrlEncode ( category ) + categories ) ;
2017-04-15 08:45:10 +00:00
}
else
{
// Build categories parameter
2017-11-05 09:42:03 +00:00
categories + = "&" + Uri . EscapeDataString ( "cat[]" ) + "=" + WebUtility . UrlEncode ( category ) ;
2017-04-15 08:45:10 +00:00
}
}
// If search term provided
if ( ! string . IsNullOrWhiteSpace ( term ) )
{
// Add search term
2017-11-05 09:42:03 +00:00
parameters . Add ( "search" , WebUtility . UrlEncode ( term ) ) ;
2017-04-15 08:45:10 +00:00
}
else
{
2017-11-05 09:42:03 +00:00
parameters . Add ( "search" , WebUtility . UrlEncode ( "%" ) ) ;
2017-04-15 08:45:10 +00:00
// Showing all torrents (just for output function)
term = "all" ;
}
// Building our query -- Cannot use GetQueryString due to UrlEncode (generating wrong cat[] param)
url + = "?" + string . Join ( "&" , parameters . AllKeys . Select ( a = > a + "=" + parameters [ a ] ) ) ;
output ( "\nBuilded query for \"" + term + "\"... " + url ) ;
// Return our search url
return url ;
}
/// <summary>
/// Switch Method for Querying
/// </summary>
/// <param name="request">URL created by Query Builder</param>
/// <returns>Results from query</returns>
2020-02-10 22:16:19 +00:00
private async Task < string > queryExec ( string request )
2017-04-15 08:45:10 +00:00
{
2020-02-10 22:16:19 +00:00
string results = null ;
2017-04-15 08:45:10 +00:00
// Switch in we are in DEV mode with Hard Drive Cache or not
if ( DevMode & & CacheMode )
{
// Check Cache before querying and load previous results if available
results = await queryCache ( request ) ;
}
else
{
// Querying tracker directly
results = await queryTracker ( request ) ;
}
return results ;
}
/// <summary>
/// Get Torrents Page from Cache by Query Provided
/// </summary>
/// <param name="request">URL created by Query Builder</param>
/// <returns>Results from query</returns>
2020-02-10 22:16:19 +00:00
private async Task < string > queryCache ( string request )
2017-04-15 08:45:10 +00:00
{
2020-02-10 22:16:19 +00:00
string results ;
2017-04-15 08:45:10 +00:00
// Create Directory if not exist
2017-08-13 22:15:11 +00:00
System . IO . Directory . CreateDirectory ( Directory ) ;
2017-04-15 08:45:10 +00:00
// Clean Storage Provider Directory from outdated cached queries
cleanCacheStorage ( ) ;
2017-08-13 22:15:11 +00:00
// File Name
2020-02-10 22:16:19 +00:00
var fileName = StringUtil . HashSHA1 ( request ) + ".json" ;
2017-08-13 22:15:11 +00:00
2017-04-15 08:45:10 +00:00
// Create fingerprint for request
2020-02-10 22:16:19 +00:00
var file = Path . Combine ( Directory , fileName ) ;
2017-04-15 08:45:10 +00:00
// Checking modes states
2017-08-13 22:15:11 +00:00
if ( File . Exists ( file ) )
2017-04-15 08:45:10 +00:00
{
// File exist... loading it right now !
2017-08-13 22:15:11 +00:00
output ( "Loading results from hard drive cache ..." + fileName ) ;
try
{
2020-02-10 22:16:19 +00:00
using ( var fileReader = File . OpenText ( file ) )
2017-08-13 22:15:11 +00:00
{
2020-02-10 22:16:19 +00:00
var serializer = new JsonSerializer ( ) ;
results = ( string ) serializer . Deserialize ( fileReader , typeof ( string ) ) ;
2017-08-13 22:15:11 +00:00
}
}
catch ( Exception e )
{
output ( "Error loading cached results ! " + e . Message , "error" ) ;
results = null ;
}
2017-04-15 08:45:10 +00:00
}
else
{
// No cached file found, querying tracker directly
results = await queryTracker ( request ) ;
// Cached file didn't exist for our query, writing it right now !
2017-08-13 22:15:11 +00:00
output ( "Writing results to hard drive cache ..." + fileName ) ;
2020-02-10 22:16:19 +00:00
using ( var fileWriter = File . CreateText ( file ) )
2017-08-13 22:15:11 +00:00
{
2020-02-10 22:16:19 +00:00
var serializer = new JsonSerializer ( ) ;
2017-08-13 22:15:11 +00:00
serializer . Serialize ( fileWriter , results ) ;
}
2017-04-15 08:45:10 +00:00
}
return results ;
}
/// <summary>
/// Get Torrents Page from Tracker by Query Provided
/// </summary>
/// <param name="request">URL created by Query Builder</param>
/// <returns>Results from query</returns>
2020-02-10 22:16:19 +00:00
private async Task < string > queryTracker ( string request )
2017-04-15 08:45:10 +00:00
{
// Cache mode not enabled or cached file didn't exist for our query
output ( "\nQuerying tracker for results...." ) ;
// Request our first page
latencyNow ( ) ;
2020-06-11 15:09:27 +00:00
var results = await RequestWithCookiesAndRetryAsync ( request , headers : emulatedBrowserHeaders ) ;
2017-04-15 08:45:10 +00:00
// Return results from tracker
2020-06-09 17:36:57 +00:00
return results . ContentString ;
2017-04-15 08:45:10 +00:00
}
/// <summary>
/// Clean Hard Drive Cache Storage
/// </summary>
/// <param name="force">Force Provider Folder deletion</param>
2017-08-13 22:15:11 +00:00
private void cleanCacheStorage ( bool force = false )
2017-04-15 08:45:10 +00:00
{
// Check cleaning method
2017-07-10 20:58:44 +00:00
if ( force )
2017-04-15 08:45:10 +00:00
{
// Deleting Provider Storage folder and all files recursively
output ( "\nDeleting Provider Storage folder and all files recursively ..." ) ;
2017-07-10 20:58:44 +00:00
2017-04-15 08:45:10 +00:00
// Check if directory exist
2017-08-13 22:15:11 +00:00
if ( System . IO . Directory . Exists ( Directory ) )
2017-04-15 08:45:10 +00:00
{
// Delete storage directory of provider
2017-08-13 22:15:11 +00:00
System . IO . Directory . Delete ( Directory , true ) ;
2017-04-15 08:45:10 +00:00
output ( "-> Storage folder deleted successfully." ) ;
}
else
{
// No directory, so nothing to do
output ( "-> No Storage folder found for this provider !" ) ;
}
}
else
{
2017-08-13 22:15:11 +00:00
var i = 0 ;
2017-04-15 08:45:10 +00:00
// Check if there is file older than ... and delete them
output ( "\nCleaning Provider Storage folder... in progress." ) ;
2017-08-13 22:15:11 +00:00
System . IO . Directory . GetFiles ( Directory )
. Select ( f = > new FileInfo ( f ) )
2017-04-15 08:45:10 +00:00
. Where ( f = > f . LastAccessTime < DateTime . Now . AddMilliseconds ( - Convert . ToInt32 ( ConfigData . HardDriveCacheKeepTime . Value ) ) )
. ToList ( )
2017-07-10 20:58:44 +00:00
. ForEach ( f = >
{
2017-04-15 08:45:10 +00:00
output ( "Deleting cached file << " + f . Name + " >> ... done." ) ;
f . Delete ( ) ;
i + + ;
2017-07-10 20:58:44 +00:00
} ) ;
2017-04-15 08:45:10 +00:00
// Inform on what was cleaned during process
2017-07-10 20:58:44 +00:00
if ( i > 0 )
{
2017-04-15 08:45:10 +00:00
output ( "-> Deleted " + i + " cached files during cleaning." ) ;
}
2017-07-10 20:58:44 +00:00
else
{
2017-04-15 08:45:10 +00:00
output ( "-> Nothing deleted during cleaning." ) ;
}
}
}
/// <summary>
/// Generate a random fake latency to avoid detection on tracker side
/// </summary>
private void latencyNow ( )
{
// Need latency ?
2017-07-10 20:58:44 +00:00
if ( Latency )
2017-04-15 08:45:10 +00:00
{
// Generate a random value in our range
var random = new Random ( DateTime . Now . Millisecond ) ;
2020-02-10 22:16:19 +00:00
var waiting = random . Next ( Convert . ToInt32 ( ConfigData . LatencyStart . Value ) , Convert . ToInt32 ( ConfigData . LatencyEnd . Value ) ) ;
2017-04-15 08:45:10 +00:00
output ( "\nLatency Faker => Sleeping for " + waiting + " ms..." ) ;
// Sleep now...
System . Threading . Thread . Sleep ( waiting ) ;
}
}
/// <summary>
/// Find torrent rows in search pages
/// </summary>
2020-03-01 01:46:34 +00:00
/// <returns>List of rows</returns>
private IHtmlCollection < IElement > findTorrentRows ( IHtmlDocument dom ) = >
dom . QuerySelectorAll ( ".torrent_table > tbody > tr:not(.colhead)" ) ;
2017-04-15 08:45:10 +00:00
/// <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
2017-07-10 20:58:44 +00:00
if ( DevMode )
2017-04-15 08:45:10 +00:00
{
// Output message to console
Console . WriteLine ( message ) ;
}
else
{
// Send message to logger with level
switch ( level )
{
default :
goto case "debug" ;
case "debug" :
// Only if Debug Level Enabled on Jackett
2017-11-05 09:42:03 +00:00
if ( logger . IsDebugEnabled )
2017-04-15 08:45:10 +00:00
{
logger . Debug ( message ) ;
}
break ;
2017-10-29 06:21:18 +00:00
2017-04-15 08:45:10 +00:00
case "info" :
logger . Info ( message ) ;
break ;
2017-10-29 06:21:18 +00:00
2017-04-15 08:45:10 +00:00
case "error" :
logger . Error ( message ) ;
break ;
}
}
}
/// <summary>
/// Validate Config entered by user on Jackett
/// </summary>
private void validateConfig ( )
{
output ( "\nValidating Settings ... \n" ) ;
// Check Username Setting
if ( string . IsNullOrEmpty ( ConfigData . Username . Value ) )
{
throw new ExceptionWithConfigData ( "You must provide a username for this tracker to login !" , ConfigData ) ;
}
else
{
output ( "Validated Setting -- Username (auth) => " + ConfigData . Username . Value . ToString ( ) ) ;
}
// 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 ) ;
}
else
{
output ( "Validated Setting -- Password (auth) => " + ConfigData . Password . Value . ToString ( ) ) ;
}
// Check Max Page Setting
if ( ! string . IsNullOrEmpty ( ConfigData . Pages . Value ) )
{
try
{
output ( "Validated Setting -- 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 )
{
output ( "\nValidated Setting -- Latency Simulation enabled" ) ;
// Check Latency Start Setting
if ( ! string . IsNullOrEmpty ( ConfigData . LatencyStart . Value ) )
{
try
{
output ( "Validated Setting -- 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 ( "Validated Setting -- 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 )
{
output ( "\nValidated Setting -- Browser Simulation enabled" ) ;
// Check ACCEPT header Setting
if ( string . IsNullOrEmpty ( ConfigData . HeaderAccept . Value ) )
{
throw new ExceptionWithConfigData ( "Browser Simulation enabled, Please enter an ACCEPT header !" , ConfigData ) ;
}
else
{
output ( "Validated Setting -- ACCEPT (header) => " + ConfigData . HeaderAccept . Value . ToString ( ) ) ;
}
// Check ACCEPT-LANG header Setting
if ( string . IsNullOrEmpty ( ConfigData . HeaderAcceptLang . Value ) )
{
throw new ExceptionWithConfigData ( "Browser Simulation enabled, Please enter an ACCEPT-LANG header !" , ConfigData ) ;
}
else
{
output ( "Validated Setting -- ACCEPT-LANG (header) => " + ConfigData . HeaderAcceptLang . Value . ToString ( ) ) ;
}
// Check USER-AGENT header Setting
if ( string . IsNullOrEmpty ( ConfigData . HeaderUserAgent . Value ) )
{
throw new ExceptionWithConfigData ( "Browser Simulation enabled, Please enter an USER-AGENT header !" , ConfigData ) ;
}
else
{
output ( "Validated Setting -- USER-AGENT (header) => " + ConfigData . HeaderUserAgent . Value . ToString ( ) ) ;
}
}
// Check Dev Cache Settings
if ( ConfigData . HardDriveCache . Value = = true )
{
output ( "\nValidated Setting -- DEV Hard Drive Cache enabled" ) ;
// Check if Dev Mode enabled !
if ( ! ConfigData . DevMode . Value )
{
throw new ExceptionWithConfigData ( "Hard Drive is enabled but not in DEV MODE, Please enable DEV MODE !" , ConfigData ) ;
}
// Check Cache Keep Time Setting
if ( ! string . IsNullOrEmpty ( ConfigData . HardDriveCacheKeepTime . Value ) )
{
try
{
output ( "Validated Setting -- Cache Keep Time (ms) => " + Convert . ToInt32 ( ConfigData . HardDriveCacheKeepTime . Value ) ) ;
}
catch ( Exception )
{
throw new ExceptionWithConfigData ( "Please enter a numeric hard drive keep time in ms !" , ConfigData ) ;
}
}
else
{
throw new ExceptionWithConfigData ( "Hard Drive Cache enabled, Please enter a maximum keep time for cache !" , ConfigData ) ;
}
}
else
{
// Delete cache if previously existed
cleanCacheStorage ( true ) ;
}
}
}
2020-02-09 02:35:16 +00:00
}