mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 09:15:18 +00:00
WIP: Add playlists/queues and add support for Sailfish back
This commit is contained in:
parent
fbc154fb56
commit
86672be051
110
cmake/QmlPlugin.cmake
Normal file
110
cmake/QmlPlugin.cmake
Normal file
|
@ -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 $<TARGET_FILE_DIR:${TARGET}>/qmldir)
|
||||||
|
set(COPY_QMLFILES_COMMAND ${CMAKE_COMMAND} -E copy ${QMLPLUGIN_QMLFILES} $<TARGET_FILE_DIR:${TARGET}>)
|
||||||
|
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()
|
|
@ -8,8 +8,11 @@ set(JellyfinQt_SOURCES
|
||||||
src/model/deviceprofile.cpp
|
src/model/deviceprofile.cpp
|
||||||
src/model/item.cpp
|
src/model/item.cpp
|
||||||
src/model/playlist.cpp
|
src/model/playlist.cpp
|
||||||
|
src/model/shuffle.cpp
|
||||||
|
|
||||||
src/support/jsonconv.cpp
|
src/support/jsonconv.cpp
|
||||||
src/support/loader.cpp
|
src/support/loader.cpp
|
||||||
|
src/support/parseexception.cpp
|
||||||
src/viewmodel/item.cpp
|
src/viewmodel/item.cpp
|
||||||
src/viewmodel/itemmodel.cpp
|
src/viewmodel/itemmodel.cpp
|
||||||
src/viewmodel/loader.cpp
|
src/viewmodel/loader.cpp
|
||||||
|
@ -31,8 +34,11 @@ set(JellyfinQt_HEADERS
|
||||||
include/JellyfinQt/model/deviceprofile.h
|
include/JellyfinQt/model/deviceprofile.h
|
||||||
include/JellyfinQt/model/item.h
|
include/JellyfinQt/model/item.h
|
||||||
include/JellyfinQt/model/playlist.h
|
include/JellyfinQt/model/playlist.h
|
||||||
|
include/JellyfinQt/model/shuffle.h
|
||||||
include/JellyfinQt/support/jsonconv.h
|
include/JellyfinQt/support/jsonconv.h
|
||||||
|
include/JellyfinQt/support/jsonconvimpl.h
|
||||||
include/JellyfinQt/support/loader.h
|
include/JellyfinQt/support/loader.h
|
||||||
|
include/JellyfinQt/support/parseexception.h
|
||||||
include/JellyfinQt/viewmodel/item.h
|
include/JellyfinQt/viewmodel/item.h
|
||||||
include/JellyfinQt/viewmodel/itemmodel.h
|
include/JellyfinQt/viewmodel/itemmodel.h
|
||||||
include/JellyfinQt/viewmodel/loader.h
|
include/JellyfinQt/viewmodel/loader.h
|
||||||
|
|
|
@ -231,6 +231,9 @@ public slots:
|
||||||
protected slots:
|
protected slots:
|
||||||
void defaultNetworkErrorHandler(QNetworkReply::NetworkError error);
|
void defaultNetworkErrorHandler(QNetworkReply::NetworkError error);
|
||||||
void onUserDataChanged(const QString &itemId, UserData *newData);
|
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:
|
protected:
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -108,8 +108,7 @@ signals:
|
||||||
*/
|
*/
|
||||||
void itemsLoaded();
|
void itemsLoaded();
|
||||||
void reloadWanted();
|
void reloadWanted();
|
||||||
|
public slots:
|
||||||
protected slots:
|
|
||||||
virtual void futureReady() = 0;
|
virtual void futureReady() = 0;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -243,6 +242,18 @@ bool setRequestStartIndex(P ¶meters, int startIndex) {
|
||||||
return false;
|
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<DTO::BaseItemDto> extractRecords(const DTO::BaseItemDtoQueryResult &result);
|
||||||
|
extern template int extractTotalRecordCount(const DTO::BaseItemDtoQueryResult &result);
|
||||||
|
extern template QList<DTO::BaseItemDto> extractRecords(const QList<DTO::BaseItemDto> &result);
|
||||||
|
extern template int extractTotalRecordCount(const QList<DTO::BaseItemDto> &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.
|
* Template for implementing a loader for the given type, response and parameters using Jellyfin::Support:Loaders.
|
||||||
*
|
*
|
||||||
|
@ -301,7 +312,7 @@ protected:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
result = optResult.value();
|
result = optResult.value();
|
||||||
} catch (Support::LoadException e) {
|
} catch (Support::LoadException &e) {
|
||||||
qWarning() << "Exception while loading: " << e.what();
|
qWarning() << "Exception while loading: " << e.what();
|
||||||
this->setStatus(ViewModel::ModelStatus::Error);
|
this->setStatus(ViewModel::ModelStatus::Error);
|
||||||
return;
|
return;
|
||||||
|
@ -430,11 +441,11 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
// QList-like API
|
// 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.
|
* @return the amount of objects in this model.
|
||||||
*/
|
*/
|
||||||
int size() {
|
int size() const {
|
||||||
return m_array.size();
|
return m_array.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -101,7 +101,7 @@ public:
|
||||||
signals:
|
signals:
|
||||||
void tokenRetrieved(const QString &server, const QString &user, const QString &token) const;
|
void tokenRetrieved(const QString &server, const QString &user, const QString &token) const;
|
||||||
void serversListed(const QStringList &servers) const;
|
void serversListed(const QStringList &servers) const;
|
||||||
void usersListed(const QStringList &users) const;
|
void usersListed(const QString& server, const QStringList &users) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
explicit CredentialsManager(QObject *parent = nullptr) : QObject (parent) {}
|
explicit CredentialsManager(QObject *parent = nullptr) : QObject (parent) {}
|
||||||
|
|
|
@ -68,7 +68,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<LibraryOptions> m_libraryOptions = nullptr;
|
QSharedPointer<LibraryOptions> m_libraryOptions = QSharedPointer<LibraryOptions>();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // NS DTO
|
} // NS DTO
|
||||||
|
|
|
@ -94,7 +94,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<AlbumInfo> m_searchInfo = nullptr;
|
QSharedPointer<AlbumInfo> m_searchInfo = QSharedPointer<AlbumInfo>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_searchProviderName;
|
QString m_searchProviderName;
|
||||||
bool m_includeDisabledProviders;
|
bool m_includeDisabledProviders;
|
||||||
|
|
|
@ -78,9 +78,9 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<ThemeMediaResult> m_themeVideosResult = nullptr;
|
QSharedPointer<ThemeMediaResult> m_themeVideosResult = QSharedPointer<ThemeMediaResult>();
|
||||||
QSharedPointer<ThemeMediaResult> m_themeSongsResult = nullptr;
|
QSharedPointer<ThemeMediaResult> m_themeSongsResult = QSharedPointer<ThemeMediaResult>();
|
||||||
QSharedPointer<ThemeMediaResult> m_soundtrackSongsResult = nullptr;
|
QSharedPointer<ThemeMediaResult> m_soundtrackSongsResult = QSharedPointer<ThemeMediaResult>();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // NS DTO
|
} // NS DTO
|
||||||
|
|
|
@ -94,7 +94,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<ArtistInfo> m_searchInfo = nullptr;
|
QSharedPointer<ArtistInfo> m_searchInfo = QSharedPointer<ArtistInfo>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_searchProviderName;
|
QString m_searchProviderName;
|
||||||
bool m_includeDisabledProviders;
|
bool m_includeDisabledProviders;
|
||||||
|
|
|
@ -89,8 +89,8 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<UserDto> m_user = nullptr;
|
QSharedPointer<UserDto> m_user = QSharedPointer<UserDto>();
|
||||||
QSharedPointer<SessionInfo> m_sessionInfo = nullptr;
|
QSharedPointer<SessionInfo> m_sessionInfo = QSharedPointer<SessionInfo>();
|
||||||
QString m_accessToken;
|
QString m_accessToken;
|
||||||
QString m_serverId;
|
QString m_serverId;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1611,7 +1611,7 @@ protected:
|
||||||
QString m_parentBackdropItemId;
|
QString m_parentBackdropItemId;
|
||||||
QStringList m_parentBackdropImageTags;
|
QStringList m_parentBackdropImageTags;
|
||||||
std::optional<qint32> m_localTrailerCount = std::nullopt;
|
std::optional<qint32> m_localTrailerCount = std::nullopt;
|
||||||
QSharedPointer<UserItemDataDto> m_userData = nullptr;
|
QSharedPointer<UserItemDataDto> m_userData = QSharedPointer<UserItemDataDto>();
|
||||||
std::optional<qint32> m_recursiveItemCount = std::nullopt;
|
std::optional<qint32> m_recursiveItemCount = std::nullopt;
|
||||||
std::optional<qint32> m_childCount = std::nullopt;
|
std::optional<qint32> m_childCount = std::nullopt;
|
||||||
QString m_seriesName;
|
QString m_seriesName;
|
||||||
|
@ -1699,7 +1699,7 @@ protected:
|
||||||
std::optional<bool> m_isKids = std::nullopt;
|
std::optional<bool> m_isKids = std::nullopt;
|
||||||
std::optional<bool> m_isPremiere = std::nullopt;
|
std::optional<bool> m_isPremiere = std::nullopt;
|
||||||
QString m_timerId;
|
QString m_timerId;
|
||||||
QSharedPointer<BaseItemDto> m_currentProgram = nullptr;
|
QSharedPointer<BaseItemDto> m_currentProgram = QSharedPointer<BaseItemDto>();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // NS DTO
|
} // NS DTO
|
||||||
|
|
|
@ -94,7 +94,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<BookInfo> m_searchInfo = nullptr;
|
QSharedPointer<BookInfo> m_searchInfo = QSharedPointer<BookInfo>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_searchProviderName;
|
QString m_searchProviderName;
|
||||||
bool m_includeDisabledProviders;
|
bool m_includeDisabledProviders;
|
||||||
|
|
|
@ -94,7 +94,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<BoxSetInfo> m_searchInfo = nullptr;
|
QSharedPointer<BoxSetInfo> m_searchInfo = QSharedPointer<BoxSetInfo>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_searchProviderName;
|
QString m_searchProviderName;
|
||||||
bool m_includeDisabledProviders;
|
bool m_includeDisabledProviders;
|
||||||
|
|
|
@ -134,7 +134,7 @@ protected:
|
||||||
QString m_messageCallbackUrl;
|
QString m_messageCallbackUrl;
|
||||||
bool m_supportsPersistentIdentifier;
|
bool m_supportsPersistentIdentifier;
|
||||||
bool m_supportsSync;
|
bool m_supportsSync;
|
||||||
QSharedPointer<DeviceProfile> m_deviceProfile = nullptr;
|
QSharedPointer<DeviceProfile> m_deviceProfile = QSharedPointer<DeviceProfile>();
|
||||||
QString m_appStoreUrl;
|
QString m_appStoreUrl;
|
||||||
QString m_iconUrl;
|
QString m_iconUrl;
|
||||||
};
|
};
|
||||||
|
|
|
@ -170,7 +170,7 @@ protected:
|
||||||
QString m_messageCallbackUrl;
|
QString m_messageCallbackUrl;
|
||||||
bool m_supportsPersistentIdentifier;
|
bool m_supportsPersistentIdentifier;
|
||||||
bool m_supportsSync;
|
bool m_supportsSync;
|
||||||
QSharedPointer<DeviceProfile> m_deviceProfile = nullptr;
|
QSharedPointer<DeviceProfile> m_deviceProfile = QSharedPointer<DeviceProfile>();
|
||||||
QString m_appStoreUrl;
|
QString m_appStoreUrl;
|
||||||
QString m_iconUrl;
|
QString m_iconUrl;
|
||||||
};
|
};
|
||||||
|
|
|
@ -153,7 +153,7 @@ protected:
|
||||||
QString m_appVersion;
|
QString m_appVersion;
|
||||||
QString m_lastUserId;
|
QString m_lastUserId;
|
||||||
QDateTime m_dateLastActivity;
|
QDateTime m_dateLastActivity;
|
||||||
QSharedPointer<ClientCapabilities> m_capabilities = nullptr;
|
QSharedPointer<ClientCapabilities> m_capabilities = QSharedPointer<ClientCapabilities>();
|
||||||
QString m_iconUrl;
|
QString m_iconUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -478,7 +478,7 @@ public:
|
||||||
protected:
|
protected:
|
||||||
QString m_name;
|
QString m_name;
|
||||||
QString m_jellyfinId;
|
QString m_jellyfinId;
|
||||||
QSharedPointer<DeviceIdentification> m_identification = nullptr;
|
QSharedPointer<DeviceIdentification> m_identification = QSharedPointer<DeviceIdentification>();
|
||||||
QString m_friendlyName;
|
QString m_friendlyName;
|
||||||
QString m_manufacturer;
|
QString m_manufacturer;
|
||||||
QString m_manufacturerUrl;
|
QString m_manufacturerUrl;
|
||||||
|
|
|
@ -124,7 +124,7 @@ public:
|
||||||
protected:
|
protected:
|
||||||
QString m_guid;
|
QString m_guid;
|
||||||
QString m_name;
|
QString m_name;
|
||||||
QSharedPointer<Version> m_version = nullptr;
|
QSharedPointer<Version> m_version = QSharedPointer<Version>();
|
||||||
QString m_changelog;
|
QString m_changelog;
|
||||||
QString m_sourceUrl;
|
QString m_sourceUrl;
|
||||||
QString m_checksum;
|
QString m_checksum;
|
||||||
|
|
|
@ -134,7 +134,7 @@ protected:
|
||||||
QString m_name;
|
QString m_name;
|
||||||
QString m_description;
|
QString m_description;
|
||||||
QString m_jellyfinId;
|
QString m_jellyfinId;
|
||||||
QSharedPointer<Version> m_version = nullptr;
|
QSharedPointer<Version> m_version = QSharedPointer<Version>();
|
||||||
QString m_assemblyFilePath;
|
QString m_assemblyFilePath;
|
||||||
bool m_canUninstall;
|
bool m_canUninstall;
|
||||||
QString m_dataFolderPath;
|
QString m_dataFolderPath;
|
||||||
|
|
|
@ -68,7 +68,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<MediaSourceInfo> m_mediaSource = nullptr;
|
QSharedPointer<MediaSourceInfo> m_mediaSource = QSharedPointer<MediaSourceInfo>();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // NS DTO
|
} // NS DTO
|
||||||
|
|
|
@ -91,7 +91,7 @@ public:
|
||||||
protected:
|
protected:
|
||||||
QString m_name;
|
QString m_name;
|
||||||
QString m_path;
|
QString m_path;
|
||||||
QSharedPointer<MediaPathInfo> m_pathInfo = nullptr;
|
QSharedPointer<MediaPathInfo> m_pathInfo = QSharedPointer<MediaPathInfo>();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // NS DTO
|
} // NS DTO
|
||||||
|
|
|
@ -94,7 +94,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<MovieInfo> m_searchInfo = nullptr;
|
QSharedPointer<MovieInfo> m_searchInfo = QSharedPointer<MovieInfo>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_searchProviderName;
|
QString m_searchProviderName;
|
||||||
bool m_includeDisabledProviders;
|
bool m_includeDisabledProviders;
|
||||||
|
|
|
@ -94,7 +94,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<MusicVideoInfo> m_searchInfo = nullptr;
|
QSharedPointer<MusicVideoInfo> m_searchInfo = QSharedPointer<MusicVideoInfo>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_searchProviderName;
|
QString m_searchProviderName;
|
||||||
bool m_includeDisabledProviders;
|
bool m_includeDisabledProviders;
|
||||||
|
|
|
@ -215,7 +215,7 @@ protected:
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
std::optional<bool> m_enableDirectPlay = std::nullopt;
|
std::optional<bool> m_enableDirectPlay = std::nullopt;
|
||||||
std::optional<bool> m_enableDirectStream = std::nullopt;
|
std::optional<bool> m_enableDirectStream = std::nullopt;
|
||||||
QSharedPointer<DeviceProfile> m_deviceProfile = nullptr;
|
QSharedPointer<DeviceProfile> m_deviceProfile = QSharedPointer<DeviceProfile>();
|
||||||
QList<MediaProtocol> m_directPlayProtocols;
|
QList<MediaProtocol> m_directPlayProtocols;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<PersonLookupInfo> m_searchInfo = nullptr;
|
QSharedPointer<PersonLookupInfo> m_searchInfo = QSharedPointer<PersonLookupInfo>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_searchProviderName;
|
QString m_searchProviderName;
|
||||||
bool m_includeDisabledProviders;
|
bool m_includeDisabledProviders;
|
||||||
|
|
|
@ -231,7 +231,7 @@ protected:
|
||||||
std::optional<qint32> m_maxAudioChannels = std::nullopt;
|
std::optional<qint32> m_maxAudioChannels = std::nullopt;
|
||||||
QString m_mediaSourceId;
|
QString m_mediaSourceId;
|
||||||
QString m_liveStreamId;
|
QString m_liveStreamId;
|
||||||
QSharedPointer<DeviceProfile> m_deviceProfile = nullptr;
|
QSharedPointer<DeviceProfile> m_deviceProfile = QSharedPointer<DeviceProfile>();
|
||||||
std::optional<bool> m_enableDirectPlay = std::nullopt;
|
std::optional<bool> m_enableDirectPlay = std::nullopt;
|
||||||
std::optional<bool> m_enableDirectStream = std::nullopt;
|
std::optional<bool> m_enableDirectStream = std::nullopt;
|
||||||
std::optional<bool> m_enableTranscoding = std::nullopt;
|
std::optional<bool> m_enableTranscoding = std::nullopt;
|
||||||
|
|
|
@ -244,7 +244,7 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool m_canSeek;
|
bool m_canSeek;
|
||||||
QSharedPointer<BaseItemDto> m_item = nullptr;
|
QSharedPointer<BaseItemDto> m_item = QSharedPointer<BaseItemDto>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_sessionId;
|
QString m_sessionId;
|
||||||
QString m_mediaSourceId;
|
QString m_mediaSourceId;
|
||||||
|
|
|
@ -244,7 +244,7 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool m_canSeek;
|
bool m_canSeek;
|
||||||
QSharedPointer<BaseItemDto> m_item = nullptr;
|
QSharedPointer<BaseItemDto> m_item = QSharedPointer<BaseItemDto>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_sessionId;
|
QString m_sessionId;
|
||||||
QString m_mediaSourceId;
|
QString m_mediaSourceId;
|
||||||
|
|
|
@ -166,7 +166,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<BaseItemDto> m_item = nullptr;
|
QSharedPointer<BaseItemDto> m_item = QSharedPointer<BaseItemDto>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_sessionId;
|
QString m_sessionId;
|
||||||
QString m_mediaSourceId;
|
QString m_mediaSourceId;
|
||||||
|
|
|
@ -136,7 +136,7 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QString m_name;
|
QString m_name;
|
||||||
QSharedPointer<Version> m_version = nullptr;
|
QSharedPointer<Version> m_version = QSharedPointer<Version>();
|
||||||
QString m_configurationFileName;
|
QString m_configurationFileName;
|
||||||
QString m_description;
|
QString m_description;
|
||||||
QString m_jellyfinId;
|
QString m_jellyfinId;
|
||||||
|
|
|
@ -170,7 +170,7 @@ protected:
|
||||||
QString m_imageUrl;
|
QString m_imageUrl;
|
||||||
QString m_searchProviderName;
|
QString m_searchProviderName;
|
||||||
QString m_overview;
|
QString m_overview;
|
||||||
QSharedPointer<RemoteSearchResult> m_albumArtist = nullptr;
|
QSharedPointer<RemoteSearchResult> m_albumArtist = QSharedPointer<RemoteSearchResult>();
|
||||||
QList<RemoteSearchResult> m_artists;
|
QList<RemoteSearchResult> m_artists;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<SeriesInfo> m_searchInfo = nullptr;
|
QSharedPointer<SeriesInfo> m_searchInfo = QSharedPointer<SeriesInfo>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_searchProviderName;
|
QString m_searchProviderName;
|
||||||
bool m_includeDisabledProviders;
|
bool m_includeDisabledProviders;
|
||||||
|
|
|
@ -819,7 +819,7 @@ protected:
|
||||||
qint32 m_logFileRetentionDays;
|
qint32 m_logFileRetentionDays;
|
||||||
bool m_isStartupWizardCompleted;
|
bool m_isStartupWizardCompleted;
|
||||||
QString m_cachePath;
|
QString m_cachePath;
|
||||||
QSharedPointer<Version> m_previousVersion = nullptr;
|
QSharedPointer<Version> m_previousVersion = QSharedPointer<Version>();
|
||||||
QString m_previousVersionStr;
|
QString m_previousVersionStr;
|
||||||
bool m_enableUPnP;
|
bool m_enableUPnP;
|
||||||
bool m_enableMetrics;
|
bool m_enableMetrics;
|
||||||
|
|
|
@ -300,9 +300,9 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<PlayerStateInfo> m_playState = nullptr;
|
QSharedPointer<PlayerStateInfo> m_playState = QSharedPointer<PlayerStateInfo>();
|
||||||
QList<SessionUserInfo> m_additionalUsers;
|
QList<SessionUserInfo> m_additionalUsers;
|
||||||
QSharedPointer<ClientCapabilities> m_capabilities = nullptr;
|
QSharedPointer<ClientCapabilities> m_capabilities = QSharedPointer<ClientCapabilities>();
|
||||||
QString m_remoteEndPoint;
|
QString m_remoteEndPoint;
|
||||||
QStringList m_playableMediaTypes;
|
QStringList m_playableMediaTypes;
|
||||||
QString m_jellyfinId;
|
QString m_jellyfinId;
|
||||||
|
@ -313,12 +313,12 @@ protected:
|
||||||
QDateTime m_lastPlaybackCheckIn;
|
QDateTime m_lastPlaybackCheckIn;
|
||||||
QString m_deviceName;
|
QString m_deviceName;
|
||||||
QString m_deviceType;
|
QString m_deviceType;
|
||||||
QSharedPointer<BaseItemDto> m_nowPlayingItem = nullptr;
|
QSharedPointer<BaseItemDto> m_nowPlayingItem = QSharedPointer<BaseItemDto>();
|
||||||
QSharedPointer<BaseItem> m_fullNowPlayingItem = nullptr;
|
QSharedPointer<BaseItem> m_fullNowPlayingItem = QSharedPointer<BaseItem>();
|
||||||
QSharedPointer<BaseItemDto> m_nowViewingItem = nullptr;
|
QSharedPointer<BaseItemDto> m_nowViewingItem = QSharedPointer<BaseItemDto>();
|
||||||
QString m_deviceId;
|
QString m_deviceId;
|
||||||
QString m_applicationVersion;
|
QString m_applicationVersion;
|
||||||
QSharedPointer<TranscodingInfo> m_transcodingInfo = nullptr;
|
QSharedPointer<TranscodingInfo> m_transcodingInfo = QSharedPointer<TranscodingInfo>();
|
||||||
bool m_isActive;
|
bool m_isActive;
|
||||||
bool m_supportsMediaControl;
|
bool m_supportsMediaControl;
|
||||||
bool m_supportsRemoteControl;
|
bool m_supportsRemoteControl;
|
||||||
|
|
|
@ -168,7 +168,7 @@ protected:
|
||||||
TaskState m_state;
|
TaskState m_state;
|
||||||
std::optional<double> m_currentProgressPercentage = std::nullopt;
|
std::optional<double> m_currentProgressPercentage = std::nullopt;
|
||||||
QString m_jellyfinId;
|
QString m_jellyfinId;
|
||||||
QSharedPointer<TaskResult> m_lastExecutionResult = nullptr;
|
QSharedPointer<TaskResult> m_lastExecutionResult = QSharedPointer<TaskResult>();
|
||||||
QList<TaskTriggerInfo> m_triggers;
|
QList<TaskTriggerInfo> m_triggers;
|
||||||
QString m_description;
|
QString m_description;
|
||||||
QString m_category;
|
QString m_category;
|
||||||
|
|
|
@ -362,7 +362,7 @@ protected:
|
||||||
QString m_seriesTimerId;
|
QString m_seriesTimerId;
|
||||||
QString m_externalSeriesTimerId;
|
QString m_externalSeriesTimerId;
|
||||||
std::optional<qint64> m_runTimeTicks = std::nullopt;
|
std::optional<qint64> m_runTimeTicks = std::nullopt;
|
||||||
QSharedPointer<BaseItemDto> m_programInfo = nullptr;
|
QSharedPointer<BaseItemDto> m_programInfo = QSharedPointer<BaseItemDto>();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // NS DTO
|
} // NS DTO
|
||||||
|
|
|
@ -94,7 +94,7 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSharedPointer<TrailerInfo> m_searchInfo = nullptr;
|
QSharedPointer<TrailerInfo> m_searchInfo = QSharedPointer<TrailerInfo>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_searchProviderName;
|
QString m_searchProviderName;
|
||||||
bool m_includeDisabledProviders;
|
bool m_includeDisabledProviders;
|
||||||
|
|
|
@ -79,7 +79,7 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QString m_jellyfinId;
|
QString m_jellyfinId;
|
||||||
QSharedPointer<LibraryOptions> m_libraryOptions = nullptr;
|
QSharedPointer<LibraryOptions> m_libraryOptions = QSharedPointer<LibraryOptions>();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // NS DTO
|
} // NS DTO
|
||||||
|
|
|
@ -213,8 +213,8 @@ protected:
|
||||||
std::optional<bool> m_enableAutoLogin = std::nullopt;
|
std::optional<bool> m_enableAutoLogin = std::nullopt;
|
||||||
QDateTime m_lastLoginDate;
|
QDateTime m_lastLoginDate;
|
||||||
QDateTime m_lastActivityDate;
|
QDateTime m_lastActivityDate;
|
||||||
QSharedPointer<UserConfiguration> m_configuration = nullptr;
|
QSharedPointer<UserConfiguration> m_configuration = QSharedPointer<UserConfiguration>();
|
||||||
QSharedPointer<UserPolicy> m_policy = nullptr;
|
QSharedPointer<UserPolicy> m_policy = QSharedPointer<UserPolicy>();
|
||||||
std::optional<double> m_primaryImageAspectRatio = std::nullopt;
|
std::optional<double> m_primaryImageAspectRatio = std::nullopt;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -158,7 +158,7 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QString m_version;
|
QString m_version;
|
||||||
QSharedPointer<Version> m_versionNumber = nullptr;
|
QSharedPointer<Version> m_versionNumber = QSharedPointer<Version>();
|
||||||
QString m_changelog;
|
QString m_changelog;
|
||||||
QString m_targetAbi;
|
QString m_targetAbi;
|
||||||
QString m_sourceUrl;
|
QString m_sourceUrl;
|
||||||
|
|
|
@ -143,7 +143,7 @@ protected:
|
||||||
QString m_name;
|
QString m_name;
|
||||||
QStringList m_locations;
|
QStringList m_locations;
|
||||||
QString m_collectionType;
|
QString m_collectionType;
|
||||||
QSharedPointer<LibraryOptions> m_libraryOptions = nullptr;
|
QSharedPointer<LibraryOptions> m_libraryOptions = QSharedPointer<LibraryOptions>();
|
||||||
QString m_itemId;
|
QString m_itemId;
|
||||||
QString m_primaryImageItemId;
|
QString m_primaryImageItemId;
|
||||||
std::optional<double> m_refreshProgress = std::nullopt;
|
std::optional<double> m_refreshProgress = std::nullopt;
|
||||||
|
|
|
@ -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
|
#ifndef JELLYFIN_MODEL_PLAYLIST_H
|
||||||
#define JELLYFIN_MODEL_PLAYLIST_H
|
#define JELLYFIN_MODEL_PLAYLIST_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
|
||||||
|
#include "../viewmodel/itemmodel.h"
|
||||||
|
#include "item.h"
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
namespace Model {
|
namespace Model {
|
||||||
|
|
||||||
// Forward declaration
|
class Shuffle;
|
||||||
class Item;
|
|
||||||
|
|
||||||
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:
|
public:
|
||||||
explicit Playlist();
|
explicit Playlist(QObject *parent = nullptr);
|
||||||
|
|
||||||
/// Start loading data for the next item.
|
/// Returns the current item in the queue
|
||||||
void preloadNext();
|
QSharedPointer<Item> currentItem();
|
||||||
|
QSharedPointer<Item> 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<const Item> 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:
|
private:
|
||||||
/// Extra data about each itemId that this playlist manages
|
void reshuffle();
|
||||||
struct ExtendedItem {
|
|
||||||
QSharedPointer<Item> 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
QVector<ExtendedItem> list;
|
QSharedPointer<Item> m_currentItem;
|
||||||
|
bool m_currentItemFromQueue = false;
|
||||||
|
QSharedPointer<Item> m_nextItem;
|
||||||
|
bool m_nextItemFromQueue = false;
|
||||||
|
|
||||||
|
/// list of the items in the queue
|
||||||
|
QVector<QSharedPointer<Item>> m_queue;
|
||||||
|
/// list of the items in the playlist
|
||||||
|
QVector<QSharedPointer<Item>> m_list;
|
||||||
|
/// The current position in the playlist
|
||||||
|
int m_pos = 0;
|
||||||
|
|
||||||
|
/// Algorithm for shuffling the playlist.
|
||||||
|
Shuffle *m_shuffler;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
166
core/include/JellyfinQt/model/shuffle.h
Normal file
166
core/include/JellyfinQt/model/shuffle.h
Normal file
|
@ -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<int> 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
|
|
@ -19,159 +19,31 @@
|
||||||
#ifndef JELLYFIN_SUPPORT_JSONCONV_H
|
#ifndef JELLYFIN_SUPPORT_JSONCONV_H
|
||||||
#define JELLYFIN_SUPPORT_JSONCONV_H
|
#define JELLYFIN_SUPPORT_JSONCONV_H
|
||||||
|
|
||||||
#include <QException>
|
#include "jsonconvimpl.h"
|
||||||
|
#include "parseexception.h"
|
||||||
#include <QtGlobal>
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QJsonValue>
|
|
||||||
#include <QList>
|
|
||||||
#include <QUuid>
|
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
namespace Support {
|
namespace Support {
|
||||||
|
|
||||||
// Helper functions
|
extern template int fromJsonValue<int>(const QJsonValue &source, convertType<int>);
|
||||||
QString uuidToString(const QUuid &source);
|
extern template qint64 fromJsonValue<qint64>(const QJsonValue &source, convertType<qint64>);
|
||||||
QUuid stringToUuid(const QString &source);
|
extern template bool fromJsonValue<bool>(const QJsonValue &source, convertType<bool>);
|
||||||
|
extern template QString fromJsonValue<QString>(const QJsonValue &source, convertType<QString>);
|
||||||
/**
|
extern template QStringList fromJsonValue<QStringList>(const QJsonValue &source, convertType<QStringList>);
|
||||||
* @brief Thrown when JSON cannot be parsed.
|
extern template QJsonObject fromJsonValue<QJsonObject>(const QJsonValue &source, convertType<QJsonObject>);
|
||||||
*/
|
extern template double fromJsonValue<double>(const QJsonValue &source, convertType<double>);
|
||||||
class ParseException : public QException {
|
extern template float fromJsonValue<float>(const QJsonValue &source, convertType<float>);
|
||||||
public:
|
extern template QDateTime fromJsonValue<QDateTime>(const QJsonValue &source, convertType<QDateTime>);
|
||||||
explicit ParseException(const QString &message)
|
extern template QVariant fromJsonValue<QVariant>(const QJsonValue &source, convertType<QVariant>);
|
||||||
: m_message(message.toStdString()) {}
|
extern template QUuid fromJsonValue<QUuid>(const QJsonValue &source, convertType<QUuid>);
|
||||||
|
|
||||||
/*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 <typename T>
|
|
||||||
struct convertType{};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Template for converting types from JSON into their respective type.
|
|
||||||
*/
|
|
||||||
template <typename T>
|
|
||||||
T fromJsonValue(const QJsonValue &source, convertType<T>) {
|
|
||||||
Q_UNUSED(source)
|
|
||||||
Q_ASSERT_X(false, "fromJsonValue<T>", "fromJsonValue called with unimplemented type");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
QJsonValue toJsonValue(const T &source, convertType<T>) {
|
|
||||||
Q_UNUSED(source)
|
|
||||||
std::string msg = "toJsonValue called with unimplemented type ";
|
|
||||||
msg += typeid (T).name();
|
|
||||||
Q_ASSERT_X(false, "toJsonValue<T>", msg.c_str());
|
|
||||||
return QJsonValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
T fromJsonValue(const QJsonValue &source) {
|
|
||||||
return fromJsonValue(source, convertType<T>{});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
QJsonValue toJsonValue(const T &source) {
|
|
||||||
return toJsonValue(source, convertType<T>{});
|
|
||||||
}
|
|
||||||
|
|
||||||
// QList
|
|
||||||
template <typename T>
|
|
||||||
QList<T> fromJsonValue(const QJsonValue &source, convertType<QList<T>>) {
|
|
||||||
QList<T> result;
|
|
||||||
QJsonArray arr = source.toArray();
|
|
||||||
result.reserve(arr.size());
|
|
||||||
for (auto it = arr.cbegin(); it != arr.cend(); it++) {
|
|
||||||
result.append(fromJsonValue<T>(*it));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
QJsonValue toJsonValue(const QList<T> &source, convertType<QList<T>>) {
|
|
||||||
QJsonArray result;
|
|
||||||
for (auto it = source.cbegin(); it != source.cend(); it++) {
|
|
||||||
result.push_back(toJsonValue<T>(*it));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
std::optional<T> fromJsonValue(const QJsonValue &source, convertType<std::optional<T>>) {
|
|
||||||
if (source.isNull()) {
|
|
||||||
return std::nullopt;
|
|
||||||
} else {
|
|
||||||
return fromJsonValue<T>(source, convertType<T>{});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
QJsonValue toJsonValue(const std::optional<T> &source, convertType<std::optional<T>>) {
|
|
||||||
if (source.has_value()) {
|
|
||||||
return toJsonValue<T>(source.value(), convertType<T>{});
|
|
||||||
} else {
|
|
||||||
// Null
|
|
||||||
return QJsonValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// QSharedPointer
|
|
||||||
template <typename T>
|
|
||||||
QSharedPointer<T> fromJsonValue(const QJsonValue &source, convertType<QSharedPointer<T>>) {
|
|
||||||
if (source.isNull()) {
|
|
||||||
return QSharedPointer<T>();
|
|
||||||
}
|
|
||||||
return QSharedPointer<T>::create(fromJsonValue<T>(source));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
QJsonValue toJsonValue(const QSharedPointer<T> &source, convertType<QSharedPointer<T>>) {
|
|
||||||
if (source.isNull()) {
|
|
||||||
return QJsonValue();
|
|
||||||
}
|
|
||||||
return toJsonValue<T>(*source);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Templates for string conversion.
|
|
||||||
*/
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
QString toString(const T &source, convertType<T>) {
|
|
||||||
return toJsonValue(source).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
QString toString(const std::optional<T> &source, convertType<std::optional<T>>) {
|
|
||||||
if (source.has_value()) {
|
|
||||||
return toString<T>(source.value(), convertType<T>{});
|
|
||||||
} else {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
QString toString(const T &source) {
|
|
||||||
return toString(source, convertType<T>{});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
extern template QString toString(const QUuid &source, convertType<QUuid>);
|
||||||
|
extern template QString toString(const qint32 &source, convertType<qint32>);
|
||||||
|
extern template QString toString(const qint64 &source, convertType<qint64>);
|
||||||
|
extern template QString toString(const float &source, convertType<float>);
|
||||||
|
extern template QString toString(const double &source, convertType<double>);
|
||||||
|
extern template QString toString(const bool &source, convertType<bool>);
|
||||||
|
extern template QString toString(const QString &source, convertType<QString>);
|
||||||
|
|
||||||
} // NS Support
|
} // NS Support
|
||||||
} // NS Jellyfin
|
} // NS Jellyfin
|
||||||
|
|
165
core/include/JellyfinQt/support/jsonconvimpl.h
Normal file
165
core/include/JellyfinQt/support/jsonconvimpl.h
Normal file
|
@ -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 <QtGlobal>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonValue>
|
||||||
|
#include <QList>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
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 <typename T>
|
||||||
|
struct convertType{};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template for converting types from JSON into their respective type.
|
||||||
|
*/
|
||||||
|
template <typename T>
|
||||||
|
T fromJsonValue(const QJsonValue &source, convertType<T>) {
|
||||||
|
Q_UNUSED(source)
|
||||||
|
Q_ASSERT_X(false, "fromJsonValue<T>", "fromJsonValue called with unimplemented type");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template for converting types from their type to JSON.
|
||||||
|
*/
|
||||||
|
template <typename T>
|
||||||
|
QJsonValue toJsonValue(const T &source, convertType<T>) {
|
||||||
|
Q_UNUSED(source)
|
||||||
|
std::string msg = "toJsonValue called with unimplemented type ";
|
||||||
|
msg += typeid (T).name();
|
||||||
|
Q_ASSERT_X(false, "toJsonValue<T>", msg.c_str());
|
||||||
|
return QJsonValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T fromJsonValue(const QJsonValue &source) {
|
||||||
|
return fromJsonValue(source, convertType<T>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
QJsonValue toJsonValue(const T &source) {
|
||||||
|
return toJsonValue(source, convertType<T>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
// QList
|
||||||
|
template <typename T>
|
||||||
|
QList<T> fromJsonValue(const QJsonValue &source, convertType<QList<T>>) {
|
||||||
|
QList<T> result;
|
||||||
|
QJsonArray arr = source.toArray();
|
||||||
|
result.reserve(arr.size());
|
||||||
|
for (auto it = arr.constBegin(); it != arr.constEnd(); it++) {
|
||||||
|
result.append(fromJsonValue<T>(*it));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
QJsonValue toJsonValue(const QList<T> &source, convertType<QList<T>>) {
|
||||||
|
QJsonArray result;
|
||||||
|
for (auto it = source.cbegin(); it != source.cend(); it++) {
|
||||||
|
result.push_back(toJsonValue<T>(*it));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
std::optional<T> fromJsonValue(const QJsonValue &source, convertType<std::optional<T>>) {
|
||||||
|
if (source.isNull()) {
|
||||||
|
return std::nullopt;
|
||||||
|
} else {
|
||||||
|
return fromJsonValue<T>(source, convertType<T>{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
QJsonValue toJsonValue(const std::optional<T> &source, convertType<std::optional<T>>) {
|
||||||
|
if (source.has_value()) {
|
||||||
|
return toJsonValue<T>(source.value(), convertType<T>{});
|
||||||
|
} else {
|
||||||
|
// Null
|
||||||
|
return QJsonValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QSharedPointer
|
||||||
|
template <typename T>
|
||||||
|
QSharedPointer<T> fromJsonValue(const QJsonValue &source, convertType<QSharedPointer<T>>) {
|
||||||
|
if (source.isNull()) {
|
||||||
|
return QSharedPointer<T>();
|
||||||
|
}
|
||||||
|
return QSharedPointer<T>::create(fromJsonValue<T>(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
QJsonValue toJsonValue(const QSharedPointer<T> &source, convertType<QSharedPointer<T>>) {
|
||||||
|
if (source.isNull()) {
|
||||||
|
return QJsonValue();
|
||||||
|
}
|
||||||
|
return toJsonValue<T>(*source);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Templates for string conversion.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
QString toString(const T &source, convertType<T>) {
|
||||||
|
return toJsonValue(source).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
QString toString(const std::optional<T> &source, convertType<std::optional<T>>) {
|
||||||
|
if (source.has_value()) {
|
||||||
|
return toString<T>(source.value(), convertType<T>{});
|
||||||
|
} else {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
QString toString(const T &source) {
|
||||||
|
return toString(source, convertType<T>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} // NS Support
|
||||||
|
} // NS Jellyfin
|
||||||
|
|
||||||
|
#endif // JELLYFIN_SUPPORT_JSONCONVIMPL_H
|
50
core/include/JellyfinQt/support/parseexception.h
Normal file
50
core/include/JellyfinQt/support/parseexception.h
Normal file
|
@ -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 <QException>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
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
|
|
@ -84,14 +84,14 @@ public:
|
||||||
Q_PROPERTY(QStringList productionLocations READ productionLocations NOTIFY productionLocationsChanged)
|
Q_PROPERTY(QStringList productionLocations READ productionLocations NOTIFY productionLocationsChanged)
|
||||||
|
|
||||||
// Handpicked, important ones
|
// 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(QString overview READ overview NOTIFY overviewChanged)
|
||||||
Q_PROPERTY(int productionYear READ productionYear NOTIFY productionYearChanged)
|
Q_PROPERTY(int productionYear READ productionYear NOTIFY productionYearChanged)
|
||||||
Q_PROPERTY(int indexNumber READ indexNumber NOTIFY indexNumberChanged)
|
Q_PROPERTY(int indexNumber READ indexNumber NOTIFY indexNumberChanged)
|
||||||
Q_PROPERTY(int indexNumberEnd READ indexNumberEnd NOTIFY indexNumberEndChanged)
|
Q_PROPERTY(int indexNumberEnd READ indexNumberEnd NOTIFY indexNumberEndChanged)
|
||||||
Q_PROPERTY(bool isFolder READ isFolder NOTIFY isFolderChanged)
|
Q_PROPERTY(bool isFolder READ isFolder NOTIFY isFolderChanged)
|
||||||
Q_PROPERTY(QString type READ type NOTIFY typeChanged)
|
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(QStringList parentBackdropImageTags READ parentBackdropImageTags NOTIFY parentBackdropImageTagsChanged)
|
||||||
Q_PROPERTY(UserData *userData READ userData NOTIFY userDataChanged)
|
Q_PROPERTY(UserData *userData READ userData NOTIFY userDataChanged)
|
||||||
Q_PROPERTY(int recursiveItemCount READ recursiveItemCount NOTIFY recursiveItemCountChanged)
|
Q_PROPERTY(int recursiveItemCount READ recursiveItemCount NOTIFY recursiveItemCountChanged)
|
||||||
|
@ -125,6 +125,12 @@ public:
|
||||||
int airsBeforeSeasonNumber() const { return m_data->airsBeforeSeasonNumber().value_or(0); }
|
int airsBeforeSeasonNumber() const { return m_data->airsBeforeSeasonNumber().value_or(0); }
|
||||||
int airsAfterSeasonNumber() const { return m_data->airsAfterSeasonNumber().value_or(999); }
|
int airsAfterSeasonNumber() const { return m_data->airsAfterSeasonNumber().value_or(999); }
|
||||||
int airsBeforeEpisodeNumber() const { return m_data->airsBeforeEpisodeNumber().value_or(0); }
|
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<Model::Item> data() const { return m_data; }
|
QSharedPointer<Model::Item> data() const { return m_data; }
|
||||||
void setData(QSharedPointer<Model::Item> newData);
|
void setData(QSharedPointer<Model::Item> newData);
|
||||||
|
@ -192,7 +198,7 @@ public:
|
||||||
Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
|
Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
|
||||||
|
|
||||||
QString itemId() const { return m_parameters.itemId(); }
|
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;
|
virtual bool canReload() const override;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
|
@ -163,6 +163,10 @@ public:
|
||||||
|
|
||||||
// Hand-picked, important ones
|
// Hand-picked, important ones
|
||||||
imageTags,
|
imageTags,
|
||||||
|
imageBlurHashes,
|
||||||
|
mediaType,
|
||||||
|
type,
|
||||||
|
collectionType,
|
||||||
|
|
||||||
jellyfinExtendModelAfterHere = Qt::UserRole + 300 // Should be enough for now
|
jellyfinExtendModelAfterHere = Qt::UserRole + 300 // Should be enough for now
|
||||||
};
|
};
|
||||||
|
@ -183,9 +187,14 @@ public:
|
||||||
JFRN(extraType),
|
JFRN(extraType),
|
||||||
// Handpicked, important ones
|
// Handpicked, important ones
|
||||||
JFRN(imageTags),
|
JFRN(imageTags),
|
||||||
|
JFRN(imageBlurHashes),
|
||||||
|
JFRN(mediaType),
|
||||||
|
JFRN(type),
|
||||||
|
JFRN(collectionType),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
QVariant data(const QModelIndex &index, int role) const override;
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
QSharedPointer<Model::Item> itemAt(int index);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*class UserItemModel : public ItemModel {
|
/*class UserItemModel : public ItemModel {
|
||||||
|
|
|
@ -110,15 +110,19 @@ protected:
|
||||||
void setError(QNetworkReply::NetworkError error);
|
void setError(QNetworkReply::NetworkError error);
|
||||||
void setErrorString(const QString &newErrorString);
|
void setErrorString(const QString &newErrorString);
|
||||||
|
|
||||||
|
|
||||||
|
void reloadIfNeeded() {
|
||||||
|
if (canReload()) {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
void classBegin() override {
|
void classBegin() override {
|
||||||
m_isParsing = true;
|
m_isParsing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void componentComplete() override {
|
void componentComplete() override {
|
||||||
m_isParsing = false;
|
m_isParsing = false;
|
||||||
if (canReload()) {
|
reloadIfNeeded();
|
||||||
reload();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ApiClient *m_apiClient = nullptr;
|
ApiClient *m_apiClient = nullptr;
|
||||||
protected:
|
protected:
|
||||||
|
@ -157,7 +161,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
T *dataViewModel() const { return m_dataViewModel; }
|
T *dataViewModel() const { return m_dataViewModel; }
|
||||||
QObject *data() const { return m_dataViewModel; }
|
QObject *data() const override { return m_dataViewModel; }
|
||||||
|
|
||||||
void reload() override {
|
void reload() override {
|
||||||
if (m_futureWatcher->isRunning()) return;
|
if (m_futureWatcher->isRunning()) return;
|
||||||
|
|
|
@ -34,9 +34,13 @@
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "../dto/baseitemdto.h"
|
#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 "../support/jsonconv.h"
|
||||||
#include "../viewmodel/item.h"
|
#include "../viewmodel/item.h"
|
||||||
|
|
||||||
#include "../apiclient.h"
|
#include "../apiclient.h"
|
||||||
#include "itemmodel.h"
|
#include "itemmodel.h"
|
||||||
|
|
||||||
|
@ -50,6 +54,9 @@ class RemoteItem;
|
||||||
|
|
||||||
namespace ViewModel {
|
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
|
* @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.
|
* 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.
|
* preloading the next item in the queue. The current media player is pointed to by m_mediaPlayer.
|
||||||
*/
|
*/
|
||||||
class PlaybackManager : public QObject, public QQmlParserStatus {
|
class PlaybackManager : public QObject, public QQmlParserStatus {
|
||||||
|
friend class ItemUrlFetcherThread;
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_INTERFACES(QQmlParserStatus)
|
Q_INTERFACES(QQmlParserStatus)
|
||||||
public:
|
public:
|
||||||
enum PlayMethod {
|
|
||||||
Transcode,
|
|
||||||
Stream,
|
|
||||||
DirectPlay
|
|
||||||
};
|
|
||||||
Q_ENUM(PlayMethod)
|
|
||||||
using FetchCallback = std::function<void(QUrl &&, PlayMethod)>;
|
using FetchCallback = std::function<void(QUrl &&, PlayMethod)>;
|
||||||
|
|
||||||
explicit PlaybackManager(QObject *parent = nullptr);
|
explicit PlaybackManager(QObject *parent = nullptr);
|
||||||
|
@ -77,11 +79,11 @@ public:
|
||||||
Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged)
|
Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged)
|
||||||
Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
|
Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
|
||||||
Q_PROPERTY(bool resumePlayback MEMBER m_resumePlayback NOTIFY resumePlaybackChanged)
|
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
|
// Current Item and queue informatoion
|
||||||
Q_PROPERTY(ViewModel::Item *item READ item NOTIFY itemChanged)
|
Q_PROPERTY(QObject *item READ item NOTIFY itemChanged)
|
||||||
Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged)
|
// Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged)
|
||||||
Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged)
|
Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged)
|
||||||
|
|
||||||
// Current media player related property getters
|
// Current media player related property getters
|
||||||
|
@ -102,7 +104,7 @@ public:
|
||||||
QObject *mediaObject() const { return m_mediaPlayer; }
|
QObject *mediaObject() const { return m_mediaPlayer; }
|
||||||
qint64 position() const { return m_mediaPlayer->position(); }
|
qint64 position() const { return m_mediaPlayer->position(); }
|
||||||
qint64 duration() const { return m_mediaPlayer->duration(); }
|
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; }
|
int queueIndex() const { return m_queueIndex; }
|
||||||
|
|
||||||
// Current media player related property getters
|
// Current media player related property getters
|
||||||
|
@ -112,7 +114,7 @@ public:
|
||||||
QMediaPlayer::Error error () const { return m_mediaPlayer->error(); }
|
QMediaPlayer::Error error () const { return m_mediaPlayer->error(); }
|
||||||
QString errorString() const { return m_mediaPlayer->errorString(); }
|
QString errorString() const { return m_mediaPlayer->errorString(); }
|
||||||
signals:
|
signals:
|
||||||
void itemChanged(BaseItemDto *newItemId);
|
void itemChanged(ViewModel::Item *newItemId);
|
||||||
void streamUrlChanged(const QString &newStreamUrl);
|
void streamUrlChanged(const QString &newStreamUrl);
|
||||||
void autoOpenChanged(bool autoOpen);
|
void autoOpenChanged(bool autoOpen);
|
||||||
void audioIndexChanged(int audioIndex);
|
void audioIndexChanged(int audioIndex);
|
||||||
|
@ -125,7 +127,7 @@ signals:
|
||||||
void mediaObjectChanged(QObject *newMediaObject);
|
void mediaObjectChanged(QObject *newMediaObject);
|
||||||
void positionChanged(qint64 newPosition);
|
void positionChanged(qint64 newPosition);
|
||||||
void durationChanged(qint64 newDuration);
|
void durationChanged(qint64 newDuration);
|
||||||
void queueChanged(ItemModel *newQue);
|
//void queueChanged(ItemModel *newQue);
|
||||||
void queueIndexChanged(int newIndex);
|
void queueIndexChanged(int newIndex);
|
||||||
void playbackStateChanged(QMediaPlayer::State newState);
|
void playbackStateChanged(QMediaPlayer::State newState);
|
||||||
void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus);
|
void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus);
|
||||||
|
@ -138,9 +140,9 @@ public slots:
|
||||||
*
|
*
|
||||||
* This will construct the Jellyfin::Item internally
|
* This will construct the Jellyfin::Item internally
|
||||||
* and delete it later.
|
* 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 playItemInList(ItemModel *itemList, int index);
|
||||||
void play() { m_mediaPlayer->play(); }
|
void play() { m_mediaPlayer->play(); }
|
||||||
void pause() { m_mediaPlayer->pause(); }
|
void pause() { m_mediaPlayer->pause(); }
|
||||||
|
@ -162,24 +164,43 @@ private slots:
|
||||||
void mediaPlayerPositionChanged(qint64 position);
|
void mediaPlayerPositionChanged(qint64 position);
|
||||||
void mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus);
|
void mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus);
|
||||||
void mediaPlayerError(QMediaPlayer::Error error);
|
void mediaPlayerError(QMediaPlayer::Error error);
|
||||||
|
void mediaPlayerDurationChanged(qint64 newDuration);
|
||||||
/**
|
/**
|
||||||
* @brief updatePlaybackInfo Updates the Jellyfin server with the current playback progress etc.
|
* @brief updatePlaybackInfo Updates the Jellyfin server with the current playback progress etc.
|
||||||
*/
|
*/
|
||||||
void updatePlaybackInfo();
|
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:
|
private:
|
||||||
/// Factor to multiply with when converting from milliseconds to ticks.
|
/// Factor to multiply with when converting from milliseconds to ticks.
|
||||||
const static int MS_TICK_FACTOR = 10000;
|
const static int MS_TICK_FACTOR = 10000;
|
||||||
enum PlaybackInfoType { Started, Stopped, Progress };
|
enum PlaybackInfoType { Started, Stopped, Progress };
|
||||||
|
|
||||||
|
/// Timer used to update the play progress on the Jellyfin server
|
||||||
QTimer m_updateTimer;
|
QTimer m_updateTimer;
|
||||||
|
/// Timer used to notify ourselves when we need to preload the next item
|
||||||
|
QTimer m_preloadTimer;
|
||||||
|
|
||||||
ApiClient *m_apiClient = nullptr;
|
ApiClient *m_apiClient = nullptr;
|
||||||
|
/// The currently playing item
|
||||||
QSharedPointer<Model::Item> m_item;
|
QSharedPointer<Model::Item> m_item;
|
||||||
|
/// The item that will be played next
|
||||||
|
QSharedPointer<Model::Item> m_nextItem;
|
||||||
|
/// The currently played item that will be shown in the GUI
|
||||||
ViewModel::Item *m_displayItem = new ViewModel::Item(this);
|
ViewModel::Item *m_displayItem = new ViewModel::Item(this);
|
||||||
|
|
||||||
// Properties for making the streaming request.
|
// Properties for making the streaming request.
|
||||||
QString m_streamUrl;
|
QString m_streamUrl;
|
||||||
|
QString m_nextStreamUrl;
|
||||||
QString m_playSessionId;
|
QString m_playSessionId;
|
||||||
|
QString m_nextPlaySessionId;
|
||||||
/// The index of the mediastreams of the to-be-played item containing the audio
|
/// The index of the mediastreams of the to-be-played item containing the audio
|
||||||
int m_audioIndex = 0;
|
int m_audioIndex = 0;
|
||||||
/// The index of the mediastreams of the to-be-played item containing subtitles
|
/// The index of the mediastreams of the to-be-played item containing subtitles
|
||||||
|
@ -196,10 +217,11 @@ private:
|
||||||
*/
|
*/
|
||||||
bool m_autoOpen = false;
|
bool m_autoOpen = false;
|
||||||
|
|
||||||
|
|
||||||
// Playback-related members
|
// Playback-related members
|
||||||
|
ItemUrlFetcherThread *m_urlFetcherThread;
|
||||||
|
|
||||||
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
|
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
|
||||||
PlayMethod m_playMethod = Transcode;
|
PlayMethod m_playMethod = PlayMethod::Transcode;
|
||||||
QMediaPlayer::State m_playbackState = QMediaPlayer::StoppedState;
|
QMediaPlayer::State m_playbackState = QMediaPlayer::StoppedState;
|
||||||
/// Pointer to the current media player.
|
/// Pointer to the current media player.
|
||||||
QMediaPlayer *m_mediaPlayer = nullptr;
|
QMediaPlayer *m_mediaPlayer = nullptr;
|
||||||
|
@ -211,28 +233,18 @@ private:
|
||||||
QMediaPlayer *m_mediaPlayer1;
|
QMediaPlayer *m_mediaPlayer1;
|
||||||
/// Media player 2
|
/// Media player 2
|
||||||
QMediaPlayer *m_mediaPlayer2;
|
QMediaPlayer *m_mediaPlayer2;
|
||||||
ItemModel *m_queue = nullptr;
|
|
||||||
|
Model::Playlist *m_queue = nullptr;
|
||||||
int m_queueIndex = 0;
|
int m_queueIndex = 0;
|
||||||
bool m_resumePlayback = true;
|
bool m_resumePlayback = true;
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
void setItem(ViewModel::Item *newItem);
|
void setItem(QSharedPointer<Model::Item> newItem);
|
||||||
void swapMediaPlayer();
|
void swapMediaPlayer();
|
||||||
|
|
||||||
|
void setStreamUrl(const QUrl &streamUrl);
|
||||||
/**
|
|
||||||
* @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 setPlaybackState(QMediaPlayer::State newState);
|
void setPlaybackState(QMediaPlayer::State newState);
|
||||||
|
|
||||||
Model::Item *nextItem();
|
|
||||||
void setQueue(ItemModel *itemModel);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Posts the playback information
|
* @brief Posts the playback information
|
||||||
*/
|
*/
|
||||||
|
@ -243,6 +255,60 @@ private:
|
||||||
void classBegin() override { m_qmlIsParsingComponent = true; }
|
void classBegin() override { m_qmlIsParsingComponent = true; }
|
||||||
void componentComplete() override;
|
void componentComplete() override;
|
||||||
bool m_qmlIsParsingComponent = false;
|
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<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, 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<DTO::PlaybackInfoResponse, Jellyfin::Loader::GetPostedPlaybackInfoParams> *m_loader;
|
||||||
|
|
||||||
|
QMutex m_queueModifyMutex;
|
||||||
|
QQueue<QSharedPointer<Model::Item>> 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 ViewModel
|
||||||
|
|
|
@ -29,109 +29,30 @@
|
||||||
#include <QWaitCondition>
|
#include <QWaitCondition>
|
||||||
#include <QtMultimedia/QMediaPlaylist>
|
#include <QtMultimedia/QMediaPlaylist>
|
||||||
|
|
||||||
#include "../dto/playbackinfodto.h"
|
#include "../apiclient.h"
|
||||||
#include "../loader/requesttypes.h"
|
|
||||||
#include "../loader/http/getpostedplaybackinfo.h"
|
|
||||||
#include "itemmodel.h"
|
#include "itemmodel.h"
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
namespace ViewModel {
|
namespace ViewModel {
|
||||||
|
|
||||||
class ItemUrlFetcherThread;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Playlist/queue that can be exposed to the UI. It also containts the playlist-related logic,
|
* @brief Playlist/queue that can be exposed to the UI. It also containts the playlist-related logic,
|
||||||
* which is mostly relevant
|
* which is mostly relevant
|
||||||
*/
|
*/
|
||||||
class Playlist : public ItemModel {
|
/*class Playlist : public ItemModel {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
friend class ItemUrlFetcherThread;
|
friend class ItemUrlFetcherThread;
|
||||||
public:
|
public:
|
||||||
explicit Playlist(ApiClient *apiClient, QObject *parent = nullptr);
|
explicit Playlist(ApiClient *apiClient, QObject *parent = nullptr);
|
||||||
enum ExtraRoles {
|
|
||||||
Url = ItemModel::RoleNames::jellyfinExtendModelAfterHere + 1,
|
|
||||||
PlaySession,
|
|
||||||
ErrorText
|
|
||||||
};
|
|
||||||
|
|
||||||
QHash<int, QByteArray> roleNames() const override {
|
|
||||||
QHash<int, QByteArray> result(ItemModel::roleNames());
|
|
||||||
result.insert(Url, "url");
|
|
||||||
result.insert(PlaySession, "playSession");
|
|
||||||
result.insert(ErrorText, "errorText");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onItemsAdded(const QModelIndex &parent, int startIndex, int endIndex);
|
void onItemsAdded(const QModelIndex &parent, int startIndex, int endIndex);
|
||||||
void onItemsMoved(const QModelIndex &parent, int startIndex, int endIndex, const QModelIndex &destination, int destinationRow);
|
void onItemsMoved(const QModelIndex &parent, int startIndex, int endIndex, const QModelIndex &destination, int destinationRow);
|
||||||
void onItemsRemoved(const QModelIndex &parent, int startIndex, int endIndex);
|
void onItemsRemoved(const QModelIndex &parent, int startIndex, int endIndex);
|
||||||
void onItemsReset();
|
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<QString, ExtraData> 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<DTO::PlaybackInfoResponse, Jellyfin::Loader::GetPostedPlaybackInfoParams> *m_loader;
|
|
||||||
|
|
||||||
QMutex m_queueModifyMutex;
|
|
||||||
QQueue<const Model::Item&> 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 ViewModel
|
||||||
} // NS Jellyfin
|
} // NS Jellyfin
|
||||||
|
|
|
@ -944,7 +944,7 @@ public:
|
||||||
|
|
||||||
string typeNameWithQualifiers() {
|
string typeNameWithQualifiers() {
|
||||||
if (needsPointer) {
|
if (needsPointer) {
|
||||||
return "QSharedPointer<" ~ typeName~ ">";
|
return "QSharedPointer<" ~ typeName ~ ">";
|
||||||
}
|
}
|
||||||
if (needsOptional) {
|
if (needsOptional) {
|
||||||
return "std::optional<" ~ typeName ~ ">";
|
return "std::optional<" ~ typeName ~ ">";
|
||||||
|
@ -977,7 +977,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
string defaultInitializer() {
|
string defaultInitializer() {
|
||||||
if (needsPointer) return "nullptr";
|
if (needsPointer) return "QSharedPointer<" ~ typeName ~ ">()";
|
||||||
if (needsOptional) return "std::nullopt";
|
if (needsOptional) return "std::nullopt";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,10 @@ ApiClient::ApiClient(QObject *parent)
|
||||||
m_eventbus(new EventBus(this)),
|
m_eventbus(new EventBus(this)),
|
||||||
m_deviceName(QHostInfo::localHostName()) {
|
m_deviceName(QHostInfo::localHostName()) {
|
||||||
|
|
||||||
m_deviceId = Support::uuidToString(retrieveDeviceId());
|
m_deviceId = Support::toString(retrieveDeviceId());
|
||||||
|
|
||||||
m_credManager = CredentialsManager::newInstance(this);
|
m_credManager = CredentialsManager::newInstance(this);
|
||||||
|
connect(m_credManager, &CredentialsManager::serversListed, this, &ApiClient::credManagerServersListed);
|
||||||
|
connect(m_credManager, &CredentialsManager::usersListed, this, &ApiClient::credManagerUsersListed);
|
||||||
generateDeviceProfile();
|
generateDeviceProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,49 +92,48 @@ QNetworkReply *ApiClient::post(const QString &path, const QJsonDocument &data, c
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
void ApiClient::restoreSavedSession(){
|
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();
|
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() {
|
void ApiClient::setupConnection() {
|
||||||
// First detect redirects:
|
// First detect redirects:
|
||||||
// Note that this is done without calling JellyfinApiClient::get since that automatically includes the base_url,
|
// Note that this is done without calling JellyfinApiClient::get since that automatically includes the base_url,
|
||||||
|
|
|
@ -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
|
License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
#define JELLYFIN_APIMODEL_CPP
|
||||||
#include "JellyfinQt/apimodel.h"
|
#include "JellyfinQt/apimodel.h"
|
||||||
|
|
||||||
#include "JellyfinQt/dto/baseitemdto.h"
|
#include "JellyfinQt/dto/baseitemdto.h"
|
||||||
|
|
|
@ -87,5 +87,5 @@ void FallbackCredentialsManager::listUsers(const QString &server) {
|
||||||
qDebug() << "Users: " << users;
|
qDebug() << "Users: " << users;
|
||||||
m_settings.endGroup();
|
m_settings.endGroup();
|
||||||
m_settings.endGroup();
|
m_settings.endGroup();
|
||||||
emit CredentialsManager::usersListed(users);
|
emit CredentialsManager::usersListed(server, users);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,10 @@ void registerTypes(const char *uri) {
|
||||||
qmlRegisterType<ViewModel::UserViewsLoader>(uri, 1, 0, "UsersViewsLoader");
|
qmlRegisterType<ViewModel::UserViewsLoader>(uri, 1, 0, "UsersViewsLoader");
|
||||||
|
|
||||||
// Enumerations
|
// Enumerations
|
||||||
qmlRegisterUncreatableType<DTO::GeneralCommandTypeClass>(uri, 1, 0, "GeneralCommandType", "Is an enum");
|
qmlRegisterUncreatableType<Jellyfin::DTO::GeneralCommandTypeClass>(uri, 1, 0, "GeneralCommandType", "Is an enum");
|
||||||
qmlRegisterUncreatableType<ViewModel::ModelStatusClass>(uri, 1, 0, "ModelStatus", "Is an enum");
|
qmlRegisterUncreatableType<Jellyfin::ViewModel::ModelStatusClass>(uri, 1, 0, "ModelStatus", "Is an enum");
|
||||||
|
qmlRegisterUncreatableType<Jellyfin::DTO::PlayMethodClass>(uri, 1, 0, "PlayMethod", "Is an enum");
|
||||||
|
|
||||||
|
qRegisterMetaType<Jellyfin::DTO::PlayMethodClass::Value>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<const Item> Playlist::listAt(int index) const {
|
||||||
|
return m_list.at(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSharedPointer<Item> Playlist::currentItem() {
|
||||||
|
return m_currentItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSharedPointer<Item> 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<Model::Item>::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
|
146
core/src/model/shuffle.cpp
Normal file
146
core/src/model/shuffle.cpp
Normal file
|
@ -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 <QRandomGenerator>
|
||||||
|
#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<int>(min + static_cast<double>(max - min) / static_cast<double>(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
|
|
@ -16,25 +16,14 @@
|
||||||
* License along with this library; if not, write to the Free Software
|
* License along with this library; if not, write to the Free Software
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
* 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 <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
namespace Support {
|
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 uuidToString(const QUuid &source) {
|
||||||
QString str = source.toString();
|
QString str = source.toString();
|
||||||
// Convert {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} (length: 38)
|
// Convert {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} (length: 38)
|
||||||
|
@ -195,7 +184,8 @@ QDateTime fromJsonValue<QDateTime>(const QJsonValue &source, convertType<QDateTi
|
||||||
case QJsonValue::Null:
|
case QJsonValue::Null:
|
||||||
return QDateTime();
|
return QDateTime();
|
||||||
case QJsonValue::String:
|
case QJsonValue::String:
|
||||||
return QDateTime::fromString(source.toString(), Qt::ISODateWithMs);
|
// 2005-02-21T00:00:00.0000000Z
|
||||||
|
return QDateTime::fromString(source.toString(), Qt::ISODate);
|
||||||
default:
|
default:
|
||||||
throw ParseException("Error while trying to parse JSON value as DateTime: not a string");
|
throw ParseException("Error while trying to parse JSON value as DateTime: not a string");
|
||||||
}
|
}
|
||||||
|
@ -203,7 +193,18 @@ QDateTime fromJsonValue<QDateTime>(const QJsonValue &source, convertType<QDateTi
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
QJsonValue toJsonValue<QDateTime>(const QDateTime &source, convertType<QDateTime>) {
|
QJsonValue toJsonValue<QDateTime>(const QDateTime &source, convertType<QDateTime>) {
|
||||||
return QJsonValue(source.toString(Qt::ISODateWithMs));
|
return QJsonValue(source.toString(Qt::ISODate));
|
||||||
|
}
|
||||||
|
|
||||||
|
// QVariant
|
||||||
|
template <>
|
||||||
|
QVariant fromJsonValue<QVariant>(const QJsonValue &source, convertType<QVariant>) {
|
||||||
|
return source.toVariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
QJsonValue toJsonValue<QVariant>(const QVariant &source, convertType<QVariant>) {
|
||||||
|
return QJsonValue::fromVariant(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
// QUuid
|
// QUuid
|
||||||
|
|
38
core/src/support/parseexception.cpp
Normal file
38
core/src/support/parseexception.cpp
Normal file
|
@ -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
|
|
@ -27,7 +27,6 @@ Item::Item(QObject *parent, QSharedPointer<Model::Item> data)
|
||||||
}
|
}
|
||||||
|
|
||||||
void Item::setData(QSharedPointer<Model::Item> newData) {
|
void Item::setData(QSharedPointer<Model::Item> newData) {
|
||||||
Model::Item oldData = *m_data.data();
|
|
||||||
m_data = newData;
|
m_data = newData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,21 +43,32 @@ void ItemLoader::onApiClientChanged(ApiClient *newApiClient) {
|
||||||
disconnect(m_apiClient, &ApiClient::userIdChanged, this, &ItemLoader::setUserId);
|
disconnect(m_apiClient, &ApiClient::userIdChanged, this, &ItemLoader::setUserId);
|
||||||
}
|
}
|
||||||
if (newApiClient != nullptr) {
|
if (newApiClient != nullptr) {
|
||||||
m_parameters.setUserId(newApiClient->userId());
|
|
||||||
connect(newApiClient, &ApiClient::userIdChanged, this, &ItemLoader::setUserId);
|
connect(newApiClient, &ApiClient::userIdChanged, this, &ItemLoader::setUserId);
|
||||||
|
setUserId(newApiClient->userId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ItemLoader::setUserId(const QString &newUserId) {
|
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 {
|
bool ItemLoader::canReload() const {
|
||||||
|
qDebug() << "ItemLoader::canReload(): baseClass=" << BaseClass::canReload() << ", itemId=" << m_parameters.userId() << ", userId=" << m_parameters.userId();
|
||||||
return BaseClass::canReload()
|
return BaseClass::canReload()
|
||||||
&& !m_parameters.itemId().isEmpty()
|
&& !m_parameters.itemId().isEmpty()
|
||||||
&& !m_parameters.userId().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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
#define JF_CASE(roleName) case roleName: \
|
#define JF_CASE(roleName) case roleName: \
|
||||||
try { \
|
try { \
|
||||||
return QVariant(item.roleName()); \
|
return QVariant(item.roleName()); \
|
||||||
} catch(std::bad_optional_access e) { \
|
} catch(std::bad_optional_access &e) { \
|
||||||
return QVariant(); \
|
return QVariant(); \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,12 +62,20 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const {
|
||||||
JF_CASE(extraType)
|
JF_CASE(extraType)
|
||||||
// Handpicked, important ones
|
// Handpicked, important ones
|
||||||
JF_CASE(imageTags)
|
JF_CASE(imageTags)
|
||||||
|
JF_CASE(imageBlurHashes)
|
||||||
|
JF_CASE(mediaType)
|
||||||
|
JF_CASE(type)
|
||||||
|
JF_CASE(collectionType)
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QSharedPointer<Model::Item> ItemModel::itemAt(int index) {
|
||||||
|
return QSharedPointer<Model::Item>::create(m_array[index]);
|
||||||
|
}
|
||||||
|
|
||||||
} // NS ViewModel
|
} // NS ViewModel
|
||||||
|
|
||||||
} // NS Jellyfin
|
} // NS Jellyfin
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
// #include "JellyfinQt/DTO/dto.h"
|
// #include "JellyfinQt/DTO/dto.h"
|
||||||
#include <JellyfinQt/dto/useritemdatadto.h>
|
#include <JellyfinQt/dto/useritemdatadto.h>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
class ItemModel;
|
class ItemModel;
|
||||||
|
@ -37,125 +38,67 @@ PlaybackManager::PlaybackManager(QObject *parent)
|
||||||
: QObject(parent),
|
: QObject(parent),
|
||||||
m_item(nullptr),
|
m_item(nullptr),
|
||||||
m_mediaPlayer1(new QMediaPlayer(this)),
|
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.
|
// Set up connections.
|
||||||
swapMediaPlayer();
|
swapMediaPlayer();
|
||||||
m_updateTimer.setInterval(10000); // 10 seconds
|
m_updateTimer.setInterval(10000); // 10 seconds
|
||||||
m_updateTimer.setSingleShot(false);
|
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_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) {
|
void PlaybackManager::setApiClient(ApiClient *apiClient) {
|
||||||
m_item->setApiClient(apiClient);
|
if (!m_item.isNull()) {
|
||||||
}
|
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;
|
|
||||||
}
|
}
|
||||||
QString itemId(item->jellyfinId());
|
m_apiClient = apiClient;
|
||||||
m_resumePosition = 0;
|
|
||||||
if (m_resumePlayback && !item->userData().isNull()) {
|
|
||||||
QSharedPointer<UserData> 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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::fetchAndSetStreamUrl(const Model::Item *item) {
|
void PlaybackManager::setItem(QSharedPointer<Model::Item> newItem) {
|
||||||
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) {
|
|
||||||
if (m_mediaPlayer != nullptr) m_mediaPlayer->stop();
|
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) {
|
if (newItem.isNull()) {
|
||||||
this->m_item = newItem->data();
|
m_displayItem->setData(QSharedPointer<Model::Item>::create());
|
||||||
|
} else {
|
||||||
|
m_displayItem->setData(newItem);
|
||||||
}
|
}
|
||||||
//emit itemChanged(newItem);
|
emit itemChanged(m_displayItem);
|
||||||
|
|
||||||
if (m_apiClient == nullptr) {
|
if (m_apiClient == nullptr) {
|
||||||
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
|
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Deinitialize the streamUrl
|
// Deinitialize the streamUrl
|
||||||
setStreamUrl("");
|
if (shouldFetchStreamUrl) {
|
||||||
if (newItem != nullptr) {
|
setStreamUrl(QUrl());
|
||||||
fetchAndSetStreamUrl(m_item.data());
|
m_urlFetcherThread->addItemToQueue(m_item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void PlaybackManager::setStreamUrl(const QString &streamUrl) {
|
void PlaybackManager::setStreamUrl(const QUrl &streamUrl) {
|
||||||
this->m_streamUrl = streamUrl;
|
m_streamUrl = streamUrl.toString();
|
||||||
// Inspired by PHP naming schemes
|
// Inspired by PHP naming schemes
|
||||||
QUrl realStreamUrl(streamUrl);
|
Q_ASSERT_X(streamUrl.isValid() || streamUrl.isEmpty(), "setStreamUrl", "StreamURL Jellyfin returned is not valid");
|
||||||
Q_ASSERT_X(realStreamUrl.isValid(), "setStreamUrl", "StreamURL Jellyfin returned is not valid");
|
emit streamUrlChanged(m_streamUrl);
|
||||||
emit streamUrlChanged(streamUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::setPlaybackState(QMediaPlayer::State newState) {
|
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);
|
qDebug() << "Resuming playback by seeking to " << (m_resumePosition / MS_TICK_FACTOR);
|
||||||
m_mediaPlayer->setPosition(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<int>(newDuration - PRELOAD_DURATION), 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,46 +168,47 @@ void PlaybackManager::updatePlaybackInfo() {
|
||||||
postPlaybackInfo(Progress);
|
postPlaybackInfo(Progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::playItem(const QString &itemId) {
|
void PlaybackManager::playItem(Item *item) {
|
||||||
Q_UNUSED(itemId)
|
setItem(item->data());
|
||||||
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::playItemInList(ItemModel *playlist, int itemIdx) {
|
void PlaybackManager::playItemInList(ItemModel *playlist, int index) {
|
||||||
playlist->setParent(this);
|
m_queue->clearList();
|
||||||
setQueue(playlist);
|
m_queue->appendToList(*playlist);
|
||||||
m_queueIndex = itemIdx;
|
m_queue->play(index);
|
||||||
|
m_queueIndex = index;
|
||||||
emit queueIndexChanged(m_queueIndex);
|
emit queueIndexChanged(m_queueIndex);
|
||||||
//setItem(playlist->at(itemIdx));
|
setItem(playlist->itemAt(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::next() {
|
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() {
|
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) {
|
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
|
@ -263,7 +217,7 @@ void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
||||||
qWarning() << "Item is null. Not posting playback info";
|
qWarning() << "Item is null. Not posting playback info";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
root["ItemId"] = Support::uuidToString(m_item->jellyfinId());
|
root["ItemId"] = Support::toString(m_item->jellyfinId());
|
||||||
root["SessionId"] = m_playSessionId;
|
root["SessionId"] = m_playSessionId;
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
|
@ -308,11 +262,11 @@ void PlaybackManager::swapMediaPlayer() {
|
||||||
if (m_mediaPlayer != nullptr) {
|
if (m_mediaPlayer != nullptr) {
|
||||||
disconnect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
disconnect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
||||||
disconnect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
|
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::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
|
||||||
disconnect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
|
disconnect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
|
||||||
// I do not like the complicated overload cast
|
// 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) {
|
if (m_mediaPlayer == m_mediaPlayer1) {
|
||||||
m_mediaPlayer = m_mediaPlayer2;
|
m_mediaPlayer = m_mediaPlayer2;
|
||||||
|
@ -323,41 +277,154 @@ void PlaybackManager::swapMediaPlayer() {
|
||||||
}
|
}
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
|
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::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
|
connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
|
||||||
// I do not like the complicated overload cast
|
// I do not like the complicated overload cast
|
||||||
connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::error)), this, SLOT(mediaPlayerError(QmediaPlayer::error)));
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::componentComplete() {
|
void PlaybackManager::componentComplete() {
|
||||||
if (m_apiClient == nullptr) qWarning() << "No ApiClient set for PlaybackManager";
|
if (m_apiClient == nullptr) qWarning() << "No ApiClient set for PlaybackManager";
|
||||||
m_qmlIsParsingComponent = false;
|
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<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->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<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());
|
||||||
|
m_waitLoaderPreparedMutex.lock();
|
||||||
|
while (!m_loaderPrepared) {
|
||||||
|
m_waitLoaderPrepared.wait(&m_waitLoaderPreparedMutex);
|
||||||
|
}
|
||||||
|
m_waitLoaderPreparedMutex.unlock();
|
||||||
|
|
||||||
|
DTO::PlaybackInfoResponse response;
|
||||||
|
try {
|
||||||
|
std::optional<DTO::PlaybackInfoResponse> 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<DTO::MediaSourceInfo> 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 ViewModel
|
||||||
} // NS Jellyfin
|
} // NS Jellyfin
|
||||||
|
|
|
@ -21,173 +21,15 @@
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
namespace ViewModel {
|
namespace ViewModel {
|
||||||
|
|
||||||
Playlist::Playlist(ApiClient *apiClient, QObject *parent)
|
/*Playlist::Playlist(ApiClient *apiClient, QObject *parent)
|
||||||
: ItemModel(parent), m_apiClient(apiClient), m_fetcherThread(new ItemUrlFetcherThread(this)){
|
: ItemModel(parent) {
|
||||||
|
|
||||||
connect(this, &QAbstractListModel::rowsInserted, this, &Playlist::onItemsAdded);
|
connect(this, &QAbstractListModel::rowsInserted, this, &Playlist::onItemsAdded);
|
||||||
connect(this, &QAbstractListModel::rowsRemoved, this, &Playlist::onItemsRemoved);
|
connect(this, &QAbstractListModel::rowsRemoved, this, &Playlist::onItemsRemoved);
|
||||||
connect(this, &QAbstractListModel::rowsMoved, this, &Playlist::onItemsMoved);
|
connect(this, &QAbstractListModel::rowsMoved, this, &Playlist::onItemsMoved);
|
||||||
connect(this, &QAbstractListModel::modelReset, this, &Playlist::onItemsReset);
|
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<QString> 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<DTO::PlaybackInfoResponse> 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<DTO::MediaSourceInfo> 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 ViewModel
|
||||||
} // NS Jellyfin
|
} // NS Jellyfin
|
||||||
|
|
|
@ -66,6 +66,7 @@ void WebSocket::onDisconnected() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocket::textMessageReceived(const QString &message) {
|
void WebSocket::textMessageReceived(const QString &message) {
|
||||||
|
qDebug() << "WebSocket: message received: " << message;
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
|
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
|
||||||
if (doc.isNull() || !doc.isObject()) {
|
if (doc.isNull() || !doc.isObject()) {
|
||||||
qWarning() << "Malformed message received over WebSocket: parse error or root not an object.";
|
qWarning() << "Malformed message received over WebSocket: parse error or root not an object.";
|
||||||
|
|
|
@ -2,6 +2,8 @@ import QtQuick 2.12
|
||||||
import QtQuick.Controls 2.12
|
import QtQuick.Controls 2.12
|
||||||
import QtQuick.Window 2.12
|
import QtQuick.Window 2.12
|
||||||
|
|
||||||
|
import QtMultimedia 5.12
|
||||||
|
|
||||||
import nl.netsoj.chris.Jellyfin 1.0 as J
|
import nl.netsoj.chris.Jellyfin 1.0 as J
|
||||||
|
|
||||||
import "components"
|
import "components"
|
||||||
|
@ -18,6 +20,7 @@ ApplicationWindow {
|
||||||
|
|
||||||
J.PlaybackManager {
|
J.PlaybackManager {
|
||||||
id: playbackManager
|
id: playbackManager
|
||||||
|
apiClient: ApiClient
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Background {
|
background: Background {
|
||||||
|
@ -49,16 +52,45 @@ ApplicationWindow {
|
||||||
ApiClient.restoreSavedSession()
|
ApiClient.restoreSavedSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
footer: Column {
|
footer: Item {
|
||||||
id: footer
|
id: footer
|
||||||
Text {
|
height: Math.max(details.height, playButtons.height)
|
||||||
text: qsTr("Now playing")
|
Column {
|
||||||
color: "white"
|
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 {
|
Row {
|
||||||
text: playbackManager.item.name ? playbackManager.item.name : "Nothing"
|
id: playButtons
|
||||||
color: "white"
|
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 {
|
Rectangle {
|
||||||
color: "darkblue"
|
color: "darkblue"
|
||||||
|
|
|
@ -38,22 +38,24 @@ Page {
|
||||||
height: parent.height / 3
|
height: parent.height / 3
|
||||||
source: ApiClient.baseUrl + "/Items/" + itemId + "/Images/Primary?tag=" + jellyfinItem.tag
|
source: ApiClient.baseUrl + "/Items/" + itemId + "/Images/Primary?tag=" + jellyfinItem.tag
|
||||||
}
|
}
|
||||||
|
J.ItemModel {
|
||||||
|
id: tracks
|
||||||
|
loader: J.UserItemsLoader {
|
||||||
|
apiClient: ApiClient
|
||||||
|
parentId: detailPage.itemId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height / 3 * 2
|
height: parent.height / 3 * 2
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
model: J.ItemModel {
|
model: tracks
|
||||||
loader: J.UserItemsLoader {
|
delegate: ItemDelegate {
|
||||||
apiClient: ApiClient
|
|
||||||
parentId: detailPage.itemId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delegate: ItemDelegate{
|
|
||||||
icon.source: ApiClient.baseUrl + "/Items/" + model.jellyfinId + "/Images/Primary?tag=" + model.tag
|
icon.source: ApiClient.baseUrl + "/Items/" + model.jellyfinId + "/Images/Primary?tag=" + model.tag
|
||||||
text: model.name
|
text: model.name
|
||||||
width: parent.width
|
width: ListView.view.width
|
||||||
onClicked: playbackManager.playItem(model.jellyfinId)
|
onClicked: playbackManager.playItemInList(tracks, model.index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
[Controls]
|
[Controls]
|
||||||
Style=SailfinStyle
|
Style=SailfinStyle
|
||||||
FallbackStyle=Universal
|
FallbackStyle=Material
|
||||||
|
|
|
@ -43,7 +43,7 @@ Files:
|
||||||
|
|
||||||
Macros:
|
Macros:
|
||||||
- '__provides_exclude_from; ^%{_datadir}/.*$'
|
- '__provides_exclude_from; ^%{_datadir}/.*$'
|
||||||
- '__requires_exclude; ^libjellyfin-qt.*$'
|
- '__requires_exclude; ^libJellyfinQt.*$'
|
||||||
# Turn off facist build policy
|
# Turn off facist build policy
|
||||||
- '_unpackaged_files_terminate_build; 0 '
|
- '_unpackaged_files_terminate_build; 0 '
|
||||||
|
|
||||||
|
|
|
@ -18,25 +18,27 @@ set(harbour-sailfin_SOURCES
|
||||||
src/harbour-sailfin.cpp)
|
src/harbour-sailfin.cpp)
|
||||||
|
|
||||||
set(sailfin_QML_SOURCES
|
set(sailfin_QML_SOURCES
|
||||||
|
qml/ApiClient.qml
|
||||||
qml/Constants.qml
|
qml/Constants.qml
|
||||||
qml/Utils.js
|
qml/Utils.js
|
||||||
qml/components/music/NarrowAlbumCover.qml
|
qml/components/music/NarrowAlbumCover.qml
|
||||||
qml/components/music/WideAlbumCover.qml
|
qml/components/music/WideAlbumCover.qml
|
||||||
qml/components/music/SongDelegate.qml
|
qml/components/music/SongDelegate.qml
|
||||||
qml/components/videoplayer/VideoError.qml
|
qml/components/videoplayer/VideoError.qml
|
||||||
qml/components/videoplayer/VideoHud.qml
|
qml/components/videoplayer/VideoHud.qml
|
||||||
qml/components/IconListItem.qml
|
qml/components/IconListItem.qml
|
||||||
qml/components/LibraryItemDelegate.qml
|
qml/components/JItem.qml
|
||||||
|
qml/components/LibraryItemDelegate.qml
|
||||||
qml/components/MoreSection.qml
|
qml/components/MoreSection.qml
|
||||||
qml/components/PlainLabel.qml
|
qml/components/PlainLabel.qml
|
||||||
qml/components/PlaybackBar.qml
|
qml/components/PlaybackBar.qml
|
||||||
qml/components/PlayQueue.qml
|
qml/components/PlayQueue.qml
|
||||||
qml/components/PlayToolbar.qml
|
qml/components/PlayToolbar.qml
|
||||||
qml/components/RemoteImage.qml
|
qml/components/RemoteImage.qml
|
||||||
qml/components/Shim.qml
|
qml/components/Shim.qml
|
||||||
qml/components/UserGridDelegate.qml
|
qml/components/UserGridDelegate.qml
|
||||||
qml/components/VideoPlayer.qml
|
qml/components/VideoPlayer.qml
|
||||||
qml/components/VideoTrackSelector.qml
|
qml/components/VideoTrackSelector.qml
|
||||||
qml/cover/CoverPage.qml
|
qml/cover/CoverPage.qml
|
||||||
qml/cover/PosterCover.qml
|
qml/cover/PosterCover.qml
|
||||||
qml/cover/VideoCover.qml
|
qml/cover/VideoCover.qml
|
||||||
|
@ -51,21 +53,22 @@ set(sailfin_QML_SOURCES
|
||||||
qml/pages/itemdetails/EpisodePage.qml
|
qml/pages/itemdetails/EpisodePage.qml
|
||||||
qml/pages/itemdetails/FilmPage.qml
|
qml/pages/itemdetails/FilmPage.qml
|
||||||
qml/pages/itemdetails/MusicAlbumPage.qml
|
qml/pages/itemdetails/MusicAlbumPage.qml
|
||||||
qml/pages/itemdetails/PhotoPage.qml
|
qml/pages/itemdetails/PhotoPage.qml
|
||||||
qml/pages/itemdetails/SeasonPage.qml
|
qml/pages/itemdetails/SeasonPage.qml
|
||||||
qml/pages/itemdetails/SeriesPage.qml
|
qml/pages/itemdetails/SeriesPage.qml
|
||||||
qml/pages/itemdetails/UnsupportedPage.qml
|
qml/pages/itemdetails/UnsupportedPage.qml
|
||||||
qml/pages/itemdetails/VideoPage.qml
|
qml/pages/itemdetails/VideoPage.qml
|
||||||
qml/pages/settings/DebugPage.qml
|
qml/pages/settings/DebugPage.qml
|
||||||
qml/pages/setup/AddServerConnectingPage.qml
|
qml/pages/setup/AddServerConnectingPage.qml
|
||||||
qml/pages/setup/LoginDialog.qml
|
qml/pages/setup/AddServerPage.qml
|
||||||
|
qml/pages/setup/LoginDialog.qml
|
||||||
qml/qmldir)
|
qml/qmldir)
|
||||||
|
|
||||||
add_executable(harbour-sailfin ${harbour-sailfin_SOURCES} ${sailfin_QML_SOURCES})
|
add_executable(harbour-sailfin ${harbour-sailfin_SOURCES} ${sailfin_QML_SOURCES})
|
||||||
target_link_libraries(harbour-sailfin PRIVATE Qt5::Gui Qt5::Qml Qt5::Quick SailfishApp::SailfishApp
|
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
|
# Note: this may break when the compiler changes. -rdynamic and -pie seem to be needed for the
|
||||||
# invoker/booster to work
|
# 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
|
target_compile_definitions(harbour-sailfin
|
||||||
PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)
|
PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ Type=Application
|
||||||
Version=1.1
|
Version=1.1
|
||||||
X-Nemo-Application-Type=silica-qt5
|
X-Nemo-Application-Type=silica-qt5
|
||||||
Icon=harbour-sailfin
|
Icon=harbour-sailfin
|
||||||
Exec=harbour-sailfin --attempt-sandbox
|
Exec=harbour-sailfin
|
||||||
|
|
||||||
|
|
||||||
Name=Sailfin
|
Name=Sailfin
|
||||||
|
|
7
sailfish/qml/ApiClient.qml
Normal file
7
sailfish/qml/ApiClient.qml
Normal file
|
@ -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]
|
||||||
|
}
|
|
@ -68,6 +68,7 @@ function itemModelImageUrl(baseUrl, itemId, tag, type, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePortraitCover(itemType) {
|
function usePortraitCover(itemType) {
|
||||||
|
console.log("ItemType: " + itemType)
|
||||||
return ["Series", "Movie", "tvshows", "movies"].indexOf(itemType) >= 0
|
return ["Series", "Movie", "tvshows", "movies"].indexOf(itemType) >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
9
sailfish/qml/components/JItem.qml
Normal file
9
sailfish/qml/components/JItem.qml
Normal file
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import Nemo.KeepAlive 1.2
|
||||||
|
|
||||||
import "components"
|
import "components"
|
||||||
import "pages"
|
import "pages"
|
||||||
|
import "." as D
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: appWindow
|
id: appWindow
|
||||||
|
@ -35,14 +36,13 @@ ApplicationWindow {
|
||||||
//readonly property MediaPlayer mediaPlayer: _mediaPlayer
|
//readonly property MediaPlayer mediaPlayer: _mediaPlayer
|
||||||
readonly property PlaybackManager playbackManager: _playbackManager
|
readonly property PlaybackManager playbackManager: _playbackManager
|
||||||
|
|
||||||
// Data of the currently selected item. For use on the cover.
|
// Due QTBUG-10822, declarartions such as `property J.Item foo` are not possible.
|
||||||
property JellyfinItem itemData
|
property QtObject itemData
|
||||||
// Id of the collection currently browsing. For use on the cover.
|
// Id of the collection currently browsing. For use on the cover.
|
||||||
property string collectionId
|
property string collectionId
|
||||||
|
|
||||||
// Bad way to implement settings, but it'll do for now.
|
// Bad way to implement settings, but it'll do for now.
|
||||||
property bool showDebugInfo: true
|
property bool showDebugInfo: true
|
||||||
|
|
||||||
property bool _hidePlaybackBar: false
|
property bool _hidePlaybackBar: false
|
||||||
|
|
||||||
bottomMargin: playbackBar.visibleSize
|
bottomMargin: playbackBar.visibleSize
|
||||||
|
@ -52,14 +52,14 @@ ApplicationWindow {
|
||||||
initialPage: Component {
|
initialPage: Component {
|
||||||
MainPage {
|
MainPage {
|
||||||
Connections {
|
Connections {
|
||||||
target: ApiClient
|
target: D.ApiClient
|
||||||
// Replace the MainPage if no server was set up.
|
// Replace the MainPage if no server was set up.
|
||||||
|
|
||||||
}
|
}
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (status == PageStatus.Active && !_hasInitialized) {
|
if (status == PageStatus.Active && !_hasInitialized) {
|
||||||
_hasInitialized = true;
|
_hasInitialized = true;
|
||||||
ApiClient.restoreSavedSession();
|
D.ApiClient.restoreSavedSession();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ ApplicationWindow {
|
||||||
|
|
||||||
PlaybackManager {
|
PlaybackManager {
|
||||||
id: _playbackManager
|
id: _playbackManager
|
||||||
apiClient: ApiClient
|
apiClient: D.ApiClient
|
||||||
audioIndex: 0
|
audioIndex: 0
|
||||||
autoOpen: true
|
autoOpen: true
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ ApplicationWindow {
|
||||||
|
|
||||||
//FIXME: proper error handling
|
//FIXME: proper error handling
|
||||||
Connections {
|
Connections {
|
||||||
target: ApiClient
|
target: D.ApiClient
|
||||||
onNetworkError: errorNotification.show("Network error: " + error)
|
onNetworkError: errorNotification.show("Network error: " + error)
|
||||||
onConnectionFailed: errorNotification.show("Connect error: " + error)
|
onConnectionFailed: errorNotification.show("Connect error: " + error)
|
||||||
//onConnectionSuccess: errorNotification.show("Success: " + loginMessage)
|
//onConnectionSuccess: errorNotification.show("Success: " + loginMessage)
|
||||||
|
|
|
@ -19,9 +19,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.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 "../components"
|
||||||
|
import ".."
|
||||||
|
|
||||||
Page {
|
Page {
|
||||||
id: page
|
id: page
|
||||||
|
|
|
@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import Sailfish.Silica 1.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 "../components"
|
||||||
import "../"
|
import "../"
|
||||||
|
@ -32,7 +32,7 @@ Page {
|
||||||
/// True if the models on this page already have been loaded and don't necessarily need a refresh
|
/// True if the models on this page already have been loaded and don't necessarily need a refresh
|
||||||
property bool _modelsLoaded: false
|
property bool _modelsLoaded: false
|
||||||
|
|
||||||
id: page
|
id: mainPage
|
||||||
allowedOrientations: Orientation.All
|
allowedOrientations: Orientation.All
|
||||||
|
|
||||||
// This component is reused both in the normal state and error state
|
// This component is reused both in the normal state and error state
|
||||||
|
@ -49,7 +49,7 @@ Page {
|
||||||
text: qsTr("Reload")
|
text: qsTr("Reload")
|
||||||
onClicked: loadModels(true)
|
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.
|
// of the page, followed by our content.
|
||||||
Column {
|
Column {
|
||||||
id: column
|
id: column
|
||||||
width: page.width
|
width: mainPage.width
|
||||||
|
|
||||||
UserViewModel {
|
J.ItemModel {
|
||||||
id: mediaLibraryModel2
|
id: mediaLibraryModel
|
||||||
apiClient: ApiClient
|
loader: J.UsersViewsLoader {
|
||||||
|
id: mediaLibraryLoader
|
||||||
|
apiClient: ApiClient
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MoreSection {
|
MoreSection {
|
||||||
//- Section header for films and TV shows that an user hasn't completed yet.
|
//- Section header for films and TV shows that an user hasn't completed yet.
|
||||||
text: qsTr("Resume watching")
|
text: qsTr("Resume watching")
|
||||||
clickable: false
|
clickable: false
|
||||||
busy: userResumeModel.status === ApiModel.Loading
|
//busy: userResumeModel.status === J.ApiModel.Loading
|
||||||
Loader {
|
Loader {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
sourceComponent: carrouselView
|
sourceComponent: carrouselView
|
||||||
property alias itemModel: userResumeModel
|
property alias itemModel: userResumeModel
|
||||||
property string collectionType: "series"
|
property string collectionType: "series"
|
||||||
|
|
||||||
UserItemResumeModel {
|
J.ItemModel {
|
||||||
id: userResumeModel
|
id: userResumeModel
|
||||||
apiClient: ApiClient
|
// Resume model
|
||||||
|
/*apiClient: ApiClient
|
||||||
limit: 12
|
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.
|
//- Section header for next episodes in a TV show that an user was watching.
|
||||||
text: qsTr("Next up")
|
text: qsTr("Next up")
|
||||||
clickable: false
|
clickable: false
|
||||||
busy: showNextUpModel.status === ApiModel.Loading
|
//busy: showNextUpModel.status === .Loading
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
@ -105,23 +109,18 @@ Page {
|
||||||
property alias itemModel: showNextUpModel
|
property alias itemModel: showNextUpModel
|
||||||
property string collectionType: "series"
|
property string collectionType: "series"
|
||||||
|
|
||||||
ShowNextUpModel {
|
J.ItemModel {
|
||||||
id: showNextUpModel
|
id: showNextUpModel
|
||||||
apiClient: ApiClient
|
/*apiClient: ApiClient
|
||||||
limit: 12
|
limit: 12*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UserViewModel {
|
|
||||||
id: mediaLibraryModel
|
|
||||||
apiClient: ApiClient
|
|
||||||
}
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: mediaLibraryModel
|
model: mediaLibraryModel
|
||||||
MoreSection {
|
MoreSection {
|
||||||
text: model.name
|
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})
|
onHeaderClicked: pageStack.push(Qt.resolvedUrl("itemdetails/CollectionPage.qml"), {"itemId": model.jellyfinId})
|
||||||
Loader {
|
Loader {
|
||||||
|
@ -130,11 +129,12 @@ Page {
|
||||||
property alias itemModel: userItemModel
|
property alias itemModel: userItemModel
|
||||||
property string collectionType: model.collectionType || ""
|
property string collectionType: model.collectionType || ""
|
||||||
|
|
||||||
UserItemLatestModel {
|
J.ItemModel {
|
||||||
id: userItemModel
|
id: userItemModel
|
||||||
apiClient: ApiClient
|
loader: J.LatestMediaLoader {
|
||||||
parentId: jellyfinId
|
apiClient: ApiClient
|
||||||
limit: 16
|
parentId: jellyfinId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: mediaLibraryModel
|
target: mediaLibraryModel
|
||||||
|
@ -184,8 +184,8 @@ Page {
|
||||||
if (force || (ApiClient.authenticated && !_modelsLoaded)) {
|
if (force || (ApiClient.authenticated && !_modelsLoaded)) {
|
||||||
_modelsLoaded = true;
|
_modelsLoaded = true;
|
||||||
mediaLibraryModel.reload()
|
mediaLibraryModel.reload()
|
||||||
userResumeModel.reload()
|
//userResumeModel.reload()
|
||||||
showNextUpModel.reload()
|
//showNextUpModel.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,11 +236,11 @@ Page {
|
||||||
states: [
|
states: [
|
||||||
State {
|
State {
|
||||||
name: "default"
|
name: "default"
|
||||||
when: mediaLibraryModel2.status !== ApiModel.Error
|
when: mediaLibraryLoader.status !== J.UsersViewsLoader.Error
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "error"
|
name: "error"
|
||||||
when: mediaLibraryModel2.status === ApiModel.Error
|
when: mediaLibraryLoader.status === J.UsersViewsLoader.Error
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: errorFlickable
|
target: errorFlickable
|
||||||
|
|
|
@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.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 "../components"
|
||||||
|
|
||||||
|
@ -57,9 +57,9 @@ Page {
|
||||||
rightMargin: Theme.horizontalPageMargin
|
rightMargin: Theme.horizontalPageMargin
|
||||||
}
|
}
|
||||||
height: user.implicitHeight + server.implicitHeight + Theme.paddingMedium
|
height: user.implicitHeight + server.implicitHeight + Theme.paddingMedium
|
||||||
User {
|
QtObject {
|
||||||
id: loggedInUser
|
id: loggedInUser
|
||||||
apiClient: ApiClient
|
//apiClient: ApiClient
|
||||||
}
|
}
|
||||||
RemoteImage {
|
RemoteImage {
|
||||||
id: userIcon
|
id: userIcon
|
||||||
|
|
|
@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.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 "../../components"
|
||||||
import "../.."
|
import "../.."
|
||||||
|
@ -32,14 +32,14 @@ import "../.."
|
||||||
*/
|
*/
|
||||||
Page {
|
Page {
|
||||||
id: pageRoot
|
id: pageRoot
|
||||||
property alias itemId: jItem.jellyfinId
|
property string itemId: ""
|
||||||
property alias itemData: jItem
|
property alias itemData: jItemLoader.data
|
||||||
//property string itemId: ""
|
//property string itemId: ""
|
||||||
//property var itemData: ({})
|
//property var itemData: ({})
|
||||||
property bool _loading: jItem.status === "Loading"
|
property bool _loading: jItemLoader.status === J.ItemLoader.Loading
|
||||||
readonly property bool hasLogo: (typeof itemData.ImageTags !== "undefined") && (typeof itemData.ImageTags["Logo"] !== "undefined")
|
readonly property bool hasLogo: (typeof itemData.imageTags !== "undefined") && (typeof itemData.imageTags["Logo"] !== "undefined")
|
||||||
property string _chosenBackdropImage: ""
|
property string _chosenBackdropImage: ""
|
||||||
readonly property string parentId: itemData.ParentId || ""
|
readonly property string parentId: itemData.parentId || ""
|
||||||
|
|
||||||
function updateBackdrop() {
|
function updateBackdrop() {
|
||||||
var rand = 0;
|
var rand = 0;
|
||||||
|
@ -62,13 +62,13 @@ Page {
|
||||||
SilicaFlickable {
|
SilicaFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
contentHeight: errorContent.height
|
contentHeight: errorContent.height
|
||||||
visible: jItem.status == JellyfinItem.Error
|
visible: jItemLoader.status === J.ItemLoader.Error
|
||||||
|
|
||||||
PullDownMenu {
|
PullDownMenu {
|
||||||
busy: jItem.status == JellyfinItem.Loading
|
busy: jItemLoader.status === J.ItemLoader.Loading
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Retry")
|
text: qsTr("Retry")
|
||||||
onClicked: jItem.reload()
|
onClicked: jItemLoader.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,28 +79,33 @@ Page {
|
||||||
ViewPlaceholder {
|
ViewPlaceholder {
|
||||||
enabled: true
|
enabled: true
|
||||||
text: qsTr("An error has occured")
|
text: qsTr("An error has occured")
|
||||||
hintText: jItem.errorString
|
hintText: jItemLoader.errorString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
JellyfinItem {
|
J.ItemLoader {
|
||||||
id: jItem
|
id: jItemLoader
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
|
itemId: pageRoot.itemId
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
//console.log("Status changed: " + newStatus, JSON.stringify(jItem))
|
console.log("Status changed: " + newStatus, JSON.stringify(jItemLoader.data))
|
||||||
if (status == JellyfinItem.Ready) {
|
if (status === J.ItemLoader.Ready) {
|
||||||
updateBackdrop()
|
updateBackdrop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "ItemLoader status=%1, \nitemId=%2\nitemData=%3".arg(jItemLoader.status).arg(jItemLoader.itemId).arg(jItemLoader.data)
|
||||||
|
}
|
||||||
|
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (status == PageStatus.Deactivating) {
|
if (status == PageStatus.Deactivating) {
|
||||||
//appWindow.itemData = ({})
|
//appWindow.itemData = ({})
|
||||||
}
|
}
|
||||||
if (status == PageStatus.Active) {
|
if (status == PageStatus.Active) {
|
||||||
appWindow.itemData = jItem
|
//appWindow.itemData = jItemLoader.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.0
|
import Sailfish.Silica 1.0
|
||||||
|
|
||||||
import nl.netsoj.chris.Jellyfin 1.0
|
import nl.netsoj.chris.Jellyfin 1.0 as J
|
||||||
|
|
||||||
import "../.."
|
import "../.."
|
||||||
import "../../components"
|
import "../../components"
|
||||||
|
@ -27,12 +27,13 @@ import "../../components"
|
||||||
BaseDetailPage {
|
BaseDetailPage {
|
||||||
id: pageRoot
|
id: pageRoot
|
||||||
|
|
||||||
UserItemModel {
|
J.ItemModel {
|
||||||
id: collectionModel
|
id: collectionModel
|
||||||
apiClient: ApiClient
|
//sortBy: ["SortName"]
|
||||||
parentId: itemData.jellyfinId
|
loader: J.UserItemsLoader {
|
||||||
sortBy: ["SortName"]
|
apiClient: ApiClient
|
||||||
onParentIdChanged: reload()
|
parentId: itemData.jellyfinId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SilicaGridView {
|
SilicaGridView {
|
||||||
|
@ -42,7 +43,7 @@ BaseDetailPage {
|
||||||
cellWidth: Constants.libraryDelegateWidth
|
cellWidth: Constants.libraryDelegateWidth
|
||||||
cellHeight: Utils.usePortraitCover(itemData.collectionType) ? Constants.libraryDelegatePosterHeight
|
cellHeight: Utils.usePortraitCover(itemData.collectionType) ? Constants.libraryDelegatePosterHeight
|
||||||
: Constants.libraryDelegateHeight
|
: Constants.libraryDelegateHeight
|
||||||
visible: itemData.status !== JellyfinItem.Error
|
visible: itemData.status !== J.ItemLoader.Error
|
||||||
|
|
||||||
header: PageHeader {
|
header: PageHeader {
|
||||||
title: itemData.name || qsTr("Loading")
|
title: itemData.name || qsTr("Loading")
|
||||||
|
@ -54,7 +55,7 @@ BaseDetailPage {
|
||||||
text: qsTr("Sort by")
|
text: qsTr("Sort by")
|
||||||
onClicked: pageStack.push(sortPageComponent)
|
onClicked: pageStack.push(sortPageComponent)
|
||||||
}
|
}
|
||||||
busy: collectionModel.status === ApiModel.Loading
|
busy: collectionModel.status === J.UserItemsLoader.Loading
|
||||||
}
|
}
|
||||||
delegate: GridItem {
|
delegate: GridItem {
|
||||||
RemoteImage {
|
RemoteImage {
|
||||||
|
|
|
@ -20,7 +20,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.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 "../../components"
|
||||||
import "../.."
|
import "../.."
|
||||||
|
|
|
@ -20,7 +20,7 @@ import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.0
|
import Sailfish.Silica 1.0
|
||||||
import QtQuick.Layouts 1.1
|
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"
|
||||||
import "../../components/music"
|
import "../../components/music"
|
||||||
|
@ -33,13 +33,15 @@ BaseDetailPage {
|
||||||
|
|
||||||
readonly property bool _twoColumns: albumPageRoot.width / Theme.pixelRatio >= 800
|
readonly property bool _twoColumns: albumPageRoot.width / Theme.pixelRatio >= 800
|
||||||
|
|
||||||
UserItemModel {
|
J.ItemModel {
|
||||||
id: collectionModel
|
id: collectionModel
|
||||||
apiClient: ApiClient
|
loader: J.UserItemsLoader {
|
||||||
sortBy: ["SortName"]
|
apiClient: ApiClient
|
||||||
fields: ["ItemCounts","PrimaryImageAspectRatio","BasicSyncInfo","CanDelete","MediaSourceCount"]
|
//sortBy: ["SortName"]
|
||||||
parentId: itemData.jellyfinId
|
//fields: ["ItemCounts","PrimaryImageAspectRatio","BasicSyncInfo","CanDelete","MediaSourceCount"]
|
||||||
onParentIdChanged: reload()
|
parentId: itemData.jellyfinId
|
||||||
|
onParentIdChanged: reload()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -50,9 +52,9 @@ BaseDetailPage {
|
||||||
visible: _twoColumns
|
visible: _twoColumns
|
||||||
Layout.minimumWidth: 1000 / Theme.pixelRatio
|
Layout.minimumWidth: 1000 / Theme.pixelRatio
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
source: visible
|
/*source: visible
|
||||||
? "../../components/music/WideAlbumCover.qml" : ""
|
? "../../components/music/WideAlbumCover.qml" : ""
|
||||||
onLoaded: bindAlbum(item)
|
onLoaded: bindAlbum(item)*/
|
||||||
}
|
}
|
||||||
Item {height: 1; width: Theme.horizontalPageMargin; visible: wideAlbumCover.visible; }
|
Item {height: 1; width: Theme.horizontalPageMargin; visible: wideAlbumCover.visible; }
|
||||||
SilicaListView {
|
SilicaListView {
|
||||||
|
@ -62,8 +64,8 @@ BaseDetailPage {
|
||||||
model: collectionModel
|
model: collectionModel
|
||||||
header: Loader {
|
header: Loader {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
source: "../../components/music/NarrowAlbumCover.qml"
|
/*source: "../../components/music/NarrowAlbumCover.qml"
|
||||||
onLoaded: bindAlbum(item)
|
onLoaded: bindAlbum(item)*/
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
property: "parentIndexNumber"
|
property: "parentIndexNumber"
|
||||||
|
@ -84,14 +86,8 @@ BaseDetailPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: itemData
|
|
||||||
onAlbumArtistsChanged: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindAlbum(item) {
|
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.name = Qt.binding(function(){ return itemData.name})
|
||||||
item.releaseYear = Qt.binding(function() { return itemData.productionYear})
|
item.releaseYear = Qt.binding(function() { return itemData.productionYear})
|
||||||
item.albumArtist = Qt.binding(function() { return itemData.albumArtist})
|
item.albumArtist = Qt.binding(function() { return itemData.albumArtist})
|
||||||
|
|
|
@ -20,7 +20,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.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 "../../components"
|
||||||
import "../.."
|
import "../.."
|
||||||
|
@ -33,11 +33,11 @@ BaseDetailPage {
|
||||||
property alias subtitle: pageHeader.description
|
property alias subtitle: pageHeader.description
|
||||||
default property alias _data: content.data
|
default property alias _data: content.data
|
||||||
property real _playbackProsition: itemData.userData.playbackPositionTicks
|
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 {
|
SilicaFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
contentHeight: content.height + Theme.paddingLarge
|
contentHeight: content.height + Theme.paddingLarge
|
||||||
visible: itemData.status !== JellyfinItem.Error
|
visible: itemData.status !== J.ItemLoader.Error
|
||||||
|
|
||||||
VerticalScrollDecorator {}
|
VerticalScrollDecorator {}
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ BaseDetailPage {
|
||||||
Connections {
|
Connections {
|
||||||
target: itemData
|
target: itemData
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (status == JellyfinItem.Ready) {
|
if (status === J.ItemLoader.Ready) {
|
||||||
console.log(itemData.mediaStreams)
|
console.log(itemData.mediaStreams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.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 "../../components"
|
||||||
|
import "../.."
|
||||||
|
|
||||||
Page {
|
Page {
|
||||||
id: page
|
id: page
|
||||||
|
@ -53,7 +54,7 @@ Page {
|
||||||
label: qsTr("Connection state")
|
label: qsTr("Connection state")
|
||||||
value: {
|
value: {
|
||||||
var stateText
|
var stateText
|
||||||
switch( ApiClient.websocket.state) {
|
switch(ApiClient.websocket.state) {
|
||||||
case 0:
|
case 0:
|
||||||
//- Socket state
|
//- Socket state
|
||||||
stateText = qsTr("Unconnected");
|
stateText = qsTr("Unconnected");
|
||||||
|
|
|
@ -18,7 +18,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.0
|
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.
|
* Page to indicate that the application is connecting to a server.
|
||||||
|
|
|
@ -18,7 +18,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.0
|
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.
|
* Dialog showed when the user has to connect to a Jellyfin server.
|
||||||
|
@ -49,7 +51,7 @@ Dialog {
|
||||||
title: qsTr("Connect to Jellyfin")
|
title: qsTr("Connect to Jellyfin")
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerDiscoveryModel {
|
J.ServerDiscoveryModel {
|
||||||
id: serverModel
|
id: serverModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.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 "../../components"
|
||||||
|
import "../../"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page where the user can login on their server. Is displayed after the AddServerPage successfully connected
|
* Page where the user can login on their server. Is displayed after the AddServerPage successfully connected
|
||||||
|
@ -31,7 +32,7 @@ Dialog {
|
||||||
id: loginDialog
|
id: loginDialog
|
||||||
property string loginMessage
|
property string loginMessage
|
||||||
property Page firstPage
|
property Page firstPage
|
||||||
property User selectedUser: null
|
property QtObject /*User*/ selectedUser: null
|
||||||
|
|
||||||
property string error
|
property string error
|
||||||
|
|
||||||
|
@ -65,11 +66,13 @@ Dialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PublicUserModel {
|
QtObject { id: userModel; }
|
||||||
|
|
||||||
|
/*PublicUserModel {
|
||||||
id: userModel
|
id: userModel
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
Component.onCompleted: reload();
|
Component.onCompleted: reload();
|
||||||
}
|
}*/
|
||||||
|
|
||||||
DialogHeader {
|
DialogHeader {
|
||||||
id: dialogHeader
|
id: dialogHeader
|
||||||
|
@ -97,7 +100,7 @@ Dialog {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
Repeater {
|
Repeater {
|
||||||
id: userRepeater
|
id: userRepeater
|
||||||
model: userModel
|
model: 0 //userModel
|
||||||
delegate: UserGridDelegate {
|
delegate: UserGridDelegate {
|
||||||
name: model.name
|
name: model.name
|
||||||
image: model.primaryImageTag ? "%1/Users/%2/Images/Primary?tag=%3".arg(ApiClient.baseUrl).arg(model.jellyfinId).arg(model.primaryImageTag) : ""
|
image: model.primaryImageTag ? "%1/Users/%2/Images/Primary?tag=%3".arg(ApiClient.baseUrl).arg(model.jellyfinId).arg(model.primaryImageTag) : ""
|
||||||
|
|
|
@ -16,4 +16,5 @@
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
singleton Constants 1.0 Constants.qml
|
singleton Constants 1.0 Constants.qml
|
||||||
|
singleton ApiClient 1.0 ApiClient.qml
|
||||||
Utils 1.0 Utils.js
|
Utils 1.0 Utils.js
|
||||||
|
|
|
@ -60,14 +60,14 @@ int main(int argc, char *argv[]) {
|
||||||
QCommandLineParser cmdParser;
|
QCommandLineParser cmdParser;
|
||||||
cmdParser.addHelpOption();
|
cmdParser.addHelpOption();
|
||||||
cmdParser.addVersionOption();
|
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) {
|
if (canSanbox) {
|
||||||
cmdParser.addOption(sandboxOption);
|
cmdParser.addOption(sandboxOption);
|
||||||
}
|
}
|
||||||
cmdParser.process(*app);
|
cmdParser.process(*app);
|
||||||
|
|
||||||
if (canSanbox && cmdParser.isSet(sandboxOption)) {
|
if (canSanbox && !cmdParser.isSet(sandboxOption)) {
|
||||||
qDebug() << "Restarting in Sanbox mode";
|
qDebug() << "Restarting in sandbox mode";
|
||||||
QProcess::execute(QString(SANDBOX_PROGRAM),
|
QProcess::execute(QString(SANDBOX_PROGRAM),
|
||||||
QStringList() << "-p" << "harbour-sailfin.desktop" << "/usr/bin/harbour-sailfin");
|
QStringList() << "-p" << "harbour-sailfin.desktop" << "/usr/bin/harbour-sailfin");
|
||||||
return 0;
|
return 0;
|
||||||
|
|
Loading…
Reference in a new issue