diff --git a/.gitignore b/.gitignore index f147edf..fab7372 100644 --- a/.gitignore +++ b/.gitignore @@ -1,52 +1,73 @@ -# C++ objects and libs -*.slo -*.lo -*.o +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave *.a -*.la -*.lai +*.core +*.moc +*.o +*.obj +*.orig +*.rej *.so *.so.* -*.dll -*.dylib - -# Qt-es -object_script.*.Release -object_script.*.Debug -*_plugin_import.cpp +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc /.qmake.cache /.qmake.stash -*.pro.user -*.pro.user.* -*.qbs.user -*.qbs.user.* -*.moc -moc_*.cpp -moc_*.h -qrc_*.cpp -ui_*.h -*.qmlc -*.jsc -Makefile* -*build-* -*.qm -*.prl -# Qt unit tests -target_wrapper.* +# qtcreator generated files +*.pro.user* -# QtCreator -*.autosave +# xemacs temporary files +*.flc -# QtCreator Qml -*.qmlproject.user -*.qmlproject.user.* +# Vim temporary files +.*.swp -# QtCreator CMake -CMakeLists.txt.user* +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* -# QtCreator 4.8< compilation database -compile_commands.json +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe -# QtCreator local machine specific files for imported projects -*creator.user* diff --git a/cmainwindow.cpp b/cmainwindow.cpp new file mode 100644 index 0000000..58149de --- /dev/null +++ b/cmainwindow.cpp @@ -0,0 +1,52 @@ +#include "cmainwindow.h" +#include "ui_cmainwindow.h" + +#include "csubsonic.h" + +#include + + +cMainWindow::cMainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::cMainWindow) +{ + ui->setupUi(this); + + cSubsonic* subsonic = new cSubsonic("https://music.windesign.at", "birkeh", "WeissIchNicht8"); + + bool licenseValid; + QString licenseEmail; + QDateTime licenseExpires; + QMap musicFolders; + QMap shortcuts; + QStringList indexes; + QMap indexArtists; + + QString artistName; + qint32 artistPlayCount; + QMap albums; + + qint32 albumParent; + QString albumName; + qint32 albumPlayCount; + QMap tracks; + + QMap genres; + + QMap artists; + + subsonic->getLicense(licenseValid, licenseEmail, licenseExpires); + subsonic->getMusicFolders(musicFolders); + subsonic->getIndexes(shortcuts, indexes, indexArtists); + subsonic->getMusicDirectory(230, artistName, artistPlayCount, albums); + subsonic->getMusicDirectory(885, albumParent, albumName, albumPlayCount, tracks); + subsonic->getGenres(genres); + subsonic->getArtists(indexes, artists); + + delete subsonic; +} + +cMainWindow::~cMainWindow() +{ + delete ui; +} diff --git a/cmainwindow.h b/cmainwindow.h new file mode 100644 index 0000000..65f0642 --- /dev/null +++ b/cmainwindow.h @@ -0,0 +1,21 @@ +#ifndef CMAINWINDOW_H +#define CMAINWINDOW_H + +#include + +QT_BEGIN_NAMESPACE +namespace Ui { class cMainWindow; } +QT_END_NAMESPACE + +class cMainWindow : public QMainWindow +{ + Q_OBJECT + +public: + cMainWindow(QWidget *parent = nullptr); + ~cMainWindow(); + +private: + Ui::cMainWindow *ui; +}; +#endif // CMAINWINDOW_H diff --git a/cmainwindow.ui b/cmainwindow.ui new file mode 100644 index 0000000..7dddbe4 --- /dev/null +++ b/cmainwindow.ui @@ -0,0 +1,22 @@ + + + cMainWindow + + + + 0 + 0 + 800 + 600 + + + + cMainWindow + + + + + + + + diff --git a/csubsonic.cpp b/csubsonic.cpp new file mode 100644 index 0000000..0ea94ad --- /dev/null +++ b/csubsonic.cpp @@ -0,0 +1,464 @@ +#include "csubsonic.h" + +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include + + +cSubsonic::cSubsonic() : + m_server(""), + m_user(""), + m_password(""), + m_version("1.15.0") +{ + verify(); +} + +cSubsonic::cSubsonic(const QString& server, const QString& user, const QString& password) : + m_server(server), + m_user(user), + m_password(password), + m_version("1.15.0") +{ + verify(); +} + +void cSubsonic::setServer(const QString& server) +{ + m_server = server; +} + +QString cSubsonic::server() +{ + return(m_server); +} + +void cSubsonic::setUser(const QString& user) +{ + m_user = user; +} + +QString cSubsonic::user() +{ + return(m_user); +} + +void cSubsonic::setPassword(const QString& password) +{ + m_password = password; +} + +bool cSubsonic::verify() +{ + QDomElement element; + + if(!sendRequest("ping.view", element)) + { + m_version = element.attribute("version"); + + if(!sendRequest("ping.view", element)) + return(false); + } + + return(true); +} + +bool cSubsonic::getLicense(bool& valid, QString& email, QDateTime& licenseExpires) +{ + QDomElement element; + + if(!sendRequest("getLicense", element, "license")) + return(false); + + valid = element.attribute("valid") == "true"; + email = element.attribute("email"); + licenseExpires = QDateTime::fromString(element.attribute("licenseExpires"), "yyyy-MM-ddThh:mm:ss.zzzZ"); + + return(true); +} + +bool cSubsonic::getMusicFolders(QMap& folders) +{ + QDomElement element; + + if(!sendRequest("getMusicFolders", element, "musicFolders")) + return(false); + + QDomNode child = element.firstChild(); + QDomElement childElement; + qint32 id; + QString name; + + while(!child.isNull()) + { + childElement = child.toElement(); + + if(!childElement.tagName().compare("musicFolder")) + { + id = childElement.attribute("id").toInt(); + name = childElement.attribute("name"); + folders.insert(id, name); + } + child = child.nextSibling(); + } + + return(true); +} + +bool cSubsonic::getIndexes(QMap& shortcuts, QStringList& indexes, QMap &artists, const qint32 id, const QDate& modifiedSince) +{ + QMap parameter; + QDomElement element; + + if(id != -1) + parameter.insert("musicFolderId", QString::number(id)); + + if(modifiedSince.isValid()) + parameter.insert("ifModifiedSince", QString::number(QDateTime(modifiedSince, QTime(0, 0)).toMSecsSinceEpoch())); + + if(!sendRequest("getIndexes", element, "indexes", parameter)) + return(false); + + QDomNode child = element.firstChild(); + QDomElement childElement; + QDomElement artistElement; + + qint32 shortcutID; + QString shortcutName; + QString indexName; + qint32 artistID; + QString artistName; + + while(!child.isNull()) + { + childElement = child.toElement(); + + if(!childElement.tagName().compare("shortcut")) + { + shortcutID = childElement.attribute("id").toInt(); + shortcutName = childElement.attribute("name"); + shortcuts.insert(shortcutID, shortcutName); + } + else if(!childElement.tagName().compare("index")) + { + indexName = childElement.attribute("name"); + indexes.append(indexName); + + QDomNode artistChild = childElement.firstChild(); + + while(!artistChild.isNull()) + { + artistElement = artistChild.toElement(); + + if(!artistElement.tagName().compare("artist")) + { + artistID = artistElement.attribute("id").toInt(); + artistName = artistElement.attribute("name"); + + INDEX artist = { indexName, artistName, -1 }; + artists.insert(artistID, artist); + } + + artistChild = artistChild.nextSibling(); + } + } + + child = child.nextSibling(); + } + + return(true); +} + +bool cSubsonic::getMusicDirectory(const qint32 id, QString &name, qint32 &playCount, QMap& albums) +{ + QMap parameter; + QDomElement element; + + parameter.insert("id", QString::number(id)); + + if(!sendRequest("getMusicDirectory", element, "directory", parameter)) + return(false); + + name = element.attribute("name"); + playCount = element.attribute("playCount").toInt(); + + QDomNode child = element.firstChild(); + QDomElement childElement; + QDomElement artistElement; + + while(!child.isNull()) + { + childElement = child.toElement(); + + if(!childElement.tagName().compare("child")) + { + if(!childElement.hasAttribute("contentType")) + { + ALBUM album; + qint32 albumID; + + albumID = childElement.attribute("id").toInt(); + album.parent = childElement.attribute("parent").toInt(); + album.title = childElement.attribute("title"); + album.album = childElement.attribute("album"); + album.artist = childElement.attribute("artist"); + album.year = childElement.attribute("year").toInt(); + album.genre = childElement.attribute("genre"); + album.coverArt = childElement.attribute("coverArt").toInt(); + album.playCount = childElement.attribute("playCount").toInt(); + album.created = QDateTime::fromString(childElement.attribute("created"), "yyyy-MM-ddThh:mm:ss.zzzZ"); + + albums.insert(albumID, album); + } + } + + child = child.nextSibling(); + } + + return(true); +} + +bool cSubsonic::getMusicDirectory(const qint32 id, qint32& parent, QString& name, qint32& playCount, QMap& tracks) +{ + QMap parameter; + QDomElement element; + + parameter.insert("id", QString::number(id)); + + if(!sendRequest("getMusicDirectory", element, "directory", parameter)) + return(false); + + parent = element.attribute("parent").toInt(); + name = element.attribute("name"); + playCount = element.attribute("playCount").toInt(); + + QDomNode child = element.firstChild(); + QDomElement childElement; + QDomElement artistElement; + + while(!child.isNull()) + { + childElement = child.toElement(); + + if(!childElement.tagName().compare("child")) + { + if(childElement.hasAttribute("contentType")) + { + TRACK track; + qint32 trackID; + + trackID = childElement.attribute("id").toInt(); + track.parent = childElement.attribute("parent").toInt(); + track.title = childElement.attribute("title"); + track.album = childElement.attribute("album"); + track.artist = childElement.attribute("artist"); + track.track = childElement.attribute("track"); + track.year = childElement.attribute("year").toInt(); + track.genre = childElement.attribute("genre"); + track.coverArt = childElement.attribute("coverArt").toInt(); + track.size = childElement.attribute("size").toInt(); + track.contentType = childElement.attribute("contentType"); + track.suffix = childElement.attribute("suffix"); + track.duration = childElement.attribute("duration").toInt(); + track.bitRate = childElement.attribute("duration").toInt(); + track.path = childElement.attribute("path"); + track.isVideo = childElement.attribute("isVideo") == "true"; + track.playCount = childElement.attribute("playCount").toInt(); + track.discNumber = childElement.attribute("discNumber"); + track.created = QDateTime::fromString(childElement.attribute("created"), "yyyy-MM-ddThh:mm:ss.zzzZ"); + track.albumID = childElement.attribute("albumID").toInt(); + track.artistID = childElement.attribute("artistID").toInt(); + track.type = childElement.attribute("type"); + + tracks.insert(trackID, track); + } + } + + child = child.nextSibling(); + } + + return(true); +} + +bool cSubsonic::getGenres(QMap& genres) +{ + QDomElement element; + + if(!sendRequest("getGenres", element, "genres")) + return(false); + + QDomNode child = element.firstChild(); + QDomElement childElement; + + while(!child.isNull()) + { + childElement = child.toElement(); + + if(!childElement.tagName().compare("genre")) + { + GENRE genre; + QString name; + + genre.songCount = childElement.attribute("songCount").toInt(); + genre.albumCount = childElement.attribute("albumCount").toInt(); + name = childElement.text(); + + genres.insert(name, genre); + } + + child = child.nextSibling(); + } + + return(true); +} + +bool cSubsonic::getArtists(QStringList& indexes, QMap& artists, const qint32 id) +{ + QMap parameter; + QDomElement element; + + if(id != -1) + parameter.insert("musicFolderId", QString::number(id)); + + if(!sendRequest("getArtists", element, "artists", parameter)) + return(false); + + QDomNode child = element.firstChild(); + QDomElement childElement; + QDomElement artistElement; + + QString index; + + while(!child.isNull()) + { + childElement = child.toElement(); + + if(!childElement.tagName().compare("index")) + { + index = childElement.attribute("name"); + indexes.append(index); + + QDomNode artistChild = childElement.firstChild(); + + while(!artistChild.isNull()) + { + artistElement = artistChild.toElement(); + + if(!artistElement.tagName().compare("artist")) + { + ARTIST artist; + + artist.index = index; + artist.name = artistElement.attribute("name"); + artist.coverArt = artistElement.attribute("coverArt"); + artist.albumCount = artistElement.attribute("albumCount").toInt(); + + artists.insert(artistElement.attribute("id").toInt(), artist); + } + + artistChild = artistChild.nextSibling(); + } + } + + child = child.nextSibling(); + } + + return(true); +} + +QString cSubsonic::randomString() +{ + const QString possibleCharacters("abcdef0123456789"); + const int randomStringLength = 6; + + QString randomString; + + for(int i = 0; i < randomStringLength; ++i) + { + int index = rand() % possibleCharacters.length(); + QChar nextChar = possibleCharacters.at(index); + randomString.append(nextChar); + } + return(randomString); +} + +bool cSubsonic::sendRequest(const QString& request, QDomElement& element, const QString& verify, const QMap& parameter) +{ + QNetworkAccessManager networkManager; + QString salt = randomString(); + QString token = QCryptographicHash::hash(QString(m_password+salt).toUtf8(), QCryptographicHash::Md5).toHex(); + QString strRequest = QString("%1/rest/%2?u=%3&t=%4&s=%5&v=%6&c=subJuke").arg(m_server, request, m_user, token, salt, m_version); + + if(parameter.count()) + { + QString param; + + QMapIterator i(parameter); + while(i.hasNext()) + { + i.next(); + param.append(QString("&%1=%2").arg(i.key(), i.value())); + } + + strRequest.append(param); + } + +// qDebug() << strRequest; + + QUrl url(strRequest); + QNetworkRequest networkRequest(url); + QNetworkReply* reply = networkManager.get(networkRequest); + QEventLoop loop; + + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + if(reply->error() == QNetworkReply::NoError) + { + QDomDocument doc; + QString errorStr; + int errorLine; + int errorColumn; + if(!doc.setContent(reply->readAll(), false, &errorStr, &errorLine, &errorColumn)) + return(false); + +// qDebug() << doc.toString(); + element = doc.documentElement(); + if(element.tagName().compare("subsonic-response")) + return(false); + + QString status = element.attribute("status"); + if(status == "failed") + return(false); + + if(verify.isEmpty()) + { + if(element.isNull()) + return(false); + return(true); + } + + element = element.firstChild().toElement(); + if(element.isNull()) + return(false); + + if(element.tagName().compare(verify)) + return(false); + return(true); + } + else + { +// qDebug() << reply->errorString(); + return(false); + } +} diff --git a/csubsonic.h b/csubsonic.h new file mode 100644 index 0000000..4d08877 --- /dev/null +++ b/csubsonic.h @@ -0,0 +1,102 @@ +#ifndef CSUBSONIC_H +#define CSUBSONIC_H + + +#include +#include +#include +#include + + +typedef struct tagINDEX +{ + QString index; + QString name; + qint32 playCount; +} INDEX; + +typedef struct tagALBUM +{ + qint32 parent; + QString title; + QString album; + QString artist; + qint16 year; + QString genre; + qint32 coverArt; + qint32 playCount; + QDateTime created; +} ALBUM; + +typedef struct tagTRACK +{ + qint32 parent; + QString title; + QString album; + QString artist; + QString track; + qint16 year; + QString genre; + qint32 coverArt; + qint32 size; + QString contentType; + QString suffix; + qint32 duration; + qint32 bitRate; + QString path; + bool isVideo; + qint32 playCount; + QString discNumber; + QDateTime created; + qint32 albumID; + qint32 artistID; + QString type; +} TRACK; + +typedef struct tagGENRE +{ + qint32 songCount; + qint32 albumCount; +} GENRE; + +typedef struct tagARTIST +{ + QString index; + QString name; + QString coverArt; + qint32 albumCount; +} ARTIST; + +class cSubsonic +{ +public: + cSubsonic(); + cSubsonic(const QString& server, const QString& user, const QString& password); + + void setServer(const QString& server); + QString server(); + + void setUser(const QString& user); + QString user(); + + void setPassword(const QString& password); + + bool verify(); + bool getLicense(bool &valid, QString &email, QDateTime &licenseExpires); + bool getMusicFolders(QMap& folders); + bool getIndexes(QMap& shortcuts, QStringList& indexes, QMap& artists, const qint32 id = -1, const QDate& modifiedSince = QDate()); + bool getMusicDirectory(const qint32 id, QString& name, qint32& playCount, QMap& albums); + bool getMusicDirectory(const qint32 id, qint32& parent, QString& name, qint32& playCount, QMap& tracks); + bool getGenres(QMap& genres); + bool getArtists(QStringList& indexes, QMap& artists, const qint32 id = -1); +private: + QString m_server; + QString m_user; + QString m_password; + QString m_version; + + QString randomString(); + bool sendRequest(const QString& request, QDomElement& element, const QString& verify = "", const QMap& parameter = QMap()); +}; + +#endif // CSUBSONIC_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..eaa4c0b --- /dev/null +++ b/main.cpp @@ -0,0 +1,24 @@ +#include "cmainwindow.h" + +#include +#include +#include + + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + + QTranslator translator; + const QStringList uiLanguages = QLocale::system().uiLanguages(); + for (const QString &locale : uiLanguages) { + const QString baseName = "subJuke_" + QLocale(locale).name(); + if (translator.load(":/i18n/" + baseName)) { + a.installTranslator(&translator); + break; + } + } + cMainWindow w; + w.show(); + return a.exec(); +} diff --git a/subJuke.pro b/subJuke.pro new file mode 100644 index 0000000..d9b6e1a --- /dev/null +++ b/subJuke.pro @@ -0,0 +1,31 @@ +QT += core gui network xml + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++11 + +# You can make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + csubsonic.cpp \ + main.cpp \ + cmainwindow.cpp + +HEADERS += \ + cmainwindow.h \ + csubsonic.h + +FORMS += \ + cmainwindow.ui + +TRANSLATIONS += \ + subJuke_de_AT.ts +CONFIG += lrelease +CONFIG += embed_translations + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target diff --git a/subJuke_de_AT.ts b/subJuke_de_AT.ts new file mode 100644 index 0000000..b01af89 --- /dev/null +++ b/subJuke_de_AT.ts @@ -0,0 +1,3 @@ + + +