Merge pull request #4392 from Chocobozzz/master

Add Seafile Support
This commit is contained in:
John Maguire 2014-06-05 16:50:45 +02:00
commit 96ad4554d9
19 changed files with 1794 additions and 4 deletions

View File

@ -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
)

View File

@ -386,6 +386,7 @@
<file>schema/schema-44.sql</file>
<file>schema/schema-45.sql</file>
<file>schema/schema-46.sql</file>
<file>schema/schema-47.sql</file>
<file>schema/schema-4.sql</file>
<file>schema/schema-5.sql</file>
<file>schema/schema-6.sql</file>
@ -424,5 +425,6 @@
<file>vk/deactivated.gif</file>
<file>providers/vk.png</file>
<file>vk/link.png</file>
<file>providers/seafile.png</file>
</qresource>
</RCC>

BIN
data/providers/seafile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

50
data/schema/schema-47.sql Normal file
View File

@ -0,0 +1,50 @@
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,
performer TEXT,
grouping TEXT
);
CREATE VIRTUAL TABLE seafile_songs_fts USING fts3 (
ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment,
tokenize=unicode
);
UPDATE schema_version SET version=47;

View File

@ -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

View File

@ -36,6 +36,7 @@
#cmakedefine HAVE_LIBPULSE
#cmakedefine HAVE_MOODBAR
#cmakedefine HAVE_QCA
#cmakedefine HAVE_SEAFILE
#cmakedefine HAVE_SKYDRIVE
#cmakedefine HAVE_SPARKLE
#cmakedefine HAVE_SPOTIFY_DOWNLOADER

View File

@ -39,7 +39,7 @@
#include <QVariant>
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;

View File

@ -53,6 +53,9 @@
#ifdef HAVE_VK
#include "vkservice.h"
#endif
#ifdef HAVE_SEAFILE
#include "seafileservice.h"
#endif
using smart_playlists::Generator;
using smart_playlists::GeneratorMimeData;
@ -95,6 +98,9 @@ InternetModel::InternetModel(Application* app, QObject* parent)
#ifdef HAVE_GOOGLE_DRIVE
AddService(new GoogleDriveService(app, this));
#endif
#ifdef HAVE_SEAFILE
AddService(new SeafileService(app, this));
#endif
#ifdef HAVE_SKYDRIVE
AddService(new SkydriveService(app, this));
#endif
@ -223,8 +229,9 @@ QStringList InternetModel::mimeTypes() const {
QMimeData* InternetModel::mimeData(const QModelIndexList& indexes) const {
// Special case for when the user double clicked on a special item.
if (indexes.count() == 1 && indexes[0].data(Role_PlayBehaviour).toInt() ==
PlayBehaviour_DoubleClickAction) {
if (indexes.count() == 1 &&
indexes[0].data(Role_PlayBehaviour).toInt() ==
PlayBehaviour_DoubleClickAction) {
InternetModel::ServiceForIndex(indexes[0])
->ItemDoubleClicked(itemFromIndex(indexes[0]));
return nullptr;

View File

@ -0,0 +1,597 @@
#include "seafileservice.h"
#include <qjson/parser.h>
#include <QTimer>
#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* kAuthTokenUrl = "/api2/auth-token/";
static const char* kFolderItemsUrl = "/api2/repos/%1/dir/";
static const char* kListReposUrl = "/api2/repos/";
static const char* kFileUrl = "/api2/repos/%1/file/";
static const char* kFileContentUrl = "/api2/repos/%1/file/detail/";
static const int kMaxTries = 10;
}
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)));
}
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("Token %1").arg(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 + kAuthTokenUrl);
QNetworkRequest request(url);
AddAuthorizationHeader(&request);
url.addQueryItem("username", mail);
url.addQueryItem("password", password);
QNetworkReply* reply = network_->post(request, url.encodedQuery());
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_ + kListReposUrl);
QNetworkRequest request(url);
AddAuthorizationHeader(&request);
QNetworkReply* reply = network_->get(request);
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<QString, QString> libraries;
QByteArray data = reply->readAll();
QJson::Parser parser;
QList<QVariant> repos = parser.parse(data).toList();
for (int i = 0; i < repos.size(); ++i) {
QVariantMap repo = repos.at(i).toMap();
QString repo_name = repo["name"].toString(),
repo_id = repo["id"].toString();
// One library can appear several times and we don't add encrypted libraries
// (not supported yet)
if (!libraries.contains(repo_id) && !repo["encrypted"].toBool()) {
libraries.insert(repo_id, repo_name);
}
}
emit GetLibrariesFinishedSignal(libraries);
}
void SeafileService::ChangeLibrary(const QString& new_library) {
// Every other libraries have to be destroyed from the tree
if (new_library != "all") {
for (SeafileTree::TreeItem* library : tree_.libraries()) {
if (new_library != library->entry().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<QString, QString>)),
this, SLOT(UpdateLibrariesInProgress(QMap<QString, QString>)));
GetLibraries();
}
void SeafileService::UpdateLibrariesInProgress(
const QMap<QString, QString>& libraries) {
disconnect(this, SIGNAL(GetLibrariesFinishedSignal(QMap<QString, QString>)),
this, SLOT(UpdateLibrariesInProgress(QMap<QString, QString>)));
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<QString, QString> 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(kFolderItemsUrl).arg(library));
url.addQueryItem("p", path);
QNetworkRequest request(url);
AddAuthorizationHeader(&request);
QNetworkReply* reply = network_->get(request);
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<QVariant> 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<QVariant> 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(kFileContentUrl).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);
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<QPair<QString, SeafileTree::Entry>> 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<QString, SeafileTree::Entry>& 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, int tries) {
if (!(*reply)) {
return false;
}
else if (tries > kMaxTries) {
(*reply)->deleteLater();
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...";
int seconds_to_wait;
if ((*reply)->hasRawHeader("X-Throttle-Wait-Seconds")) {
seconds_to_wait =
((*reply)->rawHeader("X-Throttle-Wait-Seconds").toInt() + 1) * 1000;
} else {
seconds_to_wait = pow(tries, 2) * 1000;
}
QTimer timer;
timer.start(seconds_to_wait);
WaitForSignal(&timer, SIGNAL(timeout()));
(*reply)->deleteLater();
// We execute the reply again
*reply = network_->get((*reply)->request());
WaitForSignal(*reply, SIGNAL(finished()));
return CheckReply(reply, ++tries);
}
}
// Unknown, 404 ...
(*reply)->deleteLater();
qLog(Warning) << "Error for reply : " << status_code_variant.toInt();
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);
}

View File

@ -0,0 +1,135 @@
/* Contacts (for explanations, congratulations, insults) :
* - <florian.bigard@gmail.com>
*
* 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 <QDateTime>
#include <QMutex>
#include "seafiletree.h"
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<QString, QString>);
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<QString, QString>& 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, int tries = 1);
SeafileTree tree_;
QString access_token_;
QString server_;
QString library_updated_;
};
#endif // SEAFILESERVICE_H

View File

@ -0,0 +1,151 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
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 <http://www.gnu.org/licenses/>.
*/
#include "seafilesettingspage.h"
#include <QMessageBox>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSettings>
#include <QtDebug>
#include "seafileservice.h"
#include "internetmodel.h"
#include "core/logging.h"
#include "core/network.h"
#include "ui_seafilesettingspage.h"
#include "ui/iconloader.h"
SeafileSettingsPage::SeafileSettingsPage(SettingsDialog* dialog)
: SettingsPage(dialog),
ui_(new Ui_SeafileSettingsPage),
service_(InternetModel::Service<SeafileService>()) {
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<QString, QString>)),
this, SLOT(GetLibrariesFinished(QMap<QString, QString>)));
}
SeafileSettingsPage::~SeafileSettingsPage() {}
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(
const QMap<QString, QString>& 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<QString, QString> 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);
}

View File

@ -0,0 +1,50 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
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 <http://www.gnu.org/licenses/>.
*/
#ifndef SEAFILESETTINGSPAGE_H
#define SEAFILESETTINGSPAGE_H
#include "ui/settingspage.h"
#include <QModelIndex>
#include <QWidget>
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(const QMap<QString, QString>& libraries);
private:
std::unique_ptr<Ui_SeafileSettingsPage> ui_;
SeafileService* service_;
};
#endif // SEAFILESETTINGSPAGE_H

View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SeafileSettingsPage</class>
<widget class="QWidget" name="SeafileSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>261</height>
</rect>
</property>
<property name="windowTitle">
<string>Seafile</string>
</property>
<property name="toolTip">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="LoginStateWidget" name="login_state" native="true"/>
</item>
<item>
<widget class="QGroupBox" name="account_group">
<property name="title">
<string>Account details</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QWidget" name="login_container" native="true">
<property name="enabled">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="2" column="0">
<widget class="QLabel" name="password_label">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="mail_label">
<property name="text">
<string>Email</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="login_button">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="server_label">
<property name="text">
<string>Server</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QLineEdit" name="mail">
<property name="placeholderText">
<string/>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="server">
<property name="placeholderText">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="preference_group">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Preference</string>
</property>
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>19</y>
<width>431</width>
<height>31</height>
</rect>
</property>
<layout class="QGridLayout" name="preference_container">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item row="0" column="1">
<widget class="QComboBox" name="library_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="library_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Library</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LoginStateWidget</class>
<extends>QWidget</extends>
<header>widgets/loginstatewidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>server</tabstop>
<tabstop>mail</tabstop>
<tabstop>password</tabstop>
<tabstop>login_button</tabstop>
<tabstop>library_box</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,377 @@
#include "seafiletree.h"
#include <QStringList>
#include <QRegExp>
#include <QDir>
#include "core/logging.h"
SeafileTree::SeafileTree() {}
SeafileTree::SeafileTree(const SeafileTree& copy) {
libraries_ = copy.libraries();
}
QList<SeafileTree::TreeItem*> 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->children_entries();
for (const Entry& server_entry : server_entries) {
bool is_in_tree = false;
for (int i = 0; i < local_entries.size(); ++i) {
Entry local_entry = local_entries.at(i);
// We found the entry in the tree
if (local_entry.name() == server_entry.name() &&
local_entry.type() == server_entry.type()) {
is_in_tree = true;
// Need to update
if (local_entry.id() != server_entry.id()) {
emit ToUpdate(library.id(), path, server_entry);
// Set the new id to the local entry
local_entry.set_id(server_entry.id());
}
// local_entries could be named "local_entries_to_proceed"
// So we delete from the list the entry we just proceeded
local_entries.removeAt(i);
break;
}
}
// Need to add the entry
if (!is_in_tree) {
emit ToAdd(library.id(), path, server_entry);
}
}
// Each entry in this list corresponds to an entry that we didn't proceed
// So if the entry is in the tree but not on the server : we have to delete it
for (const Entry& local_entry : local_entries) {
emit ToDelete(library.id(), path, local_entry);
}
}
SeafileTree::TreeItem* SeafileTree::FindLibrary(const QString& library) {
for (TreeItem* item : libraries_) {
if (item->entry().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<QPair<QString, SeafileTree::Entry>> SeafileTree::GetRecursiveFilesOfDir(
const QString& path, const TreeItem* item) {
// key = path, value = entry
QList<QPair<QString, Entry>> 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->children()) {
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 ################################# */
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 {
return a.name() == name() && a.id() == id() && a.type() == type();
}
bool SeafileTree::Entry::operator!=(const Entry& a) const {
return !(operator==(a));
}
SeafileTree::Entry::~Entry() {}
QString SeafileTree::Entry::TypeToString(const Type& type) {
switch (type) {
case DIR:
return "dir";
case FILE:
return "file";
case LIBRARY:
return "library";
default:
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::Entries SeafileTree::TreeItem::children_entries() const {
Entries entries;
for (TreeItem* item : children_) {
entries.append(Entry(item->entry()));
}
return entries;
}
SeafileTree::TreeItem* SeafileTree::TreeItem::child(int i) const {
return children_.at(i);
}
QList<SeafileTree::TreeItem*> SeafileTree::TreeItem::children() const {
return children_;
}
SeafileTree::Entry SeafileTree::TreeItem::entry() const { return entry_; }
void SeafileTree::TreeItem::set_entry(const Entry& entry) { entry_ = entry; }
void SeafileTree::TreeItem::set_children(const QList<TreeItem*>& children) {
children_ = children;
}
void SeafileTree::TreeItem::AppendChild(TreeItem* child) {
children_.append(child);
}
void SeafileTree::TreeItem::AppendChild(const Entry& entry) {
children_.append(new TreeItem(entry));
}
bool SeafileTree::TreeItem::RemoveChild(TreeItem* child) {
return children_.removeOne(child);
}
SeafileTree::TreeItem* SeafileTree::TreeItem::FindChild(
const QString& name) const {
for (TreeItem* item : children_) {
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 : children_) {
res += item->ToString(i + 1);
}
return res;
}
SeafileTree::TreeItem::~TreeItem() {
// We need to delete children
qDeleteAll(children_);
}
QDataStream& operator<<(QDataStream& out, const SeafileTree::Entry& entry) {
out << entry.name_ << entry.id_ << static_cast<quint8>(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->children_;
return out;
}
QDataStream& operator>>(QDataStream& in, SeafileTree::TreeItem*& item) {
SeafileTree::Entry entry;
QList<SeafileTree::TreeItem*> children;
in >> entry;
in >> children;
item = new SeafileTree::TreeItem(entry, children);
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;
}

167
src/internet/seafiletree.h Normal file
View File

@ -0,0 +1,167 @@
/* Contacts (for explanations, congratulations, insults) :
* - <florian.bigard@gmail.com>
*/
#ifndef SEAFILETREE_H
#define SEAFILETREE_H
#include <QObject>
#include <QString>
#include <QList>
#include <QPair>
#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& copy);
~SeafileTree();
class Entry {
public:
enum Type { DIR = 0, FILE = 1, LIBRARY = 2, NONE = 3 };
Entry(const QString& name = QString(), const QString& id = QString(),
const Type& type = NONE)
: name_(name), id_(id), type_(type) {}
Entry(const Entry& entry)
: name_(entry.name()), id_(entry.id()), type_(entry.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<Entry> Entries;
// Node of the tree
// Contains an entry
class TreeItem {
public:
TreeItem(const Entry& entry = Entry(),
const QList<TreeItem*>& children = QList<TreeItem*>())
: entry_(entry), children_(children) {}
TreeItem(const TreeItem& copy)
: entry_(copy.entry()), children_(copy.children()) {}
~TreeItem();
TreeItem* child(int i) const;
QList<TreeItem*> children() const;
// List of each child's entry
Entries children_entries() const;
void set_children(const QList<TreeItem*>& children);
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<TreeItem*> children_;
friend QDataStream& operator<<(QDataStream& out,
SeafileTree::TreeItem* item);
friend QDataStream& operator>>(QDataStream& in,
SeafileTree::TreeItem*& item);
};
QList<TreeItem*> 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<QPair<QString, SeafileTree::Entry>> 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<TreeItem*> 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

View File

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

View File

@ -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

View File

@ -79,6 +79,12 @@
#include "internet/skydrivesettingspage.h"
#endif
#ifdef HAVE_SEAFILE
#include "internet/seafilesettingspage.h"
#endif
#include <QAbstractButton>
#include <QDesktopWidget>
#include <QPainter>
@ -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);

View File

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