qBittorrent
automatedrssdownloader.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2017 Vladimir Golovnev <[email protected]>
4  * Copyright (C) 2010 Christophe Dumez <[email protected]>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19  *
20  * In addition, as a special exception, the copyright holders give permission to
21  * link this program with the OpenSSL project's "OpenSSL" library (or with
22  * modified versions of it that use the same license as the "OpenSSL" library),
23  * and distribute the linked executables. You must obey the GNU General Public
24  * License in all respects for all of the code used other than "OpenSSL". If you
25  * modify file(s), you may extend this exception to your version of the file(s),
26  * but you are not obligated to do so. If you do not wish to do so, delete this
27  * exception statement from your version.
28  */
29 
30 #include "automatedrssdownloader.h"
31 
32 #include <QCursor>
33 #include <QFileDialog>
34 #include <QMenu>
35 #include <QMessageBox>
36 #include <QRegularExpression>
37 #include <QShortcut>
38 #include <QSignalBlocker>
39 #include <QString>
40 
42 #include "base/global.h"
43 #include "base/preferences.h"
44 #include "base/rss/rss_article.h"
46 #include "base/rss/rss_feed.h"
47 #include "base/rss/rss_folder.h"
48 #include "base/rss/rss_session.h"
49 #include "base/utils/compare.h"
50 #include "base/utils/fs.h"
51 #include "base/utils/io.h"
52 #include "base/utils/string.h"
55 #include "gui/uithememanager.h"
56 #include "gui/utils.h"
57 #include "ui_automatedrssdownloader.h"
58 
59 const QString EXT_JSON {QStringLiteral(".json")};
60 const QString EXT_LEGACY {QStringLiteral(".rssrules")};
61 
63  : QDialog(parent)
64  , m_formatFilterJSON(QString::fromLatin1("%1 (*%2)").arg(tr("Rules"), EXT_JSON))
65  , m_formatFilterLegacy(QString::fromLatin1("%1 (*%2)").arg(tr("Rules (legacy)"), EXT_LEGACY))
66  , m_ui(new Ui::AutomatedRssDownloader)
67  , m_currentRuleItem(nullptr)
68 {
69  m_ui->setupUi(this);
70  // Icons
71  m_ui->removeRuleBtn->setIcon(UIThemeManager::instance()->getIcon("list-remove"));
72  m_ui->addRuleBtn->setIcon(UIThemeManager::instance()->getIcon("list-add"));
73  m_ui->addCategoryBtn->setIcon(UIThemeManager::instance()->getIcon("list-add"));
74 
75  // Ui Settings
76  m_ui->listRules->setSortingEnabled(true);
77  m_ui->listRules->setSelectionMode(QAbstractItemView::ExtendedSelection);
78  m_ui->treeMatchingArticles->setSortingEnabled(true);
79  m_ui->treeMatchingArticles->sortByColumn(0, Qt::AscendingOrder);
80  m_ui->hsplitter->setCollapsible(0, false);
81  m_ui->hsplitter->setCollapsible(1, false);
82  m_ui->hsplitter->setCollapsible(2, true); // Only the preview list is collapsible
83  m_ui->lineSavePath->setDialogCaption(tr("Destination directory"));
84  m_ui->lineSavePath->setMode(FileSystemPathEdit::Mode::DirectorySave);
85 
86  connect(m_ui->checkRegex, &QAbstractButton::toggled, this, &AutomatedRssDownloader::updateFieldsToolTips);
87  connect(m_ui->listRules, &QWidget::customContextMenuRequested, this, &AutomatedRssDownloader::displayRulesListMenu);
88 
89  m_episodeRegex = new QRegularExpression("^(^\\d{1,4}x(\\d{1,4}(-(\\d{1,4})?)?;){1,}){1,1}"
90  , QRegularExpression::CaseInsensitiveOption);
91  QString tip = "<p>" + tr("Matches articles based on episode filter.") + "</p><p><b>" + tr("Example: ")
92  + "1x2;8-15;5;30-;</b>" + tr(" will match 2, 5, 8 through 15, 30 and onward episodes of season one", "example X will match") + "</p>";
93  tip += "<p>" + tr("Episode filter rules: ") + "</p><ul><li>" + tr("Season number is a mandatory non-zero value") + "</li>"
94  + "<li>" + tr("Episode number is a mandatory positive value") + "</li>"
95  + "<li>" + tr("Filter must end with semicolon") + "</li>"
96  + "<li>" + tr("Three range types for episodes are supported: ") + "</li>" + "<li><ul>"
97  + "<li>" + tr("Single number: <b>1x25;</b> matches episode 25 of season one") + "</li>"
98  + "<li>" + tr("Normal range: <b>1x25-40;</b> matches episodes 25 through 40 of season one") + "</li>"
99  + "<li>" + tr("Infinite range: <b>1x25-;</b> matches episodes 25 and upward of season one, and all episodes of later seasons") + "</li>" + "</ul></li></ul>";
100  m_ui->lineEFilter->setToolTip(tip);
101 
103  loadSettings();
104 
109 
110  // Update matching articles when necessary
111  connect(m_ui->lineContains, &QLineEdit::textEdited, this, &AutomatedRssDownloader::handleRuleDefinitionChanged);
112  connect(m_ui->lineContains, &QLineEdit::textEdited, this, &AutomatedRssDownloader::updateMustLineValidity);
113  connect(m_ui->lineNotContains, &QLineEdit::textEdited, this, &AutomatedRssDownloader::handleRuleDefinitionChanged);
114  connect(m_ui->lineNotContains, &QLineEdit::textEdited, this, &AutomatedRssDownloader::updateMustNotLineValidity);
115  connect(m_ui->lineEFilter, &QLineEdit::textEdited, this, &AutomatedRssDownloader::handleRuleDefinitionChanged);
116  connect(m_ui->lineEFilter, &QLineEdit::textEdited, this, &AutomatedRssDownloader::updateEpisodeFilterValidity);
117  connect(m_ui->checkRegex, &QCheckBox::stateChanged, this, &AutomatedRssDownloader::handleRuleDefinitionChanged);
118  connect(m_ui->checkRegex, &QCheckBox::stateChanged, this, &AutomatedRssDownloader::updateMustLineValidity);
119  connect(m_ui->checkRegex, &QCheckBox::stateChanged, this, &AutomatedRssDownloader::updateMustNotLineValidity);
120  connect(m_ui->checkSmart, &QCheckBox::stateChanged, this, &AutomatedRssDownloader::handleRuleDefinitionChanged);
121  connect(m_ui->spinIgnorePeriod, qOverload<int>(&QSpinBox::valueChanged)
123 
124  connect(m_ui->listFeeds, &QListWidget::itemChanged, this, &AutomatedRssDownloader::handleFeedCheckStateChange);
125 
126  connect(m_ui->listRules, &QListWidget::itemSelectionChanged, this, &AutomatedRssDownloader::updateRuleDefinitionBox);
127  connect(m_ui->listRules, &QListWidget::itemChanged, this, &AutomatedRssDownloader::handleRuleCheckStateChange);
128 
129  const auto *editHotkey = new QShortcut(Qt::Key_F2, m_ui->listRules, nullptr, nullptr, Qt::WidgetShortcut);
130  connect(editHotkey, &QShortcut::activated, this, &AutomatedRssDownloader::renameSelectedRule);
131  const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, m_ui->listRules, nullptr, nullptr, Qt::WidgetShortcut);
132  connect(deleteHotkey, &QShortcut::activated, this, &AutomatedRssDownloader::on_removeRuleBtn_clicked);
133 
134  connect(m_ui->listRules, &QAbstractItemView::doubleClicked, this, &AutomatedRssDownloader::renameSelectedRule);
135 
136  loadFeedList();
137 
138  m_ui->listRules->blockSignals(true);
139  for (const RSS::AutoDownloadRule &rule : asConst(RSS::AutoDownloader::instance()->rules()))
140  createRuleItem(rule);
141  m_ui->listRules->blockSignals(false);
142 
144 
145  if (RSS::AutoDownloader::instance()->isProcessingEnabled())
146  m_ui->labelWarn->hide();
149 }
150 
152 {
153  // Save current item on exit
154  saveEditedRule();
155  saveSettings();
156 
157  delete m_ui;
158  delete m_episodeRegex;
159 }
160 
162 {
163  const Preferences *const pref = Preferences::instance();
164  Utils::Gui::resize(this, pref->getRssGeometrySize());
165  m_ui->hsplitter->restoreState(pref->getRssHSplitterSizes());
166 }
167 
169 {
170  Preferences *const pref = Preferences::instance();
171  pref->setRssGeometrySize(size());
172  pref->setRssHSplitterSizes(m_ui->hsplitter->saveState());
173 }
174 
176 {
177  QListWidgetItem *item = new QListWidgetItem(rule.name(), m_ui->listRules);
178  m_itemsByRuleName.insert(rule.name(), item);
179  item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
180  item->setCheckState(rule.isEnabled() ? Qt::Checked : Qt::Unchecked);
181 }
182 
184 {
185  const QSignalBlocker feedListSignalBlocker(m_ui->listFeeds);
186 
187  for (const auto feed : asConst(RSS::Session::instance()->feeds()))
188  {
189  QListWidgetItem *item = new QListWidgetItem(feed->name(), m_ui->listFeeds);
190  item->setData(Qt::UserRole, feed->url());
191  item->setFlags(item->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate);
192  }
193 
194  updateFeedList();
195 }
196 
198 {
199  const QSignalBlocker feedListSignalBlocker(m_ui->listFeeds);
200 
201  QList<QListWidgetItem *> selection;
202 
203  if (m_currentRuleItem)
204  selection << m_currentRuleItem;
205  else
206  selection = m_ui->listRules->selectedItems();
207 
208  bool enable = !selection.isEmpty();
209 
210  for (int i = 0; i < m_ui->listFeeds->count(); ++i)
211  {
212  QListWidgetItem *item = m_ui->listFeeds->item(i);
213  const QString feedURL = item->data(Qt::UserRole).toString();
214  item->setHidden(!enable);
215 
216  bool allEnabled = true;
217  bool anyEnabled = false;
218 
219  for (const QListWidgetItem *ruleItem : asConst(selection))
220  {
221  const auto rule = RSS::AutoDownloader::instance()->ruleByName(ruleItem->text());
222  if (rule.feedURLs().contains(feedURL))
223  anyEnabled = true;
224  else
225  allEnabled = false;
226  }
227 
228  if (anyEnabled && allEnabled)
229  item->setCheckState(Qt::Checked);
230  else if (anyEnabled)
231  item->setCheckState(Qt::PartiallyChecked);
232  else
233  item->setCheckState(Qt::Unchecked);
234  }
235 
236  m_ui->listFeeds->sortItems();
237  m_ui->lblListFeeds->setEnabled(enable);
238  m_ui->listFeeds->setEnabled(enable);
239 }
240 
242 {
243  const QList<QListWidgetItem *> selection = m_ui->listRules->selectedItems();
244  QListWidgetItem *currentRuleItem = ((selection.count() == 1) ? selection.first() : nullptr);
245  if (m_currentRuleItem != currentRuleItem)
246  {
247  saveEditedRule(); // Save previous rule first
248  m_currentRuleItem = currentRuleItem;
249  //m_ui->listRules->setCurrentItem(m_currentRuleItem);
250  }
251 
252  // Update rule definition box
253  if (m_currentRuleItem)
254  {
256 
257  m_ui->lineContains->setText(m_currentRule.mustContain());
258  m_ui->lineNotContains->setText(m_currentRule.mustNotContain());
259  if (!m_currentRule.episodeFilter().isEmpty())
260  m_ui->lineEFilter->setText(m_currentRule.episodeFilter());
261  else
262  m_ui->lineEFilter->clear();
263  m_ui->checkBoxSaveDiffDir->setChecked(!m_currentRule.savePath().isEmpty());
264  m_ui->lineSavePath->setSelectedPath(Utils::Fs::toNativePath(m_currentRule.savePath()));
265  m_ui->checkRegex->blockSignals(true);
266  m_ui->checkRegex->setChecked(m_currentRule.useRegex());
267  m_ui->checkRegex->blockSignals(false);
268  m_ui->checkSmart->blockSignals(true);
269  m_ui->checkSmart->setChecked(m_currentRule.useSmartFilter());
270  m_ui->checkSmart->blockSignals(false);
271  m_ui->comboCategory->setCurrentIndex(m_ui->comboCategory->findText(m_currentRule.assignedCategory()));
272  if (m_currentRule.assignedCategory().isEmpty())
273  m_ui->comboCategory->clearEditText();
274  int index = 0;
275  if (m_currentRule.addPaused().has_value())
276  index = (*m_currentRule.addPaused() ? 1 : 2);
277  m_ui->comboAddPaused->setCurrentIndex(index);
278  index = 0;
280  index = static_cast<int>(*m_currentRule.torrentContentLayout()) + 1;
281  m_ui->comboContentLayout->setCurrentIndex(index);
282  m_ui->spinIgnorePeriod->setValue(m_currentRule.ignoreDays());
283  QDateTime dateTime = m_currentRule.lastMatch();
284  QString lMatch;
285  if (dateTime.isValid())
286  lMatch = tr("Last Match: %1 days ago").arg(dateTime.daysTo(QDateTime::currentDateTime()));
287  else
288  lMatch = tr("Last Match: Unknown");
289  m_ui->lblLastMatch->setText(lMatch);
293 
294  updateFieldsToolTips(m_ui->checkRegex->isChecked());
295  m_ui->ruleDefBox->setEnabled(true);
296  }
297  else
298  {
301  m_ui->ruleDefBox->setEnabled(false);
302  }
303 
304  updateFeedList();
306 }
307 
309 {
310  m_ui->lineContains->clear();
311  m_ui->lineNotContains->clear();
312  m_ui->lineEFilter->clear();
313  m_ui->checkBoxSaveDiffDir->setChecked(false);
314  m_ui->lineSavePath->clear();
315  m_ui->comboCategory->clearEditText();
316  m_ui->comboCategory->setCurrentIndex(-1);
317  m_ui->checkRegex->setChecked(false);
318  m_ui->checkSmart->setChecked(false);
319  m_ui->spinIgnorePeriod->setValue(0);
320  m_ui->comboAddPaused->clearEditText();
321  m_ui->comboAddPaused->setCurrentIndex(-1);
322  m_ui->comboContentLayout->clearEditText();
323  m_ui->comboContentLayout->setCurrentIndex(-1);
324  updateFieldsToolTips(m_ui->checkRegex->isChecked());
328 }
329 
331 {
332  // Load torrent categories
333  QStringList categories = BitTorrent::Session::instance()->categories();
334  std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
335  m_ui->comboCategory->addItem("");
336  m_ui->comboCategory->addItems(categories);
337 }
338 
340 {
341  if (!m_currentRuleItem || !m_ui->ruleDefBox->isEnabled()) return;
342 
343  m_currentRule.setEnabled(m_currentRuleItem->checkState() != Qt::Unchecked);
344  m_currentRule.setUseRegex(m_ui->checkRegex->isChecked());
345  m_currentRule.setUseSmartFilter(m_ui->checkSmart->isChecked());
346  m_currentRule.setMustContain(m_ui->lineContains->text());
347  m_currentRule.setMustNotContain(m_ui->lineNotContains->text());
348  m_currentRule.setEpisodeFilter(m_ui->lineEFilter->text());
349  m_currentRule.setSavePath(m_ui->checkBoxSaveDiffDir->isChecked() ? m_ui->lineSavePath->selectedPath() : "");
350  m_currentRule.setCategory(m_ui->comboCategory->currentText());
351  std::optional<bool> addPaused;
352  if (m_ui->comboAddPaused->currentIndex() == 1)
353  addPaused = true;
354  else if (m_ui->comboAddPaused->currentIndex() == 2)
355  addPaused = false;
356  m_currentRule.setAddPaused(addPaused);
357 
358  std::optional<BitTorrent::TorrentContentLayout> contentLayout;
359  if (m_ui->comboContentLayout->currentIndex() > 0)
360  contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->comboContentLayout->currentIndex() - 1);
361  m_currentRule.setTorrentContentLayout(contentLayout);
362 
363  m_currentRule.setIgnoreDays(m_ui->spinIgnorePeriod->value());
364 }
365 
367 {
368  if (!m_currentRuleItem || !m_ui->ruleDefBox->isEnabled()) return;
369 
372 }
373 
375 {
376 // saveEditedRule();
377 
378  // Ask for a rule name
379  const QString ruleName = AutoExpandableDialog::getText(
380  this, tr("New rule name"), tr("Please type the name of the new download rule."));
381  if (ruleName.isEmpty()) return;
382 
383  // Check if this rule name already exists
384  if (RSS::AutoDownloader::instance()->hasRule(ruleName))
385  {
386  QMessageBox::warning(this, tr("Rule name conflict")
387  , tr("A rule with this name already exists, please choose another name."));
388  return;
389  }
390 
392 }
393 
395 {
396  const QList<QListWidgetItem *> selection = m_ui->listRules->selectedItems();
397  if (selection.isEmpty()) return;
398 
399  // Ask for confirmation
400  const QString confirmText = ((selection.count() == 1)
401  ? tr("Are you sure you want to remove the download rule named '%1'?")
402  .arg(selection.first()->text())
403  : tr("Are you sure you want to remove the selected download rules?"));
404  if (QMessageBox::question(this, tr("Rule deletion confirmation"), confirmText, QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes)
405  return;
406 
407  for (const QListWidgetItem *item : selection)
408  RSS::AutoDownloader::instance()->removeRule(item->text());
409 }
410 
412 {
413  const QString newCategoryName = TorrentCategoryDialog::createCategory(this);
414 
415  if (!newCategoryName.isEmpty())
416  {
417  m_ui->comboCategory->addItem(newCategoryName);
418  m_ui->comboCategory->setCurrentText(newCategoryName);
419  }
420 }
421 
423 {
424  if (RSS::AutoDownloader::instance()->rules().isEmpty())
425  {
426  QMessageBox::warning(this, tr("Invalid action")
427  , tr("The list is empty, there is nothing to export."));
428  return;
429  }
430 
431  QString selectedFilter {m_formatFilterJSON};
432  QString path = QFileDialog::getSaveFileName(
433  this, tr("Export RSS rules"), QDir::homePath()
434  , QString::fromLatin1("%1;;%2").arg(m_formatFilterJSON, m_formatFilterLegacy), &selectedFilter);
435  if (path.isEmpty()) return;
436 
438  {
439  (selectedFilter == m_formatFilterJSON)
442  };
443 
445  {
446  if (!path.endsWith(EXT_JSON, Qt::CaseInsensitive))
447  path += EXT_JSON;
448  }
449  else
450  {
451  if (!path.endsWith(EXT_LEGACY, Qt::CaseInsensitive))
452  path += EXT_LEGACY;
453  }
454 
455  const QByteArray rules = RSS::AutoDownloader::instance()->exportRules(format);
456  const nonstd::expected<void, QString> result = Utils::IO::saveToFile(path, rules);
457  if (!result)
458  {
459  QMessageBox::critical(this, tr("I/O Error")
460  , tr("Failed to create the destination file. Reason: %1").arg(result.error()));
461  }
462 }
463 
465 {
466  QString selectedFilter {m_formatFilterJSON};
467  QString path = QFileDialog::getOpenFileName(
468  this, tr("Import RSS rules"), QDir::homePath()
469  , QString::fromLatin1("%1;;%2").arg(m_formatFilterJSON, m_formatFilterLegacy), &selectedFilter);
470  if (path.isEmpty() || !QFile::exists(path))
471  return;
472 
473  QFile file {path};
474  if (!file.open(QIODevice::ReadOnly))
475  {
476  QMessageBox::critical(
477  this, tr("I/O Error")
478  , tr("Failed to open the file. Reason: %1").arg(file.errorString()));
479  return;
480  }
481 
483  {
484  (selectedFilter == m_formatFilterJSON)
487  };
488 
489  try
490  {
491  RSS::AutoDownloader::instance()->importRules(file.readAll(),format);
492  }
493  catch (const RSS::ParsingError &error)
494  {
495  QMessageBox::critical(
496  this, tr("Import Error")
497  , tr("Failed to import the selected rules file. Reason: %1").arg(error.message()));
498  }
499 }
500 
502 {
503  QMenu *menu = new QMenu(this);
504  menu->setAttribute(Qt::WA_DeleteOnClose);
505 
506  menu->addAction(UIThemeManager::instance()->getIcon("list-add"), tr("Add new rule...")
508 
509  const QList<QListWidgetItem *> selection = m_ui->listRules->selectedItems();
510 
511  if (!selection.isEmpty())
512  {
513  if (selection.count() == 1)
514  {
515  menu->addAction(UIThemeManager::instance()->getIcon("list-remove"), tr("Delete rule")
517  menu->addSeparator();
518  menu->addAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Rename rule...")
520  }
521  else
522  {
523  menu->addAction(UIThemeManager::instance()->getIcon("list-remove"), tr("Delete selected rules")
525  }
526 
527  menu->addSeparator();
528  menu->addAction(UIThemeManager::instance()->getIcon("edit-clear"), tr("Clear downloaded episodes...")
530  }
531 
532  menu->popup(QCursor::pos());
533 }
534 
536 {
537  const QList<QListWidgetItem *> selection = m_ui->listRules->selectedItems();
538  if (selection.isEmpty()) return;
539 
540  QListWidgetItem *item = selection.first();
541  forever
542  {
543  QString newName = AutoExpandableDialog::getText(
544  this, tr("Rule renaming"), tr("Please type the new rule name")
545  , QLineEdit::Normal, item->text());
546  newName = newName.trimmed();
547  if (newName.isEmpty()) return;
548 
549  if (RSS::AutoDownloader::instance()->hasRule(newName))
550  {
551  QMessageBox::warning(this, tr("Rule name conflict")
552  , tr("A rule with this name already exists, please choose another name."));
553  }
554  else
555  {
556  // Rename the rule
557  RSS::AutoDownloader::instance()->renameRule(item->text(), newName);
558  return;
559  }
560  }
561 }
562 
564 {
565  m_ui->listRules->setCurrentItem(ruleItem);
566 }
567 
569 {
570  const QMessageBox::StandardButton reply = QMessageBox::question(
571  this,
572  tr("Clear downloaded episodes"),
573  tr("Are you sure you want to clear the list of downloaded episodes for the selected rule?"),
574  QMessageBox::Yes | QMessageBox::No);
575 
576  if (reply == QMessageBox::Yes)
577  {
580  }
581 }
582 
584 {
585  const QString feedURL = feedItem->data(Qt::UserRole).toString();
586  for (QListWidgetItem *ruleItem : asConst(m_ui->listRules->selectedItems()))
587  {
588  RSS::AutoDownloadRule rule = (ruleItem == m_currentRuleItem
589  ? m_currentRule
590  : RSS::AutoDownloader::instance()->ruleByName(ruleItem->text()));
591  QStringList affectedFeeds = rule.feedURLs();
592  if ((feedItem->checkState() == Qt::Checked) && !affectedFeeds.contains(feedURL))
593  affectedFeeds << feedURL;
594  else if ((feedItem->checkState() == Qt::Unchecked) && affectedFeeds.contains(feedURL))
595  affectedFeeds.removeOne(feedURL);
596 
597  rule.setFeedURLs(affectedFeeds);
598  if (ruleItem != m_currentRuleItem)
600  else
601  m_currentRule = rule;
602  }
603 
605 }
606 
608 {
609  m_ui->treeMatchingArticles->clear();
610 
611  for (const QListWidgetItem *ruleItem : asConst(m_ui->listRules->selectedItems()))
612  {
613  RSS::AutoDownloadRule rule = (ruleItem == m_currentRuleItem
614  ? m_currentRule
615  : RSS::AutoDownloader::instance()->ruleByName(ruleItem->text()));
616  for (const QString &feedURL : asConst(rule.feedURLs()))
617  {
618  auto feed = RSS::Session::instance()->feedByURL(feedURL);
619  if (!feed) continue; // feed doesn't exist
620 
621  QStringList matchingArticles;
622  for (const auto article : asConst(feed->articles()))
623  if (rule.matches(article->data()))
624  matchingArticles << article->title();
625  if (!matchingArticles.isEmpty())
626  addFeedArticlesToTree(feed, matchingArticles);
627  }
628  }
629 
630  m_treeListEntries.clear();
631 }
632 
633 void AutomatedRssDownloader::addFeedArticlesToTree(RSS::Feed *feed, const QStringList &articles)
634 {
635  // Turn off sorting while inserting
636  m_ui->treeMatchingArticles->setSortingEnabled(false);
637 
638  // Check if this feed is already in the tree
639  QTreeWidgetItem *treeFeedItem = nullptr;
640  for (int i = 0; i < m_ui->treeMatchingArticles->topLevelItemCount(); ++i)
641  {
642  QTreeWidgetItem *item = m_ui->treeMatchingArticles->topLevelItem(i);
643  if (item->data(0, Qt::UserRole).toString() == feed->url())
644  {
645  treeFeedItem = item;
646  break;
647  }
648  }
649 
650  // If there is none, create it
651  if (!treeFeedItem)
652  {
653  treeFeedItem = new QTreeWidgetItem(QStringList() << feed->name());
654  treeFeedItem->setToolTip(0, feed->name());
655  QFont f = treeFeedItem->font(0);
656  f.setBold(true);
657  treeFeedItem->setFont(0, f);
658  treeFeedItem->setData(0, Qt::DecorationRole, UIThemeManager::instance()->getIcon("inode-directory"));
659  treeFeedItem->setData(0, Qt::UserRole, feed->url());
660  m_ui->treeMatchingArticles->addTopLevelItem(treeFeedItem);
661  }
662 
663  // Insert the articles
664  for (const QString &article : articles)
665  {
666  const std::pair<QString, QString> key(feed->name(), article);
667 
668  if (!m_treeListEntries.contains(key))
669  {
670  m_treeListEntries << key;
671  QTreeWidgetItem *item = new QTreeWidgetItem(QStringList() << article);
672  item->setToolTip(0, article);
673  treeFeedItem->addChild(item);
674  }
675  }
676 
677  m_ui->treeMatchingArticles->expandItem(treeFeedItem);
678  m_ui->treeMatchingArticles->sortItems(0, Qt::AscendingOrder);
679  m_ui->treeMatchingArticles->setSortingEnabled(true);
680 }
681 
683 {
684  QString tip;
685  if (regex)
686  {
687  tip = "<p>" + tr("Regex mode: use Perl-compatible regular expressions") + "</p>";
688  }
689  else
690  {
691  tip = "<p>" + tr("Wildcard mode: you can use") + "<ul>"
692  + "<li>" + tr("? to match any single character") + "</li>"
693  + "<li>" + tr("* to match zero or more of any characters") + "</li>"
694  + "<li>" + tr("Whitespaces count as AND operators (all words, any order)") + "</li>"
695  + "<li>" + tr("| is used as OR operator") + "</li></ul></p>"
696  + "<p>" + tr("If word order is important use * instead of whitespace.") + "</p>";
697  }
698 
699  // Whether regex or wildcard, warn about a potential gotcha for users.
700  // Explanatory string broken over multiple lines for readability (and multiple
701  // statements to prevent uncrustify indenting excessively.
702  tip += "<p>";
703  tip += tr("An expression with an empty %1 clause (e.g. %2)",
704  "We talk about regex/wildcards in the RSS filters section here."
705  " So a valid sentence would be: An expression with an empty | clause (e.g. expr|)"
706  ).arg("<tt>|</tt>", "<tt>expr|</tt>");
707  m_ui->lineContains->setToolTip(tip + tr(" will match all articles.") + "</p>");
708  m_ui->lineNotContains->setToolTip(tip + tr(" will exclude all articles.") + "</p>");
709 }
710 
712 {
713  const QString text = m_ui->lineContains->text();
714  bool isRegex = m_ui->checkRegex->isChecked();
715  bool valid = true;
716  QString error;
717 
718  if (!text.isEmpty())
719  {
720  QStringList tokens;
721  if (isRegex)
722  {
723  tokens << text;
724  }
725  else
726  {
727  for (const QString &token : asConst(text.split('|')))
728  tokens << Utils::String::wildcardToRegexPattern(token);
729  }
730 
731  for (const QString &token : asConst(tokens))
732  {
733  QRegularExpression reg(token, QRegularExpression::CaseInsensitiveOption);
734  if (!reg.isValid())
735  {
736  if (isRegex)
737  error = tr("Position %1: %2").arg(reg.patternErrorOffset()).arg(reg.errorString());
738  valid = false;
739  break;
740  }
741  }
742  }
743 
744  if (valid)
745  {
746  m_ui->lineContains->setStyleSheet("");
747  m_ui->labelMustStat->setPixmap(QPixmap());
748  m_ui->labelMustStat->setToolTip("");
749  }
750  else
751  {
752  m_ui->lineContains->setStyleSheet("QLineEdit { color: #ff0000; }");
753  m_ui->labelMustStat->setPixmap(UIThemeManager::instance()->getIcon("task-attention").pixmap(16, 16));
754  m_ui->labelMustStat->setToolTip(error);
755  }
756 }
757 
759 {
760  const QString text = m_ui->lineNotContains->text();
761  bool isRegex = m_ui->checkRegex->isChecked();
762  bool valid = true;
763  QString error;
764 
765  if (!text.isEmpty())
766  {
767  QStringList tokens;
768  if (isRegex)
769  {
770  tokens << text;
771  }
772  else
773  {
774  for (const QString &token : asConst(text.split('|')))
775  tokens << Utils::String::wildcardToRegexPattern(token);
776  }
777 
778  for (const QString &token : asConst(tokens))
779  {
780  QRegularExpression reg(token, QRegularExpression::CaseInsensitiveOption);
781  if (!reg.isValid())
782  {
783  if (isRegex)
784  error = tr("Position %1: %2").arg(reg.patternErrorOffset()).arg(reg.errorString());
785  valid = false;
786  break;
787  }
788  }
789  }
790 
791  if (valid)
792  {
793  m_ui->lineNotContains->setStyleSheet("");
794  m_ui->labelMustNotStat->setPixmap(QPixmap());
795  m_ui->labelMustNotStat->setToolTip("");
796  }
797  else
798  {
799  m_ui->lineNotContains->setStyleSheet("QLineEdit { color: #ff0000; }");
800  m_ui->labelMustNotStat->setPixmap(UIThemeManager::instance()->getIcon("task-attention").pixmap(16, 16));
801  m_ui->labelMustNotStat->setToolTip(error);
802  }
803 }
804 
806 {
807  const QString text = m_ui->lineEFilter->text();
808  bool valid = text.isEmpty() || m_episodeRegex->match(text).hasMatch();
809 
810  if (valid)
811  {
812  m_ui->lineEFilter->setStyleSheet("");
813  m_ui->labelEpFilterStat->setPixmap(QPixmap());
814  }
815  else
816  {
817  m_ui->lineEFilter->setStyleSheet("QLineEdit { color: #ff0000; }");
818  m_ui->labelEpFilterStat->setPixmap(UIThemeManager::instance()->getIcon("task-attention").pixmap(16, 16));
819  }
820 }
821 
823 {
826 }
827 
828 void AutomatedRssDownloader::handleRuleAdded(const QString &ruleName)
829 {
831 }
832 
833 void AutomatedRssDownloader::handleRuleRenamed(const QString &ruleName, const QString &oldRuleName)
834 {
835  auto item = m_itemsByRuleName.take(oldRuleName);
836  m_itemsByRuleName.insert(ruleName, item);
837  if (m_currentRule.name() == oldRuleName)
838  m_currentRule.setName(ruleName);
839  item->setText(ruleName);
840 }
841 
842 void AutomatedRssDownloader::handleRuleChanged(const QString &ruleName)
843 {
844  auto item = m_itemsByRuleName.value(ruleName);
845  if (item && (item != m_currentRuleItem))
846  item->setCheckState(RSS::AutoDownloader::instance()->ruleByName(ruleName).isEnabled() ? Qt::Checked : Qt::Unchecked);
847 }
848 
850 {
851  m_currentRuleItem = nullptr;
852  delete m_itemsByRuleName.take(ruleName);
853 }
854 
856 {
857  m_ui->labelWarn->setVisible(!enabled);
858 }
const QString EXT_JSON
const QString EXT_LEGACY
static QString getText(QWidget *parent, const QString &title, const QString &label, QLineEdit::EchoMode mode=QLineEdit::Normal, const QString &text={}, bool *ok=nullptr, bool excludeExtension=false, Qt::InputMethodHints inputMethodHints=Qt::ImhNone)
QRegularExpression * m_episodeRegex
AutomatedRssDownloader(QWidget *parent=nullptr)
void handleProcessingStateChanged(bool enabled)
QSet< std::pair< QString, QString > > m_treeListEntries
void handleRuleRenamed(const QString &ruleName, const QString &oldRuleName)
void handleFeedCheckStateChange(QListWidgetItem *feedItem)
QListWidgetItem * m_currentRuleItem
void handleRuleAboutToBeRemoved(const QString &ruleName)
RSS::AutoDownloadRule m_currentRule
void handleRuleCheckStateChange(QListWidgetItem *ruleItem)
void handleRuleAdded(const QString &ruleName)
QHash< QString, QListWidgetItem * > m_itemsByRuleName
Ui::AutomatedRssDownloader * m_ui
void addFeedArticlesToTree(RSS::Feed *feed, const QStringList &articles)
void createRuleItem(const RSS::AutoDownloadRule &rule)
void handleRuleChanged(const QString &ruleName)
static Session * instance()
Definition: session.cpp:997
QStringList categories() const
Definition: session.cpp:659
QString message() const noexcept
Definition: exceptions.cpp:36
@ DirectorySave
selecting directories for saving
static Preferences * instance()
void setRssGeometrySize(const QSize &geometry)
QSize getRssGeometrySize() const
void setRssHSplitterSizes(const QByteArray &sizes)
QByteArray getRssHSplitterSizes() const
void setTorrentContentLayout(std::optional< BitTorrent::TorrentContentLayout > contentLayout)
void setEpisodeFilter(const QString &e)
std::optional< BitTorrent::TorrentContentLayout > torrentContentLayout() const
void setPreviouslyMatchedEpisodes(const QStringList &previouslyMatchedEpisodes)
void setUseRegex(bool enabled)
void setMustContain(const QString &tokens)
void setSavePath(const QString &savePath)
std::optional< bool > addPaused() const
void setCategory(const QString &category)
void setUseSmartFilter(bool enabled)
void setFeedURLs(const QStringList &urls)
void setName(const QString &name)
QStringList feedURLs() const
bool matches(const QVariantHash &articleData) const
void setMustNotContain(const QString &tokens)
void setAddPaused(std::optional< bool > addPaused)
void ruleChanged(const QString &ruleName)
void removeRule(const QString &ruleName)
void processingStateChanged(bool enabled)
bool renameRule(const QString &ruleName, const QString &newRuleName)
static AutoDownloader * instance()
void ruleAdded(const QString &ruleName)
void ruleAboutToBeRemoved(const QString &ruleName)
void ruleRenamed(const QString &ruleName, const QString &oldRuleName)
AutoDownloadRule ruleByName(const QString &ruleName) const
QByteArray exportRules(RulesFileFormat format=RulesFileFormat::JSON) const
void insertRule(const AutoDownloadRule &rule)
void importRules(const QByteArray &data, RulesFileFormat format=RulesFileFormat::JSON)
QString title() const
Definition: rss_feed.cpp:159
QString url() const
Definition: rss_feed.cpp:154
QString name() const
Definition: rss_item.cpp:62
static Session * instance()
Feed * feedByURL(const QString &url) const
static QString createCategory(QWidget *parent, const QString &parentCategoryName={})
static UIThemeManager * instance()
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
QString toNativePath(const QString &path)
Definition: fs.cpp:64
void resize(QWidget *widget, const QSize &newSize={})
Definition: utils.cpp:54
nonstd::expected< void, QString > saveToFile(const QString &path, const QByteArray &data)
Definition: io.cpp:69
QString wildcardToRegexPattern(const QString &pattern)
Definition: string.cpp:57
file(GLOB QBT_TS_FILES "${qBittorrent_SOURCE_DIR}/src/lang/*.ts") set_source_files_properties($
Definition: CMakeLists.txt:5
void f()
Definition: test2.c:1