funfile: migrate to yml and add missing categories (#17020)

This commit is contained in:
wastaken7
2026-08-21 22:45:37 -03:00
committed by GitHub
parent 71cafd083a
commit d64b528b1c
2 changed files with 156 additions and 298 deletions
+156
View File
@@ -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;
}
}
}