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 }