diff --git a/cmake/QmlPlugin.cmake b/cmake/QmlPlugin.cmake new file mode 100644 index 0000000..8c29ffd --- /dev/null +++ b/cmake/QmlPlugin.cmake @@ -0,0 +1,110 @@ +include(CMakeParseArguments) + +### Finds where to qmlplugindump binary is installed +### Requires that 'qmake' directory is in PATH +function(FindQmlPluginDump) + execute_process( + COMMAND qmake -query QT_INSTALL_BINS + OUTPUT_VARIABLE QT_BIN_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + set(QMLPLUGINDUMP_BIN ${QT_BIN_DIR}/qmlplugindump PARENT_SCOPE) +endfunction() + +### Sets QT_INSTALL_QML to the directory where QML Plugins should be installed +function(FindQtInstallQml) + execute_process( + COMMAND qmake -query QT_INSTALL_QML + OUTPUT_VARIABLE PROC_RESULT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + set(QT_INSTALL_QML ${PROC_RESULT} PARENT_SCOPE) +endfunction() + +function(add_qmlplugin TARGET) + set(options NO_AUTORCC NO_AUTOMOC) + set(oneValueArgs URI VERSION BINARY_DIR) + set(multiValueArgs SOURCES QMLFILES) + cmake_parse_arguments(QMLPLUGIN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + ### At least TARGET, URI and VERSION must be specified + if(NOT QMLPLUGIN_URI OR NOT QMLPLUGIN_VERSION) + message(WARNING "TARGET, URI and VERSION must be set, no files generated") + return() + endif() + + ### Depending on project hierarchy, one might want to specify a custom binary dir + if(NOT QMLPLUGIN_BINARY_DIR) + set(QMLPLUGIN_BINARY_DIR ${CMAKE_BINARY_DIR}) + endif() + + ### Source files + add_library(${TARGET} SHARED + ${QMLPLUGIN_SOURCES} + ) + + ### QML files, just to make them visible in the editor + add_custom_target("${TARGET}-qmlfiles" SOURCES ${QMLPLUGIN_QMLFILES}) + + ### No AutoMOC or AutoRCC + if(QMLPLUGIN_NO_AUTORCC) + set_target_properties(${TARGET} PROPERTIES AUTOMOC OFF) + else() + set_target_properties(${TARGET} PROPERTIES AUTOMOC ON) + endif() + if(QMLPLUGIN_NO_AUTOMOC) + set_target_properties(${TARGET} PROPERTIES AUTOMOC OFF) + else() + set_target_properties(${TARGET} PROPERTIES AUTOMOC ON) + endif() + + ### Find location of qmlplugindump (stored in QMLPLUGINDUMP_BIN) + FindQmlPluginDump() + ### Find where to install QML Plugins (stored in QT_INSTALL_QML) + FindQtInstallQml() + + set(COPY_QMLDIR_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/qmldir $/qmldir) + set(COPY_QMLFILES_COMMAND ${CMAKE_COMMAND} -E copy ${QMLPLUGIN_QMLFILES} $) + set(GENERATE_QMLTYPES_COMMAND ${QMLPLUGINDUMP_BIN} -nonrelocatable ${QMLPLUGIN_URI} ${QMLPLUGIN_VERSION} ${QMLPLUGIN_BINARY_DIR} > ${CMAKE_CURRENT_BINARY_DIR}/plugin.qmltypes) + + ### Copy qmldir from project source to binary dir + add_custom_command( + TARGET ${TARGET} + POST_BUILD + COMMAND ${COPY_QMLDIR_COMMAND} + COMMENT "Copying qmldir to binary directory" + ) + + ### Copy QML-files from project source to binary dir + if(QMLPLUGIN_QMLFILES) + add_custom_command( + TARGET ${TARGET} + POST_BUILD + COMMAND ${COPY_QMLFILES_COMMAND} + COMMENT "Copying QML files to binary directory" + ) + endif() + + ### Create command to generate plugin.qmltypes after build + add_custom_command( + TARGET ${TARGET} + POST_BUILD + COMMAND ${GENERATE_QMLTYPES_COMMAND} + COMMENT "Generating plugin.qmltypes" + ) + + string(REPLACE "." "/" QMLPLUGIN_INSTALL_URI ${QMLPLUGIN_URI}) + + ### Install library + install(TARGETS ${TARGET} + DESTINATION ${QT_INSTALL_QML}/${QMLPLUGIN_INSTALL_URI} + ) + + ### Install aditional files + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/qmldir + ${CMAKE_CURRENT_BINARY_DIR}/plugin.qmltypes + ${QMLPLUGIN_QMLFILES} + DESTINATION ${QT_INSTALL_QML}/${QMLPLUGIN_INSTALL_URI} + ) +endfunction() diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index dba3f77..26cc0d6 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -8,8 +8,11 @@ set(JellyfinQt_SOURCES src/model/deviceprofile.cpp src/model/item.cpp src/model/playlist.cpp + src/model/shuffle.cpp + src/support/jsonconv.cpp src/support/loader.cpp + src/support/parseexception.cpp src/viewmodel/item.cpp src/viewmodel/itemmodel.cpp src/viewmodel/loader.cpp @@ -31,8 +34,11 @@ set(JellyfinQt_HEADERS include/JellyfinQt/model/deviceprofile.h include/JellyfinQt/model/item.h include/JellyfinQt/model/playlist.h + include/JellyfinQt/model/shuffle.h include/JellyfinQt/support/jsonconv.h + include/JellyfinQt/support/jsonconvimpl.h include/JellyfinQt/support/loader.h + include/JellyfinQt/support/parseexception.h include/JellyfinQt/viewmodel/item.h include/JellyfinQt/viewmodel/itemmodel.h include/JellyfinQt/viewmodel/loader.h diff --git a/core/include/JellyfinQt/apiclient.h b/core/include/JellyfinQt/apiclient.h index 0068f7f..001b63a 100644 --- a/core/include/JellyfinQt/apiclient.h +++ b/core/include/JellyfinQt/apiclient.h @@ -231,6 +231,9 @@ public slots: protected slots: void defaultNetworkErrorHandler(QNetworkReply::NetworkError error); void onUserDataChanged(const QString &itemId, UserData *newData); + void credManagerServersListed(QStringList users); + void credManagerUsersListed(const QString &server, QStringList users); + void credManagerTokenRetrieved(const QString &server, const QString &user, const QString &token); protected: /** diff --git a/core/include/JellyfinQt/apimodel.h b/core/include/JellyfinQt/apimodel.h index 4d3e90b..86e8fa1 100644 --- a/core/include/JellyfinQt/apimodel.h +++ b/core/include/JellyfinQt/apimodel.h @@ -108,8 +108,7 @@ signals: */ void itemsLoaded(); void reloadWanted(); - -protected slots: +public slots: virtual void futureReady() = 0; protected: @@ -243,6 +242,18 @@ bool setRequestStartIndex(P ¶meters, int startIndex) { return false; } +#ifndef JELLYFIN_APIMODEL_CPP +extern template bool setRequestStartIndex(Loader::GetUserViewsParams ¶ms, int startIndex); +extern template void setRequestLimit(Loader::GetUserViewsParams ¶ms, int limit); +extern template QList extractRecords(const DTO::BaseItemDtoQueryResult &result); +extern template int extractTotalRecordCount(const DTO::BaseItemDtoQueryResult &result); +extern template QList extractRecords(const QList &result); +extern template int extractTotalRecordCount(const QList &result); +extern template void setRequestLimit(Loader::GetLatestMediaParams ¶ms, int limit); +extern template bool setRequestStartIndex(Loader::GetLatestMediaParams ¶ms, int offset); + +#endif + /** * Template for implementing a loader for the given type, response and parameters using Jellyfin::Support:Loaders. * @@ -301,7 +312,7 @@ protected: return; } result = optResult.value(); - } catch (Support::LoadException e) { + } catch (Support::LoadException &e) { qWarning() << "Exception while loading: " << e.what(); this->setStatus(ViewModel::ModelStatus::Error); return; @@ -430,11 +441,11 @@ public: } // QList-like API - const T& at(int index) { return m_array.at(index); } + const T& at(int index) const { return m_array.at(index); } /** * @return the amount of objects in this model. */ - int size() { + int size() const { return m_array.size(); } diff --git a/core/include/JellyfinQt/credentialmanager.h b/core/include/JellyfinQt/credentialmanager.h index 15e2779..872287a 100644 --- a/core/include/JellyfinQt/credentialmanager.h +++ b/core/include/JellyfinQt/credentialmanager.h @@ -101,7 +101,7 @@ public: signals: void tokenRetrieved(const QString &server, const QString &user, const QString &token) const; void serversListed(const QStringList &servers) const; - void usersListed(const QStringList &users) const; + void usersListed(const QString& server, const QStringList &users) const; protected: explicit CredentialsManager(QObject *parent = nullptr) : QObject (parent) {} diff --git a/core/include/JellyfinQt/dto/addvirtualfolderdto.h b/core/include/JellyfinQt/dto/addvirtualfolderdto.h index cca3d18..a6420c5 100644 --- a/core/include/JellyfinQt/dto/addvirtualfolderdto.h +++ b/core/include/JellyfinQt/dto/addvirtualfolderdto.h @@ -68,7 +68,7 @@ public: protected: - QSharedPointer m_libraryOptions = nullptr; + QSharedPointer m_libraryOptions = QSharedPointer(); }; } // NS DTO diff --git a/core/include/JellyfinQt/dto/albuminforemotesearchquery.h b/core/include/JellyfinQt/dto/albuminforemotesearchquery.h index d161ee5..1574371 100644 --- a/core/include/JellyfinQt/dto/albuminforemotesearchquery.h +++ b/core/include/JellyfinQt/dto/albuminforemotesearchquery.h @@ -94,7 +94,7 @@ public: protected: - QSharedPointer m_searchInfo = nullptr; + QSharedPointer m_searchInfo = QSharedPointer(); QString m_itemId; QString m_searchProviderName; bool m_includeDisabledProviders; diff --git a/core/include/JellyfinQt/dto/allthememediaresult.h b/core/include/JellyfinQt/dto/allthememediaresult.h index bec99ea..12f7c20 100644 --- a/core/include/JellyfinQt/dto/allthememediaresult.h +++ b/core/include/JellyfinQt/dto/allthememediaresult.h @@ -78,9 +78,9 @@ public: protected: - QSharedPointer m_themeVideosResult = nullptr; - QSharedPointer m_themeSongsResult = nullptr; - QSharedPointer m_soundtrackSongsResult = nullptr; + QSharedPointer m_themeVideosResult = QSharedPointer(); + QSharedPointer m_themeSongsResult = QSharedPointer(); + QSharedPointer m_soundtrackSongsResult = QSharedPointer(); }; } // NS DTO diff --git a/core/include/JellyfinQt/dto/artistinforemotesearchquery.h b/core/include/JellyfinQt/dto/artistinforemotesearchquery.h index 5dffaa9..cdf6758 100644 --- a/core/include/JellyfinQt/dto/artistinforemotesearchquery.h +++ b/core/include/JellyfinQt/dto/artistinforemotesearchquery.h @@ -94,7 +94,7 @@ public: protected: - QSharedPointer m_searchInfo = nullptr; + QSharedPointer m_searchInfo = QSharedPointer(); QString m_itemId; QString m_searchProviderName; bool m_includeDisabledProviders; diff --git a/core/include/JellyfinQt/dto/authenticationresult.h b/core/include/JellyfinQt/dto/authenticationresult.h index 2de547e..eefa646 100644 --- a/core/include/JellyfinQt/dto/authenticationresult.h +++ b/core/include/JellyfinQt/dto/authenticationresult.h @@ -89,8 +89,8 @@ public: protected: - QSharedPointer m_user = nullptr; - QSharedPointer m_sessionInfo = nullptr; + QSharedPointer m_user = QSharedPointer(); + QSharedPointer m_sessionInfo = QSharedPointer(); QString m_accessToken; QString m_serverId; }; diff --git a/core/include/JellyfinQt/dto/baseitemdto.h b/core/include/JellyfinQt/dto/baseitemdto.h index a054ccb..46aecb1 100644 --- a/core/include/JellyfinQt/dto/baseitemdto.h +++ b/core/include/JellyfinQt/dto/baseitemdto.h @@ -1611,7 +1611,7 @@ protected: QString m_parentBackdropItemId; QStringList m_parentBackdropImageTags; std::optional m_localTrailerCount = std::nullopt; - QSharedPointer m_userData = nullptr; + QSharedPointer m_userData = QSharedPointer(); std::optional m_recursiveItemCount = std::nullopt; std::optional m_childCount = std::nullopt; QString m_seriesName; @@ -1699,7 +1699,7 @@ protected: std::optional m_isKids = std::nullopt; std::optional m_isPremiere = std::nullopt; QString m_timerId; - QSharedPointer m_currentProgram = nullptr; + QSharedPointer m_currentProgram = QSharedPointer(); }; } // NS DTO diff --git a/core/include/JellyfinQt/dto/bookinforemotesearchquery.h b/core/include/JellyfinQt/dto/bookinforemotesearchquery.h index 5cfa3fa..471d89a 100644 --- a/core/include/JellyfinQt/dto/bookinforemotesearchquery.h +++ b/core/include/JellyfinQt/dto/bookinforemotesearchquery.h @@ -94,7 +94,7 @@ public: protected: - QSharedPointer m_searchInfo = nullptr; + QSharedPointer m_searchInfo = QSharedPointer(); QString m_itemId; QString m_searchProviderName; bool m_includeDisabledProviders; diff --git a/core/include/JellyfinQt/dto/boxsetinforemotesearchquery.h b/core/include/JellyfinQt/dto/boxsetinforemotesearchquery.h index 9c20d90..37f27fc 100644 --- a/core/include/JellyfinQt/dto/boxsetinforemotesearchquery.h +++ b/core/include/JellyfinQt/dto/boxsetinforemotesearchquery.h @@ -94,7 +94,7 @@ public: protected: - QSharedPointer m_searchInfo = nullptr; + QSharedPointer m_searchInfo = QSharedPointer(); QString m_itemId; QString m_searchProviderName; bool m_includeDisabledProviders; diff --git a/core/include/JellyfinQt/dto/clientcapabilities.h b/core/include/JellyfinQt/dto/clientcapabilities.h index b4eb113..9c9db18 100644 --- a/core/include/JellyfinQt/dto/clientcapabilities.h +++ b/core/include/JellyfinQt/dto/clientcapabilities.h @@ -134,7 +134,7 @@ protected: QString m_messageCallbackUrl; bool m_supportsPersistentIdentifier; bool m_supportsSync; - QSharedPointer m_deviceProfile = nullptr; + QSharedPointer m_deviceProfile = QSharedPointer(); QString m_appStoreUrl; QString m_iconUrl; }; diff --git a/core/include/JellyfinQt/dto/clientcapabilitiesdto.h b/core/include/JellyfinQt/dto/clientcapabilitiesdto.h index b9e06df..0979c2d 100644 --- a/core/include/JellyfinQt/dto/clientcapabilitiesdto.h +++ b/core/include/JellyfinQt/dto/clientcapabilitiesdto.h @@ -170,7 +170,7 @@ protected: QString m_messageCallbackUrl; bool m_supportsPersistentIdentifier; bool m_supportsSync; - QSharedPointer m_deviceProfile = nullptr; + QSharedPointer m_deviceProfile = QSharedPointer(); QString m_appStoreUrl; QString m_iconUrl; }; diff --git a/core/include/JellyfinQt/dto/deviceinfo.h b/core/include/JellyfinQt/dto/deviceinfo.h index d574463..9ef1387 100644 --- a/core/include/JellyfinQt/dto/deviceinfo.h +++ b/core/include/JellyfinQt/dto/deviceinfo.h @@ -153,7 +153,7 @@ protected: QString m_appVersion; QString m_lastUserId; QDateTime m_dateLastActivity; - QSharedPointer m_capabilities = nullptr; + QSharedPointer m_capabilities = QSharedPointer(); QString m_iconUrl; }; diff --git a/core/include/JellyfinQt/dto/deviceprofile.h b/core/include/JellyfinQt/dto/deviceprofile.h index 0ff9d82..66f2c34 100644 --- a/core/include/JellyfinQt/dto/deviceprofile.h +++ b/core/include/JellyfinQt/dto/deviceprofile.h @@ -478,7 +478,7 @@ public: protected: QString m_name; QString m_jellyfinId; - QSharedPointer m_identification = nullptr; + QSharedPointer m_identification = QSharedPointer(); QString m_friendlyName; QString m_manufacturer; QString m_manufacturerUrl; diff --git a/core/include/JellyfinQt/dto/installationinfo.h b/core/include/JellyfinQt/dto/installationinfo.h index 10b8415..e3ffc1b 100644 --- a/core/include/JellyfinQt/dto/installationinfo.h +++ b/core/include/JellyfinQt/dto/installationinfo.h @@ -124,7 +124,7 @@ public: protected: QString m_guid; QString m_name; - QSharedPointer m_version = nullptr; + QSharedPointer m_version = QSharedPointer(); QString m_changelog; QString m_sourceUrl; QString m_checksum; diff --git a/core/include/JellyfinQt/dto/iplugin.h b/core/include/JellyfinQt/dto/iplugin.h index 4055873..2515d31 100644 --- a/core/include/JellyfinQt/dto/iplugin.h +++ b/core/include/JellyfinQt/dto/iplugin.h @@ -134,7 +134,7 @@ protected: QString m_name; QString m_description; QString m_jellyfinId; - QSharedPointer m_version = nullptr; + QSharedPointer m_version = QSharedPointer(); QString m_assemblyFilePath; bool m_canUninstall; QString m_dataFolderPath; diff --git a/core/include/JellyfinQt/dto/livestreamresponse.h b/core/include/JellyfinQt/dto/livestreamresponse.h index a680333..c486b28 100644 --- a/core/include/JellyfinQt/dto/livestreamresponse.h +++ b/core/include/JellyfinQt/dto/livestreamresponse.h @@ -68,7 +68,7 @@ public: protected: - QSharedPointer m_mediaSource = nullptr; + QSharedPointer m_mediaSource = QSharedPointer(); }; } // NS DTO diff --git a/core/include/JellyfinQt/dto/mediapathdto.h b/core/include/JellyfinQt/dto/mediapathdto.h index 6060a64..d577462 100644 --- a/core/include/JellyfinQt/dto/mediapathdto.h +++ b/core/include/JellyfinQt/dto/mediapathdto.h @@ -91,7 +91,7 @@ public: protected: QString m_name; QString m_path; - QSharedPointer m_pathInfo = nullptr; + QSharedPointer m_pathInfo = QSharedPointer(); }; } // NS DTO diff --git a/core/include/JellyfinQt/dto/movieinforemotesearchquery.h b/core/include/JellyfinQt/dto/movieinforemotesearchquery.h index c997d04..25967e5 100644 --- a/core/include/JellyfinQt/dto/movieinforemotesearchquery.h +++ b/core/include/JellyfinQt/dto/movieinforemotesearchquery.h @@ -94,7 +94,7 @@ public: protected: - QSharedPointer m_searchInfo = nullptr; + QSharedPointer m_searchInfo = QSharedPointer(); QString m_itemId; QString m_searchProviderName; bool m_includeDisabledProviders; diff --git a/core/include/JellyfinQt/dto/musicvideoinforemotesearchquery.h b/core/include/JellyfinQt/dto/musicvideoinforemotesearchquery.h index 611cfe0..9c8f8fb 100644 --- a/core/include/JellyfinQt/dto/musicvideoinforemotesearchquery.h +++ b/core/include/JellyfinQt/dto/musicvideoinforemotesearchquery.h @@ -94,7 +94,7 @@ public: protected: - QSharedPointer m_searchInfo = nullptr; + QSharedPointer m_searchInfo = QSharedPointer(); QString m_itemId; QString m_searchProviderName; bool m_includeDisabledProviders; diff --git a/core/include/JellyfinQt/dto/openlivestreamdto.h b/core/include/JellyfinQt/dto/openlivestreamdto.h index 5441f4b..5a08476 100644 --- a/core/include/JellyfinQt/dto/openlivestreamdto.h +++ b/core/include/JellyfinQt/dto/openlivestreamdto.h @@ -215,7 +215,7 @@ protected: QString m_itemId; std::optional m_enableDirectPlay = std::nullopt; std::optional m_enableDirectStream = std::nullopt; - QSharedPointer m_deviceProfile = nullptr; + QSharedPointer m_deviceProfile = QSharedPointer(); QList m_directPlayProtocols; }; diff --git a/core/include/JellyfinQt/dto/personlookupinforemotesearchquery.h b/core/include/JellyfinQt/dto/personlookupinforemotesearchquery.h index 0bbf993..95a7bc9 100644 --- a/core/include/JellyfinQt/dto/personlookupinforemotesearchquery.h +++ b/core/include/JellyfinQt/dto/personlookupinforemotesearchquery.h @@ -94,7 +94,7 @@ public: protected: - QSharedPointer m_searchInfo = nullptr; + QSharedPointer m_searchInfo = QSharedPointer(); QString m_itemId; QString m_searchProviderName; bool m_includeDisabledProviders; diff --git a/core/include/JellyfinQt/dto/playbackinfodto.h b/core/include/JellyfinQt/dto/playbackinfodto.h index 0c3ace0..f2f5b58 100644 --- a/core/include/JellyfinQt/dto/playbackinfodto.h +++ b/core/include/JellyfinQt/dto/playbackinfodto.h @@ -231,7 +231,7 @@ protected: std::optional m_maxAudioChannels = std::nullopt; QString m_mediaSourceId; QString m_liveStreamId; - QSharedPointer m_deviceProfile = nullptr; + QSharedPointer m_deviceProfile = QSharedPointer(); std::optional m_enableDirectPlay = std::nullopt; std::optional m_enableDirectStream = std::nullopt; std::optional m_enableTranscoding = std::nullopt; diff --git a/core/include/JellyfinQt/dto/playbackprogressinfo.h b/core/include/JellyfinQt/dto/playbackprogressinfo.h index 28725f5..8c9c08c 100644 --- a/core/include/JellyfinQt/dto/playbackprogressinfo.h +++ b/core/include/JellyfinQt/dto/playbackprogressinfo.h @@ -244,7 +244,7 @@ public: protected: bool m_canSeek; - QSharedPointer m_item = nullptr; + QSharedPointer m_item = QSharedPointer(); QString m_itemId; QString m_sessionId; QString m_mediaSourceId; diff --git a/core/include/JellyfinQt/dto/playbackstartinfo.h b/core/include/JellyfinQt/dto/playbackstartinfo.h index a4c9287..6fdd0a9 100644 --- a/core/include/JellyfinQt/dto/playbackstartinfo.h +++ b/core/include/JellyfinQt/dto/playbackstartinfo.h @@ -244,7 +244,7 @@ public: protected: bool m_canSeek; - QSharedPointer m_item = nullptr; + QSharedPointer m_item = QSharedPointer(); QString m_itemId; QString m_sessionId; QString m_mediaSourceId; diff --git a/core/include/JellyfinQt/dto/playbackstopinfo.h b/core/include/JellyfinQt/dto/playbackstopinfo.h index b277af4..bb2092f 100644 --- a/core/include/JellyfinQt/dto/playbackstopinfo.h +++ b/core/include/JellyfinQt/dto/playbackstopinfo.h @@ -166,7 +166,7 @@ public: protected: - QSharedPointer m_item = nullptr; + QSharedPointer m_item = QSharedPointer(); QString m_itemId; QString m_sessionId; QString m_mediaSourceId; diff --git a/core/include/JellyfinQt/dto/plugininfo.h b/core/include/JellyfinQt/dto/plugininfo.h index ccf575f..26c5a98 100644 --- a/core/include/JellyfinQt/dto/plugininfo.h +++ b/core/include/JellyfinQt/dto/plugininfo.h @@ -136,7 +136,7 @@ public: protected: QString m_name; - QSharedPointer m_version = nullptr; + QSharedPointer m_version = QSharedPointer(); QString m_configurationFileName; QString m_description; QString m_jellyfinId; diff --git a/core/include/JellyfinQt/dto/remotesearchresult.h b/core/include/JellyfinQt/dto/remotesearchresult.h index f405425..09e623c 100644 --- a/core/include/JellyfinQt/dto/remotesearchresult.h +++ b/core/include/JellyfinQt/dto/remotesearchresult.h @@ -170,7 +170,7 @@ protected: QString m_imageUrl; QString m_searchProviderName; QString m_overview; - QSharedPointer m_albumArtist = nullptr; + QSharedPointer m_albumArtist = QSharedPointer(); QList m_artists; }; diff --git a/core/include/JellyfinQt/dto/seriesinforemotesearchquery.h b/core/include/JellyfinQt/dto/seriesinforemotesearchquery.h index 541864f..de4264b 100644 --- a/core/include/JellyfinQt/dto/seriesinforemotesearchquery.h +++ b/core/include/JellyfinQt/dto/seriesinforemotesearchquery.h @@ -94,7 +94,7 @@ public: protected: - QSharedPointer m_searchInfo = nullptr; + QSharedPointer m_searchInfo = QSharedPointer(); QString m_itemId; QString m_searchProviderName; bool m_includeDisabledProviders; diff --git a/core/include/JellyfinQt/dto/serverconfiguration.h b/core/include/JellyfinQt/dto/serverconfiguration.h index 094e620..b6ea8ac 100644 --- a/core/include/JellyfinQt/dto/serverconfiguration.h +++ b/core/include/JellyfinQt/dto/serverconfiguration.h @@ -819,7 +819,7 @@ protected: qint32 m_logFileRetentionDays; bool m_isStartupWizardCompleted; QString m_cachePath; - QSharedPointer m_previousVersion = nullptr; + QSharedPointer m_previousVersion = QSharedPointer(); QString m_previousVersionStr; bool m_enableUPnP; bool m_enableMetrics; diff --git a/core/include/JellyfinQt/dto/sessioninfo.h b/core/include/JellyfinQt/dto/sessioninfo.h index 024b35e..b71673a 100644 --- a/core/include/JellyfinQt/dto/sessioninfo.h +++ b/core/include/JellyfinQt/dto/sessioninfo.h @@ -300,9 +300,9 @@ public: protected: - QSharedPointer m_playState = nullptr; + QSharedPointer m_playState = QSharedPointer(); QList m_additionalUsers; - QSharedPointer m_capabilities = nullptr; + QSharedPointer m_capabilities = QSharedPointer(); QString m_remoteEndPoint; QStringList m_playableMediaTypes; QString m_jellyfinId; @@ -313,12 +313,12 @@ protected: QDateTime m_lastPlaybackCheckIn; QString m_deviceName; QString m_deviceType; - QSharedPointer m_nowPlayingItem = nullptr; - QSharedPointer m_fullNowPlayingItem = nullptr; - QSharedPointer m_nowViewingItem = nullptr; + QSharedPointer m_nowPlayingItem = QSharedPointer(); + QSharedPointer m_fullNowPlayingItem = QSharedPointer(); + QSharedPointer m_nowViewingItem = QSharedPointer(); QString m_deviceId; QString m_applicationVersion; - QSharedPointer m_transcodingInfo = nullptr; + QSharedPointer m_transcodingInfo = QSharedPointer(); bool m_isActive; bool m_supportsMediaControl; bool m_supportsRemoteControl; diff --git a/core/include/JellyfinQt/dto/taskinfo.h b/core/include/JellyfinQt/dto/taskinfo.h index 065632f..02c5138 100644 --- a/core/include/JellyfinQt/dto/taskinfo.h +++ b/core/include/JellyfinQt/dto/taskinfo.h @@ -168,7 +168,7 @@ protected: TaskState m_state; std::optional m_currentProgressPercentage = std::nullopt; QString m_jellyfinId; - QSharedPointer m_lastExecutionResult = nullptr; + QSharedPointer m_lastExecutionResult = QSharedPointer(); QList m_triggers; QString m_description; QString m_category; diff --git a/core/include/JellyfinQt/dto/timerinfodto.h b/core/include/JellyfinQt/dto/timerinfodto.h index d521c7e..0524566 100644 --- a/core/include/JellyfinQt/dto/timerinfodto.h +++ b/core/include/JellyfinQt/dto/timerinfodto.h @@ -362,7 +362,7 @@ protected: QString m_seriesTimerId; QString m_externalSeriesTimerId; std::optional m_runTimeTicks = std::nullopt; - QSharedPointer m_programInfo = nullptr; + QSharedPointer m_programInfo = QSharedPointer(); }; } // NS DTO diff --git a/core/include/JellyfinQt/dto/trailerinforemotesearchquery.h b/core/include/JellyfinQt/dto/trailerinforemotesearchquery.h index 04124b0..9063f75 100644 --- a/core/include/JellyfinQt/dto/trailerinforemotesearchquery.h +++ b/core/include/JellyfinQt/dto/trailerinforemotesearchquery.h @@ -94,7 +94,7 @@ public: protected: - QSharedPointer m_searchInfo = nullptr; + QSharedPointer m_searchInfo = QSharedPointer(); QString m_itemId; QString m_searchProviderName; bool m_includeDisabledProviders; diff --git a/core/include/JellyfinQt/dto/updatelibraryoptionsdto.h b/core/include/JellyfinQt/dto/updatelibraryoptionsdto.h index bc2ccdc..67827c5 100644 --- a/core/include/JellyfinQt/dto/updatelibraryoptionsdto.h +++ b/core/include/JellyfinQt/dto/updatelibraryoptionsdto.h @@ -79,7 +79,7 @@ public: protected: QString m_jellyfinId; - QSharedPointer m_libraryOptions = nullptr; + QSharedPointer m_libraryOptions = QSharedPointer(); }; } // NS DTO diff --git a/core/include/JellyfinQt/dto/userdto.h b/core/include/JellyfinQt/dto/userdto.h index 31addf5..640470d 100644 --- a/core/include/JellyfinQt/dto/userdto.h +++ b/core/include/JellyfinQt/dto/userdto.h @@ -213,8 +213,8 @@ protected: std::optional m_enableAutoLogin = std::nullopt; QDateTime m_lastLoginDate; QDateTime m_lastActivityDate; - QSharedPointer m_configuration = nullptr; - QSharedPointer m_policy = nullptr; + QSharedPointer m_configuration = QSharedPointer(); + QSharedPointer m_policy = QSharedPointer(); std::optional m_primaryImageAspectRatio = std::nullopt; }; diff --git a/core/include/JellyfinQt/dto/versioninfo.h b/core/include/JellyfinQt/dto/versioninfo.h index ec9ce2b..ca8de47 100644 --- a/core/include/JellyfinQt/dto/versioninfo.h +++ b/core/include/JellyfinQt/dto/versioninfo.h @@ -158,7 +158,7 @@ public: protected: QString m_version; - QSharedPointer m_versionNumber = nullptr; + QSharedPointer m_versionNumber = QSharedPointer(); QString m_changelog; QString m_targetAbi; QString m_sourceUrl; diff --git a/core/include/JellyfinQt/dto/virtualfolderinfo.h b/core/include/JellyfinQt/dto/virtualfolderinfo.h index a652cdc..5ab7eab 100644 --- a/core/include/JellyfinQt/dto/virtualfolderinfo.h +++ b/core/include/JellyfinQt/dto/virtualfolderinfo.h @@ -143,7 +143,7 @@ protected: QString m_name; QStringList m_locations; QString m_collectionType; - QSharedPointer m_libraryOptions = nullptr; + QSharedPointer m_libraryOptions = QSharedPointer(); QString m_itemId; QString m_primaryImageItemId; std::optional m_refreshProgress = std::nullopt; diff --git a/core/include/JellyfinQt/model/playlist.h b/core/include/JellyfinQt/model/playlist.h index c1d807d..3e58e32 100644 --- a/core/include/JellyfinQt/model/playlist.h +++ b/core/include/JellyfinQt/model/playlist.h @@ -1,39 +1,110 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #ifndef JELLYFIN_MODEL_PLAYLIST_H #define JELLYFIN_MODEL_PLAYLIST_H +#include #include #include #include #include +#include "../viewmodel/itemmodel.h" +#include "item.h" namespace Jellyfin { namespace Model { -// Forward declaration -class Item; +class Shuffle; -class Playlist { +/** + * @brief Model of a playlist, a list of items that can be played. + * + * This tries to take the managing what items to play away from the PlaybackManager, + * which now only will be informed about the current and next item to play. + * + * The playlist has actually two list, one named list and the other named queue. When + * playing, the queue has priority over the list and will not be affected by the + * shuffle mode. After all items of the queue are played, the items in the list are played. + * Items in the list may be shuffled. + */ +class Playlist : public QObject { + Q_OBJECT public: - explicit Playlist(); + explicit Playlist(QObject *parent = nullptr); - /// Start loading data for the next item. - void preloadNext(); + /// Returns the current item in the queue + QSharedPointer currentItem(); + QSharedPointer nextItem(); + /** + * @brief Determine the previous item to be played. + */ + void previous(); + /** + * @brief Determine the next item to be played. + */ + void next(); + + // int queueSize() { return m_queue.size(); }; + int listSize() const { return m_list.size(); }; + int totalSize() const { return m_queue.size() + m_list.size(); } + + QSharedPointer listAt(int index) const; + /** + * @brief Removes all the items from the playlist + */ + void clearList(); + + /** + * @brief Appends all items from the given itemModel to this list + */ + void appendToList(const ViewModel::ItemModel &model); + + /** + * @brief Start playing this playlist + * @param index The index to start from. + */ + void play(int index = 0); +signals: + void listCleared(); + void itemsAddedToQueue(int index, int count); + void itemsAddedToList(int index, int count); + void listReshuffled(); private: - /// Extra data about each itemId that this playlist manages - struct ExtendedItem { - QSharedPointer item; - /// The url from which this item can be streamed. - QUrl url; - /// Playsession that should be reported to Jellyfin's server. - QString playSession; - /// Text to be shown when an error occurred while fetching playback information. - QString errorText; - }; + void reshuffle(); - QVector list; + QSharedPointer m_currentItem; + bool m_currentItemFromQueue = false; + QSharedPointer m_nextItem; + bool m_nextItemFromQueue = false; + + /// list of the items in the queue + QVector> m_queue; + /// list of the items in the playlist + QVector> m_list; + /// The current position in the playlist + int m_pos = 0; + + /// Algorithm for shuffling the playlist. + Shuffle *m_shuffler; }; } diff --git a/core/include/JellyfinQt/model/shuffle.h b/core/include/JellyfinQt/model/shuffle.h new file mode 100644 index 0000000..55c4731 --- /dev/null +++ b/core/include/JellyfinQt/model/shuffle.h @@ -0,0 +1,166 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#ifndef JELLYFIN_MODEL_SHUFFLE_H +#define JELLYFIN_MODEL_SHUFFLE_H + +#include "playlist.h" +#include "../model/item.h" + + +namespace Jellyfin { +namespace Model { + +/** + * @brief Interface for an algorithm shuffling a playlist. + */ +class Shuffle { +public: + Shuffle(const Playlist *parent) : m_playlist(parent) {} + + /** + * @brief If this Shuffle implementation shuffles the entire list in advance. + * @return True if this shuffle class shuffles the entire list in advance + * + * Some shuffle implementations may only shuffle the next item as they go. + */ + virtual bool canShuffleInAdvance() { return true; } + + /** + * @brief Shuffle the list in advance. Should only be called if canShuffleInAdvance() + * is called. + */ + virtual void shuffleInAdvance() {} + + /** + * @brief The shuffle should determine the next item. + */ + virtual void next() {}; + + /** + * @brief The shuffle should determine the previous item. + */ + virtual void previous() {}; + + /** + * @brief Set the index of the now playing item. + * @param i + */ + virtual void setIndex(int i) {}; + + /** + * @return the index of the currently playing item + */ + virtual int currentItem() const { return -1; } + + /** + * @brief Determine the item index at at the shuffled index + * @param index The shuffled index + * @return The actual index + * + * If canShuffleInAdvance() returns false, a new implemention is not needed + * or -1 may be returned. This function should not even be called in that case. + */ + virtual int itemAt(int index) const { return -1; } + + /** + * @return the index of the next item + */ + virtual int nextItem() const { return -1; } + + /** + * @brief Sets whether the shuffler to loop over the list if all items are played. + */ + void setRepeatAll(bool repeatAll) { m_repeatAll = repeatAll; } +protected: + /// Playlist that can be used to gather information about the songs if needed for the algorithm + const Playlist *m_playlist; + bool m_repeatAll = false; + static int random(int max, int min = 0); +}; + +/** + * @brief A shuffler that does not shuffle. + */ +class NoShuffle : public Shuffle { +public: + NoShuffle(const Playlist *parent); + + virtual int currentItem() const override; + virtual int nextItem() const override; + + virtual void previous() override; + virtual void next() override; + virtual void setIndex(int i) override; +protected: + int nextIndex() const; + int previousIndex() const; + int m_index = 0; + +}; + +/** + * @brief Base class for shuffles that shuffle the entire list in advance + */ +class ListShuffleBase : public NoShuffle { +public: + ListShuffleBase(const Playlist *parent); + virtual int currentItem() const override; + virtual int nextItem() const override; +protected: + QVector m_map; +}; + +/** + * @brief A simple shuffler which shuffles each item in the list in advance + */ +class SimpleListShuffle : public ListShuffleBase { +public: + SimpleListShuffle(const Playlist *parent); + virtual void shuffleInAdvance() override; +}; + +/** + * @brief A shuffler that is pretty random. Does not care about repeating items in a list. + */ +class RandomShuffle : public Shuffle { +public: + RandomShuffle(const Playlist *parent); + bool canShuffleInAdvance() override; + virtual int currentItem() const override; + virtual int nextItem() const override; + virtual void previous() override; + virtual void next() override; +protected: + int m_previous, m_current, m_next = -1; +}; + +/** + * @brief A smart shuffler that shuffles a list with a few constraints to make it appear "more random" to an user. + * + * This shuffler tries to place to avoid placing tracks of the same album, artist, and genre next to each other. + * This way, the user may perceive the list as more random + */ +class VariedListShuffle { + +}; + +} // NS Model +} // NS Jellyfin + +#endif // SHUFFLE_H diff --git a/core/include/JellyfinQt/support/jsonconv.h b/core/include/JellyfinQt/support/jsonconv.h index c8cc6c9..e778022 100644 --- a/core/include/JellyfinQt/support/jsonconv.h +++ b/core/include/JellyfinQt/support/jsonconv.h @@ -19,159 +19,31 @@ #ifndef JELLYFIN_SUPPORT_JSONCONV_H #define JELLYFIN_SUPPORT_JSONCONV_H -#include - -#include -#include -#include -#include -#include -#include -#include +#include "jsonconvimpl.h" +#include "parseexception.h" namespace Jellyfin { namespace Support { -// Helper functions -QString uuidToString(const QUuid &source); -QUuid stringToUuid(const QString &source); - -/** - * @brief Thrown when JSON cannot be parsed. - */ -class ParseException : public QException { -public: - explicit ParseException(const QString &message) - : m_message(message.toStdString()) {} - - /*explicit ParseException(const ParseException &other) - : m_message(other.m_message) {}*/ - - virtual const char *what() const noexcept override; - - virtual QException *clone() const override; - virtual void raise() const override; -private: - std::string m_message; -}; - -// https://www.fluentcpp.com/2017/08/15/function-templates-partial-specialization-cpp/ -template -struct convertType{}; - -/** - * Template for converting types from JSON into their respective type. - */ -template -T fromJsonValue(const QJsonValue &source, convertType) { - Q_UNUSED(source) - Q_ASSERT_X(false, "fromJsonValue", "fromJsonValue called with unimplemented type"); -} - - -template -QJsonValue toJsonValue(const T &source, convertType) { - Q_UNUSED(source) - std::string msg = "toJsonValue called with unimplemented type "; - msg += typeid (T).name(); - Q_ASSERT_X(false, "toJsonValue", msg.c_str()); - return QJsonValue(); -} - -template -T fromJsonValue(const QJsonValue &source) { - return fromJsonValue(source, convertType{}); -} - - -template -QJsonValue toJsonValue(const T &source) { - return toJsonValue(source, convertType{}); -} - -// QList -template -QList fromJsonValue(const QJsonValue &source, convertType>) { - QList result; - QJsonArray arr = source.toArray(); - result.reserve(arr.size()); - for (auto it = arr.cbegin(); it != arr.cend(); it++) { - result.append(fromJsonValue(*it)); - } - return result; -} - -template -QJsonValue toJsonValue(const QList &source, convertType>) { - QJsonArray result; - for (auto it = source.cbegin(); it != source.cend(); it++) { - result.push_back(toJsonValue(*it)); - } - return result; -} - -// Optional - -template -std::optional fromJsonValue(const QJsonValue &source, convertType>) { - if (source.isNull()) { - return std::nullopt; - } else { - return fromJsonValue(source, convertType{}); - } -} - -template -QJsonValue toJsonValue(const std::optional &source, convertType>) { - if (source.has_value()) { - return toJsonValue(source.value(), convertType{}); - } else { - // Null - return QJsonValue(); - } -} - -// QSharedPointer -template -QSharedPointer fromJsonValue(const QJsonValue &source, convertType>) { - if (source.isNull()) { - return QSharedPointer(); - } - return QSharedPointer::create(fromJsonValue(source)); -} - -template -QJsonValue toJsonValue(const QSharedPointer &source, convertType>) { - if (source.isNull()) { - return QJsonValue(); - } - return toJsonValue(*source); -} - - -/** - * Templates for string conversion. - */ - -template -QString toString(const T &source, convertType) { - return toJsonValue(source).toString(); -} - -template -QString toString(const std::optional &source, convertType>) { - if (source.has_value()) { - return toString(source.value(), convertType{}); - } else { - return QString(); - } -} - -template -QString toString(const T &source) { - return toString(source, convertType{}); -} +extern template int fromJsonValue(const QJsonValue &source, convertType); +extern template qint64 fromJsonValue(const QJsonValue &source, convertType); +extern template bool fromJsonValue(const QJsonValue &source, convertType); +extern template QString fromJsonValue(const QJsonValue &source, convertType); +extern template QStringList fromJsonValue(const QJsonValue &source, convertType); +extern template QJsonObject fromJsonValue(const QJsonValue &source, convertType); +extern template double fromJsonValue(const QJsonValue &source, convertType); +extern template float fromJsonValue(const QJsonValue &source, convertType); +extern template QDateTime fromJsonValue(const QJsonValue &source, convertType); +extern template QVariant fromJsonValue(const QJsonValue &source, convertType); +extern template QUuid fromJsonValue(const QJsonValue &source, convertType); +extern template QString toString(const QUuid &source, convertType); +extern template QString toString(const qint32 &source, convertType); +extern template QString toString(const qint64 &source, convertType); +extern template QString toString(const float &source, convertType); +extern template QString toString(const double &source, convertType); +extern template QString toString(const bool &source, convertType); +extern template QString toString(const QString &source, convertType); } // NS Support } // NS Jellyfin diff --git a/core/include/JellyfinQt/support/jsonconvimpl.h b/core/include/JellyfinQt/support/jsonconvimpl.h new file mode 100644 index 0000000..d1714a8 --- /dev/null +++ b/core/include/JellyfinQt/support/jsonconvimpl.h @@ -0,0 +1,165 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#ifndef JELLYFIN_SUPPORT_JSONCONVIMPL_H +#define JELLYFIN_SUPPORT_JSONCONVIMPL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace Jellyfin { +namespace Support { + +// Helper functions +//QString uuidToString(const QUuid &source); +//QUuid stringToUuid(const QString &source); + + +// https://www.fluentcpp.com/2017/08/15/function-templates-partial-specialization-cpp/ +template +struct convertType{}; + +/** + * Template for converting types from JSON into their respective type. + */ +template +T fromJsonValue(const QJsonValue &source, convertType) { + Q_UNUSED(source) + Q_ASSERT_X(false, "fromJsonValue", "fromJsonValue called with unimplemented type"); +} + +/** + * Template for converting types from their type to JSON. + */ +template +QJsonValue toJsonValue(const T &source, convertType) { + Q_UNUSED(source) + std::string msg = "toJsonValue called with unimplemented type "; + msg += typeid (T).name(); + Q_ASSERT_X(false, "toJsonValue", msg.c_str()); + return QJsonValue(); +} + +template +T fromJsonValue(const QJsonValue &source) { + return fromJsonValue(source, convertType{}); +} + + +template +QJsonValue toJsonValue(const T &source) { + return toJsonValue(source, convertType{}); +} + +// QList +template +QList fromJsonValue(const QJsonValue &source, convertType>) { + QList result; + QJsonArray arr = source.toArray(); + result.reserve(arr.size()); + for (auto it = arr.constBegin(); it != arr.constEnd(); it++) { + result.append(fromJsonValue(*it)); + } + return result; +} + +template +QJsonValue toJsonValue(const QList &source, convertType>) { + QJsonArray result; + for (auto it = source.cbegin(); it != source.cend(); it++) { + result.push_back(toJsonValue(*it)); + } + return result; +} + +// Optional + +template +std::optional fromJsonValue(const QJsonValue &source, convertType>) { + if (source.isNull()) { + return std::nullopt; + } else { + return fromJsonValue(source, convertType{}); + } +} + +template +QJsonValue toJsonValue(const std::optional &source, convertType>) { + if (source.has_value()) { + return toJsonValue(source.value(), convertType{}); + } else { + // Null + return QJsonValue(); + } +} + +// QSharedPointer +template +QSharedPointer fromJsonValue(const QJsonValue &source, convertType>) { + if (source.isNull()) { + return QSharedPointer(); + } + return QSharedPointer::create(fromJsonValue(source)); +} + +template +QJsonValue toJsonValue(const QSharedPointer &source, convertType>) { + if (source.isNull()) { + return QJsonValue(); + } + return toJsonValue(*source); +} + + +/** + * Templates for string conversion. + */ + +template +QString toString(const T &source, convertType) { + return toJsonValue(source).toString(); +} + +template +QString toString(const std::optional &source, convertType>) { + if (source.has_value()) { + return toString(source.value(), convertType{}); + } else { + return QString(); + } +} + +template +QString toString(const T &source) { + return toString(source, convertType{}); +} + + +} // NS Support +} // NS Jellyfin + +#endif // JELLYFIN_SUPPORT_JSONCONVIMPL_H diff --git a/core/include/JellyfinQt/support/parseexception.h b/core/include/JellyfinQt/support/parseexception.h new file mode 100644 index 0000000..20d1cb7 --- /dev/null +++ b/core/include/JellyfinQt/support/parseexception.h @@ -0,0 +1,50 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#ifndef JELLYFIN_SUPPORT_PARSEEXCEPTION_H +#define JELLYFIN_SUPPORT_PARSEEXCEPTION_H + +#include +#include + +namespace Jellyfin { +namespace Support { + +/** + * @brief Thrown when JSON cannot be parsed. + */ +class ParseException : public QException { +public: + explicit ParseException(const QString &message) + : m_message(message.toStdString()) {} + + /*explicit ParseException(const ParseException &other) + : m_message(other.m_message) {}*/ + + virtual const char *what() const noexcept override; + + virtual QException *clone() const override; + virtual void raise() const override; +private: + std::string m_message; +}; + +} // NS Support +} // NS Jellyfin + +#endif // JELLYFIN_SUPPORT_PARSEEXCEPTION_H diff --git a/core/include/JellyfinQt/viewmodel/item.h b/core/include/JellyfinQt/viewmodel/item.h index 74d5eae..bac8590 100644 --- a/core/include/JellyfinQt/viewmodel/item.h +++ b/core/include/JellyfinQt/viewmodel/item.h @@ -84,14 +84,14 @@ public: Q_PROPERTY(QStringList productionLocations READ productionLocations NOTIFY productionLocationsChanged) // Handpicked, important ones - Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks NOTIFY runTimeTicksChanged) + Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks NOTIFY runTimeTicksChanged)*/ Q_PROPERTY(QString overview READ overview NOTIFY overviewChanged) Q_PROPERTY(int productionYear READ productionYear NOTIFY productionYearChanged) Q_PROPERTY(int indexNumber READ indexNumber NOTIFY indexNumberChanged) Q_PROPERTY(int indexNumberEnd READ indexNumberEnd NOTIFY indexNumberEndChanged) Q_PROPERTY(bool isFolder READ isFolder NOTIFY isFolderChanged) Q_PROPERTY(QString type READ type NOTIFY typeChanged) - Q_PROPERTY(QString parentBackdropItemId READ parentBackdropItemId NOTIFY parentBackdropItemIdChanged) + /*Q_PROPERTY(QString parentBackdropItemId READ parentBackdropItemId NOTIFY parentBackdropItemIdChanged) Q_PROPERTY(QStringList parentBackdropImageTags READ parentBackdropImageTags NOTIFY parentBackdropImageTagsChanged) Q_PROPERTY(UserData *userData READ userData NOTIFY userDataChanged) Q_PROPERTY(int recursiveItemCount READ recursiveItemCount NOTIFY recursiveItemCountChanged) @@ -125,6 +125,12 @@ public: int airsBeforeSeasonNumber() const { return m_data->airsBeforeSeasonNumber().value_or(0); } int airsAfterSeasonNumber() const { return m_data->airsAfterSeasonNumber().value_or(999); } int airsBeforeEpisodeNumber() const { return m_data->airsBeforeEpisodeNumber().value_or(0); } + QString overview() const { return m_data->overview(); } + int productionYear() const { return m_data->productionYear().value_or(0); } + int indexNumber() const { return m_data->indexNumber().value_or(-1); } + int indexNumberEnd() const { return m_data->indexNumberEnd().value_or(-1); } + bool isFolder() const { return m_data->isFolder().value_or(false); } + QString type() const { return m_data->type(); } QSharedPointer data() const { return m_data; } void setData(QSharedPointer newData); @@ -192,7 +198,7 @@ public: Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged) QString itemId() const { return m_parameters.itemId(); } - void setItemId(QString newItemId) { m_parameters.setItemId(newItemId); emit itemIdChanged(newItemId); } + void setItemId(QString newItemId); virtual bool canReload() const override; signals: diff --git a/core/include/JellyfinQt/viewmodel/itemmodel.h b/core/include/JellyfinQt/viewmodel/itemmodel.h index 805b9be..330931c 100644 --- a/core/include/JellyfinQt/viewmodel/itemmodel.h +++ b/core/include/JellyfinQt/viewmodel/itemmodel.h @@ -163,6 +163,10 @@ public: // Hand-picked, important ones imageTags, + imageBlurHashes, + mediaType, + type, + collectionType, jellyfinExtendModelAfterHere = Qt::UserRole + 300 // Should be enough for now }; @@ -183,9 +187,14 @@ public: JFRN(extraType), // Handpicked, important ones JFRN(imageTags), + JFRN(imageBlurHashes), + JFRN(mediaType), + JFRN(type), + JFRN(collectionType), }; } QVariant data(const QModelIndex &index, int role) const override; + QSharedPointer itemAt(int index); }; /*class UserItemModel : public ItemModel { diff --git a/core/include/JellyfinQt/viewmodel/loader.h b/core/include/JellyfinQt/viewmodel/loader.h index 3ed7f9d..7803ff2 100644 --- a/core/include/JellyfinQt/viewmodel/loader.h +++ b/core/include/JellyfinQt/viewmodel/loader.h @@ -110,15 +110,19 @@ protected: void setError(QNetworkReply::NetworkError error); void setErrorString(const QString &newErrorString); + + void reloadIfNeeded() { + if (canReload()) { + reload(); + } + } void classBegin() override { m_isParsing = true; } void componentComplete() override { m_isParsing = false; - if (canReload()) { - reload(); - } + reloadIfNeeded(); } ApiClient *m_apiClient = nullptr; protected: @@ -157,7 +161,7 @@ public: } T *dataViewModel() const { return m_dataViewModel; } - QObject *data() const { return m_dataViewModel; } + QObject *data() const override { return m_dataViewModel; } void reload() override { if (m_futureWatcher->isRunning()) return; diff --git a/core/include/JellyfinQt/viewmodel/playbackmanager.h b/core/include/JellyfinQt/viewmodel/playbackmanager.h index b901a03..e11b552 100644 --- a/core/include/JellyfinQt/viewmodel/playbackmanager.h +++ b/core/include/JellyfinQt/viewmodel/playbackmanager.h @@ -34,9 +34,13 @@ #include #include "../dto/baseitemdto.h" +#include "../dto/playbackinfodto.h" +#include "../dto/playmethod.h" +#include "../loader/requesttypes.h" +#include "../loader/http/getpostedplaybackinfo.h" +#include "../model/playlist.h" #include "../support/jsonconv.h" #include "../viewmodel/item.h" - #include "../apiclient.h" #include "itemmodel.h" @@ -50,6 +54,9 @@ class RemoteItem; namespace ViewModel { +// Later defined in this file +class ItemUrlFetcherThread; + /** * @brief The PlaybackManager class manages the playback of Jellyfin items. It fetches streams based on Jellyfin items, posts * the current playback state to the Jellyfin Server, contains the actual media player and so on. @@ -58,15 +65,10 @@ namespace ViewModel { * preloading the next item in the queue. The current media player is pointed to by m_mediaPlayer. */ class PlaybackManager : public QObject, public QQmlParserStatus { + friend class ItemUrlFetcherThread; Q_OBJECT Q_INTERFACES(QQmlParserStatus) public: - enum PlayMethod { - Transcode, - Stream, - DirectPlay - }; - Q_ENUM(PlayMethod) using FetchCallback = std::function; explicit PlaybackManager(QObject *parent = nullptr); @@ -77,11 +79,11 @@ public: Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged) Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged) Q_PROPERTY(bool resumePlayback MEMBER m_resumePlayback NOTIFY resumePlaybackChanged) - Q_PROPERTY(PlayMethod playMethod READ playMethod NOTIFY playMethodChanged) + Q_PROPERTY(Jellyfin::DTO::PlayMethodClass::Value playMethod READ playMethod NOTIFY playMethodChanged) // Current Item and queue informatoion - Q_PROPERTY(ViewModel::Item *item READ item NOTIFY itemChanged) - Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged) + Q_PROPERTY(QObject *item READ item NOTIFY itemChanged) + // Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged) Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged) // Current media player related property getters @@ -102,7 +104,7 @@ public: QObject *mediaObject() const { return m_mediaPlayer; } qint64 position() const { return m_mediaPlayer->position(); } qint64 duration() const { return m_mediaPlayer->duration(); } - ItemModel *queue() const { return m_queue; } + //ItemModel *queue() const { return m_queue; } int queueIndex() const { return m_queueIndex; } // Current media player related property getters @@ -112,7 +114,7 @@ public: QMediaPlayer::Error error () const { return m_mediaPlayer->error(); } QString errorString() const { return m_mediaPlayer->errorString(); } signals: - void itemChanged(BaseItemDto *newItemId); + void itemChanged(ViewModel::Item *newItemId); void streamUrlChanged(const QString &newStreamUrl); void autoOpenChanged(bool autoOpen); void audioIndexChanged(int audioIndex); @@ -125,7 +127,7 @@ signals: void mediaObjectChanged(QObject *newMediaObject); void positionChanged(qint64 newPosition); void durationChanged(qint64 newDuration); - void queueChanged(ItemModel *newQue); + //void queueChanged(ItemModel *newQue); void queueIndexChanged(int newIndex); void playbackStateChanged(QMediaPlayer::State newState); void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus); @@ -138,9 +140,9 @@ public slots: * * This will construct the Jellyfin::Item internally * and delete it later. - * @param itemId The id of the item to play. + * @param item The item to play. */ - void playItem(const QString &itemId); + void playItem(Item *item); void playItemInList(ItemModel *itemList, int index); void play() { m_mediaPlayer->play(); } void pause() { m_mediaPlayer->pause(); } @@ -162,24 +164,43 @@ private slots: void mediaPlayerPositionChanged(qint64 position); void mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus); void mediaPlayerError(QMediaPlayer::Error error); + void mediaPlayerDurationChanged(qint64 newDuration); /** * @brief updatePlaybackInfo Updates the Jellyfin server with the current playback progress etc. */ void updatePlaybackInfo(); + /// Called when the fetcherThread has fetched the playback URL and playSession + void onItemExtraDataReceived(const QString &itemId, const QUrl &url, const QString &playSession, + // Fully specify class to please MOC + Jellyfin::DTO::PlayMethodClass::Value playMethod); + /// Called when the fetcherThread encountered an error + void onItemErrorReceived(const QString &itemId, const QString &errorString); + void onDestroyed(); + private: /// Factor to multiply with when converting from milliseconds to ticks. const static int MS_TICK_FACTOR = 10000; enum PlaybackInfoType { Started, Stopped, Progress }; + /// Timer used to update the play progress on the Jellyfin server QTimer m_updateTimer; + /// Timer used to notify ourselves when we need to preload the next item + QTimer m_preloadTimer; + ApiClient *m_apiClient = nullptr; + /// The currently playing item QSharedPointer m_item; + /// The item that will be played next + QSharedPointer m_nextItem; + /// The currently played item that will be shown in the GUI ViewModel::Item *m_displayItem = new ViewModel::Item(this); // Properties for making the streaming request. QString m_streamUrl; + QString m_nextStreamUrl; QString m_playSessionId; + QString m_nextPlaySessionId; /// The index of the mediastreams of the to-be-played item containing the audio int m_audioIndex = 0; /// The index of the mediastreams of the to-be-played item containing subtitles @@ -196,10 +217,11 @@ private: */ bool m_autoOpen = false; - // Playback-related members + ItemUrlFetcherThread *m_urlFetcherThread; + QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState; - PlayMethod m_playMethod = Transcode; + PlayMethod m_playMethod = PlayMethod::Transcode; QMediaPlayer::State m_playbackState = QMediaPlayer::StoppedState; /// Pointer to the current media player. QMediaPlayer *m_mediaPlayer = nullptr; @@ -211,28 +233,18 @@ private: QMediaPlayer *m_mediaPlayer1; /// Media player 2 QMediaPlayer *m_mediaPlayer2; - ItemModel *m_queue = nullptr; + + Model::Playlist *m_queue = nullptr; int m_queueIndex = 0; bool m_resumePlayback = true; // Helper methods - void setItem(ViewModel::Item *newItem); + void setItem(QSharedPointer newItem); void swapMediaPlayer(); - - /** - * @brief Retrieves the URL of the stream to open. - */ - void fetchStreamUrl(const Model::Item *item, bool autoOpen, const FetchCallback &callback); - void fetchAndSetStreamUrl(const Model::Item *item); - void setStreamUrl(const QString &streamUrl); + void setStreamUrl(const QUrl &streamUrl); void setPlaybackState(QMediaPlayer::State newState); - Model::Item *nextItem(); - void setQueue(ItemModel *itemModel); - - - /** * @brief Posts the playback information */ @@ -243,6 +255,60 @@ private: void classBegin() override { m_qmlIsParsingComponent = true; } void componentComplete() override; bool m_qmlIsParsingComponent = false; + + /// Time in ms at what moment this playbackmanager should start loading the next item. + const qint64 PRELOAD_DURATION = 15 * 1000; +}; + +/// Thread that fetches the Item's stream URL always in the given order they were requested +class ItemUrlFetcherThread : public QThread { + Q_OBJECT +public: + ItemUrlFetcherThread(PlaybackManager *manager); + + /** + * @brief Adds an item to the queue of items that should be requested + * @param item The item to fetch the URL of + */ + void addItemToQueue(QSharedPointer item); + +signals: + /** + * @brief Emitted when the url of the item with the itemId has been retrieved. + * @param itemId The id of the item of which the URL has been retrieved + * @param itemUrl The retrieved url + * @param playSession The playsession set by the Jellyfin Server + */ + void itemUrlFetched(QString itemId, QUrl itemUrl, QString playSession, Jellyfin::DTO::PlayMethodClass::Value playMethod); + void itemUrlFetchError(QString itemId, QString errorString); + + void prepareLoaderRequested(QPrivateSignal); +public slots: + /** + * @brief Ask the thread nicely to stop running. + */ + void cleanlyStop(); +private slots: + void onPrepareLoader(); +protected: + void run() override; +private: + PlaybackManager *m_parent; + Support::Loader *m_loader; + + QMutex m_queueModifyMutex; + QQueue> m_queue; + + QMutex m_urlWaitConditionMutex; + /// WaitCondition on which this threads waits until an Item is put into the queue + QWaitCondition m_urlWaitCondition; + + QMutex m_waitLoaderPreparedMutex; + /// WaitCondition on which this threads waits until the loader has been prepared. + QWaitCondition m_waitLoaderPrepared; + + bool m_keepRunning = true; + bool m_loaderPrepared = false; }; } // NS ViewModel diff --git a/core/include/JellyfinQt/viewmodel/playlist.h b/core/include/JellyfinQt/viewmodel/playlist.h index 6e98cd0..7e43d92 100644 --- a/core/include/JellyfinQt/viewmodel/playlist.h +++ b/core/include/JellyfinQt/viewmodel/playlist.h @@ -29,109 +29,30 @@ #include #include -#include "../dto/playbackinfodto.h" -#include "../loader/requesttypes.h" -#include "../loader/http/getpostedplaybackinfo.h" +#include "../apiclient.h" #include "itemmodel.h" namespace Jellyfin { namespace ViewModel { -class ItemUrlFetcherThread; - - /** * @brief Playlist/queue that can be exposed to the UI. It also containts the playlist-related logic, * which is mostly relevant */ -class Playlist : public ItemModel { +/*class Playlist : public ItemModel { Q_OBJECT friend class ItemUrlFetcherThread; public: explicit Playlist(ApiClient *apiClient, QObject *parent = nullptr); - enum ExtraRoles { - Url = ItemModel::RoleNames::jellyfinExtendModelAfterHere + 1, - PlaySession, - ErrorText - }; - - QHash roleNames() const override { - QHash result(ItemModel::roleNames()); - result.insert(Url, "url"); - result.insert(PlaySession, "playSession"); - result.insert(ErrorText, "errorText"); - return result; - } private slots: void onItemsAdded(const QModelIndex &parent, int startIndex, int endIndex); void onItemsMoved(const QModelIndex &parent, int startIndex, int endIndex, const QModelIndex &destination, int destinationRow); void onItemsRemoved(const QModelIndex &parent, int startIndex, int endIndex); void onItemsReset(); - /// Called when the fetcherThread has fetched the playback URL and playSession - void onItemExtraDataReceived(const QString &itemId, const QUrl &url, const QString &playSession); - /// Called when the fetcherThread encountered an error - void onItemErrorReceived(const QString &itemId, const QString &errorString); -private: - /// Map from ItemId to ExtraData - QHash m_cache; +};*/ - ApiClient *m_apiClient; - /// Thread that fetches the URLS asynchronously - ItemUrlFetcherThread *m_fetcherThread; -}; - -/// Thread that fetches the Item's stream URL always in the given order they were requested -class ItemUrlFetcherThread : public QThread { - Q_OBJECT -public: - ItemUrlFetcherThread(Playlist *playlist); - - /** - * @brief Adds an item to the queue of items that should be requested - * @param item The item to fetch the URL of - */ - void addItemToQueue(const Model::Item item); - -signals: - /** - * @brief Emitted when the url of the item with the itemId has been retrieved. - * @param itemId The id of the item of which the URL has been retrieved - * @param itemUrl The retrieved url - * @param playSession The playsession set by the Jellyfin Server - */ - void itemUrlFetched(QString itemId, QUrl itemUrl, QString playSession); - void itemUrlFetchError(QString itemId, QString errorString); - - void prepareLoaderRequested(QPrivateSignal); -public slots: - /** - * @brief Ask the thread nicely to stop running. - */ - void cleanlyStop(); -private slots: - void onPrepareLoader(); -protected: - void run() override; -private: - Playlist *m_parent; - Support::Loader *m_loader; - - QMutex m_queueModifyMutex; - QQueue m_queue; - - QMutex m_urlWaitConditionMutex; - /// WaitCondition on which this threads waits until an Item is put into the queue - QWaitCondition m_urlWaitCondition; - - QMutex m_waitLoaderPreparedMutex; - /// WaitCondition on which this threads waits until the loader has been prepared. - QWaitCondition m_waitLoaderPrepared; - - bool m_keepRunning = true; - bool m_loaderPrepared = false; -}; } // NS ViewModel } // NS Jellyfin diff --git a/core/openapigenerator.d b/core/openapigenerator.d index 94f8c00..7b06767 100755 --- a/core/openapigenerator.d +++ b/core/openapigenerator.d @@ -944,7 +944,7 @@ public: string typeNameWithQualifiers() { if (needsPointer) { - return "QSharedPointer<" ~ typeName~ ">"; + return "QSharedPointer<" ~ typeName ~ ">"; } if (needsOptional) { return "std::optional<" ~ typeName ~ ">"; @@ -977,7 +977,7 @@ public: } string defaultInitializer() { - if (needsPointer) return "nullptr"; + if (needsPointer) return "QSharedPointer<" ~ typeName ~ ">()"; if (needsOptional) return "std::nullopt"; return ""; } diff --git a/core/src/apiclient.cpp b/core/src/apiclient.cpp index 541b4db..7b89285 100644 --- a/core/src/apiclient.cpp +++ b/core/src/apiclient.cpp @@ -31,10 +31,10 @@ ApiClient::ApiClient(QObject *parent) m_eventbus(new EventBus(this)), m_deviceName(QHostInfo::localHostName()) { - m_deviceId = Support::uuidToString(retrieveDeviceId()); - + m_deviceId = Support::toString(retrieveDeviceId()); m_credManager = CredentialsManager::newInstance(this); - + connect(m_credManager, &CredentialsManager::serversListed, this, &ApiClient::credManagerServersListed); + connect(m_credManager, &CredentialsManager::usersListed, this, &ApiClient::credManagerUsersListed); generateDeviceProfile(); } @@ -92,49 +92,48 @@ QNetworkReply *ApiClient::post(const QString &path, const QJsonDocument &data, c //////////////////////////////////////////////////////////////////////////////////////////////////// void ApiClient::restoreSavedSession(){ - QObject *ctx1 = new QObject(this); - connect(m_credManager, &CredentialsManager::serversListed, ctx1, [this, ctx1](const QStringList &servers) { - qDebug() << "Servers listed: " << servers; - if (servers.size() == 0) { - emit this->setupRequired(); - return; - } - - //FIXME: support multiple servers - QString server = servers[0]; - this->m_baseUrl = server; - qDebug() << "Server: " << server; - QObject *ctx2 = new QObject(this); - connect(m_credManager, &CredentialsManager::usersListed, ctx2, [this, server, ctx2](const QStringList &users) { - if (users.size() == 0) { - emit this->setupRequired(); - return; - } - //FIXME: support multiple users - QString user = users[0]; - qDebug() << "User: " << user; - - QObject *ctx3 = new QObject(this); - connect(m_credManager, &CredentialsManager::tokenRetrieved, ctx3, [this, ctx3] - (const QString &server, const QString &user, const QString &token) { - Q_UNUSED(server) - this->m_token = token; - this->setUserId(user); - this->setAuthenticated(true); - this->postCapabilities(); - disconnect(ctx3); - }, Qt::UniqueConnection); - m_credManager->get(server, user); - delete ctx2; - }, Qt::UniqueConnection); - m_credManager->listUsers(server); - qDebug() << "Listing users"; - delete ctx1; - }, Qt::UniqueConnection); - qDebug() << "Listing servers"; m_credManager->listServers(); } +void ApiClient::credManagerServersListed(QStringList servers) { + qDebug() << "Servers listed: " << servers; + if (servers.size() == 0) { + emit this->setupRequired(); + return; + } + + //FIXME: support multiple servers + QString server = servers[0]; + this->m_baseUrl = server; + qDebug() << "Chosen server: " << server; + m_credManager->listUsers(server); +} + +void ApiClient::credManagerUsersListed(const QString &server, QStringList users) { + if (users.size() == 0) { + emit this->setupRequired(); + return; + } + //FIXME: support multiple users + QString user = users[0]; + qDebug() << "Chosen user: " << user; + + QObject *ctx3 = new QObject(this); + connect(m_credManager, &CredentialsManager::tokenRetrieved, ctx3, [this, ctx3] + (const QString &server, const QString &user, const QString &token) { + Q_UNUSED(server) + this->m_token = token; + this->setUserId(user); + this->setAuthenticated(true); + this->postCapabilities(); + disconnect(ctx3); + }, Qt::UniqueConnection); + m_credManager->get(server, user); +} +void ApiClient::credManagerTokenRetrieved(const QString &server, const QString &user, const QString &token) { + +} + void ApiClient::setupConnection() { // First detect redirects: // Note that this is done without calling JellyfinApiClient::get since that automatically includes the base_url, diff --git a/core/src/apimodel.cpp b/core/src/apimodel.cpp index 90e0839..d47407f 100644 --- a/core/src/apimodel.cpp +++ b/core/src/apimodel.cpp @@ -16,7 +16,7 @@ You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ - +#define JELLYFIN_APIMODEL_CPP #include "JellyfinQt/apimodel.h" #include "JellyfinQt/dto/baseitemdto.h" diff --git a/core/src/credentialmanager.cpp b/core/src/credentialmanager.cpp index 63aea27..e307aa6 100644 --- a/core/src/credentialmanager.cpp +++ b/core/src/credentialmanager.cpp @@ -87,5 +87,5 @@ void FallbackCredentialsManager::listUsers(const QString &server) { qDebug() << "Users: " << users; m_settings.endGroup(); m_settings.endGroup(); - emit CredentialsManager::usersListed(users); + emit CredentialsManager::usersListed(server, users); } diff --git a/core/src/jellyfin.cpp b/core/src/jellyfin.cpp index 2e6faee..e47c5e9 100644 --- a/core/src/jellyfin.cpp +++ b/core/src/jellyfin.cpp @@ -40,8 +40,10 @@ void registerTypes(const char *uri) { qmlRegisterType(uri, 1, 0, "UsersViewsLoader"); // Enumerations - qmlRegisterUncreatableType(uri, 1, 0, "GeneralCommandType", "Is an enum"); - qmlRegisterUncreatableType(uri, 1, 0, "ModelStatus", "Is an enum"); + qmlRegisterUncreatableType(uri, 1, 0, "GeneralCommandType", "Is an enum"); + qmlRegisterUncreatableType(uri, 1, 0, "ModelStatus", "Is an enum"); + qmlRegisterUncreatableType(uri, 1, 0, "PlayMethod", "Is an enum"); + qRegisterMetaType(); } } diff --git a/core/src/model/playlist.cpp b/core/src/model/playlist.cpp index e69de29..06f5ad4 100644 --- a/core/src/model/playlist.cpp +++ b/core/src/model/playlist.cpp @@ -0,0 +1,148 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#include "JellyfinQt/model/playlist.h" + +#include "JellyfinQt/model/shuffle.h" + +namespace Jellyfin { +namespace Model { + +Playlist::Playlist(QObject *parent) + : QObject(parent), + m_shuffler(new NoShuffle(this)){} + +void Playlist::clearList() { + m_currentItem.clear(); + m_nextItem.clear(); + m_list.clear(); + emit listCleared(); +} + +void Playlist::previous() { + m_shuffler->previous(); + int curItem = m_shuffler->currentItem(); + if (curItem >= 0) { + m_currentItem = m_list[curItem]; + } else { + m_currentItem.clear(); + } + int nextItem = m_shuffler->nextItem(); + if (nextItem) { + m_nextItem = m_list[m_shuffler->nextItem()]; + } else { + m_nextItem.clear(); + } + m_nextItemFromQueue = !m_queue.isEmpty(); + m_currentItemFromQueue = false; +} + +void Playlist::next() { + // Determine the new current item + if (!m_queue.isEmpty()) { + m_currentItem = m_queue.first(); + m_queue.removeFirst(); + m_currentItemFromQueue = true; + } else if (!m_list.isEmpty()) { + // The queue is empty + m_currentItemFromQueue = false; + m_shuffler->next(); + int next = m_shuffler->currentItem(); + if (next >= 0) { + m_currentItem = m_list[next]; + } else { + m_currentItem.clear(); + } + } else { + m_currentItem.clear(); + } + + // Determine the new next item + if (!m_queue.isEmpty()) { + m_nextItem = m_queue.first(); + m_queue.removeFirst(); + m_nextItemFromQueue = true; + } else if (!m_list.isEmpty()) { + // The queue is empty + m_nextItemFromQueue = false; + int next = m_shuffler->nextItem(); + if (next >= 0) { + m_nextItem = m_list[next]; + } else { + m_nextItem.clear(); + } + } else { + m_nextItem.clear(); + } +} + +QSharedPointer Playlist::listAt(int index) const { + return m_list.at(index); +} + +QSharedPointer Playlist::currentItem() { + return m_currentItem; +} + +QSharedPointer Playlist::nextItem() { + return m_nextItem; +} + +void Playlist::appendToList(const ViewModel::ItemModel &model) { + int start = m_list.size(); + int count = model.size(); + m_list.reserve(count); + for (int i = 0; i < count; i++) { + m_list.append(QSharedPointer::create(model.at(i))); + } + emit itemsAddedToList(start, count); + reshuffle(); +} + +void Playlist::reshuffle() { + if (m_shuffler->canShuffleInAdvance()) { + m_shuffler->shuffleInAdvance(); + } else { + m_shuffler->next(); + } + + if (!m_nextItemFromQueue) { + int nextItemIdx = m_shuffler->nextItem(); + if (nextItemIdx >= 0) { + m_nextItem = m_list[m_shuffler->nextItem()]; + } else { + m_nextItem.clear(); + } + } + emit listReshuffled(); +} + +void Playlist::play(int index) { + m_shuffler->setIndex(index); + if (!m_nextItemFromQueue) { + int nextItemIdx = m_shuffler->nextItem(); + if (nextItemIdx >= 0) { + m_nextItem = m_list[m_shuffler->nextItem()]; + } else { + m_nextItem.clear(); + } + } +} + +} // NS Model +} // NS Jellyfin diff --git a/core/src/model/shuffle.cpp b/core/src/model/shuffle.cpp new file mode 100644 index 0000000..25d816c --- /dev/null +++ b/core/src/model/shuffle.cpp @@ -0,0 +1,146 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#include "JellyfinQt/model/shuffle.h" + +#if QT_VERSION > QT_VERSION_CHECK(5, 10, 0) +#include +#endif + +namespace Jellyfin { +namespace Model { + +int Shuffle::random(int max, int min) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + return QRandomGenerator::global()->bounded(min, max); +#else + return static_cast(min + static_cast(max - min) / static_cast(RAND_MAX) * qrand()); +#endif +} + +NoShuffle::NoShuffle(const Playlist *parent) + : Shuffle(parent) {} + +void NoShuffle::next() { + m_index = nextIndex(); +} + +void NoShuffle::previous() { + m_index = previousIndex(); +} + +int NoShuffle::currentItem() const { + return m_index; +} +int NoShuffle::nextItem() const { + return nextIndex(); +} + +int NoShuffle::previousIndex() const { + if (m_repeatAll) { + return (m_index - 1) % m_playlist->listSize(); + } else { + if (m_index - 1 < 0) { + return -1; + } else { + return m_index - 1; + } + } +} + +int NoShuffle::nextIndex() const { + if (m_repeatAll) { + return (m_index + 1) % m_playlist->listSize(); + } else { + if (m_index + 1 >= m_playlist->listSize()) { + return -1; + } else { + return m_index + 1; + } + } +} + +void NoShuffle::setIndex(int i) { + m_index = i; +} + +// ListShuffleBase +ListShuffleBase::ListShuffleBase(const Playlist *parent) + : NoShuffle(parent) {} + +int ListShuffleBase::currentItem() const { + return m_map[m_index]; +} + +int ListShuffleBase::nextItem() const { + return m_map[nextIndex()]; +} + +// SimpleListShuffle +SimpleListShuffle::SimpleListShuffle(const Playlist *parent) + : ListShuffleBase(parent) {} + +void SimpleListShuffle::shuffleInAdvance() { + int count = m_playlist->listSize(); + m_map.clear(); + m_map.reserve(count); + for (int i = 0; i < count; i++) { + m_map.append(i); + } + + // Swap the list + for (int i = 0; i < count; i++) { + int tmp = m_map[i]; + int other = random(count, i); + m_map[i] = m_map[other]; + m_map[other] = tmp; + } + +} + +// RandomShuffle +RandomShuffle::RandomShuffle(const Playlist *parent) + : Shuffle(parent) {} + +bool RandomShuffle::canShuffleInAdvance() { + return false; +} + +int RandomShuffle::currentItem() const { + return m_current; +}; + +int RandomShuffle::nextItem() const { + return m_next; +} +void RandomShuffle::previous() { + m_next = m_current; + m_current = m_previous; + m_previous = random(m_playlist->listSize()); +} +void RandomShuffle::next() { + m_previous = m_current; + if (m_next == -1) { + m_next = random(m_playlist->listSize()); + } + m_current = m_next; + m_next = random(m_playlist->listSize()); +} + +} // NS Model +} // NS Jellyfin diff --git a/core/src/support/jsonconv.cpp b/core/src/support/jsonconv.cpp index f6a959e..9710b49 100644 --- a/core/src/support/jsonconv.cpp +++ b/core/src/support/jsonconv.cpp @@ -16,25 +16,14 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#include "JellyfinQt/support/jsonconv.h" +#include "JellyfinQt/support/jsonconvimpl.h" +#include "JellyfinQt/support/parseexception.h" #include namespace Jellyfin { namespace Support { -const char * ParseException::what() const noexcept { - return m_message.c_str(); -} - -QException *ParseException::clone() const { - return new ParseException(*this); -} - -void ParseException::raise() const { - throw *this; -} - QString uuidToString(const QUuid &source) { QString str = source.toString(); // Convert {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} (length: 38) @@ -195,7 +184,8 @@ QDateTime fromJsonValue(const QJsonValue &source, convertType(const QJsonValue &source, convertType QJsonValue toJsonValue(const QDateTime &source, convertType) { - return QJsonValue(source.toString(Qt::ISODateWithMs)); + return QJsonValue(source.toString(Qt::ISODate)); +} + +// QVariant +template <> +QVariant fromJsonValue(const QJsonValue &source, convertType) { + return source.toVariant(); +} + +template<> +QJsonValue toJsonValue(const QVariant &source, convertType) { + return QJsonValue::fromVariant(source); } // QUuid diff --git a/core/src/support/parseexception.cpp b/core/src/support/parseexception.cpp new file mode 100644 index 0000000..524e7ba --- /dev/null +++ b/core/src/support/parseexception.cpp @@ -0,0 +1,38 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "JellyfinQt/support/parseexception.h" + +namespace Jellyfin { +namespace Support { + +const char * ParseException::what() const noexcept { + return m_message.c_str(); +} + +QException *ParseException::clone() const { + return new ParseException(*this); +} + +void ParseException::raise() const { + throw *this; +} + +} // NS Support +} // NS Jellyfin diff --git a/core/src/viewmodel/item.cpp b/core/src/viewmodel/item.cpp index b7a673c..0dc0dc4 100644 --- a/core/src/viewmodel/item.cpp +++ b/core/src/viewmodel/item.cpp @@ -27,7 +27,6 @@ Item::Item(QObject *parent, QSharedPointer data) } void Item::setData(QSharedPointer newData) { - Model::Item oldData = *m_data.data(); m_data = newData; } @@ -44,21 +43,32 @@ void ItemLoader::onApiClientChanged(ApiClient *newApiClient) { disconnect(m_apiClient, &ApiClient::userIdChanged, this, &ItemLoader::setUserId); } if (newApiClient != nullptr) { - m_parameters.setUserId(newApiClient->userId()); connect(newApiClient, &ApiClient::userIdChanged, this, &ItemLoader::setUserId); + setUserId(newApiClient->userId()); } } void ItemLoader::setUserId(const QString &newUserId) { - m_parameters.setUserId(newUserId); + qDebug() << "ItemLoader: userId set to " << m_apiClient->userId(); + m_parameters.setUserId(m_apiClient->userId()); + qDebug() << "New userId: " << m_parameters.userId(); + reloadIfNeeded(); } bool ItemLoader::canReload() const { + qDebug() << "ItemLoader::canReload(): baseClass=" << BaseClass::canReload() << ", itemId=" << m_parameters.userId() << ", userId=" << m_parameters.userId(); return BaseClass::canReload() && !m_parameters.itemId().isEmpty() && !m_parameters.userId().isEmpty(); } +void ItemLoader::setItemId(QString newItemId) { + qDebug() << "ItemLoader: itemId set to " << newItemId; + m_parameters.setItemId(newItemId); + qDebug() << "New itemId: " << m_parameters.itemId(); + emit itemIdChanged(newItemId); + reloadIfNeeded(); +} } } diff --git a/core/src/viewmodel/itemmodel.cpp b/core/src/viewmodel/itemmodel.cpp index 36d0ba1..d45c372 100644 --- a/core/src/viewmodel/itemmodel.cpp +++ b/core/src/viewmodel/itemmodel.cpp @@ -24,7 +24,7 @@ #define JF_CASE(roleName) case roleName: \ try { \ return QVariant(item.roleName()); \ - } catch(std::bad_optional_access e) { \ + } catch(std::bad_optional_access &e) { \ return QVariant(); \ } @@ -62,12 +62,20 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const { JF_CASE(extraType) // Handpicked, important ones JF_CASE(imageTags) + JF_CASE(imageBlurHashes) + JF_CASE(mediaType) + JF_CASE(type) + JF_CASE(collectionType) default: return QVariant(); } } +QSharedPointer ItemModel::itemAt(int index) { + return QSharedPointer::create(m_array[index]); +} + } // NS ViewModel } // NS Jellyfin diff --git a/core/src/viewmodel/playbackmanager.cpp b/core/src/viewmodel/playbackmanager.cpp index 13ba72a..f5522f8 100644 --- a/core/src/viewmodel/playbackmanager.cpp +++ b/core/src/viewmodel/playbackmanager.cpp @@ -23,6 +23,7 @@ // #include "JellyfinQt/DTO/dto.h" #include +#include namespace Jellyfin { class ItemModel; @@ -37,125 +38,67 @@ PlaybackManager::PlaybackManager(QObject *parent) : QObject(parent), m_item(nullptr), m_mediaPlayer1(new QMediaPlayer(this)), - m_mediaPlayer2(new QMediaPlayer(this)) { + m_mediaPlayer2(new QMediaPlayer(this)), + m_urlFetcherThread(new ItemUrlFetcherThread(this)), + m_queue(new Model::Playlist(this)) { // Set up connections. swapMediaPlayer(); m_updateTimer.setInterval(10000); // 10 seconds m_updateTimer.setSingleShot(false); + + m_preloadTimer.setSingleShot(true); + + connect(this, &QObject::destroyed, this, &PlaybackManager::onDestroyed); connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo); + connect(m_urlFetcherThread, &ItemUrlFetcherThread::itemUrlFetched, this, &PlaybackManager::onItemExtraDataReceived); + m_urlFetcherThread->start(); +} + +void PlaybackManager::onDestroyed() { + m_urlFetcherThread->cleanlyStop(); } void PlaybackManager::setApiClient(ApiClient *apiClient) { - m_item->setApiClient(apiClient); -} - -void PlaybackManager::fetchStreamUrl(const Model::Item *item, bool autoOpen, const FetchCallback &callback) { - if (item == nullptr || m_apiClient == nullptr) { - qDebug() << "Item or apiClient not set"; - return; + if (!m_item.isNull()) { + m_item->setApiClient(apiClient); } - QString itemId(item->jellyfinId()); - m_resumePosition = 0; - if (m_resumePlayback && !item->userData().isNull()) { - QSharedPointer userData = m_item->userData(); - if (!userData.isNull()) { - m_resumePosition = userData->playbackPositionTicks(); - } - } - QUrlQuery params; - params.addQueryItem("UserId", m_apiClient->userId()); - params.addQueryItem("StartTimeTicks", QString::number(m_resumePosition)); - params.addQueryItem("IsPlayback", "true"); - params.addQueryItem("AutoOpenLiveStream", autoOpen? "true" : "false"); - params.addQueryItem("MediaSourceId", itemId); - params.addQueryItem("SubtitleStreamIndex", QString::number(m_subtitleIndex)); - params.addQueryItem("AudioStreamIndex", QString::number(m_audioIndex)); - - QJsonObject root; - root["DeviceProfile"] = m_apiClient->playbackDeviceProfile(); - - QNetworkReply *rep = m_apiClient->post("/Items/" + itemId + "/PlaybackInfo", QJsonDocument(root), params); - connect(rep, &QNetworkReply::finished, this, [this, rep, callback, itemId]() { - QJsonObject root = QJsonDocument::fromJson(rep->readAll()).object(); - this->m_playSessionId = root["PlaySessionId"].toString(); - qDebug() << "Session id: " << this->m_playSessionId; - - if (this->m_autoOpen) { - QJsonArray mediaSources = root["MediaSources"].toArray(); - QJsonObject firstMediaSource = mediaSources[0].toObject(); - //FIXME: relies on the fact that the returned transcode url always has at least one result! - if (firstMediaSource.isEmpty()) { - qWarning() << "No media source found"; - } else if (firstMediaSource["SupportsDirectStream"].toBool()) { - QUrlQuery query; - query.addQueryItem("mediaSourceId", firstMediaSource["Id"].toString()); - query.addQueryItem("deviceId", m_apiClient->deviceId()); - query.addQueryItem("api_key", m_apiClient->token()); - query.addQueryItem("Static", "True"); - QString mediaType = "unknown"; - if (m_item->mediaType() == "Audio") { - mediaType = "Audio"; - } else if (m_item->mediaType() == "Video") { - mediaType = "Videos"; - } - QString streamUrl = this->m_apiClient->baseUrl() + "/" + mediaType + "/" + itemId + "/stream." - + firstMediaSource["Container"].toString() + "?" + query.toString(QUrl::EncodeReserved); - callback(QUrl(streamUrl), DirectPlay); - } else if (firstMediaSource["SupportsTranscoding"].toBool() && !firstMediaSource["TranscodingUrl"].isNull()) { - QString streamUrl = this->m_apiClient->baseUrl() - + firstMediaSource["TranscodingUrl"].toString(); - - this->m_playMethod = Transcode; - callback(QUrl(streamUrl), Transcode); - } else { - qDebug() << "No stream url found"; - return; - } - } - rep->deleteLater(); - }); + m_apiClient = apiClient; } -void PlaybackManager::fetchAndSetStreamUrl(const Model::Item *item) { - fetchStreamUrl(item, m_autoOpen, [this, item](QUrl &&url, PlayMethod playbackMethod) { - if (m_item == item) { - setStreamUrl(url.toString()); - m_playMethod = playbackMethod; - emit playMethodChanged(m_playMethod); - m_mediaPlayer->setMedia(QMediaContent(url)); - m_mediaPlayer->play(); - } - }); - -} - -void PlaybackManager::setItem(ViewModel::Item *newItem) { +void PlaybackManager::setItem(QSharedPointer newItem) { if (m_mediaPlayer != nullptr) m_mediaPlayer->stop(); + bool shouldFetchStreamUrl = !newItem.isNull() + && ((m_streamUrl.isEmpty() || (!m_item.isNull() + && m_item->jellyfinId() != newItem->jellyfinId())) + || (m_nextStreamUrl.isEmpty() || (!m_nextItem.isNull() + && m_nextItem->jellyfinId() != newItem->jellyfinId()))); + this->m_item = newItem; - if (newItem != nullptr) { - this->m_item = newItem->data(); + if (newItem.isNull()) { + m_displayItem->setData(QSharedPointer::create()); + } else { + m_displayItem->setData(newItem); } - //emit itemChanged(newItem); + emit itemChanged(m_displayItem); if (m_apiClient == nullptr) { qWarning() << "apiClient is not set on this MediaSource instance! Aborting."; return; } // Deinitialize the streamUrl - setStreamUrl(""); - if (newItem != nullptr) { - fetchAndSetStreamUrl(m_item.data()); + if (shouldFetchStreamUrl) { + setStreamUrl(QUrl()); + m_urlFetcherThread->addItemToQueue(m_item); } } -void PlaybackManager::setStreamUrl(const QString &streamUrl) { - this->m_streamUrl = streamUrl; +void PlaybackManager::setStreamUrl(const QUrl &streamUrl) { + m_streamUrl = streamUrl.toString(); // Inspired by PHP naming schemes - QUrl realStreamUrl(streamUrl); - Q_ASSERT_X(realStreamUrl.isValid(), "setStreamUrl", "StreamURL Jellyfin returned is not valid"); - emit streamUrlChanged(streamUrl); + Q_ASSERT_X(streamUrl.isValid() || streamUrl.isEmpty(), "setStreamUrl", "StreamURL Jellyfin returned is not valid"); + emit streamUrlChanged(m_streamUrl); } void PlaybackManager::setPlaybackState(QMediaPlayer::State newState) { @@ -203,6 +146,16 @@ void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus ne qDebug() << "Resuming playback by seeking to " << (m_resumePosition / MS_TICK_FACTOR); m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR); } + } else if (newStatus == QMediaPlayer::EndOfMedia) { + next(); + } +} + +void PlaybackManager::mediaPlayerDurationChanged(qint64 newDuration) { + emit durationChanged(newDuration); + if (newDuration > 0 && !m_nextItem.isNull()) { + m_preloadTimer.stop(); + m_preloadTimer.start(std::max(static_cast(newDuration - PRELOAD_DURATION), 0)); } } @@ -215,46 +168,47 @@ void PlaybackManager::updatePlaybackInfo() { postPlaybackInfo(Progress); } -void PlaybackManager::playItem(const QString &itemId) { - Q_UNUSED(itemId) - Q_UNIMPLEMENTED(); - /*RemoteItem *newItem = new RemoteItem(itemId, m_apiClient, this); - ItemModel *queue = new UserItemModel(this); - setQueue(queue); - QString parentId = newItem->data()->parentId(); - queue->setParentId(parentId); - queue->setLimit(10000); - queue->setApiClient(m_apiClient); - queue->reload(); - setItem(newItem->data()); - connect(queue, &BaseApiModel::ready, this, [this, queue, newItem]() { - for (int i = 0; i < queue->size(); i++) { - if (queue->at(i)->jellyfinId() == newItem->jellyfinId()) { - m_queueIndex = i; - emit queueIndexChanged(m_queueIndex); - break; - } - } - });*/ - setPlaybackState(QMediaPlayer::PlayingState); +void PlaybackManager::playItem(Item *item) { + setItem(item->data()); } -void PlaybackManager::playItemInList(ItemModel *playlist, int itemIdx) { - playlist->setParent(this); - setQueue(playlist); - m_queueIndex = itemIdx; +void PlaybackManager::playItemInList(ItemModel *playlist, int index) { + m_queue->clearList(); + m_queue->appendToList(*playlist); + m_queue->play(index); + m_queueIndex = index; emit queueIndexChanged(m_queueIndex); - //setItem(playlist->at(itemIdx)); + setItem(playlist->itemAt(index)); } void PlaybackManager::next() { - Q_UNIMPLEMENTED(); + m_mediaPlayer->stop(); + m_mediaPlayer->setMedia(QMediaContent()); + swapMediaPlayer(); + + if (m_nextItem.isNull()) { + setItem(m_queue->nextItem()); + m_queue->next(); + m_nextItem.clear(); + } else { + m_item = m_nextItem; + setItem(m_nextItem); + } + m_mediaPlayer->play(); } void PlaybackManager::previous() { - Q_UNIMPLEMENTED(); -} + m_mediaPlayer->stop(); + m_mediaPlayer->setPosition(0); + m_nextStreamUrl = m_streamUrl; + m_streamUrl = QString(); + m_nextItem = m_item; + swapMediaPlayer(); + m_queue->previous(); + setItem(m_queue->currentItem()); + m_mediaPlayer->play(); +} void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) { QJsonObject root; @@ -263,7 +217,7 @@ void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) { qWarning() << "Item is null. Not posting playback info"; return; } - root["ItemId"] = Support::uuidToString(m_item->jellyfinId()); + root["ItemId"] = Support::toString(m_item->jellyfinId()); root["SessionId"] = m_playSessionId; switch(type) { @@ -308,11 +262,11 @@ void PlaybackManager::swapMediaPlayer() { if (m_mediaPlayer != nullptr) { disconnect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged); disconnect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged); - disconnect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::durationChanged); + disconnect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::mediaPlayerDurationChanged); disconnect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged); disconnect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged); // I do not like the complicated overload cast - disconnect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::error)), this, SLOT(mediaPlayerError(QmediaPlayer::error))); + disconnect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)), this, SLOT(mediaPlayerError(QMediaPlayer::Error))); } if (m_mediaPlayer == m_mediaPlayer1) { m_mediaPlayer = m_mediaPlayer2; @@ -323,41 +277,154 @@ void PlaybackManager::swapMediaPlayer() { } connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged); connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged); - connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::durationChanged); + connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::mediaPlayerDurationChanged); connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged); connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged); // I do not like the complicated overload cast - connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::error)), this, SLOT(mediaPlayerError(QmediaPlayer::error))); -} - -Model::Item *PlaybackManager::nextItem() { - if (m_queue == nullptr) return nullptr; - // TODO: shuffle etc. - if (m_queueIndex < m_queue->size()) { - //return m_queue->at(m_queueIndex + 1); - } - return nullptr; -} - -void PlaybackManager::setQueue(ItemModel *model) { - if (m_queue != nullptr) { - if (QQmlEngine::objectOwnership(m_queue) == QQmlEngine::CppOwnership) { - m_queue->deleteLater(); - } else { - m_queue->setParent(nullptr); - } - } - m_queue = model; - emit queueChanged(m_queue); + connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)), this, SLOT(mediaPlayerError(QMediaPlayer::Error))); } void PlaybackManager::componentComplete() { if (m_apiClient == nullptr) qWarning() << "No ApiClient set for PlaybackManager"; m_qmlIsParsingComponent = false; - if (!m_item.isNull()) { - fetchAndSetStreamUrl(m_item.data()); +} + +// ItemUrlFetcherThread +ItemUrlFetcherThread::ItemUrlFetcherThread(PlaybackManager *manager) : + QThread(manager), + m_parent(manager), + m_loader(new Jellyfin::Loader::HTTP::GetPostedPlaybackInfoLoader(manager->m_apiClient)) { + + connect(this, &ItemUrlFetcherThread::prepareLoaderRequested, this, &ItemUrlFetcherThread::onPrepareLoader); +} + +void ItemUrlFetcherThread::addItemToQueue(QSharedPointer item) { + QMutexLocker locker(&m_queueModifyMutex); + m_queue.enqueue(item); + m_urlWaitCondition.wakeOne(); +} + +void ItemUrlFetcherThread::cleanlyStop() { + m_keepRunning = false; + m_urlWaitCondition.wakeAll(); +} + +void ItemUrlFetcherThread::onPrepareLoader() { + m_loader->setApiClient(m_parent->m_apiClient); + m_loader->prepareLoad(); + m_loaderPrepared = true; + m_waitLoaderPrepared.wakeOne(); +} + +void ItemUrlFetcherThread::run() { + while (m_keepRunning) { + m_urlWaitConditionMutex.lock(); + while(m_queue.isEmpty() && m_keepRunning) { + m_urlWaitCondition.wait(&m_urlWaitConditionMutex); + } + m_urlWaitConditionMutex.unlock(); + if (!m_keepRunning) break; + + Jellyfin::Loader::GetPostedPlaybackInfoParams params; + QSharedPointer item = m_queue.dequeue(); + m_queueModifyMutex.lock(); + params.setItemId(item->jellyfinId()); + m_queueModifyMutex.unlock(); + params.setUserId(m_parent->m_apiClient->userId()); + params.setEnableDirectPlay(true); + params.setEnableDirectStream(true); + params.setEnableTranscoding(true); + + m_loaderPrepared = false; + m_loader->setParameters(params); + + // We cannot call m_loader->prepareLoad() from this thread, so we must + // emit a signal and hope for the best + emit prepareLoaderRequested(QPrivateSignal()); + m_waitLoaderPreparedMutex.lock(); + while (!m_loaderPrepared) { + m_waitLoaderPrepared.wait(&m_waitLoaderPreparedMutex); + } + m_waitLoaderPreparedMutex.unlock(); + + DTO::PlaybackInfoResponse response; + try { + std::optional responseOpt = m_loader->load(); + if (responseOpt.has_value()) { + response = responseOpt.value(); + } else { + qWarning() << "Cannot retrieve URL of " << params.itemId(); + continue; + } + } catch (QException &e) { + qWarning() << "Cannot retrieve URL of " << params.itemId() << ": " << e.what(); + continue; + } + + //TODO: move the item URL fetching logic out of this function, into MediaSourceInfo? + QList mediaSources = response.mediaSources(); + QUrl resultingUrl; + QString playSession = response.playSessionId(); + PlayMethod playMethod = PlayMethod::EnumNotSet; + for (int i = 0; i < mediaSources.size(); i++) { + const DTO::MediaSourceInfo &source = mediaSources.at(i); + if (source.supportsDirectPlay() && QFile::exists(source.path())) { + resultingUrl = QUrl::fromLocalFile(source.path()); + playMethod = PlayMethod::DirectPlay; + } else if (source.supportsDirectStream()) { + QString mediaType = item->mediaType(); + QUrlQuery query; + query.addQueryItem("mediaSourceId", source.jellyfinId()); + query.addQueryItem("deviceId", m_parent->m_apiClient->deviceId()); + query.addQueryItem("api_key", m_parent->m_apiClient->token()); + query.addQueryItem("Static", "True"); + resultingUrl = QUrl(this->m_parent->m_apiClient->baseUrl() + "/" + mediaType + "/" + params.itemId() + + "/stream." + source.container() + "?" + query.toString(QUrl::EncodeReserved)); + playMethod = PlayMethod::DirectStream; + } else if (source.supportsTranscoding()) { + resultingUrl = QUrl(m_parent->m_apiClient->baseUrl() + source.transcodingUrl()); + playMethod = PlayMethod::Transcode; + } else { + qDebug() << "No suitable sources for item " << item->jellyfinId(); + } + if (!resultingUrl.isEmpty()) break; + } + if (resultingUrl.isEmpty()) { + qWarning() << "Could not find suitable media source for item " << params.itemId(); + emit itemUrlFetchError(item->jellyfinId(), tr("Cannot fetch stream URL")); + } else { + emit itemUrlFetched(item->jellyfinId(), resultingUrl, playSession, playMethod); + } } } +void PlaybackManager::onItemExtraDataReceived(const QString &itemId, const QUrl &url, + const QString &playSession, PlayMethod playMethod) { + Q_UNUSED(url) + Q_UNUSED(playSession) + if (!m_item.isNull() && m_item->jellyfinId() == itemId) { + // We want to play the item probably right now + m_playSessionId = playSession; + m_playMethod = playMethod; + setStreamUrl(url); + emit playMethodChanged(m_playMethod); + m_mediaPlayer->setMedia(QMediaContent(url)); + m_mediaPlayer->play(); + } else if (!m_nextItem.isNull() && m_nextItem->jellyfinId() == itemId){ + QMediaPlayer *otherMediaPlayer = m_mediaPlayer == m_mediaPlayer1 ? m_mediaPlayer2 : m_mediaPlayer1; + m_nextPlaySessionId = playSession; + m_nextStreamUrl = url.toString(); + otherMediaPlayer->setMedia(QMediaContent(url)); + } else { + qDebug() << "Late reply for " << itemId << " received, ignoring"; + } + +} +/// Called when the fetcherThread encountered an error +void PlaybackManager::onItemErrorReceived(const QString &itemId, const QString &errorString) { + Q_UNUSED(itemId) + Q_UNUSED(errorString) +} + } // NS ViewModel } // NS Jellyfin diff --git a/core/src/viewmodel/playlist.cpp b/core/src/viewmodel/playlist.cpp index a09832f..8ee2ae4 100644 --- a/core/src/viewmodel/playlist.cpp +++ b/core/src/viewmodel/playlist.cpp @@ -21,173 +21,15 @@ namespace Jellyfin { namespace ViewModel { -Playlist::Playlist(ApiClient *apiClient, QObject *parent) - : ItemModel(parent), m_apiClient(apiClient), m_fetcherThread(new ItemUrlFetcherThread(this)){ +/*Playlist::Playlist(ApiClient *apiClient, QObject *parent) + : ItemModel(parent) { connect(this, &QAbstractListModel::rowsInserted, this, &Playlist::onItemsAdded); connect(this, &QAbstractListModel::rowsRemoved, this, &Playlist::onItemsRemoved); connect(this, &QAbstractListModel::rowsMoved, this, &Playlist::onItemsMoved); connect(this, &QAbstractListModel::modelReset, this, &Playlist::onItemsReset); - connect(m_fetcherThread, &ItemUrlFetcherThread::itemUrlFetched, this, &Playlist::onItemExtraDataReceived); -} - -void Playlist::onItemsAdded(const QModelIndex &parent, int startIndex, int endIndex) { - if (parent.isValid()) return; - // Retrieve added items. - for (int i = startIndex; i <= endIndex; i++) { - m_fetcherThread->addItemToQueue(at(i)); - } -} - -void Playlist::onItemsMoved(const QModelIndex &parent, int startIndex, int endIndex, const QModelIndex &destination, int index) { - if (parent.isValid()) return; - if (destination.isValid()) return; -} - -void Playlist::onItemsRemoved(const QModelIndex &parent, int startIndex, int endIndex) { - if (parent.isValid()) return; - QSet removedItemIds; - // Assume almost all items in the playlist are unique - // If you're the kind of person who puts songs multiple time inside of a playlist: - // enjoy your needlessly reserved memory. - removedItemIds.reserve(endIndex - startIndex + 1); - // First collect all the ids of items that are going to be removed and how many of them there are - for (int i = startIndex; i <= endIndex; i++) { - removedItemIds.insert(at(i).jellyfinId()); - } - - // Look for itemIds which appear outside of the removed range - // these do not need to be removed from the cahe - for (int i = 0; i < startIndex; i++) { - const QString &id = at(i).jellyfinId(); - if (removedItemIds.contains(id)) { - removedItemIds.remove(id); - } - } - - for (int i = endIndex + 1; i < size(); i++) { - const QString &id = at(i).jellyfinId(); - if (removedItemIds.contains(id)) { - removedItemIds.remove(id); - } - } - - for (auto it = removedItemIds.cbegin(); it != removedItemIds.cend(); it++) { - m_cache.remove(*it); - } -} - -void Playlist::onItemsReset() { - -} - -void Playlist::onItemExtraDataReceived(const QString &itemId, const QUrl &url, const QString &playSession) { - m_cache.insert(itemId, ExtraData { url, playSession, QString()}); -} - -void Playlist::onItemErrorReceived(const QString &itemId, const QString &errorString) { - m_cache.insert(itemId, ExtraData {QUrl(), QString(), errorString}); -} - -ItemUrlFetcherThread::ItemUrlFetcherThread(Playlist *playlist) : - QThread(playlist), - m_parent(playlist), - m_loader(new Loader::HTTP::GetPostedPlaybackInfoLoader(playlist->m_apiClient)) { - - connect(this, &ItemUrlFetcherThread::prepareLoaderRequested, this, &ItemUrlFetcherThread::onPrepareLoader); -} - -void ItemUrlFetcherThread::addItemToQueue(const Model::Item item) { - QMutexLocker locker(&m_queueModifyMutex); - m_queue.enqueue(item); - m_urlWaitCondition.wakeOne(); -} - -void ItemUrlFetcherThread::cleanlyStop() { - m_keepRunning = false; - m_urlWaitCondition.wakeAll(); -} - -void ItemUrlFetcherThread::onPrepareLoader() { - m_loader->prepareLoad(); - m_loaderPrepared = true; - m_waitLoaderPrepared.wakeOne(); -} - -void ItemUrlFetcherThread::run() { - while (m_keepRunning) { - while(m_queue.isEmpty() && m_keepRunning) { - m_urlWaitConditionMutex.lock(); - m_urlWaitCondition.wait(&m_urlWaitConditionMutex); - } - if (!m_keepRunning) break; - - Jellyfin::Loader::GetPostedPlaybackInfoParams params; - const Model::Item& item = m_queue.dequeue(); - m_queueModifyMutex.lock(); - params.setItemId(item.jellyfinId()); - m_queueModifyMutex.unlock(); - params.setUserId(m_parent->m_apiClient->userId()); - params.setEnableDirectPlay(true); - params.setEnableDirectStream(true); - params.setEnableTranscoding(true); - - m_loaderPrepared = false; - m_loader->setParameters(params); - - // We cannot call m_loader->prepareLoad() from this thread, so we must - // emit a signal and hope for the best - emit prepareLoaderRequested(QPrivateSignal()); - while (!m_loaderPrepared) { - m_waitLoaderPreparedMutex.lock(); - m_waitLoaderPrepared.wait(&m_waitLoaderPreparedMutex); - } - - DTO::PlaybackInfoResponse response; - try { - std::optional responseOpt = m_loader->load(); - if (responseOpt.has_value()) { - response = responseOpt.value(); - } else { - qWarning() << "Cannot retrieve URL of " << params.itemId(); - continue; - } - } catch (QException e) { - qWarning() << "Cannot retrieve URL of " << params.itemId() << ": " << e.what(); - continue; - } - - QList mediaSources = response.mediaSources(); - QUrl resultingUrl; - QString playSession = response.playSessionId(); - for (int i = 0; i < mediaSources.size(); i++) { - const DTO::MediaSourceInfo &source = mediaSources.at(i); - if (source.supportsDirectPlay()) { - resultingUrl = QUrl::fromLocalFile(source.path()); - } else if (source.supportsDirectStream()) { - QString mediaType = item.mediaType(); - QUrlQuery query; - query.addQueryItem("mediaSourceId", source.jellyfinId()); - query.addQueryItem("deviceId", m_parent->m_apiClient->deviceId()); - query.addQueryItem("api_key", m_parent->m_apiClient->token()); - query.addQueryItem("Static", "True"); - resultingUrl = QUrl(this->m_parent->m_apiClient->baseUrl() + "/" + mediaType + "/" + params.itemId() - + "/stream." + source.container() + "?" + query.toString(QUrl::EncodeReserved)); - } else if (source.supportsTranscoding()) { - resultingUrl = QUrl(m_parent->m_apiClient->baseUrl() + source.transcodingUrl()); - } else { - qDebug() << "No suitable sources for item " << item.jellyfinId(); - } - if (!resultingUrl.isEmpty()) break; - } - if (resultingUrl.isEmpty()) { - qWarning() << "Could not find suitable media source for item " << params.itemId(); - emit itemUrlFetchError(item.jellyfinId(), tr("Cannot fetch stream URL")); - } - emit itemUrlFetched(item.jellyfinId(), resultingUrl, playSession); - } -} +}*/ } // NS ViewModel } // NS Jellyfin diff --git a/core/src/websocket.cpp b/core/src/websocket.cpp index fb38c9b..1c194a7 100644 --- a/core/src/websocket.cpp +++ b/core/src/websocket.cpp @@ -66,6 +66,7 @@ void WebSocket::onDisconnected() { } void WebSocket::textMessageReceived(const QString &message) { + qDebug() << "WebSocket: message received: " << message; QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8()); if (doc.isNull() || !doc.isObject()) { qWarning() << "Malformed message received over WebSocket: parse error or root not an object."; diff --git a/qtquick/qml/main.qml b/qtquick/qml/main.qml index 88ee587..505e3d0 100644 --- a/qtquick/qml/main.qml +++ b/qtquick/qml/main.qml @@ -2,6 +2,8 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Window 2.12 +import QtMultimedia 5.12 + import nl.netsoj.chris.Jellyfin 1.0 as J import "components" @@ -18,6 +20,7 @@ ApplicationWindow { J.PlaybackManager { id: playbackManager + apiClient: ApiClient } background: Background { @@ -49,16 +52,45 @@ ApplicationWindow { ApiClient.restoreSavedSession() } - footer: Column { + footer: Item { id: footer - Text { - text: qsTr("Now playing") - color: "white" + height: Math.max(details.height, playButtons.height) + Column { + id: details + anchors.verticalCenter: parent.verticalCenter + Text { + text: qsTr("Now playing") + color: "white" + } + Text { + text: "%1\n%2".arg(playbackManager.item.name ? playbackManager.item.name : "Nothing").arg(playbackManager.streamUrl) + color: "white" + } } - Text { - text: playbackManager.item.name ? playbackManager.item.name : "Nothing" - color: "white" + Row { + id: playButtons + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + } + Button { + anchors.verticalCenter: parent.verticalCenter + text: "Previous" + onClicked: playbackManager.previous(); + } + Button { + readonly property bool _playing: manager.playbackState === MediaPlayer.PlayingState; + anchors.verticalCenter: parent.verticalCenter + text: _playing ? "Pause" : "Play" + onClicked: _playing ? playbackManager.pause() : playbackManager.play() + } + Button { + anchors.verticalCenter: parent.verticalCenter + text: "Next" + onClicked: playbackManager.next(); + } } + } Rectangle { color: "darkblue" diff --git a/qtquick/qml/pages/DetailPage.qml b/qtquick/qml/pages/DetailPage.qml index 9d271d7..a44685a 100644 --- a/qtquick/qml/pages/DetailPage.qml +++ b/qtquick/qml/pages/DetailPage.qml @@ -38,22 +38,24 @@ Page { height: parent.height / 3 source: ApiClient.baseUrl + "/Items/" + itemId + "/Images/Primary?tag=" + jellyfinItem.tag } + J.ItemModel { + id: tracks + loader: J.UserItemsLoader { + apiClient: ApiClient + parentId: detailPage.itemId + } + } ListView { width: parent.width height: parent.height / 3 * 2 anchors.bottom: parent.bottom - model: J.ItemModel { - loader: J.UserItemsLoader { - apiClient: ApiClient - parentId: detailPage.itemId - } - } - delegate: ItemDelegate{ + model: tracks + delegate: ItemDelegate { icon.source: ApiClient.baseUrl + "/Items/" + model.jellyfinId + "/Images/Primary?tag=" + model.tag text: model.name - width: parent.width - onClicked: playbackManager.playItem(model.jellyfinId) + width: ListView.view.width + onClicked: playbackManager.playItemInList(tracks, model.index) } } } diff --git a/qtquick/qtquickcontrols2.conf b/qtquick/qtquickcontrols2.conf index 9f31dc2..7587943 100644 --- a/qtquick/qtquickcontrols2.conf +++ b/qtquick/qtquickcontrols2.conf @@ -1,3 +1,3 @@ [Controls] Style=SailfinStyle -FallbackStyle=Universal +FallbackStyle=Material diff --git a/rpm/harbour-sailfin.yaml b/rpm/harbour-sailfin.yaml index 22e1d5e..9b483eb 100644 --- a/rpm/harbour-sailfin.yaml +++ b/rpm/harbour-sailfin.yaml @@ -43,7 +43,7 @@ Files: Macros: - '__provides_exclude_from; ^%{_datadir}/.*$' - - '__requires_exclude; ^libjellyfin-qt.*$' + - '__requires_exclude; ^libJellyfinQt.*$' # Turn off facist build policy - '_unpackaged_files_terminate_build; 0 ' diff --git a/sailfish/CMakeLists.txt b/sailfish/CMakeLists.txt index be10c20..77df1cf 100644 --- a/sailfish/CMakeLists.txt +++ b/sailfish/CMakeLists.txt @@ -18,25 +18,27 @@ set(harbour-sailfin_SOURCES src/harbour-sailfin.cpp) set(sailfin_QML_SOURCES + qml/ApiClient.qml qml/Constants.qml qml/Utils.js - qml/components/music/NarrowAlbumCover.qml - qml/components/music/WideAlbumCover.qml - qml/components/music/SongDelegate.qml - qml/components/videoplayer/VideoError.qml - qml/components/videoplayer/VideoHud.qml + qml/components/music/NarrowAlbumCover.qml + qml/components/music/WideAlbumCover.qml + qml/components/music/SongDelegate.qml + qml/components/videoplayer/VideoError.qml + qml/components/videoplayer/VideoHud.qml qml/components/IconListItem.qml - qml/components/LibraryItemDelegate.qml + qml/components/JItem.qml + qml/components/LibraryItemDelegate.qml qml/components/MoreSection.qml qml/components/PlainLabel.qml - qml/components/PlaybackBar.qml - qml/components/PlayQueue.qml - qml/components/PlayToolbar.qml + qml/components/PlaybackBar.qml + qml/components/PlayQueue.qml + qml/components/PlayToolbar.qml qml/components/RemoteImage.qml qml/components/Shim.qml qml/components/UserGridDelegate.qml qml/components/VideoPlayer.qml - qml/components/VideoTrackSelector.qml + qml/components/VideoTrackSelector.qml qml/cover/CoverPage.qml qml/cover/PosterCover.qml qml/cover/VideoCover.qml @@ -51,21 +53,22 @@ set(sailfin_QML_SOURCES qml/pages/itemdetails/EpisodePage.qml qml/pages/itemdetails/FilmPage.qml qml/pages/itemdetails/MusicAlbumPage.qml - qml/pages/itemdetails/PhotoPage.qml + qml/pages/itemdetails/PhotoPage.qml qml/pages/itemdetails/SeasonPage.qml qml/pages/itemdetails/SeriesPage.qml qml/pages/itemdetails/UnsupportedPage.qml qml/pages/itemdetails/VideoPage.qml - qml/pages/settings/DebugPage.qml + qml/pages/settings/DebugPage.qml qml/pages/setup/AddServerConnectingPage.qml - qml/pages/setup/LoginDialog.qml + qml/pages/setup/AddServerPage.qml + qml/pages/setup/LoginDialog.qml qml/qmldir) add_executable(harbour-sailfin ${harbour-sailfin_SOURCES} ${sailfin_QML_SOURCES}) target_link_libraries(harbour-sailfin PRIVATE Qt5::Gui Qt5::Qml Qt5::Quick SailfishApp::SailfishApp # Note: this may break when the compiler changes. -rdynamic and -pie seem to be needed for the # invoker/booster to work - jellyfin-qt "-Wl,-rpath,${CMAKE_INSTALL_LIBDIR} -rdynamic -pie") + JellyfinQt "-Wl,-rpath,${CMAKE_INSTALL_LIBDIR} -rdynamic -pie") target_compile_definitions(harbour-sailfin PRIVATE $<$,$>:QT_QML_DEBUG>) diff --git a/sailfish/harbour-sailfin.desktop b/sailfish/harbour-sailfin.desktop index ef6febb..891bad3 100644 --- a/sailfish/harbour-sailfin.desktop +++ b/sailfish/harbour-sailfin.desktop @@ -3,7 +3,7 @@ Type=Application Version=1.1 X-Nemo-Application-Type=silica-qt5 Icon=harbour-sailfin -Exec=harbour-sailfin --attempt-sandbox +Exec=harbour-sailfin Name=Sailfin diff --git a/sailfish/qml/ApiClient.qml b/sailfish/qml/ApiClient.qml new file mode 100644 index 0000000..e591c6d --- /dev/null +++ b/sailfish/qml/ApiClient.qml @@ -0,0 +1,7 @@ +pragma Singleton +import QtQuick 2.6 +import nl.netsoj.chris.Jellyfin 1.0 as J + +J.ApiClient { + supportedCommands: [J.GeneralCommandType.Play, J.GeneralCommandType.DisplayContent, J.GeneralCommandType.DisplayMessage] +} diff --git a/sailfish/qml/Utils.js b/sailfish/qml/Utils.js index 6ac575e..ded24b0 100644 --- a/sailfish/qml/Utils.js +++ b/sailfish/qml/Utils.js @@ -68,6 +68,7 @@ function itemModelImageUrl(baseUrl, itemId, tag, type, options) { } function usePortraitCover(itemType) { + console.log("ItemType: " + itemType) return ["Series", "Movie", "tvshows", "movies"].indexOf(itemType) >= 0 } diff --git a/sailfish/qml/components/JItem.qml b/sailfish/qml/components/JItem.qml new file mode 100644 index 0000000..3caf4f1 --- /dev/null +++ b/sailfish/qml/components/JItem.qml @@ -0,0 +1,9 @@ +import QtQuick 2.6 +import nl.netsoj.chris.Jellyfin 1.0 as J + +// Due QTBUG-10822, declarartions such as `property J.Item foo` are not possible. +// Since J.Item clashses with the QtQuick item type, this is a workaround until +// Sailfish OS upgrades to a Qt > 5.8. Maybe in 2023? +J.Item { + +} diff --git a/sailfish/qml/harbour-sailfin.qml b/sailfish/qml/harbour-sailfin.qml index 44af789..1495079 100644 --- a/sailfish/qml/harbour-sailfin.qml +++ b/sailfish/qml/harbour-sailfin.qml @@ -27,6 +27,7 @@ import Nemo.KeepAlive 1.2 import "components" import "pages" +import "." as D ApplicationWindow { id: appWindow @@ -35,14 +36,13 @@ ApplicationWindow { //readonly property MediaPlayer mediaPlayer: _mediaPlayer readonly property PlaybackManager playbackManager: _playbackManager - // Data of the currently selected item. For use on the cover. - property JellyfinItem itemData + // Due QTBUG-10822, declarartions such as `property J.Item foo` are not possible. + property QtObject itemData // Id of the collection currently browsing. For use on the cover. property string collectionId // Bad way to implement settings, but it'll do for now. property bool showDebugInfo: true - property bool _hidePlaybackBar: false bottomMargin: playbackBar.visibleSize @@ -52,14 +52,14 @@ ApplicationWindow { initialPage: Component { MainPage { Connections { - target: ApiClient + target: D.ApiClient // Replace the MainPage if no server was set up. } onStatusChanged: { if (status == PageStatus.Active && !_hasInitialized) { _hasInitialized = true; - ApiClient.restoreSavedSession(); + D.ApiClient.restoreSavedSession(); } } } @@ -94,7 +94,7 @@ ApplicationWindow { PlaybackManager { id: _playbackManager - apiClient: ApiClient + apiClient: D.ApiClient audioIndex: 0 autoOpen: true } @@ -118,7 +118,7 @@ ApplicationWindow { //FIXME: proper error handling Connections { - target: ApiClient + target: D.ApiClient onNetworkError: errorNotification.show("Network error: " + error) onConnectionFailed: errorNotification.show("Connect error: " + error) //onConnectionSuccess: errorNotification.show("Success: " + loginMessage) diff --git a/sailfish/qml/pages/AboutPage.qml b/sailfish/qml/pages/AboutPage.qml index b5fc19d..5fe62ec 100644 --- a/sailfish/qml/pages/AboutPage.qml +++ b/sailfish/qml/pages/AboutPage.qml @@ -19,9 +19,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../components" +import ".." Page { id: page diff --git a/sailfish/qml/pages/MainPage.qml b/sailfish/qml/pages/MainPage.qml index 6cd44b0..d69dbfb 100644 --- a/sailfish/qml/pages/MainPage.qml +++ b/sailfish/qml/pages/MainPage.qml @@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import QtQuick 2.0 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../components" import "../" @@ -32,7 +32,7 @@ Page { /// True if the models on this page already have been loaded and don't necessarily need a refresh property bool _modelsLoaded: false - id: page + id: mainPage allowedOrientations: Orientation.All // This component is reused both in the normal state and error state @@ -49,7 +49,7 @@ Page { text: qsTr("Reload") onClicked: loadModels(true) } - busy: mediaLibraryModel.status == ApiModel.Loading + busy: userViewsLoader.status === J.UsersViewsLoader.Loading } } @@ -67,29 +67,33 @@ Page { // of the page, followed by our content. Column { id: column - width: page.width + width: mainPage.width - UserViewModel { - id: mediaLibraryModel2 - apiClient: ApiClient + J.ItemModel { + id: mediaLibraryModel + loader: J.UsersViewsLoader { + id: mediaLibraryLoader + apiClient: ApiClient + } } MoreSection { //- Section header for films and TV shows that an user hasn't completed yet. text: qsTr("Resume watching") clickable: false - busy: userResumeModel.status === ApiModel.Loading + //busy: userResumeModel.status === J.ApiModel.Loading Loader { width: parent.width sourceComponent: carrouselView property alias itemModel: userResumeModel property string collectionType: "series" - UserItemResumeModel { + J.ItemModel { id: userResumeModel - apiClient: ApiClient + // Resume model + /*apiClient: ApiClient limit: 12 - recursive: true + recursive: true*/ } } } @@ -97,7 +101,7 @@ Page { //- Section header for next episodes in a TV show that an user was watching. text: qsTr("Next up") clickable: false - busy: showNextUpModel.status === ApiModel.Loading + //busy: showNextUpModel.status === .Loading Loader { width: parent.width @@ -105,23 +109,18 @@ Page { property alias itemModel: showNextUpModel property string collectionType: "series" - ShowNextUpModel { + J.ItemModel { id: showNextUpModel - apiClient: ApiClient - limit: 12 + /*apiClient: ApiClient + limit: 12*/ } } } - - UserViewModel { - id: mediaLibraryModel - apiClient: ApiClient - } Repeater { model: mediaLibraryModel MoreSection { text: model.name - busy: userItemModel.status !== ApiModel.Ready + busy: userItemModel.status !== J.UsersViewsLoader.Ready onHeaderClicked: pageStack.push(Qt.resolvedUrl("itemdetails/CollectionPage.qml"), {"itemId": model.jellyfinId}) Loader { @@ -130,11 +129,12 @@ Page { property alias itemModel: userItemModel property string collectionType: model.collectionType || "" - UserItemLatestModel { + J.ItemModel { id: userItemModel - apiClient: ApiClient - parentId: jellyfinId - limit: 16 + loader: J.LatestMediaLoader { + apiClient: ApiClient + parentId: jellyfinId + } } Connections { target: mediaLibraryModel @@ -184,8 +184,8 @@ Page { if (force || (ApiClient.authenticated && !_modelsLoaded)) { _modelsLoaded = true; mediaLibraryModel.reload() - userResumeModel.reload() - showNextUpModel.reload() + //userResumeModel.reload() + //showNextUpModel.reload() } } @@ -236,11 +236,11 @@ Page { states: [ State { name: "default" - when: mediaLibraryModel2.status !== ApiModel.Error + when: mediaLibraryLoader.status !== J.UsersViewsLoader.Error }, State { name: "error" - when: mediaLibraryModel2.status === ApiModel.Error + when: mediaLibraryLoader.status === J.UsersViewsLoader.Error PropertyChanges { target: errorFlickable diff --git a/sailfish/qml/pages/SettingsPage.qml b/sailfish/qml/pages/SettingsPage.qml index 4710325..53b2517 100644 --- a/sailfish/qml/pages/SettingsPage.qml +++ b/sailfish/qml/pages/SettingsPage.qml @@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../components" @@ -57,9 +57,9 @@ Page { rightMargin: Theme.horizontalPageMargin } height: user.implicitHeight + server.implicitHeight + Theme.paddingMedium - User { + QtObject { id: loggedInUser - apiClient: ApiClient + //apiClient: ApiClient } RemoteImage { id: userIcon diff --git a/sailfish/qml/pages/itemdetails/BaseDetailPage.qml b/sailfish/qml/pages/itemdetails/BaseDetailPage.qml index bdcefcc..e0b067e 100644 --- a/sailfish/qml/pages/itemdetails/BaseDetailPage.qml +++ b/sailfish/qml/pages/itemdetails/BaseDetailPage.qml @@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../../components" import "../.." @@ -32,14 +32,14 @@ import "../.." */ Page { id: pageRoot - property alias itemId: jItem.jellyfinId - property alias itemData: jItem + property string itemId: "" + property alias itemData: jItemLoader.data //property string itemId: "" //property var itemData: ({}) - property bool _loading: jItem.status === "Loading" - readonly property bool hasLogo: (typeof itemData.ImageTags !== "undefined") && (typeof itemData.ImageTags["Logo"] !== "undefined") + property bool _loading: jItemLoader.status === J.ItemLoader.Loading + readonly property bool hasLogo: (typeof itemData.imageTags !== "undefined") && (typeof itemData.imageTags["Logo"] !== "undefined") property string _chosenBackdropImage: "" - readonly property string parentId: itemData.ParentId || "" + readonly property string parentId: itemData.parentId || "" function updateBackdrop() { var rand = 0; @@ -62,13 +62,13 @@ Page { SilicaFlickable { anchors.fill: parent contentHeight: errorContent.height - visible: jItem.status == JellyfinItem.Error + visible: jItemLoader.status === J.ItemLoader.Error PullDownMenu { - busy: jItem.status == JellyfinItem.Loading + busy: jItemLoader.status === J.ItemLoader.Loading MenuItem { text: qsTr("Retry") - onClicked: jItem.reload() + onClicked: jItemLoader.reload() } } @@ -79,28 +79,33 @@ Page { ViewPlaceholder { enabled: true text: qsTr("An error has occured") - hintText: jItem.errorString + hintText: jItemLoader.errorString } } } - JellyfinItem { - id: jItem + J.ItemLoader { + id: jItemLoader apiClient: ApiClient + itemId: pageRoot.itemId onStatusChanged: { - //console.log("Status changed: " + newStatus, JSON.stringify(jItem)) - if (status == JellyfinItem.Ready) { + console.log("Status changed: " + newStatus, JSON.stringify(jItemLoader.data)) + if (status === J.ItemLoader.Ready) { updateBackdrop() } } } + Label { + text: "ItemLoader status=%1, \nitemId=%2\nitemData=%3".arg(jItemLoader.status).arg(jItemLoader.itemId).arg(jItemLoader.data) + } + onStatusChanged: { if (status == PageStatus.Deactivating) { //appWindow.itemData = ({}) } if (status == PageStatus.Active) { - appWindow.itemData = jItem + //appWindow.itemData = jItemLoader.data } } } diff --git a/sailfish/qml/pages/itemdetails/CollectionPage.qml b/sailfish/qml/pages/itemdetails/CollectionPage.qml index 9a9f89d..ee90d9d 100644 --- a/sailfish/qml/pages/itemdetails/CollectionPage.qml +++ b/sailfish/qml/pages/itemdetails/CollectionPage.qml @@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../.." import "../../components" @@ -27,12 +27,13 @@ import "../../components" BaseDetailPage { id: pageRoot - UserItemModel { + J.ItemModel { id: collectionModel - apiClient: ApiClient - parentId: itemData.jellyfinId - sortBy: ["SortName"] - onParentIdChanged: reload() + //sortBy: ["SortName"] + loader: J.UserItemsLoader { + apiClient: ApiClient + parentId: itemData.jellyfinId + } } SilicaGridView { @@ -42,7 +43,7 @@ BaseDetailPage { cellWidth: Constants.libraryDelegateWidth cellHeight: Utils.usePortraitCover(itemData.collectionType) ? Constants.libraryDelegatePosterHeight : Constants.libraryDelegateHeight - visible: itemData.status !== JellyfinItem.Error + visible: itemData.status !== J.ItemLoader.Error header: PageHeader { title: itemData.name || qsTr("Loading") @@ -54,7 +55,7 @@ BaseDetailPage { text: qsTr("Sort by") onClicked: pageStack.push(sortPageComponent) } - busy: collectionModel.status === ApiModel.Loading + busy: collectionModel.status === J.UserItemsLoader.Loading } delegate: GridItem { RemoteImage { diff --git a/sailfish/qml/pages/itemdetails/FilmPage.qml b/sailfish/qml/pages/itemdetails/FilmPage.qml index 5632fad..dd7a4ae 100644 --- a/sailfish/qml/pages/itemdetails/FilmPage.qml +++ b/sailfish/qml/pages/itemdetails/FilmPage.qml @@ -20,7 +20,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../../components" import "../.." diff --git a/sailfish/qml/pages/itemdetails/MusicAlbumPage.qml b/sailfish/qml/pages/itemdetails/MusicAlbumPage.qml index 37cc64e..bf9be2c 100644 --- a/sailfish/qml/pages/itemdetails/MusicAlbumPage.qml +++ b/sailfish/qml/pages/itemdetails/MusicAlbumPage.qml @@ -20,7 +20,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import QtQuick.Layouts 1.1 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../../components" import "../../components/music" @@ -33,13 +33,15 @@ BaseDetailPage { readonly property bool _twoColumns: albumPageRoot.width / Theme.pixelRatio >= 800 - UserItemModel { + J.ItemModel { id: collectionModel - apiClient: ApiClient - sortBy: ["SortName"] - fields: ["ItemCounts","PrimaryImageAspectRatio","BasicSyncInfo","CanDelete","MediaSourceCount"] - parentId: itemData.jellyfinId - onParentIdChanged: reload() + loader: J.UserItemsLoader { + apiClient: ApiClient + //sortBy: ["SortName"] + //fields: ["ItemCounts","PrimaryImageAspectRatio","BasicSyncInfo","CanDelete","MediaSourceCount"] + parentId: itemData.jellyfinId + onParentIdChanged: reload() + } } RowLayout { anchors.fill: parent @@ -50,9 +52,9 @@ BaseDetailPage { visible: _twoColumns Layout.minimumWidth: 1000 / Theme.pixelRatio Layout.fillHeight: true - source: visible + /*source: visible ? "../../components/music/WideAlbumCover.qml" : "" - onLoaded: bindAlbum(item) + onLoaded: bindAlbum(item)*/ } Item {height: 1; width: Theme.horizontalPageMargin; visible: wideAlbumCover.visible; } SilicaListView { @@ -62,8 +64,8 @@ BaseDetailPage { model: collectionModel header: Loader { width: parent.width - source: "../../components/music/NarrowAlbumCover.qml" - onLoaded: bindAlbum(item) + /*source: "../../components/music/NarrowAlbumCover.qml" + onLoaded: bindAlbum(item)*/ } section { property: "parentIndexNumber" @@ -84,14 +86,8 @@ BaseDetailPage { } } - Connections { - target: itemData - onAlbumArtistsChanged: { - } - } - function bindAlbum(item) { - item.albumArt = Qt.binding(function(){ return Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})}) + //item.albumArt = Qt.binding(function(){ return Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})}) item.name = Qt.binding(function(){ return itemData.name}) item.releaseYear = Qt.binding(function() { return itemData.productionYear}) item.albumArtist = Qt.binding(function() { return itemData.albumArtist}) diff --git a/sailfish/qml/pages/itemdetails/VideoPage.qml b/sailfish/qml/pages/itemdetails/VideoPage.qml index 4c8bc4f..d230eba 100644 --- a/sailfish/qml/pages/itemdetails/VideoPage.qml +++ b/sailfish/qml/pages/itemdetails/VideoPage.qml @@ -20,7 +20,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../../components" import "../.." @@ -33,11 +33,11 @@ BaseDetailPage { property alias subtitle: pageHeader.description default property alias _data: content.data property real _playbackProsition: itemData.userData.playbackPositionTicks - readonly property bool _userdataReady: itemData.status == JellyfinItem.Ready && itemData.userData != null + readonly property bool _userdataReady: itemData.status === J.ItemLoader.Ready && itemData.userData !== null SilicaFlickable { anchors.fill: parent contentHeight: content.height + Theme.paddingLarge - visible: itemData.status !== JellyfinItem.Error + visible: itemData.status !== J.ItemLoader.Error VerticalScrollDecorator {} @@ -84,7 +84,7 @@ BaseDetailPage { Connections { target: itemData onStatusChanged: { - if (status == JellyfinItem.Ready) { + if (status === J.ItemLoader.Ready) { console.log(itemData.mediaStreams) } } diff --git a/sailfish/qml/pages/settings/DebugPage.qml b/sailfish/qml/pages/settings/DebugPage.qml index b01f57b..7717415 100644 --- a/sailfish/qml/pages/settings/DebugPage.qml +++ b/sailfish/qml/pages/settings/DebugPage.qml @@ -19,9 +19,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../../components" +import "../.." Page { id: page @@ -53,7 +54,7 @@ Page { label: qsTr("Connection state") value: { var stateText - switch( ApiClient.websocket.state) { + switch(ApiClient.websocket.state) { case 0: //- Socket state stateText = qsTr("Unconnected"); diff --git a/sailfish/qml/pages/setup/AddServerConnectingPage.qml b/sailfish/qml/pages/setup/AddServerConnectingPage.qml index 2549afe..8c02fe0 100644 --- a/sailfish/qml/pages/setup/AddServerConnectingPage.qml +++ b/sailfish/qml/pages/setup/AddServerConnectingPage.qml @@ -18,7 +18,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J + +import "../.." /** * Page to indicate that the application is connecting to a server. diff --git a/sailfish/qml/pages/setup/AddServerPage.qml b/sailfish/qml/pages/setup/AddServerPage.qml index 8c218f0..d8810bb 100644 --- a/sailfish/qml/pages/setup/AddServerPage.qml +++ b/sailfish/qml/pages/setup/AddServerPage.qml @@ -18,7 +18,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J + +import "../../" /** * Dialog showed when the user has to connect to a Jellyfin server. @@ -49,7 +51,7 @@ Dialog { title: qsTr("Connect to Jellyfin") } - ServerDiscoveryModel { + J.ServerDiscoveryModel { id: serverModel } diff --git a/sailfish/qml/pages/setup/LoginDialog.qml b/sailfish/qml/pages/setup/LoginDialog.qml index 569bfe9..007bbc3 100644 --- a/sailfish/qml/pages/setup/LoginDialog.qml +++ b/sailfish/qml/pages/setup/LoginDialog.qml @@ -18,9 +18,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../../components" +import "../../" /** * Page where the user can login on their server. Is displayed after the AddServerPage successfully connected @@ -31,7 +32,7 @@ Dialog { id: loginDialog property string loginMessage property Page firstPage - property User selectedUser: null + property QtObject /*User*/ selectedUser: null property string error @@ -65,11 +66,13 @@ Dialog { } } - PublicUserModel { + QtObject { id: userModel; } + + /*PublicUserModel { id: userModel apiClient: ApiClient Component.onCompleted: reload(); - } + }*/ DialogHeader { id: dialogHeader @@ -97,7 +100,7 @@ Dialog { width: parent.width Repeater { id: userRepeater - model: userModel + model: 0 //userModel delegate: UserGridDelegate { name: model.name image: model.primaryImageTag ? "%1/Users/%2/Images/Primary?tag=%3".arg(ApiClient.baseUrl).arg(model.jellyfinId).arg(model.primaryImageTag) : "" diff --git a/sailfish/qml/qmldir b/sailfish/qml/qmldir index 0b84cf2..401c447 100644 --- a/sailfish/qml/qmldir +++ b/sailfish/qml/qmldir @@ -16,4 +16,5 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA singleton Constants 1.0 Constants.qml +singleton ApiClient 1.0 ApiClient.qml Utils 1.0 Utils.js diff --git a/sailfish/src/harbour-sailfin.cpp b/sailfish/src/harbour-sailfin.cpp index f3a86ca..b3d1d44 100644 --- a/sailfish/src/harbour-sailfin.cpp +++ b/sailfish/src/harbour-sailfin.cpp @@ -60,14 +60,14 @@ int main(int argc, char *argv[]) { QCommandLineParser cmdParser; cmdParser.addHelpOption(); cmdParser.addVersionOption(); - QCommandLineOption sandboxOption("attempt-sandbox", app->translate("Command line argument description", "Try to start with FireJail.")); + QCommandLineOption sandboxOption("no-attempt-sandbox", app->translate("Command line argument description", "Try to not start with FireJail.")); if (canSanbox) { cmdParser.addOption(sandboxOption); } cmdParser.process(*app); - if (canSanbox && cmdParser.isSet(sandboxOption)) { - qDebug() << "Restarting in Sanbox mode"; + if (canSanbox && !cmdParser.isSet(sandboxOption)) { + qDebug() << "Restarting in sandbox mode"; QProcess::execute(QString(SANDBOX_PROGRAM), QStringList() << "-p" << "harbour-sailfin.desktop" << "/usr/bin/harbour-sailfin"); return 0;