Merge pull request #12557 from SeproDE/webui-rss

Implemented RSS reader and auto downloader in reference WebUI
This commit is contained in:
Mike Tzou
2020-06-09 14:34:56 +08:00
committed by GitHub
18 changed files with 2902 additions and 27 deletions

View File

@@ -595,6 +595,54 @@
</fieldset>
</div>
<div id="RSSTab" class="PrefTab invisible">
<fieldset class="settings">
<legend>QBT_TR(RSS Reader)QBT_TR[CONTEXT=OptionsDialog]</legend>
<div class="formRow">
<input type="checkbox" id="enable_fetching_rss_feeds_checkbox" />
<label for="enable_fetching_rss_feeds_checkbox">QBT_TR(Enable fetching RSS feeds)QBT_TR[CONTEXT=OptionsDialog]</label>
</div>
<table>
<tr>
<td>
<label for="feed_refresh_interval">QBT_TR(Feeds refresh interval:)QBT_TR[CONTEXT=OptionsDialog]</label>
</td>
<td>
<input type="text" id="feed_refresh_interval" style="width: 4em;" />&nbsp;&nbsp;QBT_TR( min)QBT_TR[CONTEXT=OptionsDialog]
</td>
</tr>
<tr>
<td>
<label for="maximum_article_number">QBT_TR(Maximum number of articles per feed:)QBT_TR[CONTEXT=OptionsDialog]</label>
</td>
<td>
<input type="text" id="maximum_article_number" style="width: 4em;" />
</td>
</tr>
</table>
</fieldset>
<fieldset class="settings">
<legend>QBT_TR(RSS Torrent Auto Downloader)QBT_TR[CONTEXT=OptionsDialog]</legend>
<div class="formRow">
<input type="checkbox" id="enable_auto_downloading_rss_torrents_checkbox" />
<label for="enable_auto_downloading_rss_torrents_checkbox">QBT_TR(Enable auto downloading of RSS torrents)QBT_TR[CONTEXT=OptionsDialog]</label>
</div>
<button style="margin: 0 1em; width: calc(100% - 2.2em)" onclick="window.qBittorrent.Rss.openRssDownloader();">QBT_TR(Edit auto downloading rules...)QBT_TR[CONTEXT=OptionsDialog]</button>
</fieldset>
<fieldset class="settings">
<legend>QBT_TR(RSS Smart Episode Filter)QBT_TR[CONTEXT=OptionsDialog]</legend>
<div class="formRow">
<input type="checkbox" id="downlock_repack_proper_episodes" />
<label for="downlock_repack_proper_episodes">QBT_TR(Download REPACK/PROPER episodes)QBT_TR[CONTEXT=OptionsDialog]</label>
</div>
<label for="rss_filter_textarea">QBT_TR(Filters:)QBT_TR[CONTEXT=OptionsDialog]</label><br>
<textarea id="rss_filter_textarea" rows="6" cols="70"></textarea>
</fieldset>
</div>
<div id="WebUITab" class="PrefTab invisible">
<fieldset class="settings">
<legend>QBT_TR(Language)QBT_TR[CONTEXT=OptionsDialog]</legend>
@@ -1723,6 +1771,14 @@
$('add_trackers_textarea').setProperty('value', pref.add_trackers);
updateAddTrackersEnabled();
// RSS Tab
$('enable_fetching_rss_feeds_checkbox').setProperty('checked', pref.rss_processing_enabled);
$('feed_refresh_interval').setProperty('value', pref.rss_refresh_interval);
$('maximum_article_number').setProperty('value', pref.rss_max_articles_per_feed);
$('enable_auto_downloading_rss_torrents_checkbox').setProperty('checked', pref.rss_auto_downloading_enabled);
$('downlock_repack_proper_episodes').setProperty('checked', pref.rss_download_repack_proper_episodes);
$('rss_filter_textarea').setProperty('value', pref.rss_smart_episode_filters);
// Web UI tab
// Language
$('locale_select').setProperty('value', pref.locale);
@@ -2077,6 +2133,14 @@
settings.set('add_trackers_enabled', $('add_trackers_checkbox').getProperty('checked'));
settings.set('add_trackers', $('add_trackers_textarea').getProperty('value'));
// RSS Tab
settings.set('rss_processing_enabled', $('enable_fetching_rss_feeds_checkbox').getProperty('checked'));
settings.set('rss_refresh_interval', $('feed_refresh_interval').getProperty('value'));
settings.set('rss_max_articles_per_feed', $('maximum_article_number').getProperty('value'));
settings.set('rss_auto_downloading_enabled', $('enable_auto_downloading_rss_torrents_checkbox').getProperty('checked'));
settings.set('rss_download_repack_proper_episodes', $('downlock_repack_proper_episodes').getProperty('checked'));
settings.set('rss_smart_episode_filters', $('rss_filter_textarea').getProperty('value'));
// Web UI tab
// Language
settings.set('locale', $('locale_select').getProperty('value'));

View File

@@ -5,6 +5,7 @@
<li id="PrefConnectionLink"><a>QBT_TR(Connection)QBT_TR[CONTEXT=OptionsDialog]</a></li>
<li id="PrefSpeedLink"><a>QBT_TR(Speed)QBT_TR[CONTEXT=OptionsDialog]</a></li>
<li id="PrefBittorrentLink"><a>QBT_TR(BitTorrent)QBT_TR[CONTEXT=OptionsDialog]</a></li>
<li id="PrefRSSLink"><a>QBT_TR(RSS)QBT_TR[CONTEXT=OptionsDialog]</a></li>
<li id="PrefWebUILink"><a>QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog]</a></li>
<li id="PrefAdvancedLink"><a>QBT_TR(Advanced)QBT_TR[CONTEXT=OptionsDialog]</a></li>
</ul>
@@ -34,6 +35,10 @@
$$('.PrefTab').addClass('invisible');
$('BittorrentTab').removeClass('invisible');
});
$('PrefRSSLink').addEvent('click', function(e) {
$$('.PrefTab').addClass('invisible');
$('RSSTab').removeClass('invisible');
});
$('PrefWebUILink').addEvent('click', function(e) {
$$('.PrefTab').addClass('invisible');
$('WebUITab').removeClass('invisible');

View File

@@ -0,0 +1,823 @@
<style>
#rssView {
padding: 20px 20px 0 20px;
height: calc(100% - 20px);
}
#rssContentView {
display: table;
width: 100%;
height: calc(100% - 30px);
vertical-align: top;
}
#rssFeedFixedHeaderDiv .dynamicTableHeader, #rssArticleFixedHeaderDiv .dynamicTableHeader {
cursor: default;
}
.alignRight {
float: right;
}
.unreadArticle {
color: blue;
}
#rssFetchingDisabled {
color: red;
font-style: italic;
margin-bottom: 10px;
}
#centerRssColumn {
margin: 0 10px 0 10px;
}
#leftRssColumn, #centerRssColumn, #rightRssColumn {
float: left;
/* should be 20 px but due to rounding differences some browsers don't render that properly */
width: calc(calc(100% - 21px) / 3);
border: none;
}
#rightRssColumn {
overflow: auto;
}
#rssFeedTableDiv, #rssArticleTableDiv {
height: calc(100vh - 180px);
}
#rssTorrentDetailsName {
background-color: #678db2;
padding: 0;
color: white;
}
#rssTorrentDetailsDate {
background-color: #EFEFEF;
}
#rssDetailsView {
height: calc(100vh - 135px);
overflow: auto;
}
#rssButtonBar {
overflow: hidden;
height: 30px;
}
#rssContentView table {
width: 100%;
}
</style>
<div id="rssView">
<div id="rssTopBar">
<div id="rssFetchingDisabled" class="invisible">
QBT_TR(Fetching of RSS feeds is disabled now! You can enable it in application settings.)QBT_TR[CONTEXT=RSSWidget]
</div>
<div id="rssButtonBar">
<button id="newSubscriptionButton" onclick="qBittorrent.Rss.addRSSFeed()">QBT_TR(New subscription)QBT_TR[CONTEXT=RSSWidget]</button>
<button id="markReadButton" onclick="qBittorrent.Rss.markSelectedAsRead()">QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]</button>
<button id="updateAllButton" onclick="qBittorrent.Rss.refreshAllFeeds()">QBT_TR(Update all)QBT_TR[CONTEXT=RSSWidget]</button>
<button id="rssDownloaderButton" class="alignRight" onclick="qBittorrent.Rss.openRssDownloader()">QBT_TR(RSS Downloader...)QBT_TR[CONTEXT=RSSWidget]</button>
</div>
</div>
<div id="rssContentView">
<div id="leftRssColumn">
<div id="rssFeedFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssFeedTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="centerRssColumn">
<div id="rssArticleFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssArticleTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="rightRssColumn">
<div id="rssDetailsView"></div>
</div>
</div>
</div>
<ul id="rssFeedMenu" class="contextMenu">
<li><a href="#update"><img src="icons/view-refresh.svg" alt="QBT_TR(Update)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Update)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#markRead"><img src="icons/mail-mark-read.svg" alt="QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#rename"><img src="icons/edit-rename.svg" alt="QBT_TR(Rename...)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Rename...)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#delete"><img src="icons/edit-delete.svg" alt="QBT_TR(Delete)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Delete)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#newSubscription"><img src="icons/document-new.svg" alt="QBT_TR(New subscription...)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(New subscription...)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#newFolder"><img src="icons/folder-new.svg" alt="QBT_TR(New folder...)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(New folder...)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#updateAll"><img src="icons/view-refresh.svg" alt="QBT_TR(Update all feeds)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Update all feeds)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#copyFeedURL" id="CopyFeedURL"><img src="icons/edit-copy.svg" alt="QBT_TR(Copy feed URL)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Copy feed URL)QBT_TR[CONTEXT=RSSWidget]</a></li>
</ul>
<ul id="rssArticleMenu" class="contextMenu">
<li><a href="#Download"><img src="icons/download.svg" alt="QBT_TR(Download torrent)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Download torrent)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#OpenNews"><img src="icons/application-x-mswinurl.svg" alt="QBT_TR(Open news URL)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Open news URL)QBT_TR[CONTEXT=RSSWidget]</a></li>
</ul>
<script>
'use strict';
if (window.qBittorrent === undefined) {
window.qBittorrent = {};
}
let serverSyncRssDataInterval = 1500;
window.qBittorrent.Rss = (() => {
const exports = () => {
return {
init: init,
unload: unload,
load: load,
showRssFeed: showRssFeed,
showDetails: showDetails,
updateRssFeedList: updateRssFeedList,
refreshAllFeeds: refreshAllFeeds,
moveItem: moveItem,
addRSSFeed: addRSSFeed,
markSelectedAsRead : markSelectedAsRead,
openRssDownloader: openRssDownloader,
rssFeedTable: rssFeedTable
};
};
let feedData = {};
let pathByFeedId = new Map();;
let feedRefreshTimer;
let rssFeedTable = new window.qBittorrent.DynamicTable.RssFeedTable();
let rssArticleTable = new window.qBittorrent.DynamicTable.RssArticleTable();
const init = () => {
new Request.JSON({
url: 'api/v2/app/preferences',
method: 'get',
noCache: true,
onFailure: () => {
alert('Could not contact qBittorrent');
},
onSuccess: (pref) => {
if (!pref.rss_processing_enabled)
$('rssFetchingDisabled').removeClass('invisible');
// recalculate heights
let nonPageHeight = $('rssTopBar').getBoundingClientRect().height +
$('desktopHeader').getBoundingClientRect().height +
$('desktopFooterWrapper').getBoundingClientRect().height + 20;
$('rssDetailsView').style.height = 'calc(100vh - ' + nonPageHeight + 'px)';
let nonTableHeight = nonPageHeight + $('rssFeedFixedHeaderDiv').getBoundingClientRect().height;
$('rssFeedTableDiv').style.height = 'calc(100vh - ' + nonTableHeight + 'px)';
$('rssArticleTableDiv').style.height = 'calc(100vh - ' + nonTableHeight + 'px)';
$('rssContentView').style.height = 'calc(100% - ' + $('rssTopBar').getBoundingClientRect().height + 'px)';
}
}).send();
const rssFeedContextMenu = new window.qBittorrent.ContextMenu.RssFeedContextMenu({
targets: '.rssFeedContextMenuTarget',
menu: 'rssFeedMenu',
actions: {
update: (el) => {
let feedsToUpdate = new Set();
rssFeedTable.selectedRows.each((rowId) => {
let selectedPath = rssFeedTable.rows[rowId].full_data.dataPath;
rssFeedTable.rows.filter((row) => row.full_data.dataPath.slice(0, selectedPath.length) === selectedPath)
.filter((row) => row.full_data.dataUid !== '')
.each((row) => feedsToUpdate.add(row));
});
feedsToUpdate.forEach((feed) => refreshFeed(feed.full_data.dataUid))
},
markRead: markSelectedAsRead,
rename: (el) => {
moveItem(rssFeedTable.rows[rssFeedTable.selectedRows[0]].full_data.dataPath);
},
delete: (el) => {
let selectedDatapaths = rssFeedTable.selectedRows
.filter((e) => e !== 0)
.map((sRow) => rssFeedTable.rows[sRow].full_data.dataPath);
// filter children
let reducedDatapaths = selectedDatapaths.filter((path) =>
selectedDatapaths.filter((innerPath) => path.slice(0, innerPath.length) === innerPath).length === 1
);
removeItem(reducedDatapaths);
},
newSubscription: addRSSFeed,
newFolder: addFolder,
updateAll: refreshAllFeeds
},
offsets: {
x: -16,
y: -57
}
});
rssFeedContextMenu.addTarget($('rssFeedTableDiv'));
// deselect feed when clicking on empty part of table
$('rssFeedTableDiv').addEventListener('click', (e) => {
rssFeedTable.deselectAll();
rssFeedTable.deselectRow();
});
$('rssFeedTableDiv').addEventListener('contextmenu', (e) => {
if (e.toElement.nodeName === 'DIV') {
rssFeedTable.deselectAll();
rssFeedTable.deselectRow();
rssFeedContextMenu.updateMenuItems();
}
});
new ClipboardJS('#CopyFeedURL', {
text: () => {
let joined = '';
rssFeedTable.selectedRows
.filter((row) => rssFeedTable.rows[row].full_data.dataUid !== '')
.each((row) => joined += rssFeedTable.rows[row].full_data.dataUrl + '\n');
return joined.slice(0, -1);
}
});
rssFeedTable.setup('rssFeedTableDiv', 'rssFeedFixedHeaderDiv', rssFeedContextMenu);
const rssArticleContextMenu = new window.qBittorrent.ContextMenu.RssArticleContextMenu({
targets: '.rssArticleElement',
menu: 'rssArticleMenu',
actions: {
Download: (el) => {
let dlString = '';
rssArticleTable.selectedRows.each((row) => {
dlString += rssArticleTable.rows[row].full_data.torrentURL + '\n';
});
showDownloadPage([dlString]);
},
OpenNews: (el) => {
rssArticleTable.selectedRows.each((row) => {
window.open(rssArticleTable.rows[row].full_data.link);
});
}
},
offsets: {
x: -16,
y: -57
}
});
rssArticleTable.setup('rssArticleTableDiv', 'rssArticleFixedHeaderDiv', rssArticleContextMenu);
updateRssFeedList();
load();
};
const unload = () => {
clearInterval(feedRefreshTimer);
}
const load = () => {
feedRefreshTimer = setInterval(updateRssFeedList, serverSyncRssDataInterval);
}
const addRSSFeed = () => {
let path = '';
if (rssFeedTable.selectedRows.length !== 0) {
let row = rssFeedTable.rows[rssFeedTable.selectedRows[0]];
if (row.full_data.dataUid === '') {
path = row.full_data.dataPath;
}
else {
let lastIndex = row.full_data.dataPath.lastIndexOf('\\');
if (lastIndex !== -1)
path = row.full_data.dataPath.slice(0, lastIndex);
}
}
new MochaUI.Window({
id: 'newFeed',
title: 'QBT_TR(Please type a RSS feed URL)QBT_TR[CONTEXT=RSSWidget]',
loadMethod: 'iframe',
contentURL: 'newfeed.html?path=' + encodeURIComponent(path),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const addFolder = () => {
let path = '';
if (rssFeedTable.selectedRows.length !== 0) {
let row = rssFeedTable.rows[rssFeedTable.selectedRows[0]];
if (row.full_data.dataUid === '') {
path = row.full_data.dataPath;
}
else {
let lastIndex = row.full_data.dataPath.lastIndexOf('\\');
if (lastIndex !== -1)
path = row.full_data.dataPath.slice(0, lastIndex);
}
}
new MochaUI.Window({
id: 'newFolder',
title: 'QBT_TR(Please choose a folder name)QBT_TR[CONTEXT=RSSWidget]',
loadMethod: 'iframe',
contentURL: 'newfolder.html?path=' + encodeURIComponent(path),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
}
const showRssFeed = (path) => {
rssArticleTable.clear();
let rowCount = 0;
let childFeeds = new Set();
rssFeedTable.rows.filter((row) => row.full_data.dataPath.slice(0, path.length) === path)
.filter((row) => row.full_data.dataUid !== '')
.each((row) => childFeeds.add(row.full_data.dataUid));
let visibleArticles = [];
for (let feedEntry in feedData) {
if (childFeeds.has(feedEntry))
visibleArticles.append(feedData[feedEntry]
.map((a) => {
a.feedUid = feedEntry;
return a;
}));
}
//filter read articles if "Unread" feed is selected
if (path === '')
visibleArticles = visibleArticles.filter((a) => !a.isRead);
visibleArticles.sort((e1, e2) => new Date(e2.date) - new Date(e1.date))
.each((torrentEntry) => {
rssArticleTable.updateRowData({
rowId: rowCount++,
name: torrentEntry.title,
link: torrentEntry.link,
torrentURL: torrentEntry.torrentURL,
feedUid: torrentEntry.feedUid,
dataId: torrentEntry.id,
isRead: torrentEntry.isRead
});
});
$('rssDetailsView').empty();
rssArticleTable.updateTable(false);
};
const showDetails = (feedUid, articleID) => {
markArticleAsRead(pathByFeedId.get(feedUid), articleID);
$('rssDetailsView').empty();
let article = feedData[feedUid].filter((article) => article.id === articleID)[0];
if (article) {
$('rssDetailsView').append((() => {
let torrentName = document.createElement('p');
torrentName.innerText = article.title;
torrentName.setAttribute('id', 'rssTorrentDetailsName');
return torrentName;
})());
$('rssDetailsView').append((() => {
let torrentDate = document.createElement('div');
torrentDate.setAttribute('id', 'rssTorrentDetailsDate');
let torrentDateDesc = document.createElement('b');
torrentDateDesc.innerText = 'QBT_TR(Date: )QBT_TR[CONTEXT=RSSWidget]';
torrentDate.append(torrentDateDesc);
let torrentDateData = document.createElement('span');
torrentDateData.innerText = new Date(article.date).toLocaleString();
torrentDate.append(torrentDateData);
return torrentDate;
})());
// Strip script before interpreting html
let torrentDescription = document.createRange().createContextualFragment(
'<div id="rssTorrentDetailsDescription">' + article.description.stripScripts() + '</div>');
$('rssDetailsView').append(torrentDescription);
}
};
const updateRssFeedList = () => {
new Request.JSON({
url: 'api/v2/rss/items',
noCache: true,
method: 'post',
data: {
withData: true
},
onSuccess: (response) => {
// flatten folder structure
let flattenedResp = [];
let recFlatten = (current, name = '', depth = 0, fullName = '') => {
for (let child in current) {
let currentFullName = fullName ? (fullName + '\\' + child) : child;
if (current[child].uid !== undefined) {
current[child].name = child;
current[child].isFolder = false;
current[child].depth = depth;
current[child].fullName = currentFullName;
flattenedResp.push(current[child]);
}
else {
flattenedResp.push({
name: child,
isFolder: true,
depth: depth,
fullName: currentFullName
});
recFlatten(current[child], child, depth + 1, currentFullName);
}
}
}
recFlatten(response);
// check if rows matche flattend response
let match = false;
if (rssFeedTable.rows.getLength() - 1 === flattenedResp.length) {
match = true;
for (let i = 0; i < flattenedResp.length; ++i) {
if ((flattenedResp[i].uid ? flattenedResp[i].uid : '') !== rssFeedTable.rows[i + 1].full_data.dataUid ||
flattenedResp[i].fullName !== rssFeedTable.rows[i + 1].full_data.dataPath) {
match = false;
break;
}
}
}
if (match) {
// partial refresh
// update status
let statusDiffers = false;
for (let i = 0; i < flattenedResp.length; ++i) {
let oldStatus = rssFeedTable.rows[i + 1].full_data.status;
let status = 'default';
if (flattenedResp[i].hasError)
status = 'hasError';
if (flattenedResp[i].isLoading)
status = 'isLoading';
if (flattenedResp[i].isFolder)
status = 'isFolder';
if (oldStatus !== status) {
statusDiffers = true;
rssFeedTable.updateRowData({
rowId: i + 1,
status: status
});
}
}
if (statusDiffers)
rssFeedTable.updateIcons();
// get currently opened feed
let openedFeedPath = undefined;
if (rssFeedTable.selectedRows.length !== 0) {
let lastSelectedRow = rssFeedTable.selectedRows[rssFeedTable.selectedRows.length - 1];
openedFeedPath = rssFeedTable.rows[lastSelectedRow].full_data.dataPath;
}
// check if list of articles differs
let needsUpdate = false;
flattenedResp.filter((r) => !r.isFolder)
.each((r) => {
let articlesDiffer = true;
if (r.articles.length === feedData[r.uid].length) {
articlesDiffer = false;
for (let i = 0; i < r.articles.length; ++i) {
if (feedData[r.uid][i].id !== r.articles[i].id)
articlesDiffer = true;
}
}
if (articlesDiffer) {
// update unread count
let oldUnread = feedData[r.uid].map((art) => !art.isRead).filter((v) => v).length;
let newUnread = r.articles.map((art) => !art.isRead).filter((v) => v).length;
let unreadDifference = newUnread - oldUnread;
// find all parents (and self) and add unread difference
rssFeedTable.rows.filter((row) => r.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread += unreadDifference);
needsUpdate = true;
// update data
feedData[r.uid] = r.articles;
// if feed that is open changed, reload
if (openedFeedPath !== undefined && r.fullName.slice(0, openedFeedPath.length) === openedFeedPath)
showRssFeed(r.fullName);
}
else {
// calculate read differnce and update feed data
let readDifference = 0;
let readChanged = false;
for (let i = 0; i < r.articles.length; ++i) {
let oldRead = feedData[r.uid][i].isRead ? 1 : 0;
let newRead = r.articles[i].isRead ? 1 : 0;
feedData[r.uid][i].isRead = r.articles[i].isRead;
readDifference += oldRead - newRead;
if (readDifference !== 0)
readChanged = true;
}
// if read on article changed
if (readChanged) {
needsUpdate = true;
// find all items that contain this rss feed and add read difference
rssFeedTable.rows.filter((row) => r.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread += readDifference);
// if feed that is opened changed update dynamicly
if (openedFeedPath !== undefined && r.fullName.slice(0, openedFeedPath.length) === openedFeedPath) {
for (let i = 0; i < r.articles.length; ++i) {
let matchingRow = rssArticleTable.rows.filter((row) => row.full_data.feedUid === r.uid)
.filter((row) => row.full_data.dataId === r.articles[i].id);
matchingRow[Object.keys(matchingRow)[0]].full_data.isRead = r.articles[i].isRead;
}
}
}
}
});
if (needsUpdate) {
rssFeedTable.updateTable(true);
rssArticleTable.updateTable(true);
}
}
else {
// full refresh
rssFeedTable.clear();
feedData = {};
pathByFeedId = new Map();
// Unread entry at top
rssFeedTable.updateRowData({
rowId: 0,
name: 'Unread',
unread: 0,
status: 'unread',
indentaion: 0,
dataUid: '',
dataUrl: '',
dataPath: ''
});
let rowCount = 1;
for (let dataEntry of flattenedResp) {
if (dataEntry.isFolder) {
rssFeedTable.updateRowData({
rowId: rowCount,
name: dataEntry.name,
unread: 0,
status: 'isFolder',
indentaion: dataEntry.depth,
dataUid: '',
dataUrl: '',
dataPath: dataEntry.fullName
});
}
else {
let status = 'default';
if (dataEntry.hasError)
status = 'hasError';
if (dataEntry.isLoading)
status = 'isLoading';
rssFeedTable.updateRowData({
rowId: rowCount,
name: dataEntry.name,
unread: 0,
status: status,
indentaion: dataEntry.depth,
dataUid: dataEntry.uid,
dataUrl: dataEntry.url,
dataPath: dataEntry.fullName
});
// calculate number of unread
let numberOfUnread = dataEntry.articles.map((art) => !art.isRead).filter((v) => v).length;
// find all items that contain this rss feed and add unread count
rssFeedTable.rows.filter((row) => dataEntry.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread += numberOfUnread);
pathByFeedId.set(dataEntry.uid, dataEntry.fullName);
feedData[dataEntry.uid] = dataEntry.articles;
}
++rowCount;
}
rssFeedTable.updateTable(false);
rssFeedTable.updateIcons();
}
}
}).send();
};
const refreshFeed = (feedUid) => {
// set icon to loading
rssFeedTable.rows.forEach((row) => {
if (row.full_data.dataUid === feedUid)
row.full_data.status = 'isLoading';
});
rssFeedTable.updateIcons();
new Request({
url: 'api/v2/rss/refreshItem',
noCache: true,
method: 'post',
data: {
itemPath: pathByFeedId.get(feedUid)
},
onFailure: (response) => {
if (response.status === 409)
alert(response.responseText);
}
}).send();
};
const refreshAllFeeds = () => {
for (let feedEntry in feedData)
refreshFeed(feedEntry);
};
const moveItem = (oldPath) => {
new MochaUI.Window({
id: 'renamePage',
title: 'QBT_TR(Please choose a new name for this RSS feed)QBT_TR[CONTEXT=RSSWidget]',
loadMethod: 'iframe',
contentURL: 'rename_feed.html?oldPath=' + encodeURIComponent(oldPath),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const removeItem = (paths) => {
new MochaUI.Window({
id: 'confirmFeedDeletionPage',
title: 'QBT_TR(Deletion confirmation)QBT_TR[CONTEXT=RSSWidget]',
loadMethod: 'iframe',
contentURL: 'confirmfeeddeletion.html?paths=' + encodeURIComponent(paths.join('|')),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 70
});
};
const markItemAsRead = (path) => {
// feed data mark as read
for (let feedID in feedData)
if (pathByFeedId.get(feedID).slice(0, path.length) === path)
feedData[feedID].each((el) => el.isRead = true);
// mark rows as read
rssArticleTable.rows.each((el) => el.full_data.isRead = true);
// find all children and set unread count to 0
rssFeedTable.rows.filter((row) => row.full_data.dataPath.slice(0, path.length) === path && path !== row.full_data.dataPath)
.each((row) => row.full_data.unread = 0);
// find selected row
let rowId, prevUnreadCount;
rssFeedTable.rows.forEach((row) => {
if (row.full_data.dataPath === path) {
rowId = row.full_data.rowId;
prevUnreadCount = row.full_data.unread;
}
});
// find all parents (and self) and subtract previous unread count
rssFeedTable.rows.filter((row) => path.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread -= prevUnreadCount);
rssArticleTable.updateTable(false);
rssFeedTable.updateTable(true);
// send request
new Request({
url: 'api/v2/rss/markAsRead',
noCache: true,
method: 'post',
data: {
itemPath: path
},
onFailure: (response) => {
if (response.status === 409)
alert(response.responseText);
}
}).send();
};
const markArticleAsRead = (path, id) => {
// find row
let rowId, name, uid, unread;
rssFeedTable.rows.forEach((row) => {
if (row.full_data.dataPath === path) {
rowId = row.full_data.rowId;
name = row.full_data.dataPath;
uid = row.full_data.dataUid;
unread = row.full_data.unread;
}
});
// update feed data
let prevReadState = true;
feedData[uid].each((article) => {
if (article.id === id) {
prevReadState = article.isRead;
article.isRead = true;
}
});
if (!prevReadState) {
// find all items that contain this feed and subtract 1
rssFeedTable.rows.filter((row) => path.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread -= 1);
rssFeedTable.updateTable(true);
new Request({
url: 'api/v2/rss/markAsRead',
noCache: true,
method: 'post',
data: {
itemPath: path,
articleId: id
},
onFailure: (response) => {
if (response.status === 409)
alert(response.responseText);
}
}).send();
}
};
const markSelectedAsRead = () => {
let selectedDatapaths = rssFeedTable.selectedRows
.map((sRow) => rssFeedTable.rows[sRow].full_data.dataPath);
// filter children
let reducedDatapaths = selectedDatapaths.filter((path) =>
selectedDatapaths.filter((innerPath) => path.slice(0, innerPath.length) === innerPath).length === 1
);
reducedDatapaths.each((path) => markItemAsRead(path));
};
const openRssDownloader = () => {
const id = 'rssdownloaderpage';
new MochaUI.Window({
id: id,
title: 'QBT_TR(Rss Downloader)QBT_TR[CONTEXT=AutomatedRssDownloader]',
loadMethod: 'xhr',
contentURL: 'views/rssDownloader.html',
maximizable: false,
width: loadWindowWidth(id, 800),
height: loadWindowHeight(id, 650),
onResize: () => {
saveWindowSize(id);
},
resizeLimit: {
'x' :[800,2500],
'y' :[500,2000]
}
});
};
return exports();
})();
</script>

View File

@@ -0,0 +1,735 @@
<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;
}
.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;
}
</style>
<div id="RssDownloader" class="RssDownloader">
<div id="rssDownloaderDisabled" class="invisible">
QBT_TR(Auto downloading of RSS torrents is disabled now! You can enable it in application settings.)QBT_TR[CONTEXT=AutomatedRssDownloader]
</div>
<div id="leftRssDownloaderColumn">
<b id="rulesTableDesc">QBT_TR(Download Rules)QBT_TR[CONTEXT=AutomatedRssDownloader]</b>
<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>QBT_TR(Rule Definition)QBT_TR[CONTEXT=AutomatedRssDownloader]</legend>
<div class="formRow">
<input disabled type="checkbox" id="useRegEx" onclick="qBittorrent.RssDownloader.setElementTitles()"/>
<label for="useRegEx">QBT_TR(Use Regular Expressions)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</div>
<table class="fullWidth">
<tr>
<td>
<label for="mustContainText" class="noWrap">QBT_TR(Must Contain:)QBT_TR[CONTEXT=AutomatedRssDownloader]</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">QBT_TR(Must Not Contain:)QBT_TR[CONTEXT=AutomatedRssDownloader]</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">QBT_TR(Episode Filter:)QBT_TR[CONTEXT=AutomatedRssDownloader]</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.01.01 and 01.01.2017 (Date formats also support - as a separator))QBT_TR[CONTEXT=AutomatedRssDownloader]">
<input disabled type="checkbox" id="useSmartFilter" />
<label for="useSmartFilter">QBT_TR(Use Smart Episode Filter)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</div>
<hr>
<table class="fullWidth">
<tr>
<td>
<label class="noWrap">QBT_TR(Assign Category:)QBT_TR[CONTEXT=AutomatedRssDownloader]</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">QBT_TR(Save to a Different Directory)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</div>
<table class="fullWidth">
<tr>
<td>
<label class="noWrap" for="saveToText">QBT_TR(Save to:)QBT_TR[CONTEXT=AutomatedRssDownloader]</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">QBT_TR(Ignore Subsequent Matches for (0 to Disable))QBT_TR[CONTEXT=AutomatedRssDownloader]</label></td>
<td><input type="number" id="ignoreDaysValue" min="0" />QBT_TR( days)QBT_TR[CONTEXT=AutomatedRssDownloader]</td>
</tr>
</table>
<div id="lastMatchDiv">
<span id="lastMatchText">QBT_TR(Last Match: Unknown)QBT_TR[CONTEXT=AutomatedRssDownloader]</span>
</div>
<table class="fullWidth">
<tr>
<td>
<label class="noWrap">QBT_TR(Add Paused:)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</td>
<td class="fullWidth">
<select disabled id="addPausedCombobox" class="fullWidth">
<option value="default">QBT_TR(Use global settings)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
<option value="always">QBT_TR(Always)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
<option value="never">QBT_TR(Never)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
</select>
</td>
</tr>
</table>
<table class="fullWidth">
<tr>
<td>
<label class="noWrap">QBT_TR(Create Subfolder:)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</td>
<td class="fullWidth">
<select disabled id="creatSubfolderCombobox" class="fullWidth">
<option value="default">QBT_TR(Use global settings)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
<option value="always">QBT_TR(Always)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
<option value="never">QBT_TR(Never)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
</select>
</td>
</tr>
</table>
</fieldset>
<fieldset class="settings" id="rssDownloaderFeeds">
<legend>QBT_TR(Apply Rule to Feeds:)QBT_TR[CONTEXT=AutomatedRssDownloader]</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()">
QBT_TR(Save)QBT_TR[CONTEXT=HttpServer]
</button>
</div>
<div id="rightRssDownloaderColumn">
<b id="articleTableDesc">QBT_TR(Matching RSS Articles)QBT_TR[CONTEXT=AutomatedRssDownloader]</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="icons/document-new.svg" alt="QBT_TR(Add new rule...)QBT_TR[CONTEXT=AutomatedRssDownloader]" /> QBT_TR(Add new rule...)QBT_TR[CONTEXT=AutomatedRssDownloader]</a></li>
<li><a href="#deleteRule"><img src="icons/list-remove.svg" alt="QBT_TR(Delete rule)QBT_TR[CONTEXT=AutomatedRssDownloader]" /> QBT_TR(Delete rule)QBT_TR[CONTEXT=AutomatedRssDownloader]</a></li>
<li class="separator"><a href="#renameRule"><img src="icons/edit-rename.svg" alt="QBT_TR(Rename rule...)QBT_TR[CONTEXT=AutomatedRssDownloader]" /> QBT_TR(Rename rule...)QBT_TR[CONTEXT=AutomatedRssDownloader]</a></li>
<li class="separator"><a href="#clearDownloadedEpisodes"><img src="icons/edit-clear.svg" alt="QBT_TR(Clear downloaded episodes...)QBT_TR[CONTEXT=AutomatedRssDownloader]" /> QBT_TR(Clear downloaded episodes...)QBT_TR[CONTEXT=AutomatedRssDownloader]</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
};
};
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: (el) => {
removeRule(rssDownloaderRulesTable.selectedRows
.map((sRow) => rssDownloaderRulesTable.rows[sRow].full_data.name));
},
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: 'post',
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: 'QBT_TR(New rule name)QBT_TR[CONTEXT=AutomatedRssDownloader]',
loadMethod: 'iframe',
contentURL: 'newrule.html',
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const renameRule = (rule) => {
new MochaUI.Window({
id: 'renameRulePage',
title: 'QBT_TR(Rule renaming)QBT_TR[CONTEXT=AutomatedRssDownloader]',
loadMethod: 'iframe',
contentURL: 'rename_rule.html?rule=' + encodeURIComponent(rule),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const removeRule = (rules) => {
new MochaUI.Window({
id: 'removeRulePage',
title: 'QBT_TR(Rule deletion confirmation)QBT_TR[CONTEXT=AutomatedRssDownloader]',
loadMethod: 'iframe',
contentURL: 'confirmruledeletion.html?rules=' + encodeURIComponent(rules.join('|')),
scrollbars: false,
resizable: false,
maximizable: false,
width: 360,
height: 80
});
};
const clearDownloadedEpisodes = (rules) => {
new MochaUI.Window({
id: 'clearRulesPage',
title: 'QBT_TR(New rule name)QBT_TR[CONTEXT=AutomatedRssDownloader]',
loadMethod: 'iframe',
contentURL: 'confirmruleclear.html?rules=' + encodeURIComponent(rules.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 ($('creatSubfolderCombobox').value) {
case 'default':
rulesList[rule].createSubfolder = null;
break;
case 'always':
rulesList[rule].createSubfolder = true;
break;
case 'never':
rulesList[rule].createSubfolder = false;
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: 'post',
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;
$('creatSubfolderCombobox').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').innerHTML = 'QBT_TR(Last Match: Unknown)QBT_TR[CONTEXT=AutomatedRssDownloader]';
$('addPausedCombobox').value = 'default';
$('creatSubfolderCombobox').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;
$('creatSubfolderCombobox').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').innerHTML = ' QBT_TR(Last Match: %1 days ago)QBT_TR[CONTEXT=AutomatedRssDownloader]'.replace('%1', daysAgo);
}
else {
$('lastMatchText').innerHTML = 'QBT_TR(Last Match: Unknown)QBT_TR[CONTEXT=AutomatedRssDownloader]';
}
if (rulesList[ruleName].addPaused === null)
$('addPausedCombobox').value = 'default';
else
$('addPausedCombobox').value = rulesList[ruleName].addPaused ? 'always' : 'never';
if (rulesList[ruleName].createSubfolder === null)
$('creatSubfolderCombobox').value = 'default';
else
$('creatSubfolderCombobox').value = rulesList[ruleName].createSubfolder ? 'always' : 'never';
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 = 'QBT_TR(Regex mode: use Perl-compatible regular expressions)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n';
}
else {
mainPart = 'QBT_TR(Wildcard mode: you can use)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n' +
' ● QBT_TR(? to match any single character)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(* to match zero or more of any characters)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Whitespaces count as AND operators (all words, any order))QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(| is used as OR operator)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n' +
'QBT_TR(If word order is important use * instead of whitespace.)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n';
}
let secondPart = 'QBT_TR(An expression with an empty %1 clause (e.g. %2))QBT_TR[CONTEXT=AutomatedRssDownloader]'
.replace('%1', '|').replace('%2', 'expr|');
$('mustContainText').title = mainPart + secondPart + 'QBT_TR( will match all articles.)QBT_TR[CONTEXT=AutomatedRssDownloader]';
$('mustNotContainText').title = mainPart + secondPart + 'QBT_TR( will exclude all articles.)QBT_TR[CONTEXT=AutomatedRssDownloader]';
let episodeFilterTitle = 'QBT_TR(Matches articles based on episode filter.)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n' +
'QBT_TR(Example: )QBT_TR[CONTEXT=AutomatedRssDownloader]' +
'1x2;8-15;5;30-;' +
'QBT_TR( will match 2, 5, 8 through 15, 30 and onward episodes of season one)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n' +
'QBT_TR(Episode filter rules: )QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n' +
' ● QBT_TR(Season number is a mandatory non-zero value)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Episode number is a mandatory positive value)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Filter must end with semicolon)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Three range types for episodes are supported: )QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Single number: <b>1x25;</b> matches episode 25 of season one)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Normal range: <b>1x25-40;</b> matches episodes 25 through 40 of season one)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Infinite range: <b>1x25-;</b> matches episodes 25 and upward of season one, and all episodes of later seasons)QBT_TR[CONTEXT=AutomatedRssDownloader]';
episodeFilterTitle = episodeFilterTitle.replace(/<b>/g, '').replace(/<\/b>/g, '');
$('episodeFilterText').title = episodeFilterTitle;
}
initRssDownloader();
return exports();
})();
</script>