mirror of
https://github.com/Jackett/Jackett.git
synced 2026-08-30 17:48:48 +08:00
funfile: migrate to yml and add missing categories (#17020)
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
---
|
||||
id: funfile
|
||||
name: FunFile
|
||||
description: "FunFile is a Private Torrent Tracker for MOVIES / TV / GENERAL"
|
||||
language: en-US
|
||||
type: private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://www.funfile.org/
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
- {id: 45, cat: Other, desc: "0Day"}
|
||||
- {id: 44, cat: TV/Anime, desc: "Anime"}
|
||||
- {id: 22, cat: PC, desc: "Applications"}
|
||||
- {id: 43, cat: Audio/Audiobook, desc: "Audio Books"}
|
||||
- {id: 27, cat: Books/EBook, desc: "Ebook"}
|
||||
- {id: 4, cat: PC/Games, desc: "Games"}
|
||||
- {id: 40, cat: Other/Misc, desc: "Miscellaneous"}
|
||||
- {id: 19, cat: Movies, desc: "Movies"}
|
||||
- {id: 6, cat: Audio, desc: "Music"}
|
||||
- {id: 31, cat: PC/Mobile-Other, desc: "Portable"}
|
||||
- {id: 49, cat: Other, desc: "Tutorials"}
|
||||
- {id: 7, cat: TV, desc: "TV"}
|
||||
- {id: 48, cat: XXX/Other, desc: "XXX 0Day"}
|
||||
- {id: 46, cat: XXX, desc: "XXX Movies"}
|
||||
|
||||
modes:
|
||||
search: [q]
|
||||
tv-search: [q, season, ep, imdbid, genre]
|
||||
movie-search: [q, imdbid, genre]
|
||||
music-search: [q]
|
||||
book-search: [q]
|
||||
|
||||
settings:
|
||||
- name: username
|
||||
type: text
|
||||
label: Username
|
||||
- name: password
|
||||
type: password
|
||||
label: Password
|
||||
- name: freeleech
|
||||
type: checkbox
|
||||
label: Search freeleech only
|
||||
default: false
|
||||
- name: info_activity
|
||||
type: info
|
||||
label: Account Inactivity
|
||||
default: "User accounts that are inactive for more than 42 days (ie haven't logged into the site) and NOT PARKED are automatically AND irretrievably deleted. Those with Donor VIP status are immune to the 42 day purging up to 1 year."
|
||||
- name: info_results
|
||||
type: info
|
||||
label: Results Per Page
|
||||
default: "For best results, change the 'Torrents per page' setting to 100 in your profile."
|
||||
|
||||
login:
|
||||
path: login.php
|
||||
method: form
|
||||
form: form[name="login"]
|
||||
inputs:
|
||||
username: "{{ .Config.username }}"
|
||||
password: "{{ .Config.password }}"
|
||||
returnto: ""
|
||||
login: Login
|
||||
captcha:
|
||||
type: image
|
||||
selector: img#captcha
|
||||
input: captcha_code
|
||||
error:
|
||||
- selector: td.mf_content:has(h1:contains("Captcha Error"))
|
||||
- selector: td.mf_content:contains("Incorrect Captcha")
|
||||
- selector: td.mf_content:contains("Login failed")
|
||||
- selector: td.mf_content:contains("Access denied")
|
||||
- selector: td.mf_content:contains("remaining tries")
|
||||
- selector: td.mf_content:contains("Security code")
|
||||
- selector: td.mf_content:contains("Wrong Code")
|
||||
test:
|
||||
path: index.php
|
||||
selector: a[href*="logout.php"]
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: browse.php
|
||||
inputs:
|
||||
$raw: "{{ range .Categories }}c{{.}}=1&{{end}}"
|
||||
search: "{{ if .Query.Genre }}{{ .Query.Genre }} {{ else }}{{ end }}{{ if .Query.IMDBID }}{{ .Query.IMDBID }}{{ else }}{{ .Keywords }}{{ end }}"
|
||||
cat: 0
|
||||
incldead: "{{ if .Config.freeleech }}3{{ else }}1{{ end }}"
|
||||
showspam: 1
|
||||
s_title: "{{ if or .Query.IMDBID .Query.Genre }}{{ else }}1{{ end }}"
|
||||
s_desc: "{{ if .Query.IMDBID }}1{{ else }}{{ end }}"
|
||||
s_tag: "{{ if .Query.Genre }}1{{ else }}{{ end }}"
|
||||
|
||||
rows:
|
||||
selector: "table.mainframe table[cellpadding=\"2\"] > tbody > tr:has(a[href^=\"download.php\"]), table.mainframe table[cellpadding=\"2\"] > tbody > tr[id^=\"ka\"]"
|
||||
after: 1
|
||||
|
||||
fields:
|
||||
category:
|
||||
selector: a[href^="browse.php?cat="]
|
||||
attribute: href
|
||||
filters:
|
||||
- name: querystring
|
||||
args: cat
|
||||
title:
|
||||
selector: a[href^="details.php?id="]
|
||||
attribute: title
|
||||
details:
|
||||
selector: a[href^="details.php?id="]
|
||||
attribute: href
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["&hit=1", ""]
|
||||
download:
|
||||
selector: a[href^="download.php"]
|
||||
attribute: href
|
||||
files:
|
||||
selector: td.row3:nth-child(4)
|
||||
date:
|
||||
selector: td.row3:nth-child(6)
|
||||
filters:
|
||||
- name: timeago
|
||||
size:
|
||||
selector: td.row3:nth-child(8)
|
||||
grabs:
|
||||
selector: td.row3:nth-child(9)
|
||||
seeders:
|
||||
selector: td.row3:nth-child(10)
|
||||
leechers:
|
||||
selector: td.row3:nth-child(11)
|
||||
uploadvolumefactor:
|
||||
selector: td.row2 table tr:nth-child(3) td:nth-child(1)
|
||||
optional: true
|
||||
default: 1
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["X", ""]
|
||||
downloadvolumefactor:
|
||||
selector: td.row2 table tr:nth-child(3) td:nth-child(2)
|
||||
optional: true
|
||||
default: 1
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["X", ""]
|
||||
genre:
|
||||
selector: td.row2 span[style="float:left"]
|
||||
filters:
|
||||
- name: validate
|
||||
args: "action, adventure, animation, biography, comedy, crime, documentary, drama, family, fantasy, game-show, history, home_&_garden, home_and_garden, horror, music, musical, mystery, news, reality, reality-tv, romance, sci-fi, science-fiction, short, sport, talk-show, thriller, travel, war, western"
|
||||
description:
|
||||
text: "{{ .Result.genre }}"
|
||||
minimumratio:
|
||||
text: 1.0
|
||||
minimumseedtime:
|
||||
# 2 days (as seconds = 2 x 24 x 60 x 60)
|
||||
text: 172800
|
||||
# TBDev
|
||||
@@ -1,298 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Parser;
|
||||
using Jackett.Common.Models;
|
||||
using Jackett.Common.Models.IndexerConfig;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using Jackett.Common.Utils.Clients;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using static Jackett.Common.Models.IndexerConfig.ConfigurationData;
|
||||
|
||||
namespace Jackett.Common.Indexers.Definitions
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class FunFile : IndexerBase
|
||||
{
|
||||
public override string Id => "funfile";
|
||||
public override string Name => "FunFile";
|
||||
public override string Description => "FunFile is a Private general tracker";
|
||||
public override string SiteLink { get; protected set; } = "https://www.funfile.org/";
|
||||
public override Encoding Encoding => Encoding.GetEncoding("iso-8859-1");
|
||||
public override string Language => "en-US";
|
||||
public override string Type => "private";
|
||||
|
||||
public override TorznabCapabilities TorznabCaps => SetCapabilities();
|
||||
|
||||
private string LoginUrl => SiteLink + "login.php";
|
||||
private string TakeLoginUrl => SiteLink + "takelogin.php";
|
||||
private string SearchUrl => SiteLink + "browse.php";
|
||||
|
||||
private new ConfigurationDataCaptchaLogin configData
|
||||
{
|
||||
get => (ConfigurationDataCaptchaLogin)base.configData;
|
||||
set => base.configData = value;
|
||||
}
|
||||
|
||||
public FunFile(IIndexerConfigurationService configService, WebClient w, Logger l, IProtectionService ps,
|
||||
ICacheService cs)
|
||||
: base(configService: configService,
|
||||
client: w,
|
||||
logger: l,
|
||||
p: ps,
|
||||
cacheService: cs,
|
||||
configData: new ConfigurationDataCaptchaLogin("For best results, change the 'Torrents per page' setting to 100 in your profile."))
|
||||
{
|
||||
configData.AddDynamic("freeleech", new BoolConfigurationItem("Search freeleech only") { Value = false });
|
||||
configData.AddDynamic("Account Inactivity", new DisplayInfoConfigurationItem("Account Inactivity", "User accounts that are inactive for more than 42 days (ie haven't logged into the site) and NOT PARKED are automatically AND irretrievably deleted. Those with Donor VIP status are immune to the 42 day purging up to 1 year."));
|
||||
}
|
||||
|
||||
private TorznabCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new TorznabCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId, TvSearchParam.Genre
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.Genre
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
},
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
BookSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(45, TorznabCatType.Other, "0Day");
|
||||
caps.Categories.AddCategoryMapping(44, TorznabCatType.TVAnime, "Anime");
|
||||
caps.Categories.AddCategoryMapping(22, TorznabCatType.PC, "Applications");
|
||||
caps.Categories.AddCategoryMapping(43, TorznabCatType.AudioAudiobook, "Audio Books");
|
||||
caps.Categories.AddCategoryMapping(27, TorznabCatType.Books, "Ebook");
|
||||
caps.Categories.AddCategoryMapping(4, TorznabCatType.PCGames, "Games");
|
||||
caps.Categories.AddCategoryMapping(40, TorznabCatType.OtherMisc, "Miscellaneous");
|
||||
caps.Categories.AddCategoryMapping(19, TorznabCatType.Movies, "Movies");
|
||||
caps.Categories.AddCategoryMapping(6, TorznabCatType.Audio, "Music");
|
||||
caps.Categories.AddCategoryMapping(31, TorznabCatType.PCMobileOther, "Portable");
|
||||
caps.Categories.AddCategoryMapping(49, TorznabCatType.Other, "Tutorials");
|
||||
caps.Categories.AddCategoryMapping(7, TorznabCatType.TV, "TV");
|
||||
caps.Categories.AddCategoryMapping(48, TorznabCatType.XXXOther, "XXX 0Day");
|
||||
caps.Categories.AddCategoryMapping(46, TorznabCatType.XXX, "XXX Movies");
|
||||
|
||||
return caps;
|
||||
}
|
||||
public override async Task<ConfigurationData> GetConfigurationForSetup()
|
||||
{
|
||||
// Some IP addresses get a captcha
|
||||
var loginPage = await RequestWithCookiesAsync(LoginUrl, string.Empty);
|
||||
var parser = new HtmlParser();
|
||||
using var document = parser.ParseDocument(loginPage.ContentString);
|
||||
var qCaptchaImg = document.QuerySelector("img#captcha");
|
||||
if (qCaptchaImg != null)
|
||||
{
|
||||
var captchaUrl = qCaptchaImg.GetAttribute("src");
|
||||
var captchaImage = await RequestWithCookiesAsync(captchaUrl, loginPage.Cookies);
|
||||
configData.CaptchaImage.Value = captchaImage.ContentBytes;
|
||||
}
|
||||
else
|
||||
{
|
||||
configData.CaptchaImage.Value = Array.Empty<byte>();
|
||||
}
|
||||
configData.CaptchaCookie.Value = loginPage.Cookies;
|
||||
UpdateCookieHeader(loginPage.Cookies);
|
||||
return configData;
|
||||
}
|
||||
public override async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
|
||||
{
|
||||
LoadValuesFromJson(configJson);
|
||||
|
||||
var pairs = new Dictionary<string, string> {
|
||||
{ "username", configData.Username.Value },
|
||||
{ "password", configData.Password.Value },
|
||||
{ "returnto", "" },
|
||||
{ "login", "Login" }
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(configData.CaptchaText.Value))
|
||||
{
|
||||
pairs.Add("captcha_code", configData.CaptchaText.Value);
|
||||
}
|
||||
var result = await RequestLoginAndFollowRedirect(TakeLoginUrl, pairs, configData.CaptchaCookie.Value, true);
|
||||
await ConfigureIfOK(result.Cookies, result.ContentString?.Contains("logout.php") == true, () =>
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
using var dom = parser.ParseDocument(result.ContentString);
|
||||
var errorMessage = dom.QuerySelector("td.mf_content")?.TextContent.Trim();
|
||||
|
||||
throw new ExceptionWithConfigData(errorMessage ?? "Login failed.", configData);
|
||||
});
|
||||
return IndexerConfigurationStatus.RequiresTesting;
|
||||
}
|
||||
|
||||
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
||||
{
|
||||
var releases = new List<ReleaseInfo>();
|
||||
|
||||
/*
|
||||
* notes:
|
||||
* can search titles & filesnames with &s_title=1 (default)
|
||||
* and tags with &s_tag=1
|
||||
* and descriptions with &s_desc=1
|
||||
* however the parms are used in an OR fashion and not as an AND
|
||||
* so ?search=reality+love+s04e19&s_title=1&s_tag=1
|
||||
* will find Love Island S04E19 as well as My Kitchen Rules S12E02
|
||||
* since the former has a match for Love in the title, and the latter has a tag for Reality-TV.
|
||||
*
|
||||
* FunFiles only has genres for movies and tv
|
||||
*/
|
||||
|
||||
var validList = new List<string>
|
||||
{
|
||||
"action",
|
||||
"adventure",
|
||||
"animation",
|
||||
"biography",
|
||||
"comedy",
|
||||
"crime",
|
||||
"documentary",
|
||||
"drama",
|
||||
"family",
|
||||
"fantasy",
|
||||
"game-show",
|
||||
"history",
|
||||
"home_&_garden",
|
||||
"home_and_garden",
|
||||
"horror",
|
||||
"music",
|
||||
"musical",
|
||||
"mystery",
|
||||
"news",
|
||||
"reality",
|
||||
"reality-tv",
|
||||
"romance",
|
||||
"sci-fi",
|
||||
"science-fiction",
|
||||
"short",
|
||||
"sport",
|
||||
"talk-show",
|
||||
"thriller",
|
||||
"travel",
|
||||
"war",
|
||||
"western"
|
||||
};
|
||||
|
||||
var qc = new NameValueCollection
|
||||
{
|
||||
{ "cat", "0" },
|
||||
// incldead= 0 active, 1 incldead, 2 deadonly, 3 freeleech, 4 sceneonly, 5 requestsonly, 8 packsonly
|
||||
{ "incldead", ((BoolConfigurationItem)configData.GetDynamic("freeleech")).Value ? "3" : "1" },
|
||||
{ "showspam", "1" },
|
||||
{ "s_title", "1" }
|
||||
};
|
||||
|
||||
var queryCats = MapTorznabCapsToTrackers(query);
|
||||
queryCats.ForEach(cat => qc.Set($"c{cat}", "1"));
|
||||
|
||||
if (query.IsImdbQuery)
|
||||
{
|
||||
qc.Set("search", query.ImdbID);
|
||||
qc.Set("s_desc", "1");
|
||||
}
|
||||
else if (query.IsGenreQuery)
|
||||
{
|
||||
qc.Set("search", query.Genre + " " + query.GetQueryString());
|
||||
qc.Set("s_tag", "1");
|
||||
}
|
||||
else
|
||||
qc.Set("search", query.GetQueryString());
|
||||
|
||||
var searchUrl = SearchUrl + "?" + qc.GetQueryString();
|
||||
var results = await RequestWithCookiesAndRetryAsync(searchUrl);
|
||||
|
||||
if (results.IsRedirect) // re-login
|
||||
{
|
||||
await ApplyConfiguration(null);
|
||||
results = await RequestWithCookiesAndRetryAsync(searchUrl);
|
||||
}
|
||||
|
||||
char[] delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' };
|
||||
|
||||
try
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
using var dom = parser.ParseDocument(results.ContentString);
|
||||
|
||||
var rows = dom.QuerySelectorAll("table.mainframe table[cellpadding=\"2\"] > tbody > tr:has(td.row3)");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var qDownloadLink = row.QuerySelector("a[href^=\"download.php\"]");
|
||||
if (qDownloadLink == null)
|
||||
throw new Exception("Download links not found. Make sure you can download from the website.");
|
||||
|
||||
var link = new Uri(SiteLink + qDownloadLink.GetAttribute("href"));
|
||||
|
||||
var qDetailsLink = row.QuerySelector("a[href^=\"details.php?id=\"]");
|
||||
var title = qDetailsLink?.GetAttribute("title")?.Trim();
|
||||
var details = new Uri(SiteLink + qDetailsLink?.GetAttribute("href")?.Replace("&hit=1", ""));
|
||||
|
||||
var categoryLink = row.QuerySelector("a[href^=\"browse.php?cat=\"]")?.GetAttribute("href");
|
||||
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "cat");
|
||||
|
||||
var seeders = ParseUtil.CoerceInt(row.Children[9].TextContent);
|
||||
var leechers = ParseUtil.CoerceInt(row.Children[10].TextContent);
|
||||
|
||||
var release = new ReleaseInfo
|
||||
{
|
||||
Guid = link,
|
||||
Link = link,
|
||||
Details = details,
|
||||
Title = title,
|
||||
Category = MapTrackerCatToNewznab(cat),
|
||||
Size = ParseUtil.GetBytes(row.Children[7].TextContent),
|
||||
Files = ParseUtil.CoerceInt(row.Children[3].TextContent),
|
||||
Grabs = ParseUtil.CoerceInt(row.Children[8].TextContent),
|
||||
Seeders = seeders,
|
||||
Peers = leechers + seeders,
|
||||
PublishDate = DateTimeUtil.FromTimeAgo(row.Children[5].TextContent),
|
||||
DownloadVolumeFactor = 1,
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800 // 48 hours
|
||||
};
|
||||
|
||||
var nextRow = row.NextElementSibling;
|
||||
if (nextRow != null)
|
||||
{
|
||||
var qStats = nextRow.QuerySelector("table > tbody > tr:nth-child(3)");
|
||||
release.UploadVolumeFactor = ParseUtil.CoerceDouble(qStats?.Children[0].TextContent.Replace("X", ""));
|
||||
release.DownloadVolumeFactor = ParseUtil.CoerceDouble(qStats?.Children[1].TextContent.Replace("X", ""));
|
||||
|
||||
release.Description = nextRow.QuerySelector("span[style=\"float:left\"]")?.TextContent.Trim();
|
||||
var genres = release.Description.ToLower().Replace(" & ", "_&_").Replace(" and ", "_and_");
|
||||
|
||||
var releaseGenres = validList.Intersect(genres.Split(delimiters, StringSplitOptions.RemoveEmptyEntries));
|
||||
release.Genres = releaseGenres.Select(x => x.Trim().Replace("_", " ")).ToList();
|
||||
}
|
||||
|
||||
releases.Add(release);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnParseError(results.ContentString, ex);
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user