qBittorrent
rss_session.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
4  * Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
5  * Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  * In addition, as a special exception, the copyright holders give permission to
22  * link this program with the OpenSSL project's "OpenSSL" library (or with
23  * modified versions of it that use the same license as the "OpenSSL" library),
24  * and distribute the linked executables. You must obey the GNU General Public
25  * License in all respects for all of the code used other than "OpenSSL". If you
26  * modify file(s), you may extend this exception to your version of the file(s),
27  * but you are not obligated to do so. If you do not wish to do so, delete this
28  * exception statement from your version.
29  */
30 
31 #include "rss_session.h"
32 
33 #include <QDebug>
34 #include <QJsonDocument>
35 #include <QJsonObject>
36 #include <QJsonValue>
37 #include <QString>
38 #include <QThread>
39 
40 #include "../asyncfilestorage.h"
41 #include "../global.h"
42 #include "../logger.h"
43 #include "../profile.h"
44 #include "../settingsstorage.h"
45 #include "../utils/fs.h"
46 #include "rss_article.h"
47 #include "rss_feed.h"
48 #include "rss_folder.h"
49 #include "rss_item.h"
50 
51 const int MsecsPerMin = 60000;
52 const QString ConfFolderName(QStringLiteral("rss"));
53 const QString DataFolderName(QStringLiteral("rss/articles"));
54 const QString FeedsFileName(QStringLiteral("feeds.json"));
55 
56 using namespace RSS;
57 
58 QPointer<Session> Session::m_instance = nullptr;
59 
61  : m_storeProcessingEnabled("RSS/Session/EnableProcessing")
62  , m_storeRefreshInterval("RSS/Session/RefreshInterval", 30)
63  , m_storeMaxArticlesPerFeed("RSS/Session/MaxArticlesPerFeed", 50)
64  , m_workingThread(new QThread(this))
65 {
66  Q_ASSERT(!m_instance); // only one instance is allowed
67  m_instance = this;
68 
71  m_confFileStorage->moveToThread(m_workingThread);
72  connect(m_workingThread, &QThread::finished, m_confFileStorage, &AsyncFileStorage::deleteLater);
73  connect(m_confFileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString)
74  {
75  Logger::instance()->addMessage(QString("Couldn't save RSS Session configuration in %1. Error: %2")
76  .arg(fileName, errorString), Log::WARNING);
77  });
78 
81  m_dataFileStorage->moveToThread(m_workingThread);
82  connect(m_workingThread, &QThread::finished, m_dataFileStorage, &AsyncFileStorage::deleteLater);
83  connect(m_dataFileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString)
84  {
85  Logger::instance()->addMessage(QString("Couldn't save RSS Session data in %1. Error: %2")
86  .arg(fileName, errorString), Log::WARNING);
87  });
88 
89  m_itemsByPath.insert("", new Folder); // root folder
90 
91  m_workingThread->start();
92  load();
93 
94  connect(&m_refreshTimer, &QTimer::timeout, this, &Session::refresh);
95  if (isProcessingEnabled())
96  {
98  refresh();
99  }
100 
101  // Remove legacy/corrupted settings
102  // (at least on Windows, QSettings is case-insensitive and it can get
103  // confused when asked about settings that differ only in their case)
104  auto settingsStorage = SettingsStorage::instance();
105  settingsStorage->removeValue("Rss/streamList");
106  settingsStorage->removeValue("Rss/streamAlias");
107  settingsStorage->removeValue("Rss/open_folders");
108  settingsStorage->removeValue("Rss/qt5/splitter_h");
109  settingsStorage->removeValue("Rss/qt5/splitterMain");
110  settingsStorage->removeValue("Rss/hosts_cookies");
111  settingsStorage->removeValue("RSS/streamList");
112  settingsStorage->removeValue("RSS/streamAlias");
113  settingsStorage->removeValue("RSS/open_folders");
114  settingsStorage->removeValue("RSS/qt5/splitter_h");
115  settingsStorage->removeValue("RSS/qt5/splitterMain");
116  settingsStorage->removeValue("RSS/hosts_cookies");
117  settingsStorage->removeValue("Rss/Session/EnableProcessing");
118  settingsStorage->removeValue("Rss/Session/RefreshInterval");
119  settingsStorage->removeValue("Rss/Session/MaxArticlesPerFeed");
120  settingsStorage->removeValue("Rss/AutoDownloader/EnableProcessing");
121 }
122 
124 {
125  qDebug() << "Deleting RSS Session...";
126 
127  m_workingThread->quit();
128  m_workingThread->wait();
129 
130  //store();
131  delete m_itemsByPath[""]; // deleting root folder
132 
133  qDebug() << "RSS Session deleted.";
134 }
135 
137 {
138  return m_instance;
139 }
140 
141 nonstd::expected<void, QString> Session::addFolder(const QString &path)
142 {
143  const nonstd::expected<Folder *, QString> result = prepareItemDest(path);
144  if (!result)
145  return result.get_unexpected();
146 
147  const auto destFolder = result.value();
148  addItem(new Folder(path), destFolder);
149  store();
150  return {};
151 }
152 
153 nonstd::expected<void, QString> Session::addFeed(const QString &url, const QString &path)
154 {
155  if (m_feedsByURL.contains(url))
156  return nonstd::make_unexpected(tr("RSS feed with given URL already exists: %1.").arg(url));
157 
158  const nonstd::expected<Folder *, QString> result = prepareItemDest(path);
159  if (!result)
160  return result.get_unexpected();
161 
162  const auto destFolder = result.value();
163  addItem(new Feed(generateUID(), url, path, this), destFolder);
164  store();
165  if (isProcessingEnabled())
166  feedByURL(url)->refresh();
167 
168  return {};
169 }
170 
171 nonstd::expected<void, QString> Session::moveItem(const QString &itemPath, const QString &destPath)
172 {
173  if (itemPath.isEmpty())
174  return nonstd::make_unexpected(tr("Cannot move root folder."));
175 
176  auto item = m_itemsByPath.value(itemPath);
177  if (!item)
178  return nonstd::make_unexpected(tr("Item doesn't exist: %1.").arg(itemPath));
179 
180  return moveItem(item, destPath);
181 }
182 
183 nonstd::expected<void, QString> Session::moveItem(Item *item, const QString &destPath)
184 {
185  Q_ASSERT(item);
186  Q_ASSERT(item != rootFolder());
187 
188  const nonstd::expected<Folder *, QString> result = prepareItemDest(destPath);
189  if (!result)
190  return result.get_unexpected();
191 
192  auto srcFolder = static_cast<Folder *>(m_itemsByPath.value(Item::parentPath(item->path())));
193  const auto destFolder = result.value();
194  if (srcFolder != destFolder)
195  {
196  srcFolder->removeItem(item);
197  destFolder->addItem(item);
198  }
199  m_itemsByPath.insert(destPath, m_itemsByPath.take(item->path()));
200  item->setPath(destPath);
201  store();
202  return {};
203 }
204 
205 nonstd::expected<void, QString> Session::removeItem(const QString &itemPath)
206 {
207  if (itemPath.isEmpty())
208  return nonstd::make_unexpected(tr("Cannot delete root folder."));
209 
210  auto *item = m_itemsByPath.value(itemPath);
211  if (!item)
212  return nonstd::make_unexpected(tr("Item doesn't exist: %1.").arg(itemPath));
213 
214  emit itemAboutToBeRemoved(item);
215  item->cleanup();
216 
217  auto folder = static_cast<Folder *>(m_itemsByPath.value(Item::parentPath(item->path())));
218  folder->removeItem(item);
219  delete item;
220  store();
221  return {};
222 }
223 
224 QList<Item *> Session::items() const
225 {
226  return m_itemsByPath.values();
227 }
228 
229 Item *Session::itemByPath(const QString &path) const
230 {
231  return m_itemsByPath.value(path);
232 }
233 
235 {
236  QFile itemsFile(m_confFileStorage->storageDir().absoluteFilePath(FeedsFileName));
237  if (!itemsFile.exists())
238  {
239  loadLegacy();
240  return;
241  }
242 
243  if (!itemsFile.open(QFile::ReadOnly))
244  {
246  QString("Couldn't read RSS Session data from %1. Error: %2")
247  .arg(itemsFile.fileName(), itemsFile.errorString()), Log::WARNING);
248  return;
249  }
250 
251  QJsonParseError jsonError;
252  const QJsonDocument jsonDoc = QJsonDocument::fromJson(itemsFile.readAll(), &jsonError);
253  if (jsonError.error != QJsonParseError::NoError)
254  {
256  QString("Couldn't parse RSS Session data from %1. Error: %2")
257  .arg(itemsFile.fileName(), jsonError.errorString()), Log::WARNING);
258  return;
259  }
260 
261  if (!jsonDoc.isObject())
262  {
264  QString("Couldn't load RSS Session data from %1. Invalid data format.")
265  .arg(itemsFile.fileName()), Log::WARNING);
266  return;
267  }
268 
269  loadFolder(jsonDoc.object(), rootFolder());
270 }
271 
272 void Session::loadFolder(const QJsonObject &jsonObj, Folder *folder)
273 {
274  bool updated = false;
275  for (const QString &key : asConst(jsonObj.keys()))
276  {
277  const QJsonValue val {jsonObj[key]};
278  if (val.isString())
279  {
280  // previous format (reduced form) doesn't contain UID
281  QString url = val.toString();
282  if (url.isEmpty())
283  url = key;
284  addFeedToFolder(generateUID(), url, key, folder);
285  updated = true;
286  }
287  else if (val.isObject())
288  {
289  const QJsonObject valObj {val.toObject()};
290  if (valObj.contains("url"))
291  {
292  if (!valObj["url"].isString())
293  {
294  LogMsg(tr("Couldn't load RSS Feed '%1'. URL is required.")
295  .arg(QString("%1\\%2").arg(folder->path(), key)), Log::WARNING);
296  continue;
297  }
298 
299  QUuid uid;
300  if (valObj.contains("uid"))
301  {
302  uid = QUuid {valObj["uid"].toString()};
303  if (uid.isNull())
304  {
305  LogMsg(tr("Couldn't load RSS Feed '%1'. UID is invalid.")
306  .arg(QString("%1\\%2").arg(folder->path(), key)), Log::WARNING);
307  continue;
308  }
309 
310  if (m_feedsByUID.contains(uid))
311  {
312  LogMsg(tr("Duplicate RSS Feed UID: %1. Configuration seems to be corrupted.")
313  .arg(uid.toString()), Log::WARNING);
314  continue;
315  }
316  }
317  else
318  {
319  // previous format doesn't contain UID
320  uid = generateUID();
321  updated = true;
322  }
323 
324  addFeedToFolder(uid, valObj["url"].toString(), key, folder);
325  }
326  else
327  {
328  loadFolder(valObj, addSubfolder(key, folder));
329  }
330  }
331  else
332  {
333  LogMsg(tr("Couldn't load RSS Item '%1'. Invalid data format.")
334  .arg(QString::fromLatin1("%1\\%2").arg(folder->path(), key)), Log::WARNING);
335  }
336  }
337 
338  if (updated)
339  store(); // convert to updated format
340 }
341 
343 {
344  const auto legacyFeedPaths = SettingsStorage::instance()->loadValue<QStringList>("Rss/streamList");
345  const auto feedAliases = SettingsStorage::instance()->loadValue<QStringList>("Rss/streamAlias");
346  if (legacyFeedPaths.size() != feedAliases.size())
347  {
348  Logger::instance()->addMessage("Corrupted RSS list, not loading it.", Log::WARNING);
349  return;
350  }
351 
352  uint i = 0;
353  for (QString legacyPath : legacyFeedPaths)
354  {
355  if (Item::PathSeparator == QString(legacyPath[0]))
356  legacyPath.remove(0, 1);
357  const QString parentFolderPath = Item::parentPath(legacyPath);
358  const QString feedUrl = Item::relativeName(legacyPath);
359 
360  for (const QString &folderPath : asConst(Item::expandPath(parentFolderPath)))
361  addFolder(folderPath);
362 
363  const QString feedPath = feedAliases[i].isEmpty()
364  ? legacyPath
365  : Item::joinPath(parentFolderPath, feedAliases[i]);
366  addFeed(feedUrl, feedPath);
367  ++i;
368  }
369 
370  store(); // convert to new format
371 }
372 
374 {
375  m_confFileStorage->store(FeedsFileName, QJsonDocument(rootFolder()->toJsonValue().toObject()).toJson());
376 }
377 
378 nonstd::expected<Folder *, QString> Session::prepareItemDest(const QString &path)
379 {
380  if (!Item::isValidPath(path))
381  return nonstd::make_unexpected(tr("Incorrect RSS Item path: %1.").arg(path));
382 
383  if (m_itemsByPath.contains(path))
384  return nonstd::make_unexpected(tr("RSS item with given path already exists: %1.").arg(path));
385 
386  const QString destFolderPath = Item::parentPath(path);
387  const auto destFolder = qobject_cast<Folder *>(m_itemsByPath.value(destFolderPath));
388  if (!destFolder)
389  return nonstd::make_unexpected(tr("Parent folder doesn't exist: %1.").arg(destFolderPath));
390 
391  return destFolder;
392 }
393 
394 Folder *Session::addSubfolder(const QString &name, Folder *parentFolder)
395 {
396  auto folder = new Folder(Item::joinPath(parentFolder->path(), name));
397  addItem(folder, parentFolder);
398  return folder;
399 }
400 
401 Feed *Session::addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder)
402 {
403  auto feed = new Feed(uid, url, Item::joinPath(parentFolder->path(), name), this);
404  addItem(feed, parentFolder);
405  return feed;
406 }
407 
408 void Session::addItem(Item *item, Folder *destFolder)
409 {
410  if (auto feed = qobject_cast<Feed *>(item))
411  {
412  connect(feed, &Feed::titleChanged, this, &Session::handleFeedTitleChanged);
413  connect(feed, &Feed::iconLoaded, this, &Session::feedIconLoaded);
414  connect(feed, &Feed::stateChanged, this, &Session::feedStateChanged);
415  m_feedsByUID[feed->uid()] = feed;
416  m_feedsByURL[feed->url()] = feed;
417  }
418 
419  connect(item, &Item::pathChanged, this, &Session::itemPathChanged);
421  m_itemsByPath[item->path()] = item;
422  destFolder->addItem(item);
423  emit itemAdded(item);
424 }
425 
427 {
429 }
430 
431 void Session::setProcessingEnabled(const bool enabled)
432 {
433  if (m_storeProcessingEnabled != enabled)
434  {
435  m_storeProcessingEnabled = enabled;
436  if (enabled)
437  {
439  refresh();
440  }
441  else
442  {
443  m_refreshTimer.stop();
444  }
445 
446  emit processingStateChanged(enabled);
447  }
448 }
449 
451 {
452  return m_confFileStorage;
453 }
454 
456 {
457  return m_dataFileStorage;
458 }
459 
461 {
462  return static_cast<Folder *>(m_itemsByPath.value(""));
463 }
464 
465 QList<Feed *> Session::feeds() const
466 {
467  return m_feedsByURL.values();
468 }
469 
470 Feed *Session::feedByURL(const QString &url) const
471 {
472  return m_feedsByURL.value(url);
473 }
474 
476 {
477  return m_storeRefreshInterval;
478 }
479 
480 void Session::setRefreshInterval(const int refreshInterval)
481 {
483  {
486  }
487 }
488 
489 QThread *Session::workingThread() const
490 {
491  return m_workingThread;
492 }
493 
495 {
496  m_itemsByPath.remove(item->path());
497  auto feed = qobject_cast<Feed *>(item);
498  if (feed)
499  {
500  m_feedsByUID.remove(feed->uid());
501  m_feedsByURL.remove(feed->url());
502  }
503 }
504 
506 {
507  if (feed->name() == feed->url())
508  // Now we have something better than a URL.
509  // Trying to rename feed...
510  moveItem(feed, Item::joinPath(Item::parentPath(feed->path()), feed->title()));
511 }
512 
513 QUuid Session::generateUID() const
514 {
515  QUuid uid = QUuid::createUuid();
516  while (m_feedsByUID.contains(uid))
517  uid = QUuid::createUuid();
518 
519  return uid;
520 }
521 
523 {
525 }
526 
528 {
529  if (m_storeMaxArticlesPerFeed != n)
530  {
533  }
534 }
535 
537 {
538  // NOTE: Should we allow manually refreshing for disabled session?
539  rootFolder()->refresh();
540 }
void failed(const QString &fileName, const QString &errorString)
QDir storageDir() const
void store(const QString &fileName, const QByteArray &data)
void addMessage(const QString &message, const Log::MsgType &type=Log::NORMAL)
Definition: logger.cpp:73
static Logger * instance()
Definition: logger.cpp:56
void iconLoaded(Feed *feed=nullptr)
QString title() const
Definition: rss_feed.cpp:159
void refresh() override
Definition: rss_feed.cpp:132
void titleChanged(Feed *feed=nullptr)
QString url() const
Definition: rss_feed.cpp:154
void stateChanged(Feed *feed=nullptr)
void addItem(Item *item)
Definition: rss_folder.cpp:119
void removeItem(Item *item)
Definition: rss_folder.cpp:137
void refresh() override
Definition: rss_folder.cpp:88
static QString joinPath(const QString &path1, const QString &path2)
Definition: rss_item.cpp:82
void pathChanged(Item *item=nullptr)
void aboutToBeDestroyed(Item *item=nullptr)
static const QChar PathSeparator
Definition: rss_item.h:61
static QStringList expandPath(const QString &path)
Definition: rss_item.cpp:90
static QString parentPath(const QString &path)
Definition: rss_item.cpp:108
static QString relativeName(const QString &path)
Definition: rss_item.cpp:114
QString name() const
Definition: rss_item.cpp:62
void setPath(const QString &path)
Definition: rss_item.cpp:48
static bool isValidPath(const QString &path)
Definition: rss_item.cpp:67
QString path() const
Definition: rss_item.cpp:57
int refreshInterval() const
nonstd::expected< Folder *, QString > prepareItemDest(const QString &path)
void loadFolder(const QJsonObject &jsonObj, Folder *folder)
~Session() override
void handleFeedTitleChanged(Feed *feed)
bool isProcessingEnabled() const
void loadLegacy()
static Session * instance()
CachedSettingValue< int > m_storeRefreshInterval
Definition: rss_session.h:159
QTimer m_refreshTimer
Definition: rss_session.h:164
QThread * workingThread() const
QHash< QString, Item * > m_itemsByPath
Definition: rss_session.h:165
Feed * addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder)
AsyncFileStorage * m_dataFileStorage
Definition: rss_session.h:163
void itemPathChanged(Item *item)
QList< Feed * > feeds() const
AsyncFileStorage * dataFileStorage() const
void handleItemAboutToBeDestroyed(Item *item)
nonstd::expected< void, QString > addFolder(const QString &path)
void maxArticlesPerFeedChanged(int n)
Feed * feedByURL(const QString &url) const
CachedSettingValue< bool > m_storeProcessingEnabled
Definition: rss_session.h:158
CachedSettingValue< int > m_storeMaxArticlesPerFeed
Definition: rss_session.h:160
int maxArticlesPerFeed() const
Folder * rootFolder() const
nonstd::expected< void, QString > addFeed(const QString &url, const QString &path)
Folder * addSubfolder(const QString &name, Folder *parentFolder)
void feedStateChanged(Feed *feed)
QUuid generateUID() const
AsyncFileStorage * confFileStorage() const
void setRefreshInterval(int refreshInterval)
Item * itemByPath(const QString &path) const
QThread * m_workingThread
Definition: rss_session.h:161
void setMaxArticlesPerFeed(int n)
nonstd::expected< void, QString > moveItem(const QString &itemPath, const QString &destPath)
void processingStateChanged(bool enabled)
AsyncFileStorage * m_confFileStorage
Definition: rss_session.h:162
QList< Item * > items() const
void itemAdded(Item *item)
nonstd::expected< void, QString > removeItem(const QString &itemPath)
static QPointer< Session > m_instance
Definition: rss_session.h:156
QHash< QUuid, Feed * > m_feedsByUID
Definition: rss_session.h:166
void feedIconLoaded(Feed *feed)
void addItem(Item *item, Folder *destFolder)
QHash< QString, Feed * > m_feedsByURL
Definition: rss_session.h:167
void itemAboutToBeRemoved(Item *item)
void setProcessingEnabled(bool enabled)
T loadValue(const QString &key, const T &defaultValue={}) const
static SettingsStorage * instance()
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
void LogMsg(const QString &message, const Log::MsgType &type)
Definition: logger.cpp:125
@ WARNING
Definition: logger.h:47
QString fileName(const QString &filePath)
Definition: fs.cpp:87
QString expandPathAbs(const QString &path)
Definition: fs.cpp:309
QJsonValue toJsonValue(const std::optional< bool > boolValue)
QString toString(const lt::socket_type_t socketType)
Definition: session.cpp:183
QString specialFolderLocation(const SpecialFolder folder)
Definition: profile.cpp:131
const int MsecsPerMin
Definition: rss_session.cpp:51
const QString DataFolderName(QStringLiteral("rss/articles"))
const QString ConfFolderName(QStringLiteral("rss"))
const QString FeedsFileName(QStringLiteral("feeds.json"))