Deserialized a list! Restructured project!
I finally got deserializing lists working. Exposing them to QML was not a trivial task either. Note that I didn't do it the clean way. Nested lists are not supported. But it works! Because I got so frustarted at one point trying to implement things the right way, I restructured the project to seperate the Sailfish code from the Qt code and created a new, empty desktop project. The Qt code has been transformed into a happy little library, to which the Sailfish OS application links. Note that QMake doesn't seem to strip the library for some reason.
32
core/core.pro
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
TEMPLATE = lib
|
||||||
|
QT += qml multimedia network websockets
|
||||||
|
|
||||||
|
include(defines.pri)
|
||||||
|
include(../harbour-sailfin.pri)
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
src/credentialmanager.cpp \
|
||||||
|
src/jellyfin.cpp \
|
||||||
|
src/jellyfinapiclient.cpp \
|
||||||
|
src/jellyfinapimodel.cpp \
|
||||||
|
src/jellyfindeviceprofile.cpp \
|
||||||
|
src/jellyfinitem.cpp \
|
||||||
|
src/jellyfinplaybackmanager.cpp \
|
||||||
|
src/jellyfinwebsocket.cpp \
|
||||||
|
src/serverdiscoverymodel.cpp
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
include/credentialmanager.h \
|
||||||
|
include/jellyfin.h \
|
||||||
|
include/jellyfinapiclient.h \
|
||||||
|
include/jellyfinapimodel.h \
|
||||||
|
include/jellyfindeviceprofile.h \
|
||||||
|
include/jellyfinitem.h \
|
||||||
|
include/jellyfinplaybackmanager.h \
|
||||||
|
include/jellyfinwebsocket.h \
|
||||||
|
include/serverdiscoverymodel.h
|
||||||
|
|
||||||
|
VERSION = $$SAILFIN_VERSION
|
||||||
|
|
||||||
|
TARGET = jellyfin-qt
|
||||||
|
DESTDIR = lib
|
2
core/defines.pri
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
message(Including $$_FILE_ from $$IN_PWD)
|
||||||
|
INCLUDEPATH += $$IN_PWD/include
|
16
core/include/jellyfin.h
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#ifndef JELLYFIN_H
|
||||||
|
#define JELLYFIN_H
|
||||||
|
|
||||||
|
#include <QtQml>
|
||||||
|
|
||||||
|
#include "jellyfinapiclient.h"
|
||||||
|
#include "jellyfinapimodel.h"
|
||||||
|
#include "jellyfinitem.h"
|
||||||
|
#include "serverdiscoverymodel.h"
|
||||||
|
#include "jellyfinplaybackmanager.h"
|
||||||
|
|
||||||
|
namespace Jellyfin {
|
||||||
|
void registerTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // JELLYFIN_H
|
|
@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#include <QJsonParseError>
|
#include <QJsonParseError>
|
||||||
#include <QJsonValue>
|
#include <QJsonValue>
|
||||||
|
|
||||||
|
#include <QHostInfo>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QSysInfo>
|
#include <QSysInfo>
|
||||||
|
@ -68,7 +69,6 @@ class WebSocket;
|
||||||
* These steps might change. I'm considering decoupling CredentialsManager from this class to clean some code up.
|
* These steps might change. I'm considering decoupling CredentialsManager from this class to clean some code up.
|
||||||
*/
|
*/
|
||||||
class ApiClient : public QObject {
|
class ApiClient : public QObject {
|
||||||
friend class MediaSource;
|
|
||||||
friend class WebSocket;
|
friend class WebSocket;
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
@ -105,7 +105,19 @@ public:
|
||||||
QString &userId() { return m_userId; }
|
QString &userId() { return m_userId; }
|
||||||
QJsonObject &deviceProfile() { return m_deviceProfile; }
|
QJsonObject &deviceProfile() { return m_deviceProfile; }
|
||||||
QJsonObject &playbackDeviceProfile() { return m_playbackDeviceProfile; }
|
QJsonObject &playbackDeviceProfile() { return m_playbackDeviceProfile; }
|
||||||
QString version() const { return QString(m_version); }
|
QString version() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the error handler of a reply to this classes default error handler
|
||||||
|
* @param rep The reply to set the error handler on.
|
||||||
|
*
|
||||||
|
* Motivation for this helper is because I forget the correct signature each time, with all the
|
||||||
|
* funky casts.
|
||||||
|
*/
|
||||||
|
void setDefaultErrorHandler(QNetworkReply *rep) {
|
||||||
|
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
|
||||||
|
this, &ApiClient::defaultNetworkErrorHandler);
|
||||||
|
}
|
||||||
signals:
|
signals:
|
||||||
/*
|
/*
|
||||||
* Emitted when the server requires authentication. Please authenticate your user via authenticate.
|
* Emitted when the server requires authentication. Please authenticate your user via authenticate.
|
||||||
|
@ -193,7 +205,6 @@ protected:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QNetworkAccessManager m_naManager;
|
QNetworkAccessManager m_naManager;
|
||||||
const char *m_version = SAILFIN_VERSION;
|
|
||||||
/*
|
/*
|
||||||
* State information
|
* State information
|
||||||
*/
|
*/
|
||||||
|
@ -238,17 +249,6 @@ private:
|
||||||
return rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
return rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Sets the error handler of a reply to this classes default error handler
|
|
||||||
* @param rep The reply to set the error handler on.
|
|
||||||
*
|
|
||||||
* Motivation for this helper is because I forget the correct signature each time, with all the
|
|
||||||
* funky casts.
|
|
||||||
*/
|
|
||||||
void setDefaultErrorHandler(QNetworkReply *rep) {
|
|
||||||
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
|
|
||||||
this, &ApiClient::defaultNetworkErrorHandler);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
} // NS Jellyfin
|
} // NS Jellyfin
|
||||||
|
|
|
@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QtQml>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
#include "jellyfinapiclient.h"
|
#include "jellyfinapiclient.h"
|
|
@ -11,13 +11,15 @@
|
||||||
#include <QMetaProperty>
|
#include <QMetaProperty>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QtQml>
|
||||||
|
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
#include <QtQml>
|
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
#include "jellyfinapiclient.h"
|
#include "jellyfinapiclient.h"
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
|
@ -32,6 +34,7 @@ class JsonSerializable : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
Q_INVOKABLE JsonSerializable(QObject *parent);
|
Q_INVOKABLE JsonSerializable(QObject *parent);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Sets this objects properties based on obj.
|
* @brief Sets this objects properties based on obj.
|
||||||
* @param obj The data to load into this object.
|
* @param obj The data to load into this object.
|
||||||
|
@ -39,7 +42,7 @@ public:
|
||||||
void deserialize(const QJsonObject &obj);
|
void deserialize(const QJsonObject &obj);
|
||||||
QJsonObject serialize() const;
|
QJsonObject serialize() const;
|
||||||
private:
|
private:
|
||||||
QVariant jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root) const;
|
QVariant jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root);
|
||||||
QJsonValue variantToJson(const QVariant var) const;
|
QJsonValue variantToJson(const QVariant var) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,8 +57,15 @@ private:
|
||||||
* @return THe modified string
|
* @return THe modified string
|
||||||
*/
|
*/
|
||||||
static QString toPascalCase(QString str);
|
static QString toPascalCase(QString str);
|
||||||
|
|
||||||
|
static const QRegularExpression m_listExpression;
|
||||||
|
/**
|
||||||
|
* @brief Qt is doing weird. I'll keep track of the metatypes myself.
|
||||||
|
*/
|
||||||
|
QHash<QString, const QMetaType *> m_nameMetatypeMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief An "interface" for a remote data source
|
* @brief An "interface" for a remote data source
|
||||||
*
|
*
|
||||||
|
@ -108,6 +118,45 @@ private:
|
||||||
QString m_errorString;
|
QString m_errorString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class MediaStream : public JsonSerializable {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
Q_INVOKABLE explicit MediaStream(QObject *parent = nullptr);
|
||||||
|
MediaStream(const MediaStream &other);
|
||||||
|
bool operator==(const MediaStream &other);
|
||||||
|
virtual ~MediaStream() { qDebug() << "MediaStream destroyed"; }
|
||||||
|
|
||||||
|
enum MediaStreamType {
|
||||||
|
Undefined,
|
||||||
|
Audio,
|
||||||
|
Video,
|
||||||
|
Subtitle,
|
||||||
|
EmbeddedImage
|
||||||
|
};
|
||||||
|
Q_ENUM(MediaStreamType)
|
||||||
|
|
||||||
|
Q_PROPERTY(QString codec MEMBER m_codec NOTIFY codecChanged)
|
||||||
|
Q_PROPERTY(QString codecTag MEMBER m_codecTag NOTIFY codecTagChanged)
|
||||||
|
Q_PROPERTY(QString language MEMBER m_language NOTIFY languageChanged)
|
||||||
|
Q_PROPERTY(QString displayTitle MEMBER m_displayTitle NOTIFY displayTitleChanged)
|
||||||
|
Q_PROPERTY(MediaStreamType type MEMBER m_type NOTIFY typeChanged)
|
||||||
|
Q_PROPERTY(int index MEMBER m_index NOTIFY indexChanged)
|
||||||
|
signals:
|
||||||
|
void codecChanged(const QString &newCodec);
|
||||||
|
void codecTagChanged(const QString &newCodecTag);
|
||||||
|
void languageChanged(const QString &newLanguage);
|
||||||
|
void displayTitleChanged(const QString &newDisplayTitle);
|
||||||
|
void typeChanged(MediaStreamType newType);
|
||||||
|
void indexChanged(int newIndex);
|
||||||
|
private:
|
||||||
|
QString m_codec;
|
||||||
|
QString m_codecTag;
|
||||||
|
QString m_language;
|
||||||
|
QString m_displayTitle;
|
||||||
|
MediaStreamType m_type = Undefined;
|
||||||
|
int m_index = -1;
|
||||||
|
};
|
||||||
|
|
||||||
class Item : public RemoteData {
|
class Item : public RemoteData {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
@ -146,11 +195,21 @@ public:
|
||||||
Q_PROPERTY(QDateTime premiereData MEMBER m_premiereDate NOTIFY premiereDateChanged)
|
Q_PROPERTY(QDateTime premiereData MEMBER m_premiereDate NOTIFY premiereDateChanged)
|
||||||
//SKIP: ExternalUrls
|
//SKIP: ExternalUrls
|
||||||
//SKIP: MediaSources
|
//SKIP: MediaSources
|
||||||
|
Q_PROPERTY(float criticRating READ criticRating WRITE setCriticRating NOTIFY criticRatingChanged)
|
||||||
|
Q_PROPERTY(QStringList productionLocations MEMBER m_productionLocations NOTIFY productionLocationsChanged)
|
||||||
|
|
||||||
// Handpicked, important ones
|
// Handpicked, important ones
|
||||||
|
Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks WRITE setRunTimeTicks NOTIFY runTimeTicksChanged)
|
||||||
Q_PROPERTY(QString overview MEMBER m_overview NOTIFY overviewChanged)
|
Q_PROPERTY(QString overview MEMBER m_overview NOTIFY overviewChanged)
|
||||||
Q_PROPERTY(int productionYear READ productionYear WRITE setProductionYear NOTIFY productionYearChanged)
|
Q_PROPERTY(int productionYear READ productionYear WRITE setProductionYear NOTIFY productionYearChanged)
|
||||||
Q_PROPERTY(int indexNumber READ indexNumber WRITE setProductionYear NOTIFY indexNumberChanged)
|
Q_PROPERTY(int indexNumber READ indexNumber WRITE setIndexNumber NOTIFY indexNumberChanged)
|
||||||
|
Q_PROPERTY(int indexNumberEnd READ indexNumberEnd WRITE setIndexNumberEnd NOTIFY indexNumberEndChanged)
|
||||||
|
Q_PROPERTY(bool isFolder READ isFolder WRITE setIsFolder NOTIFY isFolderChanged)
|
||||||
|
Q_PROPERTY(QString type MEMBER m_type NOTIFY typeChanged)
|
||||||
|
Q_PROPERTY(QString seriesName MEMBER m_seriesName NOTIFY seriesNameChanged)
|
||||||
|
Q_PROPERTY(QString seasonName MEMBER m_seasonName NOTIFY seasonNameChanged)
|
||||||
|
Q_PROPERTY(QList<MediaStream *> __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged)
|
||||||
|
Q_PROPERTY(QVariantList mediaStreams MEMBER m_mediaStreams NOTIFY mediaStreamsChanged STORED false)
|
||||||
|
|
||||||
QString jellyfinId() const { return m_id; }
|
QString jellyfinId() const { return m_id; }
|
||||||
void setJellyfinId(QString newId);
|
void setJellyfinId(QString newId);
|
||||||
|
@ -170,12 +229,24 @@ public:
|
||||||
void setHasSubtitles(bool newHasSubtitles) { m_hasSubtitles = newHasSubtitles; emit hasSubtitlesChanged(newHasSubtitles); }
|
void setHasSubtitles(bool newHasSubtitles) { m_hasSubtitles = newHasSubtitles; emit hasSubtitlesChanged(newHasSubtitles); }
|
||||||
bool supportsSync() const { return m_supportsSync.value_or(false); }
|
bool supportsSync() const { return m_supportsSync.value_or(false); }
|
||||||
void setSupportsSync(bool newSupportsSync) { m_supportsSync = newSupportsSync; emit supportsSyncChanged(newSupportsSync); }
|
void setSupportsSync(bool newSupportsSync) { m_supportsSync = newSupportsSync; emit supportsSyncChanged(newSupportsSync); }
|
||||||
|
float criticRating() const { return m_criticRating.value_or(std::nanf("")); }
|
||||||
|
void setCriticRating(float newCriticRating) { m_criticRating = newCriticRating; emit criticRatingChanged(newCriticRating); }
|
||||||
|
|
||||||
// Handpicked, important ones
|
// Handpicked, important ones
|
||||||
|
qint64 runTimeTicks() const { return m_runTimeTicks.value_or(-1); }
|
||||||
|
void setRunTimeTicks(qint64 newRunTimeTicks) { m_runTimeTicks = newRunTimeTicks; emit runTimeTicksChanged(newRunTimeTicks); }
|
||||||
int productionYear() const { return m_productionYear.value_or(-1); }
|
int productionYear() const { return m_productionYear.value_or(-1); }
|
||||||
void setProductionYear(int newProductionYear) { m_productionYear = newProductionYear; emit productionYearChanged(newProductionYear); }
|
void setProductionYear(int newProductionYear) { m_productionYear = std::optional<int>(newProductionYear); emit productionYearChanged(newProductionYear); }
|
||||||
int indexNumber() const { return m_indexNumber.value_or(-1); }
|
int indexNumber() const { return m_indexNumber.value_or(-1); }
|
||||||
void setIndexNumber(int newIndexNumber) { m_indexNumber = newIndexNumber; emit indexNumberChanged(newIndexNumber); }
|
void setIndexNumber(int newIndexNumber) { m_indexNumber = std::optional<int>(newIndexNumber); emit indexNumberChanged(newIndexNumber); }
|
||||||
|
int indexNumberEnd() const { return m_indexNumberEnd.value_or(-1); }
|
||||||
|
void setIndexNumberEnd(int newIndexNumberEnd) { m_indexNumberEnd = std::optional<int>(newIndexNumberEnd); emit indexNumberEndChanged(newIndexNumberEnd); }
|
||||||
|
bool isFolder() const { return m_isFolder.value_or(false); }
|
||||||
|
void setIsFolder(bool newIsFolder) { m_isFolder = newIsFolder; emit isFolderChanged(newIsFolder); }
|
||||||
|
|
||||||
|
//QQmlListProperty<MediaStream> mediaStreams() { return toReadOnlyQmlListProperty<MediaStream>(m_mediaStreams); }
|
||||||
|
//QList<QObject *> mediaStreams() { return *reinterpret_cast<QList<QObject *> *>(&m_mediaStreams); }
|
||||||
|
QVariantList mediaStreams() { QVariantList l; for (auto e: m_mediaStreams) l.append(QVariant::fromValue(e)); return l;}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void jellyfinIdChanged(const QString &newId);
|
void jellyfinIdChanged(const QString &newId);
|
||||||
|
@ -201,11 +272,20 @@ signals:
|
||||||
void sortNameChanged(const QString &newSortName);
|
void sortNameChanged(const QString &newSortName);
|
||||||
void forcedSortNameChanged(const QString &newForcedSortName);
|
void forcedSortNameChanged(const QString &newForcedSortName);
|
||||||
void premiereDateChanged(QDateTime newPremiereDate);
|
void premiereDateChanged(QDateTime newPremiereDate);
|
||||||
|
void criticRatingChanged(float newCriticRating);
|
||||||
|
void productionLocationsChanged(QStringList newProductionLocations);
|
||||||
|
|
||||||
// Handpicked, important ones
|
// Handpicked, important ones
|
||||||
|
void runTimeTicksChanged(qint64 newRunTimeTicks);
|
||||||
void overviewChanged(const QString &newOverview);
|
void overviewChanged(const QString &newOverview);
|
||||||
void productionYearChanged(int newProductionYear);
|
void productionYearChanged(int newProductionYear);
|
||||||
void indexNumberChanged(int newIndexNumber);
|
void indexNumberChanged(int newIndexNumber);
|
||||||
|
void indexNumberEndChanged(int newIndexNumberEnd);
|
||||||
|
void isFolderChanged(bool newIsFolder);
|
||||||
|
void typeChanged(const QString &newType);
|
||||||
|
void seriesNameChanged(const QString &newSeriesName);
|
||||||
|
void seasonNameChanged(const QString &newSeasonName);
|
||||||
|
void mediaStreamsChanged(/*const QList<MediaStream *> &newMediaStreams*/);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
/**
|
/**
|
||||||
|
@ -236,11 +316,36 @@ protected:
|
||||||
QString m_sortName;
|
QString m_sortName;
|
||||||
QString m_forcedSortName;
|
QString m_forcedSortName;
|
||||||
QDateTime m_premiereDate;
|
QDateTime m_premiereDate;
|
||||||
|
std::optional<float> m_criticRating = std::nullopt;
|
||||||
|
QStringList m_productionLocations;
|
||||||
|
|
||||||
// Handpicked, important ones
|
// Handpicked, important ones
|
||||||
|
std::optional<qint64> m_runTimeTicks = std::nullopt;
|
||||||
QString m_overview;
|
QString m_overview;
|
||||||
std::optional<int> m_productionYear = std::nullopt;
|
std::optional<int> m_productionYear = std::nullopt;
|
||||||
std::optional<int> m_indexNumber = std::nullopt;
|
std::optional<int> m_indexNumber = std::nullopt;
|
||||||
|
std::optional<int> m_indexNumberEnd = std::nullopt;
|
||||||
|
std::optional<bool> m_isFolder = std::nullopt;
|
||||||
|
QString m_type;
|
||||||
|
QString m_seriesName;
|
||||||
|
QString m_seasonName;
|
||||||
|
QList<MediaStream *> __list__m_mediaStreams;
|
||||||
|
QVariantList m_mediaStreams;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
QQmlListProperty<T> toReadOnlyQmlListProperty(QList<T *> &list) {
|
||||||
|
return QQmlListProperty<T>(this, std::addressof(list), &qlist_count, &qlist_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static int qlist_count(QQmlListProperty<T> *p) {
|
||||||
|
return reinterpret_cast<QList<T *> *>(p->data)->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static T *qlist_at(QQmlListProperty<T> *p, int idx) {
|
||||||
|
return reinterpret_cast<QList<T *> *>(p->data)->at(idx);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void registerSerializableJsonTypes(const char* URI);
|
void registerSerializableJsonTypes(const char* URI);
|
|
@ -34,7 +34,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
|
|
||||||
class MediaSource : public QObject {
|
class PlaybackManager : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
enum PlayMethod {
|
enum PlayMethod {
|
||||||
|
@ -44,7 +44,7 @@ public:
|
||||||
};
|
};
|
||||||
Q_ENUM(PlayMethod)
|
Q_ENUM(PlayMethod)
|
||||||
|
|
||||||
explicit MediaSource(QObject *parent = nullptr);
|
explicit PlaybackManager(QObject *parent = nullptr);
|
||||||
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient)
|
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient)
|
||||||
Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
|
Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
|
||||||
Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
|
Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
|
|
@ -55,6 +55,7 @@ public slots:
|
||||||
private slots:
|
private slots:
|
||||||
void textMessageReceived(const QString &message);
|
void textMessageReceived(const QString &message);
|
||||||
void onConnected();
|
void onConnected();
|
||||||
|
void onDisconnected();
|
||||||
|
|
||||||
void sendKeepAlive();
|
void sendKeepAlive();
|
||||||
signals:
|
signals:
|
|
@ -76,8 +76,8 @@ private:
|
||||||
const QByteArray MAGIC_PACKET = "who is JellyfinServer?";
|
const QByteArray MAGIC_PACKET = "who is JellyfinServer?";
|
||||||
const quint16 BROADCAST_PORT = 7359;
|
const quint16 BROADCAST_PORT = 7359;
|
||||||
|
|
||||||
QUdpSocket m_socket;
|
|
||||||
std::vector<ServerDiscovery> m_discoveredServers;
|
std::vector<ServerDiscovery> m_discoveredServers;
|
||||||
|
QUdpSocket m_socket;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endif //SERVER_DISCOVERY_MODEL_H
|
#endif //SERVER_DISCOVERY_MODEL_H
|
19
core/src/jellyfin.cpp
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#include "jellyfin.h"
|
||||||
|
namespace Jellyfin {
|
||||||
|
|
||||||
|
void registerTypes() {
|
||||||
|
const char* QML_NAMESPACE = "nl.netsoj.chris.Jellyfin";
|
||||||
|
// Singletons are perhaps bad, but they are convenient :)
|
||||||
|
qmlRegisterSingletonType<Jellyfin::ApiClient>(QML_NAMESPACE, 1, 0, "ApiClient", [](QQmlEngine *eng, QJSEngine *js) {
|
||||||
|
Q_UNUSED(eng)
|
||||||
|
Q_UNUSED(js)
|
||||||
|
return dynamic_cast<QObject*>(new Jellyfin::ApiClient());
|
||||||
|
});
|
||||||
|
qmlRegisterType<Jellyfin::ServerDiscoveryModel>(QML_NAMESPACE, 1, 0, "ServerDiscoveryModel");
|
||||||
|
qmlRegisterType<Jellyfin::PlaybackManager>(QML_NAMESPACE, 1, 0, "PlaybackManager");
|
||||||
|
|
||||||
|
// API models
|
||||||
|
Jellyfin::registerModels(QML_NAMESPACE);
|
||||||
|
Jellyfin::registerSerializableJsonTypes(QML_NAMESPACE);
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#include "jellyfinapiclient.h"
|
#include "jellyfinapiclient.h"
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
|
|
||||||
ApiClient::ApiClient(QObject *parent)
|
ApiClient::ApiClient(QObject *parent)
|
||||||
: QObject(parent),
|
: QObject(parent),
|
||||||
m_webSocket(new WebSocket(this)) {
|
m_webSocket(new WebSocket(this)) {
|
||||||
|
@ -30,6 +31,10 @@ ApiClient::ApiClient(QObject *parent)
|
||||||
generateDeviceProfile();
|
generateDeviceProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ApiClient::version() const {
|
||||||
|
return QString(SAILFIN_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// BASE HTTP METHODS //
|
// BASE HTTP METHODS //
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -38,7 +43,7 @@ ApiClient::ApiClient(QObject *parent)
|
||||||
void ApiClient::addBaseRequestHeaders(QNetworkRequest &request, const QString &path, const QUrlQuery ¶ms) {
|
void ApiClient::addBaseRequestHeaders(QNetworkRequest &request, const QString &path, const QUrlQuery ¶ms) {
|
||||||
addTokenHeader(request);
|
addTokenHeader(request);
|
||||||
request.setRawHeader("Accept", "application/json;"); // profile=\"CamelCase\"");
|
request.setRawHeader("Accept", "application/json;"); // profile=\"CamelCase\"");
|
||||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Sailfin/%1").arg(m_version));
|
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Sailfin/%1").arg(version()));
|
||||||
QString url = this->m_baseUrl + path;
|
QString url = this->m_baseUrl + path;
|
||||||
if (!params.isEmpty()) url += "?" + params.toString();
|
if (!params.isEmpty()) url += "?" + params.toString();
|
||||||
request.setUrl(url);
|
request.setUrl(url);
|
||||||
|
@ -49,7 +54,7 @@ void ApiClient::addTokenHeader(QNetworkRequest &request) {
|
||||||
authentication += "Client=\"Sailfin\"";
|
authentication += "Client=\"Sailfin\"";
|
||||||
authentication += ", Device=\"" + m_deviceName + "\"";
|
authentication += ", Device=\"" + m_deviceName + "\"";
|
||||||
authentication += ", DeviceId=\"" + m_deviceId + "\"";
|
authentication += ", DeviceId=\"" + m_deviceId + "\"";
|
||||||
authentication += ", Version=\"" + QString(m_version) + "\"";
|
authentication += ", Version=\"" + version() + "\"";
|
||||||
if (m_authenticated) {
|
if (m_authenticated) {
|
||||||
authentication += ", token=\"" + m_token + "\"";
|
authentication += ", token=\"" + m_token + "\"";
|
||||||
}
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
#include "jellyfinitem.h"
|
#include "jellyfinitem.h"
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
JsonSerializable::JsonSerializable(QObject *parent) : QObject(parent) {}
|
const QRegularExpression JsonSerializable::m_listExpression = QRegularExpression("^QList<\\s*([a-zA-Z0-9]*)\\s*\\*?\\s*>$");
|
||||||
|
JsonSerializable::JsonSerializable(QObject *parent) : QObject(parent) {
|
||||||
|
}
|
||||||
|
|
||||||
void JsonSerializable::deserialize(const QJsonObject &jObj) {
|
void JsonSerializable::deserialize(const QJsonObject &jObj) {
|
||||||
const QMetaObject *obj = this->metaObject();
|
const QMetaObject *obj = this->metaObject();
|
||||||
|
@ -21,13 +23,29 @@ void JsonSerializable::deserialize(const QJsonObject &jObj) {
|
||||||
} else if (jObj.contains(toPascalCase(prop.name()))) {
|
} else if (jObj.contains(toPascalCase(prop.name()))) {
|
||||||
QJsonValue val = jObj[toPascalCase(prop.name())];
|
QJsonValue val = jObj[toPascalCase(prop.name())];
|
||||||
prop.write(this, jsonToVariant(prop, val, jObj));
|
prop.write(this, jsonToVariant(prop, val, jObj));
|
||||||
|
} else if (QString(prop.name()).startsWith("__list__")) {
|
||||||
|
// QML doesn't like it if we expose properties of type QList<SubclassOfQobject *>
|
||||||
|
// we need to explicitly cast them to QList<QObject *> or QQmlListProperty, which sucks.
|
||||||
|
// That's why we have a special case for properties starting with __list__. This contains
|
||||||
|
// the actual QList<SubclassOfQobject *>, so that qml can access the object with its real name.
|
||||||
|
QString realName = toPascalCase(prop.name() + 8);
|
||||||
|
if (!jObj.contains(realName)) {
|
||||||
|
qDebug() << "Ignoring " << realName << " - " << prop.name();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QJsonValue val = jObj[realName];
|
||||||
|
qDebug() << realName << " - " << prop.name() << ": " << val;
|
||||||
|
QMetaProperty realProp = obj->property(obj->indexOfProperty(prop.name() + 8));
|
||||||
|
if (!realProp.write(this, jsonToVariant(prop, val, jObj))) {
|
||||||
|
qDebug() << "Write to " << prop.name() << "failed";
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "Ignored " << prop.name() << " while deserializing";
|
qDebug() << "Ignored " << prop.name() << " while deserializing";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root) const {
|
QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root) {
|
||||||
switch(val.type()) {
|
switch(val.type()) {
|
||||||
case QJsonValue::Null:
|
case QJsonValue::Null:
|
||||||
case QJsonValue::Undefined:
|
case QJsonValue::Undefined:
|
||||||
|
@ -37,25 +55,48 @@ QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &v
|
||||||
case QJsonValue::String:
|
case QJsonValue::String:
|
||||||
return val.toVariant();
|
return val.toVariant();
|
||||||
case QJsonValue::Array:
|
case QJsonValue::Array:
|
||||||
if (prop.type() == QVariant::List) {
|
{
|
||||||
QJsonArray arr = val.toArray();
|
QJsonArray arr = val.toArray();
|
||||||
QVariantList varArr;
|
QVariantList varArr;
|
||||||
for (auto it = arr.begin(); it < arr.end(); it++) {
|
for (auto it = arr.constBegin(); it < arr.constEnd(); it++) {
|
||||||
varArr << jsonToVariant(prop, *it, root);
|
QVariant variant = jsonToVariant(prop, *it, root);
|
||||||
|
qDebug() << variant;
|
||||||
|
varArr.append(variant);
|
||||||
}
|
}
|
||||||
|
qDebug() << prop.name() << ": " << varArr.count();
|
||||||
return QVariant(varArr);
|
return QVariant(varArr);
|
||||||
} else {
|
|
||||||
qDebug() << prop.name() << " is not a " << prop.typeName();
|
|
||||||
return QVariant();
|
|
||||||
}
|
}
|
||||||
case QJsonValue::Object:
|
case QJsonValue::Object:
|
||||||
QJsonObject innerObj = val.toObject();
|
QJsonObject innerObj = val.toObject();
|
||||||
QObject *deserializedInnerObj = QMetaType::metaObjectForType(prop.userType())->newInstance();
|
int typeNo = prop.userType();
|
||||||
|
const QMetaObject *metaType = QMetaType::metaObjectForType(prop.userType());
|
||||||
|
if (metaType == nullptr) {
|
||||||
|
// Try to determine if the type is a qlist
|
||||||
|
QRegularExpressionMatch match = m_listExpression.match(prop.typeName());
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
// It is a qList! Now extract the inner type
|
||||||
|
// There should be an easier way, shouldn't there?
|
||||||
|
QString listType = match.captured(1).prepend("Jellyfin::").append("*");
|
||||||
|
// UGLY CODE HERE WE COME
|
||||||
|
typeNo = QMetaType::type(listType.toUtf8());
|
||||||
|
if (typeNo == QMetaType::UnknownType) {
|
||||||
|
qDebug() << "Unknown type: " << listType;
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
metaType = QMetaType::metaObjectForType(typeNo);
|
||||||
|
} else {
|
||||||
|
qDebug() << "No metaObject for " << prop.typeName() << ", " << prop.type() << ", " << prop.userType();
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QObject *deserializedInnerObj = metaType->newInstance();
|
||||||
|
deserializedInnerObj->setParent(this);
|
||||||
if (JsonSerializable *ser = dynamic_cast<JsonSerializable *>(deserializedInnerObj)) {
|
if (JsonSerializable *ser = dynamic_cast<JsonSerializable *>(deserializedInnerObj)) {
|
||||||
qDebug() << "Deserializing user type " << deserializedInnerObj->metaObject()->className();
|
qDebug() << "Deserializing user type " << deserializedInnerObj->metaObject()->className();
|
||||||
ser->deserialize(innerObj);
|
ser->deserialize(innerObj);
|
||||||
return QVariant::fromValue(ser);
|
return QVariant(typeNo, &ser);
|
||||||
} else {
|
} else {
|
||||||
|
deserializedInnerObj->deleteLater();
|
||||||
qDebug() << "Object is not a serializable one!";
|
qDebug() << "Object is not a serializable one!";
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
@ -146,10 +187,28 @@ void RemoteData::setApiClient(ApiClient *newApiClient) {
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MediaStream
|
||||||
|
MediaStream::MediaStream(QObject *parent) : JsonSerializable (parent) {}
|
||||||
|
MediaStream::MediaStream(const MediaStream &other)
|
||||||
|
: JsonSerializable (other.parent()),
|
||||||
|
m_codec(other.m_codec),
|
||||||
|
m_codecTag(other.m_codecTag),
|
||||||
|
m_language(other.m_language),
|
||||||
|
m_displayTitle(other.m_displayTitle),
|
||||||
|
m_type(other.m_type),
|
||||||
|
m_index(other.m_index){
|
||||||
|
}
|
||||||
|
bool MediaStream::operator==(const MediaStream &other) {
|
||||||
|
// displayTitle is explicitly left out, since it's generated based on other properties
|
||||||
|
// in the Jellyfin source code.
|
||||||
|
return m_codec == other.m_codec && m_codecTag == other.m_codecTag
|
||||||
|
&& m_language == other.m_language && m_type == other.m_type
|
||||||
|
&& m_index == other.m_index;
|
||||||
|
}
|
||||||
|
|
||||||
// Item
|
// Item
|
||||||
|
|
||||||
Item::Item(QObject *parent) : RemoteData(parent) {
|
Item::Item(QObject *parent) : RemoteData(parent) {}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void Item::setJellyfinId(QString newId) {
|
void Item::setJellyfinId(QString newId) {
|
||||||
|
@ -172,7 +231,9 @@ void Item::reload() {
|
||||||
rep->deleteLater();
|
rep->deleteLater();
|
||||||
|
|
||||||
QJsonParseError error;
|
QJsonParseError error;
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(rep->readAll(), &error);
|
QString data(rep->readAll());
|
||||||
|
data = data.normalized(QString::NormalizationForm_D);
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8(), &error);
|
||||||
if (doc.isNull()) {
|
if (doc.isNull()) {
|
||||||
this->setError(QNetworkReply::ProtocolFailure);
|
this->setError(QNetworkReply::ProtocolFailure);
|
||||||
this->setErrorString(error.errorString());
|
this->setErrorString(error.errorString());
|
||||||
|
@ -196,6 +257,7 @@ void Item::reload() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerSerializableJsonTypes(const char* URI) {
|
void registerSerializableJsonTypes(const char* URI) {
|
||||||
|
qmlRegisterType<MediaStream>(URI, 1, 0, "MediaStream");
|
||||||
qmlRegisterType<Item>(URI, 1, 0, "JellyfinItem");
|
qmlRegisterType<Item>(URI, 1, 0, "JellyfinItem");
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -17,18 +17,18 @@ License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "jellyfinmediasource.h"
|
#include "jellyfinplaybackmanager.h"
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
|
|
||||||
MediaSource::MediaSource(QObject *parent)
|
PlaybackManager::PlaybackManager(QObject *parent)
|
||||||
: QObject(parent) {
|
: QObject(parent) {
|
||||||
m_updateTimer.setInterval(10000); // 10 seconds
|
m_updateTimer.setInterval(10000); // 10 seconds
|
||||||
m_updateTimer.setSingleShot(false);
|
m_updateTimer.setSingleShot(false);
|
||||||
connect(&m_updateTimer, &QTimer::timeout, this, &MediaSource::updatePlaybackInfo);
|
connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaSource::fetchStreamUrl() {
|
void PlaybackManager::fetchStreamUrl() {
|
||||||
QUrlQuery params;
|
QUrlQuery params;
|
||||||
params.addQueryItem("UserId", m_apiClient->userId());
|
params.addQueryItem("UserId", m_apiClient->userId());
|
||||||
params.addQueryItem("StartTimeTicks", QString::number(m_position));
|
params.addQueryItem("StartTimeTicks", QString::number(m_position));
|
||||||
|
@ -62,7 +62,7 @@ void MediaSource::fetchStreamUrl() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaSource::setItemId(const QString &newItemId) {
|
void PlaybackManager::setItemId(const QString &newItemId) {
|
||||||
if (m_apiClient == nullptr) {
|
if (m_apiClient == nullptr) {
|
||||||
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
|
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
|
||||||
return;
|
return;
|
||||||
|
@ -76,12 +76,12 @@ void MediaSource::setItemId(const QString &newItemId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaSource::setStreamUrl(const QString &streamUrl) {
|
void PlaybackManager::setStreamUrl(const QString &streamUrl) {
|
||||||
this->m_streamUrl = streamUrl;
|
this->m_streamUrl = streamUrl;
|
||||||
emit streamUrlChanged(streamUrl);
|
emit streamUrlChanged(streamUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaSource::setPosition(qint64 position) {
|
void PlaybackManager::setPosition(qint64 position) {
|
||||||
if (position == 0 && m_position != 0) {
|
if (position == 0 && m_position != 0) {
|
||||||
// Save the old position when stop gets called. The QMediaPlayer will try to set
|
// Save the old position when stop gets called. The QMediaPlayer will try to set
|
||||||
// position to 0 when stopped, but we don't want to report that to Jellyfin. We
|
// position to 0 when stopped, but we don't want to report that to Jellyfin. We
|
||||||
|
@ -92,7 +92,7 @@ void MediaSource::setPosition(qint64 position) {
|
||||||
emit positionChanged(position);
|
emit positionChanged(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaSource::setState(QMediaPlayer::State newState) {
|
void PlaybackManager::setState(QMediaPlayer::State newState) {
|
||||||
if (m_state == newState) return;
|
if (m_state == newState) return;
|
||||||
if (m_state == QMediaPlayer::StoppedState) {
|
if (m_state == QMediaPlayer::StoppedState) {
|
||||||
// We're transitioning from stopped to either playing or paused.
|
// We're transitioning from stopped to either playing or paused.
|
||||||
|
@ -112,11 +112,11 @@ void MediaSource::setState(QMediaPlayer::State newState) {
|
||||||
emit this->stateChanged(newState);
|
emit this->stateChanged(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaSource::updatePlaybackInfo() {
|
void PlaybackManager::updatePlaybackInfo() {
|
||||||
postPlaybackInfo(Progress);
|
postPlaybackInfo(Progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaSource::postPlaybackInfo(PlaybackInfoType type) {
|
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
|
|
||||||
root["ItemId"] = m_itemId;
|
root["ItemId"] = m_itemId;
|
|
@ -22,6 +22,7 @@ namespace Jellyfin {
|
||||||
WebSocket::WebSocket(ApiClient *client)
|
WebSocket::WebSocket(ApiClient *client)
|
||||||
: QObject (client), m_apiClient(client){
|
: QObject (client), m_apiClient(client){
|
||||||
connect(&m_webSocket, &QWebSocket::connected, this, &WebSocket::onConnected);
|
connect(&m_webSocket, &QWebSocket::connected, this, &WebSocket::onConnected);
|
||||||
|
connect(&m_webSocket, &QWebSocket::disconnected, this, &WebSocket::onDisconnected);
|
||||||
connect(&m_webSocket, static_cast<void (QWebSocket::*)(QAbstractSocket::SocketError)>(&QWebSocket::error),
|
connect(&m_webSocket, static_cast<void (QWebSocket::*)(QAbstractSocket::SocketError)>(&QWebSocket::error),
|
||||||
this, [this](QAbstractSocket::SocketError error) {
|
this, [this](QAbstractSocket::SocketError error) {
|
||||||
Q_UNUSED(error)
|
Q_UNUSED(error)
|
||||||
|
@ -45,6 +46,11 @@ void WebSocket::onConnected() {
|
||||||
connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived);
|
connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WebSocket::onDisconnected() {
|
||||||
|
disconnect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived);
|
||||||
|
m_keepAliveTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
void WebSocket::textMessageReceived(const QString &message) {
|
void WebSocket::textMessageReceived(const QString &message) {
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
|
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
|
||||||
if (doc.isNull() || !doc.isObject()) {
|
if (doc.isNull() || !doc.isObject()) {
|
21
desktop/.qmake.stash
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
QMAKE_CXX.QT_COMPILER_STDCXX = 201402L
|
||||||
|
QMAKE_CXX.QMAKE_GCC_MAJOR_VERSION = 10
|
||||||
|
QMAKE_CXX.QMAKE_GCC_MINOR_VERSION = 2
|
||||||
|
QMAKE_CXX.QMAKE_GCC_PATCH_VERSION = 0
|
||||||
|
QMAKE_CXX.COMPILER_MACROS = \
|
||||||
|
QT_COMPILER_STDCXX \
|
||||||
|
QMAKE_GCC_MAJOR_VERSION \
|
||||||
|
QMAKE_GCC_MINOR_VERSION \
|
||||||
|
QMAKE_GCC_PATCH_VERSION
|
||||||
|
QMAKE_CXX.INCDIRS = \
|
||||||
|
/usr/include/c++/10.2.0 \
|
||||||
|
/usr/include/c++/10.2.0/x86_64-pc-linux-gnu \
|
||||||
|
/usr/include/c++/10.2.0/backward \
|
||||||
|
/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include \
|
||||||
|
/usr/local/include \
|
||||||
|
/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include-fixed \
|
||||||
|
/usr/include
|
||||||
|
QMAKE_CXX.LIBDIRS = \
|
||||||
|
/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0 \
|
||||||
|
/usr/lib \
|
||||||
|
/lib
|
14
desktop/desktop.pro
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
TEMPLATE = app
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
src/main.cpp
|
||||||
|
|
||||||
|
include(../harbour-sailfin.pri)
|
||||||
|
|
||||||
|
# Include our library
|
||||||
|
|
||||||
|
LIBS += -L$$OUT_PWD/../core/lib -ljellyfin-qt
|
||||||
|
core.files += ../core/lib
|
||||||
|
core.path = /usr/share/$${TARGET}
|
||||||
|
|
||||||
|
INSTALLS += core
|
7
desktop/src/main.cpp
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#include <QGuiApplication>
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
QGuiApplication app(argc, argv);
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
7
harbour-sailfin.pri
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
!defined(SAILFIN_VERSION, var) {
|
||||||
|
SAILFIN_VERSION = "0.0.0-unknown"
|
||||||
|
}
|
||||||
|
QMAKE_CXXFLAGS += -std=c++17
|
||||||
|
|
||||||
|
# Help, something keeps eating my quotes and backslashes
|
||||||
|
DEFINES += "SAILFIN_VERSION=\"\\\"$$SAILFIN_VERSION\\\"\""
|
|
@ -1,85 +1,20 @@
|
||||||
# NOTICE:
|
TEMPLATE = subdirs
|
||||||
#
|
SUBDIRS = core
|
||||||
# Application name defined in TARGET has a corresponding QML filename.
|
|
||||||
# If name defined in TARGET is changed, the following needs to be done
|
|
||||||
# to match new name:
|
|
||||||
# - corresponding QML filename must be changed
|
|
||||||
# - desktop icon filename must be changed
|
|
||||||
# - desktop filename must be changed
|
|
||||||
# - icon definition filename in desktop file must be changed
|
|
||||||
# - translation filenames have to be changed
|
|
||||||
|
|
||||||
# The name of your application
|
core.subdir = core
|
||||||
TARGET = harbour-sailfin
|
|
||||||
|
|
||||||
QT += multimedia websockets
|
defined(OS_SAILFISHOS, var){
|
||||||
|
SUBDIRS += sailfish
|
||||||
CONFIG += sailfishapp # c++17
|
sailfish.subdir = sailfish
|
||||||
QMAKE_CXXFLAGS += -std=c++17
|
sailfish.depends = core
|
||||||
|
}
|
||||||
# Help, something keeps eating my quotes and backslashes
|
defined(OS_DESKTOP, var) {
|
||||||
|
SUBDIRS += desktop
|
||||||
!defined(SAILFIN_VERSION, var) {
|
desktop.subdir = desktop
|
||||||
SAILFIN_VERSION = "(UNKNOWN VERSION)"
|
desktop.depends = core
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFINES += "SAILFIN_VERSION=\"\\\"$$SAILFIN_VERSION\\\"\""
|
message($$SUBDIRS)
|
||||||
|
|
||||||
SOURCES += \
|
|
||||||
src/credentialmanager.cpp \
|
|
||||||
src/harbour-sailfin.cpp \
|
|
||||||
src/jellyfinapiclient.cpp \
|
|
||||||
src/jellyfinapimodel.cpp \
|
|
||||||
src/jellyfindeviceprofile.cpp \
|
|
||||||
src/jellyfinitem.cpp \
|
|
||||||
src/jellyfinmediasource.cpp \
|
|
||||||
src/jellyfinwebsocket.cpp \
|
|
||||||
src/serverdiscoverymodel.cpp
|
|
||||||
|
|
||||||
DISTFILES += \
|
|
||||||
qml/Constants.qml \
|
|
||||||
qml/Utils.js \
|
|
||||||
qml/components/GlassyBackground.qml \
|
|
||||||
qml/components/IconListItem.qml \
|
|
||||||
qml/components/LibraryItemDelegate.qml \
|
|
||||||
qml/components/MoreSection.qml \
|
|
||||||
qml/components/PlainLabel.qml \
|
|
||||||
qml/components/PlayToolbar.qml \
|
|
||||||
qml/components/RemoteImage.qml \
|
|
||||||
qml/components/Shim.qml \
|
|
||||||
qml/components/UserGridDelegate.qml \
|
|
||||||
qml/components/VideoPlayer.qml \
|
|
||||||
qml/components/VideoTrackSelector.qml \
|
|
||||||
qml/components/itemdetails/SeasonDetails.qml \
|
|
||||||
qml/components/videoplayer/VideoError.qml \
|
|
||||||
qml/components/videoplayer/VideoHud.qml \
|
|
||||||
qml/cover/CoverPage.qml \
|
|
||||||
qml/cover/PosterCover.qml \
|
|
||||||
qml/cover/VideoCover.qml \
|
|
||||||
qml/pages/LegalPage.qml \
|
|
||||||
qml/pages/MainPage.qml \
|
|
||||||
qml/pages/AboutPage.qml \
|
|
||||||
qml/harbour-sailfin.qml \
|
|
||||||
qml/pages/SettingsPage.qml \
|
|
||||||
qml/pages/VideoPage.qml \
|
|
||||||
qml/pages/itemdetails/BaseDetailPage.qml \
|
|
||||||
qml/pages/itemdetails/CollectionPage.qml \
|
|
||||||
qml/pages/itemdetails/EpisodePage.qml \
|
|
||||||
qml/pages/itemdetails/FilmPage.qml \
|
|
||||||
qml/pages/itemdetails/MusicAlbumPage.qml \
|
|
||||||
qml/pages/itemdetails/SeasonPage.qml \
|
|
||||||
qml/pages/itemdetails/SeriesPage.qml \
|
|
||||||
qml/pages/itemdetails/UnsupportedPage.qml \
|
|
||||||
qml/pages/itemdetails/VideoPage.qml \
|
|
||||||
qml/pages/setup/AddServerConnectingPage.qml \
|
|
||||||
qml/pages/setup/LoginDialog.qml \
|
|
||||||
qml/qmldir
|
|
||||||
|
|
||||||
SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172
|
|
||||||
|
|
||||||
# to disable building translations every time, comment out the
|
|
||||||
# following CONFIG line
|
|
||||||
CONFIG += sailfishapp_i18n
|
|
||||||
|
|
||||||
# German translation is enabled as an example. If you aren't
|
# German translation is enabled as an example. If you aren't
|
||||||
# planning to localize your app, remember to comment out the
|
# planning to localize your app, remember to comment out the
|
||||||
|
@ -87,12 +22,4 @@ CONFIG += sailfishapp_i18n
|
||||||
# modify the localized app name in the the .desktop file.
|
# modify the localized app name in the the .desktop file.
|
||||||
# TRANSLATIONS += \
|
# TRANSLATIONS += \
|
||||||
|
|
||||||
HEADERS += \
|
|
||||||
src/credentialmanager.h \
|
|
||||||
src/jellyfinapiclient.h \
|
|
||||||
src/jellyfinapimodel.h \
|
|
||||||
src/jellyfindeviceprofile.h \
|
|
||||||
src/jellyfinitem.h \
|
|
||||||
src/jellyfinmediasource.h \
|
|
||||||
src/jellyfinwebsocket.h \
|
|
||||||
src/serverdiscoverymodel.h
|
|
||||||
|
|
|
@ -7,13 +7,12 @@ Release: 1
|
||||||
Group: Qt/Qt
|
Group: Qt/Qt
|
||||||
URL: https://chris.netsoj.nl/projects/harbour-sailfin
|
URL: https://chris.netsoj.nl/projects/harbour-sailfin
|
||||||
License: LGPL-2.0-or-later
|
License: LGPL-2.0-or-later
|
||||||
# This must be generated before uploading a package to a remote build service.
|
# This must be generated before uploading a package to a remote build service. Usually this line does not need to be modified.
|
||||||
# Usually this line does not need to be modified.
|
|
||||||
Sources:
|
Sources:
|
||||||
- '%{name}-%{version}.tar.bz2'
|
- '%{name}-%{version}.tar.bz2'
|
||||||
Description: |
|
Description: |
|
||||||
Play video's and music from your Jellyfin media player on your Sailfish device
|
Play video's and music from your Jellyfin media player on your Sailfish device
|
||||||
Builder: qtc5
|
Builder: qmake5
|
||||||
|
|
||||||
# This section specifies build dependencies that are resolved using pkgconfig.
|
# This section specifies build dependencies that are resolved using pkgconfig.
|
||||||
# This is the preferred way of specifying build dependencies for your package.
|
# This is the preferred way of specifying build dependencies for your package.
|
||||||
|
@ -40,7 +39,12 @@ Files:
|
||||||
- '%{_datadir}/applications/%{name}.desktop'
|
- '%{_datadir}/applications/%{name}.desktop'
|
||||||
- '%{_datadir}/icons/hicolor/*/apps/%{name}.png'
|
- '%{_datadir}/icons/hicolor/*/apps/%{name}.png'
|
||||||
|
|
||||||
|
Macros:
|
||||||
|
- '__provides_exclude_from; ^%{_datadir}/.*$'
|
||||||
|
- '__requires_exclude; ^libjellyfin-qt.*$'
|
||||||
|
|
||||||
QMakeOptions:
|
QMakeOptions:
|
||||||
|
- OS_SAILFISHOS=1
|
||||||
- SAILFIN_VERSION='%{version}-%{release}'
|
- SAILFIN_VERSION='%{version}-%{release}'
|
||||||
|
|
||||||
# For more information about yaml and what's supported in Sailfish OS
|
# For more information about yaml and what's supported in Sailfish OS
|
||||||
|
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 5 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
@ -51,7 +51,7 @@ SilicaItem {
|
||||||
color: "black"
|
color: "black"
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaSource {
|
PlaybackManager {
|
||||||
id: mediaSource
|
id: mediaSource
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
itemId: playerRoot.itemId
|
itemId: playerRoot.itemId
|
|
@ -19,6 +19,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.0
|
import Sailfish.Silica 1.0
|
||||||
|
|
||||||
|
import nl.netsoj.chris.Jellyfin 1.0
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
property var tracks
|
property var tracks
|
||||||
readonly property int audioTrack: audioSelector.currentItem ? audioSelector.currentItem._index : 0
|
readonly property int audioTrack: audioSelector.currentItem ? audioSelector.currentItem._index : 0
|
||||||
|
@ -40,8 +42,8 @@ Column {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: audioModel
|
model: audioModel
|
||||||
MenuItem {
|
MenuItem {
|
||||||
readonly property int _index: model.Index
|
readonly property int _index: model.index
|
||||||
text: model.DisplayTitle
|
text: model.displayTitle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,8 +62,8 @@ Column {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: subtitleModel
|
model: subtitleModel
|
||||||
MenuItem {
|
MenuItem {
|
||||||
readonly property int _index: model.Index
|
readonly property int _index: model.index
|
||||||
text: model.DisplayTitle
|
text: model.displayTitle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,17 +72,22 @@ Column {
|
||||||
onTracksChanged: {
|
onTracksChanged: {
|
||||||
audioModel.clear()
|
audioModel.clear()
|
||||||
subtitleModel.clear()
|
subtitleModel.clear()
|
||||||
if (typeof tracks === "undefined") return
|
if (typeof tracks === "undefined") {
|
||||||
|
console.log("tracks undefined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(tracks)
|
||||||
for(var i = 0; i < tracks.length; i++) {
|
for(var i = 0; i < tracks.length; i++) {
|
||||||
var track = tracks[i];
|
var track = tracks[i];
|
||||||
switch(track.Type) {
|
switch(track.type) {
|
||||||
case "Audio":
|
case MediaStream.Audio:
|
||||||
audioModel.append(track)
|
audioModel.append(track)
|
||||||
break;
|
break;
|
||||||
case "Subtitle":
|
case MediaStream.Subtitle:
|
||||||
subtitleModel.append(track)
|
subtitleModel.append(track)
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
console.log("Ignored " + track.displayTitle + "(" + track.type + ")")
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
@ -73,6 +73,7 @@ Page {
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
console.log("Status changed: " + newStatus, JSON.stringify(jItem))
|
console.log("Status changed: " + newStatus, JSON.stringify(jItem))
|
||||||
|
console.log(jItem.mediaStreams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,28 +83,7 @@ Page {
|
||||||
//appWindow.itemData = ({})
|
//appWindow.itemData = ({})
|
||||||
}
|
}
|
||||||
if (status == PageStatus.Active) {
|
if (status == PageStatus.Active) {
|
||||||
if (itemId) {
|
|
||||||
//ApiClient.fetchItem(itemId)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: ApiClient
|
|
||||||
onItemFetched: {
|
|
||||||
if (itemId === pageRoot.itemId) {
|
|
||||||
//console.log(JSON.stringify(result))
|
|
||||||
//pageRoot.itemData = result
|
|
||||||
pageRoot._loading = false
|
|
||||||
if (status == PageStatus.Active) {
|
|
||||||
if (itemData.Type === "CollectionFolder") {
|
|
||||||
appWindow.collectionId = itemData.Id
|
|
||||||
} else {
|
|
||||||
appWindow.itemData = result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -30,7 +30,7 @@ BaseDetailPage {
|
||||||
UserItemModel {
|
UserItemModel {
|
||||||
id: collectionModel
|
id: collectionModel
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
parentId: itemData.Id || ""
|
parentId: itemData.jellyfinId
|
||||||
sortBy: ["SortName"]
|
sortBy: ["SortName"]
|
||||||
onParentIdChanged: reload()
|
onParentIdChanged: reload()
|
||||||
}
|
}
|
|
@ -26,7 +26,7 @@ import "../../"
|
||||||
|
|
||||||
VideoPage {
|
VideoPage {
|
||||||
subtitle: {
|
subtitle: {
|
||||||
if (typeof itemData.indexNumberEnd !== "undefined") {
|
if (itemData.indexNumberEnd >= 0) {
|
||||||
qsTr("Episode %1–%2 | %3").arg(itemData.indexNumber)
|
qsTr("Episode %1–%2 | %3").arg(itemData.indexNumber)
|
||||||
.arg(itemData.indexNumberEnd)
|
.arg(itemData.indexNumberEnd)
|
||||||
.arg(itemData.seasonName)
|
.arg(itemData.seasonName)
|
|
@ -29,8 +29,8 @@ BaseDetailPage {
|
||||||
ShowEpisodesModel {
|
ShowEpisodesModel {
|
||||||
id: episodeModel
|
id: episodeModel
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
show: itemData.SeriesId
|
show: itemData.seriesId
|
||||||
seasonId: itemData.Id
|
seasonId: itemData.jellyfinId
|
||||||
fields: ["Overview"]
|
fields: ["Overview"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,8 +38,8 @@ BaseDetailPage {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
contentHeight: content.height
|
contentHeight: content.height
|
||||||
header: PageHeader {
|
header: PageHeader {
|
||||||
title: itemData.Name
|
title: itemData.name
|
||||||
description: itemData.SeriesName
|
description: itemData.seriesName
|
||||||
}
|
}
|
||||||
model: episodeModel
|
model: episodeModel
|
||||||
delegate: BackgroundItem {
|
delegate: BackgroundItem {
|
||||||
|
@ -119,10 +119,19 @@ BaseDetailPage {
|
||||||
|
|
||||||
VerticalScrollDecorator {}
|
VerticalScrollDecorator {}
|
||||||
}
|
}
|
||||||
onItemDataChanged: {
|
Connections {
|
||||||
console.log(JSON.stringify(itemData))
|
target: itemData
|
||||||
episodeModel.show = itemData.SeriesId
|
onStatusChanged: {
|
||||||
episodeModel.seasonId = itemData.Id
|
if (itemData.status == JellyfinItem.Ready) {
|
||||||
episodeModel.reload()
|
episodeModel.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status == PageStatus.Active) {
|
||||||
|
console.log(JSON.stringify(itemData))
|
||||||
|
episodeModel.show = itemData.seriesId
|
||||||
|
episodeModel.seasonId = itemData.jellyfinId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,6 +66,7 @@ BaseDetailPage {
|
||||||
id: showSeasonsModel
|
id: showSeasonsModel
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
show: itemData.jellyfinId
|
show: itemData.jellyfinId
|
||||||
|
onShowChanged: reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
SilicaListView {
|
SilicaListView {
|
||||||
|
@ -86,8 +87,17 @@ BaseDetailPage {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onItemDataChanged: {
|
|
||||||
showSeasonsModel.show = itemData.jellyfinId
|
/*onStatusChanged: {
|
||||||
|
if (status == PageStatus.Active) {
|
||||||
showSeasonsModel.reload()
|
showSeasonsModel.reload()
|
||||||
}
|
}
|
||||||
|
}*/
|
||||||
|
Connections {
|
||||||
|
target: itemData
|
||||||
|
onJellyfinIdChanged: {
|
||||||
|
console.log("Item id changed")
|
||||||
|
//showSeasonsModel.show = itemData.jellyfinId
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -46,7 +46,7 @@ BaseDetailPage {
|
||||||
PageHeader {
|
PageHeader {
|
||||||
id: pageHeader
|
id: pageHeader
|
||||||
title: itemData.name
|
title: itemData.name
|
||||||
description: qsTr("Run time: %2").arg(Utils.ticksToText(itemData.RunTimeTicks))
|
description: qsTr("Run time: %2").arg(Utils.ticksToText(itemData.runTimeTicks))
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayToolbar {
|
PlayToolbar {
|
||||||
|
@ -70,4 +70,13 @@ BaseDetailPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: itemData
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status == JellyfinItem.Ready) {
|
||||||
|
console.log(itemData.mediaStreams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
79
sailfish/sailfish.pro
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
# NOTICE:
|
||||||
|
#
|
||||||
|
# Application name defined in TARGET has a corresponding QML filename.
|
||||||
|
# If name defined in TARGET is changed, the following needs to be done
|
||||||
|
# to match new name:
|
||||||
|
# - corresponding QML filename must be changed
|
||||||
|
# - desktop icon filename must be changed
|
||||||
|
# - desktop filename must be changed
|
||||||
|
# - icon definition filename in desktop file must be changed
|
||||||
|
# - translation filenames have to be changed
|
||||||
|
|
||||||
|
# The name of your application
|
||||||
|
TARGET = harbour-sailfin
|
||||||
|
|
||||||
|
#INCLUDEPATH += ../core/include
|
||||||
|
#DEPENDPATH += ../core
|
||||||
|
#LIBS += -Lcore -lcore
|
||||||
|
include(../core/defines.pri)
|
||||||
|
include(../harbour-sailfin.pri)
|
||||||
|
|
||||||
|
# include our shared library and install it
|
||||||
|
|
||||||
|
LIBS += -L$$OUT_PWD/../core/lib -ljellyfin-qt
|
||||||
|
core.files += ../core/lib
|
||||||
|
core.path = /usr/share/$${TARGET}
|
||||||
|
|
||||||
|
INSTALLS += core
|
||||||
|
|
||||||
|
# Other configuration
|
||||||
|
|
||||||
|
CONFIG += sailfishapp # c++17
|
||||||
|
|
||||||
|
DISTFILES += \
|
||||||
|
qml/Constants.qml \
|
||||||
|
qml/Utils.js \
|
||||||
|
qml/components/GlassyBackground.qml \
|
||||||
|
qml/components/IconListItem.qml \
|
||||||
|
qml/components/LibraryItemDelegate.qml \
|
||||||
|
qml/components/MoreSection.qml \
|
||||||
|
qml/components/PlainLabel.qml \
|
||||||
|
qml/components/PlayToolbar.qml \
|
||||||
|
qml/components/RemoteImage.qml \
|
||||||
|
qml/components/Shim.qml \
|
||||||
|
qml/components/UserGridDelegate.qml \
|
||||||
|
qml/components/VideoPlayer.qml \
|
||||||
|
qml/components/VideoTrackSelector.qml \
|
||||||
|
qml/components/itemdetails/SeasonDetails.qml \
|
||||||
|
qml/components/videoplayer/VideoError.qml \
|
||||||
|
qml/components/videoplayer/VideoHud.qml \
|
||||||
|
qml/cover/CoverPage.qml \
|
||||||
|
qml/cover/PosterCover.qml \
|
||||||
|
qml/cover/VideoCover.qml \
|
||||||
|
qml/pages/LegalPage.qml \
|
||||||
|
qml/pages/MainPage.qml \
|
||||||
|
qml/pages/AboutPage.qml \
|
||||||
|
qml/harbour-sailfin.qml \
|
||||||
|
qml/pages/SettingsPage.qml \
|
||||||
|
qml/pages/VideoPage.qml \
|
||||||
|
qml/pages/itemdetails/BaseDetailPage.qml \
|
||||||
|
qml/pages/itemdetails/CollectionPage.qml \
|
||||||
|
qml/pages/itemdetails/EpisodePage.qml \
|
||||||
|
qml/pages/itemdetails/FilmPage.qml \
|
||||||
|
qml/pages/itemdetails/MusicAlbumPage.qml \
|
||||||
|
qml/pages/itemdetails/SeasonPage.qml \
|
||||||
|
qml/pages/itemdetails/SeriesPage.qml \
|
||||||
|
qml/pages/itemdetails/UnsupportedPage.qml \
|
||||||
|
qml/pages/itemdetails/VideoPage.qml \
|
||||||
|
qml/pages/setup/AddServerConnectingPage.qml \
|
||||||
|
qml/pages/setup/LoginDialog.qml \
|
||||||
|
qml/qmldir
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
src/harbour-sailfin.cpp
|
||||||
|
|
||||||
|
SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172
|
||||||
|
|
||||||
|
# to disable building translations every time, comment out the
|
||||||
|
# following CONFIG line
|
||||||
|
CONFIG += sailfishapp_i18n
|
|
@ -28,28 +28,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
#include <sailfishapp.h>
|
#include <sailfishapp.h>
|
||||||
|
|
||||||
#include "jellyfinapiclient.h"
|
#include <jellyfin.h>
|
||||||
#include "jellyfinapimodel.h"
|
|
||||||
#include "jellyfinitem.h"
|
|
||||||
#include "jellyfinmediasource.h"
|
|
||||||
#include "serverdiscoverymodel.h"
|
|
||||||
|
|
||||||
|
|
||||||
void registerQml() {
|
|
||||||
const char* QML_NAMESPACE = "nl.netsoj.chris.Jellyfin";
|
|
||||||
// Singletons are perhaps bad, but they are convenient :)
|
|
||||||
qmlRegisterSingletonType<Jellyfin::ApiClient>(QML_NAMESPACE, 1, 0, "ApiClient", [](QQmlEngine *eng, QJSEngine *js) {
|
|
||||||
Q_UNUSED(eng)
|
|
||||||
Q_UNUSED(js)
|
|
||||||
return dynamic_cast<QObject*>(new Jellyfin::ApiClient());
|
|
||||||
});
|
|
||||||
qmlRegisterType<Jellyfin::ServerDiscoveryModel>(QML_NAMESPACE, 1, 0, "ServerDiscoveryModel");
|
|
||||||
qmlRegisterType<Jellyfin::MediaSource>(QML_NAMESPACE, 1, 0, "MediaSource");
|
|
||||||
|
|
||||||
// API models
|
|
||||||
Jellyfin::registerModels(QML_NAMESPACE);
|
|
||||||
Jellyfin::registerSerializableJsonTypes(QML_NAMESPACE);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
// SailfishApp::main() will display "qml/harbour-sailfin.qml", if you need more
|
// SailfishApp::main() will display "qml/harbour-sailfin.qml", if you need more
|
||||||
|
@ -62,8 +41,7 @@ int main(int argc, char *argv[]) {
|
||||||
//
|
//
|
||||||
// To display the view, call "show()" (will show fullscreen on device).
|
// To display the view, call "show()" (will show fullscreen on device).
|
||||||
QGuiApplication *app = SailfishApp::application(argc, argv);
|
QGuiApplication *app = SailfishApp::application(argc, argv);
|
||||||
registerQml();
|
Jellyfin::registerTypes();
|
||||||
|
|
||||||
QQuickView *view = SailfishApp::createView();
|
QQuickView *view = SailfishApp::createView();
|
||||||
view->setSource(SailfishApp::pathToMainQml());
|
view->setSource(SailfishApp::pathToMainQml());
|
||||||
view->show();
|
view->show();
|
|
@ -141,13 +141,6 @@
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
|
||||||
<name>Jellyfin::Item</name>
|
|
||||||
<message>
|
|
||||||
<source>Invalid response from the server: root element is not an object.</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
</context>
|
|
||||||
<context>
|
<context>
|
||||||
<name>LegalPage</name>
|
<name>LegalPage</name>
|
||||||
<message>
|
<message>
|