mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2025-09-05 10:12:46 +00:00
WIP: Add playlists/queues and add support for Sailfish back
This commit is contained in:
parent
fbc154fb56
commit
86672be051
89 changed files with 1637 additions and 849 deletions
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
* 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
|
||||
|
|
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) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue