2022-11-30 15:05:46 -05:00

802 lines
32 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: 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 id="newRuleButton" class="imageButton" onclick="qBittorrent.RssDownloader.addRule()"></button>
<button id="deleteRuleButton" 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">
<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>
</table>
<div class="formRow" title="QBT_TR(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">
<tr>
<td>
<label class="noWrap">Assign Category:</label>
</td>
<td class="fullWidth">
<select disabled id="assignCategoryCombobox" class="fullWidth">
<option value=""></option>
</select>
</td>
</tr>
</table>
<div class="formRow">
<input disabled type="checkbox" id="savetoDifferentDir" />
<label for="savetoDifferentDir">Save to a Different Directory</label>
</div>
<table class="fullWidth">
<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>
</table>
<table>
<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>
</table>
<div id="lastMatchDiv">
<span id="lastMatchText">Last Match: Unknown</span>
</div>
<table class="fullWidth">
<tr>
<td>
<label class="noWrap">Add Paused:</label>
</td>
<td class="fullWidth">
<select disabled id="addPausedCombobox" class="fullWidth">
<option value="default">Use global settings</option>
<option value="always">Always</option>
<option value="never">Never</option>
</select>
</td>
</tr>
</table>
<table class="fullWidth">
<tr>
<td>
<label class="noWrap">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>
</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 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..." /> QBT_TR(Add new rule...</a></li>
<li><a href="#deleteRule"><img src="images/edit-clear.svg" alt="Delete rule" /> QBT_TR(Delete rule</a></li>
<li class="separator"><a href="#renameRule"><img src="images/edit-rename.svg" alt="Rename rule..." /> QBT_TR(Rename rule...</a></li>
<li class="separator"><a href="#clearDownloadedEpisodes"><img src="images/edit-clear.svg" alt="Clear downloaded episodes..." /> QBT_TR(Clear downloaded episodes...</a></li>
</ul>
<script>
'use strict';
if (window.qBittorrent === undefined) {
window.qBittorrent = {};
}
window.qBittorrent.RssDownloader = (() => {
const exports = () => {
return {
updateRulesList: updateRulesList,
showRule: showRule,
renameRule: renameRule,
modifyRuleState: modifyRuleState,
saveSettings: saveSettings,
rssDownloaderRulesTable: rssDownloaderRulesTable,
rssDownloaderFeedSelectionTable: rssDownloaderFeedSelectionTable,
setElementTitles: setElementTitles,
addRule: addRule,
removeSelectedRule: removeSelectedRule
};
};
let rssDownloaderRulesTable = new window.qBittorrent.DynamicTable.RssDownloaderRulesTable();
let rssDownloaderFeedSelectionTable = new window.qBittorrent.DynamicTable.RssDownloaderFeedSelectionTable();
let rssDownloaderArticlesTable = new window.qBittorrent.DynamicTable.RssDownloaderArticlesTable();
let rulesList = {};
let feedList = [];
const initRssDownloader = () => {
new Request.JSON({
url: 'api/v2/app/preferences',
method: 'get',
noCache: true,
onFailure: () => {
alert('Could not contact qBittorrent');
},
onSuccess: (pref) => {
if (!pref.rss_auto_downloading_enabled)
$('rssDownloaderDisabled').removeClass('invisible');
// recalculate height
let 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)';
let 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 {
let outsideTableHeight = ($('rssDownloaderFeedsTable').getBoundingClientRect().top - $('rssDownloaderFeeds').getBoundingClientRect().top) - 10;
$('rssDownloaderFeedsTable').style.height = 'calc(100% - ' + outsideTableHeight + 'px)';
}
}
}).send();
const rssDownloaderRuleContextMenu = new window.qBittorrent.ContextMenu.RssDownloaderRuleContextMenu({
targets: '',
menu: 'rssDownloaderRuleMenu',
actions: {
addRule: addRule,
deleteRule: removeSelectedRule,
renameRule: (el) => {
renameRule(rssDownloaderRulesTable.rows[rssDownloaderRulesTable.selectedRows[0]].full_data.name);
},
clearDownloadedEpisodes: (el) => {
clearDownloadedEpisodes(rssDownloaderRulesTable.selectedRows
.map((sRow) => rssDownloaderRulesTable.rows[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',
noCache: true,
method: 'get',
onSuccess: (response) => {
let combobox = $('assignCategoryCombobox');
for (let cat in response) {
let 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',
noCache: true,
method: 'get',
data: {
withData: false
},
onSuccess: (response) => {
feedList = [];
let flatten = (root) => {
for (let child in root) {
if (root[child].uid !== undefined)
feedList.push({ name: child, url: root[child].url });
else
flatten(root[child]);
}
};
flatten(response);
}
}).send();
$('savetoDifferentDir').addEvent('click', () => {
$('saveToText').disabled = !$('savetoDifferentDir').checked;
});
updateRulesList();
};
const updateRulesList = () => {
// get all rules
new Request.JSON({
url: 'api/v2/rss/rules',
noCache: true,
method: 'get',
onSuccess: (response) => {
rssDownloaderRulesTable.clear();
let rowCount = 0;
for (let rule in response) {
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',
noCache: true,
method: 'post',
data: {
ruleName: rule,
ruleDef: JSON.stringify(rulesList[rule])
},
onSuccess: () => {
callback();
}
}).send();
};
const addRule = () => {
new MochaUI.Window({
id: 'newRulePage',
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',
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.rows[sRow].full_data.name));
};
const removeRules = (rules) => {
const encodedRules = rules.map((rule) => encodeURIComponent(rule));
new MochaUI.Window({
id: 'removeRulePage',
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',
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 = () => {
let lastSelectedRow = rssDownloaderRulesTable.selectedRows[rssDownloaderRulesTable.selectedRows.length - 1];
let rule = rssDownloaderRulesTable.rows[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].assignedCategory = $('assignCategoryCombobox').value;
rulesList[rule].savePath = $('savetoDifferentDir').checked ? $('saveToText').value : '';
rulesList[rule].ignoreDays = parseInt($('ignoreDaysValue').value);
switch ($('addPausedCombobox').value) {
case 'default':
rulesList[rule].addPaused = null;
break;
case 'always':
rulesList[rule].addPaused = true;
break;
case 'never':
rulesList[rule].addPaused = false;
break;
}
switch ($('contentLayoutCombobox').value) {
case 'Default':
rulesList[rule].torrentContentLayout = null;
break;
case 'Original':
rulesList[rule].torrentContentLayout = 'Original';
break;
case 'Subfolder':
rulesList[rule].torrentContentLayout = 'Subfolder';
break;
case 'NoSubfolder':
rulesList[rule].torrentContentLayout = 'NoSubfolder';
break;
}
rulesList[rule].affectedFeeds = rssDownloaderFeedSelectionTable.rows.filter((row) => row.full_data.checked)
.map((row) => row.full_data.url)
.getValues();
new Request({
url: 'api/v2/rss/setRule',
noCache: true,
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',
noCache: true,
method: 'get',
data: {
ruleName: ruleName
},
onSuccess: (response) => {
rssDownloaderArticlesTable.clear();
let rowCount = 0;
for (let feed in response) {
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;
$('savetoDifferentDir').disabled = true;
$('saveToText').disabled = true;
$('ignoreDaysValue').disabled = true;
$('addPausedCombobox').disabled = true;
$('contentLayoutCombobox').disabled = true;
// reset all boxes
$('useRegEx').checked = false;
$('mustContainText').value = '';
$('mustNotContainText').value = '';
$('episodeFilterText').value = '';
$('useSmartFilter').checked = false;
$('assignCategoryCombobox').value = 'default';
$('savetoDifferentDir').checked = false;
$('saveToText').value = '';
$('ignoreDaysValue').value = 0;
$('lastMatchText').textContent = 'Last Match: Unknown';
$('addPausedCombobox').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;
$('savetoDifferentDir').disabled = false;
$('savetoDifferentDir').checked = rulesList[ruleName].savePath ? false : true;
$('saveToText').disabled = rulesList[ruleName].savePath ? false : true;
$('ignoreDaysValue').disabled = false;
$('addPausedCombobox').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].assignedCategory ? rulesList[ruleName].assignedCategory : 'default';
$('savetoDifferentDir').checked = rulesList[ruleName].savePath !== '';
$('saveToText').value = rulesList[ruleName].savePath;
$('ignoreDaysValue').value = rulesList[ruleName].ignoreDays;
// calculate days since last match
if (rulesList[ruleName].lastMatch !== '') {
let timeDiffInMs = new Date().getTime() - new Date(rulesList[ruleName].lastMatch).getTime();
let 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].addPaused === null)
$('addPausedCombobox').value = 'default';
else
$('addPausedCombobox').value = rulesList[ruleName].addPaused ? 'always' : 'never';
if (rulesList[ruleName].torrentContentLayout === null)
$('contentLayoutCombobox').value = 'Default';
else
$('contentLayoutCombobox').value = rulesList[ruleName].torrentContentLayout;
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';
}
let 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;
};
initRssDownloader();
return exports();
})();
Object.freeze(window.qBittorrent.RssDownloader);
</script>