Merge branch 'subsonic'
Conflicts: data/data.qrc
This commit is contained in:
commit
fe2fb788d3
@ -1,6 +1,7 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file>blank.ttf</file>
|
<file>blank.ttf</file>
|
||||||
|
<file>clementine_remote_qr.png</file>
|
||||||
<file>clementine-spotify-public.pem</file>
|
<file>clementine-spotify-public.pem</file>
|
||||||
<file>currenttrack_bar_left.png</file>
|
<file>currenttrack_bar_left.png</file>
|
||||||
<file>currenttrack_bar_mid.png</file>
|
<file>currenttrack_bar_mid.png</file>
|
||||||
@ -296,6 +297,8 @@
|
|||||||
<file>providers/somafm.png</file>
|
<file>providers/somafm.png</file>
|
||||||
<file>providers/songkick.png</file>
|
<file>providers/songkick.png</file>
|
||||||
<file>providers/soundcloud.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/ubuntuone.png</file>
|
||||||
<file>providers/wikipedia.png</file>
|
<file>providers/wikipedia.png</file>
|
||||||
<file>sample.mood</file>
|
<file>sample.mood</file>
|
||||||
@ -337,6 +340,7 @@
|
|||||||
<file>schema/schema-40.sql</file>
|
<file>schema/schema-40.sql</file>
|
||||||
<file>schema/schema-41.sql</file>
|
<file>schema/schema-41.sql</file>
|
||||||
<file>schema/schema-42.sql</file>
|
<file>schema/schema-42.sql</file>
|
||||||
|
<file>schema/schema-43.sql</file>
|
||||||
<file>schema/schema-4.sql</file>
|
<file>schema/schema-4.sql</file>
|
||||||
<file>schema/schema-5.sql</file>
|
<file>schema/schema-5.sql</file>
|
||||||
<file>schema/schema-6.sql</file>
|
<file>schema/schema-6.sql</file>
|
||||||
@ -358,6 +362,5 @@
|
|||||||
<file>volumeslider-handle_glow.png</file>
|
<file>volumeslider-handle_glow.png</file>
|
||||||
<file>volumeslider-handle.png</file>
|
<file>volumeslider-handle.png</file>
|
||||||
<file>volumeslider-inset.png</file>
|
<file>volumeslider-inset.png</file>
|
||||||
<file>clementine_remote_qr.png</file>
|
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</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/somafmservice.cpp
|
||||||
internet/somafmurlhandler.cpp
|
internet/somafmurlhandler.cpp
|
||||||
internet/soundcloudservice.cpp
|
internet/soundcloudservice.cpp
|
||||||
|
internet/subsonicservice.cpp
|
||||||
|
internet/subsonicsettingspage.cpp
|
||||||
|
internet/subsonicurlhandler.cpp
|
||||||
|
|
||||||
library/groupbydialog.cpp
|
library/groupbydialog.cpp
|
||||||
library/library.cpp
|
library/library.cpp
|
||||||
@ -478,6 +481,9 @@ set(HEADERS
|
|||||||
internet/somafmservice.h
|
internet/somafmservice.h
|
||||||
internet/somafmurlhandler.h
|
internet/somafmurlhandler.h
|
||||||
internet/soundcloudservice.h
|
internet/soundcloudservice.h
|
||||||
|
internet/subsonicservice.h
|
||||||
|
internet/subsonicsettingspage.h
|
||||||
|
internet/subsonicurlhandler.h
|
||||||
|
|
||||||
library/groupbydialog.h
|
library/groupbydialog.h
|
||||||
library/library.h
|
library/library.h
|
||||||
@ -660,6 +666,7 @@ set(UI
|
|||||||
internet/magnatunesettingspage.ui
|
internet/magnatunesettingspage.ui
|
||||||
internet/searchboxwidget.ui
|
internet/searchboxwidget.ui
|
||||||
internet/spotifysettingspage.ui
|
internet/spotifysettingspage.ui
|
||||||
|
internet/subsonicsettingspage.ui
|
||||||
|
|
||||||
library/groupbydialog.ui
|
library/groupbydialog.ui
|
||||||
library/libraryfilterwidget.ui
|
library/libraryfilterwidget.ui
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
const char* Database::kDatabaseFilename = "clementine.db";
|
const char* Database::kDatabaseFilename = "clementine.db";
|
||||||
const int Database::kSchemaVersion = 42;
|
const int Database::kSchemaVersion = 43;
|
||||||
const char* Database::kMagicAllSongsTables = "%allsongstables";
|
const char* Database::kMagicAllSongsTables = "%allsongstables";
|
||||||
|
|
||||||
int Database::sNextConnectionId = 1;
|
int Database::sNextConnectionId = 1;
|
||||||
|
@ -29,7 +29,9 @@
|
|||||||
#include "internetservice.h"
|
#include "internetservice.h"
|
||||||
#include "savedradio.h"
|
#include "savedradio.h"
|
||||||
#include "somafmservice.h"
|
#include "somafmservice.h"
|
||||||
|
#include "groovesharkservice.h"
|
||||||
#include "soundcloudservice.h"
|
#include "soundcloudservice.h"
|
||||||
|
#include "subsonicservice.h"
|
||||||
#include "core/closure.h"
|
#include "core/closure.h"
|
||||||
#include "core/logging.h"
|
#include "core/logging.h"
|
||||||
#include "core/mergedproxymodel.h"
|
#include "core/mergedproxymodel.h"
|
||||||
@ -94,6 +96,7 @@ InternetModel::InternetModel(Application* app, QObject* parent)
|
|||||||
#ifdef HAVE_SPOTIFY
|
#ifdef HAVE_SPOTIFY
|
||||||
AddService(new SpotifyService(app, this));
|
AddService(new SpotifyService(app, this));
|
||||||
#endif
|
#endif
|
||||||
|
AddService(new SubsonicService(app, this));
|
||||||
#ifdef HAVE_UBUNTU_ONE
|
#ifdef HAVE_UBUNTU_ONE
|
||||||
AddService(new UbuntuOneService(app, this));
|
AddService(new UbuntuOneService(app, this));
|
||||||
#endif
|
#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/digitallyimportedsettingspage.h"
|
||||||
#include "internet/groovesharksettingspage.h"
|
#include "internet/groovesharksettingspage.h"
|
||||||
#include "internet/magnatunesettingspage.h"
|
#include "internet/magnatunesettingspage.h"
|
||||||
|
#include "internet/subsonicsettingspage.h"
|
||||||
#include "internet/ubuntuonesettingspage.h"
|
#include "internet/ubuntuonesettingspage.h"
|
||||||
#include "library/librarysettingspage.h"
|
#include "library/librarysettingspage.h"
|
||||||
#include "playlist/playlistview.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_Magnatune, new MagnatuneSettingsPage(this), providers);
|
||||||
AddPage(Page_DigitallyImported, new DigitallyImportedSettingsPage(this), providers);
|
AddPage(Page_DigitallyImported, new DigitallyImportedSettingsPage(this), providers);
|
||||||
AddPage(Page_BackgroundStreams, new BackgroundStreamsSettingsPage(this), providers);
|
AddPage(Page_BackgroundStreams, new BackgroundStreamsSettingsPage(this), providers);
|
||||||
|
AddPage(Page_Subsonic, new SubsonicSettingsPage(this), providers);
|
||||||
AddPage(Page_Podcasts, new PodcastSettingsPage(this), providers);
|
AddPage(Page_Podcasts, new PodcastSettingsPage(this), providers);
|
||||||
|
|
||||||
// List box
|
// List box
|
||||||
|
@ -76,6 +76,7 @@ public:
|
|||||||
Page_Transcoding,
|
Page_Transcoding,
|
||||||
Page_Remote,
|
Page_Remote,
|
||||||
Page_Wiimotedev,
|
Page_Wiimotedev,
|
||||||
|
Page_Subsonic,
|
||||||
Page_Podcasts,
|
Page_Podcasts,
|
||||||
Page_GoogleDrive,
|
Page_GoogleDrive,
|
||||||
Page_UbuntuOne,
|
Page_UbuntuOne,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user