2020-02-09 02:35:16 +00:00
using System ;
2015-04-19 02:05:38 +00:00
using System.Collections.Generic ;
2020-03-08 11:32:37 +00:00
using System.Collections.Specialized ;
2020-05-03 23:35:52 +00:00
using System.Diagnostics.CodeAnalysis ;
2016-09-03 02:04:08 +00:00
using System.Globalization ;
2015-04-19 02:05:38 +00:00
using System.Linq ;
2016-12-06 13:56:47 +00:00
using System.Text ;
2015-11-18 11:55:00 +00:00
using System.Text.RegularExpressions ;
2015-04-19 02:05:38 +00:00
using System.Threading.Tasks ;
2016-09-03 02:04:08 +00:00
using AngleSharp.Dom ;
2021-03-14 21:19:48 +00:00
using AngleSharp.Html.Dom ;
2019-01-20 00:09:27 +00:00
using AngleSharp.Html.Parser ;
2018-03-10 08:05:56 +00:00
using Jackett.Common.Models ;
using Jackett.Common.Models.IndexerConfig ;
using Jackett.Common.Services.Interfaces ;
using Jackett.Common.Utils ;
2020-06-11 15:09:27 +00:00
using Jackett.Common.Utils.Clients ;
2017-10-29 06:21:18 +00:00
using Newtonsoft.Json.Linq ;
2021-03-14 21:19:48 +00:00
using System.Web ;
2017-10-29 06:21:18 +00:00
using NLog ;
2021-03-14 21:19:48 +00:00
using static Jackett . Common . Models . IndexerConfig . ConfigurationData ;
2017-04-15 08:45:10 +00:00
2018-03-10 08:05:56 +00:00
namespace Jackett.Common.Indexers
2015-04-19 02:05:38 +00:00
{
2021-03-14 21:19:48 +00:00
// This tracker uses a hybrid Luminance (based on GazelleTracker)
2020-05-03 23:35:52 +00:00
[ExcludeFromCodeCoverage]
2017-07-10 20:58:44 +00:00
public class MoreThanTV : BaseWebIndexer
2015-04-19 02:05:38 +00:00
{
2020-09-30 20:58:37 +00:00
public override string [ ] LegacySiteLinks { get ; protected set ; } = {
2020-10-19 21:19:10 +00:00
"https://www.morethan.tv/"
2020-09-30 20:58:37 +00:00
} ;
2021-03-14 21:19:48 +00:00
private string LoginUrl = > SiteLink + "login" ;
private string BrowseUrl = > SiteLink + "torrents.php" ;
private string DetailsUrl = > SiteLink + "details.php" ;
private string _sort ;
private string _order ;
2015-04-19 02:05:38 +00:00
2017-10-29 06:21:18 +00:00
private ConfigurationDataBasicLogin ConfigData = > ( ConfigurationDataBasicLogin ) configData ;
2015-08-03 21:38:45 +00:00
2021-03-14 21:19:48 +00:00
private readonly Dictionary < string , string > _emulatedBrowserHeaders = new Dictionary < string , string > ( ) ;
2020-12-11 22:14:21 +00:00
public MoreThanTV ( IIndexerConfigurationService configService , WebClient c , Logger l , IProtectionService ps ,
ICacheService cs )
2020-05-11 19:59:28 +00:00
: base ( id : "morethantv" ,
name : "MoreThanTV" ,
description : "Private torrent tracker for TV / MOVIES, and the internal tracker for the release group DRACULA." ,
2020-09-30 20:58:37 +00:00
link : "https://www.morethantv.me/" ,
2020-10-18 17:26:22 +00:00
caps : new TorznabCapabilities
{
2020-10-18 20:47:36 +00:00
TvSearchParams = new List < TvSearchParam >
{
2021-03-14 21:19:48 +00:00
TvSearchParam . Q
2020-10-18 20:47:36 +00:00
} ,
MovieSearchParams = new List < MovieSearchParam >
{
2021-03-14 21:19:48 +00:00
MovieSearchParam . Q
2020-10-18 20:47:36 +00:00
}
2020-10-18 17:26:22 +00:00
} ,
2020-05-11 19:59:28 +00:00
configService : configService ,
client : c ,
logger : l ,
p : ps ,
2020-12-11 22:14:21 +00:00
cacheService : cs ,
2020-05-11 19:59:28 +00:00
configData : new ConfigurationDataBasicLogin ( ) )
2015-04-19 02:05:38 +00:00
{
2017-11-05 09:42:03 +00:00
Encoding = Encoding . UTF8 ;
2016-12-09 17:20:58 +00:00
Language = "en-us" ;
2017-01-27 15:57:32 +00:00
Type = "private" ;
2020-03-08 11:32:37 +00:00
2021-03-16 23:29:26 +00:00
var sort = new SingleSelectConfigurationItem ( "Sort requested from site" , new Dictionary < string , string >
2021-03-14 21:19:48 +00:00
{
{ "time" , "time" } ,
{ "size" , "size" } ,
{ "snatched" , "snatched" } ,
{ "seeders" , "seeders" } ,
{ "leechers" , "leechers" } ,
} )
2021-03-16 23:29:26 +00:00
{ Value = "time" } ;
2021-03-14 21:19:48 +00:00
configData . AddDynamic ( "sort" , sort ) ;
2021-03-16 23:29:26 +00:00
var order = new SingleSelectConfigurationItem ( "Order requested from site" , new Dictionary < string , string >
2021-03-14 21:19:48 +00:00
{
{ "desc" , "desc" } ,
{ "asc" , "asc" }
} )
2021-03-16 23:29:26 +00:00
{ Value = "desc" } ;
2021-03-14 21:19:48 +00:00
configData . AddDynamic ( "order" , order ) ;
2020-10-18 17:26:22 +00:00
AddCategoryMapping ( 1 , TorznabCatType . Movies ) ;
AddCategoryMapping ( 2 , TorznabCatType . TV ) ;
2015-04-19 02:05:38 +00:00
}
2021-03-14 21:19:48 +00:00
/// <summary>
/// Parse and Return CSRF token
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
private string GetToken ( string content )
{
try
{
var parser = new HtmlParser ( ) ;
var dom = parser . ParseDocument ( content . Trim ( ) ) ;
return dom . QuerySelector < IHtmlInputElement > ( "input[name=\"token\"]" ) . Value ;
}
catch ( Exception e )
{
throw new Exception ( "Token Could not be parsed from Response, Error?" , e ) ;
}
}
/// <summary>
/// Emulate browser headers -- REQUIRED
/// </summary>
private void SetRequestHeaders ( )
{
_emulatedBrowserHeaders . Clear ( ) ;
_emulatedBrowserHeaders . Add ( "referer" , SiteLink ) ;
_emulatedBrowserHeaders . Add ( "Upgrade-Insecure-Requests" , "1" ) ;
_emulatedBrowserHeaders . Add ( "User-Agent" , "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36" ) ;
}
2017-06-28 05:31:38 +00:00
public override async Task < IndexerConfigurationStatus > ApplyConfiguration ( JToken configJson )
2015-04-19 02:05:38 +00:00
{
2017-01-02 20:39:28 +00:00
LoadValuesFromJson ( configJson ) ;
2021-03-14 21:19:48 +00:00
2015-04-23 06:44:21 +00:00
var pairs = new Dictionary < string , string > {
2016-09-03 02:04:08 +00:00
{ "username" , ConfigData . Username . Value } ,
{ "password" , ConfigData . Password . Value } ,
2021-03-14 21:19:48 +00:00
{ "submit" , "login" } ,
{ "keeplogged" , "1" } ,
{ "cinfo" , "3440|1440|24|360" }
2015-08-03 21:38:45 +00:00
} ;
2016-09-03 02:04:08 +00:00
2021-03-14 21:19:48 +00:00
SetRequestHeaders ( ) ;
// Fetch CSRF token
var preRequest = await RequestWithCookiesAndRetryAsync ( LoginUrl , referer : SiteLink , headers : _emulatedBrowserHeaders ) ;
// Check if user is logged in. /login redirects to / if so)
if ( preRequest . IsRedirect )
{
await FollowIfRedirect ( preRequest , SiteLink , null , preRequest . Cookies , true ) ;
}
// sid was not set after redirect, attempt to log in again
if ( ! preRequest . Cookies . Contains ( "sid=" ) )
2015-04-19 02:05:38 +00:00
{
2021-03-14 21:19:48 +00:00
string token = null ;
2018-03-14 14:16:20 +00:00
2021-03-14 21:19:48 +00:00
try
{
token = GetToken ( preRequest . ContentString ) ;
}
catch ( Exception )
{
2021-03-17 23:37:07 +00:00
var errorMessage = ParseErrorMessage ( preRequest ) ;
2021-03-14 21:19:48 +00:00
throw new ExceptionWithConfigData ( errorMessage , configData ) ;
}
// Add CSRF Token to payload
pairs . Add ( "token" , token ) ;
var response = await RequestLoginAndFollowRedirect ( LoginUrl , pairs , preRequest . Cookies , true , null , SiteLink , headers : _emulatedBrowserHeaders ) ;
await ConfigureIfOK ( response . Cookies , response . Cookies . Contains ( "sid=" ) , ( ) = >
{
2021-03-17 23:37:07 +00:00
// Couldn't find "sid" cookie, so check for error
var parser = new HtmlParser ( ) ;
2021-03-14 21:19:48 +00:00
var dom = parser . ParseDocument ( response . ContentString ) ;
var errorMessage = dom . QuerySelector ( ".flash.error" ) . TextContent . Trim ( ) ;
throw new ExceptionWithConfigData ( errorMessage , configData ) ;
} ) ;
}
2015-08-22 20:57:13 +00:00
return IndexerConfigurationStatus . RequiresTesting ;
2015-04-19 02:05:38 +00:00
}
2017-07-03 05:15:47 +00:00
protected override async Task < IEnumerable < ReleaseInfo > > PerformQuery ( TorznabQuery query )
2015-04-19 02:05:38 +00:00
{
2016-09-03 02:04:08 +00:00
var releases = new List < ReleaseInfo > ( ) ;
2021-03-14 21:19:48 +00:00
var searchQuery = query . GetQueryString ( ) ;
searchQuery = searchQuery . Replace ( "Marvels" , "Marvel" ) ; // strip 's for better results
var newSearchQuery = Regex . Replace ( searchQuery , @"(S\d{2})$" , "$1*" ) ; // If we're just seaching for a season (no episode) append an * to include all episodes of that season.
await GetReleasesAsync ( releases , query , newSearchQuery ) ;
// Always search for torrent groups (complete seasons) too
var seasonMatch = new Regex ( @".*\s[Ss]{1}\d{2}([Ee]{1}\d{2,3})?$" ) . Match ( searchQuery ) ;
if ( seasonMatch . Success )
2015-08-13 22:41:21 +00:00
{
2021-03-14 21:19:48 +00:00
newSearchQuery = Regex . Replace ( searchQuery , @"[Ss]{1}\d{2}([Ee]{1}\d{2,3})?" , $"Season {query.Season}" ) ;
await GetReleasesAsync ( releases , query , newSearchQuery ) ;
2015-08-13 22:41:21 +00:00
}
2016-09-03 02:04:08 +00:00
return releases ;
2015-04-19 02:05:38 +00:00
}
2021-03-14 21:19:48 +00:00
public override void LoadValuesFromJson ( JToken jsonConfig , bool useProtectionService = false )
{
base . LoadValuesFromJson ( jsonConfig , useProtectionService ) ;
2021-03-16 23:29:26 +00:00
var sort = ( SingleSelectConfigurationItem ) configData . GetDynamic ( "sort" ) ;
2021-03-14 21:19:48 +00:00
_sort = sort ! = null ? sort . Value : "time" ;
2021-03-16 23:29:26 +00:00
var order = ( SingleSelectConfigurationItem ) configData . GetDynamic ( "order" ) ;
2021-03-14 21:19:48 +00:00
_order = order ! = null & & order . Value . Equals ( "asc" ) ? order . Value : "desc" ;
}
2020-03-08 11:32:37 +00:00
private string GetTorrentSearchUrl ( TorznabQuery query , string searchQuery )
2015-04-19 02:05:38 +00:00
{
2020-03-08 11:32:37 +00:00
var qc = new NameValueCollection
{
2021-03-14 21:19:48 +00:00
{ "order_by" , _sort } ,
{ "order_way" , _order } ,
{ "action" , "advanced" } ,
{ "sizetype" , "gb" } ,
{ "sizerange" , "0.01" } ,
{ "title" , searchQuery }
2020-03-08 11:32:37 +00:00
} ;
2015-07-29 17:47:51 +00:00
2020-03-08 11:32:37 +00:00
if ( query . Categories . Contains ( TorznabCatType . Movies . ID ) )
2021-03-17 23:37:07 +00:00
{
qc . Add ( "filter_cat[1]" , "1" ) ; // HD Movies
qc . Add ( "filter_cat[2]" , "1" ) ; // SD Movies
}
2020-03-08 11:32:37 +00:00
if ( query . Categories . Contains ( TorznabCatType . TV . ID ) )
2021-03-17 23:37:07 +00:00
{
qc . Add ( "filter_cat[3]" , "1" ) ; // HD EPISODE
qc . Add ( "filter_cat[4]" , "1" ) ; // SD Episode
qc . Add ( "filter_cat[5]" , "1" ) ; // HD Season
qc . Add ( "filter_cat[6]" , "1" ) ; // SD Season
}
2015-11-18 11:55:00 +00:00
2021-03-14 21:19:48 +00:00
return BrowseUrl + "?" + qc . GetQueryString ( ) ;
2016-09-03 02:04:08 +00:00
}
2021-03-14 21:19:48 +00:00
private async Task GetReleasesAsync ( ICollection < ReleaseInfo > releases , TorznabQuery query , string searchQuery )
2016-09-03 02:04:08 +00:00
{
2020-03-08 11:32:37 +00:00
var searchUrl = GetTorrentSearchUrl ( query , searchQuery ) ;
2020-06-11 15:09:27 +00:00
var response = await RequestWithCookiesAndRetryAsync ( searchUrl ) ;
2017-04-15 08:45:10 +00:00
if ( response . IsRedirect )
{
2021-03-14 21:19:48 +00:00
// re-login
2017-04-15 08:45:10 +00:00
await ApplyConfiguration ( null ) ;
2020-06-11 15:09:27 +00:00
response = await RequestWithCookiesAndRetryAsync ( searchUrl ) ;
2017-03-03 17:52:19 +00:00
}
2015-04-19 02:05:38 +00:00
2015-07-18 20:35:02 +00:00
try
{
2016-09-03 02:04:08 +00:00
var parser = new HtmlParser ( ) ;
2020-06-09 17:36:57 +00:00
var document = parser . ParseDocument ( response . ContentString ) ;
2021-03-14 21:19:48 +00:00
var torrents = document . QuerySelectorAll ( "#torrent_table > tbody > tr.torrent" ) ;
var movies = new [ ] { "movie" } ;
var tv = new [ ] { "season" , "episode" } ;
2016-09-03 02:04:08 +00:00
2021-03-14 21:19:48 +00:00
// Loop through all torrents checking for groups
2016-09-03 02:04:08 +00:00
foreach ( var torrent in torrents )
{
// Parse required data
2021-03-14 21:19:48 +00:00
var torrentGroup = torrent . QuerySelectorAll ( "table a[href^=\"/torrents.php?action=download\"]" ) ;
foreach ( var downloadAnchor in torrentGroup )
{
var title = downloadAnchor . ParentElement . ParentElement . ParentElement . TextContent . Trim ( ) ;
title = CleanUpTitle ( title ) ;
var category = torrent . QuerySelector ( ".cats_col div" ) . GetAttribute ( "title" ) ;
// default to Other
2021-03-17 23:37:07 +00:00
var categoryId = TorznabCatType . Other . ID ;
2021-03-14 21:19:48 +00:00
if ( movies . Any ( category . Contains ) )
categoryId = TorznabCatType . Movies . ID ;
else if ( tv . Any ( category . Contains ) )
categoryId = TorznabCatType . TV . ID ;
releases . Add ( GetReleaseInfo ( torrent , downloadAnchor , title , categoryId ) ) ;
}
2015-05-04 04:12:14 +00:00
}
2015-07-18 20:35:02 +00:00
}
catch ( Exception ex )
{
2020-06-09 17:36:57 +00:00
OnParseError ( response . ContentString , ex ) ;
2015-04-19 02:05:38 +00:00
}
2016-09-03 02:04:08 +00:00
}
2015-04-19 02:05:38 +00:00
2021-03-14 21:19:48 +00:00
/// <summary>
/// Gather Release info from torrent table. Target using css
/// </summary>
/// <param name="row"></param>
/// <param name="downloadAnchor"></param>
/// <param name="title"></param>
/// <param name="category"></param>
/// <returns></returns>
2016-09-03 02:04:08 +00:00
private ReleaseInfo GetReleaseInfo ( IElement row , IElement downloadAnchor , string title , int category )
{
2021-03-14 21:19:48 +00:00
// count from bottom
const int FILES_COL = 8 ;
/*const int COMMENTS_COL = 7;*/
const int DATE_COL = 6 ;
const int FILESIZE_COL = 5 ;
const int SNATCHED_COL = 4 ;
const int SEEDS_COL = 3 ;
const int LEECHERS_COL = 2 ;
/*const int USER_COL = 1;*/
2021-03-17 23:37:07 +00:00
var downloadAnchorHref = ( downloadAnchor as IHtmlAnchorElement ) . Href ;
2021-03-14 21:19:48 +00:00
var queryParams = HttpUtility . ParseQueryString ( downloadAnchorHref , Encoding . UTF8 ) ;
2021-03-17 23:37:07 +00:00
var torrentId = queryParams [ "id" ] ;
2021-03-14 21:19:48 +00:00
var qFiles = row . QuerySelector ( "td:nth-last-child(" + FILES_COL + ")" ) . TextContent ;
var fileCount = ParseUtil . CoerceLong ( qFiles ) ;
var qPublishDate = row . QuerySelector ( "td:nth-last-child(" + DATE_COL + ") .time" ) . Attributes [ "title" ] . Value ;
2020-03-10 23:53:12 +00:00
var publishDate = DateTime . ParseExact ( qPublishDate , "MMM dd yyyy, HH:mm" , CultureInfo . InvariantCulture , DateTimeStyles . AssumeUniversal ) . ToLocalTime ( ) ;
2020-11-07 23:43:33 +00:00
var qPoster = row . QuerySelector ( "div.tp-banner img" ) ? . GetAttribute ( "src" ) ;
2021-03-14 21:19:48 +00:00
var poster = ( qPoster ! = null & & ! qPoster . Contains ( "caticons" ) ) ? new Uri ( qPoster ) : null ;
2020-03-10 23:53:12 +00:00
var description = row . QuerySelector ( "div.tags" ) ? . TextContent . Trim ( ) ;
2021-03-14 21:19:48 +00:00
var fileSize = row . QuerySelector ( "td:nth-last-child(" + FILESIZE_COL + ")" ) . TextContent . Trim ( ) ;
var snatched = row . QuerySelector ( "td:nth-last-child(" + SNATCHED_COL + ")" ) . TextContent . Trim ( ) ;
var seeds = row . QuerySelector ( "td:nth-last-child(" + SEEDS_COL + ")" ) . TextContent . Trim ( ) ;
var leechs = row . QuerySelector ( "td:nth-last-child(" + LEECHERS_COL + ")" ) . TextContent . Trim ( ) ;
2020-03-10 23:53:12 +00:00
2021-03-14 21:19:48 +00:00
if ( fileSize . Length < = 0 | | snatched . Length < = 0 | | seeds . Length < = 0 | | leechs . Length < = 0 )
{
// Size (xx.xx GB[ (Max)]) Snatches (xx) Seeders (xx) Leechers (xx)
throw new Exception ( $"We expected 4 torrent datas." ) ;
}
2016-09-03 02:04:08 +00:00
2021-03-17 23:37:07 +00:00
var size = ReleaseInfo . GetBytes ( fileSize ) ;
var grabs = int . Parse ( snatched , NumberStyles . AllowThousands , CultureInfo . InvariantCulture ) ;
var seeders = int . Parse ( seeds , NumberStyles . AllowThousands , CultureInfo . InvariantCulture ) ;
var leechers = int . Parse ( leechs , NumberStyles . AllowThousands , CultureInfo . InvariantCulture ) ;
2021-03-14 21:19:48 +00:00
var detailsUri = new Uri ( DetailsUrl + "?torrentid=" + torrentId ) ;
var downloadLink = new Uri ( BrowseUrl + "?action=download&id=" + torrentId ) ;
2020-11-07 23:43:33 +00:00
2016-09-03 02:04:08 +00:00
return new ReleaseInfo
{
Title = title ,
2021-03-14 21:19:48 +00:00
Category = new List < int > { category } ,
Link = downloadLink ,
2016-09-03 02:04:08 +00:00
PublishDate = publishDate ,
2020-11-07 23:43:33 +00:00
Poster = poster ,
2020-03-10 23:53:12 +00:00
Description = description ,
2016-09-03 02:04:08 +00:00
Seeders = seeders ,
2017-08-05 01:20:50 +00:00
Peers = seeders + leechers ,
2021-03-14 21:19:48 +00:00
Files = fileCount ,
2016-09-03 02:04:08 +00:00
Size = size ,
2016-10-27 07:35:31 +00:00
Grabs = grabs ,
2021-03-14 21:19:48 +00:00
Guid = downloadLink ,
Details = detailsUri ,
2016-10-27 07:35:31 +00:00
DownloadVolumeFactor = 0 , // ratioless tracker
UploadVolumeFactor = 1
2016-09-03 02:04:08 +00:00
} ;
}
2021-03-14 21:19:48 +00:00
/// <summary>
/// Parse Error Messages from using CSS classes
/// </summary>
/// <param name="response"></param>
/// <returns></returns>
private string ParseErrorMessage ( WebResult response )
{
var parser = new HtmlParser ( ) ;
var dom = parser . ParseDocument ( response . ContentString ) ;
2021-03-17 23:37:07 +00:00
var errorMessage = response . Status = = System . Net . HttpStatusCode . Forbidden
? dom . QuerySelector ( ".time" ) . Parent . TextContent . Trim ( )
: dom . QuerySelector ( ".flash.error" ) . TextContent . Trim ( ) ;
2021-03-14 21:19:48 +00:00
return errorMessage ;
}
/// <summary>
/// Clean Up any title stuff
/// </summary>
/// <param name="title"></param>
/// <returns></returns>
private string CleanUpTitle ( string title )
{
return title
. Replace ( "." , " " )
. Replace ( "4K" , "2160p" ) ; // sonarr cleanup
}
2015-04-19 02:05:38 +00:00
}
}