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 ;
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 ;
using NLog ;
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
{
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
} ;
2016-09-03 02:04:08 +00:00
private string LoginUrl = > SiteLink + "login.php" ;
private string SearchUrl = > SiteLink + "ajax.php?action=browse&searchstr=" ;
private string DownloadUrl = > SiteLink + "torrents.php?action=download&id=" ;
2020-11-08 02:11:27 +00:00
private string DetailsUrl = > SiteLink + "torrents.php?torrentid=" ;
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
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 >
{
TvSearchParam . Q , TvSearchParam . Season , TvSearchParam . Ep
} ,
MovieSearchParams = new List < MovieSearchParam >
{
MovieSearchParam . Q , MovieSearchParam . ImdbId
}
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
2020-10-18 17:26:22 +00:00
AddCategoryMapping ( 1 , TorznabCatType . Movies ) ;
AddCategoryMapping ( 2 , TorznabCatType . TV ) ;
AddCategoryMapping ( 3 , TorznabCatType . Other ) ;
2015-04-19 02:05:38 +00:00
}
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 ) ;
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 } ,
2015-08-03 21:38:45 +00:00
{ "login" , "Log in" } ,
{ "keeplogged" , "1" }
} ;
2020-06-11 15:09:27 +00:00
var preRequest = await RequestWithCookiesAndRetryAsync ( LoginUrl , string . Empty ) ;
2015-10-12 18:58:40 +00:00
var result = await RequestLoginAndFollowRedirect ( LoginUrl , pairs , preRequest . Cookies , true , SearchUrl , SiteLink ) ;
2016-09-03 02:04:08 +00:00
2020-06-09 17:36:57 +00:00
await ConfigureIfOK ( result . Cookies , result . ContentString ! = null & & result . ContentString . Contains ( "status\":\"success\"" ) , ( ) = >
2015-04-19 02:05:38 +00:00
{
2020-06-09 17:36:57 +00:00
if ( result . ContentString . Contains ( "Your IP address has been banned." ) )
2018-03-14 14:16:20 +00:00
throw new ExceptionWithConfigData ( "Your IP address has been banned." , ConfigData ) ;
2020-02-29 18:53:36 +00:00
var parser = new HtmlParser ( ) ;
2020-06-09 17:36:57 +00:00
var dom = parser . ParseDocument ( result . ContentString ) ;
2020-02-29 18:53:36 +00:00
foreach ( var element in dom . QuerySelectorAll ( "#loginform > table" ) )
element . Remove ( ) ;
var errorMessage = dom . QuerySelector ( "#loginform" ) . TextContent . Trim ( ) . Replace ( "\n\t" , " " ) ;
2016-09-03 02:04:08 +00:00
throw new ExceptionWithConfigData ( errorMessage , ConfigData ) ;
2015-07-27 00:03:51 +00:00
} ) ;
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 > ( ) ;
2020-03-08 11:32:37 +00:00
if ( ! string . IsNullOrWhiteSpace ( query . ImdbID ) )
await GetReleases ( releases , query , query . GetQueryString ( ) ) ;
else
2015-08-13 22:41:21 +00:00
{
2020-03-08 11:32:37 +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.
2017-07-08 13:23:36 +00:00
await GetReleases ( releases , query , newSearchQuery ) ;
2020-03-08 11:32:37 +00:00
// 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 )
{
newSearchQuery = Regex . Replace ( searchQuery , @"[Ss]{1}\d{2}([Ee]{1}\d{2,3})?" , $"Season {query.Season}" ) ;
await GetReleases ( 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
}
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
{
{ "tags_type" , "1" } ,
{ "order_by" , "time" } ,
{ "order_way" , "desc" } ,
{ "group_results" , "1" } ,
{ "action" , "basic" } ,
{ "searchsubmit" , "1" }
} ;
2015-07-29 17:47:51 +00:00
2020-03-08 11:32:37 +00:00
if ( ! string . IsNullOrWhiteSpace ( query . ImdbID ) )
qc . Add ( "description" , query . ImdbID ) ;
else
qc . Add ( "searchstr" , searchQuery ) ;
2016-09-03 02:04:08 +00:00
2020-03-08 11:32:37 +00:00
if ( query . Categories . Contains ( TorznabCatType . Movies . ID ) )
qc . Add ( "filter_cat[1]" , "1" ) ;
if ( query . Categories . Contains ( TorznabCatType . TV . ID ) )
qc . Add ( "filter_cat[2]" , "1" ) ;
if ( query . Categories . Contains ( TorznabCatType . Other . ID ) )
qc . Add ( "filter_cat[3]" , "1" ) ;
2015-11-18 11:55:00 +00:00
2020-03-08 11:32:37 +00:00
return SiteLink + "torrents.php?" + qc . GetQueryString ( ) ;
2016-09-03 02:04:08 +00:00
}
private async Task GetReleases ( ICollection < ReleaseInfo > releases , TorznabQuery query , string searchQuery )
{
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 )
{
// re login
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 ) ;
2016-09-03 02:04:08 +00:00
var groups = document . QuerySelectorAll ( ".torrent_table > tbody > tr.group" ) ;
var torrents = document . QuerySelectorAll ( ".torrent_table > tbody > tr.torrent" ) ;
// Loop through all torrent (season) groups
foreach ( var group in groups )
2015-04-23 06:44:21 +00:00
{
2016-09-03 02:04:08 +00:00
var showName = group . QuerySelector ( ".tp-showname a" ) . InnerHtml . Replace ( "(" , "" ) . Replace ( ")" , "" ) . Replace ( ' ' , '.' ) ;
var season = group . QuerySelector ( ".big_info a" ) . InnerHtml ;
2017-10-17 17:10:53 +00:00
var seasonNumber = SeasonToNumber ( season ) ;
if ( seasonNumber ! = null & & query . Season > 0 & & seasonNumber ! = query . Season ) // filter unwanted seasons
continue ;
var seasonTag = SeasonNumberToShortSeason ( seasonNumber ) ? ? season ;
2016-09-03 02:04:08 +00:00
// Loop through all group items
var previousElement = group ;
var qualityEdition = string . Empty ;
while ( true )
2015-04-19 02:05:38 +00:00
{
2016-09-03 02:04:08 +00:00
var groupItem = previousElement . NextElementSibling ;
2015-05-04 04:12:14 +00:00
2020-02-09 02:35:16 +00:00
if ( groupItem = = null )
break ;
2015-05-04 04:12:14 +00:00
2016-09-03 02:04:08 +00:00
if ( ! groupItem . ClassList [ 0 ] . Equals ( "group_torrent" ) | |
2020-02-09 02:35:16 +00:00
! groupItem . ClassList [ 1 ] . StartsWith ( "groupid_" ) )
break ;
2015-11-18 11:55:00 +00:00
2016-09-03 02:04:08 +00:00
// Found a new edition
if ( groupItem . ClassList [ 2 ] . Equals ( "edition" ) )
qualityEdition = groupItem . QuerySelector ( ".edition_info strong" ) . TextContent . Split ( '/' ) [ 1 ] . Trim ( ) ;
else if ( groupItem . ClassList [ 2 ] . StartsWith ( "edition_" ) )
{
2020-02-09 02:35:16 +00:00
if ( qualityEdition . Equals ( string . Empty ) )
break ;
2016-09-03 02:04:08 +00:00
// Parse required data
var downloadAnchor = groupItem . QuerySelectorAll ( "td a" ) . Last ( ) ;
var qualityData = downloadAnchor . InnerHtml . Split ( '/' ) ;
2019-05-26 07:28:52 +00:00
switch ( qualityData . Length )
{
case 0 :
Array . Resize ( ref qualityData , 2 ) ;
qualityData [ 0 ] = " " ;
qualityData [ 1 ] = " " ;
break ;
case 1 :
Array . Resize ( ref qualityData , 2 ) ;
qualityData [ 1 ] = " " ;
break ;
}
2019-06-13 21:21:03 +00:00
// Replace 4K quality tag with 2160p, so Sonarr etc. can properly parse it
qualityData [ 1 ] = qualityData [ 1 ] . Replace ( "4K" , "2160p" ) ;
2016-09-03 02:04:08 +00:00
// Build title
var title = string . Join ( "." , new List < string >
{
showName ,
2017-10-17 17:10:53 +00:00
seasonTag ,
2016-09-03 02:04:08 +00:00
qualityData [ 1 ] . Trim ( ) ,
qualityEdition , // Audio quality should be after this one. Unobtainable at the moment.
$"{qualityData[0].Trim()}-MTV"
2017-04-15 08:45:10 +00:00
} ) ;
2016-09-03 02:04:08 +00:00
releases . Add ( GetReleaseInfo ( groupItem , downloadAnchor , title , TorznabCatType . TV . ID ) ) ;
}
else
break ;
previousElement = groupItem ;
}
}
// Loop through all torrents
foreach ( var torrent in torrents )
{
// Parse required data
var downloadAnchor = torrent . QuerySelector ( ".big_info > .group_info > a" ) ;
var title = downloadAnchor . TextContent ;
int category ;
var categories = torrent . QuerySelector ( ".cats_col div" ) . ClassList ;
if ( categories . Contains ( "cats_tv" ) )
category = TorznabCatType . TV . ID ;
else if ( categories . Contains ( "cats_movies" ) )
category = TorznabCatType . Movies . ID ;
2017-04-17 04:22:00 +00:00
else if ( categories . Contains ( "cats_other" ) )
category = TorznabCatType . Other . ID ;
2015-07-18 20:35:02 +00:00
else
2016-09-03 02:04:08 +00:00
throw new Exception ( "Couldn't find category." ) ;
releases . Add ( GetReleaseInfo ( torrent , downloadAnchor , title , category ) ) ;
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
2016-09-03 02:04:08 +00:00
private ReleaseInfo GetReleaseInfo ( IElement row , IElement downloadAnchor , string title , int category )
{
var downloadAnchorHref = downloadAnchor . Attributes [ "href" ] . Value ;
var torrentId = downloadAnchorHref . Substring ( downloadAnchorHref . LastIndexOf ( '=' ) + 1 ) ;
2020-03-10 23:53:12 +00:00
if ( torrentId . Contains ( '#' ) )
torrentId = torrentId . Split ( '#' ) [ 0 ] ;
2016-11-01 07:55:49 +00:00
var qFiles = row . QuerySelector ( "td:nth-last-child(6)" ) ;
var files = ParseUtil . CoerceLong ( qFiles . TextContent ) ;
2020-03-10 23:53:12 +00:00
var qPublishDate = row . QuerySelector ( ".time.tooltip" ) . Attributes [ "title" ] . Value ;
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" ) ;
var poster = ( qPoster ! = null & & ! qPoster . Contains ( "/static/styles/" ) ) ? new Uri ( qPoster ) : null ;
2020-03-10 23:53:12 +00:00
var description = row . QuerySelector ( "div.tags" ) ? . TextContent . Trim ( ) ;
var torrentData = row . QuerySelectorAll ( ".number_column" ) ;
if ( torrentData . Length ! = 4 ) // Size (xx.xx GB[ (Max)]) Snatches (xx) Seeders (xx) Leechers (xx)
2016-09-03 02:04:08 +00:00
throw new Exception ( $"We expected 4 torrent datas, instead we have {torrentData.Length}." ) ;
2016-12-08 06:31:31 +00:00
var size = ReleaseInfo . GetBytes ( torrentData [ 0 ] . TextContent ) ;
2017-08-05 01:20:50 +00:00
var grabs = int . Parse ( torrentData [ 1 ] . TextContent , NumberStyles . AllowThousands , CultureInfo . InvariantCulture ) ;
var seeders = int . Parse ( torrentData [ 2 ] . TextContent , NumberStyles . AllowThousands , CultureInfo . InvariantCulture ) ;
var leechers = int . Parse ( torrentData [ 3 ] . TextContent , NumberStyles . AllowThousands , CultureInfo . InvariantCulture ) ;
2020-11-08 02:11:27 +00:00
var details = new Uri ( DetailsUrl + torrentId ) ;
2020-03-26 22:15:28 +00:00
var link = new Uri ( DownloadUrl + torrentId ) ;
2020-11-07 23:43:33 +00:00
2016-09-03 02:04:08 +00:00
return new ReleaseInfo
{
Title = title ,
2017-02-21 12:07:54 +00:00
Category = new List < int > { category } , // Who seasons movies right
2020-03-26 22:15:28 +00:00
Link = link ,
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 ,
2016-10-27 07:35:31 +00:00
Files = files ,
2016-09-03 02:04:08 +00:00
Size = size ,
2016-10-27 07:35:31 +00:00
Grabs = grabs ,
2020-11-08 02:11:27 +00:00
Guid = details ,
Details = details ,
2016-10-27 07:35:31 +00:00
DownloadVolumeFactor = 0 , // ratioless tracker
UploadVolumeFactor = 1
2016-09-03 02:04:08 +00:00
} ;
}
2017-10-17 17:10:53 +00:00
// Changes "Season 1" to "1"
private static int? SeasonToNumber ( string season )
2016-09-03 02:04:08 +00:00
{
var seasonMatch = new Regex ( @"Season (?<seasonNumber>\d{1,2})" ) . Match ( season ) ;
if ( seasonMatch . Success )
2017-10-17 17:10:53 +00:00
return int . Parse ( seasonMatch . Groups [ "seasonNumber" ] . Value ) ;
2016-09-03 02:04:08 +00:00
2017-10-17 17:10:53 +00:00
return null ;
}
// Changes "1" to "S01"
private static string SeasonNumberToShortSeason ( int? season )
{
if ( season = = null )
return null ;
return $"S{season:00}" ;
2016-09-03 02:04:08 +00:00
}
2015-04-19 02:05:38 +00:00
}
}