From f0ab7efc6eb7a2d1dfda7742aa236b6e2987c11c Mon Sep 17 00:00:00 2001 From: uncor3 Date: Sun, 9 Nov 2025 20:27:22 +0000 Subject: [PATCH] use low level apis to generate video thumbnail & remove unnecessary code - Adjusted status bar layout in MainWindow to include app version label. - Enhanced video thumbnail generation in PhotoModel using FFmpeg for better performance and resource management. - Streamlined MediaStreamerManager to ensure proper cleanup and thread safety. - Updated ServiceManager to include safe methods for retrieving file info and handling AFC operations. - Removed get_device_version - Cleaned up code and improved readability across multiple files. - Passed correct args to ZUpdater --- CMakeLists.txt | 22 +- .../helpers/read_afc_file_to_byte_array.cpp | 35 +- src/core/services/init_device.cpp | 2 +- src/core/services/mount_dev_image.cpp | 8 +- src/core/services/set_location.cpp | 2 +- src/devdiskimagehelper.cpp | 4 +- src/devdiskimageswidget.cpp | 2 +- src/devdiskmanager.cpp | 4 +- src/deviceimagewidget.cpp | 2 +- src/gallerywidget.cpp | 12 +- src/gallerywidget.h | 3 +- src/iDescriptor.h | 58 +- src/ifusewidget.cpp | 2 +- src/livescreenwidget.cpp | 34 +- src/mainwindow.cpp | 33 +- src/mediapreviewdialog.cpp | 2 +- src/mediastreamermanager.cpp | 55 +- src/mediastreamermanager.h | 7 +- src/photomodel.cpp | 573 ++++++++++++------ src/photomodel.h | 22 +- src/servicemanager.cpp | 25 + src/servicemanager.h | 10 + src/virtuallocationwidget.cpp | 2 +- 23 files changed, 553 insertions(+), 366 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b35c96..b327a71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,9 @@ cmake_minimum_required(VERSION 3.16) -project(iDescriptor VERSION 0.1 LANGUAGES CXX) +project(iDescriptor VERSION 0.1.0 LANGUAGES CXX) # Feature options option(ENABLE_RECOVERY_DEVICE_SUPPORT "Enable recovery device support (requires libirecovery)" ON) +option(PACKAGE_MANAGER_MANAGED "Build as package manager managed version (auto updates will be handled by the package manager)" OFF) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) @@ -117,6 +118,12 @@ pkg_check_modules(QRENCODE REQUIRED IMPORTED_TARGET libqrencode) pkg_check_modules(HEIF REQUIRED IMPORTED_TARGET libheif) pkg_check_modules(ZIP REQUIRED IMPORTED_TARGET libzip) +# Add FFmpeg libraries for video thumbnail generation +pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat) +pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec) +pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil) +pkg_check_modules(SWSCALE REQUIRED IMPORTED_TARGET libswscale) + if(ENABLE_RECOVERY_DEVICE_SUPPORT) find_library(IRECOVERY_LIBRARY NAMES irecovery-1.0 @@ -264,6 +271,10 @@ target_link_libraries(iDescriptor PRIVATE qtermwidget6 PkgConfig::HEIF PkgConfig::ZIP + PkgConfig::AVFORMAT + PkgConfig::AVCODEC + PkgConfig::AVUTIL + PkgConfig::SWSCALE airplay ipatool-go ZUpdater @@ -316,6 +327,15 @@ if(ENABLE_RECOVERY_DEVICE_SUPPORT) target_compile_definitions(iDescriptor PRIVATE ENABLE_RECOVERY_DEVICE_SUPPORT) endif() +if (PACKAGE_MANAGER_MANAGED) + target_compile_definitions(iDescriptor PRIVATE PACKAGE_MANAGER_MANAGED) + message(STATUS "Building as package manager managed version, updates will be handled by the package manager") +endif() + +target_compile_definitions(iDescriptor PRIVATE + APP_VERSION="${PROJECT_VERSION}" +) + set_target_properties(iDescriptor PROPERTIES ${BUNDLE_ID_OPTION} MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} diff --git a/src/core/helpers/read_afc_file_to_byte_array.cpp b/src/core/helpers/read_afc_file_to_byte_array.cpp index 88c3152..3ab98d4 100644 --- a/src/core/helpers/read_afc_file_to_byte_array.cpp +++ b/src/core/helpers/read_afc_file_to_byte_array.cpp @@ -54,18 +54,39 @@ QByteArray read_afc_file_to_byte_array(afc_client_t afcClient, const char *path) QByteArray buffer; buffer.resize(fileSize); - uint32_t bytesRead = 0; - afc_error_t read_err = afc_file_read(afcClient, fd_handle, buffer.data(), - buffer.size(), &bytesRead); + uint64_t totalBytesRead = 0; + const uint32_t CHUNK_SIZE = 1024 * 1024; // Read in 1MB chunks + char *p = buffer.data(); + + while (totalBytesRead < fileSize) { + uint32_t bytesToRead = + std::min((uint64_t)CHUNK_SIZE, fileSize - totalBytesRead); + uint32_t bytesReadThisChunk = 0; + afc_error_t read_err = + afc_file_read(afcClient, fd_handle, p + totalBytesRead, bytesToRead, + &bytesReadThisChunk); + + if (read_err != AFC_E_SUCCESS) { + qDebug() << "AFC Error: Read failed for file" << path + << "Error:" << read_err; + afc_file_close(afcClient, fd_handle); + return QByteArray(); + } + + if (bytesReadThisChunk == 0) { + // Premature end of file + break; + } + totalBytesRead += bytesReadThisChunk; + } afc_file_close(afcClient, fd_handle); - if (read_err != AFC_E_SUCCESS || bytesRead != fileSize) { + if (totalBytesRead != fileSize) { qDebug() << "AFC Error: Read mismatch for file" << path - << "Error:" << read_err << "Read:" << bytesRead - << "Expected:" << fileSize; + << "Read:" << totalBytesRead << "Expected:" << fileSize; return QByteArray(); // Read failed } return buffer; -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/core/services/init_device.cpp b/src/core/services/init_device.cpp index 7bdc40f..0062a21 100644 --- a/src/core/services/init_device.cpp +++ b/src/core/services/init_device.cpp @@ -202,7 +202,7 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc, int minor = (parts.length() > 1) ? parts[1].toInt() : 0; int patch = (parts.length() > 2) ? parts[2].toInt() : 0; - d.parsedDeviceVersion = IDESCRIPTOR_DEVICE_VERSION(major, minor, patch); + d.parsedDeviceVersion = IDEVICE_DEVICE_VERSION(major, minor, patch); /*DiskInfo*/ try { diff --git a/src/core/services/mount_dev_image.cpp b/src/core/services/mount_dev_image.cpp index 0443e23..5b9a9b3 100644 --- a/src/core/services/mount_dev_image.cpp +++ b/src/core/services/mount_dev_image.cpp @@ -97,7 +97,7 @@ mobile_image_mounter_error_t mount_dev_image(idevice_t device, goto leave; } - if (device_version >= IDESCRIPTOR_DEVICE_VERSION(7, 0, 0)) { + if (device_version >= IDEVICE_DEVICE_VERSION(7, 0, 0)) { disk_image_upload_type = DISK_IMAGE_UPLOAD_TYPE_UPLOAD_IMAGE; } @@ -139,7 +139,7 @@ mobile_image_mounter_error_t mount_dev_image(idevice_t device, qDebug() << "Using image:" << image_path; qDebug() << "Using signature:" << image_sig_path; - if (device_version >= IDESCRIPTOR_DEVICE_VERSION(16, 0, 0)) { + if (device_version >= IDEVICE_DEVICE_VERSION(16, 0, 0)) { uint8_t dev_mode_status = 0; plist_t val = NULL; ldret = lockdownd_get_value(lckd, "com.apple.security.mac.amfi", @@ -182,14 +182,14 @@ mobile_image_mounter_error_t mount_dev_image(idevice_t device, goto leave; } image_size = fst.st_size; - if (device_version < IDESCRIPTOR_DEVICE_VERSION(17, 0, 0) && + if (device_version < IDEVICE_DEVICE_VERSION(17, 0, 0) && stat(image_sig_path, &fst) != 0) { qDebug() << "ERROR: stat:" << image_sig_path << ":" << strerror(errno); res = -1; goto leave; } - if (device_version < IDESCRIPTOR_DEVICE_VERSION(17, 0, 0)) { + if (device_version < IDEVICE_DEVICE_VERSION(17, 0, 0)) { f = fopen(image_sig_path, "rb"); if (!f) { qDebug() << "Error opening signature file" << image_sig_path << ":" diff --git a/src/core/services/set_location.cpp b/src/core/services/set_location.cpp index e905eea..ae70f80 100644 --- a/src/core/services/set_location.cpp +++ b/src/core/services/set_location.cpp @@ -71,7 +71,7 @@ bool set_location(idevice_t device, char *lat, char *lon) lerr = lockdownd_start_service(lockdown, DT_SIMULATELOCATION_SERVICE, &svc); if (lerr != LOCKDOWN_E_SUCCESS) { - unsigned int device_version = get_device_version(device); + unsigned int device_version = idevice_get_device_version(device); lockdownd_client_free(lockdown); idevice_free(device); diff --git a/src/devdiskimagehelper.cpp b/src/devdiskimagehelper.cpp index 2c69902..26ad7c2 100644 --- a/src/devdiskimagehelper.cpp +++ b/src/devdiskimagehelper.cpp @@ -92,7 +92,7 @@ void DevDiskImageHelper::start() m_loadingIndicator->start(); showStatus("Please wait..."); - unsigned int device_version = get_device_version(m_device->device); + unsigned int device_version = idevice_get_device_version(m_device->device); unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF; @@ -137,7 +137,7 @@ void DevDiskImageHelper::onMountButtonClicked() m_isMounting = true; // Check if we need to download first - unsigned int device_version = get_device_version(m_device->device); + unsigned int device_version = idevice_get_device_version(m_device->device); unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF; diff --git a/src/devdiskimageswidget.cpp b/src/devdiskimageswidget.cpp index 721dcb4..6363480 100644 --- a/src/devdiskimageswidget.cpp +++ b/src/devdiskimageswidget.cpp @@ -186,7 +186,7 @@ void DevDiskImagesWidget::displayImages() // todo wtf is this if (m_currentDevice && m_currentDevice->device) { unsigned int device_version = - get_device_version(m_currentDevice->device); + idevice_get_device_version(m_currentDevice->device); deviceMajorVersion = (device_version >> 16) & 0xFF; deviceMinorVersion = (device_version >> 8) & 0xFF; hasConnectedDevice = true; diff --git a/src/devdiskmanager.cpp b/src/devdiskmanager.cpp index 515c805..c0367ab 100644 --- a/src/devdiskmanager.cpp +++ b/src/devdiskmanager.cpp @@ -338,7 +338,7 @@ bool DevDiskManager::isImageDownloaded(const QString &version, bool DevDiskManager::downloadCompatibleImage(iDescriptorDevice *device) { - unsigned int device_version = get_device_version(device->device); + unsigned int device_version = idevice_get_device_version(device->device); unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF; qDebug() << "Device version:" << deviceMajorVersion << "." @@ -400,7 +400,7 @@ bool DevDiskManager::downloadCompatibleImage(iDescriptorDevice *device) // FIXME:DOES NOT CHECK IF THERE IS ALREADY AN IMAGE MOUNTED bool DevDiskManager::mountCompatibleImage(iDescriptorDevice *device) { - unsigned int device_version = get_device_version(device->device); + unsigned int device_version = idevice_get_device_version(device->device); unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF; diff --git a/src/deviceimagewidget.cpp b/src/deviceimagewidget.cpp index dc31224..0743b0e 100644 --- a/src/deviceimagewidget.cpp +++ b/src/deviceimagewidget.cpp @@ -161,7 +161,7 @@ QString DeviceImageWidget::getMockupNameFromDisplayName( int DeviceImageWidget::getIosVersionFromDevice() const { - unsigned int version = get_device_version(m_device->device); + unsigned int version = idevice_get_device_version(m_device->device); if (version > 0) { int majorVersion = (version >> 16) & 0xFF; diff --git a/src/gallerywidget.cpp b/src/gallerywidget.cpp index 9db47e3..20a8248 100644 --- a/src/gallerywidget.cpp +++ b/src/gallerywidget.cpp @@ -182,14 +182,18 @@ void GalleryWidget::onSortOrderChanged() : "Oldest First"); } +PhotoModel::FilterType GalleryWidget::getCurrentFilterType() const +{ + int filterValue = m_filterComboBox->currentData().toInt(); + return static_cast(filterValue); +} + void GalleryWidget::onFilterChanged() { if (!m_model) return; - int filterValue = m_filterComboBox->currentData().toInt(); - PhotoModel::FilterType filter = - static_cast(filterValue); + PhotoModel::FilterType filter = getCurrentFilterType(); m_model->setFilterType(filter); QString filterName = m_filterComboBox->currentText(); @@ -447,7 +451,7 @@ void GalleryWidget::onAlbumSelected(const QString &albumPath) // Create model if not exists if (!m_model) { - m_model = new PhotoModel(m_device, this); + m_model = new PhotoModel(m_device, getCurrentFilterType(), this); m_listView->setModel(m_model); // Update export button states based on selection diff --git a/src/gallerywidget.h b/src/gallerywidget.h index 78623f3..4dae062 100644 --- a/src/gallerywidget.h +++ b/src/gallerywidget.h @@ -21,6 +21,7 @@ #define GALLERYWIDGET_H #include "iDescriptor.h" +#include "photomodel.h" #include QT_BEGIN_NAMESPACE @@ -34,7 +35,6 @@ class QLabel; class QStandardItem; QT_END_NAMESPACE -class PhotoModel; class ExportManager; class ExportProgressDialog; @@ -67,6 +67,7 @@ private: QIcon loadAlbumThumbnail(const QString &albumPath); void loadAlbumThumbnailAsync(const QString &albumPath, QStandardItem *item); void onPhotoContextMenu(const QPoint &pos); + PhotoModel::FilterType getCurrentFilterType() const; iDescriptorDevice *m_device; bool m_loaded = false; diff --git a/src/iDescriptor.h b/src/iDescriptor.h index 58b238c..c3ec18c 100644 --- a/src/iDescriptor.h +++ b/src/iDescriptor.h @@ -37,7 +37,6 @@ #define TOOL_NAME "iDescriptor" #define APP_LABEL "iDescriptor" -#define APP_VERSION "0.1.0" #define APP_COPYRIGHT "© 2025 Uncore. All rights reserved." #define AFC2_SERVICE_NAME "com.apple.afc2" #define RECOVERY_CLIENT_CONNECTION_TRIES 3 @@ -84,6 +83,7 @@ struct DiskInfo { uint64_t totalDataAvailable; }; +// Carefull not all the vars are initialized in init_device.cpp struct DeviceInfo { enum class ActivationState { Activated, @@ -147,7 +147,6 @@ struct DeviceInfo { std::string mobileSubscriberCountryCode; std::string mobileSubscriberNetworkCode; std::string modelNumber; - // NonVolatileRAM omitted (unknown type) std::string ioNVRAMSyncNowProperty; bool systemAudioVolumeSaved; bool autoBoot; @@ -425,57 +424,4 @@ QByteArray read_afc_file_to_byte_array(afc_client_t afcClient, bool isDarkMode(); instproxy_error_t install_IPA(idevice_t device, afc_client_t afc, - const char *filePath); - -#define IDESCRIPTOR_DEVICE_VERSION(maj, min, patch) \ - ((((maj) & 0xFF) << 16) | (((min) & 0xFF) << 8) | ((patch) & 0xFF)) - -/* - we need this because idevice_get_device_version - is not always available in libimobiledevice - which could cause issues when installed from package managers -*/ -inline unsigned int get_device_version(idevice_t _device) -{ - if (!_device) { - return 0; - } - - lockdownd_client_t lockdown = NULL; - if (lockdownd_client_new_with_handshake( - _device, &lockdown, "iDescriptor") != LOCKDOWN_E_SUCCESS) { - return 0; - } - - plist_t node = NULL; - if (lockdownd_get_value(lockdown, NULL, "ProductVersion", &node) != - LOCKDOWN_E_SUCCESS) { - lockdownd_client_free(lockdown); - return 0; - } - - unsigned int version_number = 0; - if (node && plist_get_node_type(node) == PLIST_STRING) { - char *version_string = NULL; - plist_get_string_val(node, &version_string); - if (version_string) { - QString q_version = QString(version_string); - QStringList parts = q_version.split('.'); - - int major = (parts.length() > 0) ? parts[0].toInt() : 0; - int minor = (parts.length() > 1) ? parts[1].toInt() : 0; - int patch = (parts.length() > 2) ? parts[2].toInt() : 0; - - version_number = IDESCRIPTOR_DEVICE_VERSION(major, minor, patch); - - free(version_string); - } - } - - if (node) { - plist_free(node); - } - lockdownd_client_free(lockdown); - - return version_number; -} \ No newline at end of file + const char *filePath); \ No newline at end of file diff --git a/src/ifusewidget.cpp b/src/ifusewidget.cpp index 944d5d2..f8e906c 100644 --- a/src/ifusewidget.cpp +++ b/src/ifusewidget.cpp @@ -352,7 +352,6 @@ void iFuseWidget::onProcessFinished(int exitCode, "Device mounted successfully at: " + m_currentMountPath, false); auto *b = new iFuseDiskUnmountButton(m_currentMountPath); - MainWindow::sharedInstance()->statusBar()->addPermanentWidget(b); QProcess *processToKill = m_ifuseProcess; QString currentMountPath = m_currentMountPath; connect(b, &iFuseDiskUnmountButton::clicked, this, @@ -369,6 +368,7 @@ void iFuseWidget::onProcessFinished(int exitCode, MainWindow::sharedInstance()->statusBar()->removeWidget(b); b->deleteLater(); }); + MainWindow::sharedInstance()->statusBar()->addPermanentWidget(b); QDesktopServices::openUrl(QUrl::fromLocalFile(currentMountPath)); } else { QString errorOutput = m_ifuseProcess->readAllStandardError(); diff --git a/src/livescreenwidget.cpp b/src/livescreenwidget.cpp index e9bcc0b..1ea3e60 100644 --- a/src/livescreenwidget.cpp +++ b/src/livescreenwidget.cpp @@ -31,14 +31,14 @@ #include #include #include - +// todo add a retry button when failed LiveScreenWidget::LiveScreenWidget(iDescriptorDevice *device, QWidget *parent) : QWidget{parent}, m_device(device), m_timer(nullptr), m_shotrClient(nullptr), m_fps(20) { setWindowTitle("Live Screen - iDescriptor"); - unsigned int device_version = get_device_version(m_device->device); + unsigned int device_version = idevice_get_device_version(m_device->device); unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; if (deviceMajorVersion > 16) { @@ -91,23 +91,21 @@ LiveScreenWidget::LiveScreenWidget(iDescriptorDevice *device, QWidget *parent) // Start the initialization process - auto-mount mode auto *helper = new DevDiskImageHelper(m_device, this); - connect( - helper, &DevDiskImageHelper::mountingCompleted, this, - [this, helper](bool success) { - helper->deleteLater(); + connect(helper, &DevDiskImageHelper::mountingCompleted, this, + [this, helper](bool success) { + helper->deleteLater(); - if (success) { - // for some reason it does not work immediately, so delay a bit - QTimer::singleShot(1000, this, [this]() { - initializeScreenshotService(true); - }); - } else { - m_statusLabel->setText("Failed to mount developer disk image"); - QMessageBox::critical(this, "Mount Failed", - "Could not mount developer disk image.\n" - "Screenshot feature is not available."); - } - }); + if (success) { + // for some reason it does not work immediately, so delay a + // bit + QTimer::singleShot(1000, this, [this]() { + initializeScreenshotService(true); + }); + } else { + m_statusLabel->setText( + "Failed to mount developer disk image"); + } + }); helper->start(); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index a22a03e..4f8cfc4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -193,9 +193,14 @@ MainWindow::MainWindow(QWidget *parent) ui->statusbar->addWidget(m_connectedDeviceCountLabel); ui->statusbar->setContentsMargins(0, 0, 0, 0); - ui->statusbar->addPermanentWidget(settingsButton); ui->statusbar->addPermanentWidget(githubButton); + ui->statusbar->addPermanentWidget(settingsButton); + QLabel *appVersionLabel = new QLabel(QString("v%1").arg(APP_VERSION)); + appVersionLabel->setContentsMargins(5, 0, 5, 0); + appVersionLabel->setStyleSheet( + "QLabel:hover { background-color : #13131319; }"); + ui->statusbar->addPermanentWidget(appVersionLabel); #ifdef __linux__ QList mounted_iFusePaths = iFuseManager::getMountPoints(); @@ -239,6 +244,17 @@ MainWindow::MainWindow(QWidget *parent) // Example usage with customization UpdateProcedure updateProcedure; + bool packageManagerManaged = false; + bool isPortable = false; + +#ifdef WIN32 + // dynamic portable detection read .portable file in app dir on Windows + QString appDir = QApplication::applicationDirPath(); + QFile portableFile(appDir + "/.portable"); + if (portableFile.exists()) { + isPortable = true; + } +#endif switch (ZUpdater::detectPlatform()) { // todo: adjust for portable @@ -261,8 +277,11 @@ MainWindow::MainWindow(QWidget *parent) "Do you want to install the downloaded update now?", }; break; - // todo: adjust for pkg managers case Platform::Linux: + // currently only on linux (arch aur) is enabled +#ifdef PACKAGE_MANAGER_MANAGED + packageManagerManaged = true; +#endif updateProcedure = UpdateProcedure{ false, true, @@ -277,13 +296,9 @@ MainWindow::MainWindow(QWidget *parent) }; } - m_updater = new ZUpdater("uncor3/libtest", APP_VERSION, "iDescriptor", - updateProcedure, - false, // isPortable - set to true if running - // portable version on Windows - false, // isPackageManaged - set to true if - // installed via package manager on Linux - this); + m_updater = + new ZUpdater("uncor3/libtest", APP_VERSION, "iDescriptor", + updateProcedure, isPortable, packageManagerManaged, this); qDebug() << "Checking for updates..."; SettingsManager::sharedInstance()->doIfEnabled( SettingsManager::Setting::AutoCheckUpdates, diff --git a/src/mediapreviewdialog.cpp b/src/mediapreviewdialog.cpp index 925ac32..6954b18 100644 --- a/src/mediapreviewdialog.cpp +++ b/src/mediapreviewdialog.cpp @@ -203,7 +203,7 @@ void MediaPreviewDialog::loadMedia() void MediaPreviewDialog::loadImage() { auto future = QtConcurrent::run( - [this]() { return PhotoModel::loadImage(m_device, m_filePath, ""); }); + [this]() { return PhotoModel::loadImage(m_device, m_filePath); }); auto *watcher = new QFutureWatcher(this); connect(watcher, &QFutureWatcher::finished, this, diff --git a/src/mediastreamermanager.cpp b/src/mediastreamermanager.cpp index 124624a..bf371cc 100644 --- a/src/mediastreamermanager.cpp +++ b/src/mediastreamermanager.cpp @@ -34,6 +34,7 @@ QUrl MediaStreamerManager::getStreamUrl(iDescriptorDevice *device, afc_client_t afcClient, const QString &filePath) { + QMutexLocker locker(&m_streamersMutex); // Check if we already have a streamer for this file auto it = m_streamers.find(filePath); @@ -55,12 +56,12 @@ QUrl MediaStreamerManager::getStreamUrl(iDescriptorDevice *device, } } - // Create new streamer - auto *streamer = new MediaStreamer(device, afcClient, filePath, this); + // Create new streamer without a QObject parent + auto *streamer = new MediaStreamer(device, afcClient, filePath, nullptr); if (!streamer->isListening()) { qWarning() << "MediaStreamerManager: Failed to create streamer for" << filePath; - streamer->deleteLater(); + delete streamer; return QUrl(); } @@ -71,10 +72,6 @@ QUrl MediaStreamerManager::getStreamUrl(iDescriptorDevice *device, info.refCount = 1; m_streamers[filePath] = info; - // Connect to destruction signal for cleanup - connect(streamer, &QObject::destroyed, this, - &MediaStreamerManager::onStreamerDestroyed); - qDebug() << "MediaStreamerManager: Created new streamer for" << filePath << "at" << streamer->getUrl().toString(); @@ -83,52 +80,34 @@ QUrl MediaStreamerManager::getStreamUrl(iDescriptorDevice *device, void MediaStreamerManager::releaseStreamer(const QString &filePath) { - + QMutexLocker locker(&m_streamersMutex); auto it = m_streamers.find(filePath); if (it != m_streamers.end()) { it->refCount--; qDebug() << "MediaStreamerManager: Released streamer for" << filePath << "refCount:" << it->refCount; - // If no more references, mark for cleanup but don't delete immediately - // This allows for quick reuse if the same file is opened again soon + // If no more references, delete it immediately. + // deleteLater() will not work in a thread without an event loop. if (it->refCount <= 0) { - qDebug() << "MediaStreamerManager: Streamer for" << filePath - << "ready for cleanup"; + qDebug() << "MediaStreamerManager: Deleting streamer for" + << filePath; + delete it->streamer; + m_streamers.erase(it); } } } void MediaStreamerManager::cleanup() { - + QMutexLocker locker(&m_streamersMutex); auto it = m_streamers.begin(); while (it != m_streamers.end()) { - if (it->refCount <= 0) { - qDebug() << "MediaStreamerManager: Cleaning up streamer for" - << it.key(); - if (it->streamer) { - it->streamer->deleteLater(); - } - it = m_streamers.erase(it); - } else { - ++it; - } - } -} - -void MediaStreamerManager::onStreamerDestroyed() -{ - // Find and remove the destroyed streamer - auto it = m_streamers.begin(); - while (it != m_streamers.end()) { - if (it->streamer == sender()) { - qDebug() << "MediaStreamerManager: Streamer destroyed for" - << it.key(); - it = m_streamers.erase(it); - break; - } else { - ++it; + qDebug() << "MediaStreamerManager: Cleaning up streamer for" + << it.key(); + if (it->streamer) { + delete it->streamer; } + it = m_streamers.erase(it); } } \ No newline at end of file diff --git a/src/mediastreamermanager.h b/src/mediastreamermanager.h index 9326c31..587f710 100644 --- a/src/mediastreamermanager.h +++ b/src/mediastreamermanager.h @@ -35,10 +35,8 @@ * streamers for the same file. It automatically cleans up unused streamers * and provides thread-safe access. */ -class MediaStreamerManager : public QObject +class MediaStreamerManager { - Q_OBJECT - public: /** * @brief Get the singleton instance @@ -69,9 +67,6 @@ public: private: ~MediaStreamerManager(); -private slots: - void onStreamerDestroyed(); - private: struct StreamerInfo { MediaStreamer *streamer; diff --git a/src/photomodel.cpp b/src/photomodel.cpp index 608c7c7..1b00498 100644 --- a/src/photomodel.cpp +++ b/src/photomodel.cpp @@ -25,32 +25,36 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include -#include +extern "C" { +#include +#include +#include +#include +} -PhotoModel::PhotoModel(iDescriptorDevice *device, QObject *parent) - : QAbstractListModel(parent), m_device(device), m_thumbnailSize(256, 256), - m_sortOrder(NewestFirst), m_filterType(All) +// Limit concurrent video thumbnail generation to 2 to prevent resource +// exhaustion +QSemaphore PhotoModel::m_videoThumbnailSemaphore(4); + +PhotoModel::PhotoModel(iDescriptorDevice *device, FilterType filterType, + QObject *parent) + : QAbstractListModel(parent), m_device(device), m_thumbnailSize(64, 64), + m_sortOrder(NewestFirst), m_filterType(filterType) { - // Set up cache directory for persistent storage - m_cacheDir = - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + - "/photo_thumbs"; - QDir().mkpath(m_cacheDir); - - // Configure memory cache (150MB limit) - m_thumbnailCache.setMaxCost(150 * 1024 * 1024); + // 350 MB cache for thumbnails + m_thumbnailCache.setMaxCost(350 * 1024 * 1024); connect(this, &PhotoModel::thumbnailNeedsToBeLoaded, this, &PhotoModel::requestThumbnail, Qt::QueuedConnection); - - // Don't populate paths in constructor - wait for setAlbumPath } PhotoModel::~PhotoModel() @@ -67,59 +71,306 @@ PhotoModel::~PhotoModel() m_activeLoaders.clear(); m_loadingPaths.clear(); m_thumbnailCache.clear(); - QDir(m_cacheDir).removeRecursively(); } -QPixmap PhotoModel::generateVideoThumbnail(iDescriptorDevice *device, - const QString &filePath, - const QSize &requestedSize) +QPixmap PhotoModel::generateVideoThumbnailFFmpeg(iDescriptorDevice *device, + const QString &filePath, + const QSize &requestedSize) { QPixmap thumbnail; - QEventLoop loop; - // Use a timer to handle potential timeouts - QTimer::singleShot(5000, &loop, &QEventLoop::quit); + uint64_t fileHandle = 0; - auto player = std::make_unique(); - auto sink = std::make_unique(); - player->setVideoSink(sink.get()); + afc_error_t openResult = ServiceManager::safeAfcFileOpen( + device, filePath.toUtf8().constData(), AFC_FOPEN_RDONLY, &fileHandle); - // This lambda will be called when a frame is ready - QObject::connect(sink.get(), &QVideoSink::videoFrameChanged, - [&](const QVideoFrame &frame) { - if (frame.isValid()) { - QImage img = frame.toImage(); - if (!img.isNull()) { - thumbnail = QPixmap::fromImage(img.scaled( - requestedSize, Qt::KeepAspectRatio, - Qt::SmoothTransformation)); - } - } - // We got our frame, so we can stop the loop - if (loop.isRunning()) { - loop.quit(); - } - }); - - // Get the streaming URL and start playback - QUrl streamUrl = MediaStreamerManager::sharedInstance()->getStreamUrl( - device, device->afcClient, filePath); - if (streamUrl.isEmpty()) { - qWarning() << "Could not get stream URL for video thumbnail:" - << filePath; + if (openResult != AFC_E_SUCCESS || fileHandle == 0) { + qWarning() << "Failed to open video file for thumbnail:" << filePath; return {}; } - player->setSource(streamUrl); - player->setPosition(1000); // Seek 1 second in to get a good frame - player->play(); // Start playback to trigger frame capture + // Get file size + char **fileInfo = nullptr; + afc_error_t infoResult = ServiceManager::safeAfcGetFileInfo( + device, filePath.toUtf8().constData(), &fileInfo); - // Wait for the videoFrameChanged signal or timeout - loop.exec(); + uint64_t fileSize = 0; + if (infoResult == AFC_E_SUCCESS && fileInfo) { + for (int i = 0; fileInfo[i]; i += 2) { + if (strcmp(fileInfo[i], "st_size") == 0) { + fileSize = strtoull(fileInfo[i + 1], nullptr, 10); + break; + } + } + afc_dictionary_free(fileInfo); + } + + if (fileSize == 0) { + ServiceManager::safeAfcFileClose(device, fileHandle); + qWarning() << "Invalid video file size for thumbnail:" << filePath; + return {}; + } + + // Create custom AVIOContext for reading from device on-demand + AVFormatContext *formatCtx = avformat_alloc_context(); + if (!formatCtx) { + ServiceManager::safeAfcFileClose(device, fileHandle); + qWarning() << "Failed to allocate format context"; + return {}; + } + + // Context for streaming read from device + struct StreamContext { + iDescriptorDevice *device; + uint64_t fileHandle; + uint64_t fileSize; + uint64_t currentPos; + }; + + StreamContext *streamCtx = + new StreamContext{device, fileHandle, fileSize, 0}; + + // Custom read function that reads from device on-demand + auto readPacket = [](void *opaque, uint8_t *buf, int bufSize) -> int { + StreamContext *ctx = static_cast(opaque); + + if (ctx->currentPos >= ctx->fileSize) { + return AVERROR_EOF; + } + + uint32_t toRead = + std::min(static_cast(bufSize), + static_cast(ctx->fileSize - ctx->currentPos)); + uint32_t bytesRead = 0; + + afc_error_t result = ServiceManager::safeAfcFileRead( + ctx->device, ctx->fileHandle, reinterpret_cast(buf), toRead, + &bytesRead); + + if (result != AFC_E_SUCCESS || bytesRead == 0) { + return AVERROR(EIO); + } + + ctx->currentPos += bytesRead; + return static_cast(bytesRead); + }; + + // Custom seek function using AFC seek + auto seekPacket = [](void *opaque, int64_t offset, int whence) -> int64_t { + StreamContext *ctx = static_cast(opaque); + + if (whence == AVSEEK_SIZE) { + return static_cast(ctx->fileSize); + } + + int64_t newPos = 0; + int seekWhence = SEEK_SET; + + if (whence == SEEK_SET) { + newPos = offset; + seekWhence = SEEK_SET; + } else if (whence == SEEK_CUR) { + newPos = static_cast(ctx->currentPos) + offset; + seekWhence = SEEK_SET; + } else if (whence == SEEK_END) { + newPos = static_cast(ctx->fileSize) + offset; + seekWhence = SEEK_SET; + } else { + return -1; + } + + if (newPos < 0 || newPos > static_cast(ctx->fileSize)) { + return -1; + } + + // Use AFC seek + afc_error_t result = ServiceManager::safeAfcFileSeek( + ctx->device, ctx->fileHandle, newPos, seekWhence); + + if (result != AFC_E_SUCCESS) { + return -1; + } + + ctx->currentPos = static_cast(newPos); + return newPos; + }; + + const int avioBufferSize = 32768; // 32KB buffer for streaming + unsigned char *avioBuffer = + static_cast(av_malloc(avioBufferSize)); + if (!avioBuffer) { + delete streamCtx; + ServiceManager::safeAfcFileClose(device, fileHandle); + avformat_free_context(formatCtx); + return {}; + } + + AVIOContext *avioCtx = + avio_alloc_context(avioBuffer, avioBufferSize, 0, streamCtx, readPacket, + nullptr, seekPacket); + + if (!avioCtx) { + av_free(avioBuffer); + delete streamCtx; + ServiceManager::safeAfcFileClose(device, fileHandle); + avformat_free_context(formatCtx); + return {}; + } + + formatCtx->pb = avioCtx; + formatCtx->flags |= AVFMT_FLAG_CUSTOM_IO; + + // Open input + if (avformat_open_input(&formatCtx, nullptr, nullptr, nullptr) < 0) { + qWarning() << "Failed to open video format"; + av_free(avioCtx->buffer); + avio_context_free(&avioCtx); + avformat_free_context(formatCtx); + return {}; + } + + // Find stream info + if (avformat_find_stream_info(formatCtx, nullptr) < 0) { + qWarning() << "Failed to find stream info"; + avformat_close_input(&formatCtx); + av_free(avioCtx->buffer); + avio_context_free(&avioCtx); + return {}; + } + + // Find video stream + int videoStreamIndex = -1; + const AVCodec *codec = nullptr; + AVCodecParameters *codecParams = nullptr; + + for (unsigned int i = 0; i < formatCtx->nb_streams; i++) { + if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + videoStreamIndex = i; + codecParams = formatCtx->streams[i]->codecpar; + codec = avcodec_find_decoder(codecParams->codec_id); + break; + } + } + + if (videoStreamIndex == -1 || !codec) { + qWarning() << "No video stream found"; + avformat_close_input(&formatCtx); + av_free(avioCtx->buffer); + avio_context_free(&avioCtx); + return {}; + } + + // Allocate codec context + AVCodecContext *codecCtx = avcodec_alloc_context3(codec); + if (!codecCtx) { + avformat_close_input(&formatCtx); + av_free(avioCtx->buffer); + avio_context_free(&avioCtx); + return {}; + } + + if (avcodec_parameters_to_context(codecCtx, codecParams) < 0) { + avcodec_free_context(&codecCtx); + avformat_close_input(&formatCtx); + av_free(avioCtx->buffer); + avio_context_free(&avioCtx); + return {}; + } + + // Open codec + if (avcodec_open2(codecCtx, codec, nullptr) < 0) { + avcodec_free_context(&codecCtx); + avformat_close_input(&formatCtx); + av_free(avioCtx->buffer); + avio_context_free(&avioCtx); + return {}; + } + + // Allocate frame + AVFrame *frame = av_frame_alloc(); + AVPacket *packet = av_packet_alloc(); + + if (!frame || !packet) { + if (frame) + av_frame_free(&frame); + if (packet) + av_packet_free(&packet); + avcodec_free_context(&codecCtx); + avformat_close_input(&formatCtx); + av_free(avioCtx->buffer); + avio_context_free(&avioCtx); + return {}; + } + + // Read frames until we get a valid one + bool frameDecoded = false; + while (av_read_frame(formatCtx, packet) >= 0) { + if (packet->stream_index == videoStreamIndex) { + if (avcodec_send_packet(codecCtx, packet) >= 0) { + if (avcodec_receive_frame(codecCtx, frame) >= 0) { + frameDecoded = true; + av_packet_unref(packet); + break; + } + } + } + av_packet_unref(packet); + } + + if (frameDecoded) { + // Convert frame to RGB24 + SwsContext *swsCtx = + sws_getContext(frame->width, frame->height, + static_cast(frame->format), + frame->width, frame->height, AV_PIX_FMT_RGB24, + SWS_BILINEAR, nullptr, nullptr, nullptr); + + if (swsCtx) { + AVFrame *rgbFrame = av_frame_alloc(); + if (rgbFrame) { + rgbFrame->format = AV_PIX_FMT_RGB24; + rgbFrame->width = frame->width; + rgbFrame->height = frame->height; + + if (av_frame_get_buffer(rgbFrame, 0) >= 0) { + sws_scale(swsCtx, frame->data, frame->linesize, 0, + frame->height, rgbFrame->data, + rgbFrame->linesize); + + // Convert to QImage + QImage img(rgbFrame->data[0], rgbFrame->width, + rgbFrame->height, rgbFrame->linesize[0], + QImage::Format_RGB888); + + // Create a deep copy since AVFrame will be freed + QImage imgCopy = img.copy(); + + // Scale to requested size + thumbnail = QPixmap::fromImage( + imgCopy.scaled(requestedSize, Qt::KeepAspectRatio, + Qt::SmoothTransformation)); + } + + av_frame_free(&rgbFrame); + } + + sws_freeContext(swsCtx); + } + } // Cleanup - player->stop(); - MediaStreamerManager::sharedInstance()->releaseStreamer(filePath); + av_frame_free(&frame); + av_packet_free(&packet); + avcodec_free_context(&codecCtx); + avformat_close_input(&formatCtx); + + // Close the AFC file handle + ServiceManager::safeAfcFileClose(device, fileHandle); + + // Free AVIO context and stream context + av_free(avioCtx->buffer); + avio_context_free(&avioCtx); + delete streamCtx; return thumbnail; } @@ -147,27 +398,24 @@ QVariant PhotoModel::data(const QModelIndex &index, int role) const case Qt::DecorationRole: { qDebug() << "DecorationRole requested for index:" << index.row(); - QString cacheKey = getThumbnailCacheKey(info.filePath); - - // Check memory cache first (works for both images AND videos) - if (QPixmap *cached = m_thumbnailCache.object(cacheKey)) { + // Check memory cache first + if (QPixmap *cached = m_thumbnailCache.object(info.filePath)) { qDebug() << "Cache HIT for:" << info.fileName; return QIcon(*cached); } - // Prevent duplicate requests - this is CRITICAL for both images and - // videos - if (m_activeLoaders.contains(cacheKey) || - m_loadingPaths.contains(info.filePath)) { + // Prevent duplicate requests + if (m_loadingPaths.contains(info.filePath) || + m_activeLoaders.contains(info.filePath)) { qDebug() << "Already loading:" << info.fileName; // Return appropriate placeholder based on file type if (info.fileName.endsWith(".MOV", Qt::CaseInsensitive) || info.fileName.endsWith(".MP4", Qt::CaseInsensitive) || info.fileName.endsWith(".M4V", Qt::CaseInsensitive)) { - // return QIcon::fromTheme("video-x-generic"); return QIcon(":/resources/icons/video-x-generic.png"); } else { - return QIcon::fromTheme("image-x-generic"); + return QIcon(":/resources/icons/" + "MaterialSymbolsLightImageOutlineSharp.png"); } } @@ -185,7 +433,8 @@ QVariant PhotoModel::data(const QModelIndex &index, int role) const // return QIcon::fromTheme("video-x-generic"); return QIcon(":/resources/icons/video-x-generic.png"); } else { - return QIcon::fromTheme("image-x-generic"); + return QIcon( + ":/resources/icons/MaterialSymbolsLightImageOutlineSharp.png"); } } @@ -222,23 +471,6 @@ void PhotoModel::clearCache() } } -QString PhotoModel::getThumbnailCacheKey(const QString &filePath) const -{ - // Create unique key based on file path and thumbnail size - QString key = QString("%1_%2x%3") - .arg(filePath) - .arg(m_thumbnailSize.width()) - .arg(m_thumbnailSize.height()); - return QString( - QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Md5) - .toHex()); -} - -QString PhotoModel::getThumbnailCachePath(const QString &filePath) const -{ - return m_cacheDir + "/" + getThumbnailCacheKey(filePath) + ".jpg"; -} - void PhotoModel::requestThumbnail(int index) { if (index < 0 || index >= m_photos.size()) @@ -247,37 +479,26 @@ void PhotoModel::requestThumbnail(int index) PhotoInfo &info = m_photos[index]; info.thumbnailRequested = true; - QString cacheKey = getThumbnailCacheKey(info.filePath); - - if (m_activeLoaders.contains(cacheKey) || - m_loadingPaths.contains(info.filePath)) + if (m_loadingPaths.contains(info.filePath)) return; m_loadingPaths.insert(info.filePath); auto *watcher = new QFutureWatcher(); - m_activeLoaders[cacheKey] = watcher; + m_activeLoaders[info.filePath] = watcher; - // Connect the finished signal to handle both images and videos connect(watcher, &QFutureWatcher::finished, this, - [this, watcher, cacheKey, filePath = info.filePath]() { + [this, watcher, filePath = info.filePath]() { + qDebug() << "Thumbnail load finished for:" << filePath; QPixmap thumbnail = watcher->result(); - // Remove from loading sets m_loadingPaths.remove(filePath); - m_activeLoaders.remove(cacheKey); - // scale down and store in cache + m_activeLoaders.remove(filePath); if (!thumbnail.isNull()) { - // Cache the thumbnail (both memory and disk) int cost = thumbnail.width() * thumbnail.height() * 4; - m_thumbnailCache.insert( - cacheKey, - new QPixmap(thumbnail.scaled(m_thumbnailSize, - Qt::KeepAspectRatio, - Qt::SmoothTransformation)), - cost); + m_thumbnailCache.insert(filePath, new QPixmap(thumbnail), + cost); - // Find the model index and emit dataChanged for (int i = 0; i < m_photos.size(); ++i) { if (m_photos[i].filePath == filePath) { QModelIndex idx = createIndex(i, 0); @@ -290,53 +511,34 @@ void PhotoModel::requestThumbnail(int index) << QFileInfo(filePath).fileName(); } - // Clean up the watcher watcher->deleteLater(); }); - // Determine if this is a video or image and load accordingly bool isVideo = info.fileName.endsWith(".MOV", Qt::CaseInsensitive) || info.fileName.endsWith(".MP4", Qt::CaseInsensitive) || info.fileName.endsWith(".M4V", Qt::CaseInsensitive); - QString cachePath = getThumbnailCachePath(info.filePath); - QFuture future; if (isVideo) { - // Load video thumbnail asynchronously - // todo: implement - future = QtConcurrent::run([this]() { - // Check disk cache first - // if (QFile::exists(cachePath)) { - // QPixmap cached(cachePath); - // if (!cached.isNull() && cached.size() == m_thumbnailSize) { - // qDebug() << "Video disk cache HIT for:" - // << QFileInfo(info.filePath).fileName(); - // return cached; - // } - // } + future = QtConcurrent::run([this, info]() { + // Acquire semaphore FIRST to limit concurrent video processing + qDebug() << "Waiting for semaphore for:" << info.fileName; + m_videoThumbnailSemaphore.acquire(); + qDebug() << "Acquired semaphore for:" << info.fileName; - // // Generate video thumbnail - // QPixmap thumbnail = generateVideoThumbnail(m_device, - // info.filePath, - // m_thumbnailSize); + // Generate video thumbnail using FFmpeg directly (no QMediaPlayer) + QPixmap thumbnail = generateVideoThumbnailFFmpeg( + m_device, info.filePath, m_thumbnailSize); - // // Save to disk cache if successful - // if (!thumbnail.isNull()) { - // QDir().mkpath(QFileInfo(cachePath).absolutePath()); - // if (thumbnail.save(cachePath, "JPG", 85)) { - // qDebug() << "Saved video thumbnail to disk cache:" - // << QFileInfo(info.filePath).fileName(); - // } - // } - return QPixmap(); // Placeholder until implemented - // return thumbnail; + // Release semaphore + qDebug() << "Releasing semaphore for:" << info.fileName; + m_videoThumbnailSemaphore.release(); + return thumbnail; }); } else { - // Load image thumbnail asynchronously (existing logic) - future = QtConcurrent::run([info, cachePath, this]() { + future = QtConcurrent::run([info, this]() { return loadThumbnailFromDevice(m_device, info.filePath, - m_thumbnailSize, cachePath); + m_thumbnailSize); }); } @@ -346,66 +548,56 @@ void PhotoModel::requestThumbnail(int index) // Static function that runs in worker thread QPixmap PhotoModel::loadThumbnailFromDevice(iDescriptorDevice *device, const QString &filePath, - const QSize &size, - const QString &cachePath) + const QSize &size) { - // Check disk cache first - if (QFile::exists(cachePath)) { - QPixmap cached(cachePath); - if (!cached.isNull() && cached.size() == size) { - qDebug() << "Disk cache HIT for:" << QFileInfo(filePath).fileName(); - return cached; - } - } - // Load from device using ServiceManager QByteArray imageData = ServiceManager::safeReadAfcFileToByteArray( device, filePath.toUtf8().constData()); if (imageData.isEmpty()) { qDebug() << "Could not read from device:" << filePath; - return QPixmap(); // Return empty pixmap on error + return {}; // Return empty pixmap on error } - if (filePath.endsWith(".HEIC")) { + if (filePath.endsWith(".HEIC", Qt::CaseInsensitive)) { qDebug() << "Loading HEIC image from data for:" << filePath; QPixmap img = load_heic(imageData); - return img.isNull() ? QPixmap() : img; + return img.isNull() ? QPixmap() + : img.scaled(size, Qt::KeepAspectRatio, + Qt::SmoothTransformation); } - // Load pixmap from data + // Use QImageReader for efficient, low-memory scaled loading + QBuffer buffer(&imageData); + buffer.open(QIODevice::ReadOnly); + + QImageReader reader(&buffer); + if (reader.canRead()) { + // This is the key optimization: it decodes a smaller image directly, + // saving a massive amount of memory. + reader.setScaledSize(size); + QImage image = reader.read(); + if (!image.isNull()) { + return QPixmap::fromImage(image); + } + qDebug() << "QImageReader failed to decode" << filePath + << "Error:" << reader.errorString(); + } + + // Fallback for formats QImageReader might struggle with QPixmap original; - if (!original.loadFromData(imageData)) { - qDebug() << "Could not decode image data for:" << filePath; - return QPixmap(); + if (original.loadFromData(imageData)) { + return original.scaled(size, Qt::KeepAspectRatio, + Qt::SmoothTransformation); } - // Scale to thumbnail size - QPixmap thumbnail = - original.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); - - // Save to disk cache - QDir().mkpath(QFileInfo(cachePath).absolutePath()); - if (thumbnail.save(cachePath, "JPG", 85)) { - qDebug() << "Saved to disk cache:" << QFileInfo(filePath).fileName(); - } - - return thumbnail; + qDebug() << "Could not decode image data for:" << filePath; + return {}; } QPixmap PhotoModel::loadImage(iDescriptorDevice *device, - const QString &filePath, const QString &cachePath) + const QString &filePath) { - // Check disk cache first - if (QFile::exists(cachePath)) { - QPixmap cached(cachePath); - if (!cached.isNull()) { - qDebug() << "Disk cache HIT for:" << QFileInfo(filePath).fileName(); - return cached; - } - } - - // Load from device using ServiceManager QByteArray imageData = ServiceManager::safeReadAfcFileToByteArray( device, filePath.toUtf8().constData()); @@ -420,20 +612,12 @@ QPixmap PhotoModel::loadImage(iDescriptorDevice *device, return img.isNull() ? QPixmap() : img; } - // TODO - // Load pixmap from data QPixmap original; if (!original.loadFromData(imageData)) { qDebug() << "Could not decode image data for:" << filePath; return QPixmap(); } - // Save to disk cache - QDir().mkpath(QFileInfo(cachePath).absolutePath()); - if (original.save(cachePath, "JPG", 85)) { - qDebug() << "Saved to disk cache:" << QFileInfo(filePath).fileName(); - } - return original; } @@ -446,25 +630,17 @@ void PhotoModel::populatePhotoPaths() return; } - beginResetModel(); m_allPhotos.clear(); - m_photos.clear(); - // // Your existing logic to populate photo paths - // char **files = nullptr; - // qDebug() << "Populating photos from album path:" << m_albumPath; - - // First verify the album path exists QByteArray albumPathBytes = m_albumPath.toUtf8(); const char *albumPathCStr = albumPathBytes.constData(); char **albumInfo = nullptr; afc_error_t infoResult = - afc_get_file_info(m_device->afcClient, albumPathCStr, &albumInfo); + ServiceManager::safeAfcGetFileInfo(m_device, albumPathCStr, &albumInfo); if (infoResult != AFC_E_SUCCESS) { qDebug() << "Album path does not exist or cannot be accessed:" << m_albumPath << "Error:" << infoResult; - endResetModel(); return; } if (albumInfo) { @@ -484,7 +660,6 @@ void PhotoModel::populatePhotoPaths() if (readResult != AFC_E_SUCCESS) { qDebug() << "Failed to read photo directory:" << photoDir << "Error:" << readResult; - endResetModel(); return; } @@ -511,11 +686,9 @@ void PhotoModel::populatePhotoPaths() afc_dictionary_free(files); } - // Apply initial filtering and sorting + // Apply initial filtering and sorting, which will also reset the model applyFilterAndSort(); - endResetModel(); - qDebug() << "Loaded" << m_allPhotos.size() << "media files from device"; qDebug() << "After filtering:" << m_photos.size() << "items shown"; } @@ -541,11 +714,16 @@ void PhotoModel::applyFilterAndSort() { beginResetModel(); + // int i = 0; // Filter photos m_photos.clear(); for (const PhotoInfo &info : m_allPhotos) { if (matchesFilter(info)) { m_photos.append(info); + // if (i == 3) { + // break; + // } + // i++; } } @@ -634,14 +812,11 @@ QStringList PhotoModel::getFilteredFilePaths() const // Helper methods QDateTime PhotoModel::extractDateTimeFromFile(const QString &filePath) const { - // Use AFC to get actual file creation time from device plist_t info = nullptr; - afc_error_t afc_err = afc_get_file_info_plist( - // TODO:AFC CLIENT IS NOT LONG LIVED - m_device->afcClient, filePath.toUtf8().constData(), &info); + afc_error_t afc_err = ServiceManager::safeAfcGetFileInfoPlist( + m_device, filePath.toUtf8().constData(), &info); if (afc_err == AFC_E_SUCCESS && info) { - // Try to get st_birthtime (creation time) first plist_t birthtime_node = plist_dict_get_item(info, "st_birthtime"); if (birthtime_node && plist_get_node_type(birthtime_node) == PLIST_UINT) { diff --git a/src/photomodel.h b/src/photomodel.h index 2d1e195..c590972 100644 --- a/src/photomodel.h +++ b/src/photomodel.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -49,7 +50,8 @@ public: enum FilterType { All, ImagesOnly, VideosOnly }; - explicit PhotoModel(iDescriptorDevice *device, QObject *parent = nullptr); + explicit PhotoModel(iDescriptorDevice *device, FilterType filterType, + QObject *parent = nullptr); ~PhotoModel(); // QAbstractItemModel interface @@ -81,13 +83,12 @@ public: QStringList getAllFilePaths() const; QStringList getFilteredFilePaths() const; - static QPixmap loadImage(iDescriptorDevice *device, const QString &filePath, - const QString &cachePath); + static QPixmap loadImage(iDescriptorDevice *device, + const QString &filePath); // Static helper methods static QPixmap loadThumbnailFromDevice(iDescriptorDevice *device, const QString &filePath, - const QSize &size, - const QString &cachePath); + const QSize &size); signals: void thumbnailNeedsToBeLoaded(int index); void exportRequested(const QStringList &filePaths); @@ -105,7 +106,6 @@ private: // Thumbnail management QSize m_thumbnailSize; mutable QCache m_thumbnailCache; - QString m_cacheDir; mutable QHash *> m_activeLoaders; mutable QSet m_loadingPaths; @@ -119,15 +119,13 @@ private: void sortPhotos(QList &photos) const; bool matchesFilter(const PhotoInfo &info) const; - QString getThumbnailCacheKey(const QString &filePath) const; - QString getThumbnailCachePath(const QString &filePath) const; - QDateTime extractDateTimeFromFile(const QString &filePath) const; PhotoInfo::FileType determineFileType(const QString &fileName) const; - static QPixmap generateVideoThumbnail(iDescriptorDevice *device, - const QString &filePath, - const QSize &requestedSize); + static QPixmap generateVideoThumbnailFFmpeg(iDescriptorDevice *device, + const QString &filePath, + const QSize &requestedSize); + static QSemaphore m_videoThumbnailSemaphore; }; #endif // PHOTOMODEL_H \ No newline at end of file diff --git a/src/servicemanager.cpp b/src/servicemanager.cpp index 228e6ea..1fe4772 100644 --- a/src/servicemanager.cpp +++ b/src/servicemanager.cpp @@ -45,6 +45,19 @@ ServiceManager::safeAfcGetFileInfo(iDescriptorDevice *device, const char *path, altAfc); } +afc_error_t +ServiceManager::safeAfcGetFileInfoPlist(iDescriptorDevice *device, + const char *path, plist_t *info, + std::optional altAfc) +{ + return executeAfcOperation( + device, + [path, info](afc_client_t client) { + return afc_get_file_info_plist(client, path, info); + }, + altAfc); +} + afc_error_t ServiceManager::safeAfcFileOpen(iDescriptorDevice *device, const char *path, afc_file_mode_t mode, @@ -112,6 +125,18 @@ afc_error_t ServiceManager::safeAfcFileSeek(iDescriptorDevice *device, altAfc); } +afc_error_t ServiceManager::safeAfcFileTell(iDescriptorDevice *device, + uint64_t handle, uint64_t *position, + std::optional altAfc) +{ + return executeAfcOperation( + device, + [handle, position](afc_client_t client) { + return afc_file_tell(client, handle, position); + }, + altAfc); +} + QByteArray ServiceManager::safeReadAfcFileToByteArray(iDescriptorDevice *device, const char *path, diff --git a/src/servicemanager.h b/src/servicemanager.h index 10f7189..446489a 100644 --- a/src/servicemanager.h +++ b/src/servicemanager.h @@ -179,6 +179,12 @@ public: safeAfcGetFileInfo(iDescriptorDevice *device, const char *path, char ***info, std::optional altAfc = std::nullopt); + + static afc_error_t + safeAfcGetFileInfoPlist(iDescriptorDevice *device, const char *path, + plist_t *info, + std::optional altAfc = std::nullopt); + static afc_error_t safeAfcFileOpen(iDescriptorDevice *device, const char *path, afc_file_mode_t mode, uint64_t *handle, @@ -198,6 +204,10 @@ public: safeAfcFileSeek(iDescriptorDevice *device, uint64_t handle, int64_t offset, int whence, std::optional altAfc = std::nullopt); + static afc_error_t + safeAfcFileTell(iDescriptorDevice *device, uint64_t handle, + uint64_t *position, + std::optional altAfc = std::nullopt); // Utility functions static QByteArray safeReadAfcFileToByteArray( diff --git a/src/virtuallocationwidget.cpp b/src/virtuallocationwidget.cpp index 010b453..8582d44 100644 --- a/src/virtuallocationwidget.cpp +++ b/src/virtuallocationwidget.cpp @@ -143,7 +143,7 @@ VirtualLocation::VirtualLocation(iDescriptorDevice *device, QWidget *parent) }); DevDiskManager::sharedInstance()->downloadCompatibleImage(m_device); - unsigned int device_version = get_device_version(m_device->device); + unsigned int device_version = idevice_get_device_version(m_device->device); unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; if (deviceMajorVersion > 16) {