File indexing completed on 2024-05-12 16:21:28

0001 /**
0002  * SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
0003  * SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006  */
0007 
0008 #include "enclosure.h"
0009 #include "enclosurelogging.h"
0010 
0011 #include <KLocalizedString>
0012 #include <QFile>
0013 #include <QFileInfo>
0014 #include <QMimeDatabase>
0015 #include <QNetworkReply>
0016 #include <QSqlQuery>
0017 
0018 #include <attachedpictureframe.h>
0019 #include <id3v2frame.h>
0020 #include <id3v2tag.h>
0021 #include <mpegfile.h>
0022 
0023 #include "audiomanager.h"
0024 #include "database.h"
0025 #include "datamanager.h"
0026 #include "enclosuredownloadjob.h"
0027 #include "entry.h"
0028 #include "error.h"
0029 #include "fetcher.h"
0030 #include "models/downloadmodel.h"
0031 #include "models/errorlogmodel.h"
0032 #include "networkconnectionmanager.h"
0033 #include "settingsmanager.h"
0034 #include "storagemanager.h"
0035 #include "sync/sync.h"
0036 
0037 Enclosure::Enclosure(Entry *entry)
0038     : QObject(entry)
0039     , m_entry(entry)
0040 {
0041     connect(this, &Enclosure::playPositionChanged, this, &Enclosure::leftDurationChanged);
0042     connect(this, &Enclosure::statusChanged, &DownloadModel::instance(), &DownloadModel::monitorDownloadStatus);
0043     connect(this, &Enclosure::downloadError, &ErrorLogModel::instance(), &ErrorLogModel::monitorErrorMessages);
0044     connect(&Fetcher::instance(), &Fetcher::entryUpdated, this, [this](const QString &url, const QString &id) {
0045         if ((m_entry->feed()->url() == url) && (m_entry->id() == id)) {
0046             updateFromDb();
0047         }
0048     });
0049 
0050     // we use the relayed signal from AudioManager::playbackRateChanged by
0051     // DataManager; this is required to avoid a dependency loop on startup
0052     connect(&DataManager::instance(), &DataManager::playbackRateChanged, this, &Enclosure::leftDurationChanged);
0053 
0054     QSqlQuery query;
0055     query.prepare(QStringLiteral("SELECT * FROM Enclosures WHERE id=:id"));
0056     query.bindValue(QStringLiteral(":id"), entry->id());
0057     Database::instance().execute(query);
0058 
0059     if (!query.next()) {
0060         return;
0061     }
0062 
0063     m_duration = query.value(QStringLiteral("duration")).toInt();
0064     m_size = query.value(QStringLiteral("size")).toInt();
0065     m_title = query.value(QStringLiteral("title")).toString();
0066     m_type = query.value(QStringLiteral("type")).toString();
0067     m_url = query.value(QStringLiteral("url")).toString();
0068     m_playposition = query.value(QStringLiteral("playposition")).toLongLong();
0069     m_status = dbToStatus(query.value(QStringLiteral("downloaded")).toInt());
0070     m_playposition_dbsave = m_playposition;
0071 
0072     checkSizeOnDisk();
0073 }
0074 
0075 void Enclosure::updateFromDb()
0076 {
0077     // This method is used to update the most relevant fields from the RSS feed,
0078     // most notably the download URL.  It's deliberatly only updating the
0079     // duration and size if the URL has changed, since these values are
0080     // notably untrustworthy.  We generally get them from the files themselves
0081     // at the time they are downloaded.
0082     QSqlQuery query;
0083     query.prepare(QStringLiteral("SELECT * FROM Enclosures WHERE id=:id"));
0084     query.bindValue(QStringLiteral(":id"), m_entry->id());
0085     Database::instance().execute(query);
0086 
0087     if (!query.next()) {
0088         return;
0089     }
0090 
0091     if (m_url != query.value(QStringLiteral("url")).toString() && m_status != Downloaded) {
0092         // this means that the audio file has changed, or at least its location
0093         // let's only do something if the file isn't downloaded.
0094         // try to delete the file first (it actually shouldn't exist)
0095         deleteFile();
0096 
0097         m_url = query.value(QStringLiteral("url")).toString();
0098         Q_EMIT urlChanged(m_url);
0099         Q_EMIT pathChanged(path());
0100 
0101         if (m_duration != query.value(QStringLiteral("duration")).toInt()) {
0102             m_duration = query.value(QStringLiteral("duration")).toInt();
0103             Q_EMIT durationChanged();
0104         }
0105         if (m_size != query.value(QStringLiteral("size")).toInt()) {
0106             m_size = query.value(QStringLiteral("size")).toInt();
0107             Q_EMIT sizeChanged();
0108         }
0109         if (m_title != query.value(QStringLiteral("title")).toString()) {
0110             m_title = query.value(QStringLiteral("title")).toString();
0111             Q_EMIT titleChanged(m_title);
0112         }
0113         if (m_type != query.value(QStringLiteral("type")).toString()) {
0114             m_type = query.value(QStringLiteral("type")).toString();
0115             Q_EMIT typeChanged(m_type);
0116         }
0117     }
0118 }
0119 
0120 int Enclosure::statusToDb(Enclosure::Status status)
0121 {
0122     switch (status) {
0123     case Enclosure::Status::Downloadable:
0124         return 0;
0125     case Enclosure::Status::Downloading:
0126         return 1;
0127     case Enclosure::Status::PartiallyDownloaded:
0128         return 2;
0129     case Enclosure::Status::Downloaded:
0130         return 3;
0131     default:
0132         return -1;
0133     }
0134 }
0135 
0136 Enclosure::Status Enclosure::dbToStatus(int value)
0137 {
0138     switch (value) {
0139     case 0:
0140         return Enclosure::Status::Downloadable;
0141     case 1:
0142         return Enclosure::Status::Downloading;
0143     case 2:
0144         return Enclosure::Status::PartiallyDownloaded;
0145     case 3:
0146         return Enclosure::Status::Downloaded;
0147     default:
0148         return Enclosure::Status::Error;
0149     }
0150 }
0151 
0152 void Enclosure::download()
0153 {
0154     if (m_status == Downloaded) {
0155         return;
0156     }
0157 
0158     if (!NetworkConnectionManager::instance().episodeDownloadsAllowed()) {
0159         Q_EMIT downloadError(Error::Type::MeteredNotAllowed,
0160                              m_entry->feed()->url(),
0161                              m_entry->id(),
0162                              0,
0163                              i18n("Podcast downloads not allowed due to user setting"),
0164                              QString());
0165         return;
0166     }
0167 
0168     checkSizeOnDisk();
0169     EnclosureDownloadJob *downloadJob = new EnclosureDownloadJob(m_url, path(), m_entry->title());
0170     downloadJob->start();
0171 
0172     qint64 resumedAt = m_sizeOnDisk;
0173     m_downloadProgress = 0;
0174     Q_EMIT downloadProgressChanged();
0175 
0176     m_entry->feed()->setErrorId(0);
0177     m_entry->feed()->setErrorString(QString());
0178 
0179     connect(downloadJob, &KJob::result, this, [this, downloadJob]() {
0180         checkSizeOnDisk();
0181         if (downloadJob->error() == 0) {
0182             processDownloadedFile();
0183         } else {
0184             QFile file(path());
0185             if (file.exists() && file.size() > 0) {
0186                 setStatus(PartiallyDownloaded);
0187             } else {
0188                 setStatus(Downloadable);
0189             }
0190             if (downloadJob->error() != QNetworkReply::OperationCanceledError) {
0191                 m_entry->feed()->setErrorId(downloadJob->error());
0192                 m_entry->feed()->setErrorString(downloadJob->errorString());
0193                 Q_EMIT downloadError(Error::Type::MediaDownload,
0194                                      m_entry->feed()->url(),
0195                                      m_entry->id(),
0196                                      downloadJob->error(),
0197                                      downloadJob->errorString(),
0198                                      QString());
0199             }
0200         }
0201         disconnect(this, &Enclosure::cancelDownload, this, nullptr);
0202         Q_EMIT statusChanged(m_entry, m_status);
0203     });
0204 
0205     connect(this, &Enclosure::cancelDownload, this, [this, downloadJob]() {
0206         downloadJob->doKill();
0207         checkSizeOnDisk();
0208         QFile file(path());
0209         if (file.exists() && file.size() > 0) {
0210             setStatus(PartiallyDownloaded);
0211         } else {
0212             setStatus(Downloadable);
0213         }
0214         disconnect(this, &Enclosure::cancelDownload, this, nullptr);
0215     });
0216 
0217     connect(downloadJob, &KJob::processedAmountChanged, this, [=](KJob *kjob, KJob::Unit unit, qulonglong amount) {
0218         Q_ASSERT(unit == KJob::Unit::Bytes);
0219 
0220         qint64 totalSize = static_cast<qint64>(kjob->totalAmount(unit));
0221         qint64 currentSize = static_cast<qint64>(amount);
0222 
0223         if ((totalSize > 0) && (m_size != totalSize + resumedAt)) {
0224             qCDebug(kastsEnclosure) << "Correct filesize for enclosure" << m_entry->title() << "from" << m_size << "to" << totalSize + resumedAt;
0225             setSize(totalSize + resumedAt);
0226         }
0227 
0228         m_downloadSize = currentSize + resumedAt;
0229         m_downloadProgress = static_cast<double>(m_downloadSize) / static_cast<double>(m_size);
0230         Q_EMIT downloadProgressChanged();
0231 
0232         qCDebug(kastsEnclosure) << "m_downloadSize" << m_downloadSize;
0233         qCDebug(kastsEnclosure) << "m_downloadProgress" << m_downloadProgress;
0234         qCDebug(kastsEnclosure) << "m_size" << m_size;
0235     });
0236 
0237     setStatus(Downloading);
0238 }
0239 
0240 void Enclosure::processDownloadedFile()
0241 {
0242     // This will be run if the enclosure has been downloaded successfully
0243 
0244     // First check if file size is larger than 0; otherwise something unexpected
0245     // must have happened
0246     checkSizeOnDisk();
0247     if (m_sizeOnDisk == 0) {
0248         deleteFile();
0249         return;
0250     }
0251 
0252     // Check if reported filesize in rss feed corresponds to real file size
0253     // if not, correct the filesize in the database
0254     // otherwise the file will get deleted because of mismatch in signature
0255     if (m_sizeOnDisk != size()) {
0256         qCDebug(kastsEnclosure) << "Correcting enclosure file size mismatch" << m_entry->title() << "from" << size() << "to" << m_sizeOnDisk;
0257         setSize(m_sizeOnDisk);
0258         setStatus(Downloaded);
0259     }
0260 
0261     // Unset "new" status of item
0262     if (m_entry->getNew()) {
0263         m_entry->setNew(false);
0264     }
0265 
0266     // Trigger update of image since the downloaded file can have an embedded image
0267     Q_EMIT m_entry->imageChanged(m_entry->image());
0268     Q_EMIT m_entry->cachedImageChanged(m_entry->cachedImage());
0269 }
0270 
0271 void Enclosure::deleteFile()
0272 {
0273     qCDebug(kastsEnclosure) << "Trying to delete enclosure file" << path();
0274     if (AudioManager::instance().entry() && (m_entry == AudioManager::instance().entry())) {
0275         qCDebug(kastsEnclosure) << "Track is still playing; let's unload it before deleting";
0276         AudioManager::instance().setEntry(nullptr);
0277     }
0278 
0279     // First check if file still exists; you never know what has happened
0280     if (QFile(path()).exists()) {
0281         QFile(path()).remove();
0282     }
0283 
0284     // If file disappeared unexpectedly, then still change status to downloadable
0285     setStatus(Downloadable);
0286     m_sizeOnDisk = 0;
0287     Q_EMIT sizeOnDiskChanged();
0288 }
0289 
0290 QString Enclosure::url() const
0291 {
0292     return m_url;
0293 }
0294 
0295 QString Enclosure::path() const
0296 {
0297     return StorageManager::instance().enclosurePath(m_entry->title(), m_url, m_entry->feed()->dirname());
0298 }
0299 
0300 Enclosure::Status Enclosure::status() const
0301 {
0302     return m_status;
0303 }
0304 
0305 QString Enclosure::cachedEmbeddedImage() const
0306 {
0307     // if image is already cached, then return the path
0308     QString cachedpath = StorageManager::instance().imagePath(m_url);
0309     if (QFileInfo::exists(cachedpath)) {
0310         if (QFileInfo(cachedpath).size() != 0) {
0311             return QUrl::fromLocalFile(cachedpath).toString();
0312         }
0313     }
0314 
0315     if (m_status != Downloaded || path().isEmpty()) {
0316         return QStringLiteral("");
0317     }
0318 
0319     const auto mime = QMimeDatabase().mimeTypeForFile(path()).name();
0320     if (mime != QStringLiteral("audio/mpeg")) {
0321         return QStringLiteral("");
0322     }
0323 
0324     TagLib::MPEG::File f(path().toLatin1().data());
0325     if (!f.hasID3v2Tag()) {
0326         return QStringLiteral("");
0327     }
0328 
0329     bool imageFound = false;
0330     for (const auto &frame : f.ID3v2Tag()->frameListMap()["APIC"]) {
0331         auto pictureFrame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(frame);
0332         QByteArray data(pictureFrame->picture().data(), pictureFrame->picture().size());
0333         if (!data.isEmpty()) {
0334             QFile file(cachedpath);
0335             file.open(QIODevice::WriteOnly);
0336             file.write(data);
0337             file.close();
0338             imageFound = true;
0339         }
0340     }
0341 
0342     if (imageFound) {
0343         return cachedpath;
0344     } else {
0345         return QStringLiteral("");
0346     }
0347 }
0348 
0349 qint64 Enclosure::playPosition() const
0350 {
0351     return m_playposition;
0352 }
0353 
0354 qint64 Enclosure::duration() const
0355 {
0356     return m_duration;
0357 }
0358 
0359 qint64 Enclosure::size() const
0360 {
0361     return m_size;
0362 }
0363 
0364 qint64 Enclosure::sizeOnDisk() const
0365 {
0366     return m_sizeOnDisk;
0367 }
0368 
0369 void Enclosure::setStatus(Enclosure::Status status)
0370 {
0371     if (m_status != status) {
0372         m_status = status;
0373 
0374         QSqlQuery query;
0375         query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id AND feed=:feed;"));
0376         query.bindValue(QStringLiteral(":id"), m_entry->id());
0377         query.bindValue(QStringLiteral(":feed"), m_entry->feed()->url());
0378         query.bindValue(QStringLiteral(":downloaded"), statusToDb(m_status));
0379         Database::instance().execute(query);
0380 
0381         Q_EMIT statusChanged(m_entry, m_status);
0382     }
0383 }
0384 
0385 void Enclosure::setPlayPosition(const qint64 &position)
0386 {
0387     if (m_playposition != position) {
0388         m_playposition = position;
0389         qCDebug(kastsEnclosure) << "save playPosition" << position << m_entry->title();
0390 
0391         // let's only save the play position to the database every 15 seconds
0392         if ((abs(m_playposition - m_playposition_dbsave) > 15000) || position == 0) {
0393             qCDebug(kastsEnclosure) << "save playPosition to database" << position << m_entry->title();
0394             QSqlQuery query;
0395             query.prepare(QStringLiteral("UPDATE Enclosures SET playposition=:playposition WHERE id=:id AND feed=:feed"));
0396             query.bindValue(QStringLiteral(":id"), m_entry->id());
0397             query.bindValue(QStringLiteral(":feed"), m_entry->feed()->url());
0398             query.bindValue(QStringLiteral(":playposition"), m_playposition);
0399             Database::instance().execute(query);
0400             m_playposition_dbsave = m_playposition;
0401 
0402             // Also store position change to make sure that it can be synced to
0403             // e.g. gpodder
0404             Sync::instance().storePlayEpisodeAction(m_entry->id(), m_playposition_dbsave, m_playposition);
0405         }
0406 
0407         Q_EMIT playPositionChanged();
0408     }
0409 }
0410 
0411 void Enclosure::setDuration(const qint64 &duration)
0412 {
0413     if (m_duration != duration) {
0414         m_duration = duration;
0415 
0416         // also save to database
0417         qCDebug(kastsEnclosure) << "updating entry duration" << duration << m_entry->title();
0418         QSqlQuery query;
0419         query.prepare(QStringLiteral("UPDATE Enclosures SET duration=:duration WHERE id=:id AND feed=:feed"));
0420         query.bindValue(QStringLiteral(":id"), m_entry->id());
0421         query.bindValue(QStringLiteral(":feed"), m_entry->feed()->url());
0422         query.bindValue(QStringLiteral(":duration"), m_duration);
0423         Database::instance().execute(query);
0424 
0425         Q_EMIT durationChanged();
0426     }
0427 }
0428 
0429 void Enclosure::setSize(const qint64 &size)
0430 {
0431     if (m_size != size) {
0432         m_size = size;
0433 
0434         // also save to database
0435         QSqlQuery query;
0436         query.prepare(QStringLiteral("UPDATE Enclosures SET size=:size WHERE id=:id AND feed=:feed"));
0437         query.bindValue(QStringLiteral(":id"), m_entry->id());
0438         query.bindValue(QStringLiteral(":feed"), m_entry->feed()->url());
0439         query.bindValue(QStringLiteral(":size"), m_size);
0440         Database::instance().execute(query);
0441 
0442         Q_EMIT sizeChanged();
0443     }
0444 }
0445 
0446 void Enclosure::checkSizeOnDisk()
0447 {
0448     // In principle the database contains this status, we check anyway in case
0449     // something changed on disk
0450     QFile file(path());
0451     if (file.exists()) {
0452         if (file.size() == m_size && file.size() > 0) {
0453             // file is on disk and has correct size, write to database if it
0454             // wasn't already registered so
0455             // this should, in principle, never happen unless the db was deleted
0456             setStatus(Downloaded);
0457         } else if (file.size() > 0) {
0458             // file was downloaded, but there is a size mismatch
0459             // set to PartiallyDownloaded such that download can be resumed
0460             setStatus(PartiallyDownloaded);
0461         } else {
0462             // file is empty
0463             setStatus(Downloadable);
0464         }
0465         if (file.size() != m_sizeOnDisk) {
0466             m_sizeOnDisk = file.size();
0467             m_downloadSize = m_sizeOnDisk;
0468             m_downloadProgress = (m_size == 0) ? 0.0 : static_cast<double>(m_sizeOnDisk) / static_cast<double>(m_size);
0469             Q_EMIT sizeOnDiskChanged();
0470         }
0471     } else {
0472         // file does not exist
0473         setStatus(Downloadable);
0474         if (m_sizeOnDisk != 0) {
0475             m_sizeOnDisk = 0;
0476             m_downloadSize = 0;
0477             m_downloadProgress = 0.0;
0478             Q_EMIT sizeOnDiskChanged();
0479         }
0480     }
0481 }
0482 
0483 QString Enclosure::formattedSize() const
0484 {
0485     return m_kformat.formatByteSize(m_size);
0486 }
0487 
0488 QString Enclosure::formattedDownloadSize() const
0489 {
0490     return m_kformat.formatByteSize(m_downloadSize);
0491 }
0492 
0493 QString Enclosure::formattedDuration() const
0494 {
0495     return m_kformat.formatDuration(m_duration * 1000);
0496 }
0497 
0498 QString Enclosure::formattedLeftDuration() const
0499 {
0500     qreal rate = 1.0;
0501     if (SettingsManager::self()->adjustTimeLeft()) {
0502         rate = AudioManager::instance().playbackRate();
0503         rate = (rate > 0.0) ? rate : 1.0;
0504     }
0505     qint64 diff = duration() * 1000 - playPosition();
0506     return m_kformat.formatDuration(diff / rate);
0507 }
0508 
0509 QString Enclosure::formattedPlayPosition() const
0510 {
0511     return m_kformat.formatDuration(m_playposition);
0512 }