mirror of
https://github.com/clementine-player/Clementine
synced 2025-01-31 19:45:31 +01:00
Merge branch 'subsonic'
Conflicts: data/data.qrc
This commit is contained in:
commit
fe2fb788d3
@ -1,6 +1,7 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>blank.ttf</file>
|
||||
<file>clementine_remote_qr.png</file>
|
||||
<file>clementine-spotify-public.pem</file>
|
||||
<file>currenttrack_bar_left.png</file>
|
||||
<file>currenttrack_bar_mid.png</file>
|
||||
@ -296,6 +297,8 @@
|
||||
<file>providers/somafm.png</file>
|
||||
<file>providers/songkick.png</file>
|
||||
<file>providers/soundcloud.png</file>
|
||||
<file>providers/subsonic-32.png</file>
|
||||
<file>providers/subsonic.png</file>
|
||||
<file>providers/ubuntuone.png</file>
|
||||
<file>providers/wikipedia.png</file>
|
||||
<file>sample.mood</file>
|
||||
@ -337,6 +340,7 @@
|
||||
<file>schema/schema-40.sql</file>
|
||||
<file>schema/schema-41.sql</file>
|
||||
<file>schema/schema-42.sql</file>
|
||||
<file>schema/schema-43.sql</file>
|
||||
<file>schema/schema-4.sql</file>
|
||||
<file>schema/schema-5.sql</file>
|
||||
<file>schema/schema-6.sql</file>
|
||||
@ -358,6 +362,5 @@
|
||||
<file>volumeslider-handle_glow.png</file>
|
||||
<file>volumeslider-handle.png</file>
|
||||
<file>volumeslider-inset.png</file>
|
||||
<file>clementine_remote_qr.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
BIN
data/providers/subsonic-32.png
Normal file
BIN
data/providers/subsonic-32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
data/providers/subsonic.png
Normal file
BIN
data/providers/subsonic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 665 B |
48
data/schema/schema-43.sql
Normal file
48
data/schema/schema-43.sql
Normal file
@ -0,0 +1,48 @@
|
||||
CREATE TABLE subsonic_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 subsonic_songs_fts USING fts3 (
|
||||
ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsgenre, ftscomment,
|
||||
tokenize=unicode
|
||||
);
|
||||
|
||||
UPDATE schema_version SET version=43;
|
@ -194,6 +194,9 @@ set(SOURCES
|
||||
internet/somafmservice.cpp
|
||||
internet/somafmurlhandler.cpp
|
||||
internet/soundcloudservice.cpp
|
||||
internet/subsonicservice.cpp
|
||||
internet/subsonicsettingspage.cpp
|
||||
internet/subsonicurlhandler.cpp
|
||||
|
||||
library/groupbydialog.cpp
|
||||
library/library.cpp
|
||||
@ -478,6 +481,9 @@ set(HEADERS
|
||||
internet/somafmservice.h
|
||||
internet/somafmurlhandler.h
|
||||
internet/soundcloudservice.h
|
||||
internet/subsonicservice.h
|
||||
internet/subsonicsettingspage.h
|
||||
internet/subsonicurlhandler.h
|
||||
|
||||
library/groupbydialog.h
|
||||
library/library.h
|
||||
@ -660,6 +666,7 @@ set(UI
|
||||
internet/magnatunesettingspage.ui
|
||||
internet/searchboxwidget.ui
|
||||
internet/spotifysettingspage.ui
|
||||
internet/subsonicsettingspage.ui
|
||||
|
||||
library/groupbydialog.ui
|
||||
library/libraryfilterwidget.ui
|
||||
|
@ -37,7 +37,7 @@
|
||||
#include <QVariant>
|
||||
|
||||
const char* Database::kDatabaseFilename = "clementine.db";
|
||||
const int Database::kSchemaVersion = 42;
|
||||
const int Database::kSchemaVersion = 43;
|
||||
const char* Database::kMagicAllSongsTables = "%allsongstables";
|
||||
|
||||
int Database::sNextConnectionId = 1;
|
||||
|
@ -29,7 +29,9 @@
|
||||
#include "internetservice.h"
|
||||
#include "savedradio.h"
|
||||
#include "somafmservice.h"
|
||||
#include "groovesharkservice.h"
|
||||
#include "soundcloudservice.h"
|
||||
#include "subsonicservice.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/mergedproxymodel.h"
|
||||
@ -94,6 +96,7 @@ InternetModel::InternetModel(Application* app, QObject* parent)
|
||||
#ifdef HAVE_SPOTIFY
|
||||
AddService(new SpotifyService(app, this));
|
||||
#endif
|
||||
AddService(new SubsonicService(app, this));
|
||||
#ifdef HAVE_UBUNTU_ONE
|
||||
AddService(new UbuntuOneService(app, this));
|
||||
#endif
|
||||
|
433
src/internet/subsonicservice.cpp
Normal file
433
src/internet/subsonicservice.cpp
Normal file
@ -0,0 +1,433 @@
|
||||
#include "subsonicurlhandler.h"
|
||||
#include "subsonicservice.h"
|
||||
#include "internetmodel.h"
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/player.h"
|
||||
#include "core/utilities.h"
|
||||
#include "ui/iconloader.h"
|
||||
#include "library/librarybackend.h"
|
||||
#include "library/libraryfilterwidget.h"
|
||||
#include "core/mergedproxymodel.h"
|
||||
#include "core/database.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "globalsearch/globalsearch.h"
|
||||
#include "globalsearch/librarysearchprovider.h"
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkCookieJar>
|
||||
#include <QSslConfiguration>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QMenu>
|
||||
|
||||
const char* SubsonicService::kServiceName = "Subsonic";
|
||||
const char* SubsonicService::kSettingsGroup = "Subsonic";
|
||||
const char* SubsonicService::kApiVersion = "1.8.0";
|
||||
const char* SubsonicService::kApiClientName = "Clementine";
|
||||
|
||||
const char* SubsonicService::kSongsTable = "subsonic_songs";
|
||||
const char* SubsonicService::kFtsTable = "subsonic_songs_fts";
|
||||
|
||||
SubsonicService::SubsonicService(Application* app, InternetModel *parent)
|
||||
: InternetService(kServiceName, app, parent, parent),
|
||||
network_(new QNetworkAccessManager(this)),
|
||||
url_handler_(new SubsonicUrlHandler(this, this)),
|
||||
scanner_(new SubsonicLibraryScanner(this, this)),
|
||||
load_database_task_id_(0),
|
||||
context_menu_(NULL),
|
||||
root_(NULL),
|
||||
library_backend_(NULL),
|
||||
library_model_(NULL),
|
||||
library_filter_(NULL),
|
||||
library_sort_model_(new QSortFilterProxyModel(this)),
|
||||
total_song_count_(0),
|
||||
login_state_(LoginState_OtherError)
|
||||
{
|
||||
app_->player()->RegisterUrlHandler(url_handler_);
|
||||
|
||||
connect(scanner_, SIGNAL(ScanFinished()),
|
||||
SLOT(ReloadDatabaseFinished()));
|
||||
|
||||
library_backend_ = new LibraryBackend;
|
||||
library_backend_->moveToThread(app_->database()->thread());
|
||||
library_backend_->Init(app_->database(),
|
||||
kSongsTable,
|
||||
QString::null,
|
||||
QString::null,
|
||||
kFtsTable);
|
||||
connect(library_backend_, SIGNAL(TotalSongCountUpdated(int)),
|
||||
SLOT(UpdateTotalSongCount(int)));
|
||||
|
||||
library_model_ = new LibraryModel(library_backend_, app_, this);
|
||||
library_model_->set_show_various_artists(false);
|
||||
library_model_->set_show_smart_playlists(false);
|
||||
|
||||
library_filter_ = new LibraryFilterWidget(0);
|
||||
library_filter_->SetSettingsGroup(kSettingsGroup);
|
||||
library_filter_->SetLibraryModel(library_model_);
|
||||
library_filter_->SetFilterHint(tr("Search Subsonic"));
|
||||
library_filter_->SetAgeFilterEnabled(false);
|
||||
|
||||
library_sort_model_->setSourceModel(library_model_);
|
||||
library_sort_model_->setSortRole(LibraryModel::Role_SortText);
|
||||
library_sort_model_->setDynamicSortFilter(true);
|
||||
library_sort_model_->sort(0);
|
||||
|
||||
connect(this, SIGNAL(LoginStateChanged(SubsonicService::LoginState)),
|
||||
SLOT(onLoginStateChanged(SubsonicService::LoginState)));
|
||||
|
||||
context_menu_ = new QMenu;
|
||||
context_menu_->addActions(GetPlaylistActions());
|
||||
context_menu_->addSeparator();
|
||||
context_menu_->addAction(IconLoader::Load("view-refresh"), tr("Refresh catalogue"), this, SLOT(ReloadDatabase()));
|
||||
QAction* config_action = context_menu_->addAction(IconLoader::Load("configure"),
|
||||
tr("Configure Subsonic..."), this, SLOT(ShowConfig()));
|
||||
context_menu_->addSeparator();
|
||||
context_menu_->addMenu(library_filter_->menu());
|
||||
|
||||
library_filter_->AddMenuAction(config_action);
|
||||
|
||||
app_->global_search()->AddProvider(new LibrarySearchProvider(
|
||||
library_backend_,
|
||||
tr("Subsonic"),
|
||||
"subsonic",
|
||||
QIcon(":/providers/subsonic.png"),
|
||||
true, app_, this));
|
||||
}
|
||||
|
||||
SubsonicService::~SubsonicService()
|
||||
{
|
||||
}
|
||||
|
||||
QStandardItem* SubsonicService::CreateRootItem()
|
||||
{
|
||||
root_ = new QStandardItem(QIcon(":providers/subsonic.png"), kServiceName);
|
||||
root_->setData(true, InternetModel::Role_CanLazyLoad);
|
||||
return root_;
|
||||
}
|
||||
|
||||
void SubsonicService::LazyPopulate(QStandardItem *item)
|
||||
{
|
||||
switch (item->data(InternetModel::Role_Type).toInt())
|
||||
{
|
||||
case InternetModel::Type_Service:
|
||||
library_model_->Init();
|
||||
if (login_state() != LoginState_Loggedin) {
|
||||
ShowConfig();
|
||||
} else if (total_song_count_ == 0 && !load_database_task_id_) {
|
||||
ReloadDatabase();
|
||||
}
|
||||
model()->merged_model()->AddSubModel(item->index(), library_sort_model_);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SubsonicService::ShowContextMenu(const QPoint &global_pos)
|
||||
{
|
||||
const bool is_valid = model()->current_index().model() == library_sort_model_;
|
||||
|
||||
GetAppendToPlaylistAction()->setEnabled(is_valid);
|
||||
GetReplacePlaylistAction()->setEnabled(is_valid);
|
||||
GetOpenInNewPlaylistAction()->setEnabled(is_valid);
|
||||
context_menu_->popup(global_pos);
|
||||
}
|
||||
|
||||
QWidget* SubsonicService::HeaderWidget() const
|
||||
{
|
||||
return library_filter_;
|
||||
}
|
||||
|
||||
void SubsonicService::ReloadSettings()
|
||||
{
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
server_ = s.value("server").toString();
|
||||
username_ = s.value("username").toString();
|
||||
password_ = s.value("password").toString();
|
||||
|
||||
Login();
|
||||
}
|
||||
|
||||
void SubsonicService::Login()
|
||||
{
|
||||
// Forget session ID
|
||||
network_->setCookieJar(new QNetworkCookieJar(network_));
|
||||
// Forget login state whilst waiting
|
||||
login_state_ = LoginState_Unknown;
|
||||
// Ping is enough to check credentials
|
||||
Ping();
|
||||
}
|
||||
|
||||
void SubsonicService::Login(const QString &server, const QString &username, const QString &password)
|
||||
{
|
||||
server_ = QString(server);
|
||||
username_ = QString(username);
|
||||
password_ = QString(password);
|
||||
Login();
|
||||
}
|
||||
|
||||
void SubsonicService::Ping()
|
||||
{
|
||||
QNetworkReply* reply = Send(BuildRequestUrl("ping"));
|
||||
NewClosure(reply, SIGNAL(finished()),
|
||||
this, SLOT(onPingFinished(QNetworkReply*)),
|
||||
reply);
|
||||
}
|
||||
|
||||
QUrl SubsonicService::BuildRequestUrl(const QString &view)
|
||||
{
|
||||
QUrl url(server_ + "rest/" + view + ".view");
|
||||
url.addQueryItem("v", kApiVersion);
|
||||
url.addQueryItem("c", kApiClientName);
|
||||
url.addQueryItem("u", username_);
|
||||
url.addQueryItem("p", password_);
|
||||
return url;
|
||||
}
|
||||
|
||||
QNetworkReply* SubsonicService::Send(const QUrl &url)
|
||||
{
|
||||
QNetworkRequest request(url);
|
||||
// Don't try and check the authenticity of the SSL certificate - it'll almost
|
||||
// certainly be self-signed.
|
||||
QSslConfiguration sslconfig = QSslConfiguration::defaultConfiguration();
|
||||
sslconfig.setPeerVerifyMode(QSslSocket::VerifyNone);
|
||||
request.setSslConfiguration(sslconfig);
|
||||
QNetworkReply *reply = network_->get(request);
|
||||
return reply;
|
||||
}
|
||||
|
||||
void SubsonicService::UpdateTotalSongCount(int count)
|
||||
{
|
||||
total_song_count_ = count;
|
||||
}
|
||||
|
||||
void SubsonicService::ReloadDatabase()
|
||||
{
|
||||
if (!load_database_task_id_)
|
||||
load_database_task_id_ = app_->task_manager()->StartTask(tr("Fetching Subsonic library"));
|
||||
scanner_->Scan();
|
||||
}
|
||||
|
||||
void SubsonicService::ReloadDatabaseFinished()
|
||||
{
|
||||
app_->task_manager()->SetTaskFinished(load_database_task_id_);
|
||||
load_database_task_id_ = 0;
|
||||
|
||||
library_backend_->DeleteAll();
|
||||
library_backend_->AddOrUpdateSongs(scanner_->GetSongs());
|
||||
library_model_->Reset();
|
||||
}
|
||||
|
||||
void SubsonicService::onLoginStateChanged(SubsonicService::LoginState newstate)
|
||||
{
|
||||
// TODO: library refresh logic?
|
||||
}
|
||||
|
||||
void SubsonicService::onPingFinished(QNetworkReply *reply)
|
||||
{
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
login_state_ = LoginState_BadServer;
|
||||
qLog(Error) << "Failed to connect ("
|
||||
<< Utilities::EnumToString(QNetworkReply::staticMetaObject, "NetworkError", reply->error())
|
||||
<< "):" << reply->errorString();
|
||||
}
|
||||
else
|
||||
{
|
||||
QXmlStreamReader reader(reply);
|
||||
reader.readNextStartElement();
|
||||
QStringRef status = reader.attributes().value("status");
|
||||
if (status == "ok")
|
||||
{
|
||||
login_state_ = LoginState_Loggedin;
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.readNextStartElement();
|
||||
int error = reader.attributes().value("code").toString().toInt();
|
||||
qLog(Error) << "Subsonic error ("
|
||||
<< Utilities::EnumToString(SubsonicService::staticMetaObject, "ApiError", error)
|
||||
<< "):" << reader.attributes().value("message").toString();
|
||||
switch (error)
|
||||
{
|
||||
// "Parameter missing" for "ping" is always blank username or password
|
||||
case ApiError_ParameterMissing:
|
||||
case ApiError_BadCredentials:
|
||||
login_state_ = LoginState_BadCredentials;
|
||||
break;
|
||||
case ApiError_OutdatedClient:
|
||||
login_state_ = LoginState_OutdatedClient;
|
||||
break;
|
||||
case ApiError_OutdatedServer:
|
||||
login_state_ = LoginState_OutdatedServer;
|
||||
break;
|
||||
case ApiError_Unlicensed:
|
||||
login_state_ = LoginState_Unlicensed;
|
||||
break;
|
||||
default:
|
||||
login_state_ = LoginState_OtherError;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
qLog(Debug) << "Login state changed:"
|
||||
<< Utilities::EnumToString(SubsonicService::staticMetaObject, "LoginState", login_state_);
|
||||
emit LoginStateChanged(login_state_);
|
||||
}
|
||||
|
||||
void SubsonicService::ShowConfig()
|
||||
{
|
||||
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Subsonic);
|
||||
}
|
||||
|
||||
|
||||
const int SubsonicLibraryScanner::kAlbumChunkSize = 500;
|
||||
const int SubsonicLibraryScanner::kConcurrentRequests = 8;
|
||||
|
||||
SubsonicLibraryScanner::SubsonicLibraryScanner(SubsonicService* service, QObject* parent)
|
||||
: QObject(parent),
|
||||
service_(service),
|
||||
scanning_(false)
|
||||
{
|
||||
}
|
||||
|
||||
SubsonicLibraryScanner::~SubsonicLibraryScanner()
|
||||
{
|
||||
}
|
||||
|
||||
void SubsonicLibraryScanner::Scan()
|
||||
{
|
||||
if (scanning_)
|
||||
return;
|
||||
|
||||
album_queue_.clear();
|
||||
pending_requests_.clear();
|
||||
songs_.clear();
|
||||
scanning_ = true;
|
||||
GetAlbumList(0);
|
||||
}
|
||||
|
||||
void SubsonicLibraryScanner::onGetAlbumListFinished(QNetworkReply *reply, int offset)
|
||||
{
|
||||
reply->deleteLater();
|
||||
|
||||
QXmlStreamReader reader(reply);
|
||||
reader.readNextStartElement();
|
||||
Q_ASSERT(reader.name() == "subsonic-response");
|
||||
if (reader.attributes().value("status") != "ok") {
|
||||
// TODO: error handling
|
||||
return;
|
||||
}
|
||||
|
||||
int albums_added = 0;
|
||||
reader.readNextStartElement();
|
||||
Q_ASSERT(reader.name() == "albumList2");
|
||||
while (reader.readNextStartElement()) {
|
||||
Q_ASSERT(reader.name() == "album");
|
||||
album_queue_ << reader.attributes().value("id").toString();
|
||||
albums_added++;
|
||||
reader.skipCurrentElement();
|
||||
}
|
||||
|
||||
if (albums_added > 0) {
|
||||
// Non-empty reply means potentially more albums to fetch
|
||||
GetAlbumList(offset + kAlbumChunkSize);
|
||||
} else if (album_queue_.size() == 0) {
|
||||
// Empty reply and no albums means an empty Subsonic server
|
||||
scanning_ = false;
|
||||
} else {
|
||||
// Empty reply but we have some albums, time to start fetching songs
|
||||
// Start up the maximum number of concurrent requests, finished requests get replaced with new ones
|
||||
for (int i = 0; i < kConcurrentRequests && !album_queue_.empty(); ++i) {
|
||||
GetAlbum(album_queue_.dequeue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SubsonicLibraryScanner::onGetAlbumFinished(QNetworkReply *reply)
|
||||
{
|
||||
reply->deleteLater();
|
||||
pending_requests_.remove(reply);
|
||||
|
||||
QXmlStreamReader reader(reply);
|
||||
reader.readNextStartElement();
|
||||
Q_ASSERT(reader.name() == "subsonic-response");
|
||||
if (reader.attributes().value("status") != "ok") {
|
||||
// TODO: error handling
|
||||
return;
|
||||
}
|
||||
|
||||
// Read album information
|
||||
reader.readNextStartElement();
|
||||
Q_ASSERT(reader.name() == "album");
|
||||
QString album_artist = reader.attributes().value("artist").toString();
|
||||
|
||||
// Read song information
|
||||
while (reader.readNextStartElement()) {
|
||||
Q_ASSERT(reader.name() == "song");
|
||||
Song song;
|
||||
QString id = reader.attributes().value("id").toString();
|
||||
song.set_title(reader.attributes().value("title").toString());
|
||||
song.set_album(reader.attributes().value("album").toString());
|
||||
song.set_track(reader.attributes().value("track").toString().toInt());
|
||||
song.set_artist(reader.attributes().value("artist").toString());
|
||||
song.set_albumartist(album_artist);
|
||||
song.set_bitrate(reader.attributes().value("bitRate").toString().toInt());
|
||||
song.set_year(reader.attributes().value("year").toString().toInt());
|
||||
song.set_genre(reader.attributes().value("genre").toString());
|
||||
qint64 length = reader.attributes().value("duration").toString().toInt();
|
||||
length *= 1000000000;
|
||||
song.set_length_nanosec(length);
|
||||
QUrl url = QUrl(QString("subsonic://%1").arg(id));
|
||||
song.set_url(url);
|
||||
song.set_filesize(reader.attributes().value("size").toString().toInt());
|
||||
// We need to set these to satisfy the database constraints
|
||||
song.set_directory_id(0);
|
||||
song.set_mtime(0);
|
||||
song.set_ctime(0);
|
||||
songs_ << song;
|
||||
reader.skipCurrentElement();
|
||||
}
|
||||
|
||||
// Start the next request if albums remain
|
||||
if (!album_queue_.empty())
|
||||
GetAlbum(album_queue_.dequeue());
|
||||
|
||||
// If this was the last response, we're done!
|
||||
if (album_queue_.empty() && pending_requests_.empty()) {
|
||||
scanning_ = false;
|
||||
emit ScanFinished();
|
||||
}
|
||||
}
|
||||
|
||||
void SubsonicLibraryScanner::GetAlbumList(int offset)
|
||||
{
|
||||
QUrl url = service_->BuildRequestUrl("getAlbumList2");
|
||||
url.addQueryItem("type", "alphabeticalByName");
|
||||
url.addQueryItem("size", QString::number(kAlbumChunkSize));
|
||||
url.addQueryItem("offset", QString::number(offset));
|
||||
QNetworkReply* reply = service_->Send(url);
|
||||
NewClosure(reply, SIGNAL(finished()),
|
||||
this, SLOT(onGetAlbumListFinished(QNetworkReply*,int)),
|
||||
reply, offset);
|
||||
}
|
||||
|
||||
void SubsonicLibraryScanner::GetAlbum(QString id)
|
||||
{
|
||||
QUrl url = service_->BuildRequestUrl("getAlbum");
|
||||
url.addQueryItem("id", id);
|
||||
QNetworkReply* reply = service_->Send(url);
|
||||
NewClosure(reply, SIGNAL(finished()),
|
||||
this, SLOT(onGetAlbumFinished(QNetworkReply*)),
|
||||
reply);
|
||||
pending_requests_.insert(reply);
|
||||
}
|
157
src/internet/subsonicservice.h
Normal file
157
src/internet/subsonicservice.h
Normal file
@ -0,0 +1,157 @@
|
||||
#ifndef SUBSONICSERVICE_H
|
||||
#define SUBSONICSERVICE_H
|
||||
|
||||
#include "internetmodel.h"
|
||||
#include "internetservice.h"
|
||||
|
||||
#include <QQueue>
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QXmlStreamReader;
|
||||
class QSortFilterProxyModel;
|
||||
class QNetworkReply;
|
||||
|
||||
class SubsonicUrlHandler;
|
||||
class SubsonicLibraryScanner;
|
||||
|
||||
class SubsonicService : public InternetService
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_ENUMS(LoginState)
|
||||
Q_ENUMS(ApiError)
|
||||
|
||||
public:
|
||||
SubsonicService(Application* app, InternetModel *parent);
|
||||
~SubsonicService();
|
||||
|
||||
enum LoginState {
|
||||
LoginState_Loggedin,
|
||||
LoginState_BadServer,
|
||||
LoginState_OutdatedClient,
|
||||
LoginState_OutdatedServer,
|
||||
LoginState_BadCredentials,
|
||||
LoginState_Unlicensed,
|
||||
LoginState_OtherError,
|
||||
LoginState_Unknown,
|
||||
};
|
||||
|
||||
enum ApiError {
|
||||
ApiError_Generic = 0,
|
||||
ApiError_ParameterMissing = 10,
|
||||
ApiError_OutdatedClient = 20,
|
||||
ApiError_OutdatedServer = 30,
|
||||
ApiError_BadCredentials = 40,
|
||||
ApiError_Unauthorized = 50,
|
||||
ApiError_Unlicensed = 60,
|
||||
ApiError_NotFound = 70,
|
||||
};
|
||||
|
||||
enum Type {
|
||||
Type_Artist = InternetModel::TypeCount,
|
||||
Type_Album,
|
||||
Type_Track,
|
||||
};
|
||||
|
||||
enum Role {
|
||||
Role_Id = InternetModel::RoleCount,
|
||||
};
|
||||
|
||||
typedef QMap<QString, QString> RequestOptions;
|
||||
|
||||
QStandardItem* CreateRootItem();
|
||||
void LazyPopulate(QStandardItem *item);
|
||||
void ShowContextMenu(const QPoint &global_pos);
|
||||
QWidget* HeaderWidget() const;
|
||||
void ReloadSettings();
|
||||
|
||||
void Login();
|
||||
void Login(const QString &server, const QString &username, const QString &password);
|
||||
LoginState login_state() const { return login_state_; }
|
||||
|
||||
// Subsonic API methods
|
||||
void Ping();
|
||||
|
||||
QUrl BuildRequestUrl(const QString &view);
|
||||
// Convenience function to reduce QNetworkRequest/QSslConfiguration boilerplate
|
||||
QNetworkReply* Send(const QUrl &url);
|
||||
|
||||
static const char* kServiceName;
|
||||
static const char* kSettingsGroup;
|
||||
static const char* kApiVersion;
|
||||
static const char* kApiClientName;
|
||||
|
||||
static const char* kSongsTable;
|
||||
static const char* kFtsTable;
|
||||
|
||||
signals:
|
||||
void LoginStateChanged(SubsonicService::LoginState newstate);
|
||||
|
||||
private:
|
||||
void EnsureMenuCreated();
|
||||
|
||||
QNetworkAccessManager* network_;
|
||||
SubsonicUrlHandler* url_handler_;
|
||||
|
||||
SubsonicLibraryScanner* scanner_;
|
||||
int load_database_task_id_;
|
||||
|
||||
QMenu* context_menu_;
|
||||
QStandardItem* root_;
|
||||
|
||||
LibraryBackend* library_backend_;
|
||||
LibraryModel* library_model_;
|
||||
LibraryFilterWidget* library_filter_;
|
||||
QSortFilterProxyModel* library_sort_model_;
|
||||
int total_song_count_;
|
||||
|
||||
// Configuration
|
||||
QString server_;
|
||||
QString username_;
|
||||
QString password_;
|
||||
|
||||
LoginState login_state_;
|
||||
|
||||
private slots:
|
||||
void UpdateTotalSongCount(int count);
|
||||
void ReloadDatabase();
|
||||
void ReloadDatabaseFinished();
|
||||
void onLoginStateChanged(SubsonicService::LoginState newstate);
|
||||
void onPingFinished(QNetworkReply* reply);
|
||||
|
||||
void ShowConfig();
|
||||
};
|
||||
|
||||
class SubsonicLibraryScanner : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SubsonicLibraryScanner(SubsonicService* service, QObject* parent=0);
|
||||
~SubsonicLibraryScanner();
|
||||
|
||||
void Scan();
|
||||
const SongList& GetSongs() const { return songs_; }
|
||||
|
||||
static const int kAlbumChunkSize;
|
||||
static const int kConcurrentRequests;
|
||||
|
||||
signals:
|
||||
void ScanFinished();
|
||||
|
||||
private slots:
|
||||
// Step 1: use getAlbumList2 type=alphabeticalByName to list all albums
|
||||
void onGetAlbumListFinished(QNetworkReply* reply, int offset);
|
||||
// Step 2: use getAlbum id=? to list all songs for each album
|
||||
void onGetAlbumFinished(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
void GetAlbumList(int offset);
|
||||
void GetAlbum(QString id);
|
||||
|
||||
SubsonicService* service_;
|
||||
bool scanning_;
|
||||
QQueue<QString> album_queue_;
|
||||
QSet<QNetworkReply*> pending_requests_;
|
||||
SongList songs_;
|
||||
};
|
||||
|
||||
#endif // SUBSONICSERVICE_H
|
109
src/internet/subsonicsettingspage.cpp
Normal file
109
src/internet/subsonicsettingspage.cpp
Normal file
@ -0,0 +1,109 @@
|
||||
#include "subsonicsettingspage.h"
|
||||
#include "ui_subsonicsettingspage.h"
|
||||
#include "internetmodel.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
SubsonicSettingsPage::SubsonicSettingsPage(SettingsDialog *dialog)
|
||||
: SettingsPage(dialog),
|
||||
ui_(new Ui_SubsonicSettingsPage),
|
||||
service_(InternetModel::Service<SubsonicService>())
|
||||
{
|
||||
ui_->setupUi(this);
|
||||
setWindowIcon(QIcon(":/providers/subsonic-32.png"));
|
||||
|
||||
connect(ui_->login, SIGNAL(clicked()), SLOT(Login()));
|
||||
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout()));
|
||||
connect(service_, SIGNAL(LoginStateChanged(SubsonicService::LoginState)),
|
||||
SLOT(LoginStateChanged(SubsonicService::LoginState)));
|
||||
|
||||
ui_->login_state->AddCredentialField(ui_->server);
|
||||
ui_->login_state->AddCredentialField(ui_->username);
|
||||
ui_->login_state->AddCredentialField(ui_->password);
|
||||
ui_->login_state->AddCredentialGroup(ui_->server_group);
|
||||
}
|
||||
|
||||
SubsonicSettingsPage::~SubsonicSettingsPage()
|
||||
{
|
||||
delete ui_;
|
||||
}
|
||||
|
||||
void SubsonicSettingsPage::Load()
|
||||
{
|
||||
QSettings s;
|
||||
s.beginGroup(SubsonicService::kSettingsGroup);
|
||||
|
||||
ui_->server->setText(s.value("server").toString());
|
||||
ui_->username->setText(s.value("username").toString());
|
||||
ui_->password->setText(s.value("password").toString());
|
||||
|
||||
// These are the same settings SubsonicService will have used already, so see if
|
||||
// they were successful...
|
||||
LoginStateChanged(service_->login_state());
|
||||
}
|
||||
|
||||
void SubsonicSettingsPage::Save()
|
||||
{
|
||||
QSettings s;
|
||||
s.beginGroup(SubsonicService::kSettingsGroup);
|
||||
|
||||
s.setValue("server", ui_->server->text());
|
||||
s.setValue("username", ui_->username->text());
|
||||
s.setValue("password", ui_->password->text());
|
||||
}
|
||||
|
||||
void SubsonicSettingsPage::LoginStateChanged(SubsonicService::LoginState newstate)
|
||||
{
|
||||
const bool logged_in = newstate == SubsonicService::LoginState_Loggedin;
|
||||
|
||||
ui_->login_state->SetLoggedIn(logged_in ? LoginStateWidget::LoggedIn
|
||||
: LoginStateWidget::LoggedOut,
|
||||
QString("%1 (%2)")
|
||||
.arg(ui_->username->text())
|
||||
.arg(ui_->server->text()));
|
||||
ui_->login_state->SetAccountTypeVisible(!logged_in);
|
||||
|
||||
switch (newstate)
|
||||
{
|
||||
case SubsonicService::LoginState_BadServer:
|
||||
ui_->login_state->SetAccountTypeText(tr("Could not connect to Subsonic, check server URL. "
|
||||
"Example: http://localhost:4040/"));
|
||||
break;
|
||||
|
||||
case SubsonicService::LoginState_BadCredentials:
|
||||
ui_->login_state->SetAccountTypeText(tr("Wrong username or password."));
|
||||
break;
|
||||
|
||||
case SubsonicService::LoginState_OutdatedClient:
|
||||
ui_->login_state->SetAccountTypeText(tr("Incompatible Subsonic REST protocol version. Client must upgrade."));
|
||||
break;
|
||||
|
||||
case SubsonicService::LoginState_OutdatedServer:
|
||||
ui_->login_state->SetAccountTypeText(tr("Incompatible Subsonic REST protocol version. Server must upgrade."));
|
||||
break;
|
||||
|
||||
case SubsonicService::LoginState_Unlicensed:
|
||||
ui_->login_state->SetAccountTypeText(tr("The trial period for the Subsonic server is over. "
|
||||
"Please donate to get a license key. Visit subsonic.org for details."));
|
||||
break;
|
||||
|
||||
case SubsonicService::LoginState_OtherError:
|
||||
ui_->login_state->SetAccountTypeText(tr("An unspecified error occurred."));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SubsonicSettingsPage::Login()
|
||||
{
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
|
||||
service_->Login(ui_->server->text(), ui_->username->text(), ui_->password->text());
|
||||
}
|
||||
|
||||
void SubsonicSettingsPage::Logout()
|
||||
{
|
||||
ui_->username->setText("");
|
||||
ui_->password->setText("");
|
||||
}
|
32
src/internet/subsonicsettingspage.h
Normal file
32
src/internet/subsonicsettingspage.h
Normal file
@ -0,0 +1,32 @@
|
||||
#ifndef SUBSONICSETTINGSPAGE_H
|
||||
#define SUBSONICSETTINGSPAGE_H
|
||||
|
||||
#include "ui/settingspage.h"
|
||||
#include "subsonicservice.h"
|
||||
|
||||
class Ui_SubsonicSettingsPage;
|
||||
|
||||
class SubsonicSettingsPage : public SettingsPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SubsonicSettingsPage(SettingsDialog *dialog);
|
||||
~SubsonicSettingsPage();
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
|
||||
public slots:
|
||||
void LoginStateChanged(SubsonicService::LoginState newstate);
|
||||
|
||||
private slots:
|
||||
void Login();
|
||||
void Logout();
|
||||
|
||||
private:
|
||||
Ui_SubsonicSettingsPage* ui_;
|
||||
SubsonicService* service_;
|
||||
};
|
||||
|
||||
#endif // SUBSONICSETTINGSPAGE_H
|
101
src/internet/subsonicsettingspage.ui
Normal file
101
src/internet/subsonicsettingspage.ui
Normal file
@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SubsonicSettingsPage</class>
|
||||
<widget class="QWidget" name="SubsonicSettingsPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>505</width>
|
||||
<height>219</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Subsonic</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="LoginStateWidget" name="login_state" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="server_group">
|
||||
<property name="title">
|
||||
<string>Server details</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="server_label">
|
||||
<property name="text">
|
||||
<string>Server</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="username_label">
|
||||
<property name="text">
|
||||
<string>Username</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<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>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="username"/>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QLineEdit" name="server"/>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="login">
|
||||
<property name="text">
|
||||
<string>Login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</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>username</tabstop>
|
||||
<tabstop>password</tabstop>
|
||||
<tabstop>login</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
16
src/internet/subsonicurlhandler.cpp
Normal file
16
src/internet/subsonicurlhandler.cpp
Normal file
@ -0,0 +1,16 @@
|
||||
#include "subsonicservice.h"
|
||||
#include "subsonicurlhandler.h"
|
||||
|
||||
SubsonicUrlHandler::SubsonicUrlHandler(SubsonicService* service, QObject* parent)
|
||||
: UrlHandler(parent),
|
||||
service_(service) {
|
||||
}
|
||||
|
||||
UrlHandler::LoadResult SubsonicUrlHandler::StartLoading(const QUrl& url) {
|
||||
if (service_->login_state() != SubsonicService::LoginState_Loggedin)
|
||||
return LoadResult();
|
||||
|
||||
QUrl newurl = service_->BuildRequestUrl("stream");
|
||||
newurl.addQueryItem("id", url.host());
|
||||
return LoadResult(url, LoadResult::TrackAvailable, newurl);
|
||||
}
|
23
src/internet/subsonicurlhandler.h
Normal file
23
src/internet/subsonicurlhandler.h
Normal file
@ -0,0 +1,23 @@
|
||||
#ifndef SUBSONICURLHANDLER_H
|
||||
#define SUBSONICURLHANDLER_H
|
||||
|
||||
#include "core/urlhandler.h"
|
||||
|
||||
class SubsonicService;
|
||||
|
||||
// Subsonic URL handler: subsonic://id
|
||||
class SubsonicUrlHandler : public UrlHandler {
|
||||
Q_OBJECT
|
||||
public:
|
||||
SubsonicUrlHandler(SubsonicService* service, QObject* parent);
|
||||
|
||||
QString scheme() const { return "subsonic"; }
|
||||
QIcon icon() const { return QIcon(":providers/subsonic-32.png"); }
|
||||
LoadResult StartLoading(const QUrl& url);
|
||||
//LoadResult LoadNext(const QUrl& url);
|
||||
|
||||
private:
|
||||
SubsonicService* service_;
|
||||
};
|
||||
|
||||
#endif // SUBSONICURLHANDLER_H
|
@ -38,6 +38,7 @@
|
||||
#include "internet/digitallyimportedsettingspage.h"
|
||||
#include "internet/groovesharksettingspage.h"
|
||||
#include "internet/magnatunesettingspage.h"
|
||||
#include "internet/subsonicsettingspage.h"
|
||||
#include "internet/ubuntuonesettingspage.h"
|
||||
#include "library/librarysettingspage.h"
|
||||
#include "playlist/playlistview.h"
|
||||
@ -173,6 +174,7 @@ SettingsDialog::SettingsDialog(Application* app, BackgroundStreams* streams, QWi
|
||||
AddPage(Page_Magnatune, new MagnatuneSettingsPage(this), providers);
|
||||
AddPage(Page_DigitallyImported, new DigitallyImportedSettingsPage(this), providers);
|
||||
AddPage(Page_BackgroundStreams, new BackgroundStreamsSettingsPage(this), providers);
|
||||
AddPage(Page_Subsonic, new SubsonicSettingsPage(this), providers);
|
||||
AddPage(Page_Podcasts, new PodcastSettingsPage(this), providers);
|
||||
|
||||
// List box
|
||||
|
@ -76,6 +76,7 @@ public:
|
||||
Page_Transcoding,
|
||||
Page_Remote,
|
||||
Page_Wiimotedev,
|
||||
Page_Subsonic,
|
||||
Page_Podcasts,
|
||||
Page_GoogleDrive,
|
||||
Page_UbuntuOne,
|
||||
|
Loading…
x
Reference in New Issue
Block a user