1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2025-09-05 10:12:46 +00:00

WIP: Playlist support

This commit is contained in:
Chris Josten 2021-05-21 15:46:30 +02:00
parent 228f81984b
commit fbc154fb56
16 changed files with 481 additions and 27 deletions

View file

@ -24,6 +24,7 @@ void registerTypes(const char *uri) {
qmlRegisterType<ServerDiscoveryModel>(uri, 1, 0, "ServerDiscoveryModel");
qmlRegisterType<ViewModel::PlaybackManager>(uri, 1, 0, "PlaybackManager");
qmlRegisterUncreatableType<ViewModel::Item>(uri, 1, 0, "Item", "Acquire one via ItemLoader or exposed properties");
qmlRegisterUncreatableType<EventBus>(uri, 1, 0, "EventBus", "Obtain one via your ApiClient");
qmlRegisterUncreatableType<WebSocket>(uri, 1, 0, "WebSocket", "Obtain one via your ApiClient");
// AbstractItemModels

View file

View file

@ -0,0 +1,193 @@
/*
* 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/viewmodel/playlist.h"
namespace Jellyfin {
namespace ViewModel {
Playlist::Playlist(ApiClient *apiClient, QObject *parent)
: ItemModel(parent), m_apiClient(apiClient), m_fetcherThread(new ItemUrlFetcherThread(this)){
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

@ -19,6 +19,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include "JellyfinQt/websocket.h"
#include <JellyfinQt/dto/generalcommand.h>
#include <JellyfinQt/dto/generalcommandtype.h>
#include <JellyfinQt/dto/useritemdatadto.h>
namespace Jellyfin {
@ -70,15 +72,21 @@ void WebSocket::textMessageReceived(const QString &message) {
return;
}
QJsonObject messageRoot = doc.object();
if (!messageRoot.contains("MessageType") || !messageRoot.contains("Data")) {
qWarning() << "Malformed message received over WebSocket: no MessageType and Data set.";
if (!messageRoot.contains("MessageType")) {
qWarning() << "Malformed message received over WebSocket: no MessageType set.";
return;
}
// Convert the type so we can use it in our enums.
QString messageTypeStr = messageRoot["MessageType"].toString();
QJsonValue data = messageRoot["Data"];
if (messageTypeStr == QStringLiteral("ForceKeepAlive")) {
setupKeepAlive(data.toInt());
} else {
qDebug() << messageTypeStr;
}
bool ok;
MessageType messageType = static_cast<MessageType>(QMetaEnum::fromType<WebSocket::MessageType>().keyToValue(messageTypeStr.toLatin1(), &ok));
/*MessageType messageType = static_cast<MessageType>(QMetaEnum::fromType<WebSocket::MessageType>().keyToValue(messageTypeStr.toLatin1(), &ok));
if (!ok) {
qWarning() << "Unknown message arrived: " << messageTypeStr;
if (messageRoot.contains("Data")) {
@ -87,7 +95,6 @@ void WebSocket::textMessageReceived(const QString &message) {
return;
}
QJsonValue data = messageRoot["Data"];
qDebug() << "Received message: " << messageTypeStr;
switch (messageType) {
@ -111,7 +118,7 @@ void WebSocket::textMessageReceived(const QString &message) {
}
break;
}
}*/
}