2019-02-22 07:12:15 +00:00
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
2020-04-03 00:43:32 +00:00
|
|
|
using System.Collections.Specialized;
|
2020-05-03 23:35:52 +00:00
|
|
|
using System.Diagnostics.CodeAnalysis;
|
2019-02-22 07:12:15 +00:00
|
|
|
using System.Globalization;
|
2020-03-27 03:13:38 +00:00
|
|
|
using System.Linq;
|
2019-02-22 07:12:15 +00:00
|
|
|
using System.Text;
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
using Jackett.Common.Models;
|
|
|
|
using Jackett.Common.Models.IndexerConfig.Bespoke;
|
|
|
|
using Jackett.Common.Services.Interfaces;
|
|
|
|
using Jackett.Common.Utils;
|
|
|
|
using Jackett.Common.Utils.Clients;
|
|
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
using NLog;
|
|
|
|
|
|
|
|
namespace Jackett.Common.Indexers
|
|
|
|
{
|
2020-05-03 23:35:52 +00:00
|
|
|
[ExcludeFromCodeCoverage]
|
2020-05-11 22:58:10 +00:00
|
|
|
public class TVStore : BaseWebIndexer
|
2019-02-22 07:12:15 +00:00
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
private readonly Dictionary<int, long> _imdbLookup = new Dictionary<int, long>(); // _imdbLookup[internalId] = imdbId
|
|
|
|
|
|
|
|
private readonly Dictionary<long, int>
|
|
|
|
_internalLookup = new Dictionary<long, int>(); // _internalLookup[imdbId] = internalId
|
|
|
|
|
|
|
|
private readonly Regex _seriesInfoMatch = new Regex(
|
|
|
|
@"catl\[\d+\]=(?<seriesID>\d+).*catIM\[\k<seriesID>]='(?<ImdbId>\d+)'", RegexOptions.Compiled);
|
|
|
|
|
|
|
|
private readonly Regex _seriesInfoSearchRegex = new Regex(
|
|
|
|
@"S(?<season>\d{1,3})(?:E(?<episode>\d{1,3}))?$", RegexOptions.IgnoreCase);
|
|
|
|
|
2020-12-11 22:14:21 +00:00
|
|
|
public TVStore(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps,
|
|
|
|
ICacheService cs) :
|
2020-05-11 19:59:28 +00:00
|
|
|
base(id: "tvstore",
|
|
|
|
name: "TV Store",
|
2020-04-03 00:43:32 +00:00
|
|
|
description: "TV Store is a HUNGARIAN Private Torrent Tracker for TV",
|
|
|
|
link: "https://tvstore.me/",
|
|
|
|
caps: new TorznabCapabilities
|
|
|
|
{
|
2020-10-18 20:47:36 +00:00
|
|
|
TvSearchParams = new List<TvSearchParam>
|
|
|
|
{
|
|
|
|
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
|
|
|
|
},
|
|
|
|
MovieSearchParams = new List<MovieSearchParam>
|
|
|
|
{
|
|
|
|
MovieSearchParam.Q, MovieSearchParam.ImdbId
|
2020-10-19 21:19:10 +00:00
|
|
|
}
|
2020-04-03 00:43:32 +00:00
|
|
|
},
|
|
|
|
configService: configService,
|
|
|
|
client: wc,
|
|
|
|
logger: l,
|
|
|
|
p: ps,
|
2020-12-11 22:14:21 +00:00
|
|
|
cacheService: cs,
|
2020-04-03 00:43:32 +00:00
|
|
|
configData: new ConfigurationDataTVstore())
|
2019-02-22 07:12:15 +00:00
|
|
|
{
|
|
|
|
Encoding = Encoding.UTF8;
|
2021-09-08 01:02:29 +00:00
|
|
|
Language = "hu-HU";
|
2019-02-22 07:12:15 +00:00
|
|
|
Type = "private";
|
2020-10-18 20:47:36 +00:00
|
|
|
|
2019-02-22 07:12:15 +00:00
|
|
|
AddCategoryMapping(1, TorznabCatType.TV);
|
2019-02-22 19:14:53 +00:00
|
|
|
AddCategoryMapping(2, TorznabCatType.TVHD);
|
|
|
|
AddCategoryMapping(3, TorznabCatType.TVSD);
|
2019-02-22 07:12:15 +00:00
|
|
|
}
|
|
|
|
|
2020-04-03 00:43:32 +00:00
|
|
|
private string LoginUrl => SiteLink + "takelogin.php";
|
|
|
|
private string LoginPageUrl => SiteLink + "login.php?returnto=%2F";
|
|
|
|
private string SearchUrl => SiteLink + "torrent/br_process.php";
|
|
|
|
private string DownloadUrl => SiteLink + "torrent/download.php";
|
|
|
|
private string BrowseUrl => SiteLink + "torrent/browse.php";
|
|
|
|
|
|
|
|
private new ConfigurationDataTVstore configData => (ConfigurationDataTVstore)base.configData;
|
|
|
|
|
2019-02-22 07:12:15 +00:00
|
|
|
public override async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
|
|
|
|
{
|
|
|
|
LoadValuesFromJson(configJson);
|
2020-09-21 16:39:47 +00:00
|
|
|
var loginPage = await RequestWithCookiesAsync(LoginPageUrl, string.Empty);
|
2020-04-03 00:43:32 +00:00
|
|
|
var pairs = new Dictionary<string, string>
|
|
|
|
{
|
|
|
|
{"username", configData.Username.Value},
|
|
|
|
{"password", configData.Password.Value},
|
|
|
|
{"back", "%2F"},
|
|
|
|
{"logout", "1"}
|
2019-02-22 07:12:15 +00:00
|
|
|
};
|
|
|
|
var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, loginPage.Cookies, true, referer: SiteLink);
|
2020-04-03 00:43:32 +00:00
|
|
|
await ConfigureIfOK(
|
2020-06-09 17:36:57 +00:00
|
|
|
result.Cookies, result.ContentString?.Contains("Főoldal") == true,
|
2020-04-03 00:43:32 +00:00
|
|
|
() => throw new ExceptionWithConfigData("Error while trying to login.", configData));
|
2019-02-22 07:12:15 +00:00
|
|
|
return IndexerConfigurationStatus.RequiresTesting;
|
|
|
|
}
|
|
|
|
|
2019-08-02 21:34:01 +00:00
|
|
|
/// <summary>
|
2020-04-03 00:43:32 +00:00
|
|
|
/// Calculate the Upload Factor for the torrents
|
2019-08-02 21:34:01 +00:00
|
|
|
/// </summary>
|
|
|
|
/// <returns>The calculated factor</returns>
|
|
|
|
/// <param name="dateTime">Date time.</param>
|
2020-04-03 00:43:32 +00:00
|
|
|
/// <param name="isSeasonPack">Determine if torrent type is season pack or single episode</param>
|
|
|
|
private static double UploadFactorCalculator(DateTime dateTime, bool isSeasonPack)
|
2019-08-02 21:34:01 +00:00
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
var dd = (DateTime.Now - dateTime).Days;
|
2019-08-02 21:34:01 +00:00
|
|
|
/* In case of season Packs */
|
2020-04-03 00:43:32 +00:00
|
|
|
if (isSeasonPack)
|
2019-08-02 21:34:01 +00:00
|
|
|
{
|
2020-02-09 02:35:16 +00:00
|
|
|
if (dd >= 90)
|
|
|
|
return 4;
|
|
|
|
if (dd >= 30)
|
|
|
|
return 2;
|
|
|
|
if (dd >= 14)
|
|
|
|
return 1.5;
|
2019-08-02 21:34:01 +00:00
|
|
|
}
|
|
|
|
else /* In case of single episodes */
|
|
|
|
{
|
2020-02-09 02:35:16 +00:00
|
|
|
if (dd >= 60)
|
|
|
|
return 2;
|
|
|
|
if (dd >= 30)
|
|
|
|
return 1.5;
|
2019-08-02 21:34:01 +00:00
|
|
|
}
|
2020-04-03 00:43:32 +00:00
|
|
|
|
2019-08-02 21:34:01 +00:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2019-02-22 07:12:15 +00:00
|
|
|
/// <summary>
|
2020-04-03 00:43:32 +00:00
|
|
|
/// Parses the torrents from the content
|
2019-02-22 07:12:15 +00:00
|
|
|
/// </summary>
|
|
|
|
/// <returns>The parsed torrents.</returns>
|
|
|
|
/// <param name="results">The result of the query</param>
|
2020-04-03 00:43:32 +00:00
|
|
|
/// <param name="alreadyFound">Number of the already found torrents.(used for limit)</param>
|
2019-02-22 07:12:15 +00:00
|
|
|
/// <param name="limit">The limit to the number of torrents to download </param>
|
2020-04-03 00:43:32 +00:00
|
|
|
/// <param name="previouslyParsedOnPage">Current position in parsed results</param>
|
2020-06-10 21:22:29 +00:00
|
|
|
private async Task<List<ReleaseInfo>> ParseTorrentsAsync(WebResult results, int alreadyFound, int limit,
|
2020-04-03 00:43:32 +00:00
|
|
|
int previouslyParsedOnPage)
|
2019-02-22 07:12:15 +00:00
|
|
|
{
|
|
|
|
var releases = new List<ReleaseInfo>();
|
2020-04-03 00:43:32 +00:00
|
|
|
var queryParams = new NameValueCollection
|
|
|
|
{
|
|
|
|
{"func", "getToggle"},
|
|
|
|
{"w", "F"},
|
|
|
|
{"pg", "0"}
|
|
|
|
};
|
2019-02-22 07:12:15 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
/* Content Looks like this
|
|
|
|
* 2\15\2\1\1727\207244\1x08 \[WebDL-720p - Eng - AJP69]\gb\2018-03-09 08:11:53\akció, kaland, sci-fi \0\0\1\191170047\1\0\Anonymous\50\0\0\\0\4\0\174\0\
|
|
|
|
* 1\ 0\0\1\1727\207243\1x08 \[WebDL-1080p - Eng - AJP69]\gb\2018-03-09 08:11:49\akció, kaland, sci-fi \0\0\1\305729738\1\0\Anonymous\50\0\0\\0\8\0\102\0\0\0\0\1\\\
|
2020-04-03 00:43:32 +00:00
|
|
|
* First 3 items per page are total results, results per page, and results this page
|
|
|
|
* There is also a tail of ~4 items after the results for some reason. Looks like \1\\\
|
2019-02-22 07:12:15 +00:00
|
|
|
*/
|
2020-06-09 17:36:57 +00:00
|
|
|
var parameters = results.ContentString.Split('\\');
|
2020-04-03 00:43:32 +00:00
|
|
|
var torrentsThisPage = int.Parse(parameters[2]);
|
|
|
|
var maxTorrents = Math.Min(torrentsThisPage, limit - alreadyFound);
|
|
|
|
var rows = parameters.Skip(3) //Skip pages info
|
|
|
|
.Select((str, index) => (index, str)) //Index each string for grouping
|
|
|
|
.GroupBy(n => n.index / 27) // each torrent is divided into 27 parts
|
|
|
|
.Skip(previouslyParsedOnPage).Take(maxTorrents)// only parse the rows we want
|
2020-04-07 16:17:17 +00:00
|
|
|
//Convert above query into a List<string>(27) in prep for parsing
|
2020-04-03 00:43:32 +00:00
|
|
|
.Select(entry => entry.Select(item => item.str).ToList());
|
|
|
|
foreach (var row in rows)
|
2019-02-22 07:12:15 +00:00
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
var torrentId = row[(int)TorrentParts.TorrentId];
|
|
|
|
var downloadLink = new Uri(DownloadUrl + "?id=" + torrentId);
|
|
|
|
var imdbId = _imdbLookup.TryGetValue(int.Parse(row[(int)TorrentParts.InternalId]), out var imdb)
|
|
|
|
? (long?)imdb
|
|
|
|
: null;
|
|
|
|
var files = int.Parse(row[(int)TorrentParts.Files]);
|
|
|
|
var size = long.Parse(row[(int)TorrentParts.SizeBytes]);
|
|
|
|
var seeders = int.Parse(row[(int)TorrentParts.Seeders]);
|
|
|
|
var leechers = int.Parse(row[(int)TorrentParts.Leechers]);
|
|
|
|
var grabs = int.Parse(row[(int)TorrentParts.Grabs]);
|
|
|
|
var publishDate = DateTime.Parse(row[(int)TorrentParts.PublishDate]);
|
|
|
|
var isSeasonPack = row[(int)TorrentParts.EpisodeInfo].Contains("évad");
|
|
|
|
queryParams["id"] = torrentId;
|
|
|
|
queryParams["now"] = DateTimeUtil.DateTimeToUnixTimestamp(DateTime.UtcNow)
|
|
|
|
.ToString(CultureInfo.InvariantCulture);
|
2020-06-11 15:09:27 +00:00
|
|
|
var filesList = (await RequestWithCookiesAndRetryAsync(SearchUrl + "?" + queryParams.GetQueryString()))
|
2020-06-09 17:36:57 +00:00
|
|
|
.ContentString;
|
2020-04-03 00:43:32 +00:00
|
|
|
var firstFileName = filesList.Split(
|
|
|
|
new[]
|
|
|
|
{
|
|
|
|
@"\\"
|
|
|
|
}, StringSplitOptions.None)[1];
|
|
|
|
// Delete the file extension. Many first files are either mkv or nfo.
|
|
|
|
// Cannot confirm these are the only extensions, so generic remove all 3 char extensions at end of section.
|
|
|
|
firstFileName = Regex.Replace(firstFileName, @"\.\w{3}$", string.Empty);
|
|
|
|
if (isSeasonPack)
|
|
|
|
firstFileName = Regex.Replace(
|
|
|
|
firstFileName, @"(?<=S\d+)E\d{2,3}", string.Empty, RegexOptions.IgnoreCase);
|
|
|
|
var category = new[]
|
2019-08-02 21:34:01 +00:00
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
TvCategoryParser.ParseTvShowQuality(firstFileName)
|
|
|
|
};
|
|
|
|
var release = new ReleaseInfo
|
2019-02-22 07:12:15 +00:00
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
Title = firstFileName,
|
|
|
|
Link = downloadLink,
|
|
|
|
Guid = downloadLink,
|
|
|
|
PublishDate = publishDate,
|
|
|
|
Files = files,
|
|
|
|
Size = size,
|
|
|
|
Category = category,
|
|
|
|
Seeders = seeders,
|
|
|
|
Peers = leechers + seeders,
|
|
|
|
Grabs = grabs,
|
|
|
|
MinimumRatio = 1,
|
|
|
|
MinimumSeedTime = 172800, // 48 hours
|
|
|
|
DownloadVolumeFactor = 1,
|
|
|
|
UploadVolumeFactor = UploadFactorCalculator(publishDate, isSeasonPack),
|
|
|
|
Imdb = imdbId
|
|
|
|
};
|
|
|
|
releases.Add(release);
|
2019-02-22 07:12:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
2020-06-09 17:36:57 +00:00
|
|
|
OnParseError(results.ContentString, ex);
|
2019-02-22 07:12:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return releases;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
2020-04-03 00:43:32 +00:00
|
|
|
/// Map internally used series info to its corresponding IMDB number.
|
|
|
|
/// Saves this data into 2 dictionaries for easy lookup from one value to the other
|
2019-02-22 07:12:15 +00:00
|
|
|
/// </summary>
|
2020-04-03 00:43:32 +00:00
|
|
|
private async Task PopulateImdbMapAsync()
|
2019-02-22 07:12:15 +00:00
|
|
|
{
|
2020-06-11 15:09:27 +00:00
|
|
|
var result = await RequestWithCookiesAndRetryAsync(BrowseUrl);
|
2020-06-09 17:36:57 +00:00
|
|
|
foreach (Match match in _seriesInfoMatch.Matches(result.ContentString))
|
2019-02-22 07:12:15 +00:00
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
var internalId = int.Parse(match.Groups["seriesID"].Value);
|
|
|
|
var imdbId = long.Parse(match.Groups["ImdbId"].Value);
|
|
|
|
_imdbLookup[internalId] = imdbId;
|
|
|
|
_internalLookup[imdbId] = internalId;
|
2019-02-22 07:12:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
|
|
|
{
|
|
|
|
var releases = new List<ReleaseInfo>();
|
2020-04-03 00:43:32 +00:00
|
|
|
if (!_imdbLookup.Any())
|
|
|
|
await PopulateImdbMapAsync();
|
|
|
|
var queryParams = new NameValueCollection
|
|
|
|
{
|
|
|
|
{"now", DateTimeUtil.DateTimeToUnixTimestamp(DateTime.UtcNow).ToString(CultureInfo.InvariantCulture)},
|
|
|
|
{"p", "1"}
|
|
|
|
};
|
|
|
|
if (query.Limit == 0)
|
|
|
|
query.Limit = 100;
|
2019-08-02 21:34:01 +00:00
|
|
|
if (query.IsImdbQuery)
|
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
if (!string.IsNullOrEmpty(query.ImdbIDShort) && _internalLookup.TryGetValue(
|
|
|
|
long.Parse(query.ImdbIDShort), out var internalId))
|
|
|
|
queryParams.Add("g", internalId.ToString());
|
2020-01-09 03:32:02 +00:00
|
|
|
else
|
2020-04-03 00:43:32 +00:00
|
|
|
return Enumerable.Empty<ReleaseInfo>();
|
2019-08-02 21:34:01 +00:00
|
|
|
}
|
2020-04-03 00:43:32 +00:00
|
|
|
else
|
2019-02-22 07:12:15 +00:00
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
queryParams.Add("g", "0");
|
|
|
|
if (!string.IsNullOrWhiteSpace(query.SearchTerm))
|
2019-02-23 09:01:49 +00:00
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
var searchString = query.SanitizedSearchTerm;
|
|
|
|
if (query.Season == 0 && string.IsNullOrWhiteSpace(query.Episode))
|
2019-02-23 09:01:49 +00:00
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
//Jackett doesn't check for lowercase s00e00 so do it here.
|
|
|
|
var searchMatch = _seriesInfoSearchRegex.Match(searchString);
|
2019-08-02 21:34:01 +00:00
|
|
|
if (searchMatch.Success)
|
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
query.Season = int.Parse(searchMatch.Groups["season"].Value);
|
|
|
|
query.Episode = searchMatch.Groups["episode"].Success
|
|
|
|
? $"{int.Parse(searchMatch.Groups["episode"].Value):00}"
|
|
|
|
: null;
|
|
|
|
query.SearchTerm = searchString.Remove(searchMatch.Index, searchMatch.Length).Trim(); // strip SnnEnn
|
2019-08-02 21:34:01 +00:00
|
|
|
}
|
2019-02-23 09:01:49 +00:00
|
|
|
}
|
|
|
|
}
|
2020-04-03 00:43:32 +00:00
|
|
|
else if (query.IsTest)
|
|
|
|
query.Limit = 20;
|
2019-02-23 09:01:49 +00:00
|
|
|
|
2020-04-03 00:43:32 +00:00
|
|
|
// Search string must be converted to Base64
|
|
|
|
var plainTextBytes = Encoding.UTF8.GetBytes(query.SanitizedSearchTerm);
|
|
|
|
queryParams.Add("c", Convert.ToBase64String(plainTextBytes));
|
2019-08-02 21:34:01 +00:00
|
|
|
}
|
2019-02-22 07:12:15 +00:00
|
|
|
|
2020-04-03 00:43:32 +00:00
|
|
|
if (query.Season != 0)
|
|
|
|
{
|
|
|
|
queryParams.Add("s", query.Season.ToString());
|
|
|
|
if (!string.IsNullOrWhiteSpace(query.Episode))
|
|
|
|
queryParams.Add("e", query.Episode);
|
|
|
|
}
|
2019-02-22 07:12:15 +00:00
|
|
|
|
2020-06-11 15:09:27 +00:00
|
|
|
var results = await RequestWithCookiesAndRetryAsync(SearchUrl + "?" + queryParams.GetQueryString());
|
2020-04-03 00:43:32 +00:00
|
|
|
// Parse page Information from result
|
2020-06-09 17:36:57 +00:00
|
|
|
var content = results.ContentString;
|
2019-02-22 07:12:15 +00:00
|
|
|
var splits = content.Split('\\');
|
2020-04-03 00:43:32 +00:00
|
|
|
var totalFound = int.Parse(splits[0]);
|
|
|
|
var torrentPerPage = int.Parse(splits[1]);
|
|
|
|
if (totalFound == 0 || query.Offset > totalFound)
|
|
|
|
return Enumerable.Empty<ReleaseInfo>();
|
|
|
|
var startPage = query.Offset / torrentPerPage + 1;
|
|
|
|
var previouslyParsedOnPage = query.Offset % torrentPerPage;
|
|
|
|
var pages = totalFound / torrentPerPage + 1;
|
|
|
|
// First page content is already ready
|
|
|
|
if (startPage == 1)
|
2019-08-02 21:34:01 +00:00
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
releases.AddRange(await ParseTorrentsAsync(results, releases.Count, query.Limit, previouslyParsedOnPage));
|
|
|
|
previouslyParsedOnPage = 0;
|
|
|
|
startPage++;
|
2019-08-02 21:34:01 +00:00
|
|
|
}
|
2019-02-22 07:12:15 +00:00
|
|
|
|
2020-04-03 00:43:32 +00:00
|
|
|
for (var page = startPage; page <= pages && releases.Count < query.Limit; page++)
|
2019-02-22 07:12:15 +00:00
|
|
|
{
|
2020-04-03 00:43:32 +00:00
|
|
|
queryParams["page"] = page.ToString();
|
2020-06-11 15:09:27 +00:00
|
|
|
results = await RequestWithCookiesAndRetryAsync(SearchUrl + "?" + queryParams.GetQueryString());
|
2020-04-03 00:43:32 +00:00
|
|
|
releases.AddRange(await ParseTorrentsAsync(results, releases.Count, query.Limit, previouslyParsedOnPage));
|
|
|
|
previouslyParsedOnPage = 0;
|
2019-02-22 07:12:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return releases;
|
|
|
|
}
|
|
|
|
|
2020-04-03 00:43:32 +00:00
|
|
|
private enum TorrentParts
|
|
|
|
{
|
|
|
|
InternalId = 1,
|
|
|
|
TorrentId = 2,
|
|
|
|
EpisodeInfo = 3,
|
|
|
|
PublishDate = 6,
|
|
|
|
Files = 10,
|
|
|
|
SizeBytes = 11,
|
|
|
|
Seeders = 20,
|
|
|
|
Leechers = 21,
|
|
|
|
Grabs = 22
|
|
|
|
}
|
|
|
|
}
|
2019-02-22 07:12:15 +00:00
|
|
|
}
|