1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2025-09-04 01:42:44 +00:00

WIP: Add playlists/queues and add support for Sailfish back

This commit is contained in:
Chris Josten 2021-07-31 15:06:17 +02:00
parent fbc154fb56
commit 86672be051
89 changed files with 1637 additions and 849 deletions

View file

@ -8,8 +8,11 @@ set(JellyfinQt_SOURCES
src/model/deviceprofile.cpp
src/model/item.cpp
src/model/playlist.cpp
src/model/shuffle.cpp
src/support/jsonconv.cpp
src/support/loader.cpp
src/support/parseexception.cpp
src/viewmodel/item.cpp
src/viewmodel/itemmodel.cpp
src/viewmodel/loader.cpp
@ -31,8 +34,11 @@ set(JellyfinQt_HEADERS
include/JellyfinQt/model/deviceprofile.h
include/JellyfinQt/model/item.h
include/JellyfinQt/model/playlist.h
include/JellyfinQt/model/shuffle.h
include/JellyfinQt/support/jsonconv.h
include/JellyfinQt/support/jsonconvimpl.h
include/JellyfinQt/support/loader.h
include/JellyfinQt/support/parseexception.h
include/JellyfinQt/viewmodel/item.h
include/JellyfinQt/viewmodel/itemmodel.h
include/JellyfinQt/viewmodel/loader.h

View file

@ -231,6 +231,9 @@ public slots:
protected slots:
void defaultNetworkErrorHandler(QNetworkReply::NetworkError error);
void onUserDataChanged(const QString &itemId, UserData *newData);
void credManagerServersListed(QStringList users);
void credManagerUsersListed(const QString &server, QStringList users);
void credManagerTokenRetrieved(const QString &server, const QString &user, const QString &token);
protected:
/**

View file

@ -108,8 +108,7 @@ signals:
*/
void itemsLoaded();
void reloadWanted();
protected slots:
public slots:
virtual void futureReady() = 0;
protected:
@ -243,6 +242,18 @@ bool setRequestStartIndex(P &parameters, int startIndex) {
return false;
}
#ifndef JELLYFIN_APIMODEL_CPP
extern template bool setRequestStartIndex(Loader::GetUserViewsParams &params, int startIndex);
extern template void setRequestLimit(Loader::GetUserViewsParams &params, 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 &params, int limit);
extern template bool setRequestStartIndex(Loader::GetLatestMediaParams &params, int offset);
#endif
/**
* Template for implementing a loader for the given type, response and parameters using Jellyfin::Support:Loaders.
*
@ -301,7 +312,7 @@ protected:
return;
}
result = optResult.value();
} catch (Support::LoadException e) {
} catch (Support::LoadException &e) {
qWarning() << "Exception while loading: " << e.what();
this->setStatus(ViewModel::ModelStatus::Error);
return;
@ -430,11 +441,11 @@ public:
}
// QList-like API
const T& at(int index) { return m_array.at(index); }
const T& at(int index) const { return m_array.at(index); }
/**
* @return the amount of objects in this model.
*/
int size() {
int size() const {
return m_array.size();
}

View file

@ -101,7 +101,7 @@ public:
signals:
void tokenRetrieved(const QString &server, const QString &user, const QString &token) const;
void serversListed(const QStringList &servers) const;
void usersListed(const QStringList &users) const;
void usersListed(const QString& server, const QStringList &users) const;
protected:
explicit CredentialsManager(QObject *parent = nullptr) : QObject (parent) {}

View file

@ -68,7 +68,7 @@ public:
protected:
QSharedPointer<LibraryOptions> m_libraryOptions = nullptr;
QSharedPointer<LibraryOptions> m_libraryOptions = QSharedPointer<LibraryOptions>();
};
} // NS DTO

View file

@ -94,7 +94,7 @@ public:
protected:
QSharedPointer<AlbumInfo> m_searchInfo = nullptr;
QSharedPointer<AlbumInfo> m_searchInfo = QSharedPointer<AlbumInfo>();
QString m_itemId;
QString m_searchProviderName;
bool m_includeDisabledProviders;

View file

@ -78,9 +78,9 @@ public:
protected:
QSharedPointer<ThemeMediaResult> m_themeVideosResult = nullptr;
QSharedPointer<ThemeMediaResult> m_themeSongsResult = nullptr;
QSharedPointer<ThemeMediaResult> m_soundtrackSongsResult = nullptr;
QSharedPointer<ThemeMediaResult> m_themeVideosResult = QSharedPointer<ThemeMediaResult>();
QSharedPointer<ThemeMediaResult> m_themeSongsResult = QSharedPointer<ThemeMediaResult>();
QSharedPointer<ThemeMediaResult> m_soundtrackSongsResult = QSharedPointer<ThemeMediaResult>();
};
} // NS DTO

View file

@ -94,7 +94,7 @@ public:
protected:
QSharedPointer<ArtistInfo> m_searchInfo = nullptr;
QSharedPointer<ArtistInfo> m_searchInfo = QSharedPointer<ArtistInfo>();
QString m_itemId;
QString m_searchProviderName;
bool m_includeDisabledProviders;

View file

@ -89,8 +89,8 @@ public:
protected:
QSharedPointer<UserDto> m_user = nullptr;
QSharedPointer<SessionInfo> m_sessionInfo = nullptr;
QSharedPointer<UserDto> m_user = QSharedPointer<UserDto>();
QSharedPointer<SessionInfo> m_sessionInfo = QSharedPointer<SessionInfo>();
QString m_accessToken;
QString m_serverId;
};

View file

@ -1611,7 +1611,7 @@ protected:
QString m_parentBackdropItemId;
QStringList m_parentBackdropImageTags;
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_childCount = std::nullopt;
QString m_seriesName;
@ -1699,7 +1699,7 @@ protected:
std::optional<bool> m_isKids = std::nullopt;
std::optional<bool> m_isPremiere = std::nullopt;
QString m_timerId;
QSharedPointer<BaseItemDto> m_currentProgram = nullptr;
QSharedPointer<BaseItemDto> m_currentProgram = QSharedPointer<BaseItemDto>();
};
} // NS DTO

View file

@ -94,7 +94,7 @@ public:
protected:
QSharedPointer<BookInfo> m_searchInfo = nullptr;
QSharedPointer<BookInfo> m_searchInfo = QSharedPointer<BookInfo>();
QString m_itemId;
QString m_searchProviderName;
bool m_includeDisabledProviders;

View file

@ -94,7 +94,7 @@ public:
protected:
QSharedPointer<BoxSetInfo> m_searchInfo = nullptr;
QSharedPointer<BoxSetInfo> m_searchInfo = QSharedPointer<BoxSetInfo>();
QString m_itemId;
QString m_searchProviderName;
bool m_includeDisabledProviders;

View file

@ -134,7 +134,7 @@ protected:
QString m_messageCallbackUrl;
bool m_supportsPersistentIdentifier;
bool m_supportsSync;
QSharedPointer<DeviceProfile> m_deviceProfile = nullptr;
QSharedPointer<DeviceProfile> m_deviceProfile = QSharedPointer<DeviceProfile>();
QString m_appStoreUrl;
QString m_iconUrl;
};

View file

@ -170,7 +170,7 @@ protected:
QString m_messageCallbackUrl;
bool m_supportsPersistentIdentifier;
bool m_supportsSync;
QSharedPointer<DeviceProfile> m_deviceProfile = nullptr;
QSharedPointer<DeviceProfile> m_deviceProfile = QSharedPointer<DeviceProfile>();
QString m_appStoreUrl;
QString m_iconUrl;
};

View file

@ -153,7 +153,7 @@ protected:
QString m_appVersion;
QString m_lastUserId;
QDateTime m_dateLastActivity;
QSharedPointer<ClientCapabilities> m_capabilities = nullptr;
QSharedPointer<ClientCapabilities> m_capabilities = QSharedPointer<ClientCapabilities>();
QString m_iconUrl;
};

View file

@ -478,7 +478,7 @@ public:
protected:
QString m_name;
QString m_jellyfinId;
QSharedPointer<DeviceIdentification> m_identification = nullptr;
QSharedPointer<DeviceIdentification> m_identification = QSharedPointer<DeviceIdentification>();
QString m_friendlyName;
QString m_manufacturer;
QString m_manufacturerUrl;

View file

@ -124,7 +124,7 @@ public:
protected:
QString m_guid;
QString m_name;
QSharedPointer<Version> m_version = nullptr;
QSharedPointer<Version> m_version = QSharedPointer<Version>();
QString m_changelog;
QString m_sourceUrl;
QString m_checksum;

View file

@ -134,7 +134,7 @@ protected:
QString m_name;
QString m_description;
QString m_jellyfinId;
QSharedPointer<Version> m_version = nullptr;
QSharedPointer<Version> m_version = QSharedPointer<Version>();
QString m_assemblyFilePath;
bool m_canUninstall;
QString m_dataFolderPath;

View file

@ -68,7 +68,7 @@ public:
protected:
QSharedPointer<MediaSourceInfo> m_mediaSource = nullptr;
QSharedPointer<MediaSourceInfo> m_mediaSource = QSharedPointer<MediaSourceInfo>();
};
} // NS DTO

View file

@ -91,7 +91,7 @@ public:
protected:
QString m_name;
QString m_path;
QSharedPointer<MediaPathInfo> m_pathInfo = nullptr;
QSharedPointer<MediaPathInfo> m_pathInfo = QSharedPointer<MediaPathInfo>();
};
} // NS DTO

View file

@ -94,7 +94,7 @@ public:
protected:
QSharedPointer<MovieInfo> m_searchInfo = nullptr;
QSharedPointer<MovieInfo> m_searchInfo = QSharedPointer<MovieInfo>();
QString m_itemId;
QString m_searchProviderName;
bool m_includeDisabledProviders;

View file

@ -94,7 +94,7 @@ public:
protected:
QSharedPointer<MusicVideoInfo> m_searchInfo = nullptr;
QSharedPointer<MusicVideoInfo> m_searchInfo = QSharedPointer<MusicVideoInfo>();
QString m_itemId;
QString m_searchProviderName;
bool m_includeDisabledProviders;

View file

@ -215,7 +215,7 @@ protected:
QString m_itemId;
std::optional<bool> m_enableDirectPlay = std::nullopt;
std::optional<bool> m_enableDirectStream = std::nullopt;
QSharedPointer<DeviceProfile> m_deviceProfile = nullptr;
QSharedPointer<DeviceProfile> m_deviceProfile = QSharedPointer<DeviceProfile>();
QList<MediaProtocol> m_directPlayProtocols;
};

View file

@ -94,7 +94,7 @@ public:
protected:
QSharedPointer<PersonLookupInfo> m_searchInfo = nullptr;
QSharedPointer<PersonLookupInfo> m_searchInfo = QSharedPointer<PersonLookupInfo>();
QString m_itemId;
QString m_searchProviderName;
bool m_includeDisabledProviders;

View file

@ -231,7 +231,7 @@ protected:
std::optional<qint32> m_maxAudioChannels = std::nullopt;
QString m_mediaSourceId;
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_enableDirectStream = std::nullopt;
std::optional<bool> m_enableTranscoding = std::nullopt;

View file

@ -244,7 +244,7 @@ public:
protected:
bool m_canSeek;
QSharedPointer<BaseItemDto> m_item = nullptr;
QSharedPointer<BaseItemDto> m_item = QSharedPointer<BaseItemDto>();
QString m_itemId;
QString m_sessionId;
QString m_mediaSourceId;

View file

@ -244,7 +244,7 @@ public:
protected:
bool m_canSeek;
QSharedPointer<BaseItemDto> m_item = nullptr;
QSharedPointer<BaseItemDto> m_item = QSharedPointer<BaseItemDto>();
QString m_itemId;
QString m_sessionId;
QString m_mediaSourceId;

View file

@ -166,7 +166,7 @@ public:
protected:
QSharedPointer<BaseItemDto> m_item = nullptr;
QSharedPointer<BaseItemDto> m_item = QSharedPointer<BaseItemDto>();
QString m_itemId;
QString m_sessionId;
QString m_mediaSourceId;

View file

@ -136,7 +136,7 @@ public:
protected:
QString m_name;
QSharedPointer<Version> m_version = nullptr;
QSharedPointer<Version> m_version = QSharedPointer<Version>();
QString m_configurationFileName;
QString m_description;
QString m_jellyfinId;

View file

@ -170,7 +170,7 @@ protected:
QString m_imageUrl;
QString m_searchProviderName;
QString m_overview;
QSharedPointer<RemoteSearchResult> m_albumArtist = nullptr;
QSharedPointer<RemoteSearchResult> m_albumArtist = QSharedPointer<RemoteSearchResult>();
QList<RemoteSearchResult> m_artists;
};

View file

@ -94,7 +94,7 @@ public:
protected:
QSharedPointer<SeriesInfo> m_searchInfo = nullptr;
QSharedPointer<SeriesInfo> m_searchInfo = QSharedPointer<SeriesInfo>();
QString m_itemId;
QString m_searchProviderName;
bool m_includeDisabledProviders;

View file

@ -819,7 +819,7 @@ protected:
qint32 m_logFileRetentionDays;
bool m_isStartupWizardCompleted;
QString m_cachePath;
QSharedPointer<Version> m_previousVersion = nullptr;
QSharedPointer<Version> m_previousVersion = QSharedPointer<Version>();
QString m_previousVersionStr;
bool m_enableUPnP;
bool m_enableMetrics;

View file

@ -300,9 +300,9 @@ public:
protected:
QSharedPointer<PlayerStateInfo> m_playState = nullptr;
QSharedPointer<PlayerStateInfo> m_playState = QSharedPointer<PlayerStateInfo>();
QList<SessionUserInfo> m_additionalUsers;
QSharedPointer<ClientCapabilities> m_capabilities = nullptr;
QSharedPointer<ClientCapabilities> m_capabilities = QSharedPointer<ClientCapabilities>();
QString m_remoteEndPoint;
QStringList m_playableMediaTypes;
QString m_jellyfinId;
@ -313,12 +313,12 @@ protected:
QDateTime m_lastPlaybackCheckIn;
QString m_deviceName;
QString m_deviceType;
QSharedPointer<BaseItemDto> m_nowPlayingItem = nullptr;
QSharedPointer<BaseItem> m_fullNowPlayingItem = nullptr;
QSharedPointer<BaseItemDto> m_nowViewingItem = nullptr;
QSharedPointer<BaseItemDto> m_nowPlayingItem = QSharedPointer<BaseItemDto>();
QSharedPointer<BaseItem> m_fullNowPlayingItem = QSharedPointer<BaseItem>();
QSharedPointer<BaseItemDto> m_nowViewingItem = QSharedPointer<BaseItemDto>();
QString m_deviceId;
QString m_applicationVersion;
QSharedPointer<TranscodingInfo> m_transcodingInfo = nullptr;
QSharedPointer<TranscodingInfo> m_transcodingInfo = QSharedPointer<TranscodingInfo>();
bool m_isActive;
bool m_supportsMediaControl;
bool m_supportsRemoteControl;

View file

@ -168,7 +168,7 @@ protected:
TaskState m_state;
std::optional<double> m_currentProgressPercentage = std::nullopt;
QString m_jellyfinId;
QSharedPointer<TaskResult> m_lastExecutionResult = nullptr;
QSharedPointer<TaskResult> m_lastExecutionResult = QSharedPointer<TaskResult>();
QList<TaskTriggerInfo> m_triggers;
QString m_description;
QString m_category;

View file

@ -362,7 +362,7 @@ protected:
QString m_seriesTimerId;
QString m_externalSeriesTimerId;
std::optional<qint64> m_runTimeTicks = std::nullopt;
QSharedPointer<BaseItemDto> m_programInfo = nullptr;
QSharedPointer<BaseItemDto> m_programInfo = QSharedPointer<BaseItemDto>();
};
} // NS DTO

View file

@ -94,7 +94,7 @@ public:
protected:
QSharedPointer<TrailerInfo> m_searchInfo = nullptr;
QSharedPointer<TrailerInfo> m_searchInfo = QSharedPointer<TrailerInfo>();
QString m_itemId;
QString m_searchProviderName;
bool m_includeDisabledProviders;

View file

@ -79,7 +79,7 @@ public:
protected:
QString m_jellyfinId;
QSharedPointer<LibraryOptions> m_libraryOptions = nullptr;
QSharedPointer<LibraryOptions> m_libraryOptions = QSharedPointer<LibraryOptions>();
};
} // NS DTO

View file

@ -213,8 +213,8 @@ protected:
std::optional<bool> m_enableAutoLogin = std::nullopt;
QDateTime m_lastLoginDate;
QDateTime m_lastActivityDate;
QSharedPointer<UserConfiguration> m_configuration = nullptr;
QSharedPointer<UserPolicy> m_policy = nullptr;
QSharedPointer<UserConfiguration> m_configuration = QSharedPointer<UserConfiguration>();
QSharedPointer<UserPolicy> m_policy = QSharedPointer<UserPolicy>();
std::optional<double> m_primaryImageAspectRatio = std::nullopt;
};

View file

@ -158,7 +158,7 @@ public:
protected:
QString m_version;
QSharedPointer<Version> m_versionNumber = nullptr;
QSharedPointer<Version> m_versionNumber = QSharedPointer<Version>();
QString m_changelog;
QString m_targetAbi;
QString m_sourceUrl;

View file

@ -143,7 +143,7 @@ protected:
QString m_name;
QStringList m_locations;
QString m_collectionType;
QSharedPointer<LibraryOptions> m_libraryOptions = nullptr;
QSharedPointer<LibraryOptions> m_libraryOptions = QSharedPointer<LibraryOptions>();
QString m_itemId;
QString m_primaryImageItemId;
std::optional<double> m_refreshProgress = std::nullopt;

View file

@ -1,39 +1,110 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef JELLYFIN_MODEL_PLAYLIST_H
#define JELLYFIN_MODEL_PLAYLIST_H
#include <QObject>
#include <QSharedPointer>
#include <QString>
#include <QUrl>
#include <QVector>
#include "../viewmodel/itemmodel.h"
#include "item.h"
namespace Jellyfin {
namespace Model {
// Forward declaration
class Item;
class Shuffle;
class Playlist {
/**
* @brief Model of a playlist, a list of items that can be played.
*
* This tries to take the managing what items to play away from the PlaybackManager,
* which now only will be informed about the current and next item to play.
*
* The playlist has actually two list, one named list and the other named queue. When
* playing, the queue has priority over the list and will not be affected by the
* shuffle mode. After all items of the queue are played, the items in the list are played.
* Items in the list may be shuffled.
*/
class Playlist : public QObject {
Q_OBJECT
public:
explicit Playlist();
explicit Playlist(QObject *parent = nullptr);
/// Start loading data for the next item.
void preloadNext();
/// Returns the current item in the queue
QSharedPointer<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:
/// Extra data about each itemId that this playlist manages
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;
};
void reshuffle();
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;
};
}

View 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

View file

@ -19,159 +19,31 @@
#ifndef JELLYFIN_SUPPORT_JSONCONV_H
#define JELLYFIN_SUPPORT_JSONCONV_H
#include <QException>
#include <QtGlobal>
#include <QDateTime>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QList>
#include <QUuid>
#include "jsonconvimpl.h"
#include "parseexception.h"
namespace Jellyfin {
namespace Support {
// Helper functions
QString uuidToString(const QUuid &source);
QUuid stringToUuid(const QString &source);
/**
* @brief Thrown when JSON cannot be parsed.
*/
class ParseException : public QException {
public:
explicit ParseException(const QString &message)
: m_message(message.toStdString()) {}
/*explicit ParseException(const ParseException &other)
: m_message(other.m_message) {}*/
virtual const char *what() const noexcept override;
virtual QException *clone() const override;
virtual void raise() const override;
private:
std::string m_message;
};
// https://www.fluentcpp.com/2017/08/15/function-templates-partial-specialization-cpp/
template <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 int fromJsonValue<int>(const QJsonValue &source, convertType<int>);
extern template qint64 fromJsonValue<qint64>(const QJsonValue &source, convertType<qint64>);
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>);
extern template QJsonObject fromJsonValue<QJsonObject>(const QJsonValue &source, convertType<QJsonObject>);
extern template double fromJsonValue<double>(const QJsonValue &source, convertType<double>);
extern template float fromJsonValue<float>(const QJsonValue &source, convertType<float>);
extern template QDateTime fromJsonValue<QDateTime>(const QJsonValue &source, convertType<QDateTime>);
extern template QVariant fromJsonValue<QVariant>(const QJsonValue &source, convertType<QVariant>);
extern template QUuid fromJsonValue<QUuid>(const QJsonValue &source, convertType<QUuid>);
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 Jellyfin

View 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

View 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

View file

@ -84,14 +84,14 @@ public:
Q_PROPERTY(QStringList productionLocations READ productionLocations NOTIFY productionLocationsChanged)
// Handpicked, important ones
Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks NOTIFY runTimeTicksChanged)
Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks NOTIFY runTimeTicksChanged)*/
Q_PROPERTY(QString overview READ overview NOTIFY overviewChanged)
Q_PROPERTY(int productionYear READ productionYear NOTIFY productionYearChanged)
Q_PROPERTY(int indexNumber READ indexNumber NOTIFY indexNumberChanged)
Q_PROPERTY(int indexNumberEnd READ indexNumberEnd NOTIFY indexNumberEndChanged)
Q_PROPERTY(bool isFolder READ isFolder NOTIFY isFolderChanged)
Q_PROPERTY(QString type READ type NOTIFY typeChanged)
Q_PROPERTY(QString parentBackdropItemId READ parentBackdropItemId NOTIFY parentBackdropItemIdChanged)
/*Q_PROPERTY(QString parentBackdropItemId READ parentBackdropItemId NOTIFY parentBackdropItemIdChanged)
Q_PROPERTY(QStringList parentBackdropImageTags READ parentBackdropImageTags NOTIFY parentBackdropImageTagsChanged)
Q_PROPERTY(UserData *userData READ userData NOTIFY userDataChanged)
Q_PROPERTY(int recursiveItemCount READ recursiveItemCount NOTIFY recursiveItemCountChanged)
@ -125,6 +125,12 @@ public:
int airsBeforeSeasonNumber() const { return m_data->airsBeforeSeasonNumber().value_or(0); }
int airsAfterSeasonNumber() const { return m_data->airsAfterSeasonNumber().value_or(999); }
int airsBeforeEpisodeNumber() const { return m_data->airsBeforeEpisodeNumber().value_or(0); }
QString overview() const { return m_data->overview(); }
int productionYear() const { return m_data->productionYear().value_or(0); }
int indexNumber() const { return m_data->indexNumber().value_or(-1); }
int indexNumberEnd() const { return m_data->indexNumberEnd().value_or(-1); }
bool isFolder() const { return m_data->isFolder().value_or(false); }
QString type() const { return m_data->type(); }
QSharedPointer<Model::Item> data() const { return m_data; }
void setData(QSharedPointer<Model::Item> newData);
@ -192,7 +198,7 @@ public:
Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
QString itemId() const { return m_parameters.itemId(); }
void setItemId(QString newItemId) { m_parameters.setItemId(newItemId); emit itemIdChanged(newItemId); }
void setItemId(QString newItemId);
virtual bool canReload() const override;
signals:

View file

@ -163,6 +163,10 @@ public:
// Hand-picked, important ones
imageTags,
imageBlurHashes,
mediaType,
type,
collectionType,
jellyfinExtendModelAfterHere = Qt::UserRole + 300 // Should be enough for now
};
@ -183,9 +187,14 @@ public:
JFRN(extraType),
// Handpicked, important ones
JFRN(imageTags),
JFRN(imageBlurHashes),
JFRN(mediaType),
JFRN(type),
JFRN(collectionType),
};
}
QVariant data(const QModelIndex &index, int role) const override;
QSharedPointer<Model::Item> itemAt(int index);
};
/*class UserItemModel : public ItemModel {

View file

@ -110,15 +110,19 @@ protected:
void setError(QNetworkReply::NetworkError error);
void setErrorString(const QString &newErrorString);
void reloadIfNeeded() {
if (canReload()) {
reload();
}
}
void classBegin() override {
m_isParsing = true;
}
void componentComplete() override {
m_isParsing = false;
if (canReload()) {
reload();
}
reloadIfNeeded();
}
ApiClient *m_apiClient = nullptr;
protected:
@ -157,7 +161,7 @@ public:
}
T *dataViewModel() const { return m_dataViewModel; }
QObject *data() const { return m_dataViewModel; }
QObject *data() const override { return m_dataViewModel; }
void reload() override {
if (m_futureWatcher->isRunning()) return;

View file

@ -34,9 +34,13 @@
#include <functional>
#include "../dto/baseitemdto.h"
#include "../dto/playbackinfodto.h"
#include "../dto/playmethod.h"
#include "../loader/requesttypes.h"
#include "../loader/http/getpostedplaybackinfo.h"
#include "../model/playlist.h"
#include "../support/jsonconv.h"
#include "../viewmodel/item.h"
#include "../apiclient.h"
#include "itemmodel.h"
@ -50,6 +54,9 @@ class RemoteItem;
namespace ViewModel {
// Later defined in this file
class ItemUrlFetcherThread;
/**
* @brief The PlaybackManager class manages the playback of Jellyfin items. It fetches streams based on Jellyfin items, posts
* the current playback state to the Jellyfin Server, contains the actual media player and so on.
@ -58,15 +65,10 @@ namespace ViewModel {
* preloading the next item in the queue. The current media player is pointed to by m_mediaPlayer.
*/
class PlaybackManager : public QObject, public QQmlParserStatus {
friend class ItemUrlFetcherThread;
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
public:
enum PlayMethod {
Transcode,
Stream,
DirectPlay
};
Q_ENUM(PlayMethod)
using FetchCallback = std::function<void(QUrl &&, PlayMethod)>;
explicit PlaybackManager(QObject *parent = nullptr);
@ -77,11 +79,11 @@ public:
Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged)
Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
Q_PROPERTY(bool resumePlayback MEMBER m_resumePlayback NOTIFY resumePlaybackChanged)
Q_PROPERTY(PlayMethod playMethod READ playMethod NOTIFY playMethodChanged)
Q_PROPERTY(Jellyfin::DTO::PlayMethodClass::Value playMethod READ playMethod NOTIFY playMethodChanged)
// Current Item and queue informatoion
Q_PROPERTY(ViewModel::Item *item READ item NOTIFY itemChanged)
Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged)
Q_PROPERTY(QObject *item READ item NOTIFY itemChanged)
// Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged)
Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged)
// Current media player related property getters
@ -102,7 +104,7 @@ public:
QObject *mediaObject() const { return m_mediaPlayer; }
qint64 position() const { return m_mediaPlayer->position(); }
qint64 duration() const { return m_mediaPlayer->duration(); }
ItemModel *queue() const { return m_queue; }
//ItemModel *queue() const { return m_queue; }
int queueIndex() const { return m_queueIndex; }
// Current media player related property getters
@ -112,7 +114,7 @@ public:
QMediaPlayer::Error error () const { return m_mediaPlayer->error(); }
QString errorString() const { return m_mediaPlayer->errorString(); }
signals:
void itemChanged(BaseItemDto *newItemId);
void itemChanged(ViewModel::Item *newItemId);
void streamUrlChanged(const QString &newStreamUrl);
void autoOpenChanged(bool autoOpen);
void audioIndexChanged(int audioIndex);
@ -125,7 +127,7 @@ signals:
void mediaObjectChanged(QObject *newMediaObject);
void positionChanged(qint64 newPosition);
void durationChanged(qint64 newDuration);
void queueChanged(ItemModel *newQue);
//void queueChanged(ItemModel *newQue);
void queueIndexChanged(int newIndex);
void playbackStateChanged(QMediaPlayer::State newState);
void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus);
@ -138,9 +140,9 @@ public slots:
*
* This will construct the Jellyfin::Item internally
* and delete it later.
* @param itemId The id of the item to play.
* @param item The item to play.
*/
void playItem(const QString &itemId);
void playItem(Item *item);
void playItemInList(ItemModel *itemList, int index);
void play() { m_mediaPlayer->play(); }
void pause() { m_mediaPlayer->pause(); }
@ -162,24 +164,43 @@ private slots:
void mediaPlayerPositionChanged(qint64 position);
void mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus);
void mediaPlayerError(QMediaPlayer::Error error);
void mediaPlayerDurationChanged(qint64 newDuration);
/**
* @brief updatePlaybackInfo Updates the Jellyfin server with the current playback progress etc.
*/
void updatePlaybackInfo();
/// Called when the fetcherThread has fetched the playback URL and playSession
void onItemExtraDataReceived(const QString &itemId, const QUrl &url, const QString &playSession,
// Fully specify class to please MOC
Jellyfin::DTO::PlayMethodClass::Value playMethod);
/// Called when the fetcherThread encountered an error
void onItemErrorReceived(const QString &itemId, const QString &errorString);
void onDestroyed();
private:
/// Factor to multiply with when converting from milliseconds to ticks.
const static int MS_TICK_FACTOR = 10000;
enum PlaybackInfoType { Started, Stopped, Progress };
/// Timer used to update the play progress on the Jellyfin server
QTimer m_updateTimer;
/// Timer used to notify ourselves when we need to preload the next item
QTimer m_preloadTimer;
ApiClient *m_apiClient = nullptr;
/// The currently playing item
QSharedPointer<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);
// Properties for making the streaming request.
QString m_streamUrl;
QString m_nextStreamUrl;
QString m_playSessionId;
QString m_nextPlaySessionId;
/// The index of the mediastreams of the to-be-played item containing the audio
int m_audioIndex = 0;
/// The index of the mediastreams of the to-be-played item containing subtitles
@ -196,10 +217,11 @@ private:
*/
bool m_autoOpen = false;
// Playback-related members
ItemUrlFetcherThread *m_urlFetcherThread;
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
PlayMethod m_playMethod = Transcode;
PlayMethod m_playMethod = PlayMethod::Transcode;
QMediaPlayer::State m_playbackState = QMediaPlayer::StoppedState;
/// Pointer to the current media player.
QMediaPlayer *m_mediaPlayer = nullptr;
@ -211,28 +233,18 @@ private:
QMediaPlayer *m_mediaPlayer1;
/// Media player 2
QMediaPlayer *m_mediaPlayer2;
ItemModel *m_queue = nullptr;
Model::Playlist *m_queue = nullptr;
int m_queueIndex = 0;
bool m_resumePlayback = true;
// Helper methods
void setItem(ViewModel::Item *newItem);
void setItem(QSharedPointer<Model::Item> newItem);
void swapMediaPlayer();
/**
* @brief Retrieves the URL of the stream to open.
*/
void fetchStreamUrl(const Model::Item *item, bool autoOpen, const FetchCallback &callback);
void fetchAndSetStreamUrl(const Model::Item *item);
void setStreamUrl(const QString &streamUrl);
void setStreamUrl(const QUrl &streamUrl);
void setPlaybackState(QMediaPlayer::State newState);
Model::Item *nextItem();
void setQueue(ItemModel *itemModel);
/**
* @brief Posts the playback information
*/
@ -243,6 +255,60 @@ private:
void classBegin() override { m_qmlIsParsingComponent = true; }
void componentComplete() override;
bool m_qmlIsParsingComponent = false;
/// Time in ms at what moment this playbackmanager should start loading the next item.
const qint64 PRELOAD_DURATION = 15 * 1000;
};
/// Thread that fetches the Item's stream URL always in the given order they were requested
class ItemUrlFetcherThread : public QThread {
Q_OBJECT
public:
ItemUrlFetcherThread(PlaybackManager *manager);
/**
* @brief Adds an item to the queue of items that should be requested
* @param item The item to fetch the URL of
*/
void addItemToQueue(QSharedPointer<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

View file

@ -29,109 +29,30 @@
#include <QWaitCondition>
#include <QtMultimedia/QMediaPlaylist>
#include "../dto/playbackinfodto.h"
#include "../loader/requesttypes.h"
#include "../loader/http/getpostedplaybackinfo.h"
#include "../apiclient.h"
#include "itemmodel.h"
namespace Jellyfin {
namespace ViewModel {
class ItemUrlFetcherThread;
/**
* @brief Playlist/queue that can be exposed to the UI. It also containts the playlist-related logic,
* which is mostly relevant
*/
class Playlist : public ItemModel {
/*class Playlist : public ItemModel {
Q_OBJECT
friend class ItemUrlFetcherThread;
public:
explicit Playlist(ApiClient *apiClient, QObject *parent = nullptr);
enum ExtraRoles {
Url = ItemModel::RoleNames::jellyfinExtendModelAfterHere + 1,
PlaySession,
ErrorText
};
QHash<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:
void onItemsAdded(const QModelIndex &parent, int startIndex, int endIndex);
void onItemsMoved(const QModelIndex &parent, int startIndex, int endIndex, const QModelIndex &destination, int destinationRow);
void onItemsRemoved(const QModelIndex &parent, int startIndex, int endIndex);
void onItemsReset();
/// Called when the fetcherThread has fetched the playback URL and playSession
void onItemExtraDataReceived(const QString &itemId, const QUrl &url, const QString &playSession);
/// Called when the fetcherThread encountered an error
void onItemErrorReceived(const QString &itemId, const QString &errorString);
private:
/// Map from ItemId to ExtraData
QHash<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 Jellyfin

View file

@ -944,7 +944,7 @@ public:
string typeNameWithQualifiers() {
if (needsPointer) {
return "QSharedPointer<" ~ typeName~ ">";
return "QSharedPointer<" ~ typeName ~ ">";
}
if (needsOptional) {
return "std::optional<" ~ typeName ~ ">";
@ -977,7 +977,7 @@ public:
}
string defaultInitializer() {
if (needsPointer) return "nullptr";
if (needsPointer) return "QSharedPointer<" ~ typeName ~ ">()";
if (needsOptional) return "std::nullopt";
return "";
}

View file

@ -31,10 +31,10 @@ ApiClient::ApiClient(QObject *parent)
m_eventbus(new EventBus(this)),
m_deviceName(QHostInfo::localHostName()) {
m_deviceId = Support::uuidToString(retrieveDeviceId());
m_deviceId = Support::toString(retrieveDeviceId());
m_credManager = CredentialsManager::newInstance(this);
connect(m_credManager, &CredentialsManager::serversListed, this, &ApiClient::credManagerServersListed);
connect(m_credManager, &CredentialsManager::usersListed, this, &ApiClient::credManagerUsersListed);
generateDeviceProfile();
}
@ -92,49 +92,48 @@ QNetworkReply *ApiClient::post(const QString &path, const QJsonDocument &data, c
////////////////////////////////////////////////////////////////////////////////////////////////////
void ApiClient::restoreSavedSession(){
QObject *ctx1 = new QObject(this);
connect(m_credManager, &CredentialsManager::serversListed, ctx1, [this, ctx1](const QStringList &servers) {
qDebug() << "Servers listed: " << servers;
if (servers.size() == 0) {
emit this->setupRequired();
return;
}
//FIXME: support multiple servers
QString server = servers[0];
this->m_baseUrl = server;
qDebug() << "Server: " << server;
QObject *ctx2 = new QObject(this);
connect(m_credManager, &CredentialsManager::usersListed, ctx2, [this, server, ctx2](const QStringList &users) {
if (users.size() == 0) {
emit this->setupRequired();
return;
}
//FIXME: support multiple users
QString user = users[0];
qDebug() << "User: " << user;
QObject *ctx3 = new QObject(this);
connect(m_credManager, &CredentialsManager::tokenRetrieved, ctx3, [this, ctx3]
(const QString &server, const QString &user, const QString &token) {
Q_UNUSED(server)
this->m_token = token;
this->setUserId(user);
this->setAuthenticated(true);
this->postCapabilities();
disconnect(ctx3);
}, Qt::UniqueConnection);
m_credManager->get(server, user);
delete ctx2;
}, Qt::UniqueConnection);
m_credManager->listUsers(server);
qDebug() << "Listing users";
delete ctx1;
}, Qt::UniqueConnection);
qDebug() << "Listing servers";
m_credManager->listServers();
}
void ApiClient::credManagerServersListed(QStringList servers) {
qDebug() << "Servers listed: " << servers;
if (servers.size() == 0) {
emit this->setupRequired();
return;
}
//FIXME: support multiple servers
QString server = servers[0];
this->m_baseUrl = server;
qDebug() << "Chosen server: " << server;
m_credManager->listUsers(server);
}
void ApiClient::credManagerUsersListed(const QString &server, QStringList users) {
if (users.size() == 0) {
emit this->setupRequired();
return;
}
//FIXME: support multiple users
QString user = users[0];
qDebug() << "Chosen user: " << user;
QObject *ctx3 = new QObject(this);
connect(m_credManager, &CredentialsManager::tokenRetrieved, ctx3, [this, ctx3]
(const QString &server, const QString &user, const QString &token) {
Q_UNUSED(server)
this->m_token = token;
this->setUserId(user);
this->setAuthenticated(true);
this->postCapabilities();
disconnect(ctx3);
}, Qt::UniqueConnection);
m_credManager->get(server, user);
}
void ApiClient::credManagerTokenRetrieved(const QString &server, const QString &user, const QString &token) {
}
void ApiClient::setupConnection() {
// First detect redirects:
// Note that this is done without calling JellyfinApiClient::get since that automatically includes the base_url,

View file

@ -16,7 +16,7 @@ You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#define JELLYFIN_APIMODEL_CPP
#include "JellyfinQt/apimodel.h"
#include "JellyfinQt/dto/baseitemdto.h"

View file

@ -87,5 +87,5 @@ void FallbackCredentialsManager::listUsers(const QString &server) {
qDebug() << "Users: " << users;
m_settings.endGroup();
m_settings.endGroup();
emit CredentialsManager::usersListed(users);
emit CredentialsManager::usersListed(server, users);
}

View file

@ -40,8 +40,10 @@ void registerTypes(const char *uri) {
qmlRegisterType<ViewModel::UserViewsLoader>(uri, 1, 0, "UsersViewsLoader");
// Enumerations
qmlRegisterUncreatableType<DTO::GeneralCommandTypeClass>(uri, 1, 0, "GeneralCommandType", "Is an enum");
qmlRegisterUncreatableType<ViewModel::ModelStatusClass>(uri, 1, 0, "ModelStatus", "Is an enum");
qmlRegisterUncreatableType<Jellyfin::DTO::GeneralCommandTypeClass>(uri, 1, 0, "GeneralCommandType", "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>();
}
}

View file

@ -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
View 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

View file

@ -16,25 +16,14 @@
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "JellyfinQt/support/jsonconv.h"
#include "JellyfinQt/support/jsonconvimpl.h"
#include "JellyfinQt/support/parseexception.h"
#include <QDebug>
namespace Jellyfin {
namespace Support {
const char * ParseException::what() const noexcept {
return m_message.c_str();
}
QException *ParseException::clone() const {
return new ParseException(*this);
}
void ParseException::raise() const {
throw *this;
}
QString uuidToString(const QUuid &source) {
QString str = source.toString();
// Convert {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} (length: 38)
@ -195,7 +184,8 @@ QDateTime fromJsonValue<QDateTime>(const QJsonValue &source, convertType<QDateTi
case QJsonValue::Null:
return QDateTime();
case QJsonValue::String:
return QDateTime::fromString(source.toString(), Qt::ISODateWithMs);
// 2005-02-21T00:00:00.0000000Z
return QDateTime::fromString(source.toString(), Qt::ISODate);
default:
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 <>
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

View 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

View file

@ -27,7 +27,6 @@ Item::Item(QObject *parent, QSharedPointer<Model::Item> data)
}
void Item::setData(QSharedPointer<Model::Item> newData) {
Model::Item oldData = *m_data.data();
m_data = newData;
}
@ -44,21 +43,32 @@ void ItemLoader::onApiClientChanged(ApiClient *newApiClient) {
disconnect(m_apiClient, &ApiClient::userIdChanged, this, &ItemLoader::setUserId);
}
if (newApiClient != nullptr) {
m_parameters.setUserId(newApiClient->userId());
connect(newApiClient, &ApiClient::userIdChanged, this, &ItemLoader::setUserId);
setUserId(newApiClient->userId());
}
}
void ItemLoader::setUserId(const QString &newUserId) {
m_parameters.setUserId(newUserId);
qDebug() << "ItemLoader: userId set to " << m_apiClient->userId();
m_parameters.setUserId(m_apiClient->userId());
qDebug() << "New userId: " << m_parameters.userId();
reloadIfNeeded();
}
bool ItemLoader::canReload() const {
qDebug() << "ItemLoader::canReload(): baseClass=" << BaseClass::canReload() << ", itemId=" << m_parameters.userId() << ", userId=" << m_parameters.userId();
return BaseClass::canReload()
&& !m_parameters.itemId().isEmpty()
&& !m_parameters.userId().isEmpty();
}
void ItemLoader::setItemId(QString newItemId) {
qDebug() << "ItemLoader: itemId set to " << newItemId;
m_parameters.setItemId(newItemId);
qDebug() << "New itemId: " << m_parameters.itemId();
emit itemIdChanged(newItemId);
reloadIfNeeded();
}
}
}

View file

@ -24,7 +24,7 @@
#define JF_CASE(roleName) case roleName: \
try { \
return QVariant(item.roleName()); \
} catch(std::bad_optional_access e) { \
} catch(std::bad_optional_access &e) { \
return QVariant(); \
}
@ -62,12 +62,20 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const {
JF_CASE(extraType)
// Handpicked, important ones
JF_CASE(imageTags)
JF_CASE(imageBlurHashes)
JF_CASE(mediaType)
JF_CASE(type)
JF_CASE(collectionType)
default:
return QVariant();
}
}
QSharedPointer<Model::Item> ItemModel::itemAt(int index) {
return QSharedPointer<Model::Item>::create(m_array[index]);
}
} // NS ViewModel
} // NS Jellyfin

View file

@ -23,6 +23,7 @@
// #include "JellyfinQt/DTO/dto.h"
#include <JellyfinQt/dto/useritemdatadto.h>
#include <utility>
namespace Jellyfin {
class ItemModel;
@ -37,125 +38,67 @@ PlaybackManager::PlaybackManager(QObject *parent)
: QObject(parent),
m_item(nullptr),
m_mediaPlayer1(new QMediaPlayer(this)),
m_mediaPlayer2(new QMediaPlayer(this)) {
m_mediaPlayer2(new QMediaPlayer(this)),
m_urlFetcherThread(new ItemUrlFetcherThread(this)),
m_queue(new Model::Playlist(this)) {
// Set up connections.
swapMediaPlayer();
m_updateTimer.setInterval(10000); // 10 seconds
m_updateTimer.setSingleShot(false);
m_preloadTimer.setSingleShot(true);
connect(this, &QObject::destroyed, this, &PlaybackManager::onDestroyed);
connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo);
connect(m_urlFetcherThread, &ItemUrlFetcherThread::itemUrlFetched, this, &PlaybackManager::onItemExtraDataReceived);
m_urlFetcherThread->start();
}
void PlaybackManager::onDestroyed() {
m_urlFetcherThread->cleanlyStop();
}
void PlaybackManager::setApiClient(ApiClient *apiClient) {
m_item->setApiClient(apiClient);
}
void PlaybackManager::fetchStreamUrl(const Model::Item *item, bool autoOpen, const FetchCallback &callback) {
if (item == nullptr || m_apiClient == nullptr) {
qDebug() << "Item or apiClient not set";
return;
if (!m_item.isNull()) {
m_item->setApiClient(apiClient);
}
QString itemId(item->jellyfinId());
m_resumePosition = 0;
if (m_resumePlayback && !item->userData().isNull()) {
QSharedPointer<UserData> userData = m_item->userData();
if (!userData.isNull()) {
m_resumePosition = userData->playbackPositionTicks();
}
}
QUrlQuery params;
params.addQueryItem("UserId", m_apiClient->userId());
params.addQueryItem("StartTimeTicks", QString::number(m_resumePosition));
params.addQueryItem("IsPlayback", "true");
params.addQueryItem("AutoOpenLiveStream", autoOpen? "true" : "false");
params.addQueryItem("MediaSourceId", itemId);
params.addQueryItem("SubtitleStreamIndex", QString::number(m_subtitleIndex));
params.addQueryItem("AudioStreamIndex", QString::number(m_audioIndex));
QJsonObject root;
root["DeviceProfile"] = m_apiClient->playbackDeviceProfile();
QNetworkReply *rep = m_apiClient->post("/Items/" + itemId + "/PlaybackInfo", QJsonDocument(root), params);
connect(rep, &QNetworkReply::finished, this, [this, rep, callback, itemId]() {
QJsonObject root = QJsonDocument::fromJson(rep->readAll()).object();
this->m_playSessionId = root["PlaySessionId"].toString();
qDebug() << "Session id: " << this->m_playSessionId;
if (this->m_autoOpen) {
QJsonArray mediaSources = root["MediaSources"].toArray();
QJsonObject firstMediaSource = mediaSources[0].toObject();
//FIXME: relies on the fact that the returned transcode url always has at least one result!
if (firstMediaSource.isEmpty()) {
qWarning() << "No media source found";
} else if (firstMediaSource["SupportsDirectStream"].toBool()) {
QUrlQuery query;
query.addQueryItem("mediaSourceId", firstMediaSource["Id"].toString());
query.addQueryItem("deviceId", m_apiClient->deviceId());
query.addQueryItem("api_key", m_apiClient->token());
query.addQueryItem("Static", "True");
QString mediaType = "unknown";
if (m_item->mediaType() == "Audio") {
mediaType = "Audio";
} else if (m_item->mediaType() == "Video") {
mediaType = "Videos";
}
QString streamUrl = this->m_apiClient->baseUrl() + "/" + mediaType + "/" + itemId + "/stream."
+ firstMediaSource["Container"].toString() + "?" + query.toString(QUrl::EncodeReserved);
callback(QUrl(streamUrl), DirectPlay);
} else if (firstMediaSource["SupportsTranscoding"].toBool() && !firstMediaSource["TranscodingUrl"].isNull()) {
QString streamUrl = this->m_apiClient->baseUrl()
+ firstMediaSource["TranscodingUrl"].toString();
this->m_playMethod = Transcode;
callback(QUrl(streamUrl), Transcode);
} else {
qDebug() << "No stream url found";
return;
}
}
rep->deleteLater();
});
m_apiClient = apiClient;
}
void PlaybackManager::fetchAndSetStreamUrl(const Model::Item *item) {
fetchStreamUrl(item, m_autoOpen, [this, item](QUrl &&url, PlayMethod playbackMethod) {
if (m_item == item) {
setStreamUrl(url.toString());
m_playMethod = playbackMethod;
emit playMethodChanged(m_playMethod);
m_mediaPlayer->setMedia(QMediaContent(url));
m_mediaPlayer->play();
}
});
}
void PlaybackManager::setItem(ViewModel::Item *newItem) {
void PlaybackManager::setItem(QSharedPointer<Model::Item> newItem) {
if (m_mediaPlayer != nullptr) m_mediaPlayer->stop();
bool shouldFetchStreamUrl = !newItem.isNull()
&& ((m_streamUrl.isEmpty() || (!m_item.isNull()
&& m_item->jellyfinId() != newItem->jellyfinId()))
|| (m_nextStreamUrl.isEmpty() || (!m_nextItem.isNull()
&& m_nextItem->jellyfinId() != newItem->jellyfinId())));
this->m_item = newItem;
if (newItem != nullptr) {
this->m_item = newItem->data();
if (newItem.isNull()) {
m_displayItem->setData(QSharedPointer<Model::Item>::create());
} else {
m_displayItem->setData(newItem);
}
//emit itemChanged(newItem);
emit itemChanged(m_displayItem);
if (m_apiClient == nullptr) {
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
return;
}
// Deinitialize the streamUrl
setStreamUrl("");
if (newItem != nullptr) {
fetchAndSetStreamUrl(m_item.data());
if (shouldFetchStreamUrl) {
setStreamUrl(QUrl());
m_urlFetcherThread->addItemToQueue(m_item);
}
}
void PlaybackManager::setStreamUrl(const QString &streamUrl) {
this->m_streamUrl = streamUrl;
void PlaybackManager::setStreamUrl(const QUrl &streamUrl) {
m_streamUrl = streamUrl.toString();
// Inspired by PHP naming schemes
QUrl realStreamUrl(streamUrl);
Q_ASSERT_X(realStreamUrl.isValid(), "setStreamUrl", "StreamURL Jellyfin returned is not valid");
emit streamUrlChanged(streamUrl);
Q_ASSERT_X(streamUrl.isValid() || streamUrl.isEmpty(), "setStreamUrl", "StreamURL Jellyfin returned is not valid");
emit streamUrlChanged(m_streamUrl);
}
void PlaybackManager::setPlaybackState(QMediaPlayer::State newState) {
@ -203,6 +146,16 @@ void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus ne
qDebug() << "Resuming playback by seeking to " << (m_resumePosition / MS_TICK_FACTOR);
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
}
} else if (newStatus == QMediaPlayer::EndOfMedia) {
next();
}
}
void PlaybackManager::mediaPlayerDurationChanged(qint64 newDuration) {
emit durationChanged(newDuration);
if (newDuration > 0 && !m_nextItem.isNull()) {
m_preloadTimer.stop();
m_preloadTimer.start(std::max(static_cast<int>(newDuration - PRELOAD_DURATION), 0));
}
}
@ -215,46 +168,47 @@ void PlaybackManager::updatePlaybackInfo() {
postPlaybackInfo(Progress);
}
void PlaybackManager::playItem(const QString &itemId) {
Q_UNUSED(itemId)
Q_UNIMPLEMENTED();
/*RemoteItem *newItem = new RemoteItem(itemId, m_apiClient, this);
ItemModel *queue = new UserItemModel(this);
setQueue(queue);
QString parentId = newItem->data()->parentId();
queue->setParentId(parentId);
queue->setLimit(10000);
queue->setApiClient(m_apiClient);
queue->reload();
setItem(newItem->data());
connect(queue, &BaseApiModel::ready, this, [this, queue, newItem]() {
for (int i = 0; i < queue->size(); i++) {
if (queue->at(i)->jellyfinId() == newItem->jellyfinId()) {
m_queueIndex = i;
emit queueIndexChanged(m_queueIndex);
break;
}
}
});*/
setPlaybackState(QMediaPlayer::PlayingState);
void PlaybackManager::playItem(Item *item) {
setItem(item->data());
}
void PlaybackManager::playItemInList(ItemModel *playlist, int itemIdx) {
playlist->setParent(this);
setQueue(playlist);
m_queueIndex = itemIdx;
void PlaybackManager::playItemInList(ItemModel *playlist, int index) {
m_queue->clearList();
m_queue->appendToList(*playlist);
m_queue->play(index);
m_queueIndex = index;
emit queueIndexChanged(m_queueIndex);
//setItem(playlist->at(itemIdx));
setItem(playlist->itemAt(index));
}
void PlaybackManager::next() {
Q_UNIMPLEMENTED();
m_mediaPlayer->stop();
m_mediaPlayer->setMedia(QMediaContent());
swapMediaPlayer();
if (m_nextItem.isNull()) {
setItem(m_queue->nextItem());
m_queue->next();
m_nextItem.clear();
} else {
m_item = m_nextItem;
setItem(m_nextItem);
}
m_mediaPlayer->play();
}
void PlaybackManager::previous() {
Q_UNIMPLEMENTED();
}
m_mediaPlayer->stop();
m_mediaPlayer->setPosition(0);
m_nextStreamUrl = m_streamUrl;
m_streamUrl = QString();
m_nextItem = m_item;
swapMediaPlayer();
m_queue->previous();
setItem(m_queue->currentItem());
m_mediaPlayer->play();
}
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
QJsonObject root;
@ -263,7 +217,7 @@ void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
qWarning() << "Item is null. Not posting playback info";
return;
}
root["ItemId"] = Support::uuidToString(m_item->jellyfinId());
root["ItemId"] = Support::toString(m_item->jellyfinId());
root["SessionId"] = m_playSessionId;
switch(type) {
@ -308,11 +262,11 @@ void PlaybackManager::swapMediaPlayer() {
if (m_mediaPlayer != nullptr) {
disconnect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::durationChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::mediaPlayerDurationChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
// I do not like the complicated overload cast
disconnect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::error)), this, SLOT(mediaPlayerError(QmediaPlayer::error)));
disconnect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)), this, SLOT(mediaPlayerError(QMediaPlayer::Error)));
}
if (m_mediaPlayer == m_mediaPlayer1) {
m_mediaPlayer = m_mediaPlayer2;
@ -323,41 +277,154 @@ void PlaybackManager::swapMediaPlayer() {
}
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::durationChanged);
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::mediaPlayerDurationChanged);
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
// I do not like the complicated overload cast
connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::error)), this, SLOT(mediaPlayerError(QmediaPlayer::error)));
}
Model::Item *PlaybackManager::nextItem() {
if (m_queue == nullptr) return nullptr;
// TODO: shuffle etc.
if (m_queueIndex < m_queue->size()) {
//return m_queue->at(m_queueIndex + 1);
}
return nullptr;
}
void PlaybackManager::setQueue(ItemModel *model) {
if (m_queue != nullptr) {
if (QQmlEngine::objectOwnership(m_queue) == QQmlEngine::CppOwnership) {
m_queue->deleteLater();
} else {
m_queue->setParent(nullptr);
}
}
m_queue = model;
emit queueChanged(m_queue);
connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)), this, SLOT(mediaPlayerError(QMediaPlayer::Error)));
}
void PlaybackManager::componentComplete() {
if (m_apiClient == nullptr) qWarning() << "No ApiClient set for PlaybackManager";
m_qmlIsParsingComponent = false;
if (!m_item.isNull()) {
fetchAndSetStreamUrl(m_item.data());
}
// ItemUrlFetcherThread
ItemUrlFetcherThread::ItemUrlFetcherThread(PlaybackManager *manager) :
QThread(manager),
m_parent(manager),
m_loader(new Jellyfin::Loader::HTTP::GetPostedPlaybackInfoLoader(manager->m_apiClient)) {
connect(this, &ItemUrlFetcherThread::prepareLoaderRequested, this, &ItemUrlFetcherThread::onPrepareLoader);
}
void ItemUrlFetcherThread::addItemToQueue(QSharedPointer<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 Jellyfin

View file

@ -21,173 +21,15 @@
namespace Jellyfin {
namespace ViewModel {
Playlist::Playlist(ApiClient *apiClient, QObject *parent)
: ItemModel(parent), m_apiClient(apiClient), m_fetcherThread(new ItemUrlFetcherThread(this)){
/*Playlist::Playlist(ApiClient *apiClient, QObject *parent)
: ItemModel(parent) {
connect(this, &QAbstractListModel::rowsInserted, this, &Playlist::onItemsAdded);
connect(this, &QAbstractListModel::rowsRemoved, this, &Playlist::onItemsRemoved);
connect(this, &QAbstractListModel::rowsMoved, this, &Playlist::onItemsMoved);
connect(this, &QAbstractListModel::modelReset, this, &Playlist::onItemsReset);
connect(m_fetcherThread, &ItemUrlFetcherThread::itemUrlFetched, this, &Playlist::onItemExtraDataReceived);
}
void Playlist::onItemsAdded(const QModelIndex &parent, int startIndex, int endIndex) {
if (parent.isValid()) return;
// Retrieve added items.
for (int i = startIndex; i <= endIndex; i++) {
m_fetcherThread->addItemToQueue(at(i));
}
}
void Playlist::onItemsMoved(const QModelIndex &parent, int startIndex, int endIndex, const QModelIndex &destination, int index) {
if (parent.isValid()) return;
if (destination.isValid()) return;
}
void Playlist::onItemsRemoved(const QModelIndex &parent, int startIndex, int endIndex) {
if (parent.isValid()) return;
QSet<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 Jellyfin

View file

@ -66,6 +66,7 @@ void WebSocket::onDisconnected() {
}
void WebSocket::textMessageReceived(const QString &message) {
qDebug() << "WebSocket: message received: " << message;
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
if (doc.isNull() || !doc.isObject()) {
qWarning() << "Malformed message received over WebSocket: parse error or root not an object.";