mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-20 22:14:34 -04:00
300 lines
13 KiB
C#
300 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using AngleSharp.Html.Parser;
|
|
using NLog;
|
|
using NzbDrone.Common.Extensions;
|
|
using NzbDrone.Common.Http;
|
|
using NzbDrone.Core.Annotations;
|
|
using NzbDrone.Core.Configuration;
|
|
using NzbDrone.Core.Indexers.Exceptions;
|
|
using NzbDrone.Core.Indexers.Settings;
|
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
|
using NzbDrone.Core.Messaging.Events;
|
|
using NzbDrone.Core.Parser;
|
|
using NzbDrone.Core.Parser.Model;
|
|
|
|
namespace NzbDrone.Core.Indexers.Definitions
|
|
{
|
|
public class SceneTime : TorrentIndexerBase<SceneTimeSettings>
|
|
{
|
|
public override string Name => "SceneTime";
|
|
public override string[] IndexerUrls => new[] { "https://www.scenetime.com/" };
|
|
public override string Description => "Always on time";
|
|
public override string Language => "en-US";
|
|
public override Encoding Encoding => Encoding.UTF8;
|
|
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
|
public override IndexerCapabilities Capabilities => SetCapabilities();
|
|
|
|
public SceneTime(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
|
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
|
{
|
|
}
|
|
|
|
public override IIndexerRequestGenerator GetRequestGenerator()
|
|
{
|
|
return new SceneTimeRequestGenerator(Settings, Capabilities);
|
|
}
|
|
|
|
public override IParseIndexerResponse GetParser()
|
|
{
|
|
return new SceneTimeParser(Settings, Capabilities.Categories);
|
|
}
|
|
|
|
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
|
{
|
|
if (!httpResponse.Content.Contains("logout.php"))
|
|
{
|
|
throw new IndexerAuthException("SceneTime authentication with cookies failed.");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected override IDictionary<string, string> GetCookies()
|
|
{
|
|
return CookieUtil.CookieHeaderToDictionary(Settings.Cookie);
|
|
}
|
|
|
|
private IndexerCapabilities SetCapabilities()
|
|
{
|
|
var caps = new IndexerCapabilities
|
|
{
|
|
TvSearchParams = new List<TvSearchParam>
|
|
{
|
|
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
|
|
},
|
|
MovieSearchParams = new List<MovieSearchParam>
|
|
{
|
|
MovieSearchParam.Q, MovieSearchParam.ImdbId
|
|
},
|
|
MusicSearchParams = new List<MusicSearchParam>
|
|
{
|
|
MusicSearchParam.Q
|
|
},
|
|
BookSearchParams = new List<BookSearchParam>
|
|
{
|
|
BookSearchParam.Q
|
|
}
|
|
};
|
|
|
|
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.XXX, "Movies Adult");
|
|
caps.Categories.AddCategoryMapping(47, NewznabStandardCategory.Movies, "Movie Packs");
|
|
caps.Categories.AddCategoryMapping(57, NewznabStandardCategory.MoviesSD, "Movies SD");
|
|
caps.Categories.AddCategoryMapping(59, NewznabStandardCategory.MoviesHD, "Movies HD");
|
|
caps.Categories.AddCategoryMapping(64, NewznabStandardCategory.Movies3D, "Movies 3D");
|
|
caps.Categories.AddCategoryMapping(82, NewznabStandardCategory.MoviesOther, "Movies CAM/TS");
|
|
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.MoviesUHD, "Movies UHD");
|
|
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVUHD, "TV UHD");
|
|
caps.Categories.AddCategoryMapping(43, NewznabStandardCategory.TV, "TV Packs");
|
|
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.TVHD, "TV HD");
|
|
caps.Categories.AddCategoryMapping(77, NewznabStandardCategory.TVSD, "TV SD");
|
|
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "TV ANIME");
|
|
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.PCGames, "Games PC-ISO");
|
|
caps.Categories.AddCategoryMapping(48, NewznabStandardCategory.ConsoleXBox, "Games XBOX");
|
|
caps.Categories.AddCategoryMapping(51, NewznabStandardCategory.ConsoleWii, "Games Wii");
|
|
caps.Categories.AddCategoryMapping(55, NewznabStandardCategory.ConsoleNDS, "Games Nintendo");
|
|
caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.ConsolePS4, "Games PS");
|
|
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.ConsoleOther, "Games Dreamcast");
|
|
caps.Categories.AddCategoryMapping(52, NewznabStandardCategory.PCMac, "Mac/Linux");
|
|
caps.Categories.AddCategoryMapping(53, NewznabStandardCategory.PC0day, "Apps");
|
|
caps.Categories.AddCategoryMapping(24, NewznabStandardCategory.PCMobileOther, "Mobile Apps");
|
|
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.Books, "Books and Magazines");
|
|
caps.Categories.AddCategoryMapping(65, NewznabStandardCategory.BooksComics, "Books Comics");
|
|
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.Audio, "Music");
|
|
caps.Categories.AddCategoryMapping(116, NewznabStandardCategory.Audio, "Music Packs");
|
|
|
|
caps.Flags = new List<IndexerFlag>
|
|
{
|
|
IndexerFlag.FreeLeech
|
|
};
|
|
|
|
return caps;
|
|
}
|
|
}
|
|
|
|
public class SceneTimeRequestGenerator : IIndexerRequestGenerator
|
|
{
|
|
private readonly SceneTimeSettings _settings;
|
|
private readonly IndexerCapabilities _capabilities;
|
|
|
|
public SceneTimeRequestGenerator(SceneTimeSettings settings, IndexerCapabilities capabilities)
|
|
{
|
|
_settings = settings;
|
|
_capabilities = capabilities;
|
|
}
|
|
|
|
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
|
{
|
|
var pageableRequests = new IndexerPageableRequestChain();
|
|
|
|
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories, searchCriteria.FullImdbId));
|
|
|
|
return pageableRequests;
|
|
}
|
|
|
|
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
|
{
|
|
var pageableRequests = new IndexerPageableRequestChain();
|
|
|
|
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}", searchCriteria.Categories, searchCriteria.FullImdbId));
|
|
|
|
return pageableRequests;
|
|
}
|
|
|
|
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
|
{
|
|
var pageableRequests = new IndexerPageableRequestChain();
|
|
|
|
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
|
|
|
|
return pageableRequests;
|
|
}
|
|
|
|
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
|
{
|
|
var pageableRequests = new IndexerPageableRequestChain();
|
|
|
|
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
|
|
|
|
return pageableRequests;
|
|
}
|
|
|
|
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
|
{
|
|
var pageableRequests = new IndexerPageableRequestChain();
|
|
|
|
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
|
|
|
|
return pageableRequests;
|
|
}
|
|
|
|
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
|
|
{
|
|
var parameters = new NameValueCollection
|
|
{
|
|
{ "cata", "yes" }
|
|
};
|
|
|
|
var catList = _capabilities.Categories.MapTorznabCapsToTrackers(categories);
|
|
foreach (var cat in catList)
|
|
{
|
|
parameters.Set($"c{cat}", "1");
|
|
}
|
|
|
|
if (imdbId.IsNotNullOrWhiteSpace())
|
|
{
|
|
parameters.Set("imdb", imdbId);
|
|
}
|
|
|
|
if (term.IsNotNullOrWhiteSpace())
|
|
{
|
|
parameters.Set("search", term);
|
|
}
|
|
|
|
if (_settings.FreeLeechOnly)
|
|
{
|
|
parameters.Set("freeleech", "on");
|
|
}
|
|
|
|
var searchUrl = $"{_settings.BaseUrl.TrimEnd('/')}/browse.php?{parameters.GetQueryString()}";
|
|
|
|
yield return new IndexerRequest(searchUrl, HttpAccept.Html);
|
|
}
|
|
|
|
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
|
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
|
}
|
|
|
|
public class SceneTimeParser : IParseIndexerResponse
|
|
{
|
|
private readonly SceneTimeSettings _settings;
|
|
private readonly IndexerCapabilitiesCategories _categories;
|
|
|
|
public SceneTimeParser(SceneTimeSettings settings, IndexerCapabilitiesCategories categories)
|
|
{
|
|
_settings = settings;
|
|
_categories = categories;
|
|
}
|
|
|
|
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
|
{
|
|
var releaseInfos = new List<ReleaseInfo>();
|
|
|
|
var parser = new HtmlParser();
|
|
using var dom = parser.ParseDocument(indexerResponse.Content);
|
|
|
|
var table = dom.QuerySelector("table.movehere");
|
|
if (table == null)
|
|
{
|
|
return releaseInfos; // no results
|
|
}
|
|
|
|
var headerColumns = table.QuerySelectorAll("thead > tr > th.cat_Head")
|
|
.Select(x => x.GetAttribute("title").IsNotNullOrWhiteSpace() ? x.GetAttribute("title") : x.TextContent)
|
|
.ToList();
|
|
var categoryIndex = headerColumns.FindIndex(x => x.Equals("Type", StringComparison.OrdinalIgnoreCase));
|
|
var nameIndex = headerColumns.FindIndex(x => x.Equals("Name", StringComparison.OrdinalIgnoreCase));
|
|
var sizeIndex = headerColumns.FindIndex(x => x.Equals("Size", StringComparison.OrdinalIgnoreCase));
|
|
var seedersIndex = headerColumns.FindIndex(x => x.Equals("Seeder(s)", StringComparison.OrdinalIgnoreCase));
|
|
var leechersIndex = headerColumns.FindIndex(x => x.Equals("Leecher(s)", StringComparison.OrdinalIgnoreCase));
|
|
|
|
var rows = table.QuerySelectorAll("tbody > tr");
|
|
|
|
foreach (var row in rows)
|
|
{
|
|
var qDescCol = row.Children[nameIndex];
|
|
var qLink = qDescCol.QuerySelector("a");
|
|
var title = qLink.QuerySelector("span.torrent-text").TextContent.Trim();
|
|
|
|
var infoUrl = _settings.BaseUrl + qLink.GetAttribute("href")?.TrimStart('/');
|
|
var torrentId = ParseUtil.GetArgumentFromQueryString(infoUrl, "id");
|
|
var seeders = ParseUtil.CoerceInt(row.Children[seedersIndex].TextContent.Trim());
|
|
|
|
var categoryLink = row.Children[categoryIndex].QuerySelector("a")?.GetAttribute("href");
|
|
var cat = categoryLink != null ? ParseUtil.GetArgumentFromQueryString(categoryLink, "cat") : "82"; // default
|
|
|
|
var dateAdded = qDescCol.QuerySelector("span[class=\"elapsedDate\"]")?.GetAttribute("title")?.Trim();
|
|
var publishDate = DateTime.TryParseExact(dateAdded, "dddd, MMMM d, yyyy \\a\\t h:mmtt", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date)
|
|
? date
|
|
: DateTimeUtil.FromTimeAgo(qDescCol.QuerySelector("span[class=\"elapsedDate\"]").TextContent.Trim());
|
|
|
|
var release = new TorrentInfo
|
|
{
|
|
Guid = infoUrl,
|
|
InfoUrl = infoUrl,
|
|
DownloadUrl = $"{_settings.BaseUrl}download.php/{torrentId}/download.torrent",
|
|
Title = title,
|
|
Categories = _categories.MapTrackerCatToNewznab(cat),
|
|
PublishDate = publishDate,
|
|
Size = ParseUtil.GetBytes(row.Children[sizeIndex].TextContent),
|
|
Seeders = seeders,
|
|
Peers = ParseUtil.CoerceInt(row.Children[leechersIndex].TextContent.Trim()) + seeders,
|
|
DownloadVolumeFactor = row.QuerySelector("span.tag.free") is not null ? 0 : 1,
|
|
UploadVolumeFactor = 1,
|
|
MinimumRatio = 1,
|
|
MinimumSeedTime = 259200 // 72 hours
|
|
};
|
|
|
|
//TODO Do something with this filtering
|
|
//if (!query.MatchQueryStringAND(release.Title))
|
|
// continue;
|
|
releaseInfos.Add(release);
|
|
}
|
|
|
|
return releaseInfos.ToArray();
|
|
}
|
|
|
|
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
|
}
|
|
|
|
public class SceneTimeSettings : CookieTorrentBaseSettings
|
|
{
|
|
[FieldDefinition(3, Label = "FreeLeech Only", Type = FieldType.Checkbox, HelpText = "Search FreeLeech torrents only")]
|
|
public bool FreeLeechOnly { get; set; }
|
|
}
|
|
}
|