diff --git a/CMakeLists.txt b/CMakeLists.txt index cc59cbc22..38fd0c945 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,6 +203,11 @@ optional_component(BOX ON "Box support" optional_component(VK ON "Vk.com support") +optional_component(SEAFILE ON "Seafile support" + DEPENDS "Google sparsehash" SPARSEHASH_INCLUDE_DIRS + DEPENDS "Taglib 1.8" "TAGLIB_VERSION VERSION_GREATER 1.7.999" +) + optional_component(AUDIOCD ON "Devices: Audio CD support" DEPENDS "libcdio" CDIO_FOUND ) diff --git a/data/data.qrc b/data/data.qrc index c3adf5c45..25a861874 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -386,6 +386,7 @@ schema/schema-44.sql schema/schema-45.sql schema/schema-46.sql + schema/schema-47.sql schema/schema-4.sql schema/schema-5.sql schema/schema-6.sql @@ -424,5 +425,6 @@ vk/deactivated.gif providers/vk.png vk/link.png + providers/seafile.png diff --git a/data/providers/seafile.png b/data/providers/seafile.png new file mode 100644 index 000000000..e08b9407c Binary files /dev/null and b/data/providers/seafile.png differ diff --git a/data/schema/schema-45.sql b/data/schema/schema-45.sql index 058d4599b..7761d541a 100644 --- a/data/schema/schema-45.sql +++ b/data/schema/schema-45.sql @@ -1,24 +1,48 @@ -CREATE VIRTUAL TABLE playlist_items_fts USING fts3( +CREATE TABLE seafile_songs( + title TEXT, + album TEXT, + artist TEXT, + albumartist TEXT, + composer TEXT, + track INTEGER, + disc INTEGER, + bpm REAL, + year INTEGER, + genre TEXT, + comment TEXT, + compilation INTEGER, + + length INTEGER, + bitrate INTEGER, + samplerate INTEGER, + + directory INTEGER NOT NULL, + filename TEXT NOT NULL, + mtime INTEGER NOT NULL, + ctime INTEGER NOT NULL, + filesize INTEGER NOT NULL, + sampler INTEGER NOT NULL DEFAULT 0, + art_automatic TEXT, + art_manual TEXT, + filetype INTEGER NOT NULL DEFAULT 0, + playcount INTEGER NOT NULL DEFAULT 0, + lastplayed INTEGER, + rating INTEGER, + forced_compilation_on INTEGER NOT NULL DEFAULT 0, + forced_compilation_off INTEGER NOT NULL DEFAULT 0, + effective_compilation NOT NULL DEFAULT 0, + skipcount INTEGER NOT NULL DEFAULT 0, + score INTEGER NOT NULL DEFAULT 0, + beginning INTEGER NOT NULL DEFAULT 0, + cue_path TEXT, + unavailable INTEGER DEFAULT 0, + effective_albumartist TEXT, + etag TEXT +); + +CREATE VIRTUAL TABLE seafile_songs_fts USING fts3 ( ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsgenre, ftscomment, tokenize=unicode ); -DELETE FROM %allsongstables_fts; - -DROP TABLE %allsongstables_fts; - -ALTER TABLE %allsongstables ADD COLUMN performer TEXT; - -ALTER TABLE %allsongstables ADD COLUMN grouping TEXT; - -CREATE VIRTUAL TABLE %allsongstables_fts USING fts3( - ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment, - tokenize=unicode -); - -INSERT INTO %allsongstables_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment) - SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment - FROM %allsongstables; - UPDATE schema_version SET version=45; - diff --git a/data/schema/schema-46.sql b/data/schema/schema-46.sql index 2f43e6c7f..9e2438e97 100644 --- a/data/schema/schema-46.sql +++ b/data/schema/schema-46.sql @@ -1,3 +1,24 @@ -ALTER TABLE playlists ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0; +CREATE VIRTUAL TABLE playlist_items_fts USING fts3( + ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsgenre, ftscomment, + tokenize=unicode +); + +DELETE FROM %allsongstables_fts; + +DROP TABLE %allsongstables_fts; + +ALTER TABLE %allsongstables ADD COLUMN performer TEXT; + +ALTER TABLE %allsongstables ADD COLUMN grouping TEXT; + +CREATE VIRTUAL TABLE %allsongstables_fts USING fts3( + ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment, + tokenize=unicode +); + +INSERT INTO %allsongstables_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment) + SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment + FROM %allsongstables; UPDATE schema_version SET version=46; + diff --git a/data/schema/schema-47.sql b/data/schema/schema-47.sql new file mode 100644 index 000000000..bef6399f8 --- /dev/null +++ b/data/schema/schema-47.sql @@ -0,0 +1,3 @@ +ALTER TABLE playlists ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0; + +UPDATE schema_version SET version=47; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 913154385..650fa743a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1155,6 +1155,23 @@ optional_source(HAVE_VK internet/vksettingspage.ui ) +# Seafile support +optional_source(HAVE_SEAFILE + SOURCES + internet/seafileservice.cpp + internet/seafilesettingspage.cpp + internet/seafileurlhandler.cpp + internet/seafiletree.cpp + HEADERS + internet/seafileservice.h + internet/seafilesettingspage.h + internet/seafileurlhandler.h + internet/seafiletree.h + UI + internet/seafilesettingspage.ui +) + + # Pulse audio integration optional_source(HAVE_LIBPULSE INCLUDE_DIRECTORIES diff --git a/src/config.h.in b/src/config.h.in index 1352bb7ec..53758a319 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -40,6 +40,7 @@ #cmakedefine HAVE_SPARKLE #cmakedefine HAVE_SPOTIFY_DOWNLOADER #cmakedefine HAVE_VK +#cmakedefine HAVE_SEAFILE #cmakedefine HAVE_WIIMOTEDEV #cmakedefine TAGLIB_HAS_OPUS #cmakedefine USE_INSTALL_PREFIX diff --git a/src/core/database.cpp b/src/core/database.cpp index 36821fb2d..06e77f126 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -39,7 +39,7 @@ #include const char* Database::kDatabaseFilename = "clementine.db"; -const int Database::kSchemaVersion = 46; +const int Database::kSchemaVersion = 47; const char* Database::kMagicAllSongsTables = "%allsongstables"; int Database::sNextConnectionId = 1; diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp index 55a1eb766..89befe529 100644 --- a/src/internet/internetmodel.cpp +++ b/src/internet/internetmodel.cpp @@ -53,6 +53,10 @@ #ifdef HAVE_VK #include "vkservice.h" #endif +#ifdef HAVE_SEAFILE +#include "seafileservice.h" +#endif + using smart_playlists::Generator; using smart_playlists::GeneratorMimeData; @@ -101,6 +105,9 @@ InternetModel::InternetModel(Application* app, QObject* parent) #ifdef HAVE_VK AddService(new VkService(app, this)); #endif +#ifdef HAVE_SEAFILE + AddService(new SeafileService(app, this)); +#endif invisibleRootItem()->sortChildren(0, Qt::AscendingOrder); } diff --git a/src/internet/seafileservice.cpp b/src/internet/seafileservice.cpp new file mode 100644 index 000000000..fa8c306e1 --- /dev/null +++ b/src/internet/seafileservice.cpp @@ -0,0 +1,568 @@ +#include "seafileservice.h" + +#include +#include + +#include "core/application.h" +#include "core/player.h" +#include "core/waitforsignal.h" +#include "internet/seafileurlhandler.h" +#include "library/librarybackend.h" +#include "internet/oauthenticator.h" + + +const char* SeafileService::kServiceName = "Seafile"; +const char* SeafileService::kSettingsGroup = "Seafile"; + +namespace { + +static const char* kAuthToken = "/api2/auth-token/"; + +static const char* kFolderItems = "/api2/repos/%1/dir/"; +static const char* kListRepos = "/api2/repos/"; + +static const char* kFileUrl = "/api2/repos/%1/file/"; +static const char* kFileContent = "/api2/repos/%1/file/detail/"; +} + +SeafileService::SeafileService(Application* app, InternetModel* parent) + : CloudFileService(app, parent, kServiceName, kSettingsGroup, + QIcon(":/providers/seafile.png"), SettingsDialog::Page_Seafile) { + + QSettings s; + s.beginGroup(kSettingsGroup); + access_token_ = s.value("access_token").toString(); + server_ = s.value("server").toString(); + + QByteArray tree_bytes = s.value("tree").toByteArray(); + + if(!tree_bytes.isEmpty()) { + QDataStream stream(&tree_bytes, QIODevice::ReadOnly); + stream >> tree_; + } + + app->player()->RegisterUrlHandler(new SeafileUrlHandler(this, this)); + + connect(&tree_, SIGNAL(ToAdd(QString, QString, SeafileTree::Entry)), this, SLOT(AddEntry(QString, QString, SeafileTree::Entry))); + connect(&tree_, SIGNAL(ToDelete(QString, QString, SeafileTree::Entry)), this, SLOT(DeleteEntry(QString, QString, SeafileTree::Entry))); + connect(&tree_, SIGNAL(ToUpdate(QString, QString, SeafileTree::Entry)), this, SLOT(UpdateEntry(QString, QString, SeafileTree::Entry))); + + library_updated_ = QString::null; + +} + +bool SeafileService::has_credentials() const { + return !access_token().isEmpty(); +} + +bool SeafileService::is_authenticated() const { + return !access_token_.isEmpty(); +} + +QString SeafileService::access_token() const { + QSettings s; + s.beginGroup(kSettingsGroup); + + return s.value("access_token").toString(); +} + +void SeafileService::AddAuthorizationHeader(QNetworkRequest* request) const { + request->setRawHeader("Authorization", QString(QString("Token ") + QString(access_token_)).toAscii()); +} + +void SeafileService::ForgetCredentials() { + QSettings s; + s.beginGroup(kSettingsGroup); + + s.remove("access_token"); + s.remove("tree"); + access_token_.clear(); + tree_.Clear(); + + server_.clear(); +} + +bool SeafileService::GetToken(const QString &mail, const QString &password, const QString &server) { + QUrl url(server + kAuthToken); + QNetworkRequest request(url); + AddAuthorizationHeader(&request); + + url.addQueryItem("username", mail); + url.addQueryItem("password", password); + + QNetworkReply* reply = network_->post(request, url.encodedQuery()); + reply->ignoreSslErrors(); + WaitForSignal(reply, SIGNAL(finished())); + + if(!CheckReply(&reply)) { + qLog(Warning) << "Something wrong with the reply... (GetToken)"; + return false; + } + + reply->deleteLater(); + + QJson::Parser parser; + QVariantMap response = parser.parse(reply->readAll()).toMap(); + + // Because the server responds "token" + access_token_ = response["token"].toString().replace("\"", ""); + + if(access_token_.isEmpty()) { + return false; + } + + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue("access_token", access_token_); + + server_ = server; + + emit Connected(); + + return true; +} + + +void SeafileService::GetLibraries() { + QUrl url(server_ + kListRepos); + QNetworkRequest request(url); + AddAuthorizationHeader(&request); + QNetworkReply *reply = network_->get(request); + reply->ignoreSslErrors(); + + NewClosure(reply, SIGNAL(finished()), this, SLOT(GetLibrariesFinished(QNetworkReply*)), reply); +} + +void SeafileService::GetLibrariesFinished(QNetworkReply *reply) { + if(!CheckReply(&reply)) { + qLog(Warning) << "Something wrong with the reply... (GetLibraries)"; + return; + } + + reply->deleteLater(); + + // key : id, value : name + QMap libraries; + QByteArray data = reply->readAll(); + QJson::Parser parser; + QList repos = parser.parse(data).toList(); + + for (int i=0; ientry().id()) { + DeleteEntry(library->entry().id(), "/", library->entry()); + } + } + } + + + UpdateLibraries(); +} + +void SeafileService::Connect() { + if (is_authenticated()) { + UpdateLibraries(); + } + else { + ShowSettingsDialog(); + } +} + +void SeafileService::UpdateLibraries() { + + connect(this, SIGNAL(GetLibrariesFinishedSignal(QMap)), this, + SLOT(UpdateLibrariesInProgress(QMap))); + + GetLibraries(); +} + +void SeafileService::UpdateLibrariesInProgress(const QMap &libraries) { + disconnect(this, SIGNAL(GetLibrariesFinishedSignal(QMap)), this, + SLOT(UpdateLibrariesInProgress(QMap))); + + QSettings s; + s.beginGroup(kSettingsGroup); + QString library_to_update = s.value("library").toString(); + + // If the library doesn't change, we don't need to update + if (!library_updated_.isNull() && library_updated_ == library_to_update) { + return; + } + + library_updated_ = library_to_update; + + if(library_to_update == "none") { + return; + } + + QMapIterator library(libraries); + while (library.hasNext()) { + library.next(); + + // Need to check this library ? + if (library_to_update == "all" || library.key() == library_to_update) { + + FetchAndCheckFolderItems( + SeafileTree::Entry(library.value(), library.key(), SeafileTree::Entry::LIBRARY), "/"); + } + // If not, we can destroy the library from the tree + else { + // If the library was not in the tree, it's not a problem because DeleteEntry won't do anything + DeleteEntry(library.key(), "/", + SeafileTree::Entry(library.value(), library.key(), SeafileTree::Entry::LIBRARY)); + } + } + +} + +QNetworkReply* SeafileService::PrepareFetchFolderItems(const QString &library, const QString &path) { + QUrl url(server_ + QString(kFolderItems).arg(library)); + url.addQueryItem("p", path); + + QNetworkRequest request(url); + AddAuthorizationHeader(&request); + QNetworkReply* reply = network_->get(request); + reply->ignoreSslErrors(); + + return reply; +} + +void SeafileService::FetchAndCheckFolderItems(const SeafileTree::Entry &library, const QString &path) { + QNetworkReply *reply = PrepareFetchFolderItems(library.id(), path); + NewClosure(reply, SIGNAL(finished()), this, + SLOT(FetchAndCheckFolderItemsFinished(QNetworkReply*,SeafileTree::Entry,QString)), + reply, library, path); +} + +void SeafileService::FetchAndCheckFolderItemsFinished( + QNetworkReply *reply, const SeafileTree::Entry &library, const QString &path) { + if(!CheckReply(&reply)) { + qLog(Warning) << "Something wrong with the reply... (FetchFolderItemsToList)"; + return; + } + + reply->deleteLater(); + + QByteArray data = reply->readAll(); + + QJson::Parser parser; + QList variant_entries = parser.parse(data).toList(); + + SeafileTree::Entries entries; + for (const QVariant & e: variant_entries) { + QVariantMap entry = e.toMap(); + SeafileTree::Entry::Type entry_type = SeafileTree::Entry::StringToType(entry["type"].toString()); + QString entry_name = entry["name"].toString(); + + // We just want libraries/directories and files which could be songs. + if (entry_type == SeafileTree::Entry::NONE) { + qLog(Warning) << "Type entry unknown for this entry"; + } + else if (entry_type == SeafileTree::Entry::FILE && GuessMimeTypeForFile(entry_name).isNull()) { + continue; + } + + entries.append(SeafileTree::Entry(entry_name, entry["id"].toString(), entry_type)); + } + + tree_.CheckEntries(entries, library, path); +} + + +void SeafileService::AddRecursivelyFolderItems(const QString &library, const QString &path) { + QNetworkReply *reply = PrepareFetchFolderItems(library, path); + NewClosure(reply, SIGNAL(finished()), this, SLOT(AddRecursivelyFolderItemsFinished(QNetworkReply*,QString,QString)), reply, library, path); +} + +void SeafileService::AddRecursivelyFolderItemsFinished(QNetworkReply* reply, const QString &library, const QString &path) { + if(!CheckReply(&reply)) { + qLog(Warning) << "Something wrong with the reply... (FetchFolderItems)"; + return; + } + + reply->deleteLater(); + + QByteArray data = reply->readAll(); + QJson::Parser parser; + QList entries = parser.parse(data).toList(); + + for (const QVariant& e : entries) { + QVariantMap entry_map = e.toMap(); + SeafileTree::Entry::Type entry_type = SeafileTree::Entry::StringToType(entry_map["type"].toString()); + QString entry_name = entry_map["name"].toString(); + + // We just want libraries/directories and files which could be songs. + if (entry_type == SeafileTree::Entry::NONE) { + qLog(Warning) << "Type entry unknown for this entry"; + } + else if (entry_type == SeafileTree::Entry::FILE && GuessMimeTypeForFile(entry_name).isNull()) { + continue; + } + + SeafileTree::Entry entry(entry_name, entry_map["id"].toString(), entry_type); + + // If AddEntry was not successful we stop + // It could happen when the user changes the library to update while an update was in progress + if(!tree_.AddEntry(library, path, entry)) { + return; + } + + if (entry.is_dir()) { + AddRecursivelyFolderItems(library, path + entry.name() + "/"); + } + else { + MaybeAddFileEntry(entry.name(), library, path); + } + } +} + +QNetworkReply *SeafileService::PrepareFetchContentForFile(const QString &library, const QString &filepath) { + QUrl content_url(server_ + QString(kFileContent).arg(library)); + content_url.addQueryItem("p", filepath); + + QNetworkRequest request(content_url); + AddAuthorizationHeader(&request); + QNetworkReply* reply = network_->get(request); + + return reply; +} + + +void SeafileService::MaybeAddFileEntry(const QString &entry_name, const QString &library, const QString &path) { + QString mime_type = GuessMimeTypeForFile(entry_name); + + if(mime_type.isNull()) + return; + + // Get the details of the entry + QNetworkReply *reply = PrepareFetchContentForFile(library, path + entry_name); + NewClosure(reply, SIGNAL(finished()), this, + SLOT(MaybeAddFileEntryInProgress(QNetworkReply*,QString,QString,QString)), + reply, library, path, mime_type); +} + +void SeafileService::MaybeAddFileEntryInProgress( + QNetworkReply *reply, const QString &library, const QString &path, const QString &mime_type) { + + if(!CheckReply(&reply)) { + qLog(Warning) << "Something wrong with the reply... (MaybeAddFileEntry)"; + return; + } + + reply->deleteLater(); + + QByteArray data = reply->readAll(); + + QJson::Parser parser; + QVariantMap entry_detail_map = parser.parse(data).toMap(); + + QUrl url; + url.setScheme("seafile"); + url.setPath("/" + library + path + entry_detail_map["name"].toString()); + + Song song; + song.set_url(url); + song.set_ctime(0); + song.set_mtime(entry_detail_map["mtime"].toInt()); + song.set_filesize(entry_detail_map["size"].toInt()); + song.set_title(entry_detail_map["name"].toString()); + + // Get the download url of the entry + reply = PrepareFetchContentUrlForFile(library, path + entry_detail_map["name"].toString()); + NewClosure(reply, SIGNAL(finished()), this, SLOT(FetchContentUrlForFileFinished(QNetworkReply*, Song, QString)), reply, song, mime_type); +} + + +QNetworkReply* SeafileService::PrepareFetchContentUrlForFile(const QString &library, const QString &filepath) { + QUrl content_url(server_ + QString(kFileUrl).arg(library)); + content_url.addQueryItem("p", filepath); + + QNetworkRequest request(content_url); + AddAuthorizationHeader(&request); + QNetworkReply* reply = network_->get(request); + + return reply; +} + +void SeafileService::FetchContentUrlForFileFinished(QNetworkReply* reply, const Song &song, const QString &mime_type) { + + if(!CheckReply(&reply)) { + qLog(Warning) << "Something wrong with the reply... (FetchContentUrlForFile)"; + return; + } + + reply->deleteLater(); + + // Because server response is "http://..." + QString real_url = QString(reply->readAll()).replace("\"", ""); + + MaybeAddFileToDatabase(song, mime_type, QUrl(real_url), QString("Token %1").arg(access_token_)); +} + +QUrl SeafileService::GetStreamingUrlFromSongId(const QString &library, const QString &filepath) { + + QNetworkReply* reply = PrepareFetchContentUrlForFile(library, filepath); + reply->ignoreSslErrors(); + WaitForSignal(reply, SIGNAL(finished())); + + if(!CheckReply(&reply)) { + qLog(Warning) << "Something wrong with the reply... (GetStreamingUrlFromSongId)"; + return QUrl(""); + } + reply->deleteLater(); + + QString response = QString(reply->readAll()).replace("\"", ""); + + return QUrl(response); +} + + +void SeafileService::AddEntry(const QString &library, const QString &path, const SeafileTree::Entry &entry) { + + if (entry.is_library()) { + tree_.AddLibrary(entry.name(), entry.id()); + AddRecursivelyFolderItems(library, "/"); + } + else { + // If AddEntry was not successful we stop + // It could happen when the user changes the library to update while an update was in progress + if(!tree_.AddEntry(library, path, entry)) { + return; + } + + if (entry.is_file()) { + MaybeAddFileEntry(entry.name(), library, path); + } + else { + AddRecursivelyFolderItems(library, path + entry.name() + "/"); + } + } +} + +void SeafileService::UpdateEntry(const QString & library, const QString &path, const SeafileTree::Entry &entry) { + + if (entry.is_file()) { + DeleteEntry(library, path, entry); + AddEntry(library, path, entry); + } + else { + QString entry_path = path; + + if(entry.is_dir()) { + entry_path += entry.name() + "/"; + } + + FetchAndCheckFolderItems(SeafileTree::Entry("", library, SeafileTree::Entry::LIBRARY), + entry_path); + } +} + +void SeafileService::DeleteEntry(const QString &library, const QString &path, const SeafileTree::Entry &entry) { + + // For the QPair -> 1 : path, 2 : entry + QList> files_to_delete; + if(entry.is_library()) { + SeafileTree::TreeItem *item = tree_.FindLibrary(library); + files_to_delete = tree_.GetRecursiveFilesOfDir("/", item); + tree_.DeleteLibrary(library); + } + else { + if(entry.is_dir()) { + SeafileTree::TreeItem *item = tree_.FindFromAbsolutePath(library, path + entry.name() + "/"); + files_to_delete = tree_.GetRecursiveFilesOfDir(path + entry.name() + "/", item); + } + else { + files_to_delete.append(qMakePair(path, entry)); + } + + if(!tree_.DeleteEntry(library, path, entry)) { + return; + } + } + + // Delete songs from the library of Clementine + for (const QPair &file_to_delete : files_to_delete) { + if(!GuessMimeTypeForFile(file_to_delete.second.name()) + .isEmpty()) { + QUrl song_url("seafile:/" + library + file_to_delete.first + file_to_delete.second.name()); + Song song = library_backend_->GetSongByUrl(song_url); + + if (song.is_valid()) { + library_backend_->DeleteSongs(SongList() << song); + } + else { + qLog(Warning) << "Can't delete song from the Clementine's library : " << song_url; + } + } + } +} + + +bool SeafileService::CheckReply(QNetworkReply **reply) { + if (!(*reply)) { + return false; + } + + QVariant status_code_variant = (*reply)->attribute(QNetworkRequest::HttpStatusCodeAttribute); + if (status_code_variant.isValid()) { + int status_code = status_code_variant.toInt(); + + if (status_code == NO_ERROR) { + return true; + } + else if (status_code == TOO_MANY_REQUESTS) { + qLog(Debug) << "Too many requests, wait..."; + + // If there are too many requests, we just wait + QTimer timer; + timer.start(10000); + WaitForSignal(&timer, SIGNAL(timeout())); + + (*reply)->deleteLater(); + + // And we execute the reply again + *reply = network_->get((*reply)->request()); + WaitForSignal(*reply, SIGNAL(finished())); + + return CheckReply(reply); + } + } + + qLog(Warning) << "Error for reply : " << status_code_variant.toInt(); + // Unknown, 404 ... + return false; +} + + +SeafileService::~SeafileService() { + // Save the tree ! + QSettings s; + s.beginGroup(kSettingsGroup); + + QByteArray tree_byte; + QDataStream stream(&tree_byte, QIODevice::WriteOnly); + stream << tree_; + + s.setValue("tree", tree_byte); + +} + diff --git a/src/internet/seafileservice.h b/src/internet/seafileservice.h new file mode 100644 index 000000000..a839b9fce --- /dev/null +++ b/src/internet/seafileservice.h @@ -0,0 +1,117 @@ +/* Contacts (for explanations, congratulations, insults) : + * - + * + * Help : + * - The "path" variable has to end with "/". + * If we want to specify a filepath, the name of the variable has to be... filepath :) + * - Seafile stores files in libraries (or repositories) so variable with the name "library" corresponds to the + * Seafile library, not to the Clementine library + * - The authentification of Seafile's API is simply a token (REST API) + * - Seafile stores a hash for each entry. This hash changes when the entry is modified. + * This is the reason why we just have to compare the local hash with the server + * hash of a directory (for example) to know if the directory was modified. + * Libraries are an exception : Seafile stores a hash that never changes. + * This hash is called "id". + * + * Todo : + * - Add ssl certificate exception (for people who generate their own certificate on their Seafile server + * - Stop Tagreader when user changes the library +*/ + +#ifndef SEAFILESERVICE_H +#define SEAFILESERVICE_H + +#include "cloudfileservice.h" +#include "seafiletree.h" + +#include +#include + +class QNetworkReply; +class QNetworkRequest; + +// Interface between the seafile server and Clementine +class SeafileService : public CloudFileService { + Q_OBJECT + public: + + enum ApiError { + NO_ERROR = 200, + NOT_FOUND = 404, + TOO_MANY_REQUESTS = 429 + }; + + SeafileService(Application* app, InternetModel* parent); + ~SeafileService(); + + static const char* kServiceName; + static const char* kSettingsGroup; + + virtual bool has_credentials() const; + QUrl GetStreamingUrlFromSongId(const QString &library, const QString &filepath); + // Get the token for an user (simple rest api) + bool GetToken(const QString &mail, const QString &password, const QString &server); + // Get all the libraries available for the user. Will emit a signal + void GetLibraries(); + void ChangeLibrary(const QString &new_library); + + public slots: + void Connect(); + void ForgetCredentials(); + +signals: + void Connected(); + // QMap, key : library's id, value : library's name + void GetLibrariesFinishedSignal(QMap); + + private slots: + // Will emit the signal + void GetLibrariesFinished(QNetworkReply *reply); + + void FetchAndCheckFolderItemsFinished(QNetworkReply *reply, const SeafileTree::Entry &library, const QString &path); + + // Add recursively the content of a folder from a library + void AddRecursivelyFolderItemsFinished(QNetworkReply *reply, const QString &library, const QString &path); + // Get the url and try to add the file to the database + void FetchContentUrlForFileFinished(QNetworkReply* reply, const Song &song, const QString &mime_type); + // Add the entry to the tree and maybe add this entry to the database + void AddEntry(const QString &library, const QString &path, const SeafileTree::Entry &entry); + // Update the entry or check recursively the directories + void UpdateEntry(const QString &library, const QString &path, const SeafileTree::Entry &entry); + // Delete the entry (eventually the files of its subdir) of the tree and the database + void DeleteEntry(const QString &library, const QString &path, const SeafileTree::Entry &entry); + + void UpdateLibrariesInProgress(const QMap &libraries); + + void MaybeAddFileEntryInProgress(QNetworkReply *reply, const QString &library, + const QString &path, const QString &mime_type); + + private: + QString access_token() const; + bool is_authenticated() const; + + void AddAuthorizationHeader(QNetworkRequest* request) const; + + void UpdateLibraries(); + + void FetchAndCheckFolderItems(const SeafileTree::Entry &library, const QString &path); + void AddRecursivelyFolderItems(const QString &library, const QString &path); + + QNetworkReply* PrepareFetchFolderItems(const QString &library, const QString &path); + QNetworkReply* PrepareFetchContentForFile(const QString &library, const QString &filepath); + QNetworkReply* PrepareFetchContentUrlForFile(const QString &library, const QString &filepath); + + void MaybeAddFileEntry(const QString &entry_name, const QString &library, const QString &path); + + // False if not 200 or 429 + // If 429 (too many requests), re execute the request and put the reply in the argument + bool CheckReply(QNetworkReply **reply); + + + SeafileTree tree_; + QString access_token_; + QString server_; + QString library_updated_; +}; + +#endif // SEAFILESERVICE_H diff --git a/src/internet/seafilesettingspage.cpp b/src/internet/seafilesettingspage.cpp new file mode 100644 index 000000000..ca99168ca --- /dev/null +++ b/src/internet/seafilesettingspage.cpp @@ -0,0 +1,150 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#include "seafileservice.h" +#include "seafilesettingspage.h" +#include "internetmodel.h" +#include "core/logging.h" +#include "core/network.h" +#include "ui_seafilesettingspage.h" +#include "ui/iconloader.h" + +#include +#include +#include +#include +#include + +SeafileSettingsPage::SeafileSettingsPage(SettingsDialog* dialog) + : SettingsPage(dialog), + ui_(new Ui_SeafileSettingsPage), + service_(InternetModel::Service()) { + ui_->setupUi(this); + + setWindowIcon(QIcon(":/providers/seafile.png")); + + connect(ui_->login_button, SIGNAL(clicked()), SLOT(Login())); + connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout())); + + ui_->login_state->AddCredentialField(ui_->server); + ui_->login_state->AddCredentialField(ui_->mail); + ui_->login_state->AddCredentialField(ui_->password); + ui_->login_state->AddCredentialGroup(ui_->account_group); + + ui_->library_box->addItem("None", "none"); + + connect(service_, SIGNAL(GetLibrariesFinishedSignal(QMap)), this, + SLOT(GetLibrariesFinished(QMap))); +} + +SeafileSettingsPage::~SeafileSettingsPage() { delete ui_; } + + +void SeafileSettingsPage::Load() { + QSettings s; + s.beginGroup(SeafileService::kSettingsGroup); + + ui_->server->setText(s.value("server").toString()); + ui_->mail->setText(s.value("mail").toString()); + + if (!ui_->server->text().isEmpty() && !ui_->mail->text().isEmpty()) { + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn, ui_->mail->text()); + + // If there is more than "none" library, that means that we already got the libraries + if(ui_->library_box->count() <= 1) { + service_->GetLibraries(); + } + } +} + +void SeafileSettingsPage::GetLibrariesFinished(QMap libraries) { + ui_->library_box->clear(); + ui_->library_box->addItem("None", "none"); + ui_->library_box->addItem("All (could be slow)", "all"); + + // key : library's id, value : library's name + QMapIterator library(libraries); + while(library.hasNext()) { + library.next(); + ui_->library_box->addItem(library.value(), library.key()); + } + + QSettings s; + s.beginGroup(SeafileService::kSettingsGroup); + QString library_id = s.value("library").toString(); + + int saved_index = ui_->library_box->findData(library_id); + if (saved_index != -1) { + ui_->library_box->setCurrentIndex(saved_index); + } +} + + +void SeafileSettingsPage::Save() { + QString id = ui_->library_box->itemData(ui_->library_box->currentIndex()).toString(); + + QSettings s; + s.beginGroup(SeafileService::kSettingsGroup); + + s.setValue("mail", ui_->mail->text()); + s.setValue("server", ui_->server->text()); + s.setValue("library", id); + // Don't need to save the password + + service_->ChangeLibrary(id); +} + + +void SeafileSettingsPage::Login() { + + ui_->login_button->setEnabled(false); + + if(service_->GetToken(ui_->mail->text(), ui_->password->text(), ui_->server->text())) { + Save(); + + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn, ui_->mail->text()); + + service_->GetLibraries(); + } + else { + ui_->login_button->setEnabled(true); + QMessageBox::warning(this, tr("Unable to connect"), tr("Unable to connect")); + } +} + +void SeafileSettingsPage::Logout() { + service_->ForgetCredentials(); + + // We choose to keep the server + ui_->mail->clear(); + ui_->password->clear(); + + QSettings s; + s.beginGroup(SeafileService::kSettingsGroup); + + s.remove("mail"); + s.remove("server"); + s.remove("library"); + + ui_->library_box->clear(); + ui_->library_box->addItem("None", "none"); + + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut); + ui_->login_button->setEnabled(true); + +} + diff --git a/src/internet/seafilesettingspage.h b/src/internet/seafilesettingspage.h new file mode 100644 index 000000000..be0801384 --- /dev/null +++ b/src/internet/seafilesettingspage.h @@ -0,0 +1,51 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#ifndef SEAFILESETTINGSPAGE_H +#define SEAFILESETTINGSPAGE_H + +#include "ui/settingspage.h" + +#include +#include + +class Ui_SeafileSettingsPage; +class SeafileService; + +class SeafileSettingsPage : public SettingsPage { + Q_OBJECT + + public: + SeafileSettingsPage(SettingsDialog* dialog); + ~SeafileSettingsPage(); + + void Load(); + void Save(); + + private slots: + void Login(); + void Logout(); + // Map -> key : library's id, value : library's name + void GetLibrariesFinished(QMap libraries); + + private: + Ui_SeafileSettingsPage* ui_; + SeafileService* service_; + +}; + +#endif // SEAFILESETTINGSPAGE_H diff --git a/src/internet/seafilesettingspage.ui b/src/internet/seafilesettingspage.ui new file mode 100644 index 000000000..b05e740f4 --- /dev/null +++ b/src/internet/seafilesettingspage.ui @@ -0,0 +1,175 @@ + + + SeafileSettingsPage + + + + 0 + 0 + 480 + 261 + + + + Seafile + + + + + + + + + + + + Account details + + + + + + true + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Password + + + + + + + QLineEdit::Password + + + + + + + + + + Email + + + + + + + Login + + + + + + + Server + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Preference + + + + + 20 + 19 + 431 + 31 + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + Library + + + + + + + + + + + + LoginStateWidget + QWidget +
widgets/loginstatewidget.h
+ 1 +
+
+ + server + mail + password + login_button + library_box + + + +
diff --git a/src/internet/seafiletree.cpp b/src/internet/seafiletree.cpp new file mode 100644 index 000000000..251923453 --- /dev/null +++ b/src/internet/seafiletree.cpp @@ -0,0 +1,406 @@ +#include "seafiletree.h" + +#include +#include +#include +#include "core/logging.h" + +/* ############################## SeafileTree ############################## */ + +SeafileTree::SeafileTree() { } + +SeafileTree::SeafileTree(const SeafileTree ©) : SeafileTree() + { libraries_ = copy.libraries(); } + +QList SeafileTree::libraries() const { return libraries_; } + +void SeafileTree::Print() const { + qLog(Debug) << "library count : " << libraries_.count(); + + for (TreeItem *item : libraries_) { + qLog(Debug) << "library : " << item->ToString(1); + } +} + +void SeafileTree::AddLibrary(const QString &name, const QString &id) { + libraries_.append( + new TreeItem( + Entry(name, id, Entry::Type::LIBRARY))); +} + +void SeafileTree::DeleteLibrary(const QString &id) { + for (int i = 0; i < libraries_.size(); ++i) { + if (libraries_.at(i)->entry().id() == id) { + libraries_.removeAt(i); + return; + } + } +} + + +bool SeafileTree::AddEntry(const QString& library, const QString &path, const Entry &entry) { + TreeItem *dir_node = FindFromAbsolutePath(library, path); + + if(!dir_node) { + qLog(Warning) << "Can't find the path..."; + return false; + } + + // If it is not a dir or a library we can't add an entry... + if(!dir_node->entry().is_dir() && !dir_node->entry().is_library()) { + qLog(Warning) << "This is not a dir or a file..."; + return false; + } + + dir_node->AppendChild(entry); + + return true; +} + + +void SeafileTree::CheckEntries(const Entries &server_entries, const Entry &library, const QString &path) { + TreeItem *local_item = FindFromAbsolutePath(library.id(), path); + + // Don't know the path + // Have to add all entries + if(!local_item) { + emit ToAdd(library.id(), path, library); + return; + } + + Entries local_entries = local_item->childs_entry(); + + for (const Entry &server_entry : server_entries) { + bool is_in_tree = false; + + for (int i=0; ientry().id() == library) + return item; + } + + return nullptr; +} + +SeafileTree::TreeItem* SeafileTree::FindFromAbsolutePath(const QString &library, const QString &path) { + TreeItem *node_item = FindLibrary(library); + + if (!node_item) { + return nullptr; + } + + QStringList path_parts = path.split("/", QString::SkipEmptyParts); + + for (const QString &part : path_parts) { + node_item = node_item->FindChild(part); + + if(!node_item) { + break; + } + } + + return node_item; +} + +bool SeafileTree::DeleteEntry(const QString &library, const QString &path, const Entry &entry) { + TreeItem *item_parent = FindFromAbsolutePath(library, path); + + if(!item_parent) { + qLog(Debug) << "Unable to delete " << library + path + entry.name() + << " : path " << path << " not found"; + return false; + } + + TreeItem *item_entry = item_parent->FindChild(entry.name()); + + if(!item_entry) { + qLog(Debug) << "Unable to delete " << library + path + entry.name() + << " : entry " << entry.name() << " from path not found"; + return false; + } + + if(!item_parent->RemoveChild(item_entry)) { + qLog(Debug) << "Can't remove " << item_entry->entry().name() << " from parent"; + return false; + } + + delete item_entry; + + return true; +} + +void SeafileTree::Clear() { + qDeleteAll(libraries_); + libraries_.clear(); +} + + +QList> SeafileTree::GetRecursiveFilesOfDir(const QString &path, const TreeItem *item) { + // key = path, value = entry + QList> files; + + if(!item) { + return files; + } + + if(item->entry().is_file()) { + files.append(qMakePair(path, item->entry())); + } + // Get files of the dir + else { + for (TreeItem *child_item : item->childs()) { + if(child_item->entry().is_file()) { + files.append(qMakePair(path, child_item->entry())); + } + else { + QString name = child_item->entry().name() + "/"; + files.append(GetRecursiveFilesOfDir(path + name, child_item)); + } + } + } + + return files; +} + +SeafileTree::~SeafileTree() {} + + + +/* ################################# Entry ################################# */ + +SeafileTree::Entry::Entry() + : Entry("", "", Type::FILE) {} + +SeafileTree::Entry::Entry(const SeafileTree::Entry &entry) + : Entry(entry.name(), entry.id(), entry.type()) {} + +SeafileTree::Entry::Entry(const QString& name, const QString& id, const Type& type) { + name_ = name; + id_ = id; + type_ = type; +} + +QString SeafileTree::Entry::name() const { return name_; } +QString SeafileTree::Entry::id() const { return id_; } +SeafileTree::Entry::Type SeafileTree::Entry::type() const { return type_; } +bool SeafileTree::Entry::is_dir() const { return (type_ == Entry::DIR); } +bool SeafileTree::Entry::is_file() const { return (type_ == Entry::FILE); } +bool SeafileTree::Entry::is_library() const { return (type_ == Entry::LIBRARY); } +void SeafileTree::Entry::set_name(const QString &name) { name_ = name; } +void SeafileTree::Entry::set_id(const QString &id) { id_ = id; } +void SeafileTree::Entry::set_type(const Type &type) { type_ = type; } + + +QString SeafileTree::Entry::ToString() const { + return "name : " + name_ + " id : " + id_ + " type : " + TypeToString(type_); +} + +SeafileTree::Entry& SeafileTree::Entry::operator =(const Entry &entry) { + name_ = entry.name(); + id_ = entry.id(); + type_ = entry.type(); + + return *this; +} + +bool SeafileTree::Entry::operator ==(const Entry &a) const { + if(a.name() == name() && a.id() == id() && a.type() == type()) + return true; + + return false; +} + +bool SeafileTree::Entry::operator !=(const Entry &a) const { + return !(operator ==(a)); +} + +SeafileTree::Entry::~Entry() {} + + +QString SeafileTree::Entry::TypeToString(const Type &type) { + if (type == DIR) { + return "dir"; + } + else if (type == FILE) { + return "file"; + } + else if (type == LIBRARY) { + return "library"; + } + + return QString::null; +} + +SeafileTree::Entry::Type SeafileTree::Entry::StringToType(const QString &type) { + if (type == "dir") { + return DIR; + } + else if (type == "file") { + return FILE; + } + else if (type == "library") { + return LIBRARY; + } + + return NONE; +} + + +/* ############################### TreeItem ############################### */ + +SeafileTree::TreeItem::TreeItem() { entry_ = Entry(); } + +SeafileTree::TreeItem::TreeItem(const TreeItem ©) { entry_ = copy.entry(); } + +SeafileTree::TreeItem::TreeItem(const Entry &entry) { entry_ = entry; } + +SeafileTree::TreeItem::TreeItem(const Entry &entry, QList *childs) { + entry_ = entry; + childs_ = *childs; +} + +SeafileTree::Entries SeafileTree::TreeItem::childs_entry() const { + Entries entries; + + for (TreeItem *item : childs_) { + entries.append(Entry(item->entry())); + } + + return entries; +} + +SeafileTree::TreeItem* SeafileTree::TreeItem::child(int i) const { return childs_.at(i); } +QList SeafileTree::TreeItem::childs() const { return childs_; } +SeafileTree::Entry SeafileTree::TreeItem::entry() const { return entry_; } +void SeafileTree::TreeItem::set_entry(const Entry &entry) { entry_ = entry;} +void SeafileTree::TreeItem::set_childs(QList *childs) { childs_ = *childs; } + +void SeafileTree::TreeItem::AppendChild(TreeItem *child) { childs_.append(child); } + +void SeafileTree::TreeItem::AppendChild(const Entry &entry) { + childs_.append(new TreeItem(entry)); +} + +bool SeafileTree::TreeItem::RemoveChild(TreeItem *child) { return childs_.removeOne(child); } + +SeafileTree::TreeItem* SeafileTree::TreeItem::FindChild(const QString &name) const { + for (TreeItem *item : childs_) { + if (item->entry().name() == name) + return item; + } + + return nullptr; +} + +QString SeafileTree::TreeItem::ToString(int i) const { + QString res = ""; + + for (int j = 0; j < i; ++j) { + res += " "; + } + + res += entry_.ToString() + "\n"; + + for (TreeItem *item : childs_) { + res += item->ToString( i+1 ); + } + + return res; +} + +SeafileTree::TreeItem::~TreeItem() { + // We need to delete childs + for (TreeItem *item : childs_) { + delete item; + } +} + + + +QDataStream & operator << (QDataStream &out, const SeafileTree::Entry &entry) { + out << entry.name_ + << entry.id_ + << static_cast(entry.type_); + + return out; +} + +QDataStream & operator >> (QDataStream &in, SeafileTree::Entry &entry) { + quint8 temp; + + in >> entry.name_; + in >> entry.id_; + in >> temp; + entry.type_ = SeafileTree::Entry::Type(temp); + + return in; +} + +QDataStream & operator << (QDataStream &out, SeafileTree::TreeItem *item) { + out << item->entry_ + << item->childs_; + + return out; +} + + +QDataStream & operator >> (QDataStream &in, SeafileTree::TreeItem *&item) { + SeafileTree::Entry *entry = new SeafileTree::Entry(); + QList *childs = new QList; + + in >> *entry; + in >> *childs; + + item = new SeafileTree::TreeItem(*entry, childs); + + return in; +} + + +QDataStream & operator << (QDataStream &out, const SeafileTree &tree) { + out << tree.libraries_; + + return out; +} + +QDataStream & operator >> (QDataStream &in, SeafileTree &tree) { + in >> tree.libraries_; + + return in; +} diff --git a/src/internet/seafiletree.h b/src/internet/seafiletree.h new file mode 100644 index 000000000..dc20f2c7b --- /dev/null +++ b/src/internet/seafiletree.h @@ -0,0 +1,162 @@ +/* Contacts (for explanations, congratulations, insults) : + * - +*/ + +#ifndef SEAFILETREE_H +#define SEAFILETREE_H + +#include +#include +#include +#include + +#include "cloudfileservice.h" + +// Reproduce the file system of Seafile server libraries +// Analog to a tree +class SeafileTree : public QObject { + Q_OBJECT + +public: + + SeafileTree(); + SeafileTree(const SeafileTree ©); + ~SeafileTree(); + + class Entry { + + public: + + enum Type { + DIR = 0, + FILE = 1, + LIBRARY = 2, + NONE = 3 + }; + + Entry(); + Entry(const Entry &entry); + Entry(const QString &name, const QString &id, const Type &type); + ~Entry(); + + QString name() const; + void set_name(const QString &name); + QString id() const; + void set_id(const QString &id); + Type type() const; + void set_type(const Type &type); + + bool is_dir() const; + bool is_file() const; + bool is_library() const; + + Entry& operator =(const Entry &entry); + bool operator ==(const Entry &a) const; + bool operator !=(const Entry &a) const; + + QString ToString() const; + + static QString TypeToString(const Type &type); + static Type StringToType(const QString &type); + + private: + QString name_, id_; + Type type_; + + friend QDataStream & operator << (QDataStream &out, const SeafileTree::Entry &entry); + friend QDataStream & operator >> (QDataStream &in, SeafileTree::Entry &entry); + }; + + typedef QList Entries; + + // Node of the tree + // Contains an entry + class TreeItem { + public: + TreeItem(); + TreeItem(const TreeItem ©); + TreeItem(const Entry &entry); + TreeItem(const Entry &entry, QList *childs); + ~TreeItem(); + + TreeItem* child(int i) const; + QList childs() const; + // List of each child's entry + Entries childs_entry() const; + + void set_childs(QList *childs); + + Entry entry() const; + void set_entry(const Entry &entry); + + void AppendChild(TreeItem *child); + void AppendChild(const Entry &entry); + + // True if child is removed + bool RemoveChild(TreeItem *child); + + // nullptr if we didn't find a child entry with the given name + TreeItem* FindChild(const QString &name) const; + + // Convert the node in QString (for debug) + QString ToString(int i) const; + + private: + Entry entry_; + QList childs_; + + friend QDataStream & operator << (QDataStream &out, SeafileTree::TreeItem *item); + friend QDataStream & operator >> (QDataStream &in, SeafileTree::TreeItem *&item); + }; + + QList libraries() const; + + void AddLibrary(const QString &name, const QString &id); + void DeleteLibrary(const QString &id); + bool AddEntry(const QString &library, const QString &path, const Entry &entry); + bool DeleteEntry(const QString &library, const QString &path, const Entry &entry); + + // Get a list of pair (path, entry) corresponding to the subfiles (and recursively to the subsubfiles...) of the given item + QList> GetRecursiveFilesOfDir(const QString &path, const TreeItem *item); + + // nullptr if we didn't find the library with the given id + TreeItem* FindLibrary(const QString &library); + // nullptr if we didn't find the item + TreeItem* FindFromAbsolutePath(const QString &library, const QString &path); + + // Compare the server entries with the tree + // Emit signals (ToDelete, ToAdd, ToUpdate) + void CheckEntries(const Entries &server_entries, const Entry &library, const QString &path); + + // Destroy the tree + void Clear(); + + // Print the tree in the debug log + void Print() const; + +signals: + // Entry to delete in the tree + void ToDelete(const QString &library, const QString &path, const SeafileTree::Entry &entry); + // Entry to add in the tree + void ToAdd(const QString &library, const QString &path, const SeafileTree::Entry &entry); + // Entry to update in the tree + void ToUpdate(const QString &library, const QString &path, const SeafileTree::Entry &entry); + +private: + QList libraries_; + + friend QDataStream & operator << (QDataStream &out, const SeafileTree &tree); + friend QDataStream & operator >> (QDataStream &in, SeafileTree &tree); +}; + +QDataStream & operator << (QDataStream &out, const SeafileTree &tree); +QDataStream & operator >> (QDataStream &in, SeafileTree &tree); + +QDataStream & operator << (QDataStream &out, const SeafileTree::Entry &entry); +QDataStream & operator >> (QDataStream &in, SeafileTree::Entry &entry); + +QDataStream & operator << (QDataStream &out, SeafileTree::TreeItem *item); +QDataStream & operator >> (QDataStream &in, SeafileTree::TreeItem *&item); + + +#endif // SEAFILETREE_H diff --git a/src/internet/seafileurlhandler.cpp b/src/internet/seafileurlhandler.cpp new file mode 100644 index 000000000..31025da48 --- /dev/null +++ b/src/internet/seafileurlhandler.cpp @@ -0,0 +1,22 @@ +#include "seafileurlhandler.h" + +#include "seafileservice.h" + +SeafileUrlHandler::SeafileUrlHandler(SeafileService* service, QObject* parent) + : UrlHandler(parent), service_(service) {} + +UrlHandler::LoadResult SeafileUrlHandler::StartLoading(const QUrl& url) { + QString file_library_and_path = url.path(); + QRegExp reg("/([^/]+)(/.*)$"); + + if(reg.indexIn(file_library_and_path) == -1) { + qLog(Debug) << "Can't find repo and file path in " << url; + } + + QString library = reg.cap(1); + QString filepath = reg.cap(2); + + QUrl real_url = service_->GetStreamingUrlFromSongId(library, filepath); + + return LoadResult(url, LoadResult::TrackAvailable, real_url); +} diff --git a/src/internet/seafileurlhandler.h b/src/internet/seafileurlhandler.h new file mode 100644 index 000000000..e378d8833 --- /dev/null +++ b/src/internet/seafileurlhandler.h @@ -0,0 +1,21 @@ +#ifndef SEAFILEURLHANDLER_H +#define SEAFILEURLHANDLER_H + +#include "core/urlhandler.h" + +class SeafileService; + +class SeafileUrlHandler : public UrlHandler { + Q_OBJECT + public: + SeafileUrlHandler(SeafileService* service, QObject* parent = nullptr); + + QString scheme() const { return "seafile"; } + QIcon icon() const { return QIcon(":/providers/seafile.png"); } + LoadResult StartLoading(const QUrl& url); + + private: + SeafileService* service_; +}; + +#endif // SEAFILEURLHANDLER_H diff --git a/src/ui/settingsdialog.cpp b/src/ui/settingsdialog.cpp index 8a6c1e70e..b23017a35 100644 --- a/src/ui/settingsdialog.cpp +++ b/src/ui/settingsdialog.cpp @@ -79,6 +79,12 @@ #include "internet/skydrivesettingspage.h" #endif +#ifdef HAVE_SEAFILE +#include "internet/seafilesettingspage.h" +#endif + + + #include #include #include @@ -183,6 +189,12 @@ SettingsDialog::SettingsDialog(Application* app, BackgroundStreams* streams, AddPage(Page_Vk, new VkSettingsPage(this), providers); #endif +#ifdef HAVE_SEAFILE + AddPage(Page_Seafile, new SeafileSettingsPage(this), providers); +#endif + + + AddPage(Page_Magnatune, new MagnatuneSettingsPage(this), providers); AddPage(Page_DigitallyImported, new DigitallyImportedSettingsPage(this), providers); diff --git a/src/ui/settingsdialog.h b/src/ui/settingsdialog.h index 2880a5423..d3d913c71 100644 --- a/src/ui/settingsdialog.h +++ b/src/ui/settingsdialog.h @@ -84,7 +84,8 @@ class SettingsDialog : public QDialog { Page_Dropbox, Page_Skydrive, Page_Box, - Page_Vk + Page_Vk, + Page_Seafile }; enum Role { Role_IsSeparator = Qt::UserRole };