Add scrobbler with support for Last.fm, Libre.fm and ListenBrainz

This commit is contained in:
Jonas Kvinge 2018-12-23 18:54:27 +01:00
parent 517285085a
commit 0d7e12e781
43 changed files with 3565 additions and 169 deletions

View File

@ -23,9 +23,10 @@ Strawberry is a audio player and music collection organizer. It is a fork of Cle
* Song lyrics from AudD and API Seeds
* Support for multiple backends
* Audio analyzer
* Equalizer
* Audio equalizer
* Transfer music to iPod, iPhone, MTP or mass-storage USB player
* Integrated Tidal and Deezer support
* Streaming support for Tidal and Deezer
* Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
It has so far been tested to work on Linux, OpenBSD, MacOs and Windows.
@ -52,8 +53,8 @@ Either GStreamer, Xine, VLC, Deezer or Phonon engine is required, but only GStre
You should also install the gstreamer plugins base and good, and optionally bad and ugly.
Deezer streams with full songs are encrypted and only urls for preview streams (MP3) are exposed by the API.
Full length songs requires the use of deezers own engine (Deezer SDK) or the dzmedia library (I dont have it).
Deezer SDK can be found here: https://build-repo.deezer.com/native_sdk/deezer-native-sdk-v1.2.10.zip
Full length songs requires the use of deezers own engine (Deezer SDK).
The Deezer SDK can be found here: https://build-repo.deezer.com/native_sdk/deezer-native-sdk-v1.2.10.zip
Optional:

View File

@ -86,6 +86,8 @@
<file>icons/128x128/zoom-out.png</file>
<file>icons/128x128/tidal.png</file>
<file>icons/128x128/deezer.png</file>
<file>icons/128x128/scrobble.png</file>
<file>icons/128x128/scrobble-disabled.png</file>
<file>icons/64x64/albums.png</file>
<file>icons/64x64/alsa.png</file>
<file>icons/64x64/application-exit.png</file>
@ -172,6 +174,8 @@
<file>icons/64x64/zoom-out.png</file>
<file>icons/64x64/tidal.png</file>
<file>icons/64x64/deezer.png</file>
<file>icons/64x64/scrobble.png</file>
<file>icons/64x64/scrobble-disabled.png</file>
<file>icons/48x48/albums.png</file>
<file>icons/48x48/alsa.png</file>
<file>icons/48x48/application-exit.png</file>
@ -260,6 +264,8 @@
<file>icons/48x48/zoom-in.png</file>
<file>icons/48x48/zoom-out.png</file>
<file>icons/48x48/tidal.png</file>
<file>icons/48x48/scrobble.png</file>
<file>icons/48x48/scrobble-disabled.png</file>
<file>icons/32x32/albums.png</file>
<file>icons/32x32/alsa.png</file>
<file>icons/32x32/application-exit.png</file>
@ -350,6 +356,8 @@
<file>icons/32x32/zoom-out.png</file>
<file>icons/32x32/tidal.png</file>
<file>icons/32x32/deezer.png</file>
<file>icons/32x32/scrobble.png</file>
<file>icons/32x32/scrobble-disabled.png</file>
<file>icons/22x22/albums.png</file>
<file>icons/22x22/alsa.png</file>
<file>icons/22x22/application-exit.png</file>
@ -440,5 +448,7 @@
<file>icons/22x22/zoom-out.png</file>
<file>icons/22x22/tidal.png</file>
<file>icons/22x22/deezer.png</file>
<file>icons/22x22/scrobble.png</file>
<file>icons/22x22/scrobble-disabled.png</file>
</qresource>
</RCC>

5
dist/debian/control vendored
View File

@ -62,8 +62,9 @@ Description: Audio player and music collection organizer
- Song lyrics from AudD and API Seeds
- Support for multiple backends
- Audio analyzer
- Equalizer
- Audio equalizer
- Transfer music to iPod, iPhone, MTP or mass-storage USB player
- Integrated Tidal support
- Streaming support for Tidal and Deezer
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
.
It is a fork of Clementine. The name is inspired by the band Strawbs.

View File

@ -35,11 +35,10 @@ Files: src/core/main.h
src/engine/phononengine.h
src/internet/internetservice.cpp
src/internet/internetservice.h
src/lyrics/*
src/tidal/*
src/deezer/*
src/settings/backendsettingspage.cpp
src/settings/backendsettingspage.h
src/settings/scrobblersettingspage.cpp
src/settings/scrobblersettingspage.h
src/settings/deezersettingspage.cpp
src/settings/deezersettingspage.h
src/settings/tidalsettingspage.cpp
@ -48,6 +47,10 @@ Files: src/core/main.h
src/covermanager/lastfmcoverprovider.h
src/covermanager/musicbrainzcoverprovider.cpp
src/covermanager/musicbrainzcoverprovider.h
src/lyrics/*
src/scrobbler/*
src/tidal/*
src/deezer/*
Copyright: 2012-2014, 2017-2018, Jonas Kvinge <jonas@jkvinge.net>
License: GPL-3+

View File

@ -64,9 +64,10 @@ Features:
* Song lyrics from AudD and API Seeds
* Support for multiple backends
* Audio analyzer
* Equalizer
* Audio equalizer
* Transfer music to iPod, iPhone, MTP or mass-storage USB player
* Integrated Tidal and Deezer support
* Streaming support for Tidal and Deezer
* Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
%prep
%setup -qn %{name}-@STRAWBERRY_VERSION_PACKAGE@

View File

@ -75,9 +75,10 @@ Features:
* Song lyrics from AudD and API Seeds
* Support for multiple backends
* Audio analyzer
* Equalizer
* Audio equalizer
* Transfer music to iPod, iPhone, MTP or mass-storage USB player
* Integrated Tidal and Deezer support
* Streaming support for Tidal and Deezer
* Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
%prep
%setup -q -n %{name}-@STRAWBERRY_VERSION_PACKAGE@

View File

@ -213,6 +213,7 @@ set(SOURCES
settings/shortcutssettingspage.cpp
settings/appearancesettingspage.cpp
settings/notificationssettingspage.cpp
settings/scrobblersettingspage.cpp
dialogs/about.cpp
dialogs/console.cpp
@ -265,6 +266,16 @@ set(SOURCES
internet/internetsearchitemdelegate.cpp
internet/localredirectserver.cpp
scrobbler/audioscrobbler.cpp
scrobbler/scrobblerservices.cpp
scrobbler/scrobblerservice.cpp
scrobbler/scrobblercache.cpp
scrobbler/scrobblercacheitem.cpp
scrobbler/scrobblingapi20.cpp
scrobbler/lastfmscrobbler.cpp
scrobbler/librefmscrobbler.cpp
scrobbler/listenbrainzscrobbler.cpp
)
set(HEADERS
@ -379,6 +390,7 @@ set(HEADERS
settings/shortcutssettingspage.h
settings/appearancesettingspage.h
settings/notificationssettingspage.h
settings/scrobblersettingspage.h
dialogs/about.h
dialogs/errordialog.h
@ -426,6 +438,16 @@ set(HEADERS
internet/internetsearchmodel.h
internet/localredirectserver.h
scrobbler/audioscrobbler.h
scrobbler/scrobblerservices.h
scrobbler/scrobblerservice.h
scrobbler/scrobblercache.h
scrobbler/scrobblercacheitem.h
scrobbler/scrobblingapi20.h
scrobbler/lastfmscrobbler.h
scrobbler/librefmscrobbler.h
scrobbler/listenbrainzscrobbler.h
)
set(UI
@ -462,6 +484,7 @@ set(UI
settings/shortcutssettingspage.ui
settings/appearancesettingspage.ui
settings/notificationssettingspage.ui
settings/scrobblersettingspage.ui
equalizer/equalizer.ui
equalizer/equalizerslider.ui

View File

@ -69,6 +69,8 @@
# include "deezer/deezerservice.h"
#endif
#include "scrobbler/audioscrobbler.h"
bool Application::kIsPortable = false;
class ApplicationImpl {
@ -136,7 +138,7 @@ class ApplicationImpl {
#ifdef HAVE_STREAM_DEEZER
deezer_search_([=]() { return new InternetSearch(app, Song::Source_Deezer, app); }),
#endif
dummy_([=]() { return new QVariant; })
scrobbler_([=]() { return new AudioScrobbler(app, app); })
{}
Lazy<TagReaderClient> tag_reader_client_;
@ -162,7 +164,7 @@ class ApplicationImpl {
#ifdef HAVE_STREAM_DEEZER
Lazy<InternetSearch> deezer_search_;
#endif
Lazy<QVariant> dummy_;
Lazy<AudioScrobbler> scrobbler_;
};
@ -236,3 +238,4 @@ InternetSearch *Application::tidal_search() const { return p_->tidal_search_.get
#ifdef HAVE_STREAM_DEEZER
InternetSearch *Application::deezer_search() const { return p_->deezer_search_.get(); }
#endif
AudioScrobbler *Application::scrobbler() const { return p_->scrobbler_.get(); }

View File

@ -19,8 +19,8 @@
*
*/
#ifndef APPLICATION_H_
#define APPLICATION_H_
#ifndef APPLICATION_H
#define APPLICATION_H
#include "config.h"
@ -57,6 +57,7 @@ class CurrentArtLoader;
class LyricsProviders;
class InternetServices;
class InternetSearch;
class AudioScrobbler;
class Application : public QObject {
Q_OBJECT
@ -98,6 +99,8 @@ class Application : public QObject {
InternetSearch *deezer_search() const;
#endif
AudioScrobbler *scrobbler() const;
void MoveToNewThread(QObject *object);
void MoveToThread(QObject *object, QThread *thread);
@ -117,4 +120,4 @@ signals:
};
#endif // APPLICATION_H_
#endif // APPLICATION_H

View File

@ -144,6 +144,8 @@
#include "internet/internetservice.h"
#include "internet/internetsearchview.h"
#include "scrobbler/audioscrobbler.h"
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
# include "musicbrainz/tagfetcher.h"
#endif
@ -362,6 +364,10 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
ui_->action_update_collection->setIcon(IconLoader::Load("view-refresh"));
ui_->action_full_collection_scan->setIcon(IconLoader::Load("view-refresh"));
ui_->action_settings->setIcon(IconLoader::Load("configure"));
// Scrobble
ui_->action_toggle_scrobbling->setIcon(IconLoader::Load("scrobble-disabled", 22));
// File view connections
connect(file_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
@ -411,6 +417,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
connect(ui_->action_full_collection_scan, SIGNAL(triggered()), app_->collection(), SLOT(FullScan()));
//connect(ui_->action_add_files_to_transcoder, SIGNAL(triggered()), SLOT(AddFilesToTranscoder()));
connect(ui_->action_toggle_scrobbling, SIGNAL(triggered()), app_->scrobbler(), SLOT(ToggleScrobbling()));
connect(app_->scrobbler(), SIGNAL(ErrorMessage(QString)), SLOT(ShowErrorDialog(QString)));
// Playlist view actions
ui_->action_next_playlist->setShortcuts(QList<QKeySequence>() << QKeySequence::fromString("Ctrl+Tab")<< QKeySequence::fromString("Ctrl+PgDown"));
ui_->action_previous_playlist->setShortcuts(QList<QKeySequence>() << QKeySequence::fromString("Ctrl+Shift+Tab")<< QKeySequence::fromString("Ctrl+PgUp"));
@ -424,6 +433,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
ui_->back_button->setDefaultAction(ui_->action_previous_track);
ui_->pause_play_button->setDefaultAction(ui_->action_play_pause);
ui_->stop_button->setDefaultAction(ui_->action_stop);
ui_->button_scrobble->setDefaultAction(ui_->action_toggle_scrobbling);
ui_->playlist->SetActions(ui_->action_new_playlist, ui_->action_load_playlist, ui_->action_save_playlist, ui_->action_clear_playlist, ui_->action_next_playlist, /* These two actions aren't associated */ ui_->action_previous_playlist /* to a button but to the main window */ );
@ -570,7 +580,6 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
playlist_copy_to_device_ = playlist_menu_->addAction(IconLoader::Load("device"), tr("Copy to device..."), this, SLOT(PlaylistCopyToDevice()));
#endif
#endif
//playlist_delete_ = playlist_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from disk..."), this, SLOT(PlaylistDelete()));
playlist_open_in_browser_ = playlist_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, SLOT(PlaylistOpenInBrowser()));
playlist_open_in_browser_->setVisible(false);
playlist_show_in_collection_ = playlist_menu_->addAction(IconLoader::Load("edit-find"), tr("Show in collection..."), this, SLOT(ShowInCollection()));
@ -595,6 +604,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
connect(app_->device_manager()->connected_devices_model(), SIGNAL(IsEmptyChanged(bool)), playlist_copy_to_device_, SLOT(setDisabled(bool)));
#endif
connect(app_->scrobbler(), SIGNAL(ScrobblingEnabledChanged(bool)), SLOT(ScrobblingEnabledChanged(bool)));
connect(app_->scrobbler(), SIGNAL(ScrobbleButtonVisibilityChanged(bool)), SLOT(ScrobbleButtonVisibilityChanged(bool)));
#ifdef Q_OS_MACOS
mac::SetApplicationHandler(this);
#endif
@ -636,9 +648,10 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
connect(global_shortcuts_, SIGNAL(ShowHide()), SLOT(ToggleShowHide()));
connect(global_shortcuts_, SIGNAL(ShowOSD()), app_->player(), SLOT(ShowOSD()));
connect(global_shortcuts_, SIGNAL(TogglePrettyOSD()), app_->player(), SLOT(TogglePrettyOSD()));
connect(global_shortcuts_, SIGNAL(ToggleScrobbling()), app_->scrobbler(), SLOT(ToggleScrobbling()));
// Fancy tabs
connect(ui_->tabs, SIGNAL(ModeChanged(FancyTabWidget::Mode)), SLOT(SaveGeometry()));
connect(ui_->tabs, SIGNAL(ModeChanged(FancyTabWidget::Mode)), SLOT(SaveTabMode()));
connect(ui_->tabs, SIGNAL(CurrentChanged(int)), SLOT(TabSwitched()));
connect(ui_->tabs, SIGNAL(CurrentChanged(int)), SLOT(SaveGeometry()));
@ -650,22 +663,8 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
connect(app_->player(), SIGNAL(Error()), context_view_, SLOT(Error()));
// Analyzer
//ui_->analyzer->SetEngine(app_->player()->engine());
connect(ui_->analyzer, SIGNAL(WheelEvent(int)), SLOT(VolumeWheelEvent(int)));
#if 0
// Equalizer
qLog(Debug) << "Creating equalizer";
connect(equalizer_.get(), SIGNAL(ParametersChanged(int,QList<int>)), app_->player()->engine(), SLOT(SetEqualizerParameters(int,QList<int>)));
connect(equalizer_.get(), SIGNAL(EnabledChanged(bool)), app_->player()->engine(), SLOT(SetEqualizerEnabled(bool)));
connect(equalizer_.get(), SIGNAL(StereoBalanceChanged(float)), app_->player()->engine(), SLOT(SetStereoBalance(float)));
app_->player()->engine()->SetEqualizerEnabled(equalizer_->is_enabled());
app_->player()->engine()->SetEqualizerParameters(equalizer_->preamp_value(), equalizer_->gain_values());
app_->player()->engine()->SetStereoBalance(equalizer_->stereo_balance());
#endif
// Statusbar widgets
ui_->playlist_summary->setMinimumWidth(QFontMetrics(font()).width("WW selected of WW tracks - [ WW:WW ]"));
ui_->status_bar_stack->setCurrentWidget(ui_->playlist_summary_page);
@ -709,6 +708,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
connect(app_->playlist_manager()->sequence(), SIGNAL(RepeatModeChanged(PlaylistSequence::RepeatMode)), osd_, SLOT(RepeatModeChanged(PlaylistSequence::RepeatMode)));
connect(app_->playlist_manager()->sequence(), SIGNAL(ShuffleModeChanged(PlaylistSequence::ShuffleMode)), osd_, SLOT(ShuffleModeChanged(PlaylistSequence::ShuffleMode)));
ScrobbleButtonVisibilityChanged(app_->scrobbler()->ScrobbleButton());
ScrobblingEnabledChanged(app_->scrobbler()->IsEnabled());
// Load settings
qLog(Debug) << "Loading settings";
settings_.beginGroup(kSettingsGroup);
@ -724,7 +726,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
ui_->splitter->setSizes(QList<int>() << 250 << width() - 250);
}
ui_->tabs->setCurrentIndex(settings_.value("current_tab", 1 /* Collection tab */ ).toInt());
ui_->tabs->setCurrentIndex(settings_.value("current_tab", 1).toInt());
FancyTabWidget::Mode default_mode = FancyTabWidget::Mode_LargeSidebar;
int tab_mode_int = settings_.value("tab_mode", default_mode).toInt();
FancyTabWidget::Mode tab_mode = FancyTabWidget::Mode(tab_mode_int);
@ -788,6 +790,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
qLog(Debug) << "Started";
initialised_ = true;
app_->scrobbler()->ConnectError();
if (app_->scrobbler()->IsEnabled() && !app_->scrobbler()->IsOffline()) app_->scrobbler()->Submit();
}
MainWindow::~MainWindow() {
@ -916,9 +921,11 @@ void MainWindow::MediaPlaying() {
bool enable_play_pause(false);
bool can_seek(false);
if (app_->player()->GetCurrentItem()) {
enable_play_pause = !(app_->player()->GetCurrentItem()->options() & PlaylistItem::PauseDisabled);
can_seek = !(app_->player()->GetCurrentItem()->options() & PlaylistItem::SeekDisabled);
PlaylistItemPtr item(app_->player()->GetCurrentItem());
if (item) {
enable_play_pause = !(item->options() & PlaylistItem::PauseDisabled);
can_seek = !(item->options() & PlaylistItem::SeekDisabled);
}
ui_->action_play_pause->setEnabled(enable_play_pause);
ui_->track_slider->SetCanSeek(can_seek);
@ -928,6 +935,13 @@ void MainWindow::MediaPlaying() {
track_slider_timer_->start();
UpdateTrackPosition();
// Send now playing to scrobble services
Playlist *playlist = app_->playlist_manager()->active();
if (app_->scrobbler()->IsEnabled() && playlist && !playlist->nowplaying() && item->Metadata().is_metadata_good() && item->Metadata().length_nanosec() > 0) {
app_->scrobbler()->UpdateNowPlaying(item->Metadata());
playlist->set_nowplaying(true);
}
}
void MainWindow::VolumeChanged(int volume) {
@ -983,23 +997,31 @@ void MainWindow::TabSwitched() {
ui_->widget_playing->SetEnabled();
if (!initialised_) return;
SaveGeometry();
settings_.setValue("current_tab", ui_->tabs->currentIndex());
ui_->tabs->saveSettings(kSettingsGroup);
}
void MainWindow::SaveGeometry() {
if (!initialised_) return;
was_maximized_ = isMaximized();
settings_.setValue("maximized", was_maximized_);
if (was_maximized_) settings_.remove("geometry");
else settings_.setValue("geometry", saveGeometry());
settings_.setValue("splitter_state", ui_->splitter->saveState());
settings_.setValue("current_tab", ui_->tabs->currentIndex());
settings_.setValue("tab_mode", ui_->tabs->mode());
ui_->tabs->saveSettings(kSettingsGroup);
}
void MainWindow::SaveTabMode() {
if (!initialised_) return;
settings_.setValue("tab_mode", ui_->tabs->mode());
ui_->tabs->saveSettings(kSettingsGroup);
}
void MainWindow::SavePlaybackStatus() {
QSettings settings;
@ -1178,25 +1200,32 @@ void MainWindow::Seeked(qlonglong microseconds) {
void MainWindow::UpdateTrackPosition() {
// Track position in seconds
//Playlist *playlist = app_->playlist_manager()->active();
PlaylistItemPtr item(app_->player()->GetCurrentItem());
if (!item) return;
const int position = std::floor(float(app_->player()->engine()->position_nanosec()) / kNsecPerSec + 0.5);
const int length = item->Metadata().length_nanosec() / kNsecPerSec;
if (length <= 0) {
// Probably a stream that we don't know the length of
return;
}
const int length = (item->Metadata().length_nanosec() / kNsecPerSec);
if (length <= 0) return;
const int position = std::floor(float(app_->player()->engine()->position_nanosec()) / kNsecPerSec + 0.5);
// Update the tray icon every 10 seconds
if (position % 10 == 0 && tray_icon_) tray_icon_->SetProgress(double(position) / length * 100);
if (tray_icon_ && position % 10 == 0) tray_icon_->SetProgress(double(position) / length * 100);
// Send Scrobble
if (app_->scrobbler()->IsEnabled() && item->Metadata().is_metadata_good()) {
Playlist *playlist = app_->playlist_manager()->active();
if (playlist && playlist->nowplaying() && !playlist->scrobbled()) {
const int scrobble_point = (playlist->scrobble_point_nanosec() / kNsecPerSec);
if (position >= scrobble_point) {
app_->scrobbler()->Scrobble(item->Metadata(), scrobble_point);
playlist->set_scrobbled(true);
}
}
}
}
void MainWindow::UpdateTrackSliderPosition() {
PlaylistItemPtr item(app_->player()->GetCurrentItem());
const int slider_position = std::floor(float(app_->player()->engine()->position_nanosec()) / kNsecPerMsec);
@ -1204,6 +1233,7 @@ void MainWindow::UpdateTrackSliderPosition() {
// Update the slider
ui_->track_slider->SetValue(slider_position, slider_length);
}
void MainWindow::ApplyAddBehaviour(MainWindow::AddBehaviour b, MimeData *data) const {
@ -1974,47 +2004,6 @@ void MainWindow::PlaylistOrganiseSelected(bool copy) {
}
#endif
#if 0
void MainWindow::PlaylistDelete() {
// Note: copied from CollectionView::Delete
if (QMessageBox::warning(this, tr("Delete files"),
tr("These files will be deleted from disk, are you sure you want to continue?"),
QMessageBox::Yes, QMessageBox::Cancel) != QMessageBox::Yes)
return;
std::shared_ptr<MusicStorage> storage(new FilesystemMusicStorage("/"));
// Get selected songs
SongList selected_songs;
QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows();
for (const QModelIndex &proxy_index : proxy_indexes) {
QModelIndex index = app_->playlist_manager()->current()->proxy()->mapToSource(proxy_index);
selected_songs << app_->playlist_manager()->current()->item_at(index.row())->Metadata();
}
if (app_->player()->GetState() == Engine::Playing) {
if (app_->playlist_manager()->current()->rowCount() == selected_songs.length()) {
app_->player()->Stop();
}
else {
for (Song x : selected_songs) {
if (x == app_->player()->GetCurrentItem()->Metadata()) {
app_->player()->Next();
}
}
}
}
ui_->playlist->view()->RemoveSelected(true);
DeleteFiles *delete_files = new DeleteFiles(app_->task_manager(), storage);
connect(delete_files, SIGNAL(Finished(SongList)), SLOT(DeleteFinished(SongList)));
delete_files->Start(selected_songs);
}
#endif
void MainWindow::PlaylistOpenInBrowser() {
QList<QUrl> urls;
@ -2028,16 +2017,6 @@ void MainWindow::PlaylistOpenInBrowser() {
Utilities::OpenInFileBrowser(urls);
}
#if 0
void MainWindow::DeleteFinished(const SongList &songs_with_errors) {
if (songs_with_errors.isEmpty()) return;
OrganiseErrorDialog *dialog = new OrganiseErrorDialog(this);
dialog->Show(OrganiseErrorDialog::Type_Delete, songs_with_errors);
// It deletes itself when the user closes it
}
#endif
void MainWindow::PlaylistQueue() {
QModelIndexList indexes;
@ -2105,14 +2084,6 @@ void MainWindow::ChangeCollectionQueryMode(QAction *action) {
void MainWindow::ShowCoverManager() {
//if (!cover_manager_) {
//cover_manager_.reset(new AlbumCoverManager(app_, app_->collection_backend()));
//cover_manager_->Init();
// Cover manager connections
//connect(cover_manager_.get(), SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
//}
cover_manager_->show();
}
@ -2121,7 +2092,6 @@ SettingsDialog *MainWindow::CreateSettingsDialog() {
SettingsDialog *settings_dialog = new SettingsDialog(app_);
settings_dialog->SetGlobalShortcutManager(global_shortcuts_);
//settings_dialog->SetSongInfoView(song_info_view_);
// Settings
connect(settings_dialog, SIGNAL(accepted()), SLOT(ReloadAllSettings()));
@ -2132,31 +2102,13 @@ SettingsDialog *MainWindow::CreateSettingsDialog() {
}
void MainWindow::EnsureSettingsDialogCreated() {
//if (settings_dialog_) return;
//settings_dialog_.reset(new SettingsDialog(app_));
//settings_dialog_->SetGlobalShortcutManager(global_shortcuts_);
//settings_dialog_->SetSongInfoView(song_info_view_);
// Settings
//connect(settings_dialog_.get(), SIGNAL(accepted()), SLOT(ReloadAllSettings()));
// Allows custom notification preview
//connect(settings_dialog_.get(), SIGNAL(NotificationPreview(OSD::Behaviour,QString,QString)), SLOT(HandleNotificationPreview(OSD::Behaviour, QString, QString)));
}
void MainWindow::OpenSettingsDialog() {
EnsureSettingsDialogCreated();
settings_dialog_->show();
}
void MainWindow::OpenSettingsDialogAtPage(SettingsDialog::Page page) {
EnsureSettingsDialogCreated();
settings_dialog_->OpenAtPage(page);
}
@ -2169,22 +2121,8 @@ EditTagDialog *MainWindow::CreateEditTagDialog() {
}
void MainWindow::EnsureEditTagDialogCreated() {
//if (edit_tag_dialog_) return;
//edit_tag_dialog_.reset(new EditTagDialog(app_));
//connect(edit_tag_dialog_.get(), SIGNAL(accepted()), SLOT(EditTagDialogAccepted()));
//connect(edit_tag_dialog_.get(), SIGNAL(Error(QString)), SLOT(ShowErrorDialog(QString)));
}
void MainWindow::ShowAboutDialog() {
//if (!about_dialog_) {
//about_dialog_.reset(new About);
//}
about_dialog_->show();
}
@ -2192,18 +2130,13 @@ void MainWindow::ShowAboutDialog() {
#ifdef HAVE_GSTREAMER
void MainWindow::ShowTranscodeDialog() {
//if (!transcode_dialog_) {
// transcode_dialog_.reset(new TranscodeDialog);
//}
transcode_dialog_->show();
}
#endif
void MainWindow::ShowErrorDialog(const QString &message) {
//if (!error_dialog_) {
// error_dialog_.reset(new ErrorDialog);
//}
error_dialog_->ShowMessage(message);
}
@ -2270,6 +2203,7 @@ void MainWindow::Exit() {
SaveGeometry();
SavePlaybackStatus();
app_->scrobbler()->WriteCache();
if (app_->player()->engine()->is_fadeout_enabled()) {
// To shut down the application when fadeout will be finished
@ -2442,3 +2376,28 @@ void MainWindow::GetCoverAutomatically() {
if (search) album_cover_choice_controller_->SearchCoverAutomatically(song_);
}
void MainWindow::ScrobblingEnabledChanged(bool value) {
if (app_->scrobbler()->ScrobbleButton()) SetToggleScrobblingIcon(value);
}
void MainWindow::ScrobbleButtonVisibilityChanged(bool value) {
ui_->button_scrobble->setVisible(value);
ui_->action_toggle_scrobbling->setVisible(value);
if (value) SetToggleScrobblingIcon(app_->scrobbler()->IsEnabled());
}
void MainWindow::SetToggleScrobblingIcon(bool value) {
if (value) {
if (app_->playlist_manager()->active()->scrobbled())
ui_->action_toggle_scrobbling->setIcon(IconLoader::Load("scrobble", 22));
else
ui_->action_toggle_scrobbling->setIcon(IconLoader::Load("scrobble", 22)); // TODO: Create a faint version of the icon
}
else {
ui_->action_toggle_scrobbling->setIcon(IconLoader::Load("scrobble-disabled", 22));
}
}

View File

@ -194,7 +194,6 @@ signals:
#endif
void PlaylistOrganiseSelected(bool copy);
#endif
//void PlaylistDelete();
void PlaylistOpenInBrowser();
void ShowInCollection();
@ -210,9 +209,7 @@ signals:
#ifdef HAVE_GSTREAMER
void CopyFilesToCollection(const QList<QUrl>& urls);
void MoveFilesToCollection(const QList<QUrl>& urls);
//#ifndef Q_OS_WIN
void CopyFilesToDevice(const QList<QUrl>& urls);
//#endif
#endif
void EditFileTags(const QList<QUrl>& urls);
@ -253,8 +250,6 @@ signals:
void ShowTranscodeDialog();
#endif
void ShowErrorDialog(const QString& message);
void EnsureSettingsDialogCreated();
void EnsureEditTagDialogCreated();
SettingsDialog *CreateSettingsDialog();
EditTagDialog *CreateEditTagDialog();
void OpenSettingsDialog();
@ -262,6 +257,7 @@ signals:
void TabSwitched();
void SaveGeometry();
void SaveTabMode();
void SavePlaybackStatus();
void LoadPlaybackStatus();
void ResumePlayback();
@ -284,6 +280,9 @@ signals:
void SearchCoverAutomatically();
void AlbumArtLoaded(const Song &song, const QString &uri, const QImage &image);
void ScrobblingEnabledChanged(bool value);
void ScrobbleButtonVisibilityChanged(bool value);
private:
void ApplyAddBehaviour(AddBehaviour b, MimeData *data) const;
@ -296,6 +295,8 @@ signals:
void GetCoverAutomatically();
void SetToggleScrobblingIcon(bool value);
private:
Ui_MainWindow *ui_;
Windows7ThumbBar *thumbbar_;
@ -357,7 +358,6 @@ signals:
#ifndef Q_OS_WIN
QAction *playlist_copy_to_device_;
#endif
//QAction *playlist_delete_;
#endif
QAction *playlist_open_in_browser_;
QAction *playlist_queue_;

View File

@ -349,6 +349,31 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="button_scrobble">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="TrackSlider" name="track_slider" native="true">
<property name="sizePolicy">

View File

@ -76,6 +76,7 @@
#include "settings/playlistsettingspage.h"
#include "internet/internetservices.h"
#include "internet/internetservice.h"
#include "scrobbler/audioscrobbler.h"
using std::shared_ptr;
@ -538,9 +539,20 @@ void Player::PlayAt(int index, Engine::TrackChangeFlags change, bool reshuffle)
}
void Player::CurrentMetadataChanged(const Song &metadata) {
// those things might have changed (especially when a previously invalid song was reloaded) so we push the latest version into Engine
engine_->RefreshMarkers(metadata.beginning_nanosec(), metadata.end_nanosec());
// Send now playing to scrobble services
if (app_->scrobbler()->IsEnabled() && engine_->state() == Engine::Playing) {
Playlist *playlist = app_->playlist_manager()->active();
current_item_ = playlist->current_item();
if (playlist && current_item_ && !playlist->nowplaying() && current_item_->Metadata() == metadata && current_item_->Metadata().length_nanosec() > 0) {
app_->scrobbler()->UpdateNowPlaying(metadata);
playlist->set_nowplaying(true);
}
}
}
void Player::SeekTo(int seconds) {
@ -555,17 +567,18 @@ void Player::SeekTo(int seconds) {
const qint64 nanosec = qBound(0ll, qint64(seconds) * kNsecPerSec, length_nanosec);
engine_->Seek(nanosec);
qLog(Debug) << "Track seeked to" << nanosec << "ns - updating scrobble point";
app_->playlist_manager()->active()->UpdateScrobblePoint(nanosec);
emit Seeked(nanosec / 1000);
}
void Player::SeekForward() {
SeekTo(engine()->position_nanosec() / kNsecPerSec + seek_step_sec_);
//SeekTo(engine()->position_nanosec() / kNsecPerSec + 10);
}
void Player::SeekBackward() {
//SeekTo(engine()->position_nanosec() / kNsecPerSec - 10);
SeekTo(engine()->position_nanosec() / kNsecPerSec - seek_step_sec_);
}

View File

@ -307,6 +307,9 @@ bool Song::is_cdda() const { return d->source_ == Source_CDDA; }
bool Song::is_collection_song() const {
return !is_cdda() && !is_stream() && id() != -1;
}
bool Song::is_metadata_good() const {
return !d->title_.isEmpty() && !d->album_.isEmpty() && !d->artist_.isEmpty() && !d->url_.isEmpty() && d->end_ > 0;
}
const QString &Song::art_automatic() const { return d->art_automatic_; }
const QString &Song::art_manual() const { return d->art_manual_; }
bool Song::has_manually_unset_cover() const { return d->art_manual_ == kManuallyUnsetCover; }

View File

@ -227,6 +227,7 @@ class Song {
bool is_collection_song() const;
bool is_stream() const;
bool is_cdda() const;
bool is_metadata_good() const;
// Playlist views are special because you don't want to fill in album artists automatically for compilations, but you do for normal albums:
const QString &playlist_albumartist() const;

View File

@ -66,6 +66,7 @@ GlobalShortcuts::GlobalShortcuts(QWidget *parent)
AddShortcut("toggle_pretty_osd", tr("Toggle Pretty OSD"), SIGNAL(TogglePrettyOSD())); // Toggling possible only for pretty OSD
AddShortcut("shuffle_mode", tr("Change shuffle mode"), SIGNAL(CycleShuffleMode()));
AddShortcut("repeat_mode", tr("Change repeat mode"), SIGNAL(CycleRepeatMode()));
AddShortcut("toggle_scrobbling", tr("Enable/disable scrobbling"), SIGNAL(ToggleScrobbling()));
// Create backends - these do the actual shortcut registration
gnome_backend_ = new GnomeGlobalShortcutBackend(this);

View File

@ -80,6 +80,7 @@ signals:
void CycleShuffleMode();
void CycleRepeatMode();
void RemoveCurrentSong();
void ToggleScrobbling();
private:
void AddShortcut(const QString &id, const QString &name, const char *signal, const QKeySequence &default_key = QKeySequence(0));

View File

@ -73,7 +73,7 @@ bool APISeedsLyricsProvider::StartSearch(const QString &artist, const QString &a
QNetworkReply *reply = network_->get(QNetworkRequest(url));
NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleSearchReply(QNetworkReply*, quint64, QString, QString)), reply, id, artist, title);
//qLog(Debug) << "APISeedsLyrics: Sending request for" << url;
//qLog(Debug) << "APISeeds Lyrics: Sending request for" << url;
return true;
@ -90,13 +90,13 @@ void APISeedsLyricsProvider::HandleSearchReply(QNetworkReply *reply, quint64 id,
if (json_obj.isEmpty()) return;
if (!json_obj.contains("artist") || !json_obj.contains("track")) {
Error(id, "APISeedsLyrics: Invalid Json reply, result is missing artist or track.", json_obj);
Error(id, "APISeeds Lyrics: Invalid Json reply, result is missing artist or track.", json_obj);
return;
}
QJsonObject json_artist(json_obj["artist"].toObject());
QJsonObject json_track(json_obj["track"].toObject());
if (!json_track.contains("text")) {
Error(id, "APISeedsLyrics: Invalid Json reply, track is missing text.", json_obj);
Error(id, "APISeeds Lyrics: Invalid Json reply, track is missing text.", json_obj);
return;
}
@ -109,7 +109,7 @@ void APISeedsLyricsProvider::HandleSearchReply(QNetworkReply *reply, quint64 id,
if (result.artist.toLower() == artist.toLower()) result.score += 1.0;
if (result.title.toLower() == title.toLower()) result.score += 1.0;
//qLog(Debug) << "APISeedsLyrics:" << result.artist << result.title << result.lyrics;
//qLog(Debug) << "APISeeds Lyrics:" << result.artist << result.title << result.lyrics;
results << result;
@ -207,7 +207,7 @@ QJsonObject APISeedsLyricsProvider::ExtractResult(QNetworkReply *reply, quint64
void APISeedsLyricsProvider::Error(quint64 id, QString error, QVariant debug) {
LyricsSearchResults results;
if (!error.isEmpty()) qLog(Error) << "APISeedsLyrics:" << error;
if (!error.isEmpty()) qLog(Error) << "APISeeds Lyrics:" << error;
if (debug.isValid()) qLog(Debug) << debug;
emit SearchFinished(id, results);
}

View File

@ -68,6 +68,7 @@
#include "core/mimedata.h"
#include "core/tagreaderclient.h"
#include "core/song.h"
#include "core/timeconstants.h"
#include "collection/collection.h"
#include "collection/collectionbackend.h"
#include "collection/collectionplaylistitem.h"
@ -116,6 +117,9 @@ const char *Playlist::kWriteMetadata = "write_metadata";
const int Playlist::kUndoStackSize = 20;
const int Playlist::kUndoItemLimit = 500;
const qint64 Playlist::kMinScrobblePointNsecs = 1ll * kNsecPerSec;
const qint64 Playlist::kMaxScrobblePointNsecs = 240ll * kNsecPerSec;
Playlist::Playlist(PlaylistBackend *backend, TaskManager *task_manager, CollectionBackend *collection, int id, const QString &special_type, bool favorite, QObject *parent)
: QAbstractListModel(parent),
is_loading_(false),
@ -133,7 +137,10 @@ Playlist::Playlist(PlaylistBackend *backend, TaskManager *task_manager, Collecti
ignore_sorting_(false),
undo_stack_(new QUndoStack(this)),
special_type_(special_type),
cancel_restore_(false) {
cancel_restore_(false),
scrobbled_(false),
nowplaying_(false),
scrobble_point_(-1) {
undo_stack_->setUndoLimit(kUndoStackSize);
@ -612,6 +619,9 @@ void Playlist::set_current_row(int i, bool is_stopping) {
Save();
}
UpdateScrobblePoint();
nowplaying_ = false;
}
Qt::ItemFlags Playlist::flags(const QModelIndex &index) const {
@ -1488,6 +1498,8 @@ void Playlist::SetStreamMetadata(const QUrl &url, const Song &song) {
InformOfCurrentSongChange();
UpdateScrobblePoint();
}
void Playlist::ClearStreamMetadata() {
@ -1495,6 +1507,7 @@ void Playlist::ClearStreamMetadata() {
if (!current_item()) return;
current_item()->ClearTemporaryMetadata();
UpdateScrobblePoint();
emit dataChanged(index(current_item_index_.row(), 0), index(current_item_index_.row(), ColumnCount-1));
@ -1944,3 +1957,28 @@ void Playlist::SkipTracks(const QModelIndexList &source_indexes) {
}
void Playlist::UpdateScrobblePoint(qint64 seek_point_nanosec) {
const qint64 length = current_item_metadata().length_nanosec();
if (seek_point_nanosec <= 0) {
if (length == 0) {
scrobble_point_ = kMaxScrobblePointNsecs;
}
else {
scrobble_point_ = qBound(kMinScrobblePointNsecs, length / 2, kMaxScrobblePointNsecs);
}
}
else {
if (length <= 0) {
scrobble_point_ = seek_point_nanosec + kMaxScrobblePointNsecs;
}
else {
scrobble_point_ = qBound(seek_point_nanosec + kMinScrobblePointNsecs, seek_point_nanosec + (length / 2), seek_point_nanosec + kMaxScrobblePointNsecs);
}
}
scrobbled_ = false;
}

View File

@ -160,6 +160,9 @@ class Playlist : public QAbstractListModel {
static const int kUndoStackSize;
static const int kUndoItemLimit;
static const qint64 kMinScrobblePointNsecs;
static const qint64 kMaxScrobblePointNsecs;
static bool CompareItems(int column, Qt::SortOrder order, PlaylistItemPtr a, PlaylistItemPtr b);
static QString column_name(Column column);
@ -213,6 +216,13 @@ class Playlist : public QAbstractListModel {
QUndoStack *undo_stack() const { return undo_stack_; }
bool scrobbled() const { return scrobbled_; }
bool nowplaying() const { return nowplaying_; }
void set_scrobbled(bool state) { scrobbled_ = state; }
void set_nowplaying(bool state) { nowplaying_ = state; }
qint64 scrobble_point_nanosec() const { return scrobble_point_; }
void UpdateScrobblePoint(qint64 seek_point_nanosec = 0);
// Changing the playlist
void InsertItems (const PlaylistItemList &items, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
void InsertCollectionItems (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
@ -373,6 +383,11 @@ private:
// Cancel async restore if songs are already replaced
bool cancel_restore_;
bool scrobbled_;
bool nowplaying_;
qint64 scrobble_point_;
};
// QDataStream& operator <<(QDataStream&, const Playlist*);

View File

@ -0,0 +1,173 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <algorithm>
#include <QtGlobal>
#include <QDesktopServices>
#include <QUrlQuery>
#include <QCryptographicHash>
#include <QMenu>
#include <QMessageBox>
#include <QSettings>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include "core/application.h"
#include "core/closure.h"
#include "core/logging.h"
#include "core/network.h"
#include "core/player.h"
#include "core/song.h"
#include "core/taskmanager.h"
#include "core/iconloader.h"
#include "settings/settingsdialog.h"
#include "settings/scrobblersettingspage.h"
#include "audioscrobbler.h"
#include "scrobblerservices.h"
#include "scrobblerservice.h"
#include "lastfmscrobbler.h"
#include "librefmscrobbler.h"
#include "listenbrainzscrobbler.h"
AudioScrobbler::AudioScrobbler(Application *app, QObject *parent) :
QObject(parent),
app_(app),
scrobbler_services_(new ScrobblerServices(this)),
enabled_(false),
offline_(false),
scrobble_button_(false) {
scrobbler_services_->AddService(new LastFMScrobbler(app_, scrobbler_services_));
scrobbler_services_->AddService(new LibreFMScrobbler(app_, scrobbler_services_));
scrobbler_services_->AddService(new ListenBrainzScrobbler(app_, scrobbler_services_));
ReloadSettings();
}
AudioScrobbler::~AudioScrobbler() {}
void AudioScrobbler::ReloadSettings() {
QSettings s;
s.beginGroup(ScrobblerSettingsPage::kSettingsGroup);
enabled_ = s.value("enabled", false).toBool();
offline_ = s.value("offline", false).toBool();
scrobble_button_ = s.value("scrobble_button", false).toBool();
s.endGroup();
emit ScrobblingEnabledChanged(enabled_);
emit ScrobbleButtonVisibilityChanged(scrobble_button_);
for (ScrobblerService *service : scrobbler_services_->List()) {
service->ReloadSettings();
}
}
void AudioScrobbler::ToggleScrobbling() {
bool enabled_old_ = enabled_;
enabled_ = !enabled_;
QSettings s;
s.beginGroup(ScrobblerSettingsPage::kSettingsGroup);
s.setValue("enabled", enabled_);
s.endGroup();
if (enabled_ != enabled_old_) emit ScrobblingEnabledChanged(enabled_);
if (enabled_ && !offline_) { Submit(); }
}
void AudioScrobbler::ToggleOffline() {
bool offline_old_ = offline_;
offline_ = !offline_;
QSettings s;
s.beginGroup(ScrobblerSettingsPage::kSettingsGroup);
s.setValue("offline", offline_);
s.endGroup();
if (offline_ != offline_old_) { emit ScrobblingOfflineChanged(offline_); }
if (enabled_ && !offline_) { Submit(); }
}
void AudioScrobbler::ShowConfig() {
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Scrobbler);
}
void AudioScrobbler::UpdateNowPlaying(const Song &song) {
qLog(Debug) << "Sending now playing for song" << song.title();
for (ScrobblerService *service : scrobbler_services_->List()) {
if (!service->IsEnabled()) continue;
service->UpdateNowPlaying(song);
}
}
void AudioScrobbler::Scrobble(const Song &song, const int scrobble_point) {
qLog(Debug) << "Scrobbling song" << QString("") + song.title() + QString("") << "at" << scrobble_point;
for (ScrobblerService *service : scrobbler_services_->List()) {
if (!service->IsEnabled()) continue;
service->Scrobble(song);
}
}
void AudioScrobbler::Love(const Song &song) {
for (ScrobblerService *service : scrobbler_services_->List()) {
if (!service->IsEnabled() || !service->IsAuthenticated()) continue;
service->Love(song);
}
}
void AudioScrobbler::Submit() {
for (ScrobblerService *service : scrobbler_services_->List()) {
if (!service->IsEnabled() || !service->IsAuthenticated() || service->IsSubmitted()) continue;
service->Submitted();
DoInAMinuteOrSo(service, SLOT(Submit()));
}
}
void AudioScrobbler::WriteCache() {
for (ScrobblerService *service : scrobbler_services_->List()) {
if (!service->IsEnabled()) continue;
service->WriteCache();
}
}
void AudioScrobbler::ConnectError() {
for (ScrobblerService *service : scrobbler_services_->List()) {
connect(service, SIGNAL(ErrorMessage(QString)), SLOT(ErrorReceived(QString)));
}
}
void AudioScrobbler::ErrorReceived(QString error) {
emit ErrorMessage(error);
}

View File

@ -0,0 +1,88 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef AUDIOSCROBBLER_H
#define AUDIOSCROBBLER_H
#include "config.h"
#include <memory>
#include <QtGlobal>
#include <QObject>
#include <QString>
#include "scrobblerservices.h"
class Application;
class Song;
class NetworkAccessManager;
class ScrobblerService;
class AudioScrobbler : public QObject {
Q_OBJECT
public:
explicit AudioScrobbler(Application *app, QObject *parent = nullptr);
~AudioScrobbler();
void ReloadSettings();
bool IsEnabled() const { return enabled_; }
bool IsOffline() const { return offline_; }
bool ScrobbleButton() const { return scrobble_button_; }
void UpdateNowPlaying(const Song &song);
void Scrobble(const Song &song, const int scrobble_point);
void Love(const Song &song);
void ShowConfig();
void ConnectError();
ScrobblerService *ServiceByName(const QString &name) const { return scrobbler_services_->ServiceByName(name); }
template <typename T>
T *Service() {
return static_cast<T*>(this->ServiceByName(T::kName));
}
public slots:
void ToggleScrobbling();
void ToggleOffline();
void Submit();
void WriteCache();
void ErrorReceived(QString);
signals:
void ErrorMessage(QString);
void ScrobblingEnabledChanged(bool value);
void ScrobblingOfflineChanged(bool value);
void ScrobbleButtonVisibilityChanged(bool value);
private:
Application *app_;
ScrobblerServices *scrobbler_services_;
bool enabled_;
bool offline_;
bool scrobble_button_;
};
#endif // AUDIOSCROBBLER_H

View File

@ -0,0 +1,82 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <algorithm>
#include <QtGlobal>
#include <QDesktopServices>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QDateTime>
#include <QCryptographicHash>
#include <QMenu>
#include <QMessageBox>
#include <QSettings>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include "core/application.h"
#include "core/player.h"
#include "core/closure.h"
#include "core/network.h"
#include "core/song.h"
#include "core/timeconstants.h"
#include "core/logging.h"
#include "internet/localredirectserver.h"
#include "settings/settingsdialog.h"
#include "settings/scrobblersettingspage.h"
#include "audioscrobbler.h"
#include "scrobblerservices.h"
#include "scrobblercache.h"
#include "scrobblingapi20.h"
#include "lastfmscrobbler.h"
const char *LastFMScrobbler::kName = "Last.fm";
const char *LastFMScrobbler::kSettingsGroup = "LastFM";
const char *LastFMScrobbler::kAuthUrl = "https://www.last.fm/api/auth/";
const char *LastFMScrobbler::kApiUrl = "https://ws.audioscrobbler.com/2.0/";
const char *LastFMScrobbler::kCacheFile = "lastfmscrobbler.cache";
LastFMScrobbler::LastFMScrobbler(Application *app, QObject *parent) : ScrobblingAPI20(kName, kSettingsGroup, kAuthUrl, kApiUrl, true, app, parent),
auth_url_(kAuthUrl),
api_url_(kApiUrl),
app_(app),
network_(new NetworkAccessManager(this)),
cache_(new ScrobblerCache(kCacheFile, this)),
enabled_(false),
subscriber_(false),
submitted_(false) {
ReloadSettings();
LoadSession();
}
LastFMScrobbler::~LastFMScrobbler() {}

View File

@ -0,0 +1,82 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef LASTFMSCROBBLER_H
#define LASTFMSCROBBLER_H
#include "config.h"
#include <memory>
#include <QtGlobal>
#include <QObject>
#include <QNetworkReply>
#include <QPair>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QJsonObject>
#include "core/song.h"
#include "scrobblerservice.h"
#include "scrobblingapi20.h"
class Application;
class NetworkAccessManager;
class LocalRedirectServer;
class ScrobblerCache;
class LastFMScrobbler : public ScrobblingAPI20 {
Q_OBJECT
public:
explicit LastFMScrobbler(Application *app, QObject *parent = nullptr);
~LastFMScrobbler();
static const char *kName;
static const char *kSettingsGroup;
NetworkAccessManager *network() { return network_; }
ScrobblerCache *cache() { return cache_; }
private:
static const char *kAuthUrl;
static const char *kApiUrl;
static const char *kCacheFile;
QString settings_group_;
QString auth_url_;
QString api_url_;
QString api_key_;
QString secret_;
Application *app_;
NetworkAccessManager *network_;
ScrobblerCache *cache_;
bool enabled_;
bool subscriber_;
QString username_;
QString session_key_;
bool submitted_;
Song song_playing_;
quint64 timestamp_;
};
#endif // LASTFMSCROBBLER_H

View File

@ -0,0 +1,82 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <algorithm>
#include <QtGlobal>
#include <QDesktopServices>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QDateTime>
#include <QCryptographicHash>
#include <QMenu>
#include <QMessageBox>
#include <QSettings>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include "core/application.h"
#include "core/player.h"
#include "core/closure.h"
#include "core/network.h"
#include "core/song.h"
#include "core/timeconstants.h"
#include "core/logging.h"
#include "internet/localredirectserver.h"
#include "settings/settingsdialog.h"
#include "settings/scrobblersettingspage.h"
#include "audioscrobbler.h"
#include "scrobblerservices.h"
#include "scrobblercache.h"
#include "scrobblingapi20.h"
#include "librefmscrobbler.h"
const char *LibreFMScrobbler::kName = "Libre.fm";
const char *LibreFMScrobbler::kSettingsGroup = "LibreFM";
const char *LibreFMScrobbler::kAuthUrl = "https://www.libre.fm/api/auth/";
const char *LibreFMScrobbler::kApiUrl = "https://libre.fm/2.0/";
const char *LibreFMScrobbler::kCacheFile = "librefmscrobbler.cache";
LibreFMScrobbler::LibreFMScrobbler(Application *app, QObject *parent) : ScrobblingAPI20(kName, kSettingsGroup, kAuthUrl, kApiUrl, false, app, parent),
auth_url_(kAuthUrl),
api_url_(kApiUrl),
app_(app),
network_(new NetworkAccessManager(this)),
cache_(new ScrobblerCache(kCacheFile, this)),
enabled_(false),
subscriber_(false),
submitted_(false) {
ReloadSettings();
LoadSession();
}
LibreFMScrobbler::~LibreFMScrobbler() {}

View File

@ -0,0 +1,82 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef LIBREFMSCROBBLER_H
#define LIBREFMSCROBBLER_H
#include "config.h"
#include <memory>
#include <QtGlobal>
#include <QObject>
#include <QNetworkReply>
#include <QPair>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QJsonObject>
#include "core/song.h"
#include "scrobblerservice.h"
#include "scrobblingapi20.h"
class Application;
class NetworkAccessManager;
class LocalRedirectServer;
class ScrobblerCache;
class LibreFMScrobbler : public ScrobblingAPI20 {
Q_OBJECT
public:
explicit LibreFMScrobbler(Application *app, QObject *parent = nullptr);
~LibreFMScrobbler();
static const char *kName;
static const char *kSettingsGroup;
NetworkAccessManager *network() { return network_; }
ScrobblerCache *cache() { return cache_; }
private:
static const char *kAuthUrl;
static const char *kApiUrl;
static const char *kCacheFile;
QString settings_group_;
QString auth_url_;
QString api_url_;
QString api_key_;
QString secret_;
Application *app_;
NetworkAccessManager *network_;
ScrobblerCache *cache_;
bool enabled_;
bool subscriber_;
QString username_;
QString session_key_;
bool submitted_;
Song song_playing_;
quint64 timestamp_;
};
#endif // LIBREFMSCROBBLER_H

View File

@ -0,0 +1,482 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <algorithm>
#include <QtGlobal>
#include <QDesktopServices>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QDateTime>
#include <QCryptographicHash>
#include <QMenu>
#include <QMessageBox>
#include <QSettings>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include "core/application.h"
#include "core/player.h"
#include "core/closure.h"
#include "core/network.h"
#include "core/song.h"
#include "core/timeconstants.h"
#include "core/logging.h"
#include "internet/localredirectserver.h"
#include "settings/settingsdialog.h"
#include "settings/scrobblersettingspage.h"
#include "audioscrobbler.h"
#include "scrobblerservices.h"
#include "scrobblerservice.h"
#include "scrobblercache.h"
#include "scrobblercacheitem.h"
#include "listenbrainzscrobbler.h"
const char *ListenBrainzScrobbler::kName = "ListenBrainz";
const char *ListenBrainzScrobbler::kSettingsGroup = "ListenBrainz";
const char *ListenBrainzScrobbler::kAuthUrl = "https://musicbrainz.org/oauth2/authorize";
const char *ListenBrainzScrobbler::kAuthTokenUrl = "https://musicbrainz.org/oauth2/token";
const char *ListenBrainzScrobbler::kRedirectUrl = "http://localhost";
const char *ListenBrainzScrobbler::kApiUrl = "https://api.listenbrainz.org";
const char *ListenBrainzScrobbler::kClientID = "oeAUNwqSQer0er09Fiqi0Q";
const char *ListenBrainzScrobbler::kClientSecret = "ROFghkeQ3F3oPyEhqiyWPA";
const char *ListenBrainzScrobbler::kCacheFile = "listenbrainzscrobbler.cache";
const int ListenBrainzScrobbler::kScrobblesPerRequest = 10;
ListenBrainzScrobbler::ListenBrainzScrobbler(Application *app, QObject *parent) : ScrobblerService(kName, app, parent),
app_(app),
network_(new NetworkAccessManager(this)),
cache_(new ScrobblerCache(kCacheFile, this)),
enabled_(false),
expires_in_(-1),
submitted_(false) {
ReloadSettings();
LoadSession();
}
ListenBrainzScrobbler::~ListenBrainzScrobbler() {}
void ListenBrainzScrobbler::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
enabled_ = s.value("enabled", false).toBool();
user_token_ = s.value("user_token").toString();
s.endGroup();
}
void ListenBrainzScrobbler::LoadSession() {
QSettings s;
s.beginGroup(kSettingsGroup);
access_token_ = s.value("access_token").toString();
expires_in_ = s.value("expires_in", -1).toInt();
token_type_ = s.value("token_type").toString();
refresh_token_ = s.value("refresh_token").toString();
s.endGroup();
}
void ListenBrainzScrobbler::Logout() {
access_token_.clear();
expires_in_ = -1;
token_type_.clear();
refresh_token_.clear();
QSettings settings;
settings.beginGroup(kSettingsGroup);
settings.remove("access_token");
settings.remove("expires_in");
settings.remove("token_type");
settings.remove("refresh_token");
settings.endGroup();
}
void ListenBrainzScrobbler::Authenticate() {
QUrl url(kAuthUrl);
LocalRedirectServer *server = new LocalRedirectServer(this);
server->Listen();
NewClosure(server, SIGNAL(Finished()), this, &ListenBrainzScrobbler::RedirectArrived, server);
QUrl redirect_url(kRedirectUrl);
redirect_url.setPort(server->url().port());
QUrlQuery url_query;
url_query.addQueryItem("response_type", "code");
url_query.addQueryItem("client_id", kClientID);
url_query.addQueryItem("redirect_uri", redirect_url.toString());
url_query.addQueryItem("scope", "profile;email;tag;rating;collection;submit_isrc;submit_barcode");
url.setQuery(url_query);
bool result = QDesktopServices::openUrl(url);
if (!result) {
QMessageBox box(QMessageBox::NoIcon, "Scrobbler Authentication", QString("Please open this URL in your browser: <a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
box.setTextFormat(Qt::RichText);
qLog(Debug) << "Scrobbler authentication URL: " << url.toString();
box.exec();
}
}
void ListenBrainzScrobbler::RedirectArrived(LocalRedirectServer *server) {
server->deleteLater();
QUrl url = server->request_url();
if (!QUrlQuery(url).queryItemValue("error").isEmpty()) {
AuthError(QUrlQuery(url).queryItemValue("error"));
return;
}
if (QUrlQuery(url).queryItemValue("code").isEmpty()) {
AuthError("Redirect missing token code!");
return;
}
RequestSession(url, QUrlQuery(url).queryItemValue("code").toUtf8());
}
void ListenBrainzScrobbler::RequestSession(QUrl url, QString token) {
QUrl session_url(kAuthTokenUrl);
QUrlQuery url_query;
url_query.addQueryItem("grant_type", "authorization_code");
url_query.addQueryItem("code", token);
url_query.addQueryItem("client_id", kClientID);
url_query.addQueryItem("client_secret", kClientSecret);
url_query.addQueryItem("redirect_uri", url.toString());
QNetworkRequest req(session_url);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
QNetworkReply *reply = network_->post(req, query);
NewClosure(reply, SIGNAL(finished()), this, SLOT(AuthenticateReplyFinished(QNetworkReply*)), reply);
}
void ListenBrainzScrobbler::AuthenticateReplyFinished(QNetworkReply *reply) {
reply->deleteLater();
QByteArray data;
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
else {
if (reply->error() < 200) {
// This is a network error, there is nothing more to do.
QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
AuthError(failure_reason);
}
else {
// See if there is Json data containing "error" and "error_description" - then use that instead.
data = reply->readAll();
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
QString failure_reason;
if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains("error") && json_obj.contains("error_description")) {
QString error = json_obj["error"].toString();
failure_reason = json_obj["error_description"].toString();
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
AuthError(failure_reason);
}
return;
}
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
AuthError("Json document from server was empty.");
return;
}
if (json_obj.contains("error") && json_obj.contains("error_description")) {
QString error = json_obj["error"].toString();
QString failure_reason = json_obj["error_description"].toString();
AuthError(failure_reason);
return;
}
if (!json_obj.contains("access_token") || !json_obj.contains("expires_in") || !json_obj.contains("token_type") || !json_obj.contains("refresh_token")) {
AuthError("Json session object is missing values.");
return;
}
access_token_ = json_obj["access_token"].toString();
expires_in_ = json_obj["expires_in"].toInt();
token_type_ = json_obj["token_type"].toString();
refresh_token_ = json_obj["refresh_token"].toString();
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("access_token", access_token_);
s.setValue("expires_in", expires_in_);
s.setValue("token_type", token_type_);
s.setValue("refresh_token", refresh_token_);
s.endGroup();
emit AuthenticationComplete(true);
}
QNetworkReply *ListenBrainzScrobbler::CreateRequest(const QUrl &url, const QJsonDocument &json_doc) {
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
req.setRawHeader("Authorization", QString("Token %1").arg(user_token_).toUtf8());
QNetworkReply *reply = network_->post(req, json_doc.toJson());
//qLog(Debug) << "ListenBrainz: Sending request" << json_doc.toJson();
return reply;
}
QByteArray ListenBrainzScrobbler::GetReplyData(QNetworkReply *reply) {
QByteArray data;
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
else {
if (reply->error() < 200) {
// This is a network error, there is nothing more to do.
QString error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
Error(error_reason);
}
else {
// See if there is Json data containing "error" and "error_description" - then use that instead.
data = reply->readAll();
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
int error_code = -1;
QString error_reason;
if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains("code") && json_obj.contains("error")) {
error_code = json_obj["code"].toInt();
QString message = json_obj["error"].toString();
error_reason = QString("%1 (%2)").arg(message).arg(error_code);
}
else {
error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
}
else {
error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
if (reply->error() == QNetworkReply::ContentAccessDenied || reply->error() == QNetworkReply::ContentOperationNotPermittedError || reply->error() == QNetworkReply::AuthenticationRequiredError) {
// Session is probably expired
Logout();
Error(error_reason);
}
else if (reply->error() == QNetworkReply::ContentNotFoundError) { // Ignore this error
Error(error_reason);
}
else { // Fail
Error(error_reason);
}
}
return QByteArray();
}
return data;
}
void ListenBrainzScrobbler::UpdateNowPlaying(const Song &song) {
song_playing_ = song;
timestamp_ = QDateTime::currentDateTime().toTime_t();
if (!song.is_metadata_good()) return;
// FIXME: This does not work: BAD REQUEST (302)
return;
QJsonObject object_listen;
object_listen.insert("listened_at", QJsonValue::fromVariant(timestamp_));
QJsonObject object_track_metadata;
object_track_metadata.insert("artist_name", QJsonValue::fromVariant(song.artist()));
object_track_metadata.insert("release_name", QJsonValue::fromVariant(song.album()));
object_track_metadata.insert("track_name", QJsonValue::fromVariant(song.title()));
object_listen.insert("track_metadata", object_track_metadata);
QJsonArray array_payload;
array_payload.append(object_listen);
QJsonObject object;
object.insert("listen_type", "playing_now");
object.insert("payload", array_payload);
QJsonDocument doc(object);
QUrl url(QString("%1/1/submit-listens").arg(kApiUrl));
QNetworkReply *reply = CreateRequest(url, doc);
NewClosure(reply, SIGNAL(finished()), this, SLOT(UpdateNowPlayingRequestFinished(QNetworkReply*)), reply);
}
void ListenBrainzScrobbler::UpdateNowPlayingRequestFinished(QNetworkReply *reply) {
reply->deleteLater();
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
return;
}
qLog(Debug) << data;
// TODO
}
void ListenBrainzScrobbler::Scrobble(const Song &song) {
if (song.id() != song_playing_.id() || song.url() != song_playing_.url() || !song.is_metadata_good()) return;
cache_->Add(song, timestamp_);
if (app_->scrobbler()->IsOffline()) return;
if (!IsAuthenticated()) {
emit ErrorMessage("ListenBrainz is not authenticated!");
return;
}
if (!submitted_) {
DoInAMinuteOrSo(this, SLOT(Submit()));
submitted_ = true;
}
}
void ListenBrainzScrobbler::Submit() {
qLog(Debug) << __PRETTY_FUNCTION__;
submitted_ = false;
if (!IsEnabled() || !IsAuthenticated() || app_->scrobbler()->IsOffline()) return;
QJsonArray array;
int i(0);
QList<quint64> list;
for (ScrobblerCacheItem *item : cache_->List()) {
if (item->sent_) continue;
item->sent_ = true;
i++;
list << item->timestamp_;
QJsonObject object_listen;
object_listen.insert("listened_at", QJsonValue::fromVariant(item->timestamp_));
QJsonObject object_track_metadata;
object_track_metadata.insert("artist_name", QJsonValue::fromVariant(item->artist_));
object_track_metadata.insert("release_name", QJsonValue::fromVariant(item->album_));
object_track_metadata.insert("track_name", QJsonValue::fromVariant(item->song_));
object_listen.insert("track_metadata", object_track_metadata);
array.append(QJsonValue::fromVariant(object_listen));
if (i >= kScrobblesPerRequest) break;
}
if (i <= 0) return;
QJsonObject object;
object.insert("listen_type", "import");
object.insert("payload", array);
QJsonDocument doc(object);
QUrl url(QString("%1/1/submit-listens").arg(kApiUrl));
QNetworkReply *reply = CreateRequest(url, doc);
NewClosure(reply, SIGNAL(finished()), this, SLOT(ScrobbleRequestFinished(QNetworkReply*, QList<quint64>)), reply, list);
}
void ListenBrainzScrobbler::ScrobbleRequestFinished(QNetworkReply *reply, QList<quint64> list) {
reply->deleteLater();
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
cache_->ClearSent(list);
return;
}
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
cache_->ClearSent(list);
return;
}
if (json_obj.contains("error") && json_obj.contains("error_description")) {
QString error_code = json_obj["error"].toString();
QString error_desc = json_obj["error_description"].toString();
Error(error_desc);
cache_->ClearSent(list);
return;
}
if (json_obj.contains("status")) {
QString status = json_obj["status"].toString();
qLog(Debug) << "MusicBrainz: Received scrobble status:" << status;
}
cache_->Flush(list);
}
void ListenBrainzScrobbler::Love(const Song &song) {}
void ListenBrainzScrobbler::AuthError(QString error) {
emit AuthenticationComplete(false, error);
}
void ListenBrainzScrobbler::Error(QString error, QVariant debug) {
qLog(Error) << "ListenBrainz:" << error;
if (debug.isValid()) qLog(Debug) << debug;
}

View File

@ -0,0 +1,117 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef LISTENBRAINZSCROBBLER_H
#define LISTENBRAINZSCROBBLER_H
#include "config.h"
#include <memory>
#include <QtGlobal>
#include <QObject>
#include <QNetworkReply>
#include <QPair>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QJsonObject>
#include "core/song.h"
#include "scrobblerservice.h"
#include "scrobblercache.h"
class Application;
class NetworkAccessManager;
class LocalRedirectServer;
class ScrobblerCacheItem;
class ListenBrainzScrobbler : public ScrobblerService {
Q_OBJECT
public:
explicit ListenBrainzScrobbler(Application *app, QObject *parent = nullptr);
~ListenBrainzScrobbler();
static const char *kName;
static const char *kSettingsGroup;
void ReloadSettings();
void LoadSession();
bool IsEnabled() const { return enabled_; }
bool IsAuthenticated() const { return !access_token_.isEmpty() && !user_token_.isEmpty(); }
bool IsSubmitted() const { return submitted_; }
void Submitted() { submitted_ = true; }
QString user_token() const { return user_token_; }
void Authenticate();
void Logout();
void ShowConfig();
void Submit();
void UpdateNowPlaying(const Song &song);
void Scrobble(const Song &song);
void Love(const Song &song);
signals:
void AuthenticationComplete(bool success, QString error = QString());
public slots:
void WriteCache() { cache_->WriteCache(); }
private slots:
void RedirectArrived(LocalRedirectServer *server);
void AuthenticateReplyFinished(QNetworkReply *reply);
void UpdateNowPlayingRequestFinished(QNetworkReply *reply);
void ScrobbleRequestFinished(QNetworkReply *reply, QList<quint64>);
private:
QNetworkReply *CreateRequest(const QUrl &url, const QJsonDocument &json_doc);
QByteArray GetReplyData(QNetworkReply *reply);
void RequestSession(QUrl url, QString token);
void AuthError(QString error);
void Error(QString error, QVariant debug = QVariant());
static const char *kAuthUrl;
static const char *kAuthTokenUrl;
static const char *kApiUrl;
static const char *kClientID;
static const char *kClientSecret;
static const char *kCacheFile;
static const char *kRedirectUrl;
static const int kScrobblesPerRequest;
Application *app_;
NetworkAccessManager *network_;
ScrobblerCache *cache_;
bool enabled_;
QString user_token_;
QString access_token_;
qint64 expires_in_;
QString token_type_;
QString refresh_token_;
bool submitted_;
Song song_playing_;
quint64 timestamp_;
};
#endif // LISTENBRAINZSCROBBLER_H

View File

@ -0,0 +1,230 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QObject>
#include <QStandardPaths>
#include <QString>
#include <QFile>
#include <QIODevice>
#include <QTextStream>
#include <QDateTime>
#include <QMultiHash>
#include <QJsonValue>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonParseError>
#include "core/song.h"
#include "core/logging.h"
#include "core/closure.h"
#include "scrobblercache.h"
#include "scrobblercacheitem.h"
ScrobblerCache::ScrobblerCache(const QString &filename, QObject *parent) :
QObject(parent),
filename_(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + filename),
loaded_(false) {
ReadCache();
loaded_ = true;
}
ScrobblerCache::~ScrobblerCache() {}
void ScrobblerCache::ReadCache() {
QFile file(filename_);
bool result = file.open(QIODevice::ReadOnly | QIODevice::Text);
if (!result) return;
QTextStream stream(&file);
stream.setCodec("UTF-8");
QString data = stream.readAll();
file.close();
if (data.isEmpty()) return;
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data.toUtf8(), &error);
if (error.error != QJsonParseError::NoError) {
qLog(Error) << "Scrobbler cache is missing JSON data.";
return;
}
if (json_doc.isNull() || json_doc.isEmpty()) {
qLog(Error) << "Scrobbler cache has empty JSON document.";
return;
}
if (!json_doc.isObject()) {
qLog(Error) << "Scrobbler cache JSON document is not an object.";
return;
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
qLog(Error) << "Scrobbler cache has empty JSON object.";
return;
}
if (!json_obj.contains("tracks")) {
qLog(Error) << "Scrobbler cache is missing JSON tracks.";
return;
}
QJsonValue json_tracks = json_obj["tracks"];
if (!json_tracks.isArray()) {
qLog(Error) << "Scrobbler cache JSON tracks is not an array.";
return;
}
QJsonArray json_array = json_tracks.toArray();
if (json_array.isEmpty()) {
return;
}
for (const QJsonValue &value : json_array) {
if (!value.isObject()) {
qLog(Error) << "Scrobbler cache JSON tracks array value is not an object.";
qLog(Debug) << value;
continue;
}
QJsonObject json_obj_track = value.toObject();
if (
!json_obj_track.contains("timestamp") ||
!json_obj_track.contains("song") ||
!json_obj_track.contains("album") ||
!json_obj_track.contains("artist") ||
!json_obj_track.contains("albumartist") ||
!json_obj_track.contains("track") ||
!json_obj_track.contains("duration")
) {
qLog(Error) << "Scrobbler cache JSON tracks array value is missing data.";
qLog(Debug) << value;
continue;
}
quint64 timestamp = json_obj_track["timestamp"].toVariant().toULongLong();
QString artist = json_obj_track["artist"].toString();
QString album = json_obj_track["album"].toString();
QString song = json_obj_track["song"].toString();
QString albumartist = json_obj_track["albumartist"].toString();
int track = json_obj_track["track"].toInt();
qint64 duration = json_obj_track["duration"].toVariant().toLongLong();
if (timestamp <= 0 || artist.isEmpty() || album.isEmpty() || song.isEmpty() || duration <= 0) {
qLog(Error) << "Invalid cache data" << "for song" << song;
continue;
}
if (scrobbler_cache_.contains(timestamp)) continue;
ScrobblerCacheItem *item = new ScrobblerCacheItem(artist, album, song, albumartist, track, duration, timestamp);
scrobbler_cache_.insert(timestamp, item);
}
}
void ScrobblerCache::WriteCache() {
if (!loaded_) return;
qLog(Debug) << "Writing scrobbler cache file" << filename_;
QJsonArray array;
QMultiHash <quint64, ScrobblerCacheItem*> ::iterator i;
for (i = scrobbler_cache_.begin() ; i != scrobbler_cache_.end() ; ++i) {
ScrobblerCacheItem *item = i.value();
QJsonObject object;
object.insert("timestamp", QJsonValue::fromVariant(item->timestamp_));
object.insert("artist", QJsonValue::fromVariant(item->artist_));
object.insert("album", QJsonValue::fromVariant(item->album_));
object.insert("song", QJsonValue::fromVariant(item->song_));
object.insert("albumartist", QJsonValue::fromVariant(item->albumartist_));
object.insert("track", QJsonValue::fromVariant(item->track_));
object.insert("duration", QJsonValue::fromVariant(item->duration_));
array.append(QJsonValue::fromVariant(object));
}
QJsonObject object;
object.insert("tracks", array);
QJsonDocument doc(object);
QFile file(filename_);
bool result = file.open(QIODevice::WriteOnly | QIODevice::Text);
if (!result) {
qLog(Error) << "Unable to open scrobbler cache file" << filename_;
return;
}
QTextStream stream(&file);
stream.setCodec("UTF-8");
stream << doc.toJson();
file.close();
}
ScrobblerCacheItem *ScrobblerCache::Add(const Song &song, const quint64 &timestamp) {
if (scrobbler_cache_.contains(timestamp)) return nullptr;
ScrobblerCacheItem *item = new ScrobblerCacheItem(song.artist(), song.album(), song.title(), song.albumartist(), song.track(), song.length_nanosec(), timestamp);
scrobbler_cache_.insert(timestamp, item);
if (loaded_) DoInAMinuteOrSo(this, SLOT(WriteCache()));
return item;
}
ScrobblerCacheItem *ScrobblerCache::Get(const quint64 hash) {
if (scrobbler_cache_.contains(hash)) { return scrobbler_cache_.value(hash); }
else return nullptr;
}
void ScrobblerCache::Remove(const quint64 hash) {
if (!scrobbler_cache_.contains(hash)) {
qLog(Error) << "Tried to remove non-existing hash" << hash;
return;
}
delete scrobbler_cache_.take(hash);
}
void ScrobblerCache::Remove(ScrobblerCacheItem &item) {
delete scrobbler_cache_.take(item.timestamp_);
}
void ScrobblerCache::ClearSent(const QList<quint64> list) {
for (quint64 timestamp : list) {
if (!scrobbler_cache_.contains(timestamp)) continue;
ScrobblerCacheItem *item = scrobbler_cache_.take(timestamp);
item->sent_ = false;
}
}
void ScrobblerCache::Flush(const QList<quint64> list) {
for (quint64 timestamp : list) {
if (!scrobbler_cache_.contains(timestamp)) continue;
delete scrobbler_cache_.take(timestamp);
}
DoInAMinuteOrSo(this, SLOT(WriteCache()));
}

View File

@ -0,0 +1,64 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SCROBBLERCACHE_H
#define SCROBBLERCACHE_H
#include "config.h"
#include <stdbool.h>
#include <QObject>
#include <QList>
#include <QHash>
#include <QString>
#include <QDateTime>
class Application;
class Song;
class ScrobblerCacheItem;
class ScrobblerCache : public QObject {
Q_OBJECT
public:
explicit ScrobblerCache(const QString &filename, QObject *parent);
~ScrobblerCache();
void ReadCache();
ScrobblerCacheItem *Add(const Song &song, const quint64 &timestamp);
ScrobblerCacheItem *Get(const quint64 hash);
void Remove(const quint64 hash);
void Remove(ScrobblerCacheItem &item);
QList<ScrobblerCacheItem*> List() const { return scrobbler_cache_.values(); }
void ClearSent(const QList<quint64> list);
void Flush(const QList<quint64> list);
public slots:
void WriteCache();
private:
QString filename_;
bool loaded_;
QHash <quint64, ScrobblerCacheItem*> scrobbler_cache_;
};
#endif // SCROBBLERCACHE_H

View File

@ -0,0 +1,39 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QObject>
#include <QtGlobal>
#include <QString>
#include <QHash>
#include "scrobblercacheitem.h"
ScrobblerCacheItem::ScrobblerCacheItem(const QString &artist, const QString &album, const QString &song, const QString &albumartist, const int track, const qint64 duration, const quint64 &timestamp) :
artist_(artist),
album_(album),
song_(song),
albumartist_(albumartist),
track_(track),
duration_(duration),
timestamp_(timestamp),
sent_(false) {}
ScrobblerCacheItem::~ScrobblerCacheItem() {}

View File

@ -0,0 +1,48 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SCROBBLERCACHEITEM_H
#define SCROBBLERCACHEITEM_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QString>
class ScrobblerCacheItem : public QObject {
Q_OBJECT
public:
explicit ScrobblerCacheItem(const QString &artist, const QString &album, const QString &song, const QString &albumartist, const int track, const qint64 duration, const quint64 &timestamp);
~ScrobblerCacheItem();
public:
QString artist_;
QString album_;
QString song_;
QString albumartist_;
int track_;
qint64 duration_;
quint64 timestamp_;
bool sent_;
};
#endif // SCROBBLERCACHEITEM_H

View File

@ -0,0 +1,57 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QObject>
#include <QString>
#include <QByteArray>
#include <QJsonDocument>
#include <QJsonObject>
#include "scrobblerservice.h"
ScrobblerService::ScrobblerService(const QString &name, Application *app, QObject *parent) : QObject(parent), name_(name) {}
QJsonObject ScrobblerService::ExtractJsonObj(const QByteArray &data) {
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
Error("Reply from server missing Json data.", data);
return QJsonObject();
}
if (json_doc.isNull() || json_doc.isEmpty()) {
Error("Received empty Json document.", json_doc);
return QJsonObject();
}
if (!json_doc.isObject()) {
Error("Json document is not an object.", json_doc);
return QJsonObject();
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
Error("Received empty Json object.", json_doc);
return QJsonObject();
}
return json_obj;
}

View File

@ -0,0 +1,76 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SCROBBLERSERVICE_H
#define SCROBBLERSERVICE_H
#include "config.h"
#include <stdbool.h>
#include <QObject>
#include <QString>
#include <QVariant>
#include <QByteArray>
#include <QJsonDocument>
#include <QJsonObject>
class Application;
class Song;
class ScrobblerService : public QObject {
Q_OBJECT
public:
explicit ScrobblerService(const QString &name, Application *app, QObject *parent);
QString name() const { return name_; }
virtual void ReloadSettings() = 0;
virtual bool IsEnabled() const { return false; }
virtual bool IsAuthenticated() const { return false; }
virtual void UpdateNowPlaying(const Song &song) = 0;
virtual void Scrobble(const Song &song) = 0;
virtual void Love(const Song &song) = 0;
virtual void Error(QString error, QVariant debug = QVariant()) = 0;
virtual void Submitted() = 0;
virtual bool IsSubmitted() const { return false; }
typedef QPair<QString, QString> Param;
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<Param> ParamList;
QJsonObject ExtractJsonObj(const QByteArray &data);
public slots:
virtual void Submit() = 0;
virtual void WriteCache() = 0;
signals:
void ErrorMessage(QString);
private:
QString name_;
};
#endif // SCROBBLERSERVICE_H

View File

@ -0,0 +1,77 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QObject>
#include <QMutexLocker>
#include <QString>
#include <QtDebug>
#include "core/application.h"
#include "core/logging.h"
#include "scrobblerservices.h"
#include "scrobblerservice.h"
ScrobblerServices::ScrobblerServices(QObject *parent) : QObject(parent) {}
ScrobblerServices::~ScrobblerServices() {}
void ScrobblerServices::AddService(ScrobblerService *service) {
{
QMutexLocker locker(&mutex_);
scrobbler_services_.insert(service->name(), service);
connect(service, SIGNAL(destroyed()), SLOT(ServiceDestroyed()));
}
qLog(Debug) << "Registered scrobbler service" << service->name();
}
void ScrobblerServices::RemoveService(ScrobblerService *service) {
if (!service || !scrobbler_services_.contains(service->name())) return;
{
QMutexLocker locker(&mutex_);
scrobbler_services_.remove(service->name());
disconnect(service, 0, this, 0);
}
qLog(Debug) << "Unregistered scrobbler service" << service->name();
}
void ScrobblerServices::ServiceDestroyed() {
ScrobblerService *service = static_cast<ScrobblerService*>(sender());
RemoveService(service);
}
int ScrobblerServices::NextId() { return next_id_.fetchAndAddRelaxed(1); }
ScrobblerService *ScrobblerServices::ServiceByName(const QString &name) {
if (scrobbler_services_.contains(name)) return scrobbler_services_.value(name);
return nullptr;
}

View File

@ -0,0 +1,70 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SCROBBLERSERVICES_H
#define SCROBBLERSERVICES_H
#include "config.h"
#include <stdbool.h>
#include <QtGlobal>
#include <QObject>
#include <QMutex>
#include <QList>
#include <QMap>
#include <QString>
#include <QAtomicInt>
class Application;
class ScrobblerService;
class ScrobblerServices : public QObject {
Q_OBJECT
public:
explicit ScrobblerServices(QObject *parent = nullptr);
~ScrobblerServices();
void AddService(ScrobblerService *service);
void RemoveService(ScrobblerService *service);
QList<ScrobblerService*> List() const { return scrobbler_services_.values(); }
bool HasAnyServices() const { return !scrobbler_services_.isEmpty(); }
int NextId();
ScrobblerService *ServiceByName(const QString &name);
template <typename T>
T *Service() {
return static_cast<T*>(this->ServiceByName(T::kName));
}
private slots:
void ServiceDestroyed();
private:
Q_DISABLE_COPY(ScrobblerServices);
QMap<QString, ScrobblerService *> scrobbler_services_;
QMutex mutex_;
QAtomicInt next_id_;
};
#endif // SCROBBLERSERVICES_H

View File

@ -0,0 +1,740 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <algorithm>
#include <QtGlobal>
#include <QDesktopServices>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QDateTime>
#include <QCryptographicHash>
#include <QMenu>
#include <QMessageBox>
#include <QSettings>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include "core/application.h"
#include "core/player.h"
#include "core/closure.h"
#include "core/network.h"
#include "core/song.h"
#include "core/timeconstants.h"
#include "core/logging.h"
#include "internet/localredirectserver.h"
#include "settings/settingsdialog.h"
#include "settings/scrobblersettingspage.h"
#include "audioscrobbler.h"
#include "scrobblerservices.h"
#include "scrobblingapi20.h"
#include "scrobblercache.h"
#include "scrobblercacheitem.h"
const char *ScrobblingAPI20::kApiKey = "211990b4c96782c05d1536e7219eb56e";
const char *ScrobblingAPI20::kSecret = "80fd738f49596e9709b1bf9319c444a8";
const char *ScrobblingAPI20::kRedirectUrl = "https://oauth.strawbs.net";
const int ScrobblingAPI20::kScrobblesPerRequest = 50;
ScrobblingAPI20::ScrobblingAPI20(const QString &name, const QString &settings_group, const QString &auth_url, const QString &api_url, const bool batch, Application *app, QObject *parent) :
ScrobblerService(name, app, parent),
name_(name),
settings_group_(settings_group),
auth_url_(auth_url),
api_url_(api_url),
batch_(batch),
app_(app),
enabled_(false),
subscriber_(false),
submitted_(false) {}
ScrobblingAPI20::~ScrobblingAPI20() {}
void ScrobblingAPI20::ReloadSettings() {
QSettings s;
s.beginGroup(settings_group_);
enabled_ = s.value("enabled", false).toBool();
s.endGroup();
}
void ScrobblingAPI20::LoadSession() {
QSettings s;
s.beginGroup(settings_group_);
subscriber_ = s.value("subscriber", false).toBool();
username_ = s.value("username").toString();
session_key_ = s.value("session_key").toString();
s.endGroup();
}
void ScrobblingAPI20::Logout() {
subscriber_ = false;
username_.clear();
session_key_.clear();
QSettings settings;
settings.beginGroup(settings_group_);
settings.remove("subscriber");
settings.remove("username");
settings.remove("session_key");
settings.endGroup();
}
void ScrobblingAPI20::Authenticate() {
QUrl url(auth_url_);
LocalRedirectServer *server = new LocalRedirectServer(this);
server->Listen();
NewClosure(server, SIGNAL(Finished()), this, &ScrobblingAPI20::RedirectArrived, server);
QUrl redirect_url(kRedirectUrl);
QUrlQuery redirect_url_query;
const QString port = QString::number(server->url().port());
redirect_url_query.addQueryItem("port", port);
redirect_url.setQuery(redirect_url_query);
QUrlQuery url_query;
url_query.addQueryItem("api_key", kApiKey);
url_query.addQueryItem("cb", redirect_url.toString());
url.setQuery(url_query);
bool result = QDesktopServices::openUrl(url);
if (!result) {
QMessageBox box(QMessageBox::NoIcon, "Scrobbler Authentication", QString("Please open this URL in your browser: <a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
box.setTextFormat(Qt::RichText);
qLog(Debug) << "Scrobbler authentication URL: " << url.toString();
box.exec();
}
}
void ScrobblingAPI20::RedirectArrived(LocalRedirectServer *server) {
server->deleteLater();
QUrl url = server->request_url();
RequestSession(QUrlQuery(url).queryItemValue("token").toUtf8());
}
void ScrobblingAPI20::RequestSession(QString token) {
QUrl session_url(api_url_);
QUrlQuery session_url_query;
session_url_query.addQueryItem("api_key", kApiKey);
session_url_query.addQueryItem("method", "auth.getSession");
session_url_query.addQueryItem("token", token);
QString data_to_sign;
for (QPair<QString, QString> param : session_url_query.queryItems()) {
data_to_sign += param.first + param.second;
}
data_to_sign += kSecret;
QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5);
QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, '0').toLower();
session_url_query.addQueryItem("api_sig", signature);
session_url_query.addQueryItem(QUrl::toPercentEncoding("format"), QUrl::toPercentEncoding("json"));
session_url.setQuery(session_url_query);
QNetworkRequest req(session_url);
QNetworkReply *reply = network()->get(req);
NewClosure(reply, SIGNAL(finished()), this, SLOT(AuthenticateReplyFinished(QNetworkReply*)), reply);
}
void ScrobblingAPI20::AuthenticateReplyFinished(QNetworkReply *reply) {
reply->deleteLater();
QByteArray data;
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
else {
if (reply->error() < 200) {
// This is a network error, there is nothing more to do.
QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
AuthError(failure_reason);
}
else {
// See if there is Json data containing "error" and "message" - then use that instead.
data = reply->readAll();
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
QString failure_reason;
if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains("error") && json_obj.contains("message")) {
int error = json_obj["error"].toInt();
QString message = json_obj["message"].toString();
failure_reason = "Error: " + QString::number(error) + ": " + message;
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
AuthError(failure_reason);
}
return;
}
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
AuthError("Json document from server was empty.");
return;
}
if (json_obj.contains("error") && json_obj.contains("message")) {
int error = json_obj["error"].toInt();
QString message = json_obj["message"].toString();
QString failure_reason = "Error: " + QString::number(error) + ": " + message;
AuthError(failure_reason);
return;
}
if (!json_obj.contains("session")) {
AuthError("Json reply from server is missing session.");
return;
}
QJsonValue json_session = json_obj["session"];
if (!json_session.isObject()) {
AuthError("Json session is not an object.");
return;
}
json_obj = json_session.toObject();
if (json_obj.isEmpty()) {
AuthError("Json session object is empty.");
return;
}
if (!json_obj.contains("subscriber") || !json_obj.contains("name") || !json_obj.contains("key")) {
AuthError("Json session object is missing values.");
return;
}
subscriber_ = json_obj["subscriber"].toBool();
username_ = json_obj["name"].toString();
session_key_ = json_obj["key"].toString();
QSettings s;
s.beginGroup(settings_group_);
s.setValue("subscriber", subscriber_);
s.setValue("username", username_);
s.setValue("session_key", session_key_);
s.endGroup();
emit AuthenticationComplete(true);
}
QNetworkReply *ScrobblingAPI20::CreateRequest(const ParamList &request_params) {
ParamList params = ParamList()
<< Param("api_key", kApiKey)
<< Param("sk", session_key_)
<< Param("lang", QLocale().name().left(2).toLower())
<< request_params;
std::sort(params.begin(), params.end());
QUrlQuery url_query;
QString data_to_sign;
for (const Param &param : params) {
EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
url_query.addQueryItem(encoded_param.first, encoded_param.second);
data_to_sign += param.first + param.second;
}
data_to_sign += kSecret;
QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5);
QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, '0').toLower();
url_query.addQueryItem("api_sig", QUrl::toPercentEncoding(signature));
url_query.addQueryItem("format", QUrl::toPercentEncoding("json"));
QUrl url(api_url_);
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
QNetworkReply *reply = network()->post(req, query);
//qLog(Debug) << name_ << "Sending request" << query;
return reply;
}
QByteArray ScrobblingAPI20::GetReplyData(QNetworkReply *reply) {
QByteArray data;
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
else {
if (reply->error() < 200) {
// This is a network error, there is nothing more to do.
QString error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
Error(error_reason);
}
else {
// See if there is Json data containing "error" and "message" - then use that instead.
data = reply->readAll();
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
int error_code = -1;
QString error_reason;
if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains("error") && json_obj.contains("message")) {
error_code = json_obj["error"].toInt();
QString message = json_obj["message"].toString();
error_reason = QString("%1 (%2)").arg(message).arg(error_code);
}
else {
error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
}
else {
error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
if (reply->error() == QNetworkReply::ContentAccessDenied ||
reply->error() == QNetworkReply::ContentOperationNotPermittedError ||
reply->error() == QNetworkReply::AuthenticationRequiredError ||
error_code == ScrobbleErrorCode::InvalidSessionKey ||
error_code == ScrobbleErrorCode::AuthenticationFailed
){
// Session is probably expired
Logout();
Error(error_reason);
}
else if (reply->error() == QNetworkReply::ContentNotFoundError) { // Ignore this error
Error(error_reason);
}
else { // Fail
Error(error_reason);
}
}
return QByteArray();
}
return data;
}
void ScrobblingAPI20::UpdateNowPlaying(const Song &song) {
song_playing_ = song;
timestamp_ = QDateTime::currentDateTime().toTime_t();
if (!IsAuthenticated() || !song.is_metadata_good()) return;
ParamList params = ParamList()
<< Param("method", "track.updateNowPlaying")
<< Param("artist", song.artist())
<< Param("track", song.title())
<< Param("album", song.album());
QNetworkReply *reply = CreateRequest(params);
NewClosure(reply, SIGNAL(finished()), this, SLOT(UpdateNowPlayingRequestFinished(QNetworkReply*)), reply);
}
void ScrobblingAPI20::UpdateNowPlayingRequestFinished(QNetworkReply *reply) {
reply->deleteLater();
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
return;
}
// TODO
}
void ScrobblingAPI20::Scrobble(const Song &song) {
if (song.id() != song_playing_.id() || song.url() != song_playing_.url() || !song.is_metadata_good()) return;
cache()->Add(song, timestamp_);
if (app_->scrobbler()->IsOffline()) return;
if (!IsAuthenticated()) {
emit ErrorMessage(QString("Scrobbler %1 is not authenticated!").arg(name_));
return;
}
if (!submitted_) {
DoInAMinuteOrSo(this, SLOT(Submit()));
submitted_ = true;
}
}
void ScrobblingAPI20::Submit() {
qLog(Debug) << __PRETTY_FUNCTION__ << name_;
submitted_ = false;
if (!IsEnabled() || !IsAuthenticated() || app_->scrobbler()->IsOffline()) return;
ParamList params = ParamList()
<< Param("method", "track.scrobble");
int i(0);
QList<quint64> list;
for (ScrobblerCacheItem *item : cache()->List()) {
if (item->sent_) continue;
item->sent_ = true;
if (!batch_) { SendSingleScrobble(item); continue; }
i++;
list << item->timestamp_;
params << Param(QString("%1[%2]").arg("artist").arg(i), item->artist_);
params << Param(QString("%1[%2]").arg("album").arg(i), item->album_);
params << Param(QString("%1[%2]").arg("track").arg(i), item->song_);
params << Param(QString("%1[%2]").arg("timestamp").arg(i), QString::number(item->timestamp_));
params << Param(QString("%1[%2]").arg("duration").arg(i), QString::number(item->duration_ / kNsecPerSec));
if (!item->albumartist_.isEmpty()) params << Param(QString("%1[%2]").arg("albumArtist").arg(i), item->albumartist_);
if (item->track_ > 0) params << Param(QString("%1[%2]").arg(i).arg("trackNumber"), QString::number(item->track_));
if (i >= kScrobblesPerRequest) break;
}
if (!batch_ || i <= 0) return;
QNetworkReply *reply = CreateRequest(params);
NewClosure(reply, SIGNAL(finished()), this, SLOT(ScrobbleRequestFinished(QNetworkReply*, QList<quint64>)), reply, list);
}
void ScrobblingAPI20::ScrobbleRequestFinished(QNetworkReply *reply, QList<quint64> list) {
reply->deleteLater();
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
cache()->ClearSent(list);
return;
}
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
cache()->ClearSent(list);
return;
}
if (!json_obj.contains("scrobbles")) {
if (json_obj.contains("error")) {
int error = json_obj["error"].toInt();
Error(QString("Error: %1: %2").arg(QString::number(error)).arg(error));
}
Error("Json reply from server is missing session.");
cache()->ClearSent(list);
return;
}
cache()->Flush(list);
QJsonValue json_scrobbles = json_obj["scrobbles"];
if (!json_scrobbles.isObject()) {
Error("Json scrobbles is not an object.", json_obj);
return;
}
json_obj = json_scrobbles.toObject();
if (json_obj.isEmpty()) {
Error("Json scrobbles object is empty.", json_scrobbles);
return;
}
if (!json_obj.contains("@attr") || !json_obj.contains("scrobble")) {
Error("Json scrobbles object is missing values.", json_obj);
return;
}
QJsonValue json_attr = json_obj["@attr"];
if (!json_attr.isObject()) {
Error("Json scrobbles attr is not an object.", json_attr);
return;
}
QJsonObject json_obj_attr = json_attr.toObject();
if (json_obj_attr.isEmpty()) {
Error("Json scrobbles attr is empty.", json_attr);
return;
}
if (!json_obj_attr.contains("accepted") || !json_obj_attr.contains("ignored")) {
Error("Json scrobbles attr is missing values.", json_obj_attr);
return;
}
int accepted = json_obj_attr["accepted"].toInt();
int ignored = json_obj_attr["ignored"].toInt();
qLog(Debug) << name_ << "Scrobbles accepted:" << accepted << "ignored:" << ignored;
QJsonValue json_scrobble = json_obj["scrobble"];
if (!json_scrobble.isArray()) {
Error("Json scrobbles scrobble is not array.", json_scrobble);
return;
}
QJsonArray json_array_scrobble = json_scrobble.toArray();
if (json_array_scrobble.isEmpty()) {
Error("Json scrobbles scrobble array is empty.", json_scrobble);
return;
}
for (const QJsonValue &value : json_array_scrobble) {
if (!value.isObject()) {
Error("Json scrobbles scrobble array value is not an object.", value);
continue;
}
QJsonObject json_track = value.toObject();
if (json_track.isEmpty()) {
continue;
}
if (!json_track.contains("artist") ||
!json_track.contains("album") ||
!json_track.contains("albumArtist") ||
!json_track.contains("track") ||
!json_track.contains("timestamp") ||
!json_track.contains("ignoredMessage")
) {
Error("Json scrobbles scrobble is missing values.", json_track);
return;
}
QJsonValue json_value_artist = json_track["artist"];
QJsonValue json_value_album = json_track["album"];
QJsonValue json_value_song = json_track["track"];
//quint64 timestamp = json_track["timestamp"].toVariant().toULongLong();
if (!json_value_artist.isObject() || !json_value_album.isObject() || !json_value_song.isObject()) {
Error("Json scrobbles scrobble values are not objects.", json_track);
continue;
}
QJsonObject json_obj_artist = json_value_artist.toObject();
QJsonObject json_obj_album = json_value_album.toObject();
QJsonObject json_obj_song = json_value_song.toObject();
if (json_obj_artist.isEmpty() || json_obj_album.isEmpty() || json_obj_song.isEmpty()) {
Error("Json scrobbles scrobble values objects are empty.", json_track);
continue;
}
if (!json_obj_artist.contains("#text") || !json_obj_album.contains("#text") || !json_obj_song.contains("#text")) {
Error("Json scrobbles scrobble values objects are empty.", json_track);
continue;
}
// TODO
}
}
void ScrobblingAPI20::SendSingleScrobble(ScrobblerCacheItem *item) {
ParamList params = ParamList()
<< Param("method", "track.scrobble")
<< Param("artist", item->artist_)
<< Param("album", item->album_)
<< Param("track", item->song_)
<< Param("timestamp", QString::number(item->timestamp_))
<< Param("duration", QString::number(item->duration_ / kNsecPerSec));
if (!item->albumartist_.isEmpty()) params << Param("albumArtist", item->albumartist_);
if (item->track_ > 0) params << Param("trackNumber", QString::number(item->track_));
QNetworkReply *reply = CreateRequest(params);
NewClosure(reply, SIGNAL(finished()), this, SLOT(SingleScrobbleRequestFinished(QNetworkReply*, quint64)), reply, item->timestamp_);
}
void ScrobblingAPI20::SingleScrobbleRequestFinished(QNetworkReply *reply, quint64 timestamp) {
reply->deleteLater();
ScrobblerCacheItem *item = cache()->Get(timestamp);
if (!item) {
Error(QString("Received reply for non-existing cache entry %1.").arg(timestamp));
return;
}
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
return;
}
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
return;
}
if (!json_obj.contains("scrobbles")) {
if (json_obj.contains("error")) {
int error = json_obj["error"].toInt();
Error(QString("Error: %1: %2").arg(QString::number(error)).arg(error));
}
Error("Json reply from server is missing session.");
return;
}
QJsonValue json_scrobbles = json_obj["scrobbles"];
if (!json_scrobbles.isObject()) {
Error("Json scrobbles is not an object.", json_obj);
return;
}
json_obj = json_scrobbles.toObject();
if (json_obj.isEmpty()) {
Error("Json scrobbles object is empty.", json_scrobbles);
return;
}
if (!json_obj.contains("@attr") || !json_obj.contains("scrobble")) {
Error("Json scrobbles object is missing values.", json_obj);
return;
}
QJsonValue json_attr = json_obj["@attr"];
if (!json_attr.isObject()) {
Error("Json scrobbles attr is not an object.", json_attr);
return;
}
QJsonObject json_obj_attr = json_attr.toObject();
if (json_obj_attr.isEmpty()) {
Error("Json scrobbles attr is empty.", json_attr);
return;
}
QJsonValue json_scrobble = json_obj["scrobble"];
if (!json_scrobble.isObject()) {
Error("Json scrobbles scrobble is not an object.", json_scrobble);
return;
}
QJsonObject json_obj_scrobble = json_scrobble.toObject();
if (json_obj_scrobble.isEmpty()) {
Error("Json scrobbles scrobble is empty.", json_scrobble);
return;
}
if (!json_obj_attr.contains("accepted") || !json_obj_attr.contains("ignored")) {
Error("Json scrobbles attr is missing values.", json_obj_attr);
return;
}
if (!json_obj_scrobble.contains("artist") || !json_obj_scrobble.contains("album") || !json_obj_scrobble.contains("albumArtist") || !json_obj_scrobble.contains("track") || !json_obj_scrobble.contains("timestamp")) {
Error("Json scrobbles scrobble is missing values.", json_obj_scrobble);
return;
}
int accepted = json_obj_attr["accepted"].toVariant().toInt();
if (accepted == 1) {
qLog(Debug) << name_ << "Scrobble for" << item->song_ << "accepted";
}
else {
Error(QString("Scrobble for \"%1\" not accepted").arg(item->song_));
qLog(Debug) << name_ << json_obj_attr["accepted"];
}
cache()->Remove(timestamp);
}
void ScrobblingAPI20::Love(const Song &song) {
if (!IsAuthenticated()) app_->scrobbler()->ShowConfig();
ParamList params = ParamList()
<< Param("method", "track.love")
<< Param("artist", song.artist())
<< Param("track", song.title())
<< Param("album", song.album());
QNetworkReply *reply = CreateRequest(params);
NewClosure(reply, SIGNAL(finished()), this, SLOT(RequestFinished(QNetworkReply*)), reply);
}
void ScrobblingAPI20::AuthError(QString error) {
emit AuthenticationComplete(false, error);
}
void ScrobblingAPI20::Error(QString error, QVariant debug) {
qLog(Error) << name_ << error;
if (debug.isValid()) qLog(Debug) << debug;
}
QString ScrobblingAPI20::ErrorString(ScrobbleErrorCode error) const {
switch (error) {
case ScrobbleErrorCode::InvalidService:
return QString("Invalid service - This service does not exist");
case ScrobbleErrorCode::InvalidMethod:
return QString("Invalid Method - No method with that name in this package");
case ScrobbleErrorCode::AuthenticationFailed:
return QString("Authentication Failed - You do not have permissions to access the service");
case ScrobbleErrorCode::InvalidFormat:
return QString("Invalid format - This service doesn't exist in that format");
case ScrobbleErrorCode::InvalidParameters:
return QString("Invalid parameters - Your request is missing a required parameter");
case ScrobbleErrorCode::InvalidResourceSpecified:
return QString("Invalid resource specified");
case ScrobbleErrorCode::OperationFailed:
return QString("Operation failed - Something else went wrong");
case ScrobbleErrorCode::InvalidSessionKey:
return QString("Invalid session key - Please re-authenticate");
case ScrobbleErrorCode::InvalidApiKey:
return QString("Invalid API key - You must be granted a valid key by last.fm");
case ScrobbleErrorCode::ServiceOffline:
return QString("Service Offline - This service is temporarily offline. Try again later.");
case ScrobbleErrorCode::InvalidMethodSignature:
return QString("Invalid method signature supplied");
case ScrobbleErrorCode::TempError:
return QString("There was a temporary error processing your request. Please try again");
case ScrobbleErrorCode::SuspendedAPIKey:
return QString("Suspended API key - Access for your account has been suspended, please contact Last.fm");
case ScrobbleErrorCode::RateLimitExceeded:
return QString("Rate limit exceeded - Your IP has made too many requests in a short period");
default:
return QString("Unknown error");
}
}

View File

@ -0,0 +1,158 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SCROBBLINGAPI20_H
#define SCROBBLINGAPI20_H
#include "config.h"
#include <memory>
#include <QtGlobal>
#include <QObject>
#include <QNetworkReply>
#include <QPair>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QJsonObject>
#include "core/song.h"
#include "scrobblerservice.h"
#include "scrobblercache.h"
class Application;
class NetworkAccessManager;
class LocalRedirectServer;
class ScrobblerService;
class ScrobblerCache;
class ScrobblerCacheItem;
class ScrobblingAPI20 : public ScrobblerService {
Q_OBJECT
public:
explicit ScrobblingAPI20(const QString &name, const QString &settings_group, const QString &auth_url, const QString &api_url, const bool batch, Application *app, QObject *parent = nullptr);
~ScrobblingAPI20();
static const char *kRedirectUrl;
void ReloadSettings();
void LoadSession();
virtual NetworkAccessManager *network() = 0;
virtual ScrobblerCache *cache() = 0;
bool IsEnabled() const { return enabled_; }
bool IsAuthenticated() const { return !username_.isEmpty() && !session_key_.isEmpty(); }
bool IsSubscriber() const { return subscriber_; }
bool IsSubmitted() const { return submitted_; }
void Submitted() { submitted_ = true; }
QString username() const { return username_; }
void Authenticate();
void Logout();
void UpdateNowPlaying(const Song &song);
void Scrobble(const Song &song);
void Submit();
void Love(const Song &song);
signals:
void AuthenticationComplete(bool success, QString error = QString());
public slots:
void WriteCache() { cache()->WriteCache(); }
private slots:
void RedirectArrived(LocalRedirectServer *server);
void AuthenticateReplyFinished(QNetworkReply *reply);
void UpdateNowPlayingRequestFinished(QNetworkReply *reply);
void ScrobbleRequestFinished(QNetworkReply *reply, QList<quint64>);
void SingleScrobbleRequestFinished(QNetworkReply *reply, quint64 timestamp);
private:
enum ScrobbleErrorCode {
Unknown = 0,
NoError = 1,
InvalidService = 2,
InvalidMethod = 3,
AuthenticationFailed = 4,
InvalidFormat = 5,
InvalidParameters = 6,
InvalidResourceSpecified = 7,
OperationFailed = 8,
InvalidSessionKey = 9,
InvalidApiKey = 10,
ServiceOffline = 11,
Reserved12 = 12,
InvalidMethodSignature = 13,
Reserved14 = 14,
Reserved15 = 15,
TempError = 16,
Reserved17 = 17,
Reserved18 = 18,
Reserved19 = 19,
Reserved20 = 20,
Reserved21 = 21,
Reserved22 = 22,
Reserved23 = 23,
Reserved24 = 24,
Reserved25 = 25,
SuspendedAPIKey = 26,
Reserved27 = 27,
Reserved28 = 28,
RateLimitExceeded = 29,
};
static const char *kApiKey;
static const char *kSecret;
static const int kScrobblesPerRequest;
QNetworkReply *CreateRequest(const ParamList &request_params);
QByteArray GetReplyData(QNetworkReply *reply);
void RequestSession(QString token);
void AuthError(QString error);
void SendSingleScrobble(ScrobblerCacheItem *item);
void Error(QString error, QVariant debug = QVariant());
QString ErrorString(ScrobbleErrorCode error) const;
QString name_;
QString settings_group_;
QString auth_url_;
QString api_url_;
bool batch_;
Application *app_;
bool enabled_;
bool subscriber_;
QString username_;
QString session_key_;
bool submitted_;
Song song_playing_;
quint64 timestamp_;
};
#endif // SCROBBLINGAPI20_H

View File

@ -0,0 +1,230 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "scrobblersettingspage.h"
#include "ui_scrobblersettingspage.h"
#include <QDesktopServices>
#include <QMessageBox>
#include <QSettings>
#include "core/application.h"
#include "core/iconloader.h"
#include "scrobbler/audioscrobbler.h"
#include "scrobbler/scrobblerservice.h"
#include "scrobbler/lastfmscrobbler.h"
#include "scrobbler/librefmscrobbler.h"
#include "scrobbler/listenbrainzscrobbler.h"
const char *ScrobblerSettingsPage::kSettingsGroup = "Scrobbler";
ScrobblerSettingsPage::ScrobblerSettingsPage(SettingsDialog *parent)
: SettingsPage(parent),
scrobbler_(dialog()->app()->scrobbler()),
lastfmscrobbler_(dialog()->app()->scrobbler()->Service<LastFMScrobbler>()),
librefmscrobbler_(dialog()->app()->scrobbler()->Service<LibreFMScrobbler>()),
listenbrainzscrobbler_(dialog()->app()->scrobbler()->Service<ListenBrainzScrobbler>()),
ui_(new Ui_ScrobblerSettingsPage),
lastfm_waiting_for_auth_(false),
librefm_waiting_for_auth_(false),
listenbrainz_waiting_for_auth_(false)
{
ui_->setupUi(this);
setWindowIcon(IconLoader::Load("scrobble"));
// Last.fm
connect(lastfmscrobbler_, SIGNAL(AuthenticationComplete(bool, QString)), SLOT(LastFM_AuthenticationComplete(bool, QString)));
connect(ui_->button_lastfm_login, SIGNAL(clicked()), SLOT(LastFM_Login()));
connect(ui_->widget_lastfm_login_state, SIGNAL(LoginClicked()), SLOT(LastFM_Login()));
connect(ui_->widget_lastfm_login_state, SIGNAL(LogoutClicked()), SLOT(LastFM_Logout()));
ui_->widget_lastfm_login_state->AddCredentialGroup(ui_->widget_lastfm_login);
// Libre.fm
connect(librefmscrobbler_, SIGNAL(AuthenticationComplete(bool, QString)), SLOT(LibreFM_AuthenticationComplete(bool, QString)));
connect(ui_->button_librefm_login, SIGNAL(clicked()), SLOT(LibreFM_Login()));
connect(ui_->widget_librefm_login_state, SIGNAL(LoginClicked()), SLOT(LibreFM_Login()));
connect(ui_->widget_librefm_login_state, SIGNAL(LogoutClicked()), SLOT(LibreFM_Logout()));
ui_->widget_librefm_login_state->AddCredentialGroup(ui_->widget_librefm_login);
// ListenBrainz
connect(listenbrainzscrobbler_, SIGNAL(AuthenticationComplete(bool, QString)), SLOT(ListenBrainz_AuthenticationComplete(bool, QString)));
connect(ui_->button_listenbrainz_login, SIGNAL(clicked()), SLOT(ListenBrainz_Login()));
connect(ui_->widget_listenbrainz_login_state, SIGNAL(LoginClicked()), SLOT(ListenBrainz_Login()));
connect(ui_->widget_listenbrainz_login_state, SIGNAL(LogoutClicked()), SLOT(ListenBrainz_Logout()));
ui_->widget_listenbrainz_login_state->AddCredentialGroup(ui_->widget_listenbrainz_login);
resize(sizeHint());
}
ScrobblerSettingsPage::~ScrobblerSettingsPage() { delete ui_; }
void ScrobblerSettingsPage::Load() {
ui_->checkbox_enable->setChecked(scrobbler_->IsEnabled());
ui_->checkbox_scrobble_button->setChecked(scrobbler_->ScrobbleButton());
ui_->checkbox_offline->setChecked(scrobbler_->IsOffline());
ui_->checkbox_lastfm_enable->setChecked(lastfmscrobbler_->IsEnabled());
LastFM_RefreshControls(lastfmscrobbler_->IsAuthenticated());
ui_->checkbox_librefm_enable->setChecked(librefmscrobbler_->IsEnabled());
LibreFM_RefreshControls(librefmscrobbler_->IsAuthenticated());
ui_->checkbox_listenbrainz_enable->setChecked(listenbrainzscrobbler_->IsEnabled());
ui_->lineedit_listenbrainz_user_token->setText(listenbrainzscrobbler_->user_token());
ListenBrainz_RefreshControls(listenbrainzscrobbler_->IsAuthenticated());
}
void ScrobblerSettingsPage::Save() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("enabled", ui_->checkbox_enable->isChecked());
s.setValue("scrobble_button", ui_->checkbox_scrobble_button->isChecked());
s.setValue("offline", ui_->checkbox_offline->isChecked());
s.endGroup();
s.beginGroup(LastFMScrobbler::kSettingsGroup);
s.setValue("enabled", ui_->checkbox_lastfm_enable->isChecked());
s.endGroup();
s.beginGroup(LibreFMScrobbler::kSettingsGroup);
s.setValue("enabled", ui_->checkbox_librefm_enable->isChecked());
s.endGroup();
s.beginGroup(ListenBrainzScrobbler::kSettingsGroup);
s.setValue("enabled", ui_->checkbox_listenbrainz_enable->isChecked());
s.setValue("user_token", ui_->lineedit_listenbrainz_user_token->text());
s.endGroup();
scrobbler_->ReloadSettings();
}
void ScrobblerSettingsPage::LastFM_Login() {
lastfm_waiting_for_auth_ = true;
ui_->widget_lastfm_login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
lastfmscrobbler_->Authenticate();
}
void ScrobblerSettingsPage::LastFM_Logout() {
lastfmscrobbler_->Logout();
LastFM_RefreshControls(false);
}
void ScrobblerSettingsPage::LastFM_AuthenticationComplete(const bool success, QString error) {
if (!lastfm_waiting_for_auth_) return;
lastfm_waiting_for_auth_ = false;
if (success) {
Save();
}
else {
QMessageBox::warning(this, "Authentication failed", error);
}
LastFM_RefreshControls(success);
}
void ScrobblerSettingsPage::LastFM_RefreshControls(bool authenticated) {
ui_->widget_lastfm_login_state->SetLoggedIn(authenticated ? LoginStateWidget::LoggedIn : LoginStateWidget::LoggedOut, lastfmscrobbler_->username());
}
void ScrobblerSettingsPage::LibreFM_Login() {
librefm_waiting_for_auth_ = true;
ui_->widget_librefm_login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
librefmscrobbler_->Authenticate();
}
void ScrobblerSettingsPage::LibreFM_Logout() {
librefmscrobbler_->Logout();
LibreFM_RefreshControls(false);
}
void ScrobblerSettingsPage::LibreFM_AuthenticationComplete(const bool success, QString error) {
if (!librefm_waiting_for_auth_) return;
librefm_waiting_for_auth_ = false;
if (success) {
Save();
}
else {
QMessageBox::warning(this, "Authentication failed", error);
}
LibreFM_RefreshControls(success);
}
void ScrobblerSettingsPage::LibreFM_RefreshControls(bool authenticated) {
ui_->widget_librefm_login_state->SetLoggedIn(authenticated ? LoginStateWidget::LoggedIn : LoginStateWidget::LoggedOut, librefmscrobbler_->username());
}
void ScrobblerSettingsPage::ListenBrainz_Login() {
listenbrainz_waiting_for_auth_ = true;
ui_->widget_listenbrainz_login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
listenbrainzscrobbler_->Authenticate();
}
void ScrobblerSettingsPage::ListenBrainz_Logout() {
listenbrainzscrobbler_->Logout();
ListenBrainz_RefreshControls(false);
}
void ScrobblerSettingsPage::ListenBrainz_AuthenticationComplete(const bool success, QString error) {
if (!listenbrainz_waiting_for_auth_) return;
listenbrainz_waiting_for_auth_ = false;
if (success) {
Save();
}
else {
QMessageBox::warning(this, "Authentication failed", error);
}
ListenBrainz_RefreshControls(success);
}
void ScrobblerSettingsPage::ListenBrainz_RefreshControls(bool authenticated) {
ui_->widget_listenbrainz_login_state->SetLoggedIn(authenticated ? LoginStateWidget::LoggedIn : LoginStateWidget::LoggedOut);
}

View File

@ -0,0 +1,71 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SCROBBLERSETTINGSPAGE_H
#define SCROBBLERSETTINGSPAGE_H
#include "settingspage.h"
class Ui_ScrobblerSettingsPage;
class AudioScrobbler;
class LastFMScrobbler;
class LibreFMScrobbler;
class ListenBrainzScrobbler;
class ScrobblerSettingsPage : public SettingsPage {
Q_OBJECT
public:
explicit ScrobblerSettingsPage(SettingsDialog *dialog);
~ScrobblerSettingsPage();
static const char *kSettingsGroup;
void Load();
void Save();
private slots:
void LastFM_Login();
void LastFM_Logout();
void LastFM_AuthenticationComplete(const bool success, QString error = QString());
void LibreFM_Login();
void LibreFM_Logout();
void LibreFM_AuthenticationComplete(const bool success, QString error = QString());
void ListenBrainz_Login();
void ListenBrainz_Logout();
void ListenBrainz_AuthenticationComplete(const bool success, QString error = QString());
private:
AudioScrobbler *scrobbler_;
LastFMScrobbler *lastfmscrobbler_;
LibreFMScrobbler *librefmscrobbler_;
ListenBrainzScrobbler *listenbrainzscrobbler_;
Ui_ScrobblerSettingsPage* ui_;
bool lastfm_waiting_for_auth_;
bool librefm_waiting_for_auth_;
bool listenbrainz_waiting_for_auth_;
void LastFM_RefreshControls(bool authenticated);
void LibreFM_RefreshControls(bool authenticated);
void ListenBrainz_RefreshControls(bool authenticated);
};
#endif // SCROBBLERSETTINGSPAGE_H

View File

@ -0,0 +1,243 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ScrobblerSettingsPage</class>
<widget class="QWidget" name="ScrobblerSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>769</width>
<height>611</height>
</rect>
</property>
<property name="windowTitle">
<string>Scrobbler</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="checkbox_enable">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Enable</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkbox_offline">
<property name="text">
<string>Enable offline mode</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkbox_scrobble_button">
<property name="text">
<string>Show scrobble button</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupbox_lastfm">
<property name="title">
<string>Last.fm</string>
</property>
<layout class="QVBoxLayout" name="layout_lastfm">
<item>
<widget class="QCheckBox" name="checkbox_lastfm_enable">
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
<item>
<widget class="LoginStateWidget" name="widget_lastfm_login_state" native="true"/>
</item>
<item>
<widget class="QWidget" name="widget_lastfm_login" native="true">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="layout_lastfm_button_login">
<item>
<widget class="QPushButton" name="button_lastfm_login">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
<item>
<spacer name="spacer_lastfm_login">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupbox_librefm">
<property name="title">
<string>Libre.fm</string>
</property>
<layout class="QVBoxLayout" name="layout_lastfm_3">
<item>
<widget class="QCheckBox" name="checkbox_librefm_enable">
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
<item>
<widget class="LoginStateWidget" name="widget_librefm_login_state" native="true"/>
</item>
<item>
<widget class="QWidget" name="widget_librefm_login" native="true">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="layout_librefm_button_login">
<item>
<widget class="QPushButton" name="button_librefm_login">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
<item>
<spacer name="spacer_librefm_login">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupbox_listenbrainz">
<property name="title">
<string>Listenbrainz</string>
</property>
<layout class="QVBoxLayout" name="layout_lastfm_4">
<item>
<widget class="QCheckBox" name="checkbox_listenbrainz_enable">
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="layout_listenbrainz_user_token">
<item>
<widget class="QLabel" name="label_listenbrainz_user_token">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>User token:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineedit_listenbrainz_user_token"/>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter your user token found on: &lt;a href=&quot;https://listenbrainz.org/profile/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;https://listenbrainz.org/profile/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="LoginStateWidget" name="widget_listenbrainz_login_state" native="true"/>
</item>
<item>
<widget class="QWidget" name="widget_listenbrainz_login" native="true">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QHBoxLayout" name="layout_listenbrainz_button_login">
<item>
<widget class="QPushButton" name="button_listenbrainz_login">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
<item>
<spacer name="spacer_listenbrainz_login">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</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>
<resources/>
<connections/>
</ui>

View File

@ -56,12 +56,13 @@
#include "backendsettingspage.h"
#include "behavioursettingspage.h"
#include "collectionsettingspage.h"
#include "networkproxysettingspage.h"
#include "notificationssettingspage.h"
#include "playbacksettingspage.h"
#include "playlistsettingspage.h"
#include "shortcutssettingspage.h"
#include "transcodersettingspage.h"
#include "networkproxysettingspage.h"
#include "scrobblersettingspage.h"
#ifdef HAVE_STREAM_TIDAL
# include "tidalsettingspage.h"
#endif
@ -123,10 +124,11 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent)
AddPage(Page_Backend, new BackendSettingsPage(this), general);
AddPage(Page_Playback, new PlaybackSettingsPage(this), general);
AddPage(Page_Playlist, new PlaylistSettingsPage(this), general);
AddPage(Page_Proxy, new NetworkProxySettingsPage(this), general);
#ifdef HAVE_GSTREAMER
AddPage(Page_Transcoding, new TranscoderSettingsPage(this), general);
#endif
AddPage(Page_Proxy, new NetworkProxySettingsPage(this), general);
AddPage(Page_Scrobbler, new ScrobblerSettingsPage(this), general);
QTreeWidgetItem *iface = AddCategory(tr("User interface"));
AddPage(Page_Appearance, new AppearanceSettingsPage(this), iface);
@ -134,13 +136,13 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent)
AddPage(Page_GlobalShortcuts, new GlobalShortcutsSettingsPage(this), iface);
#if defined(HAVE_STREAM_TIDAL) || defined(HAVE_STREAM_DEEZER)
QTreeWidgetItem *internet = AddCategory(tr("Streaming"));
QTreeWidgetItem *streaming = AddCategory(tr("Streaming"));
#endif
#ifdef HAVE_STREAM_TIDAL
AddPage(Page_Tidal, new TidalSettingsPage(this), internet);
AddPage(Page_Tidal, new TidalSettingsPage(this), streaming);
#endif
#ifdef HAVE_STREAM_DEEZER
AddPage(Page_Deezer, new DeezerSettingsPage(this), internet);
AddPage(Page_Deezer, new DeezerSettingsPage(this), streaming);
#endif
// List box

View File

@ -79,8 +79,9 @@ public:
Page_GlobalShortcuts,
Page_Appearance,
Page_Notifications,
Page_Proxy,
Page_Transcoding,
Page_Proxy,
Page_Scrobbler,
Page_Tidal,
Page_Deezer,
};