File indexing completed on 2025-02-02 05:18:03

0001 /*
0002  *  SPDX-FileCopyrightText: 2015 Lars Pontoppidan <dev.larpon@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "mediaframe.h"
0008 
0009 #include <QCryptographicHash>
0010 #include <QDebug>
0011 #include <QDir>
0012 #include <QDirIterator>
0013 #include <QFileInfo>
0014 #include <QImageReader>
0015 #include <QMimeDatabase>
0016 #include <QRandomGenerator>
0017 #include <QRegularExpression>
0018 #include <QTime>
0019 #include <QUrl>
0020 
0021 #include <KIO/StoredTransferJob>
0022 
0023 MediaFrame::MediaFrame(QObject *parent)
0024     : QObject(parent)
0025 {
0026     const auto imageMimeTypeNames = QImageReader::supportedMimeTypes();
0027     QMimeDatabase mimeDb;
0028     for (const auto &imageMimeTypeName : imageMimeTypeNames) {
0029         const auto mimeType = mimeDb.mimeTypeForName(QLatin1String(imageMimeTypeName));
0030         m_filters << mimeType.globPatterns();
0031     }
0032     qDebug() << "Added" << m_filters.count() << "filters";
0033     // qDebug() << m_filters;
0034     m_next = 0;
0035 
0036     connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &MediaFrame::slotItemChanged);
0037     connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &MediaFrame::slotItemChanged);
0038 }
0039 
0040 MediaFrame::~MediaFrame() = default;
0041 
0042 int MediaFrame::count() const
0043 {
0044     return m_allFiles.count();
0045 }
0046 
0047 bool MediaFrame::random() const
0048 {
0049     return m_random;
0050 }
0051 
0052 void MediaFrame::setRandom(bool random)
0053 {
0054     if (random != m_random) {
0055         m_random = random;
0056         Q_EMIT randomChanged();
0057     }
0058 }
0059 
0060 int MediaFrame::random(int min, int max)
0061 {
0062     if (min > max) {
0063         int temp = min;
0064         min = max;
0065         max = temp;
0066     }
0067 
0068     // qDebug() << "random" << min << "<->" << max << "=" << ((qrand()%(max-min+1))+min);
0069     return (QRandomGenerator::global()->bounded((max - min + 1)) + min);
0070 }
0071 
0072 QString MediaFrame::getCacheDirectory()
0073 {
0074     return QDir::temp().absolutePath();
0075 }
0076 
0077 QString MediaFrame::hash(const QString &str)
0078 {
0079     return QString::fromLatin1(QCryptographicHash::hash(str.toUtf8(), QCryptographicHash::Md5).toHex());
0080 }
0081 
0082 bool MediaFrame::isDir(const QString &path)
0083 {
0084     return QDir(path).exists();
0085 }
0086 
0087 bool MediaFrame::isDirEmpty(const QString &path)
0088 {
0089     return (isDir(path) && QDir(path).entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty());
0090 }
0091 
0092 bool MediaFrame::isFile(const QString &path)
0093 {
0094     // Check if the file exists and is not a directory
0095     return (QFileInfo::exists(path) && QFileInfo(path).isFile());
0096 }
0097 
0098 void MediaFrame::add(const QString &path)
0099 {
0100     add(path, AddOption::NON_RECURSIVE);
0101 }
0102 
0103 void MediaFrame::add(const QString &path, AddOption option)
0104 {
0105     if (isAdded(path)) {
0106         qWarning() << "Path" << path << "already added";
0107         return;
0108     }
0109 
0110     QUrl url = QUrl(path);
0111     QString localPath = url.toString(QUrl::PreferLocalFile);
0112     // qDebug() << "Local path" << localPath << "Path" << path;
0113 
0114     QStringList paths;
0115     QString filePath;
0116 
0117     if (isDir(localPath)) {
0118         if (!isDirEmpty(localPath)) {
0119             QDirIterator dirIterator(
0120                 localPath,
0121                 m_filters,
0122                 QDir::Files,
0123                 (option == AddOption::RECURSIVE ? QDirIterator::Subdirectories | QDirIterator::FollowSymlinks : QDirIterator::NoIteratorFlags));
0124 
0125             while (dirIterator.hasNext()) {
0126                 dirIterator.next();
0127 
0128                 filePath = "file://" + dirIterator.filePath();
0129                 paths.append(filePath);
0130                 m_allFiles.append(filePath);
0131                 // qDebug() << "Appended" << filePath;
0132                 Q_EMIT countChanged();
0133             }
0134             if (paths.count() > 0) {
0135                 m_pathMap.insert(path, paths);
0136                 qDebug() << "Added" << paths.count() << "files from" << path;
0137             } else {
0138                 qWarning() << "No images found in directory" << path;
0139             }
0140         } else {
0141             qWarning() << "Not adding empty directory" << path;
0142         }
0143 
0144         // the pictures have to be sorted before adding them to the list,
0145         // because the QDirIterator sorts them in a different way than QDir::entryList
0146         // paths.sort();
0147 
0148     } else if (isFile(localPath)) {
0149         paths.append(path);
0150         m_pathMap.insert(path, paths);
0151         m_allFiles.append(path);
0152         qDebug() << "Added" << paths.count() << "files from" << path;
0153         Q_EMIT countChanged();
0154     } else {
0155         if (url.isValid() && !url.isLocalFile()) {
0156             qDebug() << "Adding" << url.toString() << "as remote file";
0157             paths.append(path);
0158             m_pathMap.insert(path, paths);
0159             m_allFiles.append(path);
0160             Q_EMIT countChanged();
0161         } else {
0162             qWarning() << "Path" << path << "is not a valid file url or directory";
0163         }
0164     }
0165 }
0166 
0167 void MediaFrame::clear()
0168 {
0169     m_pathMap.clear();
0170     m_allFiles.clear();
0171     Q_EMIT countChanged();
0172 }
0173 
0174 void MediaFrame::watch(const QString &path)
0175 {
0176     QUrl url = QUrl(path);
0177     QString localPath = url.toString(QUrl::PreferLocalFile);
0178     if (isFile(localPath)) {
0179         if (!m_watchFile.isEmpty()) {
0180             // qDebug() << "Removing" << m_watchFile << "from watch list";
0181             m_watcher.removePath(m_watchFile);
0182         } else {
0183             qDebug() << "Nothing in watch list";
0184         }
0185 
0186         // qDebug() << "watching" << localPath << "for changes";
0187         m_watcher.addPath(localPath);
0188         m_watchFile = localPath;
0189     } else {
0190         qWarning() << "Can't watch remote file" << path << "for changes";
0191     }
0192 }
0193 
0194 bool MediaFrame::isAdded(const QString &path)
0195 {
0196     return (m_pathMap.contains(path));
0197 }
0198 
0199 void MediaFrame::get(QJSValue successCallback)
0200 {
0201     get(successCallback, QJSValue::UndefinedValue);
0202 }
0203 
0204 void MediaFrame::get(QJSValue successCallback, QJSValue errorCallback)
0205 {
0206     int size = m_allFiles.count() - 1;
0207 
0208     QString path;
0209     QString errorMessage;
0210     QJSValueList args;
0211 
0212     if (size < 1) {
0213         if (size == 0) {
0214             path = m_allFiles.at(0);
0215 
0216             if (successCallback.isCallable()) {
0217                 args << QJSValue(path);
0218                 successCallback.call(args);
0219             }
0220             return;
0221         } else {
0222             errorMessage = QStringLiteral("No files available");
0223             qWarning() << errorMessage;
0224 
0225             args << QJSValue(errorMessage);
0226             errorCallback.call(args);
0227             return;
0228         }
0229     }
0230 
0231     if (m_random) {
0232         path = m_allFiles.at(this->random(0, size));
0233     } else {
0234         path = m_allFiles.at(m_next);
0235         m_next++;
0236         if (m_next > size) {
0237             qDebug() << "Resetting next count from" << m_next << "due to queue size" << size;
0238             m_next = 0;
0239         }
0240     }
0241 
0242     QUrl url = QUrl(path);
0243 
0244     if (url.isValid()) {
0245         QString localPath = url.toString(QUrl::PreferLocalFile);
0246 
0247         if (!isFile(localPath)) {
0248             m_filename = path.section(QLatin1Char('/'), -1);
0249 
0250             QString cachedFile = getCacheDirectory() + QLatin1Char('/') + hash(path) + QLatin1Char('_') + m_filename;
0251 
0252             if (isFile(cachedFile)) {
0253                 // File has been cached
0254                 qDebug() << path << "is cached as" << cachedFile;
0255 
0256                 if (successCallback.isCallable()) {
0257                     args << QJSValue(cachedFile);
0258                     successCallback.call(args);
0259                 }
0260                 return;
0261             }
0262 
0263             m_successCallback = successCallback;
0264             m_errorCallback = errorCallback;
0265             m_filename = cachedFile;
0266 
0267             qDebug() << path << "doesn't exist locally, trying remote.";
0268 
0269             KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo);
0270             connect(job, &KJob::finished, this, &MediaFrame::slotFinished);
0271 
0272         } else {
0273             if (successCallback.isCallable()) {
0274                 args << QJSValue(path);
0275                 successCallback.call(args);
0276             }
0277             return;
0278         }
0279     } else {
0280         errorMessage = path + QLatin1String(" is not a valid URL");
0281         qCritical() << errorMessage;
0282 
0283         if (errorCallback.isCallable()) {
0284             args << QJSValue(errorMessage);
0285             errorCallback.call(args);
0286         }
0287         return;
0288     }
0289 }
0290 
0291 void MediaFrame::pushHistory(const QString &string)
0292 {
0293     const int oldCount = m_history.count();
0294 
0295     m_history.prepend(string);
0296 
0297     // Keep a sane history size
0298     if (m_history.length() > 50) {
0299         m_history.removeLast();
0300     }
0301 
0302     if (oldCount != m_history.count()) {
0303         Q_EMIT historyLengthChanged();
0304     }
0305 }
0306 
0307 QString MediaFrame::popHistory()
0308 {
0309     if (m_history.isEmpty()) {
0310         return QString();
0311     }
0312 
0313     const QString item = m_history.takeFirst();
0314     Q_EMIT historyLengthChanged();
0315     return item;
0316 }
0317 
0318 int MediaFrame::historyLength() const
0319 {
0320     return m_history.length();
0321 }
0322 
0323 void MediaFrame::pushFuture(const QString &string)
0324 {
0325     m_future.prepend(string);
0326     Q_EMIT futureLengthChanged();
0327 }
0328 
0329 QString MediaFrame::popFuture()
0330 {
0331     if (m_future.isEmpty()) {
0332         return QString();
0333     }
0334 
0335     const QString item = m_future.takeFirst();
0336     Q_EMIT futureLengthChanged();
0337     return item;
0338 }
0339 
0340 int MediaFrame::futureLength() const
0341 {
0342     return m_future.length();
0343 }
0344 
0345 void MediaFrame::slotItemChanged(const QString &path)
0346 {
0347     Q_EMIT itemChanged(path);
0348 }
0349 
0350 void MediaFrame::slotFinished(KJob *job)
0351 {
0352     QString errorMessage;
0353     QJSValueList args;
0354 
0355     if (job->error()) {
0356         errorMessage = QLatin1String("Error loading image: ") + job->errorString();
0357         qCritical() << errorMessage;
0358 
0359         if (m_errorCallback.isCallable()) {
0360             args << QJSValue(errorMessage);
0361             m_errorCallback.call(args);
0362         }
0363     } else if (KIO::StoredTransferJob *transferJob = qobject_cast<KIO::StoredTransferJob *>(job)) {
0364         QImage image;
0365 
0366         // TODO make proper caching calls
0367         QString path = m_filename;
0368         qDebug() << "Saving download to" << path;
0369 
0370         image.loadFromData(transferJob->data());
0371         image.save(path);
0372 
0373         qDebug() << "Saved to" << path;
0374 
0375         if (m_successCallback.isCallable()) {
0376             args << QJSValue(path);
0377             m_successCallback.call(args);
0378         }
0379     } else {
0380         errorMessage = QStringLiteral("Unknown error occurred");
0381 
0382         qCritical() << errorMessage;
0383 
0384         if (m_errorCallback.isCallable()) {
0385             args << QJSValue(errorMessage);
0386             m_errorCallback.call(args);
0387         }
0388     }
0389 }