2024-10-04 15:54:08 -04:00

832 lines
34 KiB
HTML

<style>
#rssdownloaderpage_content {
height: calc(100% - 30px);
}
#RssDownloader {
height: 100%;
}
#leftRssDownloaderColumn {
width: 25%;
float: left;
}
#rulesTable,
#rssDownloaderArticlesTable {
overflow: auto;
width: 100%;
height: 100%;
}
#centerRssDownloaderColumn {
width: 50%;
float: left;
}
#rightRssDownloaderColumn {
width: 25%;
float: left;
}
.fullWidth {
width: 100%;
max-width: none;
box-sizing: border-box;
}
.noWrap {
white-space: nowrap;
}
#rssDownloaderFeeds {
height: calc(100% - 355px);
overflow: hidden;
}
#rssDownloaderFeedsTable {
height: calc(100% - 21px);
width: 100%;
overflow: auto;
}
#saveButton {
margin-top: 5px;
width: 100%;
}
.articleTableFeed td {
font-weight: bold;
}
.articleTableArticle td {
padding-left: 22px;
}
#rssDownloaderDisabled {
color: var(--color-text-red);
font-style: italic;
margin-bottom: 10px;
}
#rulesTable table,
#rssDownloaderFeedsTable table,
#rssDownloaderArticlesTable table {
width: 100%;
}
#ignoreDaysValue {
width: 4em;
}
#lastMatchDiv {
float: right;
}
#ruleSettings {
padding: 4px 10px 4px 10px
}
#topMenuBar {
height: 26px;
}
#rulesTableDesc {
vertical-align: middle;
}
.imageButton {
height: 22px;
width: 24px;
margin: 1px 2px 1px 2px;
border: 1px solid;
border-radius: 2px;
background-color: #efefef;
border-color: #767676;
background-position: center center;
vertical-align: middle;
}
.imageButton:hover {
background-color: #e5e5e5;
border-color: #4f4f4f;
}
.imageButton:active {
background-color: #f5f5f5;
border-color: #8d8d8d;
}
#newRuleButton {
float: right;
background-image: url('images/list-add.svg');
}
#deleteRuleButton {
float: right;
background-image: url('images/edit-clear.svg');
}
</style>
<div id="RssDownloader" class="RssDownloader">
<div id="rssDownloaderDisabled" class="invisible">
Auto downloading of RSS torrents is disabled now! You can enable it in application settings.
</div>
<div id="leftRssDownloaderColumn">
<div id="topMenuBar">
<b id="rulesTableDesc">Download Rules</b>
<button type="button" id="newRuleButton" aria-label="Add rule" class="imageButton" onclick="qBittorrent.RssDownloader.addRule()"></button>
<button type="button" id="deleteRuleButton" aria-label="Remove rule" class="imageButton" onclick="qBittorrent.RssDownloader.removeSelectedRule()"></button>
</div>
<div id="rulesTable">
<div id="rulesSelectionCheckBoxList">
<div id="rssDownloaderRuleFixedHeaderDiv" class="dynamicTableFixedHeaderDiv invisible">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssDownloaderRuleTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<div id="centerRssDownloaderColumn">
<fieldset class="settings" id="ruleSettings">
<legend>Rule Definition</legend>
<div class="formRow">
<input disabled type="checkbox" id="useRegEx" onclick="qBittorrent.RssDownloader.setElementTitles()">
<label for="useRegEx">Use Regular Expressions</label>
</div>
<table class="fullWidth">
<tbody>
<tr>
<td>
<label for="mustContainText" class="noWrap">Must Contain:</label>
</td>
<td class="fullWidth">
<input disabled type="text" id="mustContainText" class="fullWidth" autocorrect="off" autocapitalize="none">
</td>
</tr>
<tr>
<td>
<label for="mustNotContainText" class="noWrap">Must Not Contain:</label>
</td>
<td class="fullWidth">
<input disabled type="text" id="mustNotContainText" class="fullWidth" autocorrect="off" autocapitalize="none">
</td>
</tr>
<tr>
<td>
<label for="episodeFilterText" class="noWrap">Episode Filter:</label>
</td>
<td class="fullWidth">
<input disabled type="text" id="episodeFilterText" class="fullWidth" autocorrect="off" autocapitalize="none">
</td>
</tr>
</tbody>
</table>
<div class="formRow" title="Smart Episode Filter will check the episode number to prevent downloading of duplicates.
Supports the formats: S01E01, 1x1, 2017.12.31 and 31.12.2017 (Date formats also support - as a separator)">
<input disabled type="checkbox" id="useSmartFilter">
<label for="useSmartFilter">Use Smart Episode Filter</label>
</div>
<hr>
<table class="fullWidth">
<tbody>
<tr>
<td>
<label class="noWrap" for="assignCategoryCombobox">Assign Category:</label>
</td>
<td class="fullWidth">
<select disabled id="assignCategoryCombobox" class="fullWidth">
<option value=""></option>
</select>
</td>
</tr>
<tr>
<td>
<label class="noWrap" for="ruleAddTags">Add Tags:</label>
</td>
<td class="fullWidth">
<input type="text" id="ruleAddTags" class="fullWidth" autocapitalize="none" autocorrect="off">
</td>
</tr>
</tbody>
</table>
<div class="formRow">
<input disabled type="checkbox" id="savetoDifferentDir">
<label for="savetoDifferentDir">Save to a Different Directory</label>
</div>
<table class="fullWidth">
<tbody>
<tr>
<td>
<label class="noWrap" for="saveToText">Save to:</label>
</td>
<td class="fullWidth">
<input disabled type="text" class="fullWidth" id="saveToText" autocorrect="off" autocapitalize="none">
</td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<td><label for="ignoreDaysValue">Ignore Subsequent Matches for (0 to Disable)</label></td>
<td><input type="number" id="ignoreDaysValue" min="0"> days</td>
</tr>
</tbody>
</table>
<div id="lastMatchDiv">
<span id="lastMatchText">Last Match: Unknown</span>
</div>
<table class="fullWidth">
<tbody>
<tr>
<td>
<label class="noWrap" for="addStoppedCombobox">Add Stopped:</label>
</td>
<td class="fullWidth">
<select disabled id="addStoppedCombobox" class="fullWidth">
<option value="default">Use global settings</option>
<option value="always">Always</option>
<option value="never">Never</option>
</select>
</td>
</tr>
</tbody>
</table>
<table class="fullWidth">
<tbody>
<tr>
<td>
<label class="noWrap" for="contentLayoutCombobox">Torrent content layout:</label>
</td>
<td class="fullWidth">
<select disabled id="contentLayoutCombobox" class="fullWidth">
<option value="Default">Use global settings</option>
<option value="Original">Original</option>
<option value="Subfolder">Create subfolder</option>
<option value="NoSubfolder">Don't create subfolder</option>
</select>
</td>
</tr>
</tbody>
</table>
</fieldset>
<fieldset class="settings" id="rssDownloaderFeeds">
<legend>Apply Rule to Feeds:</legend>
<div id="rssDownloaderFeedsTable">
<div id="rssDownloaderFeedSelectionFixedHeaderDiv" class="dynamicTableFixedHeaderDiv invisible">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssDownloaderFeedSelectionTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</fieldset>
<button disabled type="button" id="saveButton" onclick="qBittorrent.RssDownloader.saveSettings()">
Save
</button>
</div>
<div id="rightRssDownloaderColumn">
<b id="articleTableDesc">Matching RSS Articles</b>
<div id="rssDownloaderArticlesTable">
<div id="rssDownloaderArticlesFixedHeaderDiv" class="dynamicTableFixedHeaderDiv invisible">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssDownloaderArticlesTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<ul id="rssDownloaderRuleMenu" class="contextMenu">
<li><a href="#addRule"><img src="images/list-add.svg" alt="Add new rule..."> Add new rule...</a></li>
<li><a href="#deleteRule"><img src="images/edit-clear.svg" alt="Delete rule"> Delete rule</a></li>
<li class="separator"><a href="#renameRule"><img src="images/edit-rename.svg" alt="Rename rule..."> Rename rule...</a></li>
<li class="separator"><a href="#clearDownloadedEpisodes"><img src="images/edit-clear.svg" alt="Clear downloaded episodes..."> Clear downloaded episodes...</a></li>
</ul>
<script>
"use strict";
window.qBittorrent ??= {};
window.qBittorrent.RssDownloader ??= (() => {
const exports = () => {
return {
setup: setup,
updateRulesList: updateRulesList,
showRule: showRule,
renameRule: renameRule,
modifyRuleState: modifyRuleState,
saveSettings: saveSettings,
rssDownloaderRulesTable: rssDownloaderRulesTable,
rssDownloaderFeedSelectionTable: rssDownloaderFeedSelectionTable,
setElementTitles: setElementTitles,
addRule: addRule,
removeSelectedRule: removeSelectedRule
};
};
const rssDownloaderRulesTable = new window.qBittorrent.DynamicTable.RssDownloaderRulesTable();
const rssDownloaderFeedSelectionTable = new window.qBittorrent.DynamicTable.RssDownloaderFeedSelectionTable();
const rssDownloaderArticlesTable = new window.qBittorrent.DynamicTable.RssDownloaderArticlesTable();
let rulesList = {};
let feedList = [];
const setup = () => {
const pref = window.parent.qBittorrent.Cache.preferences.get();
if (!pref.rss_auto_downloading_enabled)
$("rssDownloaderDisabled").removeClass("invisible");
// recalculate height
const warningHeight = $("rssDownloaderDisabled").getBoundingClientRect().height;
$("leftRssDownloaderColumn").style.height = "calc(100% - " + warningHeight + "px)";
$("centerRssDownloaderColumn").style.height = "calc(100% - " + warningHeight + "px)";
$("rightRssDownloaderColumn").style.height = "calc(100% - " + warningHeight + "px)";
$("rulesTable").style.height = "calc(100% - " + $("rulesTableDesc").getBoundingClientRect().height + "px)";
$("rssDownloaderArticlesTable").style.height = "calc(100% - " + $("articleTableDesc").getBoundingClientRect().height + "px)";
const centerRowNotTableHeight = $("saveButton").getBoundingClientRect().height
+ $("ruleSettings").getBoundingClientRect().height + 15;
$("rssDownloaderFeeds").style.height = "calc(100% - " + centerRowNotTableHeight + "px)";
// firefox calculates the height of the table inside fieldset differently and thus doesn't need the offset
if (navigator.userAgent.toLowerCase().indexOf("firefox") > -1) {
$("rssDownloaderFeedsTable").style.height = "100%";
}
else {
const outsideTableHeight = ($("rssDownloaderFeedsTable").getBoundingClientRect().top - $("rssDownloaderFeeds").getBoundingClientRect().top) - 10;
$("rssDownloaderFeedsTable").style.height = "calc(100% - " + outsideTableHeight + "px)";
}
const rssDownloaderRuleContextMenu = new window.qBittorrent.ContextMenu.RssDownloaderRuleContextMenu({
targets: "",
menu: "rssDownloaderRuleMenu",
actions: {
addRule: addRule,
deleteRule: removeSelectedRule,
renameRule: (el) => {
renameRule(rssDownloaderRulesTable.getRow(rssDownloaderRulesTable.selectedRows[0]).full_data.name);
},
clearDownloadedEpisodes: (el) => {
clearDownloadedEpisodes(rssDownloaderRulesTable.selectedRows
.map((sRow) => rssDownloaderRulesTable.getRow(sRow).full_data.name));
}
},
offsets: {
x: -22,
y: -4
}
});
rssDownloaderRulesTable.setup("rssDownloaderRuleTableDiv", "rssDownloaderRuleFixedHeaderDiv", rssDownloaderRuleContextMenu);
rssDownloaderFeedSelectionTable.setup("rssDownloaderFeedSelectionTableDiv", "rssDownloaderFeedSelectionFixedHeaderDiv");
rssDownloaderArticlesTable.setup("rssDownloaderArticlesTableDiv", "rssDownloaderArticlesFixedHeaderDiv");
rssDownloaderRuleContextMenu.addTarget($("rulesTable"));
// deselect feed when clicking on empty part of table
$("rulesTable").addEventListener("click", (e) => {
rssDownloaderRulesTable.deselectAll();
rssDownloaderRulesTable.deselectRow();
showRule("");
});
$("rulesTable").addEventListener("contextmenu", (e) => {
if (e.toElement.nodeName === "DIV") {
rssDownloaderRulesTable.deselectAll();
rssDownloaderRulesTable.deselectRow();
rssDownloaderRuleContextMenu.updateMenuItems();
showRule("");
}
});
// get all categories and add to combobox
new Request.JSON({
url: "api/v2/torrents/categories",
method: "get",
noCache: true,
onSuccess: (response) => {
const combobox = $("assignCategoryCombobox");
for (const cat in response) {
if (!Object.hasOwn(response, cat))
continue;
const option = document.createElement("option");
option.text = option.value = cat;
combobox.add(option);
}
}
}).send();
// get all rss feed
new Request.JSON({
url: "api/v2/rss/items",
method: "get",
noCache: true,
data: {
withData: false
},
onSuccess: (response) => {
feedList = [];
const flatten = (root) => {
for (const child in root) {
if (root[child].uid !== undefined)
feedList.push({ name: child, url: root[child].url });
else
flatten(root[child]);
}
};
flatten(response);
}
}).send();
$("savetoDifferentDir").addEventListener("click", () => {
$("saveToText").disabled = !$("savetoDifferentDir").checked;
});
updateRulesList();
};
const updateRulesList = () => {
// get all rules
new Request.JSON({
url: "api/v2/rss/rules",
method: "get",
noCache: true,
onSuccess: (response) => {
rssDownloaderRulesTable.clear();
let rowCount = 0;
for (const rule in response) {
if (!Object.hasOwn(response, rule))
continue;
rssDownloaderRulesTable.updateRowData({
rowId: rowCount++,
checked: response[rule].enabled,
name: rule
});
}
rssDownloaderRulesTable.updateTable(false);
rulesList = response;
}
}).send();
};
const modifyRuleState = (rule, setting, newState, callback = () => {}) => {
rulesList[rule][setting] = newState;
new Request({
url: "api/v2/rss/setRule",
method: "post",
data: {
ruleName: rule,
ruleDef: JSON.stringify(rulesList[rule])
},
onSuccess: () => {
callback();
}
}).send();
};
const addRule = () => {
new MochaUI.Window({
id: "newRulePage",
icon: "images/qbittorrent-tray.svg",
title: "New rule name",
loadMethod: "iframe",
contentURL: "newrule.html",
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const renameRule = (rule) => {
new MochaUI.Window({
id: "renameRulePage",
icon: "images/qbittorrent-tray.svg",
title: "Rule renaming",
loadMethod: "iframe",
contentURL: "rename_rule.html?rule=" + encodeURIComponent(rule),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const removeSelectedRule = () => {
if (rssDownloaderRulesTable.selectedRows.length === 0)
return;
removeRules(rssDownloaderRulesTable.selectedRows.map((sRow) =>
rssDownloaderRulesTable.getRow(sRow).full_data.name));
};
const removeRules = (rules) => {
const encodedRules = rules.map((rule) => encodeURIComponent(rule));
new MochaUI.Window({
id: "removeRulePage",
icon: "images/qbittorrent-tray.svg",
title: "Rule deletion confirmation",
loadMethod: "iframe",
contentURL: "confirmruledeletion.html?rules=" + encodeURIComponent(encodedRules.join("|")),
scrollbars: false,
resizable: false,
maximizable: false,
width: 360,
height: 80
});
};
const clearDownloadedEpisodes = (rules) => {
const encodedRules = rules.map((rule) => encodeURIComponent(rule));
new MochaUI.Window({
id: "clearRulesPage",
icon: "images/qbittorrent-tray.svg",
title: "New rule name",
loadMethod: "iframe",
contentURL: "confirmruleclear.html?rules=" + encodeURIComponent(encodedRules.join("|")),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 85
});
};
const saveSettings = () => {
const lastSelectedRow = rssDownloaderRulesTable.selectedRows.at(-1);
const rule = rssDownloaderRulesTable.getRow(lastSelectedRow).full_data.name;
rulesList[rule].useRegex = $("useRegEx").checked;
rulesList[rule].mustContain = $("mustContainText").value;
rulesList[rule].mustNotContain = $("mustNotContainText").value;
rulesList[rule].episodeFilter = $("episodeFilterText").value;
rulesList[rule].smartFilter = $("useSmartFilter").checked;
rulesList[rule].ignoreDays = parseInt($("ignoreDaysValue").value, 10);
rulesList[rule].affectedFeeds = [...rssDownloaderFeedSelectionTable.getRowValues()]
.filter((row) => row.full_data.checked)
.map((row) => row.full_data.url);
rulesList[rule].torrentParams.category = $("assignCategoryCombobox").value;
rulesList[rule].torrentParams.tags = $("ruleAddTags").value.split(",");
if ($("savetoDifferentDir").checked) {
rulesList[rule].torrentParams.save_path = $("saveToText").value;
rulesList[rule].torrentParams.use_auto_tmm = false;
}
else {
rulesList[rule].torrentParams.save_path = "";
}
switch ($("addStoppedCombobox").value) {
case "default":
rulesList[rule].torrentParams.stopped = null;
break;
case "always":
rulesList[rule].torrentParams.stopped = true;
break;
case "never":
rulesList[rule].torrentParams.stopped = false;
break;
}
switch ($("contentLayoutCombobox").value) {
case "Default":
rulesList[rule].torrentParams.content_layout = null;
break;
case "Original":
rulesList[rule].torrentParams.content_layout = "Original";
break;
case "Subfolder":
rulesList[rule].torrentParams.content_layout = "Subfolder";
break;
case "NoSubfolder":
rulesList[rule].torrentParams.content_layout = "NoSubfolder";
break;
}
new Request({
url: "api/v2/rss/setRule",
method: "post",
data: {
ruleName: rule,
ruleDef: JSON.stringify(rulesList[rule])
},
onSuccess: () => {
updateMatchingArticles(rule);
}
}).send();
};
const updateMatchingArticles = (ruleName) => {
new Request.JSON({
url: "api/v2/rss/matchingArticles",
method: "get",
noCache: true,
data: {
ruleName: ruleName
},
onSuccess: (response) => {
rssDownloaderArticlesTable.clear();
let rowCount = 0;
for (const feed in response) {
if (!Object.hasOwn(response, feed))
continue;
rssDownloaderArticlesTable.updateRowData({
rowId: rowCount++,
name: feed,
isFeed: true
});
response[feed].each((article) => {
rssDownloaderArticlesTable.updateRowData({
rowId: rowCount++,
name: article,
isFeed: false
});
});
}
rssDownloaderArticlesTable.updateTable(false);
}
}).send();
};
const showRule = (ruleName) => {
if (ruleName === "") {
// disable all
$("saveButton").disabled = true;
$("useRegEx").disabled = true;
$("mustContainText").disabled = true;
$("mustNotContainText").disabled = true;
$("episodeFilterText").disabled = true;
$("useSmartFilter").disabled = true;
$("assignCategoryCombobox").disabled = true;
$("ruleAddTags").disabled = true;
$("savetoDifferentDir").disabled = true;
$("saveToText").disabled = true;
$("ignoreDaysValue").disabled = true;
$("addStoppedCombobox").disabled = true;
$("contentLayoutCombobox").disabled = true;
// reset all boxes
$("useRegEx").checked = false;
$("mustContainText").value = "";
$("mustNotContainText").value = "";
$("episodeFilterText").value = "";
$("useSmartFilter").checked = false;
$("assignCategoryCombobox").value = "default";
$("ruleAddTags").value = "";
$("savetoDifferentDir").checked = false;
$("saveToText").value = "";
$("ignoreDaysValue").value = 0;
$("lastMatchText").textContent = "Last Match: Unknown";
$("addStoppedCombobox").value = "default";
$("contentLayoutCombobox").value = "Default";
rssDownloaderFeedSelectionTable.clear();
rssDownloaderArticlesTable.clear();
$("mustContainText").title = "";
$("mustNotContainText").title = "";
$("episodeFilterText").title = "";
}
else {
// enable all
$("saveButton").disabled = false;
$("useRegEx").disabled = false;
$("mustContainText").disabled = false;
$("mustNotContainText").disabled = false;
$("episodeFilterText").disabled = false;
$("useSmartFilter").disabled = false;
$("assignCategoryCombobox").disabled = false;
$("ruleAddTags").disabled = false;
$("savetoDifferentDir").disabled = false;
$("ignoreDaysValue").disabled = false;
$("addStoppedCombobox").disabled = false;
$("contentLayoutCombobox").disabled = false;
// load rule settings
$("useRegEx").checked = rulesList[ruleName].useRegex;
$("mustContainText").value = rulesList[ruleName].mustContain;
$("mustNotContainText").value = rulesList[ruleName].mustNotContain;
$("episodeFilterText").value = rulesList[ruleName].episodeFilter;
$("useSmartFilter").checked = rulesList[ruleName].smartFilter;
$("assignCategoryCombobox").value = rulesList[ruleName].torrentParams.category ? rulesList[ruleName].torrentParams.category : "default";
$("ruleAddTags").value = rulesList[ruleName].torrentParams.tags.join(",");
$("savetoDifferentDir").checked = rulesList[ruleName].torrentParams.save_path !== "";
$("saveToText").disabled = !$("savetoDifferentDir").checked;
$("saveToText").value = rulesList[ruleName].torrentParams.save_path;
$("ignoreDaysValue").value = rulesList[ruleName].ignoreDays;
// calculate days since last match
if (rulesList[ruleName].lastMatch !== "") {
const timeDiffInMs = new Date().getTime() - new Date(rulesList[ruleName].lastMatch).getTime();
const daysAgo = Math.floor(timeDiffInMs / (1000 * 60 * 60 * 24)).toString();
$("lastMatchText").textContent = " Last Match: %1 days ago".replace("%1", daysAgo);
}
else {
$("lastMatchText").textContent = "Last Match: Unknown";
}
if ((rulesList[ruleName].torrentParams.stopped === undefined) || (rulesList[ruleName].torrentParams.stopped === null))
$("addStoppedCombobox").value = "default";
else
$("addStoppedCombobox").value = rulesList[ruleName].torrentParams.stopped ? "always" : "never";
if ((rulesList[ruleName].torrentParams.content_layout === undefined) || (rulesList[ruleName].torrentParams.content_layout === null))
$("contentLayoutCombobox").value = "Default";
else
$("contentLayoutCombobox").value = rulesList[ruleName].torrentParams.content_layout;
setElementTitles();
rssDownloaderFeedSelectionTable.clear();
let rowCount = 0;
feedList.forEach((feed) => {
rssDownloaderFeedSelectionTable.updateRowData({
rowId: rowCount++,
checked: rulesList[ruleName].affectedFeeds.contains(feed.url),
name: feed.name,
url: feed.url
});
});
rssDownloaderFeedSelectionTable.updateTable(false);
updateMatchingArticles(ruleName);
}
};
const setElementTitles = () => {
let mainPart;
if ($("useRegEx").checked) {
mainPart = "Regex mode: use Perl-compatible regular expressions\n\n";
}
else {
mainPart = "Wildcard mode: you can use\n\n"
+ " ● ? to match any single character\n"
+ " ● * to match zero or more of any characters\n"
+ " ● Whitespaces count as AND operators (all words, any order)\n"
+ " ● | is used as OR operator\n\n"
+ "If word order is important use * instead of whitespace.\n\n";
}
const secondPart = "An expression with an empty %1 clause (e.g. %2)"
.replace("%1", "|").replace("%2", "expr|");
$("mustContainText").title = mainPart + secondPart + " will match all articles.";
$("mustNotContainText").title = mainPart + secondPart + " will exclude all articles.";
let episodeFilterTitle = "Matches articles based on episode filter.\n\n"
+ "Example: "
+ "1x2;8-15;5;30-;"
+ " will match 2, 5, 8 through 15, 30 and onward episodes of season one\n\n"
+ "Episode filter rules: \n\n"
+ " ● Season number is a mandatory non-zero value\n"
+ " ● Episode number is a mandatory positive value\n"
+ " ● Filter must end with semicolon\n"
+ " ● Three range types for episodes are supported: \n"
+ " ● Single number: <b>1x25;</b> matches episode 25 of season one\n"
+ " ● Normal range: <b>1x25-40;</b> matches episodes 25 through 40 of season one\n"
+ " ● Infinite range: <b>1x25-;</b> matches episodes 25 and upward of season one, and all episodes of later seasons";
episodeFilterTitle = episodeFilterTitle.replace(/<b>/g, "").replace(/<\/b>/g, "");
$("episodeFilterText").title = episodeFilterTitle;
};
return exports();
})();
Object.freeze(window.qBittorrent.RssDownloader);
window.qBittorrent.RssDownloader.setup();
</script>