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:
parent
228f81984b
commit
fbc154fb56
16 changed files with 481 additions and 27 deletions
|
@ -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
|
||||
|
|
0
core/src/model/playlist.cpp
Normal file
0
core/src/model/playlist.cpp
Normal file
193
core/src/viewmodel/playlist.cpp
Normal file
193
core/src/viewmodel/playlist.cpp
Normal 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
|
|
@ -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;
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue