diff --git a/core/core.pro b/core/core.pro new file mode 100644 index 0000000..d631cdd --- /dev/null +++ b/core/core.pro @@ -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 diff --git a/core/defines.pri b/core/defines.pri new file mode 100644 index 0000000..0ac4f26 --- /dev/null +++ b/core/defines.pri @@ -0,0 +1,2 @@ +message(Including $$_FILE_ from $$IN_PWD) +INCLUDEPATH += $$IN_PWD/include diff --git a/src/credentialmanager.h b/core/include/credentialmanager.h similarity index 100% rename from src/credentialmanager.h rename to core/include/credentialmanager.h diff --git a/core/include/jellyfin.h b/core/include/jellyfin.h new file mode 100644 index 0000000..ccfed30 --- /dev/null +++ b/core/include/jellyfin.h @@ -0,0 +1,16 @@ +#ifndef JELLYFIN_H +#define JELLYFIN_H + +#include + +#include "jellyfinapiclient.h" +#include "jellyfinapimodel.h" +#include "jellyfinitem.h" +#include "serverdiscoverymodel.h" +#include "jellyfinplaybackmanager.h" + +namespace Jellyfin { +void registerTypes(); +} + +#endif // JELLYFIN_H diff --git a/src/jellyfinapiclient.h b/core/include/jellyfinapiclient.h similarity index 98% rename from src/jellyfinapiclient.h rename to core/include/jellyfinapiclient.h index c1df53f..4d8635a 100644 --- a/src/jellyfinapiclient.h +++ b/core/include/jellyfinapiclient.h @@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include #include +#include #include #include #include @@ -68,7 +69,6 @@ class WebSocket; * These steps might change. I'm considering decoupling CredentialsManager from this class to clean some code up. */ class ApiClient : public QObject { - friend class MediaSource; friend class WebSocket; Q_OBJECT public: @@ -105,7 +105,19 @@ public: QString &userId() { return m_userId; } QJsonObject &deviceProfile() { return m_deviceProfile; } 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(&QNetworkReply::error), + this, &ApiClient::defaultNetworkErrorHandler); + } signals: /* * Emitted when the server requires authentication. Please authenticate your user via authenticate. @@ -193,7 +205,6 @@ protected: private: QNetworkAccessManager m_naManager; - const char *m_version = SAILFIN_VERSION; /* * State information */ @@ -238,17 +249,6 @@ private: 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(&QNetworkReply::error), - this, &ApiClient::defaultNetworkErrorHandler); - } }; } // NS Jellyfin diff --git a/src/jellyfinapimodel.h b/core/include/jellyfinapimodel.h similarity index 99% rename from src/jellyfinapimodel.h rename to core/include/jellyfinapimodel.h index ab6136d..fde3196 100644 --- a/src/jellyfinapimodel.h +++ b/core/include/jellyfinapimodel.h @@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include #include #include +#include #include #include "jellyfinapiclient.h" diff --git a/src/jellyfindeviceprofile.h b/core/include/jellyfindeviceprofile.h similarity index 100% rename from src/jellyfindeviceprofile.h rename to core/include/jellyfindeviceprofile.h diff --git a/src/jellyfinitem.h b/core/include/jellyfinitem.h similarity index 67% rename from src/jellyfinitem.h rename to core/include/jellyfinitem.h index fb58587..d970f6d 100644 --- a/src/jellyfinitem.h +++ b/core/include/jellyfinitem.h @@ -11,13 +11,15 @@ #include #include #include +#include +#include #include -#include - #include +#include + #include "jellyfinapiclient.h" namespace Jellyfin { @@ -32,6 +34,7 @@ class JsonSerializable : public QObject { Q_OBJECT public: Q_INVOKABLE JsonSerializable(QObject *parent); + /** * @brief Sets this objects properties based on obj. * @param obj The data to load into this object. @@ -39,7 +42,7 @@ public: void deserialize(const QJsonObject &obj); QJsonObject serialize() const; 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; /** @@ -54,8 +57,15 @@ private: * @return THe modified string */ static QString toPascalCase(QString str); + + static const QRegularExpression m_listExpression; + /** + * @brief Qt is doing weird. I'll keep track of the metatypes myself. + */ + QHash m_nameMetatypeMap; }; + /** * @brief An "interface" for a remote data source * @@ -108,6 +118,45 @@ private: 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 { Q_OBJECT public: @@ -146,11 +195,21 @@ public: Q_PROPERTY(QDateTime premiereData MEMBER m_premiereDate NOTIFY premiereDateChanged) //SKIP: ExternalUrls //SKIP: MediaSources + Q_PROPERTY(float criticRating READ criticRating WRITE setCriticRating NOTIFY criticRatingChanged) + Q_PROPERTY(QStringList productionLocations MEMBER m_productionLocations NOTIFY productionLocationsChanged) // Handpicked, important ones + Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks WRITE setRunTimeTicks NOTIFY runTimeTicksChanged) Q_PROPERTY(QString overview MEMBER m_overview NOTIFY overviewChanged) 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 __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; } void setJellyfinId(QString newId); @@ -170,12 +229,24 @@ public: void setHasSubtitles(bool newHasSubtitles) { m_hasSubtitles = newHasSubtitles; emit hasSubtitlesChanged(newHasSubtitles); } bool supportsSync() const { return m_supportsSync.value_or(false); } 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 + 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); } - void setProductionYear(int newProductionYear) { m_productionYear = newProductionYear; emit productionYearChanged(newProductionYear); } + void setProductionYear(int newProductionYear) { m_productionYear = std::optional(newProductionYear); emit productionYearChanged(newProductionYear); } 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(newIndexNumber); emit indexNumberChanged(newIndexNumber); } + int indexNumberEnd() const { return m_indexNumberEnd.value_or(-1); } + void setIndexNumberEnd(int newIndexNumberEnd) { m_indexNumberEnd = std::optional(newIndexNumberEnd); emit indexNumberEndChanged(newIndexNumberEnd); } + bool isFolder() const { return m_isFolder.value_or(false); } + void setIsFolder(bool newIsFolder) { m_isFolder = newIsFolder; emit isFolderChanged(newIsFolder); } + + //QQmlListProperty mediaStreams() { return toReadOnlyQmlListProperty(m_mediaStreams); } + //QList mediaStreams() { return *reinterpret_cast *>(&m_mediaStreams); } + QVariantList mediaStreams() { QVariantList l; for (auto e: m_mediaStreams) l.append(QVariant::fromValue(e)); return l;} signals: void jellyfinIdChanged(const QString &newId); @@ -201,11 +272,20 @@ signals: void sortNameChanged(const QString &newSortName); void forcedSortNameChanged(const QString &newForcedSortName); void premiereDateChanged(QDateTime newPremiereDate); + void criticRatingChanged(float newCriticRating); + void productionLocationsChanged(QStringList newProductionLocations); // Handpicked, important ones + void runTimeTicksChanged(qint64 newRunTimeTicks); void overviewChanged(const QString &newOverview); void productionYearChanged(int newProductionYear); 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 &newMediaStreams*/); public slots: /** @@ -236,11 +316,36 @@ protected: QString m_sortName; QString m_forcedSortName; QDateTime m_premiereDate; + std::optional m_criticRating = std::nullopt; + QStringList m_productionLocations; // Handpicked, important ones + std::optional m_runTimeTicks = std::nullopt; QString m_overview; std::optional m_productionYear = std::nullopt; std::optional m_indexNumber = std::nullopt; + std::optional m_indexNumberEnd = std::nullopt; + std::optional m_isFolder = std::nullopt; + QString m_type; + QString m_seriesName; + QString m_seasonName; + QList __list__m_mediaStreams; + QVariantList m_mediaStreams; + + template + QQmlListProperty toReadOnlyQmlListProperty(QList &list) { + return QQmlListProperty(this, std::addressof(list), &qlist_count, &qlist_at); + } + + template + static int qlist_count(QQmlListProperty *p) { + return reinterpret_cast *>(p->data)->count(); + } + + template + static T *qlist_at(QQmlListProperty *p, int idx) { + return reinterpret_cast *>(p->data)->at(idx); + } }; void registerSerializableJsonTypes(const char* URI); diff --git a/src/jellyfinmediasource.h b/core/include/jellyfinplaybackmanager.h similarity index 97% rename from src/jellyfinmediasource.h rename to core/include/jellyfinplaybackmanager.h index 8761c15..04f9f82 100644 --- a/src/jellyfinmediasource.h +++ b/core/include/jellyfinplaybackmanager.h @@ -34,7 +34,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA namespace Jellyfin { -class MediaSource : public QObject { +class PlaybackManager : public QObject { Q_OBJECT public: enum PlayMethod { @@ -44,7 +44,7 @@ public: }; Q_ENUM(PlayMethod) - explicit MediaSource(QObject *parent = nullptr); + explicit PlaybackManager(QObject *parent = nullptr); Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient) Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged) Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged) diff --git a/src/jellyfinwebsocket.h b/core/include/jellyfinwebsocket.h similarity index 98% rename from src/jellyfinwebsocket.h rename to core/include/jellyfinwebsocket.h index 3b9804c..781ec88 100644 --- a/src/jellyfinwebsocket.h +++ b/core/include/jellyfinwebsocket.h @@ -55,6 +55,7 @@ public slots: private slots: void textMessageReceived(const QString &message); void onConnected(); + void onDisconnected(); void sendKeepAlive(); signals: diff --git a/src/serverdiscoverymodel.h b/core/include/serverdiscoverymodel.h similarity index 100% rename from src/serverdiscoverymodel.h rename to core/include/serverdiscoverymodel.h index 64f7071..95b8165 100644 --- a/src/serverdiscoverymodel.h +++ b/core/include/serverdiscoverymodel.h @@ -76,8 +76,8 @@ private: const QByteArray MAGIC_PACKET = "who is JellyfinServer?"; const quint16 BROADCAST_PORT = 7359; - QUdpSocket m_socket; std::vector m_discoveredServers; + QUdpSocket m_socket; }; } #endif //SERVER_DISCOVERY_MODEL_H diff --git a/src/credentialmanager.cpp b/core/src/credentialmanager.cpp similarity index 100% rename from src/credentialmanager.cpp rename to core/src/credentialmanager.cpp diff --git a/core/src/jellyfin.cpp b/core/src/jellyfin.cpp new file mode 100644 index 0000000..a26e86c --- /dev/null +++ b/core/src/jellyfin.cpp @@ -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(QML_NAMESPACE, 1, 0, "ApiClient", [](QQmlEngine *eng, QJSEngine *js) { + Q_UNUSED(eng) + Q_UNUSED(js) + return dynamic_cast(new Jellyfin::ApiClient()); + }); + qmlRegisterType(QML_NAMESPACE, 1, 0, "ServerDiscoveryModel"); + qmlRegisterType(QML_NAMESPACE, 1, 0, "PlaybackManager"); + + // API models + Jellyfin::registerModels(QML_NAMESPACE); + Jellyfin::registerSerializableJsonTypes(QML_NAMESPACE); +} +} diff --git a/src/jellyfinapiclient.cpp b/core/src/jellyfinapiclient.cpp similarity index 98% rename from src/jellyfinapiclient.cpp rename to core/src/jellyfinapiclient.cpp index f8a339a..ef1ab01 100644 --- a/src/jellyfinapiclient.cpp +++ b/core/src/jellyfinapiclient.cpp @@ -20,6 +20,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include "jellyfinapiclient.h" namespace Jellyfin { + ApiClient::ApiClient(QObject *parent) : QObject(parent), m_webSocket(new WebSocket(this)) { @@ -30,6 +31,10 @@ ApiClient::ApiClient(QObject *parent) generateDeviceProfile(); } +QString ApiClient::version() const { + return QString(SAILFIN_VERSION); +} + //////////////////////////////////////////////////////////////////////////////////////////////////// // BASE HTTP METHODS // //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -38,7 +43,7 @@ ApiClient::ApiClient(QObject *parent) void ApiClient::addBaseRequestHeaders(QNetworkRequest &request, const QString &path, const QUrlQuery ¶ms) { addTokenHeader(request); 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; if (!params.isEmpty()) url += "?" + params.toString(); request.setUrl(url); @@ -49,7 +54,7 @@ void ApiClient::addTokenHeader(QNetworkRequest &request) { authentication += "Client=\"Sailfin\""; authentication += ", Device=\"" + m_deviceName + "\""; authentication += ", DeviceId=\"" + m_deviceId + "\""; - authentication += ", Version=\"" + QString(m_version) + "\""; + authentication += ", Version=\"" + version() + "\""; if (m_authenticated) { authentication += ", token=\"" + m_token + "\""; } diff --git a/src/jellyfinapimodel.cpp b/core/src/jellyfinapimodel.cpp similarity index 100% rename from src/jellyfinapimodel.cpp rename to core/src/jellyfinapimodel.cpp diff --git a/src/jellyfindeviceprofile.cpp b/core/src/jellyfindeviceprofile.cpp similarity index 100% rename from src/jellyfindeviceprofile.cpp rename to core/src/jellyfindeviceprofile.cpp diff --git a/src/jellyfinitem.cpp b/core/src/jellyfinitem.cpp similarity index 60% rename from src/jellyfinitem.cpp rename to core/src/jellyfinitem.cpp index 51ced56..20a91b1 100644 --- a/src/jellyfinitem.cpp +++ b/core/src/jellyfinitem.cpp @@ -1,7 +1,9 @@ #include "jellyfinitem.h" 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) { const QMetaObject *obj = this->metaObject(); @@ -21,13 +23,29 @@ void JsonSerializable::deserialize(const QJsonObject &jObj) { } else if (jObj.contains(toPascalCase(prop.name()))) { QJsonValue val = jObj[toPascalCase(prop.name())]; 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 + // we need to explicitly cast them to QList or QQmlListProperty, which sucks. + // That's why we have a special case for properties starting with __list__. This contains + // the actual QList, 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 { 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()) { case QJsonValue::Null: case QJsonValue::Undefined: @@ -37,25 +55,48 @@ QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &v case QJsonValue::String: return val.toVariant(); case QJsonValue::Array: - if (prop.type() == QVariant::List) { + { QJsonArray arr = val.toArray(); QVariantList varArr; - for (auto it = arr.begin(); it < arr.end(); it++) { - varArr << jsonToVariant(prop, *it, root); + for (auto it = arr.constBegin(); it < arr.constEnd(); it++) { + QVariant variant = jsonToVariant(prop, *it, root); + qDebug() << variant; + varArr.append(variant); } + qDebug() << prop.name() << ": " << varArr.count(); return QVariant(varArr); - } else { - qDebug() << prop.name() << " is not a " << prop.typeName(); - return QVariant(); } case QJsonValue::Object: 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(deserializedInnerObj)) { qDebug() << "Deserializing user type " << deserializedInnerObj->metaObject()->className(); ser->deserialize(innerObj); - return QVariant::fromValue(ser); + return QVariant(typeNo, &ser); } else { + deserializedInnerObj->deleteLater(); qDebug() << "Object is not a serializable one!"; return QVariant(); } @@ -146,10 +187,28 @@ void RemoteData::setApiClient(ApiClient *newApiClient) { 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(QObject *parent) : RemoteData(parent) { -} +Item::Item(QObject *parent) : RemoteData(parent) {} void Item::setJellyfinId(QString newId) { @@ -172,7 +231,9 @@ void Item::reload() { rep->deleteLater(); 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()) { this->setError(QNetworkReply::ProtocolFailure); this->setErrorString(error.errorString()); @@ -196,6 +257,7 @@ void Item::reload() { } void registerSerializableJsonTypes(const char* URI) { + qmlRegisterType(URI, 1, 0, "MediaStream"); qmlRegisterType(URI, 1, 0, "JellyfinItem"); } } diff --git a/src/jellyfinmediasource.cpp b/core/src/jellyfinplaybackmanager.cpp similarity index 89% rename from src/jellyfinmediasource.cpp rename to core/src/jellyfinplaybackmanager.cpp index 8bc3027..8347c44 100644 --- a/src/jellyfinmediasource.cpp +++ b/core/src/jellyfinplaybackmanager.cpp @@ -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 */ -#include "jellyfinmediasource.h" +#include "jellyfinplaybackmanager.h" namespace Jellyfin { -MediaSource::MediaSource(QObject *parent) +PlaybackManager::PlaybackManager(QObject *parent) : QObject(parent) { m_updateTimer.setInterval(10000); // 10 seconds 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; params.addQueryItem("UserId", m_apiClient->userId()); 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) { qWarning() << "apiClient is not set on this MediaSource instance! Aborting."; 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; emit streamUrlChanged(streamUrl); } -void MediaSource::setPosition(qint64 position) { +void PlaybackManager::setPosition(qint64 position) { if (position == 0 && m_position != 0) { // 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 @@ -92,7 +92,7 @@ void MediaSource::setPosition(qint64 position) { emit positionChanged(position); } -void MediaSource::setState(QMediaPlayer::State newState) { +void PlaybackManager::setState(QMediaPlayer::State newState) { if (m_state == newState) return; if (m_state == QMediaPlayer::StoppedState) { // We're transitioning from stopped to either playing or paused. @@ -112,11 +112,11 @@ void MediaSource::setState(QMediaPlayer::State newState) { emit this->stateChanged(newState); } -void MediaSource::updatePlaybackInfo() { +void PlaybackManager::updatePlaybackInfo() { postPlaybackInfo(Progress); } -void MediaSource::postPlaybackInfo(PlaybackInfoType type) { +void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) { QJsonObject root; root["ItemId"] = m_itemId; diff --git a/src/jellyfinwebsocket.cpp b/core/src/jellyfinwebsocket.cpp similarity index 93% rename from src/jellyfinwebsocket.cpp rename to core/src/jellyfinwebsocket.cpp index 354ffc6..44afcc5 100644 --- a/src/jellyfinwebsocket.cpp +++ b/core/src/jellyfinwebsocket.cpp @@ -22,6 +22,7 @@ namespace Jellyfin { WebSocket::WebSocket(ApiClient *client) : QObject (client), m_apiClient(client){ connect(&m_webSocket, &QWebSocket::connected, this, &WebSocket::onConnected); + connect(&m_webSocket, &QWebSocket::disconnected, this, &WebSocket::onDisconnected); connect(&m_webSocket, static_cast(&QWebSocket::error), this, [this](QAbstractSocket::SocketError error) { Q_UNUSED(error) @@ -45,6 +46,11 @@ void WebSocket::onConnected() { 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) { QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8()); if (doc.isNull() || !doc.isObject()) { diff --git a/src/serverdiscoverymodel.cpp b/core/src/serverdiscoverymodel.cpp similarity index 100% rename from src/serverdiscoverymodel.cpp rename to core/src/serverdiscoverymodel.cpp diff --git a/desktop/.qmake.stash b/desktop/.qmake.stash new file mode 100644 index 0000000..febeb7a --- /dev/null +++ b/desktop/.qmake.stash @@ -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 diff --git a/desktop/desktop.pro b/desktop/desktop.pro new file mode 100644 index 0000000..941e384 --- /dev/null +++ b/desktop/desktop.pro @@ -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 diff --git a/desktop/src/main.cpp b/desktop/src/main.cpp new file mode 100644 index 0000000..77b8e94 --- /dev/null +++ b/desktop/src/main.cpp @@ -0,0 +1,7 @@ +#include + +int main(int argc, char** argv) { + QGuiApplication app(argc, argv); + + return app.exec(); +} diff --git a/harbour-sailfin.pri b/harbour-sailfin.pri new file mode 100644 index 0000000..f414e4a --- /dev/null +++ b/harbour-sailfin.pri @@ -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\\\"\"" diff --git a/harbour-sailfin.pro b/harbour-sailfin.pro index 7a4fdb5..785d1ad 100644 --- a/harbour-sailfin.pro +++ b/harbour-sailfin.pro @@ -1,85 +1,20 @@ -# 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 +TEMPLATE = subdirs +SUBDIRS = core -# The name of your application -TARGET = harbour-sailfin +core.subdir = core -QT += multimedia websockets - -CONFIG += sailfishapp # c++17 -QMAKE_CXXFLAGS += -std=c++17 - -# Help, something keeps eating my quotes and backslashes - -!defined(SAILFIN_VERSION, var) { - SAILFIN_VERSION = "(UNKNOWN VERSION)" +defined(OS_SAILFISHOS, var){ + SUBDIRS += sailfish + sailfish.subdir = sailfish + sailfish.depends = core +} +defined(OS_DESKTOP, var) { + SUBDIRS += desktop + desktop.subdir = desktop + desktop.depends = core } -DEFINES += "SAILFIN_VERSION=\"\\\"$$SAILFIN_VERSION\\\"\"" - -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 +message($$SUBDIRS) # German translation is enabled as an example. If you aren't # 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. # 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 + diff --git a/rpm/harbour-sailfin.yaml b/rpm/harbour-sailfin.yaml index bd69e09..b3fbbc6 100644 --- a/rpm/harbour-sailfin.yaml +++ b/rpm/harbour-sailfin.yaml @@ -7,13 +7,12 @@ Release: 1 Group: Qt/Qt URL: https://chris.netsoj.nl/projects/harbour-sailfin License: LGPL-2.0-or-later -# This must be generated before uploading a package to a remote build service. -# Usually this line does not need to be modified. +# This must be generated before uploading a package to a remote build service. Usually this line does not need to be modified. Sources: - '%{name}-%{version}.tar.bz2' Description: | 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 is the preferred way of specifying build dependencies for your package. @@ -39,8 +38,13 @@ Files: - '%{_datadir}/%{name}' - '%{_datadir}/applications/%{name}.desktop' - '%{_datadir}/icons/hicolor/*/apps/%{name}.png' + +Macros: + - '__provides_exclude_from; ^%{_datadir}/.*$' + - '__requires_exclude; ^libjellyfin-qt.*$' QMakeOptions: + - OS_SAILFISHOS=1 - SAILFIN_VERSION='%{version}-%{release}' # For more information about yaml and what's supported in Sailfish OS diff --git a/harbour-sailfin.desktop b/sailfish/harbour-sailfin.desktop similarity index 100% rename from harbour-sailfin.desktop rename to sailfish/harbour-sailfin.desktop diff --git a/icon.svg b/sailfish/icon.svg similarity index 100% rename from icon.svg rename to sailfish/icon.svg diff --git a/icons/108x108/harbour-sailfin.png b/sailfish/icons/108x108/harbour-sailfin.png similarity index 100% rename from icons/108x108/harbour-sailfin.png rename to sailfish/icons/108x108/harbour-sailfin.png diff --git a/icons/128x128/harbour-sailfin.png b/sailfish/icons/128x128/harbour-sailfin.png similarity index 100% rename from icons/128x128/harbour-sailfin.png rename to sailfish/icons/128x128/harbour-sailfin.png diff --git a/icons/172x172/harbour-sailfin.png b/sailfish/icons/172x172/harbour-sailfin.png similarity index 100% rename from icons/172x172/harbour-sailfin.png rename to sailfish/icons/172x172/harbour-sailfin.png diff --git a/icons/86x86/harbour-sailfin.png b/sailfish/icons/86x86/harbour-sailfin.png similarity index 100% rename from icons/86x86/harbour-sailfin.png rename to sailfish/icons/86x86/harbour-sailfin.png diff --git a/qml/3rdparty.xml b/sailfish/qml/3rdparty.xml similarity index 100% rename from qml/3rdparty.xml rename to sailfish/qml/3rdparty.xml diff --git a/qml/Constants.qml b/sailfish/qml/Constants.qml similarity index 100% rename from qml/Constants.qml rename to sailfish/qml/Constants.qml diff --git a/qml/Utils.js b/sailfish/qml/Utils.js similarity index 100% rename from qml/Utils.js rename to sailfish/qml/Utils.js diff --git a/qml/components/GlassyBackground.qml b/sailfish/qml/components/GlassyBackground.qml similarity index 100% rename from qml/components/GlassyBackground.qml rename to sailfish/qml/components/GlassyBackground.qml diff --git a/qml/components/IconListItem.qml b/sailfish/qml/components/IconListItem.qml similarity index 100% rename from qml/components/IconListItem.qml rename to sailfish/qml/components/IconListItem.qml diff --git a/qml/components/LibraryItemDelegate.qml b/sailfish/qml/components/LibraryItemDelegate.qml similarity index 100% rename from qml/components/LibraryItemDelegate.qml rename to sailfish/qml/components/LibraryItemDelegate.qml diff --git a/qml/components/MoreSection.qml b/sailfish/qml/components/MoreSection.qml similarity index 100% rename from qml/components/MoreSection.qml rename to sailfish/qml/components/MoreSection.qml diff --git a/qml/components/PlainLabel.qml b/sailfish/qml/components/PlainLabel.qml similarity index 100% rename from qml/components/PlainLabel.qml rename to sailfish/qml/components/PlainLabel.qml diff --git a/qml/components/PlayToolbar.qml b/sailfish/qml/components/PlayToolbar.qml similarity index 100% rename from qml/components/PlayToolbar.qml rename to sailfish/qml/components/PlayToolbar.qml diff --git a/qml/components/RemoteImage.qml b/sailfish/qml/components/RemoteImage.qml similarity index 100% rename from qml/components/RemoteImage.qml rename to sailfish/qml/components/RemoteImage.qml diff --git a/qml/components/Shim.qml b/sailfish/qml/components/Shim.qml similarity index 100% rename from qml/components/Shim.qml rename to sailfish/qml/components/Shim.qml diff --git a/qml/components/UserGridDelegate.qml b/sailfish/qml/components/UserGridDelegate.qml similarity index 100% rename from qml/components/UserGridDelegate.qml rename to sailfish/qml/components/UserGridDelegate.qml diff --git a/qml/components/VideoPlayer.qml b/sailfish/qml/components/VideoPlayer.qml similarity index 99% rename from qml/components/VideoPlayer.qml rename to sailfish/qml/components/VideoPlayer.qml index 340bcf5..79ed01b 100644 --- a/qml/components/VideoPlayer.qml +++ b/sailfish/qml/components/VideoPlayer.qml @@ -51,7 +51,7 @@ SilicaItem { color: "black" } - MediaSource { + PlaybackManager { id: mediaSource apiClient: ApiClient itemId: playerRoot.itemId diff --git a/qml/components/VideoTrackSelector.qml b/sailfish/qml/components/VideoTrackSelector.qml similarity index 79% rename from qml/components/VideoTrackSelector.qml rename to sailfish/qml/components/VideoTrackSelector.qml index ca9eff4..92931a7 100644 --- a/qml/components/VideoTrackSelector.qml +++ b/sailfish/qml/components/VideoTrackSelector.qml @@ -19,6 +19,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import QtQuick 2.6 import Sailfish.Silica 1.0 +import nl.netsoj.chris.Jellyfin 1.0 + Column { property var tracks readonly property int audioTrack: audioSelector.currentItem ? audioSelector.currentItem._index : 0 @@ -40,8 +42,8 @@ Column { Repeater { model: audioModel MenuItem { - readonly property int _index: model.Index - text: model.DisplayTitle + readonly property int _index: model.index + text: model.displayTitle } } } @@ -60,8 +62,8 @@ Column { Repeater { model: subtitleModel MenuItem { - readonly property int _index: model.Index - text: model.DisplayTitle + readonly property int _index: model.index + text: model.displayTitle } } } @@ -70,17 +72,22 @@ Column { onTracksChanged: { audioModel.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++) { var track = tracks[i]; - switch(track.Type) { - case "Audio": + switch(track.type) { + case MediaStream.Audio: audioModel.append(track) break; - case "Subtitle": + case MediaStream.Subtitle: subtitleModel.append(track) break; default: + console.log("Ignored " + track.displayTitle + "(" + track.type + ")") break; } } diff --git a/qml/components/videoplayer/VideoError.qml b/sailfish/qml/components/videoplayer/VideoError.qml similarity index 100% rename from qml/components/videoplayer/VideoError.qml rename to sailfish/qml/components/videoplayer/VideoError.qml diff --git a/qml/components/videoplayer/VideoHud.qml b/sailfish/qml/components/videoplayer/VideoHud.qml similarity index 100% rename from qml/components/videoplayer/VideoHud.qml rename to sailfish/qml/components/videoplayer/VideoHud.qml diff --git a/qml/cover/CoverPage.qml b/sailfish/qml/cover/CoverPage.qml similarity index 100% rename from qml/cover/CoverPage.qml rename to sailfish/qml/cover/CoverPage.qml diff --git a/qml/cover/PosterCover.qml b/sailfish/qml/cover/PosterCover.qml similarity index 100% rename from qml/cover/PosterCover.qml rename to sailfish/qml/cover/PosterCover.qml diff --git a/qml/cover/VideoCover.qml b/sailfish/qml/cover/VideoCover.qml similarity index 100% rename from qml/cover/VideoCover.qml rename to sailfish/qml/cover/VideoCover.qml diff --git a/qml/harbour-sailfin.qml b/sailfish/qml/harbour-sailfin.qml similarity index 100% rename from qml/harbour-sailfin.qml rename to sailfish/qml/harbour-sailfin.qml diff --git a/qml/icon.png b/sailfish/qml/icon.png similarity index 100% rename from qml/icon.png rename to sailfish/qml/icon.png diff --git a/qml/licenses/MIT.txt b/sailfish/qml/licenses/MIT.txt similarity index 100% rename from qml/licenses/MIT.txt rename to sailfish/qml/licenses/MIT.txt diff --git a/qml/licenses/lgpl-2.1.html b/sailfish/qml/licenses/lgpl-2.1.html similarity index 100% rename from qml/licenses/lgpl-2.1.html rename to sailfish/qml/licenses/lgpl-2.1.html diff --git a/qml/pages/AboutPage.qml b/sailfish/qml/pages/AboutPage.qml similarity index 100% rename from qml/pages/AboutPage.qml rename to sailfish/qml/pages/AboutPage.qml diff --git a/qml/pages/LegalPage.qml b/sailfish/qml/pages/LegalPage.qml similarity index 100% rename from qml/pages/LegalPage.qml rename to sailfish/qml/pages/LegalPage.qml diff --git a/qml/pages/MainPage.qml b/sailfish/qml/pages/MainPage.qml similarity index 100% rename from qml/pages/MainPage.qml rename to sailfish/qml/pages/MainPage.qml diff --git a/qml/pages/SettingsPage.qml b/sailfish/qml/pages/SettingsPage.qml similarity index 100% rename from qml/pages/SettingsPage.qml rename to sailfish/qml/pages/SettingsPage.qml diff --git a/qml/pages/VideoPage.qml b/sailfish/qml/pages/VideoPage.qml similarity index 100% rename from qml/pages/VideoPage.qml rename to sailfish/qml/pages/VideoPage.qml diff --git a/qml/pages/itemdetails/BaseDetailPage.qml b/sailfish/qml/pages/itemdetails/BaseDetailPage.qml similarity index 82% rename from qml/pages/itemdetails/BaseDetailPage.qml rename to sailfish/qml/pages/itemdetails/BaseDetailPage.qml index 174afe4..a0b9b5b 100644 --- a/qml/pages/itemdetails/BaseDetailPage.qml +++ b/sailfish/qml/pages/itemdetails/BaseDetailPage.qml @@ -73,6 +73,7 @@ Page { apiClient: ApiClient onStatusChanged: { console.log("Status changed: " + newStatus, JSON.stringify(jItem)) + console.log(jItem.mediaStreams) } } @@ -82,28 +83,7 @@ Page { //appWindow.itemData = ({}) } 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 - } - } - } - } - } } diff --git a/qml/pages/itemdetails/CollectionPage.qml b/sailfish/qml/pages/itemdetails/CollectionPage.qml similarity index 99% rename from qml/pages/itemdetails/CollectionPage.qml rename to sailfish/qml/pages/itemdetails/CollectionPage.qml index 284599c..47eacc5 100644 --- a/qml/pages/itemdetails/CollectionPage.qml +++ b/sailfish/qml/pages/itemdetails/CollectionPage.qml @@ -30,7 +30,7 @@ BaseDetailPage { UserItemModel { id: collectionModel apiClient: ApiClient - parentId: itemData.Id || "" + parentId: itemData.jellyfinId sortBy: ["SortName"] onParentIdChanged: reload() } diff --git a/qml/pages/itemdetails/EpisodePage.qml b/sailfish/qml/pages/itemdetails/EpisodePage.qml similarity index 95% rename from qml/pages/itemdetails/EpisodePage.qml rename to sailfish/qml/pages/itemdetails/EpisodePage.qml index e167140..15d5849 100644 --- a/qml/pages/itemdetails/EpisodePage.qml +++ b/sailfish/qml/pages/itemdetails/EpisodePage.qml @@ -26,7 +26,7 @@ import "../../" VideoPage { subtitle: { - if (typeof itemData.indexNumberEnd !== "undefined") { + if (itemData.indexNumberEnd >= 0) { qsTr("Episode %1–%2 | %3").arg(itemData.indexNumber) .arg(itemData.indexNumberEnd) .arg(itemData.seasonName) diff --git a/qml/pages/itemdetails/FilmPage.qml b/sailfish/qml/pages/itemdetails/FilmPage.qml similarity index 100% rename from qml/pages/itemdetails/FilmPage.qml rename to sailfish/qml/pages/itemdetails/FilmPage.qml diff --git a/qml/pages/itemdetails/MusicAlbumPage.qml b/sailfish/qml/pages/itemdetails/MusicAlbumPage.qml similarity index 100% rename from qml/pages/itemdetails/MusicAlbumPage.qml rename to sailfish/qml/pages/itemdetails/MusicAlbumPage.qml diff --git a/qml/pages/itemdetails/SeasonPage.qml b/sailfish/qml/pages/itemdetails/SeasonPage.qml similarity index 89% rename from qml/pages/itemdetails/SeasonPage.qml rename to sailfish/qml/pages/itemdetails/SeasonPage.qml index 99b852e..6273512 100644 --- a/qml/pages/itemdetails/SeasonPage.qml +++ b/sailfish/qml/pages/itemdetails/SeasonPage.qml @@ -29,8 +29,8 @@ BaseDetailPage { ShowEpisodesModel { id: episodeModel apiClient: ApiClient - show: itemData.SeriesId - seasonId: itemData.Id + show: itemData.seriesId + seasonId: itemData.jellyfinId fields: ["Overview"] } @@ -38,8 +38,8 @@ BaseDetailPage { anchors.fill: parent contentHeight: content.height header: PageHeader { - title: itemData.Name - description: itemData.SeriesName + title: itemData.name + description: itemData.seriesName } model: episodeModel delegate: BackgroundItem { @@ -119,10 +119,19 @@ BaseDetailPage { VerticalScrollDecorator {} } - onItemDataChanged: { + Connections { + target: itemData + onStatusChanged: { + if (itemData.status == JellyfinItem.Ready) { + episodeModel.reload() + } + } + } + onStatusChanged: { + if (status == PageStatus.Active) { console.log(JSON.stringify(itemData)) - episodeModel.show = itemData.SeriesId - episodeModel.seasonId = itemData.Id - episodeModel.reload() + episodeModel.show = itemData.seriesId + episodeModel.seasonId = itemData.jellyfinId + } } } diff --git a/qml/pages/itemdetails/SeriesPage.qml b/sailfish/qml/pages/itemdetails/SeriesPage.qml similarity index 89% rename from qml/pages/itemdetails/SeriesPage.qml rename to sailfish/qml/pages/itemdetails/SeriesPage.qml index 3538986..0943886 100644 --- a/qml/pages/itemdetails/SeriesPage.qml +++ b/sailfish/qml/pages/itemdetails/SeriesPage.qml @@ -66,6 +66,7 @@ BaseDetailPage { id: showSeasonsModel apiClient: ApiClient show: itemData.jellyfinId + onShowChanged: reload() } SilicaListView { @@ -86,8 +87,17 @@ BaseDetailPage { } } - onItemDataChanged: { - showSeasonsModel.show = itemData.jellyfinId - showSeasonsModel.reload() + + /*onStatusChanged: { + if (status == PageStatus.Active) { + showSeasonsModel.reload() + } + }*/ + Connections { + target: itemData + onJellyfinIdChanged: { + console.log("Item id changed") + //showSeasonsModel.show = itemData.jellyfinId + } } } diff --git a/qml/pages/itemdetails/UnsupportedPage.qml b/sailfish/qml/pages/itemdetails/UnsupportedPage.qml similarity index 100% rename from qml/pages/itemdetails/UnsupportedPage.qml rename to sailfish/qml/pages/itemdetails/UnsupportedPage.qml diff --git a/qml/pages/itemdetails/VideoPage.qml b/sailfish/qml/pages/itemdetails/VideoPage.qml similarity index 92% rename from qml/pages/itemdetails/VideoPage.qml rename to sailfish/qml/pages/itemdetails/VideoPage.qml index 17b7691..a3feac2 100644 --- a/qml/pages/itemdetails/VideoPage.qml +++ b/sailfish/qml/pages/itemdetails/VideoPage.qml @@ -46,7 +46,7 @@ BaseDetailPage { PageHeader { id: pageHeader title: itemData.name - description: qsTr("Run time: %2").arg(Utils.ticksToText(itemData.RunTimeTicks)) + description: qsTr("Run time: %2").arg(Utils.ticksToText(itemData.runTimeTicks)) } PlayToolbar { @@ -70,4 +70,13 @@ BaseDetailPage { } } } + + Connections { + target: itemData + onStatusChanged: { + if (status == JellyfinItem.Ready) { + console.log(itemData.mediaStreams) + } + } + } } diff --git a/qml/pages/setup/AddServerConnectingPage.qml b/sailfish/qml/pages/setup/AddServerConnectingPage.qml similarity index 100% rename from qml/pages/setup/AddServerConnectingPage.qml rename to sailfish/qml/pages/setup/AddServerConnectingPage.qml diff --git a/qml/pages/setup/AddServerPage.qml b/sailfish/qml/pages/setup/AddServerPage.qml similarity index 100% rename from qml/pages/setup/AddServerPage.qml rename to sailfish/qml/pages/setup/AddServerPage.qml diff --git a/qml/pages/setup/LoginDialog.qml b/sailfish/qml/pages/setup/LoginDialog.qml similarity index 100% rename from qml/pages/setup/LoginDialog.qml rename to sailfish/qml/pages/setup/LoginDialog.qml diff --git a/qml/qmldir b/sailfish/qml/qmldir similarity index 100% rename from qml/qmldir rename to sailfish/qml/qmldir diff --git a/sailfish/sailfish.pro b/sailfish/sailfish.pro new file mode 100644 index 0000000..7a4119e --- /dev/null +++ b/sailfish/sailfish.pro @@ -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 diff --git a/src/harbour-sailfin.cpp b/sailfish/src/harbour-sailfin.cpp similarity index 66% rename from src/harbour-sailfin.cpp rename to sailfish/src/harbour-sailfin.cpp index 687c252..7b0d1b3 100644 --- a/src/harbour-sailfin.cpp +++ b/sailfish/src/harbour-sailfin.cpp @@ -28,28 +28,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include -#include "jellyfinapiclient.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(QML_NAMESPACE, 1, 0, "ApiClient", [](QQmlEngine *eng, QJSEngine *js) { - Q_UNUSED(eng) - Q_UNUSED(js) - return dynamic_cast(new Jellyfin::ApiClient()); - }); - qmlRegisterType(QML_NAMESPACE, 1, 0, "ServerDiscoveryModel"); - qmlRegisterType(QML_NAMESPACE, 1, 0, "MediaSource"); - - // API models - Jellyfin::registerModels(QML_NAMESPACE); - Jellyfin::registerSerializableJsonTypes(QML_NAMESPACE); -} +#include int main(int argc, char *argv[]) { // 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). QGuiApplication *app = SailfishApp::application(argc, argv); - registerQml(); - + Jellyfin::registerTypes(); QQuickView *view = SailfishApp::createView(); view->setSource(SailfishApp::pathToMainQml()); view->show(); diff --git a/translations/harbour-sailfin-de.ts b/sailfish/translations/harbour-sailfin-de.ts similarity index 100% rename from translations/harbour-sailfin-de.ts rename to sailfish/translations/harbour-sailfin-de.ts diff --git a/translations/harbour-sailfin.ts b/sailfish/translations/harbour-sailfin.ts similarity index 98% rename from translations/harbour-sailfin.ts rename to sailfish/translations/harbour-sailfin.ts index 729b55e..9c8d94c 100644 --- a/translations/harbour-sailfin.ts +++ b/sailfish/translations/harbour-sailfin.ts @@ -141,13 +141,6 @@ - - Jellyfin::Item - - Invalid response from the server: root element is not an object. - - - LegalPage