mirror of https://github.com/Jackett/Jackett
Merge branch 'master' into dotnetcore
This commit is contained in:
commit
9710b37064
|
@ -2,8 +2,8 @@
|
|||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppName "Jackett"
|
||||
#define MyAppVersion GetFileVersion("BuildOutput\FullFramework\Windows\Jackett\Jackett.Common.dll")
|
||||
#define MyAppPublisher "Jackett Inc."
|
||||
#define MyAppVersion GetFileVersion(MyFileForVersion)
|
||||
#define MyAppPublisher "Jackett"
|
||||
#define MyAppURL "https://github.com/Jackett/Jackett"
|
||||
#define MyAppExeName "JackettTray.exe"
|
||||
|
||||
|
@ -22,9 +22,10 @@ AppUpdatesURL={#MyAppURL}
|
|||
DefaultDirName={pf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputBaseFilename=Jackett.Installer.Windows
|
||||
OutputBaseFilename={#MyOutputFilename}
|
||||
SetupIconFile=src\Jackett.Console\jackett.ico
|
||||
UninstallDisplayIcon={commonappdata}\Jackett\JackettConsole.exe
|
||||
VersionInfoVersion={#MyAppVersion}
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
DisableDirPage=yes
|
||||
|
@ -37,9 +38,7 @@ Name: "windowsService"; Description: "Install as a Windows Service"
|
|||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "BuildOutput\FullFramework\Windows\Jackett\JackettTray.exe"; DestDir: "{commonappdata}\Jackett"; Flags: ignoreversion
|
||||
Source: "BuildOutput\FullFramework\Windows\Jackett\JackettUpdater.exe"; DestDir: "{commonappdata}\Jackett"; Flags: ignoreversion
|
||||
Source: "BuildOutput\FullFramework\Windows\Jackett\*"; DestDir: "{commonappdata}\Jackett"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#MySourceFolder}\Jackett\*"; DestDir: "{commonappdata}\Jackett"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
|
|
|
@ -42,6 +42,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
|||
* KickAssTorrent (thekat.se clone)
|
||||
* LimeTorrents
|
||||
* MagnetDL
|
||||
* MejorTorrent <!-- maintained by ivandelabeldad -->
|
||||
* NextTorrent
|
||||
* Newpct (aka: tvsinpagar, descargas2020, torrentlocura, torrentrapid, etc)
|
||||
* Nyaa.si
|
||||
|
|
15
build.cake
15
build.cake
|
@ -119,9 +119,18 @@ Task("Package-Windows-Installer-Full-Framework")
|
|||
.IsDependentOn("Check-Packaging-Platform")
|
||||
.Does(() =>
|
||||
{
|
||||
InnoSetup("./Installer.iss", new InnoSetupSettings {
|
||||
OutputDirectory = workingDir + "/" + artifactsDirName
|
||||
});
|
||||
string sourceFolder = MakeAbsolute(Directory(windowsBuildFullFramework)).ToString();
|
||||
|
||||
InnoSetupSettings settings = new InnoSetupSettings();
|
||||
settings.OutputDirectory = workingDir + "/" + artifactsDirName;
|
||||
settings.Defines = new Dictionary<string, string>
|
||||
{
|
||||
{ "MyFileForVersion", sourceFolder + "/Jackett/Jackett.Common.dll" },
|
||||
{ "MySourceFolder", sourceFolder },
|
||||
{ "MyOutputFilename", "Jackett.Installer.Windows" },
|
||||
};
|
||||
|
||||
InnoSetup("./Installer.iss", settings);
|
||||
});
|
||||
|
||||
Task("Package-Files-Full-Framework-Windows")
|
||||
|
|
|
@ -40,6 +40,18 @@
|
|||
search: [q]
|
||||
tv-search: [q, season, ep]
|
||||
movie-search: [q]
|
||||
|
||||
settings:
|
||||
- name: username
|
||||
type: text
|
||||
label: Username
|
||||
- name: password
|
||||
type: password
|
||||
label: Password
|
||||
- name: info
|
||||
type: info
|
||||
label: Results Per Page
|
||||
default: For best results, change the 'Torrentliste' setting to "Platzsparendes Layout mit PopUp für zusätzliche Informationen" in your profile.
|
||||
|
||||
login:
|
||||
path: takelogin.php
|
||||
|
@ -63,9 +75,13 @@
|
|||
|
||||
rows:
|
||||
selector: table.tableinborder > tbody > tr:has(a[href^="details.php"])
|
||||
fields:
|
||||
fields: # note: two alternative layouts available
|
||||
title:
|
||||
selector: a[href^="details.php"]
|
||||
title:
|
||||
optional: true
|
||||
selector: a[href^="details.php"][title]
|
||||
attribute: title
|
||||
category:
|
||||
selector: a[href^="browse.php?cat="]
|
||||
attribute: href
|
||||
|
@ -79,29 +95,31 @@
|
|||
selector: a[href^=" /gettorrent/ssl/"]
|
||||
attribute: href
|
||||
files:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(2)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(2), a[href*="&filelist=1"]
|
||||
grabs:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(3) > b:nth-child(1)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(3) > b:nth-child(1), a[href*="&tosnatchers=1"]
|
||||
size:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(1)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(1), td:nth-child(7):has(br)
|
||||
filters:
|
||||
- name: replace
|
||||
args: [".", ""]
|
||||
- name: replace
|
||||
args: [",", "."]
|
||||
seeders:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(1)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(1), a[href*="&toseeders=1"]
|
||||
leechers:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(3)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(3), a[href*="&todlers=1"]
|
||||
date:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(5)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(5), td:nth-child(5):has(br)
|
||||
filters:
|
||||
- name: replace
|
||||
args: [" ", ""]
|
||||
- name: append
|
||||
args: " +2:00"
|
||||
- name: replace
|
||||
args: ["\xA0", " "]
|
||||
args: ["\xA0", ""]
|
||||
- name: dateparse
|
||||
args: "02.01.2006 15:04:05 -07:00"
|
||||
args: "02.01.200615:04:05 -07:00"
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
img[src="/pic/free.gif"]: "0"
|
||||
|
|
|
@ -45,10 +45,7 @@
|
|||
- selector: table:contains("Login failed!")
|
||||
test:
|
||||
path: index.php
|
||||
|
||||
download:
|
||||
selector: a[href^="download.php?id="]
|
||||
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: browse.php
|
||||
|
|
|
@ -0,0 +1,701 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom.Html;
|
||||
using AngleSharp.Parser.Html;
|
||||
using Jackett.Common.Models;
|
||||
using Jackett.Common.Models.IndexerConfig;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils.Clients;
|
||||
using Jackett.Common.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
|
||||
namespace Jackett.Common.Indexers
|
||||
{
|
||||
class MejorTorrent : BaseWebIndexer
|
||||
{
|
||||
public static Uri WebUri = new Uri("http://www.mejortorrent.com/");
|
||||
public static Uri DownloadUri = new Uri(WebUri, "secciones.php?sec=descargas&ap=contar_varios");
|
||||
private static Uri SearchUriBase = new Uri(WebUri, "secciones.php");
|
||||
public static Uri NewTorrentsUri = new Uri(WebUri, "secciones.php?sec=ultimos_torrents");
|
||||
public static Encoding MEEncoding = Encoding.GetEncoding("windows-1252");
|
||||
|
||||
public MejorTorrent(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps)
|
||||
: base(name: "MejorTorrent",
|
||||
description: "MejorTorrent - Hay veces que un torrent viene mejor! :)",
|
||||
link: WebUri.AbsoluteUri,
|
||||
caps: new TorznabCapabilities(TorznabCatType.TV,
|
||||
TorznabCatType.TVSD,
|
||||
TorznabCatType.TVHD),
|
||||
configService: configService,
|
||||
client: wc,
|
||||
logger: l,
|
||||
p: ps,
|
||||
configData: new ConfigurationData())
|
||||
{
|
||||
Encoding = MEEncoding;
|
||||
Language = "es-es";
|
||||
Type = "public";
|
||||
}
|
||||
|
||||
public override async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
|
||||
{
|
||||
configData.LoadValuesFromJson(configJson);
|
||||
var releases = await PerformQuery(new TorznabQuery());
|
||||
|
||||
await ConfigureIfOK(string.Empty, releases.Count() > 0, () =>
|
||||
{
|
||||
throw new Exception("Could not find releases from this URL");
|
||||
});
|
||||
|
||||
return IndexerConfigurationStatus.Completed;
|
||||
}
|
||||
|
||||
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
||||
{
|
||||
return await PerformQuery(query, 0);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query, int attempts)
|
||||
{
|
||||
var requester = new MejorTorrentRequester(this);
|
||||
var tvShowScraper = new TvShowScraper();
|
||||
var seasonScraper = new SeasonScraper();
|
||||
var downloadScraper = new DownloadScraper();
|
||||
var rssScraper = new RssScraper();
|
||||
var downloadGenerator = new DownloadGenerator(requester, downloadScraper);
|
||||
var tvShowPerformer = new TvShowPerformer(requester, tvShowScraper, seasonScraper, downloadGenerator);
|
||||
var rssPerformer = new RssPerformer(requester, rssScraper, seasonScraper, downloadGenerator);
|
||||
|
||||
if (string.IsNullOrEmpty(query.SanitizedSearchTerm))
|
||||
{
|
||||
return await rssPerformer.PerformQuery(query);
|
||||
}
|
||||
return await tvShowPerformer.PerformQuery(query);
|
||||
}
|
||||
|
||||
public static Uri CreateSearchUri(string search)
|
||||
{
|
||||
var finalUri = SearchUriBase.AbsoluteUri;
|
||||
finalUri += "?sec=buscador&valor=" + WebUtilityHelpers.UrlEncode(search, MEEncoding);
|
||||
return new Uri(finalUri);
|
||||
}
|
||||
|
||||
interface IScraper<T>
|
||||
{
|
||||
T Extract(IHtmlDocument html);
|
||||
}
|
||||
|
||||
class RssScraper : IScraper<IEnumerable<KeyValuePair<MejorTorrentReleaseInfo, Uri>>>
|
||||
{
|
||||
private readonly string LinkQuerySelector = "a[href*=\"/serie\"]";
|
||||
|
||||
public IEnumerable<KeyValuePair<MejorTorrentReleaseInfo, Uri>> Extract(IHtmlDocument html)
|
||||
{
|
||||
var episodes = GetNewEpisodesScratch(html);
|
||||
var links = GetLinks(html);
|
||||
var results = new List<KeyValuePair<MejorTorrentReleaseInfo, Uri>>();
|
||||
for (var i = 0; i < episodes.Count(); i++)
|
||||
{
|
||||
results.Add(new KeyValuePair<MejorTorrentReleaseInfo, Uri>(episodes.ElementAt(i), links.ElementAt(i)));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<MejorTorrentReleaseInfo> GetNewEpisodesScratch(IHtmlDocument html)
|
||||
{
|
||||
var tvShowsElements = html.QuerySelectorAll(LinkQuerySelector);
|
||||
var seasonLinks = tvShowsElements.Select(e => e.Attributes["href"].Value);
|
||||
var dates = GetDates(html);
|
||||
var titles = GetTitles(html);
|
||||
var qualities = GetQualities(html);
|
||||
var seasonsFirstEpisodesAndLast = GetSeasonsFirstEpisodesAndLast(html);
|
||||
|
||||
var episodes = new List<MejorTorrentReleaseInfo>();
|
||||
for(var i = 0; i < tvShowsElements.Count(); i++)
|
||||
{
|
||||
var e = new MejorTorrentReleaseInfo();
|
||||
e.TitleOriginal = titles.ElementAt(i);
|
||||
e.PublishDate = dates.ElementAt(i);
|
||||
e.CategoryText = qualities.ElementAt(i);
|
||||
var sfeal = seasonsFirstEpisodesAndLast.ElementAt(i);
|
||||
e.Season = sfeal.Key;
|
||||
e.EpisodeNumber = sfeal.Value.Key;
|
||||
if (sfeal.Value.Value != null && sfeal.Value.Value > sfeal.Value.Key)
|
||||
{
|
||||
e.Files = sfeal.Value.Value - sfeal.Value.Key + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Files = 1;
|
||||
}
|
||||
episodes.Add(e);
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
||||
private List<Uri> GetLinks(IHtmlDocument html)
|
||||
{
|
||||
return html.QuerySelectorAll(LinkQuerySelector)
|
||||
.Select(e => e.Attributes["href"].Value)
|
||||
.Select(relativeLink => new Uri(WebUri, relativeLink))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private List<DateTime> GetDates(IHtmlDocument html)
|
||||
{
|
||||
return html.QuerySelectorAll(LinkQuerySelector)
|
||||
.Select(e => e.PreviousElementSibling.TextContent)
|
||||
.Select(dateString => dateString.Split('-'))
|
||||
.Select(parts => new int[] { Int32.Parse(parts[0]), Int32.Parse(parts[1]), Int32.Parse(parts[2]) })
|
||||
.Select(intParts => new DateTime(intParts[0], intParts[1], intParts[2]))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private List<string> GetTitles(IHtmlDocument html)
|
||||
{
|
||||
var texts = LinkTexts(html);
|
||||
var completeTitles = texts.Select(text => text.Substring(0, text.IndexOf('-') - 1));
|
||||
var regex = new Regex(@".+\((.+)\)");
|
||||
var finalTitles = completeTitles.Select(title =>
|
||||
{
|
||||
var match = regex.Match(title);
|
||||
if (!match.Success) return title;
|
||||
return match.Groups[1].Value;
|
||||
});
|
||||
return finalTitles.ToList();
|
||||
}
|
||||
|
||||
private List<string> GetQualities(IHtmlDocument html)
|
||||
{
|
||||
var texts = LinkTexts(html);
|
||||
var regex = new Regex(@".+\[(.*)\].+");
|
||||
var qualities = texts.Select(text =>
|
||||
{
|
||||
var match = regex.Match(text);
|
||||
if (!match.Success) return "HDTV";
|
||||
var quality = match.Groups[1].Value;
|
||||
switch(quality)
|
||||
{
|
||||
case "720p":
|
||||
return "HDTV-720p";
|
||||
case "1080p":
|
||||
return "HDTV-1080p";
|
||||
default:
|
||||
return "HDTV";
|
||||
}
|
||||
});
|
||||
return qualities.ToList();
|
||||
}
|
||||
|
||||
private List<KeyValuePair<int, KeyValuePair<int,int?>>> GetSeasonsFirstEpisodesAndLast(IHtmlDocument html)
|
||||
{
|
||||
var texts = LinkTexts(html);
|
||||
// SEASON | START EPISODE | [END EPISODE]
|
||||
var regex = new Regex(@"(\d{1,2})x(\d{1,2})(?:.*\d{1,2}x(\d{1,2})?)?", RegexOptions.IgnoreCase);
|
||||
var seasonsFirstEpisodesAndLast = texts.Select(text =>
|
||||
{
|
||||
var match = regex.Match(text);
|
||||
int season = 0;
|
||||
int episode = 0;
|
||||
int? finalEpisode = null;
|
||||
if (!match.Success) return new KeyValuePair<int, KeyValuePair<int, int?>>(season, new KeyValuePair<int, int?>(episode, finalEpisode));
|
||||
season = Int32.Parse(match.Groups[1].Value);
|
||||
episode = Int32.Parse(match.Groups[2].Value);
|
||||
if (match.Groups[3].Success)
|
||||
{
|
||||
finalEpisode = Int32.Parse(match.Groups[3].Value);
|
||||
}
|
||||
return new KeyValuePair<int, KeyValuePair<int, int?>>(season, new KeyValuePair<int, int?>(episode, finalEpisode));
|
||||
});
|
||||
return seasonsFirstEpisodesAndLast.ToList();
|
||||
}
|
||||
|
||||
private List<string> LinkTexts(IHtmlDocument html)
|
||||
{
|
||||
return html.QuerySelectorAll(LinkQuerySelector)
|
||||
.Select(e => e.TextContent).ToList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TvShowScraper : IScraper<IEnumerable<Season>>
|
||||
{
|
||||
public IEnumerable<Season> Extract(IHtmlDocument html)
|
||||
{
|
||||
var tvSelector = "a[href*=\"/serie-\"]";
|
||||
var seasonsElements = html.QuerySelectorAll(tvSelector).Select(e => e.ParentElement);
|
||||
|
||||
var newTvShows = new List<Season>();
|
||||
|
||||
// EXAMPLES:
|
||||
// Stranger Things - 1ª Temporada (HDTV)
|
||||
// Stranger Things - 1ª Temporada [720p] (HDTV-720p)
|
||||
var regex = new Regex(@"(.+) - ([0-9]+).*\((.*)\)");
|
||||
foreach (var seasonElement in seasonsElements)
|
||||
{
|
||||
var link = seasonElement.QuerySelector("a[href*=\"/serie-\"]").Attributes["href"].Value;
|
||||
var info = seasonElement.TextContent; // Stranger Things - 1 ...
|
||||
var searchMatch = regex.Match(info);
|
||||
if (!searchMatch.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int seasonNumber;
|
||||
if (!Int32.TryParse(searchMatch.Groups[2].Value, out seasonNumber))
|
||||
{
|
||||
seasonNumber = 0;
|
||||
}
|
||||
var season = new Season
|
||||
{
|
||||
Title = searchMatch.Groups[1].Value,
|
||||
Number = seasonNumber,
|
||||
Type = searchMatch.Groups[3].Value,
|
||||
Link = new Uri(WebUri, link)
|
||||
};
|
||||
|
||||
// EXAMPLE: El cuento de la criada (Handmaids Tale)
|
||||
var originalTitleRegex = new Regex(@".+\((.+)\)");
|
||||
var originalTitleMath = originalTitleRegex.Match(season.Title);
|
||||
if (originalTitleMath.Success)
|
||||
{
|
||||
season.Title = originalTitleMath.Groups[1].Value;
|
||||
}
|
||||
newTvShows.Add(season);
|
||||
}
|
||||
return newTvShows;
|
||||
}
|
||||
}
|
||||
|
||||
class SeasonScraper : IScraper<IEnumerable<MejorTorrentReleaseInfo>>
|
||||
{
|
||||
public IEnumerable<MejorTorrentReleaseInfo> Extract(IHtmlDocument html)
|
||||
{
|
||||
var episodesLinksHtml = html.QuerySelectorAll("a[href*=\"/serie-episodio-descargar-torrent\"]");
|
||||
var episodesTexts = episodesLinksHtml.Select(l => l.TextContent).ToList();
|
||||
var episodesLinks = episodesLinksHtml.Select(e => e.Attributes["href"].Value).ToList();
|
||||
var dates = episodesLinksHtml
|
||||
.Select(e => e.ParentElement.ParentElement.QuerySelector("div").TextContent)
|
||||
.Select(stringDate => stringDate.Replace("Fecha: ", ""))
|
||||
.Select(stringDate => stringDate.Split('-'))
|
||||
.Select(stringParts => new int[]{ Int32.Parse(stringParts[0]), Int32.Parse(stringParts[1]), Int32.Parse(stringParts[2]) })
|
||||
.Select(intParts => new DateTime(intParts[0], intParts[1], intParts[2]));
|
||||
|
||||
var episodes = episodesLinks.Select(e => new MejorTorrentReleaseInfo()).ToList();
|
||||
|
||||
for (var i = 0; i < episodes.Count(); i++)
|
||||
{
|
||||
GuessEpisodes(episodes.ElementAt(i), episodesTexts.ElementAt(i));
|
||||
ExtractLinkInfo(episodes.ElementAt(i), episodesLinks.ElementAt(i));
|
||||
episodes.ElementAt(i).PublishDate = dates.ElementAt(i);
|
||||
}
|
||||
|
||||
return episodes;
|
||||
}
|
||||
|
||||
private void GuessEpisodes(MejorTorrentReleaseInfo release, string episodeText)
|
||||
{
|
||||
var seasonEpisodeRegex = new Regex(@"(\d{1,2}).*?(\d{1,2})", RegexOptions.IgnoreCase);
|
||||
var matchSeasonEpisode = seasonEpisodeRegex.Match(episodeText);
|
||||
if (!matchSeasonEpisode.Success) return;
|
||||
release.Season = Int32.Parse(matchSeasonEpisode.Groups[1].Value);
|
||||
release.EpisodeNumber = Int32.Parse(matchSeasonEpisode.Groups[2].Value);
|
||||
|
||||
char[] textArray = episodeText.ToCharArray();
|
||||
Array.Reverse(textArray);
|
||||
var reversedText = new string(textArray);
|
||||
var finalEpisodeRegex = new Regex(@"(\d{1,2})");
|
||||
var matchFinalEpisode = finalEpisodeRegex.Match(reversedText);
|
||||
if (!matchFinalEpisode.Success) return;
|
||||
var finalEpisodeArray = matchFinalEpisode.Groups[1].Value.ToCharArray();
|
||||
Array.Reverse(finalEpisodeArray);
|
||||
var finalEpisode = Int32.Parse(new string(finalEpisodeArray));
|
||||
if (finalEpisode > release.EpisodeNumber)
|
||||
{
|
||||
release.Files = (finalEpisode + 1) - release.EpisodeNumber;
|
||||
release.Size = release.Size * release.Files;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractLinkInfo(MejorTorrentReleaseInfo release, String link)
|
||||
{
|
||||
// LINK FORMAT: /serie-episodio-descargar-torrent-${ID}-${TITLE}-${SEASON_NUMBER}x${EPISODE_NUMBER}[range].html
|
||||
var regex = new Regex(@"\/serie-episodio-descargar-torrent-(\d+)-(.*)-(\d{1,2}).*(\d{1,2}).*\.html", RegexOptions.IgnoreCase);
|
||||
var linkMatch = regex.Match(link);
|
||||
|
||||
if (!linkMatch.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
release.MejorTorrentID = linkMatch.Groups[1].Value;
|
||||
release.Title = linkMatch.Groups[2].Value;
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadScraper : IScraper<IEnumerable<Uri>>
|
||||
{
|
||||
public IEnumerable<Uri> Extract(IHtmlDocument html)
|
||||
{
|
||||
return html.QuerySelectorAll("a[href*=\".torrent\"]")
|
||||
.Select(e => e.Attributes["href"].Value)
|
||||
.Select(link => new Uri(WebUri, link));
|
||||
}
|
||||
}
|
||||
|
||||
class Season
|
||||
{
|
||||
public String Title;
|
||||
public int Number;
|
||||
public Uri Link;
|
||||
public TorznabCategory Category; // HDTV or HDTV-720
|
||||
private string _type;
|
||||
public string Type
|
||||
{
|
||||
get { return _type; }
|
||||
set
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case "HDTV":
|
||||
Category = TorznabCatType.TVSD;
|
||||
_type = "SDTV";
|
||||
break;
|
||||
case "HDTV-720p":
|
||||
Category = TorznabCatType.TVHD;
|
||||
_type = "HDTV-720p";
|
||||
break;
|
||||
case "HDTV-1080p":
|
||||
Category = TorznabCatType.TVHD;
|
||||
_type = "HDTV-1080p";
|
||||
break;
|
||||
default:
|
||||
Category = TorznabCatType.TV;
|
||||
_type = "HDTV-720p";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MejorTorrentReleaseInfo : ReleaseInfo
|
||||
{
|
||||
public string MejorTorrentID;
|
||||
public int _season;
|
||||
public int _episodeNumber;
|
||||
private string _categoryText;
|
||||
private string _originalTitle;
|
||||
|
||||
public MejorTorrentReleaseInfo()
|
||||
{
|
||||
this.Category = new List<int>();
|
||||
this.Grabs = 5;
|
||||
this.Files = 1;
|
||||
this.PublishDate = new DateTime();
|
||||
this.Peers = 10;
|
||||
this.Seeders = 10;
|
||||
this.Size = ReleaseInfo.BytesFromGB(1);
|
||||
this._originalTitle = "";
|
||||
}
|
||||
|
||||
public int Season { get { return _season; } set { _season = value; TitleOriginal = _originalTitle; } }
|
||||
|
||||
public int EpisodeNumber { get { return _episodeNumber; } set { _episodeNumber = value; TitleOriginal = _originalTitle; } }
|
||||
|
||||
public string CategoryText {
|
||||
get { return _categoryText; }
|
||||
set
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case "SDTV":
|
||||
Category.Add(TorznabCatType.TVSD.ID);
|
||||
_categoryText = "SDTV";
|
||||
break;
|
||||
case "HDTV":
|
||||
Category.Add(TorznabCatType.TVSD.ID);
|
||||
_categoryText = "SDTV";
|
||||
break;
|
||||
case "HDTV-720p":
|
||||
Category.Add(TorznabCatType.TVHD.ID);
|
||||
_categoryText = "HDTV-720p";
|
||||
break;
|
||||
case "HDTV-1080p":
|
||||
Category.Add(TorznabCatType.TVHD.ID);
|
||||
_categoryText = "HDTV-1080p";
|
||||
break;
|
||||
default:
|
||||
Category.Add(TorznabCatType.TV.ID);
|
||||
_categoryText = "HDTV-720p";
|
||||
break;
|
||||
}
|
||||
TitleOriginal = _originalTitle;
|
||||
}
|
||||
}
|
||||
|
||||
public int FinalEpisodeNumber { get { return (int)(EpisodeNumber + Files - 1); } }
|
||||
|
||||
public string TitleOriginal
|
||||
{
|
||||
get { return _originalTitle; }
|
||||
set
|
||||
{
|
||||
_originalTitle = value;
|
||||
if (_originalTitle != "")
|
||||
{
|
||||
Title = _originalTitle.Replace(' ', '.');
|
||||
Title = char.ToUpper(Title[0]) + Title.Substring(1);
|
||||
}
|
||||
var seasonAndEpisode = "S" + Season.ToString("00") + "E" + EpisodeNumber.ToString("00");
|
||||
if (Files > 1)
|
||||
{
|
||||
seasonAndEpisode += "-" + FinalEpisodeNumber.ToString("00");
|
||||
}
|
||||
Title = String.Join(".", new List<string>() { Title, seasonAndEpisode, CategoryText, "Spanish" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IRequester
|
||||
{
|
||||
Task<IHtmlDocument> MakeRequest(
|
||||
Uri uri,
|
||||
RequestType method = RequestType.GET,
|
||||
IEnumerable<KeyValuePair<string, string>> data = null,
|
||||
Dictionary<string, string> headers = null);
|
||||
}
|
||||
|
||||
class MejorTorrentRequester : IRequester
|
||||
{
|
||||
private MejorTorrent mt;
|
||||
|
||||
public MejorTorrentRequester(MejorTorrent mt)
|
||||
{
|
||||
this.mt = mt;
|
||||
}
|
||||
|
||||
public async Task<IHtmlDocument> MakeRequest(
|
||||
Uri uri,
|
||||
RequestType method = RequestType.GET,
|
||||
IEnumerable<KeyValuePair<string, string>> data = null,
|
||||
Dictionary<string, string> headers = null)
|
||||
{
|
||||
var result = await mt.RequestBytesWithCookies(uri.AbsoluteUri, null, method, null, data, headers);
|
||||
var SearchResultParser = new HtmlParser();
|
||||
var doc = SearchResultParser.Parse(mt.Encoding.GetString(result.Content));
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
|
||||
class MejorTorrentDownloadRequesterDecorator
|
||||
{
|
||||
private IRequester r;
|
||||
|
||||
public MejorTorrentDownloadRequesterDecorator(IRequester r)
|
||||
{
|
||||
this.r = r;
|
||||
}
|
||||
|
||||
public async Task<IHtmlDocument> MakeRequest(IEnumerable<string> ids)
|
||||
{
|
||||
var downloadHtmlTasks = new List<Task<IHtmlDocument>>();
|
||||
var formData = new List<KeyValuePair<string, string>>();
|
||||
int index = 1;
|
||||
ids.ToList().ForEach(id =>
|
||||
{
|
||||
var episodeID = new KeyValuePair<string, string>("episodios[" + index + "]", id);
|
||||
formData.Add(episodeID);
|
||||
index++;
|
||||
});
|
||||
formData.Add(new KeyValuePair<string, string>("total_capis", index.ToString()));
|
||||
formData.Add(new KeyValuePair<string, string>("tabla", "series"));
|
||||
return await r.MakeRequest(DownloadUri, RequestType.POST, formData);
|
||||
}
|
||||
}
|
||||
|
||||
interface IPerformer
|
||||
{
|
||||
Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query);
|
||||
}
|
||||
|
||||
class RssPerformer : IPerformer
|
||||
{
|
||||
private IRequester requester;
|
||||
private IScraper<IEnumerable<KeyValuePair<MejorTorrentReleaseInfo, Uri>>> rssScraper;
|
||||
private IScraper<IEnumerable<MejorTorrentReleaseInfo>> seasonScraper;
|
||||
private IDownloadGenerator downloadGenerator;
|
||||
|
||||
public RssPerformer(
|
||||
IRequester requester,
|
||||
IScraper<IEnumerable<KeyValuePair<MejorTorrentReleaseInfo, Uri>>> rssScraper,
|
||||
IScraper<IEnumerable<MejorTorrentReleaseInfo>> seasonScraper,
|
||||
IDownloadGenerator downloadGenerator)
|
||||
{
|
||||
this.requester = requester;
|
||||
this.rssScraper = rssScraper;
|
||||
this.seasonScraper = seasonScraper;
|
||||
this.downloadGenerator = downloadGenerator;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
||||
{
|
||||
var html = await requester.MakeRequest(NewTorrentsUri);
|
||||
var episodesAndSeasonsUri = rssScraper.Extract(html);
|
||||
|
||||
Task.WaitAll(episodesAndSeasonsUri.ToList().Select(async epAndSeasonUri =>
|
||||
{
|
||||
var episode = epAndSeasonUri.Key;
|
||||
var seasonUri = epAndSeasonUri.Value;
|
||||
await AddMejorTorrentIDs(episode, seasonUri);
|
||||
}).ToArray());
|
||||
|
||||
var episodes = episodesAndSeasonsUri.Select(epAndSeason => epAndSeason.Key).ToList();
|
||||
await downloadGenerator.AddDownloadLinks(episodes);
|
||||
return episodes;
|
||||
}
|
||||
|
||||
private async Task AddMejorTorrentIDs(MejorTorrentReleaseInfo episode, Uri seasonUri)
|
||||
{
|
||||
var html = await requester.MakeRequest(seasonUri);
|
||||
var newEpisodes = seasonScraper.Extract(html);
|
||||
// GET BY EPISODE NUMBER
|
||||
newEpisodes = newEpisodes.Where(e => e.EpisodeNumber == episode.EpisodeNumber);
|
||||
if (newEpisodes.Count() == 0)
|
||||
{
|
||||
throw new Exception("Imposible to detect episode ID in RSS");
|
||||
}
|
||||
episode.MejorTorrentID = newEpisodes.First().MejorTorrentID;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TvShowPerformer : IPerformer
|
||||
{
|
||||
private IRequester requester;
|
||||
private IScraper<IEnumerable<Season>> tvShowScraper;
|
||||
private IScraper<IEnumerable<MejorTorrentReleaseInfo>> seasonScraper;
|
||||
private IDownloadGenerator downloadGenerator;
|
||||
|
||||
public TvShowPerformer(
|
||||
IRequester requester,
|
||||
IScraper<IEnumerable<Season>> tvShowScraper,
|
||||
IScraper<IEnumerable<MejorTorrentReleaseInfo>> seasonScraper,
|
||||
IDownloadGenerator downloadGenerator)
|
||||
{
|
||||
this.requester = requester;
|
||||
this.tvShowScraper = tvShowScraper;
|
||||
this.seasonScraper = seasonScraper;
|
||||
this.downloadGenerator = downloadGenerator;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
||||
{
|
||||
query = FixQuery(query);
|
||||
var seasons = await GetSeasons(query);
|
||||
var episodes = await GetEpisodes(query, seasons);
|
||||
await downloadGenerator.AddDownloadLinks(episodes);
|
||||
if (seasons.Count() > 0)
|
||||
{
|
||||
episodes.ForEach(e => e.TitleOriginal = seasons.First().Title);
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
||||
private TorznabQuery FixQuery(TorznabQuery query)
|
||||
{
|
||||
var seasonRegex = new Regex(@".*?(s\d{1,2})", RegexOptions.IgnoreCase);
|
||||
var episodeRegex = new Regex(@".*?(e\d{1,2})", RegexOptions.IgnoreCase);
|
||||
var seasonMatch = seasonRegex.Match(query.SearchTerm);
|
||||
var episodeMatch = episodeRegex.Match(query.SearchTerm);
|
||||
if (seasonMatch.Success)
|
||||
{
|
||||
query.Season = Int32.Parse(seasonMatch.Groups[1].Value.Substring(1));
|
||||
query.SearchTerm = query.SearchTerm.Replace(seasonMatch.Groups[1].Value, "");
|
||||
}
|
||||
if (episodeMatch.Success)
|
||||
{
|
||||
query.Episode = episodeMatch.Groups[1].Value.Substring(1);
|
||||
query.SearchTerm = query.SearchTerm.Replace(episodeMatch.Groups[1].Value, "");
|
||||
}
|
||||
query.SearchTerm = query.SearchTerm.Trim();
|
||||
return query;
|
||||
}
|
||||
|
||||
private async Task<List<Season>> GetSeasons(TorznabQuery query)
|
||||
{
|
||||
var seasonHtml = await requester.MakeRequest(CreateSearchUri(query.SanitizedSearchTerm));
|
||||
var seasons = tvShowScraper.Extract(seasonHtml);
|
||||
if (query.Season != 0)
|
||||
{
|
||||
seasons = seasons.Where(s => s.Number == query.Season);
|
||||
}
|
||||
if (query.Categories.Count() != 0)
|
||||
{
|
||||
seasons = seasons.Where(s => new List<int>(query.Categories).Contains(s.Category.ID));
|
||||
}
|
||||
return seasons.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<MejorTorrentReleaseInfo>> GetEpisodes(TorznabQuery query, IEnumerable<Season> seasons)
|
||||
{
|
||||
var episodesHtmlTasks = new Dictionary<Season, Task<IHtmlDocument>>();
|
||||
seasons.ToList().ForEach(season =>
|
||||
{
|
||||
episodesHtmlTasks.Add(season, requester.MakeRequest(new Uri(WebUri, season.Link)));
|
||||
});
|
||||
var episodesHtml = await Task.WhenAll(episodesHtmlTasks.Values);
|
||||
var episodes = episodesHtmlTasks.SelectMany(seasonAndHtml =>
|
||||
{
|
||||
var season = seasonAndHtml.Key;
|
||||
var html = seasonAndHtml.Value.Result;
|
||||
var eps = seasonScraper.Extract(html);
|
||||
return eps.ToList().Select(e =>
|
||||
{
|
||||
e.CategoryText = season.Type;
|
||||
return e;
|
||||
});
|
||||
});
|
||||
if (!string.IsNullOrEmpty(query.Episode))
|
||||
{
|
||||
var episodeNumber = Int32.Parse(query.Episode);
|
||||
episodes = episodes.Where(e => e.EpisodeNumber <= episodeNumber && episodeNumber <= e.FinalEpisodeNumber);
|
||||
}
|
||||
return episodes.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
interface IDownloadGenerator
|
||||
{
|
||||
Task AddDownloadLinks(IEnumerable<MejorTorrentReleaseInfo> episodes);
|
||||
}
|
||||
|
||||
class DownloadGenerator : IDownloadGenerator
|
||||
{
|
||||
private IRequester requester;
|
||||
private IScraper<IEnumerable<Uri>> downloadScraper;
|
||||
|
||||
public DownloadGenerator(IRequester requester, IScraper<IEnumerable<Uri>> downloadScraper)
|
||||
{
|
||||
this.requester = requester;
|
||||
this.downloadScraper = downloadScraper;
|
||||
}
|
||||
|
||||
public async Task AddDownloadLinks(IEnumerable<MejorTorrentReleaseInfo> episodes)
|
||||
{
|
||||
var downloadRequester = new MejorTorrentDownloadRequesterDecorator(requester);
|
||||
var downloadHtml = await downloadRequester.MakeRequest(episodes.Select(e => e.MejorTorrentID));
|
||||
var downloads = downloadScraper.Extract(downloadHtml).ToList();
|
||||
|
||||
for (var i = 0; i < downloads.Count; i++)
|
||||
{
|
||||
var e = episodes.ElementAt(i);
|
||||
episodes.ElementAt(i).Link = downloads.ElementAt(i);
|
||||
episodes.ElementAt(i).Guid = downloads.ElementAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -227,8 +227,7 @@ namespace Jackett.Common.Indexers
|
|||
description = release.Description.Split('[');
|
||||
description[1] = "[" + description[1];
|
||||
}
|
||||
else
|
||||
|
||||
|
||||
release.Title = (description[0].Trim() + "." + seasonep.Trim() + "." + releasedata.Trim('.')).Replace(' ', '.');
|
||||
|
||||
// if search is done for S0X than we dont want to put . between S0X and E0X
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
|
||||
</startup>
|
||||
<system.net>
|
||||
<settings>
|
||||
<!-- needed to make the broken incapsula DDoS protection work on windows(e.g. for KickAssTorrent), see https://social.technet.microsoft.com/Forums/de-DE/b10b16d1-8eea-4b52-8aeb-f96ea87135fa/sectionresponseheader-detailcr-must-be-followed-by-lf?forum=powerquery -->
|
||||
<httpWebRequest useUnsafeHeaderParsing="true" />
|
||||
</settings>
|
||||
</system.net>
|
||||
</configuration>
|
|
@ -14,6 +14,7 @@
|
|||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
||||
<RuntimeIdentifier>win</RuntimeIdentifier>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
|
@ -65,25 +66,14 @@
|
|||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="jackett.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CurlSharp\CurlSharp.csproj">
|
||||
<Project>{74420a79-cc16-442c-8b1e-7c1b913844f0}</Project>
|
||||
<Name>CurlSharp</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj">
|
||||
<Project>{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}</Project>
|
||||
<Name>Jackett.Common</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Jackett\Jackett.csproj">
|
||||
<Project>{e636d5f8-68b4-4903-b4ed-ccfd9c9e899f}</Project>
|
||||
<Name>Jackett</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
|
|
|
@ -1,29 +1,100 @@
|
|||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.ServiceProcess;
|
||||
using Jackett.Common;
|
||||
|
||||
namespace Jackett.Service
|
||||
{
|
||||
public partial class Service : ServiceBase
|
||||
{
|
||||
private IProcessService processService;
|
||||
private Process consoleProcess;
|
||||
private Logger logger;
|
||||
private bool serviceStopInitiated;
|
||||
|
||||
public Service()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
RuntimeSettings runtimeSettings = new RuntimeSettings()
|
||||
{
|
||||
CustomLogFileName = "ServiceLog.txt"
|
||||
};
|
||||
|
||||
LogManager.Configuration = LoggingSetup.GetLoggingConfiguration(runtimeSettings);
|
||||
logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
logger.Info("Initiating Jackett Service v" + EnvironmentUtil.JackettVersion);
|
||||
|
||||
processService = new ProcessService(logger);
|
||||
}
|
||||
|
||||
protected override void OnStart(string[] args)
|
||||
{
|
||||
Engine.BuildContainer(new RuntimeSettings(), new WebApi2Module());
|
||||
Engine.Logger.Info("Service starting");
|
||||
Engine.Server.Initalize();
|
||||
Engine.Server.Start();
|
||||
Engine.Logger.Info("Service started");
|
||||
logger.Info("Service starting");
|
||||
serviceStopInitiated = false;
|
||||
StartConsoleApplication();
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
Engine.Logger.Info("Service stopping");
|
||||
Engine.Server.Stop();
|
||||
logger.Info("Service stopping");
|
||||
serviceStopInitiated = true;
|
||||
StopConsoleApplication();
|
||||
}
|
||||
|
||||
private void StartConsoleApplication()
|
||||
{
|
||||
string applicationFolder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
|
||||
|
||||
var exePath = Path.Combine(applicationFolder, "JackettConsole.exe");
|
||||
|
||||
var startInfo = new ProcessStartInfo()
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = exePath,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
consoleProcess = Process.Start(startInfo);
|
||||
consoleProcess.EnableRaisingEvents = true;
|
||||
consoleProcess.Exited += ProcessExited;
|
||||
consoleProcess.ErrorDataReceived += ProcessErrorDataReceived;
|
||||
}
|
||||
|
||||
private void ProcessErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
logger.Error(e.Data);
|
||||
}
|
||||
|
||||
private void ProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
if (!serviceStopInitiated)
|
||||
{
|
||||
logger.Info("Service stop not responsible for process exit");
|
||||
OnStop();
|
||||
}
|
||||
}
|
||||
|
||||
private void StopConsoleApplication()
|
||||
{
|
||||
if (consoleProcess != null && !consoleProcess.HasExited)
|
||||
{
|
||||
consoleProcess.StandardInput.Close();
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
if (consoleProcess != null && !consoleProcess.HasExited)
|
||||
{
|
||||
consoleProcess.Kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -265,10 +265,9 @@ namespace Jackett.Tray
|
|||
|
||||
private void ProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
logger.Info("Tray icon not responsible for process exit");
|
||||
|
||||
if (!closeApplicationInitiated)
|
||||
{
|
||||
logger.Info("Tray icon not responsible for process exit");
|
||||
CloseTrayApplication();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,10 +69,6 @@
|
|||
<Project>{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}</Project>
|
||||
<Name>Jackett.Common</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Jackett.Service\Jackett.Service.csproj">
|
||||
<Project>{bf611f7b-4658-4cb8-aa9e-0736fadaa3ba}</Project>
|
||||
<Name>Jackett.Service</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Jackett\Jackett.csproj">
|
||||
<Project>{e636d5f8-68b4-4903-b4ed-ccfd9c9e899f}</Project>
|
||||
<Name>Jackett</Name>
|
||||
|
|
Loading…
Reference in New Issue