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 };