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();
</