using System ;
using System.Collections.Generic ;
using System.Collections.Specialized ;
using System.Net ;
using System.Text ;
using System.Text.RegularExpressions ;
using FluentValidation ;
using NLog ;
using NzbDrone.Common.Http ;
using NzbDrone.Core.Annotations ;
using NzbDrone.Core.Configuration ;
using NzbDrone.Core.Indexers.Exceptions ;
using NzbDrone.Core.IndexerSearch.Definitions ;
using NzbDrone.Core.Messaging.Events ;
using NzbDrone.Core.Parser ;
using NzbDrone.Core.Parser.Model ;
using NzbDrone.Core.Validation ;
namespace NzbDrone.Core.Indexers.Definitions
{
public class SubsPlease : TorrentIndexerBase < SubsPleaseSettings >
{
public override string Name = > "SubsPlease" ;
public override string [ ] IndexerUrls = > new string [ ]
{
"https://subsplease.org/" ,
"https://subsplease.nocensor.space/"
} ;
public override string Language = > "en-us" ;
public override string Description = > "SubsPlease - A better HorribleSubs/Erai replacement" ;
public override Encoding Encoding = > Encoding . UTF8 ;
public override DownloadProtocol Protocol = > DownloadProtocol . Torrent ;
public override IndexerPrivacy Privacy = > IndexerPrivacy . Public ;
public override IndexerCapabilities Capabilities = > SetCapabilities ( ) ;
public SubsPlease ( IHttpClient httpClient , IEventAggregator eventAggregator , IIndexerStatusService indexerStatusService , IConfigService configService , Logger logger )
: base ( httpClient , eventAggregator , indexerStatusService , configService , logger )
{
}
public override IIndexerRequestGenerator GetRequestGenerator ( )
{
return new SubsPleaseRequestGenerator ( ) { Settings = Settings , Capabilities = Capabilities } ;
}
public override IParseIndexerResponse GetParser ( )
{
return new SubsPleaseParser ( Settings , Capabilities . Categories ) ;
}
private IndexerCapabilities SetCapabilities ( )
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List < TvSearchParam >
{
TvSearchParam . Q , TvSearchParam . Season , TvSearchParam . Ep
} ,
} ;
caps . Categories . AddCategoryMapping ( 1 , NewznabStandardCategory . TVAnime , "Anime" ) ;
return caps ;
}
}
public class SubsPleaseRequestGenerator : IIndexerRequestGenerator
{
public SubsPleaseSettings Settings { get ; set ; }
public IndexerCapabilities Capabilities { get ; set ; }
public SubsPleaseRequestGenerator ( )
{
}
private IEnumerable < IndexerRequest > GetSearchRequests ( string term )
{
var searchUrl = string . Format ( "{0}/api/?" , Settings . BaseUrl . TrimEnd ( '/' ) ) ;
string searchTerm = Regex . Replace ( term , "\\[?SubsPlease\\]?\\s*" , string . Empty , RegexOptions . IgnoreCase ) . Trim ( ) ;
// If the search terms contain a resolution, remove it from the query sent to the API
Match resMatch = Regex . Match ( searchTerm , "\\d{3,4}[p|P]" ) ;
if ( resMatch . Success )
{
searchTerm = searchTerm . Replace ( resMatch . Value , string . Empty ) ;
}
var queryParameters = new NameValueCollection
{
{ "f" , "search" } ,
{ "tz" , "America/New_York" } ,
{ "s" , searchTerm }
} ;
var request = new IndexerRequest ( searchUrl + queryParameters . GetQueryString ( ) , HttpAccept . Json ) ;
yield return request ;
}
private IEnumerable < IndexerRequest > GetRssRequest ( )
{
var searchUrl = string . Format ( "{0}/api/?" , Settings . BaseUrl . TrimEnd ( '/' ) ) ;
var queryParameters = new NameValueCollection
{
{ "f" , "latest" } ,
{ "tz" , "America/New_York" }
} ;
var request = new IndexerRequest ( searchUrl + queryParameters . GetQueryString ( ) , HttpAccept . Json ) ;
yield return request ;
}
public IndexerPageableRequestChain GetSearchRequests ( MovieSearchCriteria searchCriteria )
{
var pageableRequests = new IndexerPageableRequestChain ( ) ;
return pageableRequests ;
}
public IndexerPageableRequestChain GetSearchRequests ( MusicSearchCriteria searchCriteria )
{
var pageableRequests = new IndexerPageableRequestChain ( ) ;
return pageableRequests ;
}
public IndexerPageableRequestChain GetSearchRequests ( TvSearchCriteria searchCriteria )
{
var pageableRequests = new IndexerPageableRequestChain ( ) ;
if ( searchCriteria . RssSearch )
{
pageableRequests . Add ( GetRssRequest ( ) ) ;
}
else
{
pageableRequests . Add ( GetSearchRequests ( string . Format ( "{0}" , searchCriteria . SanitizedTvSearchString ) ) ) ;
}
return pageableRequests ;
}
public IndexerPageableRequestChain GetSearchRequests ( BookSearchCriteria searchCriteria )
{
var pageableRequests = new IndexerPageableRequestChain ( ) ;
return pageableRequests ;
}
public IndexerPageableRequestChain GetSearchRequests ( BasicSearchCriteria searchCriteria )
{
var pageableRequests = new IndexerPageableRequestChain ( ) ;
if ( searchCriteria . RssSearch )
{
pageableRequests . Add ( GetRssRequest ( ) ) ;
}
else
{
pageableRequests . Add ( GetSearchRequests ( string . Format ( "{0}" , searchCriteria . SanitizedSearchTerm ) ) ) ;
}
return pageableRequests ;
}
public Func < IDictionary < string , string > > GetCookies { get ; set ; }
public Action < IDictionary < string , string > , DateTime ? > CookiesUpdater { get ; set ; }
}
public class SubsPleaseParser : IParseIndexerResponse
{
private readonly SubsPleaseSettings _settings ;
private readonly IndexerCapabilitiesCategories _categories ;
public SubsPleaseParser ( SubsPleaseSettings settings , IndexerCapabilitiesCategories categories )
{
_settings = settings ;
_categories = categories ;
}
public IList < ReleaseInfo > ParseResponse ( IndexerResponse indexerResponse )
{
var torrentInfos = new List < ReleaseInfo > ( ) ;
if ( indexerResponse . HttpResponse . StatusCode ! = HttpStatusCode . OK )
{
throw new IndexerException ( indexerResponse , $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request" ) ;
}
// When there are no results, the API returns an empty array or empty response instead of an object
if ( string . IsNullOrWhiteSpace ( indexerResponse . Content ) | | indexerResponse . Content = = "[]" )
{
return torrentInfos ;
}
var jsonResponse = new HttpResponse < Dictionary < string , SubPleaseRelease > > ( indexerResponse . HttpResponse ) ;
foreach ( var keyValue in jsonResponse . Resource )
{
SubPleaseRelease r = keyValue . Value ;
foreach ( var d in r . Downloads )
{
var release = new TorrentInfo
{
InfoUrl = _settings . BaseUrl + $"shows/{r.Page}/" ,
PublishDate = r . Release_Date . DateTime ,
Files = 1 ,
Categories = new List < IndexerCategory > { NewznabStandardCategory . TVAnime } ,
Seeders = 1 ,
Peers = 2 ,
MinimumRatio = 1 ,
MinimumSeedTime = 172800 , // 48 hours
DownloadVolumeFactor = 0 ,
UploadVolumeFactor = 1
} ;
// Ex: [SubsPlease] Shingeki no Kyojin (The Final Season) - 64 (1080p)
release . Title + = $"[SubsPlease] {r.Show} - {r.Episode} ({d.Res}p)" ;
release . MagnetUrl = d . Magnet ;
release . DownloadUrl = null ;
release . Guid = d . Magnet ;
// The API doesn't tell us file size, so give an estimate based on resolution
if ( string . Equals ( d . Res , "1080" ) )
{
release . Size = 1395864371 ; // 1.3GB
}
else if ( string . Equals ( d . Res , "720" ) )
{
release . Size = 734003200 ; // 700MB
}
else if ( string . Equals ( d . Res , "480" ) )
{
release . Size = 367001600 ; // 350MB
}
else
{
release . Size = 1073741824 ; // 1GB
}
torrentInfos . Add ( release ) ;
}
}
return torrentInfos . ToArray ( ) ;
}
public Action < IDictionary < string , string > , DateTime ? > CookiesUpdater { get ; set ; }
}
public class SubsPleaseSettingsValidator : AbstractValidator < SubsPleaseSettings >
{
}
public class SubsPleaseSettings : IIndexerSettings
{
private static readonly SubsPleaseSettingsValidator Validator = new SubsPleaseSettingsValidator ( ) ;
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get ; set ; }
public NzbDroneValidationResult Validate ( )
{
return new NzbDroneValidationResult ( Validator . Validate ( this ) ) ;
}
}
public class SubPleaseRelease
{
public string Time { get ; set ; }
public DateTimeOffset Release_Date { get ; set ; }
public string Show { get ; set ; }
public string Episode { get ; set ; }
public SubPleaseDownloadInfo [ ] Downloads { get ; set ; }
public string Xdcc { get ; set ; }
public string ImageUrl { get ; set ; }
public string Page { get ; set ; }
}
public class SubPleaseDownloadInfo
{
public string Res { get ; set ; }
public string Magnet { get ; set ; }
}
}