Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eb11c32b0 | ||
|
|
25457bc09a | ||
|
|
d5cfb5f733 | ||
|
|
79ba6e628e | ||
|
|
ef73add05a | ||
|
|
ec3d11fb27 | ||
|
|
3fbc7031b5 | ||
|
|
40beb5e428 | ||
|
|
0f608c8ef0 | ||
|
|
8509cb4743 | ||
|
|
f4429e8c4a | ||
|
|
e9e0829cdc | ||
|
|
93f0230423 | ||
|
|
f26a0df4a4 | ||
|
|
c7d4624282 | ||
|
|
b03eee2a22 | ||
|
|
7d4d72e706 | ||
|
|
e3c367984b | ||
|
|
0ebfa10d32 | ||
|
|
16d9a077f0 | ||
|
|
a9d8bbad42 | ||
|
|
d78bb94af3 | ||
|
|
b139c0a824 | ||
|
|
5b0b924d34 | ||
|
|
f75acf820c | ||
|
|
43a47f33ac | ||
|
|
fcea3a0877 | ||
|
|
a950ec3bd5 | ||
|
|
e35501ff0a | ||
|
|
4bfad9dad8 | ||
|
|
c5c7a07c12 | ||
|
|
7e22e0e552 | ||
|
|
84ec4bdc79 | ||
|
|
2bcad9b637 | ||
|
|
c8d5f03070 | ||
|
|
168e101a5a | ||
|
|
b4bc7333d9 | ||
|
|
e8b58c940e | ||
|
|
ec7202e3f6 | ||
|
|
9a740f7962 | ||
|
|
9210fdee0d | ||
|
|
d7661f0964 | ||
|
|
139e148912 | ||
|
|
1b8dedb4ed | ||
|
|
5d6b0fa329 | ||
|
|
f35bbd89c9 | ||
|
|
538a9e42f4 | ||
|
|
623147dea7 | ||
|
|
dfecd0cd12 | ||
|
|
fe3af3a676 | ||
|
|
25f60331ed | ||
|
|
d4860a3426 | ||
|
|
e7e77ed86b | ||
|
|
dc80459c59 | ||
|
|
2f2de59234 | ||
|
|
7bccc21878 | ||
|
|
40f9dafa44 | ||
|
|
355d436d29 | ||
|
|
079b684388 | ||
|
|
fd11f46d30 | ||
|
|
cb7099199a | ||
|
|
8566d91e89 | ||
|
|
f44ce49ea7 | ||
|
|
6ef69f6b32 | ||
|
|
f5983d5f10 | ||
|
|
54cce5e089 | ||
|
|
4e4e596a1e | ||
|
|
727a1f5ad1 | ||
|
|
85fa86625b | ||
|
|
2c91877f83 | ||
|
|
7d1fac44e9 | ||
|
|
2e34abfc0d | ||
|
|
81ba63e247 | ||
|
|
8b11a65522 | ||
|
|
7190ad1d15 | ||
|
|
1c9bae5df5 | ||
|
|
cc7fd73916 |
26
Changelog
26
Changelog
@@ -2,6 +2,32 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
0.6.11:
|
||||
|
||||
Bugfixes:
|
||||
* Fixed MPRIS missing art url when playing albums with embedded cover.
|
||||
* Fixed updating local non collection songs when manually unsetting cover.
|
||||
* Fixed infinite loop and preceding crash when CSS background-color was set in qt5ct.
|
||||
* Fixed UI freeze when updating the database from a large Subsonic or Tidal collection.
|
||||
* Fixed crash when CD loading fails in devices.
|
||||
* Fixed CD devices showing up with having 0 songs after loading.
|
||||
* Fixed the album cover loading indicator being stuck if no cover providers were available.
|
||||
* Fixed the playing widget not updating artist, album or title after metadata has changed for a song when no album cover was loaded.
|
||||
|
||||
Enhancements:
|
||||
* Sort songs in collection by song title instead of track if previous grouping is not the album.
|
||||
* Added option to switch on/off automatically searching for album covers to context settings.
|
||||
* Reset last played song when playlist is finished.
|
||||
* Checking content type of received HTTP request for image when receiving album covers.
|
||||
* Added option to scrobbler setting for turning off login error popup.
|
||||
* Made MusicBrainz and Discogs cover providers respect rate limiting.
|
||||
|
||||
New features:
|
||||
* Added option to show/hide sidebar.
|
||||
* Added settings for selecting album cover and lyrics providers.
|
||||
* Added album covers from Musixmatch and Spotify.
|
||||
* Added lyrics from Genius, Musixmatch and ChartLyrics.
|
||||
|
||||
0.6.10:
|
||||
|
||||
Bugfixes:
|
||||
|
||||
10
README.md
10
README.md
@@ -17,7 +17,7 @@ Resources:
|
||||
* PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
||||
* Translations: https://translate.zanata.org/iteration/view/strawberry/master
|
||||
|
||||
The program is free software, released under GPL. If you like this program and can make use of it, consider sponsoring or donating to help funding the project.
|
||||
The program is free software, released under GPL. If you like this program and can make use of it, consider sponsoring or donating to help fund the project.
|
||||
To sponsor, visit [my GitHub sponsors profile](https://github.com/sponsors/jonaski).
|
||||
Funding developers through GitHub Sponsors is one more way to contribute to open source projects you appreciate, it helps developers get the resources they need, and recognize contributors working behind the scenes to make open source better for everyone.
|
||||
You can also make a one-time payment through [paypal.me/jonaskvinge](https://paypal.me/jonaskvinge)
|
||||
@@ -32,19 +32,19 @@ You can also make a one-time payment through [paypal.me/jonaskvinge](https://pay
|
||||
* Advanced audio output and device configuration for bit-perfect playback on Linux
|
||||
* Edit tags on music files
|
||||
* Fetch tags from MusicBrainz
|
||||
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/) and [Deezer](https://www.deezer.com/)
|
||||
* Song lyrics from [AudD](https://audd.io/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/)
|
||||
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
|
||||
* Song lyrics from [AudD](https://audd.io/), [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/)
|
||||
* Support for multiple backends
|
||||
* Audio analyzer
|
||||
* Audio equalizer
|
||||
* Transfer music to iPod, iPhone, MTP or mass-storage USB player
|
||||
* Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
||||
* Subsonic streaming support
|
||||
* Subsonic and Tidal streaming support
|
||||
|
||||
|
||||
It has so far been tested to work on Linux, OpenBSD and Windows.
|
||||
|
||||
**We currently do not provide releases for macOS because there aren't any macOS developers actively working on this project. It is still possible to compile by following the instructions in the Wiki**
|
||||
**We currently do not provide releases for macOS because there aren't any macOS developers actively working on this project. Development builds are available**
|
||||
|
||||
### :heavy_exclamation_mark: Requirements
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 0)
|
||||
set(STRAWBERRY_VERSION_MINOR 6)
|
||||
set(STRAWBERRY_VERSION_PATCH 10)
|
||||
set(STRAWBERRY_VERSION_PATCH 11)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
|
||||
4
debian/control
vendored
4
debian/control
vendored
@@ -55,8 +55,8 @@ Description: Audio player and music collection organizer
|
||||
- Advanced audio output and device configuration for bit-perfect playback on Linux
|
||||
- Edit tags on music files
|
||||
- Fetch tags from MusicBrainz
|
||||
- Album cover art from Lastfm, Musicbrainz, Discogs and Deezer
|
||||
- Song lyrics from AudD, lyrics.ovh and lololyrics.com
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
- Support for multiple backends
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
|
||||
39
debian/copyright
vendored
39
debian/copyright
vendored
@@ -24,6 +24,8 @@ Files: src/core/main.h
|
||||
src/version.h.in
|
||||
src/context/contextview.cpp
|
||||
src/context/contextview.h
|
||||
src/context/contextalbum.cpp
|
||||
src/context/contextalbum.h
|
||||
src/engine/enginetype.cpp
|
||||
src/engine/enginetype.h
|
||||
src/engine/alsadevicefinder.cpp
|
||||
@@ -38,16 +40,36 @@ Files: src/core/main.h
|
||||
src/internet/internetservice.h
|
||||
src/internet/internettabsview.cpp
|
||||
src/internet/internettabsview.h
|
||||
src/internet/internetsongsview.cpp
|
||||
src/internet/internetsongsview.h
|
||||
src/settings/backendsettingspage.cpp
|
||||
src/settings/backendsettingspage.h
|
||||
src/settings/coverssettingspage.cpp
|
||||
src/settings/coverssettingspage.h
|
||||
src/settings/lyricssettingspage.cpp
|
||||
src/settings/lyricssettingspage.h
|
||||
src/settings/scrobblersettingspage.cpp
|
||||
src/settings/scrobblersettingspage.h
|
||||
src/settings/subsonicsettingspage.cpp
|
||||
src/settings/subsonicsettingspage.h
|
||||
src/settings/tidalsettingspage.cpp
|
||||
src/settings/tidalsettingspage.h
|
||||
src/covermanager/jsoncoverprovider.cpp
|
||||
src/covermanager/jsoncoverprovider.h
|
||||
src/covermanager/lastfmcoverprovider.cpp
|
||||
src/covermanager/lastfmcoverprovider.h
|
||||
src/covermanager/musicbrainzcoverprovider.cpp
|
||||
src/covermanager/musicbrainzcoverprovider.h
|
||||
src/covermanager/deezercoverprovider.cpp
|
||||
src/covermanager/deezercoverprovider.h
|
||||
src/covermanager/tidalcoverprovider.cpp
|
||||
src/covermanager/tidalcoverprovider.h
|
||||
src/covermanager/qobuzcoverprovider.cpp
|
||||
src/covermanager/qobuzcoverprovider.h
|
||||
src/covermanager/spotifycoverprovider.cpp
|
||||
src/covermanager/spotifycoverprovider.h
|
||||
src/covermanager/musixmatchcoverprovider.cpp
|
||||
src/covermanager/musixmatchcoverprovider.h
|
||||
src/globalshortcuts/globalshortcutbackend-system.cpp
|
||||
src/globalshortcuts/globalshortcutbackend-system.h
|
||||
src/globalshortcuts/globalshortcut.cpp
|
||||
@@ -59,9 +81,10 @@ Files: src/core/main.h
|
||||
src/lyrics/*
|
||||
src/scrobbler/*
|
||||
src/subsonic/*
|
||||
src/tidal/*
|
||||
src/transcoder/transcoderoptionswavpack.cpp
|
||||
src/transcoder/transcoderoptionswavpack.h
|
||||
Copyright: 2012-2014, 2017-2019, Jonas Kvinge <jonas@jkvinge.net>
|
||||
Copyright: 2012-2014, 2017-2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/core/main.cpp
|
||||
@@ -158,8 +181,20 @@ Files: src/core/main.cpp
|
||||
src/transcoder/transcoder.h
|
||||
src/musicbrainz/musicbrainzclient.cpp
|
||||
src/musicbrainz/musicbrainzclient.h
|
||||
src/covermanager/albumcoverloader.cpp
|
||||
src/covermanager/albumcoverloader.h
|
||||
src/covermanager/currentalbumcoverloader.cpp
|
||||
src/covermanager/currentalbumcoverloader.h
|
||||
src/covermanager/albumcoverchoicecontroller.cpp
|
||||
src/covermanager/albumcoverchoicecontroller.h
|
||||
src/covermanager/albumcoverfetchersearch.cpp
|
||||
src/covermanager/albumcoverfetchersearch.h
|
||||
src/covermanager/coverproviders.cpp
|
||||
src/covermanager/coverproviders.h
|
||||
src/covermanager/coverprovider.cpp
|
||||
src/covermanager/coverprovider.h
|
||||
Copyright: 2010, 2012-2014 David Sansome <me@davidsansome.com>
|
||||
2012-2014, 2017-2019 Jonas Kvinge <jonas@jkvinge.net>
|
||||
2012-2014, 2017-2020 Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/engine/enginebase.cpp
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
<li>Advanced audio output and device configuration for bit-perfect playback on Linux</li>
|
||||
<li>Edit tags on music files</li>
|
||||
<li>Fetch tags from MusicBrainz</li>
|
||||
<li>Album cover art from Last.fm, Musicbrainz and Discogs</li>
|
||||
<li>Song lyrics from AudD, lyrics.ovh and lololyrics.com</li>
|
||||
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
||||
<li>Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com</li>
|
||||
<li>Support for multiple backends</li>
|
||||
<li>Audio analyzer and equalizer</li>
|
||||
<li>Transfer music to iPod, iPhone, MTP or mass-storage USB player</li>
|
||||
|
||||
4
dist/unix/strawberry.1
vendored
4
dist/unix/strawberry.1
vendored
@@ -25,9 +25,9 @@ Features:
|
||||
.br
|
||||
- Fetch tags from MusicBrainz
|
||||
.br
|
||||
- Album cover art from Lastfm, Musicbrainz, Discogs and Deezer
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
.br
|
||||
- Song lyrics from AudD, lyrics.ovh and lololyrics.com
|
||||
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
.br
|
||||
- Support for multiple backends
|
||||
.br
|
||||
|
||||
4
dist/unix/strawberry.spec.in
vendored
4
dist/unix/strawberry.spec.in
vendored
@@ -104,8 +104,8 @@ Features:
|
||||
- Advanced audio output and device configuration for bit-perfect playback on Linux
|
||||
- Edit tags on music files
|
||||
- Fetch tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs and Deezer
|
||||
- Song lyrics from AudD, lyrics.ovh and lololyrics.com
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
- Support for multiple backends
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
|
||||
9
dist/windows/strawberry.nsi.in
vendored
9
dist/windows/strawberry.nsi.in
vendored
@@ -183,14 +183,14 @@ Section "Strawberry" Strawberry
|
||||
File "libgsttag-1.0-0.dll"
|
||||
File "libgstvideo-1.0-0.dll"
|
||||
File "libharfbuzz-0.dll"
|
||||
File "libhogweed-5.dll"
|
||||
File "libhogweed-6.dll"
|
||||
File "libiconv-2.dll"
|
||||
File "libidn2-0.dll"
|
||||
File "libintl-8.dll"
|
||||
File "libjpeg-9.dll"
|
||||
File "liblzma-5.dll"
|
||||
File "libmp3lame-0.dll"
|
||||
File "libnettle-7.dll"
|
||||
File "libnettle-8.dll"
|
||||
File "libogg-0.dll"
|
||||
File "libopus-0.dll"
|
||||
File "libpcre-1.dll"
|
||||
@@ -447,14 +447,14 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libgsttag-1.0-0.dll"
|
||||
Delete "$INSTDIR\libgstvideo-1.0-0.dll"
|
||||
Delete "$INSTDIR\libharfbuzz-0.dll"
|
||||
Delete "$INSTDIR\libhogweed-5.dll"
|
||||
Delete "$INSTDIR\libhogweed-6.dll"
|
||||
Delete "$INSTDIR\libiconv-2.dll"
|
||||
Delete "$INSTDIR\libidn2-0.dll"
|
||||
Delete "$INSTDIR\libintl-8.dll"
|
||||
Delete "$INSTDIR\libjpeg-9.dll"
|
||||
Delete "$INSTDIR\liblzma-5.dll"
|
||||
Delete "$INSTDIR\libmp3lame-0.dll"
|
||||
Delete "$INSTDIR\libnettle-7.dll"
|
||||
Delete "$INSTDIR\libnettle-8.dll"
|
||||
Delete "$INSTDIR\libogg-0.dll"
|
||||
Delete "$INSTDIR\libopus-0.dll"
|
||||
Delete "$INSTDIR\libpcre-1.dll"
|
||||
@@ -592,6 +592,7 @@ Section "Uninstall"
|
||||
|
||||
; Remove the installation folders.
|
||||
RMDir "$INSTDIR\platforms"
|
||||
RMDir "$INSTDIR\styles"
|
||||
RMDir "$INSTDIR\sqldrivers"
|
||||
RMDir "$INSTDIR\imageformats"
|
||||
RMDir "$INSTDIR\gio-modules"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: strawberry
|
||||
version: '0.6.10+git'
|
||||
version: '0.6.11+git'
|
||||
summary: music player and collection organizer
|
||||
description: |
|
||||
Strawberry is a music player and collection organizer.
|
||||
|
||||
@@ -182,11 +182,14 @@ set(SOURCES
|
||||
covermanager/coverexportrunnable.cpp
|
||||
covermanager/currentalbumcoverloader.cpp
|
||||
covermanager/coverfromurldialog.cpp
|
||||
covermanager/jsoncoverprovider.cpp
|
||||
covermanager/lastfmcoverprovider.cpp
|
||||
covermanager/musicbrainzcoverprovider.cpp
|
||||
covermanager/discogscoverprovider.cpp
|
||||
covermanager/deezercoverprovider.cpp
|
||||
covermanager/qobuzcoverprovider.cpp
|
||||
covermanager/musixmatchcoverprovider.cpp
|
||||
covermanager/spotifycoverprovider.cpp
|
||||
|
||||
lyrics/lyricsproviders.cpp
|
||||
lyrics/lyricsprovider.cpp
|
||||
@@ -196,18 +199,23 @@ set(SOURCES
|
||||
lyrics/auddlyricsprovider.cpp
|
||||
lyrics/ovhlyricsprovider.cpp
|
||||
lyrics/lololyricsprovider.cpp
|
||||
lyrics/geniuslyricsprovider.cpp
|
||||
lyrics/musixmatchlyricsprovider.cpp
|
||||
lyrics/chartlyricsprovider.cpp
|
||||
|
||||
settings/settingsdialog.cpp
|
||||
settings/settingspage.cpp
|
||||
settings/behavioursettingspage.cpp
|
||||
settings/collectionsettingspage.cpp
|
||||
settings/backendsettingspage.cpp
|
||||
settings/contextsettingspage.cpp
|
||||
settings/playlistsettingspage.cpp
|
||||
settings/scrobblersettingspage.cpp
|
||||
settings/coverssettingspage.cpp
|
||||
settings/lyricssettingspage.cpp
|
||||
settings/networkproxysettingspage.cpp
|
||||
settings/appearancesettingspage.cpp
|
||||
settings/contextsettingspage.cpp
|
||||
settings/notificationssettingspage.cpp
|
||||
settings/scrobblersettingspage.cpp
|
||||
|
||||
dialogs/about.cpp
|
||||
dialogs/console.cpp
|
||||
@@ -294,6 +302,7 @@ set(HEADERS
|
||||
core/standarditemiconloader.h
|
||||
core/systemtrayicon.h
|
||||
core/mimedata.h
|
||||
core/stylesheetloader.h
|
||||
|
||||
engine/enginebase.h
|
||||
engine/devicefinders.h
|
||||
@@ -369,11 +378,14 @@ set(HEADERS
|
||||
covermanager/coverexportrunnable.h
|
||||
covermanager/currentalbumcoverloader.h
|
||||
covermanager/coverfromurldialog.h
|
||||
covermanager/jsoncoverprovider.h
|
||||
covermanager/lastfmcoverprovider.h
|
||||
covermanager/musicbrainzcoverprovider.h
|
||||
covermanager/discogscoverprovider.h
|
||||
covermanager/deezercoverprovider.h
|
||||
covermanager/qobuzcoverprovider.h
|
||||
covermanager/musixmatchcoverprovider.h
|
||||
covermanager/spotifycoverprovider.h
|
||||
|
||||
lyrics/lyricsproviders.h
|
||||
lyrics/lyricsprovider.h
|
||||
@@ -383,18 +395,23 @@ set(HEADERS
|
||||
lyrics/auddlyricsprovider.h
|
||||
lyrics/ovhlyricsprovider.h
|
||||
lyrics/lololyricsprovider.h
|
||||
lyrics/geniuslyricsprovider.h
|
||||
lyrics/musixmatchlyricsprovider.h
|
||||
lyrics/chartlyricsprovider.h
|
||||
|
||||
settings/settingsdialog.h
|
||||
settings/settingspage.h
|
||||
settings/behavioursettingspage.h
|
||||
settings/collectionsettingspage.h
|
||||
settings/backendsettingspage.h
|
||||
settings/contextsettingspage.h
|
||||
settings/playlistsettingspage.h
|
||||
settings/scrobblersettingspage.h
|
||||
settings/coverssettingspage.h
|
||||
settings/lyricssettingspage.h
|
||||
settings/networkproxysettingspage.h
|
||||
settings/appearancesettingspage.h
|
||||
settings/contextsettingspage.h
|
||||
settings/notificationssettingspage.h
|
||||
settings/scrobblersettingspage.h
|
||||
|
||||
dialogs/about.h
|
||||
dialogs/errordialog.h
|
||||
@@ -487,10 +504,12 @@ set(UI
|
||||
settings/backendsettingspage.ui
|
||||
settings/contextsettingspage.ui
|
||||
settings/playlistsettingspage.ui
|
||||
settings/scrobblersettingspage.ui
|
||||
settings/coverssettingspage.ui
|
||||
settings/lyricssettingspage.ui
|
||||
settings/networkproxysettingspage.ui
|
||||
settings/appearancesettingspage.ui
|
||||
settings/notificationssettingspage.ui
|
||||
settings/scrobblersettingspage.ui
|
||||
|
||||
equalizer/equalizer.ui
|
||||
equalizer/equalizerslider.ui
|
||||
@@ -981,7 +1000,6 @@ target_link_libraries(strawberry_lib
|
||||
${GOBJECT_LIBRARIES}
|
||||
${GNUTLS_LIBRARIES}
|
||||
${QT_LIBRARIES}
|
||||
${CHROMAPRINT_LIBRARIES}
|
||||
${SQLITE_LIBRARIES}
|
||||
${TAGLIB_LIBRARIES}
|
||||
${SINGLEAPPLICATION_LIBRARIES}
|
||||
@@ -1005,6 +1023,10 @@ if(HAVE_GSTREAMER)
|
||||
target_link_libraries(strawberry_lib ${GSTREAMER_LIBRARIES} ${GSTREAMER_BASE_LIBRARIES} ${GSTREAMER_AUDIO_LIBRARIES} ${GSTREAMER_APP_LIBRARIES} ${GSTREAMER_TAG_LIBRARIES} ${GSTREAMER_PBUTILS_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(HAVE_CHROMAPRINT)
|
||||
target_link_libraries(strawberry_lib ${CHROMAPRINT_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(HAVE_XINE)
|
||||
target_link_libraries(strawberry_lib ${LIBXINE_LIBRARIES})
|
||||
endif()
|
||||
|
||||
@@ -411,6 +411,10 @@ void CollectionBackend::AddOrUpdateSubdirs(const SubdirectoryList &subdirs) {
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::AddOrUpdateSongsAsync(const SongList &songs) {
|
||||
metaObject()->invokeMethod(this, "AddOrUpdateSongs", Qt::QueuedConnection, Q_ARG(SongList, songs));
|
||||
}
|
||||
|
||||
void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
|
||||
@@ -186,6 +186,8 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
|
||||
Song::Source Source() const;
|
||||
|
||||
void AddOrUpdateSongsAsync(const SongList &songs);
|
||||
|
||||
public slots:
|
||||
void Exit();
|
||||
void LoadDirectories();
|
||||
|
||||
@@ -149,7 +149,7 @@ CollectionModel::~CollectionModel() {
|
||||
delete root_;
|
||||
}
|
||||
|
||||
void CollectionModel::set_pretty_covers(bool use_pretty_covers) {
|
||||
void CollectionModel::set_pretty_covers(const bool use_pretty_covers) {
|
||||
|
||||
if (use_pretty_covers != use_pretty_covers_) {
|
||||
use_pretty_covers_ = use_pretty_covers;
|
||||
@@ -157,7 +157,7 @@ void CollectionModel::set_pretty_covers(bool use_pretty_covers) {
|
||||
}
|
||||
}
|
||||
|
||||
void CollectionModel::set_show_dividers(bool show_dividers) {
|
||||
void CollectionModel::set_show_dividers(const bool show_dividers) {
|
||||
|
||||
if (show_dividers != show_dividers_) {
|
||||
show_dividers_ = show_dividers;
|
||||
@@ -199,7 +199,7 @@ void CollectionModel::ReloadSettings() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::Init(bool async) {
|
||||
void CollectionModel::Init(const bool async) {
|
||||
|
||||
if (async) {
|
||||
// Show a loading indicator in the model.
|
||||
@@ -338,7 +338,7 @@ void CollectionModel::SongsSlightlyChanged(const SongList &songs) {
|
||||
|
||||
}
|
||||
|
||||
CollectionItem *CollectionModel::CreateCompilationArtistNode(bool signal, CollectionItem *parent) {
|
||||
CollectionItem *CollectionModel::CreateCompilationArtistNode(const bool signal, CollectionItem *parent) {
|
||||
|
||||
if (signal) beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count());
|
||||
|
||||
@@ -354,7 +354,7 @@ CollectionItem *CollectionModel::CreateCompilationArtistNode(bool signal, Collec
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::DividerKey(GroupBy type, CollectionItem *item) const {
|
||||
QString CollectionModel::DividerKey(const GroupBy type, CollectionItem *item) const {
|
||||
|
||||
// Items which are to be grouped under the same divider must produce the same divider key. This will only get called for top-level items.
|
||||
|
||||
@@ -408,7 +408,7 @@ QString CollectionModel::DividerKey(GroupBy type, CollectionItem *item) const {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::DividerDisplayText(GroupBy type, const QString &key) const {
|
||||
QString CollectionModel::DividerDisplayText(const GroupBy type, const QString &key) const {
|
||||
|
||||
// Pretty display text for the dividers.
|
||||
|
||||
@@ -658,7 +658,7 @@ void CollectionModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderR
|
||||
|
||||
}
|
||||
|
||||
QVariant CollectionModel::data(const QModelIndex &idx, int role) const {
|
||||
QVariant CollectionModel::data(const QModelIndex &idx, const int role) const {
|
||||
|
||||
const CollectionItem *item = IndexToItem(idx);
|
||||
|
||||
@@ -670,11 +670,7 @@ QVariant CollectionModel::data(const QModelIndex &idx, int role) const {
|
||||
bool is_album_node = false;
|
||||
if (role == Qt::DecorationRole && item->type == CollectionItem::Type_Container) {
|
||||
GroupBy container_type = group_by_[item->container_level];
|
||||
is_album_node = container_type == GroupBy_Album ||
|
||||
container_type == GroupBy_AlbumDisc ||
|
||||
container_type == GroupBy_YearAlbum ||
|
||||
container_type == GroupBy_YearAlbumDisc ||
|
||||
container_type == GroupBy_OriginalYearAlbum;
|
||||
is_album_node = IsAlbumGrouping(container_type);
|
||||
}
|
||||
if (is_album_node) {
|
||||
// It has const behaviour some of the time - that's ok right?
|
||||
@@ -686,7 +682,7 @@ QVariant CollectionModel::data(const QModelIndex &idx, int role) const {
|
||||
|
||||
}
|
||||
|
||||
QVariant CollectionModel::data(const CollectionItem *item, int role) const {
|
||||
QVariant CollectionModel::data(const CollectionItem *item, const int role) const {
|
||||
|
||||
GroupBy container_type = item->type == CollectionItem::Type_Container ? group_by_[item->container_level] : GroupBy_None;
|
||||
|
||||
@@ -826,7 +822,7 @@ CollectionModel::QueryResult CollectionModel::RunQuery(CollectionItem *parent) {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::PostQuery(CollectionItem *parent, const CollectionModel::QueryResult &result, bool signal) {
|
||||
void CollectionModel::PostQuery(CollectionItem *parent, const CollectionModel::QueryResult &result, const bool signal) {
|
||||
|
||||
// Information about what we want the children to be
|
||||
int child_level = parent == root_ ? 0 : parent->container_level + 1;
|
||||
@@ -850,7 +846,7 @@ void CollectionModel::PostQuery(CollectionItem *parent, const CollectionModel::Q
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::LazyPopulate(CollectionItem *parent, bool signal) {
|
||||
void CollectionModel::LazyPopulate(CollectionItem *parent, const bool signal) {
|
||||
|
||||
if (parent->lazy_loaded) return;
|
||||
parent->lazy_loaded = true;
|
||||
@@ -918,7 +914,7 @@ void CollectionModel::Reset() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::InitQuery(GroupBy type, CollectionQuery *q) {
|
||||
void CollectionModel::InitQuery(const GroupBy type, CollectionQuery *q) {
|
||||
|
||||
// Say what type of thing we want to get back from the database.
|
||||
switch (type) {
|
||||
@@ -986,7 +982,7 @@ void CollectionModel::InitQuery(GroupBy type, CollectionQuery *q) {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::FilterQuery(GroupBy type, CollectionItem *item, CollectionQuery *q) {
|
||||
void CollectionModel::FilterQuery(const GroupBy type, CollectionItem *item, CollectionQuery *q) {
|
||||
|
||||
// Say how we want the query to be filtered. This is done once for each parent going up the tree.
|
||||
|
||||
@@ -1079,7 +1075,7 @@ void CollectionModel::FilterQuery(GroupBy type, CollectionItem *item, Collection
|
||||
|
||||
}
|
||||
|
||||
CollectionItem *CollectionModel::InitItem(GroupBy type, bool signal, CollectionItem *parent, int container_level) {
|
||||
CollectionItem *CollectionModel::InitItem(const GroupBy type, const bool signal, CollectionItem *parent, const int container_level) {
|
||||
|
||||
CollectionItem::Type item_type = type == GroupBy_None ? CollectionItem::Type_Song : CollectionItem::Type_Container;
|
||||
|
||||
@@ -1094,7 +1090,7 @@ CollectionItem *CollectionModel::InitItem(GroupBy type, bool signal, CollectionI
|
||||
|
||||
}
|
||||
|
||||
CollectionItem *CollectionModel::ItemFromQuery(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, const SqlRow &row, int container_level) {
|
||||
CollectionItem *CollectionModel::ItemFromQuery(const GroupBy type, const bool signal, const bool create_divider, CollectionItem *parent, const SqlRow &row, const int container_level) {
|
||||
|
||||
CollectionItem *item = InitItem(type, signal, parent, container_level);
|
||||
|
||||
@@ -1217,7 +1213,12 @@ CollectionItem *CollectionModel::ItemFromQuery(GroupBy type, bool signal, bool c
|
||||
item->metadata.InitFromQuery(row, true);
|
||||
item->key = item->metadata.title();
|
||||
item->display_text = item->metadata.TitleWithCompilationArtist();
|
||||
item->sort_text = SortTextForSong(item->metadata);
|
||||
if (item->container_level == 1 && !IsAlbumGrouping(group_by_[0])) {
|
||||
item->sort_text = SortText(item->metadata.title());
|
||||
}
|
||||
else {
|
||||
item->sort_text = SortTextForSong(item->metadata);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1227,7 +1228,7 @@ CollectionItem *CollectionModel::ItemFromQuery(GroupBy type, bool signal, bool c
|
||||
|
||||
}
|
||||
|
||||
CollectionItem *CollectionModel::ItemFromSong(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, const Song &s, int container_level) {
|
||||
CollectionItem *CollectionModel::ItemFromSong(const GroupBy type, const bool signal, const bool create_divider, CollectionItem *parent, const Song &s, const int container_level) {
|
||||
|
||||
CollectionItem *item = InitItem(type, signal, parent, container_level);
|
||||
|
||||
@@ -1358,7 +1359,12 @@ CollectionItem *CollectionModel::ItemFromSong(GroupBy type, bool signal, bool cr
|
||||
item->metadata = s;
|
||||
item->key = s.title();
|
||||
item->display_text = s.TitleWithCompilationArtist();
|
||||
item->sort_text = SortTextForSong(s);
|
||||
if (item->container_level == 1 && !IsAlbumGrouping(group_by_[0])) {
|
||||
item->sort_text = SortText(s.title());
|
||||
}
|
||||
else {
|
||||
item->sort_text = SortTextForSong(s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1369,7 +1375,7 @@ CollectionItem *CollectionModel::ItemFromSong(GroupBy type, bool signal, bool cr
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::FinishItem(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, CollectionItem *item) {
|
||||
void CollectionModel::FinishItem(const GroupBy type, const bool signal, const bool create_divider, CollectionItem *parent, CollectionItem *item) {
|
||||
|
||||
if (type == GroupBy_None) item->lazy_loaded = true;
|
||||
|
||||
@@ -1461,19 +1467,19 @@ QString CollectionModel::SortTextForArtist(QString artist) {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SortTextForNumber(int number) {
|
||||
QString CollectionModel::SortTextForNumber(const int number) {
|
||||
|
||||
return QString("%1").arg(number, 4, 10, QChar('0'));
|
||||
}
|
||||
|
||||
QString CollectionModel::SortTextForYear(int year) {
|
||||
QString CollectionModel::SortTextForYear(const int year) {
|
||||
|
||||
QString str = QString::number(year);
|
||||
return QString("0").repeated(qMax(0, 4 - str.length())) + str;
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SortTextForBitrate(int bitrate) {
|
||||
QString CollectionModel::SortTextForBitrate(const int bitrate) {
|
||||
|
||||
QString str = QString::number(bitrate);
|
||||
return QString("0").repeated(qMax(0, 3 - str.length())) + str;
|
||||
@@ -1597,7 +1603,7 @@ SongList CollectionModel::GetChildSongs(const QModelIndex &idx) const {
|
||||
return GetChildSongs(QModelIndexList() << idx);
|
||||
}
|
||||
|
||||
void CollectionModel::SetFilterAge(int age) {
|
||||
void CollectionModel::SetFilterAge(const int age) {
|
||||
query_options_.set_max_age(age);
|
||||
ResetAsync();
|
||||
}
|
||||
@@ -1632,7 +1638,7 @@ void CollectionModel::SetGroupBy(const Grouping &g) {
|
||||
|
||||
}
|
||||
|
||||
const CollectionModel::GroupBy &CollectionModel::Grouping::operator[](int i) const {
|
||||
const CollectionModel::GroupBy &CollectionModel::Grouping::operator[](const int i) const {
|
||||
|
||||
switch (i) {
|
||||
case 0: return first;
|
||||
@@ -1644,7 +1650,7 @@ const CollectionModel::GroupBy &CollectionModel::Grouping::operator[](int i) con
|
||||
|
||||
}
|
||||
|
||||
CollectionModel::GroupBy &CollectionModel::Grouping::operator[](int i) {
|
||||
CollectionModel::GroupBy &CollectionModel::Grouping::operator[](const int i) {
|
||||
|
||||
switch (i) {
|
||||
case 0: return first;
|
||||
@@ -1658,21 +1664,21 @@ CollectionModel::GroupBy &CollectionModel::Grouping::operator[](int i) {
|
||||
}
|
||||
|
||||
|
||||
void CollectionModel::TotalSongCountUpdatedSlot(int count) {
|
||||
void CollectionModel::TotalSongCountUpdatedSlot(const int count) {
|
||||
|
||||
total_song_count_ = count;
|
||||
emit TotalSongCountUpdated(count);
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::TotalArtistCountUpdatedSlot(int count) {
|
||||
void CollectionModel::TotalArtistCountUpdatedSlot(const int count) {
|
||||
|
||||
total_artist_count_ = count;
|
||||
emit TotalArtistCountUpdated(count);
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::TotalAlbumCountUpdatedSlot(int count) {
|
||||
void CollectionModel::TotalAlbumCountUpdatedSlot(const int count) {
|
||||
|
||||
total_album_count_ = count;
|
||||
emit TotalAlbumCountUpdated(count);
|
||||
|
||||
@@ -114,8 +114,8 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
GroupBy second;
|
||||
GroupBy third;
|
||||
|
||||
const GroupBy &operator[](int i) const;
|
||||
GroupBy &operator[](int i);
|
||||
const GroupBy &operator[](const int i) const;
|
||||
GroupBy &operator[](const int i);
|
||||
bool operator==(const Grouping &other) const {
|
||||
return first == other.first && second == other.second && third == other.third;
|
||||
}
|
||||
@@ -133,7 +133,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
CollectionDirectoryModel *directory_model() const { return dir_model_; }
|
||||
|
||||
// Call before Init()
|
||||
void set_show_various_artists(bool show_various_artists) { show_various_artists_ = show_various_artists; }
|
||||
void set_show_various_artists(const bool show_various_artists) { show_various_artists_ = show_various_artists; }
|
||||
|
||||
// Get information about the collection
|
||||
void GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const;
|
||||
@@ -146,18 +146,18 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
int total_album_count() const { return total_album_count_; }
|
||||
|
||||
// QAbstractItemModel
|
||||
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const;
|
||||
QVariant data(const QModelIndex &idx, const int role = Qt::DisplayRole) const;
|
||||
Qt::ItemFlags flags(const QModelIndex &idx) const;
|
||||
QStringList mimeTypes() const;
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const;
|
||||
bool canFetchMore(const QModelIndex &parent) const;
|
||||
|
||||
// Whether or not to use album cover art, if it exists, in the collection view
|
||||
void set_pretty_covers(bool use_pretty_covers);
|
||||
void set_pretty_covers(const bool use_pretty_covers);
|
||||
bool use_pretty_covers() const { return use_pretty_covers_; }
|
||||
|
||||
// Whether or not to show letters heading in the collection view
|
||||
void set_show_dividers(bool show_dividers);
|
||||
void set_show_dividers(const bool show_dividers);
|
||||
|
||||
// Save the current grouping
|
||||
void SaveGrouping(QString name);
|
||||
@@ -171,43 +171,45 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
static QString PrettyAlbumDisc(const QString &album, const int disc);
|
||||
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
|
||||
static QString SortText(QString text);
|
||||
static QString SortTextForNumber(int year);
|
||||
static QString SortTextForNumber(const int year);
|
||||
static QString SortTextForArtist(QString artist);
|
||||
static QString SortTextForSong(const Song &song);
|
||||
static QString SortTextForYear(int year);
|
||||
static QString SortTextForBitrate(int bitrate);
|
||||
static QString SortTextForYear(const int year);
|
||||
static QString SortTextForBitrate(const int bitrate);
|
||||
|
||||
quint64 icon_cache_disk_size() { return sIconCache->cacheSize(); }
|
||||
|
||||
static bool IsAlbumGrouping(const GroupBy group_by) { return group_by == GroupBy_Album || group_by == GroupBy_YearAlbum || group_by == GroupBy_OriginalYearAlbum || group_by == GroupBy_AlbumDisc || group_by == GroupBy_YearAlbumDisc; }
|
||||
|
||||
signals:
|
||||
void TotalSongCountUpdated(int count);
|
||||
void TotalArtistCountUpdated(int count);
|
||||
void TotalAlbumCountUpdated(int count);
|
||||
void TotalSongCountUpdated(const int count);
|
||||
void TotalArtistCountUpdated(const int count);
|
||||
void TotalAlbumCountUpdated(const int count);
|
||||
void GroupingChanged(const CollectionModel::Grouping &g);
|
||||
|
||||
public slots:
|
||||
void SetFilterAge(int age);
|
||||
void SetFilterAge(const int age);
|
||||
void SetFilterText(const QString &text);
|
||||
void SetFilterQueryMode(QueryOptions::QueryMode query_mode);
|
||||
|
||||
void SetGroupBy(const CollectionModel::Grouping &g);
|
||||
const CollectionModel::Grouping &GetGroupBy() const { return group_by_; }
|
||||
void Init(bool async = true);
|
||||
void Init(const bool async = true);
|
||||
void Reset();
|
||||
void ResetAsync();
|
||||
|
||||
protected:
|
||||
void LazyPopulate(CollectionItem *item) { LazyPopulate(item, true); }
|
||||
void LazyPopulate(CollectionItem *item, bool signal);
|
||||
void LazyPopulate(CollectionItem *item, const bool signal);
|
||||
|
||||
private slots:
|
||||
// From CollectionBackend
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
void SongsDeleted(const SongList &songs);
|
||||
void SongsSlightlyChanged(const SongList &songs);
|
||||
void TotalSongCountUpdatedSlot(int count);
|
||||
void TotalArtistCountUpdatedSlot(int count);
|
||||
void TotalAlbumCountUpdatedSlot(int count);
|
||||
void TotalSongCountUpdatedSlot(const int count);
|
||||
void TotalArtistCountUpdatedSlot(const int count);
|
||||
void TotalAlbumCountUpdatedSlot(const int count);
|
||||
void ClearDiskCache();
|
||||
|
||||
// Called after ResetAsync
|
||||
@@ -219,7 +221,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
// Provides some optimisations for loading the list of items in the root.
|
||||
// This gets called a lot when filtering the playlist, so it's nice to be able to do it in a background thread.
|
||||
QueryResult RunQuery(CollectionItem *parent);
|
||||
void PostQuery(CollectionItem *parent, const QueryResult &result, bool signal);
|
||||
void PostQuery(CollectionItem *parent, const QueryResult &result, const bool signal);
|
||||
|
||||
bool HasCompilations(const CollectionQuery &query);
|
||||
|
||||
@@ -228,27 +230,27 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
// Functions for working with queries and creating items.
|
||||
// When the model is reset or when a node is lazy-loaded the Collection constructs a database query to populate the items.
|
||||
// Filters are added for each parent item, restricting the songs returned to a particular album or artist for example.
|
||||
static void InitQuery(GroupBy type, CollectionQuery *q);
|
||||
void FilterQuery(GroupBy type, CollectionItem *item, CollectionQuery *q);
|
||||
static void InitQuery(const GroupBy type, CollectionQuery *q);
|
||||
void FilterQuery(const GroupBy type, CollectionItem *item, CollectionQuery *q);
|
||||
|
||||
// Items can be created either from a query that's been run to populate a node, or by a spontaneous SongsDiscovered emission from the backend.
|
||||
CollectionItem *ItemFromQuery(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, const SqlRow &row, int container_level);
|
||||
CollectionItem *ItemFromSong(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, const Song &s, int container_level);
|
||||
CollectionItem *ItemFromQuery(const GroupBy type, const bool signal, const bool create_divider, CollectionItem *parent, const SqlRow &row, const int container_level);
|
||||
CollectionItem *ItemFromSong(const GroupBy type, const bool signal, const bool create_divider, CollectionItem *parent, const Song &s, const int container_level);
|
||||
|
||||
// The "Various Artists" node is an annoying special case.
|
||||
CollectionItem *CreateCompilationArtistNode(bool signal, CollectionItem *parent);
|
||||
CollectionItem *CreateCompilationArtistNode(const bool signal, CollectionItem *parent);
|
||||
|
||||
// Helpers for ItemFromQuery and ItemFromSong
|
||||
CollectionItem *InitItem(GroupBy type, bool signal, CollectionItem *parent, int container_level);
|
||||
void FinishItem(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, CollectionItem *item);
|
||||
CollectionItem *InitItem(const GroupBy type, const bool signal, CollectionItem *parent, const int container_level);
|
||||
void FinishItem(const GroupBy type, const bool signal, const bool create_divider, CollectionItem *parent, CollectionItem *item);
|
||||
|
||||
QString DividerKey(GroupBy type, CollectionItem *item) const;
|
||||
QString DividerDisplayText(GroupBy type, const QString &key) const;
|
||||
QString DividerKey(const GroupBy type, CollectionItem *item) const;
|
||||
QString DividerDisplayText(const GroupBy type, const QString &key) const;
|
||||
|
||||
// Helpers
|
||||
QString AlbumIconPixmapCacheKey(const QModelIndex &idx) const;
|
||||
QVariant AlbumIcon(const QModelIndex &idx);
|
||||
QVariant data(const CollectionItem *item, int role) const;
|
||||
QVariant data(const CollectionItem *item, const int role) const;
|
||||
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
|
||||
int MaximumCacheSize(QSettings *s, const char *size_id, const char *size_unit_id, const int cache_size_default) const;
|
||||
|
||||
|
||||
@@ -57,11 +57,16 @@
|
||||
#include "covermanager/musicbrainzcoverprovider.h"
|
||||
#include "covermanager/deezercoverprovider.h"
|
||||
#include "covermanager/qobuzcoverprovider.h"
|
||||
#include "covermanager/musixmatchcoverprovider.h"
|
||||
#include "covermanager/spotifycoverprovider.h"
|
||||
|
||||
#include "lyrics/lyricsproviders.h"
|
||||
#include "lyrics/auddlyricsprovider.h"
|
||||
#include "lyrics/geniuslyricsprovider.h"
|
||||
#include "lyrics/ovhlyricsprovider.h"
|
||||
#include "lyrics/lololyricsprovider.h"
|
||||
#include "lyrics/musixmatchlyricsprovider.h"
|
||||
#include "lyrics/chartlyricsprovider.h"
|
||||
|
||||
#include "scrobbler/audioscrobbler.h"
|
||||
|
||||
@@ -118,9 +123,12 @@ class ApplicationImpl {
|
||||
cover_providers->AddProvider(new DiscogsCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new DeezerCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new QobuzCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new MusixmatchCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new SpotifyCoverProvider(app, app));
|
||||
#ifdef HAVE_TIDAL
|
||||
cover_providers->AddProvider(new TidalCoverProvider(app, app));
|
||||
#endif
|
||||
cover_providers->ReloadSettings();
|
||||
return cover_providers;
|
||||
}),
|
||||
album_cover_loader_([=]() {
|
||||
@@ -131,9 +139,14 @@ class ApplicationImpl {
|
||||
current_albumcover_loader_([=]() { return new CurrentAlbumCoverLoader(app, app); }),
|
||||
lyrics_providers_([=]() {
|
||||
LyricsProviders *lyrics_providers = new LyricsProviders(app);
|
||||
// Initialize the repository of lyrics providers.
|
||||
lyrics_providers->AddProvider(new AuddLyricsProvider(app));
|
||||
lyrics_providers->AddProvider(new GeniusLyricsProvider(app));
|
||||
lyrics_providers->AddProvider(new OVHLyricsProvider(app));
|
||||
lyrics_providers->AddProvider(new LoloLyricsProvider(app));
|
||||
lyrics_providers->AddProvider(new MusixmatchLyricsProvider(app));
|
||||
lyrics_providers->AddProvider(new ChartLyricsProvider(app));
|
||||
lyrics_providers->ReloadSettings();
|
||||
return lyrics_providers;
|
||||
}),
|
||||
internet_services_([=]() {
|
||||
|
||||
@@ -53,6 +53,7 @@ class DeviceManager;
|
||||
class CoverProviders;
|
||||
class AlbumCoverLoader;
|
||||
class CurrentAlbumCoverLoader;
|
||||
class CoverProviders;
|
||||
class LyricsProviders;
|
||||
class AudioScrobbler;
|
||||
class InternetServices;
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
#include "covermanager/albumcoverchoicecontroller.h"
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
#include "covermanager/currentalbumcoverloader.h"
|
||||
#include "covermanager/coverproviders.h"
|
||||
#include "lyrics/lyricsproviders.h"
|
||||
#ifndef Q_OS_WIN
|
||||
# include "device/devicemanager.h"
|
||||
# include "device/devicestatefiltermodel.h"
|
||||
@@ -262,15 +264,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
// Initialise the UI
|
||||
ui_->setupUi(this);
|
||||
|
||||
connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)));
|
||||
album_cover_choice_controller_->Init(app);
|
||||
connect(album_cover_choice_controller_->cover_from_file_action(), SIGNAL(triggered()), this, SLOT(LoadCoverFromFile()));
|
||||
connect(album_cover_choice_controller_->cover_to_file_action(), SIGNAL(triggered()), this, SLOT(SaveCoverToFile()));
|
||||
connect(album_cover_choice_controller_->cover_from_url_action(), SIGNAL(triggered()), this, SLOT(LoadCoverFromURL()));
|
||||
connect(album_cover_choice_controller_->search_for_cover_action(), SIGNAL(triggered()), this, SLOT(SearchForCover()));
|
||||
connect(album_cover_choice_controller_->unset_cover_action(), SIGNAL(triggered()), this, SLOT(UnsetCover()));
|
||||
connect(album_cover_choice_controller_->show_cover_action(), SIGNAL(triggered()), this, SLOT(ShowCover()));
|
||||
connect(album_cover_choice_controller_->search_cover_auto_action(), SIGNAL(triggered()), this, SLOT(SearchCoverAutomatically()));
|
||||
|
||||
ui_->multi_loading_indicator->SetTaskManager(app_->task_manager());
|
||||
context_view_->Init(app_, collection_view_->view(), album_cover_choice_controller_);
|
||||
@@ -427,6 +421,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
connect(ui_->action_auto_complete_tags, SIGNAL(triggered()), SLOT(AutoCompleteTags()));
|
||||
#endif
|
||||
connect(ui_->action_settings, SIGNAL(triggered()), SLOT(OpenSettingsDialog()));
|
||||
connect(ui_->action_toggle_show_sidebar, SIGNAL(toggled(bool)), SLOT(ToggleSidebar(bool)));
|
||||
connect(ui_->action_about_strawberry, SIGNAL(triggered()), SLOT(ShowAboutDialog()));
|
||||
connect(ui_->action_about_qt, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
|
||||
connect(ui_->action_shuffle, SIGNAL(triggered()), app_->playlist_manager(), SLOT(ShuffleCurrent()));
|
||||
@@ -546,6 +541,16 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
connect(app_->task_manager(), SIGNAL(PauseCollectionWatchers()), app_->collection(), SLOT(PauseWatcher()));
|
||||
connect(app_->task_manager(), SIGNAL(ResumeCollectionWatchers()), app_->collection(), SLOT(ResumeWatcher()));
|
||||
|
||||
connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)));
|
||||
connect(album_cover_choice_controller_->cover_from_file_action(), SIGNAL(triggered()), this, SLOT(LoadCoverFromFile()));
|
||||
connect(album_cover_choice_controller_->cover_to_file_action(), SIGNAL(triggered()), this, SLOT(SaveCoverToFile()));
|
||||
connect(album_cover_choice_controller_->cover_from_url_action(), SIGNAL(triggered()), this, SLOT(LoadCoverFromURL()));
|
||||
connect(album_cover_choice_controller_->search_for_cover_action(), SIGNAL(triggered()), this, SLOT(SearchForCover()));
|
||||
connect(album_cover_choice_controller_->unset_cover_action(), SIGNAL(triggered()), this, SLOT(UnsetCover()));
|
||||
connect(album_cover_choice_controller_->show_cover_action(), SIGNAL(triggered()), this, SLOT(ShowCover()));
|
||||
connect(album_cover_choice_controller_->search_cover_auto_action(), SIGNAL(triggered()), this, SLOT(SearchCoverAutomatically()));
|
||||
connect(album_cover_choice_controller_->search_cover_auto_action(), SIGNAL(toggled(bool)), SLOT(ToggleSearchCoverAuto(bool)));
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
// Devices connections
|
||||
connect(device_view_->view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
|
||||
@@ -589,7 +594,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
connect(tidal_view_->songs_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
|
||||
connect(tidal_view_->search_view(), SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
|
||||
if (TidalService *tidalservice = qobject_cast<TidalService*> (app_->internet_services()->ServiceBySource(Song::Source_Tidal)))
|
||||
connect(this, SIGNAL(AuthorisationUrlReceived(QUrl)), tidalservice, SLOT(AuthorisationUrlReceived(QUrl)));
|
||||
connect(this, SIGNAL(AuthorizationUrlReceived(QUrl)), tidalservice, SLOT(AuthorizationUrlReceived(QUrl)));
|
||||
#endif
|
||||
|
||||
// Playlist menu
|
||||
@@ -757,7 +762,6 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
app_->appearance()->LoadUserTheme();
|
||||
StyleSheetLoader *css_loader = new StyleSheetLoader(this);
|
||||
css_loader->SetStyleSheet(this, ":/style/strawberry.css");
|
||||
RefreshStyleSheet();
|
||||
|
||||
// Load playlists
|
||||
app_->playlist_manager()->Init(app_->collection_backend(), app_->playlist_backend(), ui_->playlist_sequence, ui_->playlist);
|
||||
@@ -802,10 +806,10 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
if (tab_mode == FancyTabWidget::Mode_None) tab_mode = default_mode;
|
||||
ui_->tabs->SetMode(tab_mode);
|
||||
|
||||
file_view_->SetPath(settings_.value("file_path", QDir::homePath()).toString());
|
||||
|
||||
TabSwitched();
|
||||
|
||||
file_view_->SetPath(settings_.value("file_path", QDir::homePath()).toString());
|
||||
|
||||
// Users often collapse one side of the splitter by mistake and don't know how to restore it. This must be set after the state is restored above.
|
||||
ui_->splitter->setChildrenCollapsible(false);
|
||||
|
||||
@@ -820,10 +824,10 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
#ifdef Q_OS_MACOS // Always show mainwindow on startup if on macos
|
||||
show();
|
||||
#else
|
||||
QSettings settings;
|
||||
settings.beginGroup(BehaviourSettingsPage::kSettingsGroup);
|
||||
StartupBehaviour behaviour = StartupBehaviour(settings.value("startupbehaviour", Startup_Remember).toInt());
|
||||
settings.endGroup();
|
||||
QSettings s;
|
||||
s.beginGroup(BehaviourSettingsPage::kSettingsGroup);
|
||||
StartupBehaviour behaviour = StartupBehaviour(s.value("startupbehaviour", Startup_Remember).toInt());
|
||||
s.endGroup();
|
||||
bool hidden = settings_.value("hidden", false).toBool();
|
||||
if (hidden && (!QSystemTrayIcon::isSystemTrayAvailable() || !tray_icon_ || !tray_icon_->IsVisible())) {
|
||||
hidden = false;
|
||||
@@ -845,6 +849,10 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
}
|
||||
#endif
|
||||
|
||||
bool show_sidebar = settings_.value("show_sidebar", true).toBool();
|
||||
ui_->sidebar_layout->setVisible(show_sidebar);
|
||||
ui_->action_toggle_show_sidebar->setChecked(show_sidebar);
|
||||
|
||||
QShortcut *close_window_shortcut = new QShortcut(this);
|
||||
close_window_shortcut->setKey(Qt::CTRL + Qt::Key_W);
|
||||
connect(close_window_shortcut, SIGNAL(activated()), SLOT(SetHiddenInTray()));
|
||||
@@ -858,8 +866,6 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
}
|
||||
if (app_->scrobbler()->IsEnabled() && !app_->scrobbler()->IsOffline()) app_->scrobbler()->Submit();
|
||||
|
||||
RefreshStyleSheet();
|
||||
|
||||
qLog(Debug) << "Started" << QThread::currentThread();
|
||||
initialised_ = true;
|
||||
|
||||
@@ -871,32 +877,28 @@ MainWindow::~MainWindow() {
|
||||
|
||||
void MainWindow::ReloadSettings() {
|
||||
|
||||
QSettings settings;
|
||||
QSettings s;
|
||||
|
||||
#ifndef Q_OS_MACOS
|
||||
settings.beginGroup(BehaviourSettingsPage::kSettingsGroup);
|
||||
bool showtrayicon = settings.value("showtrayicon", QSystemTrayIcon::isSystemTrayAvailable()).toBool();
|
||||
settings.endGroup();
|
||||
s.beginGroup(BehaviourSettingsPage::kSettingsGroup);
|
||||
bool showtrayicon = s.value("showtrayicon", QSystemTrayIcon::isSystemTrayAvailable()).toBool();
|
||||
s.endGroup();
|
||||
if (tray_icon_) tray_icon_->SetVisible(showtrayicon);
|
||||
if ((!showtrayicon || !QSystemTrayIcon::isSystemTrayAvailable()) && !isVisible()) show();
|
||||
#endif
|
||||
|
||||
settings.beginGroup(BehaviourSettingsPage::kSettingsGroup);
|
||||
playing_widget_ = settings.value("playing_widget", true).toBool();
|
||||
s.beginGroup(BehaviourSettingsPage::kSettingsGroup);
|
||||
playing_widget_ = s.value("playing_widget", true).toBool();
|
||||
if (playing_widget_ != ui_->widget_playing->IsEnabled()) TabSwitched();
|
||||
doubleclick_addmode_ = BehaviourSettingsPage::AddBehaviour(settings.value("doubleclick_addmode", BehaviourSettingsPage::AddBehaviour_Append).toInt());
|
||||
doubleclick_playmode_ = BehaviourSettingsPage::PlayBehaviour(settings.value("doubleclick_playmode", BehaviourSettingsPage::PlayBehaviour_IfStopped).toInt());
|
||||
doubleclick_playlist_addmode_ = BehaviourSettingsPage::PlaylistAddBehaviour(settings.value("doubleclick_playlist_addmode", BehaviourSettingsPage::PlaylistAddBehaviour_Play).toInt());
|
||||
menu_playmode_ = BehaviourSettingsPage::PlayBehaviour(settings.value("menu_playmode", BehaviourSettingsPage::PlayBehaviour_IfStopped).toInt());
|
||||
settings.endGroup();
|
||||
doubleclick_addmode_ = BehaviourSettingsPage::AddBehaviour(s.value("doubleclick_addmode", BehaviourSettingsPage::AddBehaviour_Append).toInt());
|
||||
doubleclick_playmode_ = BehaviourSettingsPage::PlayBehaviour(s.value("doubleclick_playmode", BehaviourSettingsPage::PlayBehaviour_IfStopped).toInt());
|
||||
doubleclick_playlist_addmode_ = BehaviourSettingsPage::PlaylistAddBehaviour(s.value("doubleclick_playlist_addmode", BehaviourSettingsPage::PlaylistAddBehaviour_Play).toInt());
|
||||
menu_playmode_ = BehaviourSettingsPage::PlayBehaviour(s.value("menu_playmode", BehaviourSettingsPage::PlayBehaviour_IfStopped).toInt());
|
||||
s.endGroup();
|
||||
|
||||
settings.beginGroup(kSettingsGroup);
|
||||
album_cover_choice_controller_->search_cover_auto_action()->setChecked(settings.value("search_for_cover_auto", true).toBool());
|
||||
settings.endGroup();
|
||||
|
||||
settings.beginGroup(BackendSettingsPage::kSettingsGroup);
|
||||
bool volume_control = settings.value("volume_control", true).toBool();
|
||||
settings.endGroup();
|
||||
s.beginGroup(BackendSettingsPage::kSettingsGroup);
|
||||
bool volume_control = s.value("volume_control", true).toBool();
|
||||
s.endGroup();
|
||||
if (volume_control != ui_->volume->isEnabled()) {
|
||||
ui_->volume->SetEnabled(volume_control);
|
||||
if (volume_control) {
|
||||
@@ -909,10 +911,12 @@ void MainWindow::ReloadSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
album_cover_choice_controller_->search_cover_auto_action()->setChecked(settings_.value("search_for_cover_auto", true).toBool());
|
||||
|
||||
#ifdef HAVE_SUBSONIC
|
||||
settings.beginGroup(SubsonicSettingsPage::kSettingsGroup);
|
||||
bool enable_subsonic = settings.value("enabled", false).toBool();
|
||||
settings.endGroup();
|
||||
s.beginGroup(SubsonicSettingsPage::kSettingsGroup);
|
||||
bool enable_subsonic = s.value("enabled", false).toBool();
|
||||
s.endGroup();
|
||||
if (enable_subsonic)
|
||||
ui_->tabs->EnableTab(subsonic_view_);
|
||||
else
|
||||
@@ -920,9 +924,9 @@ void MainWindow::ReloadSettings() {
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_TIDAL
|
||||
settings.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
bool enable_tidal = settings.value("enabled", false).toBool();
|
||||
settings.endGroup();
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
bool enable_tidal = s.value("enabled", false).toBool();
|
||||
s.endGroup();
|
||||
if (enable_tidal)
|
||||
ui_->tabs->EnableTab(tidal_view_);
|
||||
else
|
||||
@@ -949,6 +953,8 @@ void MainWindow::ReloadAllSettings() {
|
||||
album_cover_choice_controller_->ReloadSettings();
|
||||
if (cover_manager_.get()) cover_manager_->ReloadSettings();
|
||||
context_view_->ReloadSettings();
|
||||
app_->cover_providers()->ReloadSettings();
|
||||
app_->lyrics_providers()->ReloadSettings();
|
||||
#ifdef HAVE_SUBSONIC
|
||||
subsonic_view_->ReloadSettings();
|
||||
#endif
|
||||
@@ -960,7 +966,6 @@ void MainWindow::ReloadAllSettings() {
|
||||
|
||||
void MainWindow::RefreshStyleSheet() {
|
||||
QString contents(styleSheet());
|
||||
setStyleSheet("");
|
||||
setStyleSheet(contents);
|
||||
}
|
||||
|
||||
@@ -973,10 +978,8 @@ void MainWindow::SaveSettings() {
|
||||
ui_->playlist->view()->SaveSettings();
|
||||
app_->scrobbler()->WriteCache();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("search_for_cover_auto", album_cover_choice_controller_->search_cover_auto_action()->isChecked());
|
||||
s.endGroup();
|
||||
settings_.setValue("show_sidebar", ui_->action_toggle_show_sidebar->isChecked());
|
||||
settings_.setValue("search_for_cover_auto", album_cover_choice_controller_->search_cover_auto_action()->isChecked());
|
||||
|
||||
}
|
||||
|
||||
@@ -1159,7 +1162,7 @@ void MainWindow::TrackSkipped(PlaylistItemPtr item) {
|
||||
|
||||
void MainWindow::TabSwitched() {
|
||||
|
||||
if (playing_widget_ && (ui_->tabs->tabBar()->tabData(ui_->tabs->currentIndex()).toString().toLower() != "context" || !context_view_->album_enabled())) {
|
||||
if (playing_widget_ && ui_->sidebar_layout->isVisible() && (ui_->tabs->tabBar()->tabData(ui_->tabs->currentIndex()).toString().toLower() != "context" || !context_view_->album_enabled())) {
|
||||
ui_->widget_playing->SetEnabled();
|
||||
}
|
||||
else {
|
||||
@@ -1168,6 +1171,18 @@ void MainWindow::TabSwitched() {
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::ToggleSidebar(const bool checked) {
|
||||
|
||||
ui_->sidebar_layout->setVisible(checked);
|
||||
TabSwitched();
|
||||
settings_.setValue("show_sidebar", checked);
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::ToggleSearchCoverAuto(const bool checked) {
|
||||
settings_.setValue("search_for_cover_auto", checked);
|
||||
}
|
||||
|
||||
void MainWindow::SaveGeometry() {
|
||||
|
||||
if (!initialised_) return;
|
||||
@@ -1891,7 +1906,7 @@ void MainWindow::EditValue() {
|
||||
void MainWindow::AddFile() {
|
||||
|
||||
// Last used directory
|
||||
QString directory =settings_.value("add_media_path", QDir::currentPath()).toString();
|
||||
QString directory = settings_.value("add_media_path", QDir::currentPath()).toString();
|
||||
|
||||
PlaylistParser parser(app_->collection_backend());
|
||||
|
||||
@@ -1918,7 +1933,7 @@ void MainWindow::AddFile() {
|
||||
void MainWindow::AddFolder() {
|
||||
|
||||
// Last used directory
|
||||
QString directory =settings_.value("add_folder_path", QDir::currentPath()).toString();
|
||||
QString directory = settings_.value("add_folder_path", QDir::currentPath()).toString();
|
||||
|
||||
// Show dialog
|
||||
directory = QFileDialog::getExistingDirectory(this, tr("Add folder"), directory);
|
||||
@@ -2058,7 +2073,7 @@ void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
|
||||
#ifdef HAVE_TIDAL
|
||||
for (const QUrl &url : options.urls()) {
|
||||
if (url.scheme() == "tidal" && url.host() == "login") {
|
||||
emit AuthorisationUrlReceived(url);
|
||||
emit AuthorizationUrlReceived(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2656,8 +2671,8 @@ void MainWindow::GetCoverAutomatically() {
|
||||
!song_.effective_album().isEmpty();
|
||||
|
||||
if (search) {
|
||||
album_cover_choice_controller_->SearchCoverAutomatically(song_);
|
||||
emit SearchCoverInProgress();
|
||||
album_cover_choice_controller_->SearchCoverAutomatically(song_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
|
||||
void IntroPointReached();
|
||||
|
||||
void AuthorisationUrlReceived(const QUrl &url);
|
||||
void AuthorizationUrlReceived(const QUrl &url);
|
||||
|
||||
private slots:
|
||||
void FilePathChanged(const QString& path);
|
||||
@@ -232,6 +232,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
void OpenSettingsDialogAtPage(SettingsDialog::Page page);
|
||||
|
||||
void TabSwitched();
|
||||
void ToggleSidebar(const bool checked);
|
||||
void ToggleSearchCoverAuto(const bool checked);
|
||||
void SaveGeometry();
|
||||
void SavePlaybackStatus();
|
||||
void LoadPlaybackStatus();
|
||||
|
||||
@@ -506,6 +506,8 @@
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_settings"/>
|
||||
<addaction name="action_console"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_toggle_show_sidebar"/>
|
||||
</widget>
|
||||
<addaction name="menu_music"/>
|
||||
<addaction name="menu_playlist"/>
|
||||
@@ -833,6 +835,14 @@
|
||||
<string>Add stream...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_toggle_show_sidebar">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Show sidebar</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<customwidgets>
|
||||
|
||||
@@ -396,13 +396,13 @@ void Mpris2::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &re
|
||||
AddMetadata("mpris:trackid", current_track_id(), &last_metadata_);
|
||||
|
||||
QUrl cover_url;
|
||||
if (result.cover_url.isValid() && result.cover_url.isLocalFile()) {
|
||||
if (result.cover_url.isValid() && result.cover_url.isLocalFile() && QFile(result.cover_url.toLocalFile()).exists()) {
|
||||
cover_url = result.cover_url;
|
||||
}
|
||||
else if (result.temp_cover_url.isValid() && result.temp_cover_url.isLocalFile()) {
|
||||
cover_url = result.temp_cover_url;
|
||||
}
|
||||
if (cover_url.isValid()) AddMetadata("mpris:artUrl", result.cover_url.toString(), &last_metadata_);
|
||||
if (cover_url.isValid()) AddMetadata("mpris:artUrl", cover_url.toString(), &last_metadata_);
|
||||
|
||||
AddMetadata("year", song.year(), &last_metadata_);
|
||||
AddMetadata("bitrate", song.bitrate(), &last_metadata_);
|
||||
|
||||
@@ -395,6 +395,7 @@ void Player::NextItem(Engine::TrackChangeFlags change) {
|
||||
int i = active_playlist->next_row(ignore_repeat_track);
|
||||
if (i == -1) {
|
||||
app_->playlist_manager()->active()->set_current_row(i);
|
||||
app_->playlist_manager()->active()->reset_last_played();
|
||||
emit PlaylistFinished();
|
||||
Stop();
|
||||
return;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QTimer>
|
||||
#include <QIODevice>
|
||||
#include <QTextStream>
|
||||
#include <QFile>
|
||||
@@ -36,22 +37,16 @@
|
||||
#include "core/logging.h"
|
||||
#include "stylesheetloader.h"
|
||||
|
||||
StyleSheetLoader::StyleSheetLoader(QObject *parent) : QObject(parent) {}
|
||||
StyleSheetLoader::StyleSheetLoader(QObject *parent) : QObject(parent), timer_reset_counter_(new QTimer(this)) {
|
||||
|
||||
void StyleSheetLoader::SetStyleSheet(QWidget *widget, const QString &filename) {
|
||||
timer_reset_counter_->setSingleShot(true);
|
||||
timer_reset_counter_->setInterval(1000);
|
||||
|
||||
widgets_[widget] = qMakePair(filename, QString());
|
||||
widget->installEventFilter(this);
|
||||
UpdateStyleSheet(widget);
|
||||
connect(timer_reset_counter_, SIGNAL(timeout()), this, SLOT(ResetCounters()));
|
||||
|
||||
}
|
||||
|
||||
void StyleSheetLoader::UpdateStyleSheet(QWidget *widget) {
|
||||
|
||||
if (!widget || !widgets_.contains(widget)) return;
|
||||
|
||||
QString filename(widgets_[widget].first);
|
||||
QString stylesheet(widgets_[widget].second);
|
||||
void StyleSheetLoader::SetStyleSheet(QWidget *widget, const QString &filename) {
|
||||
|
||||
// Load the file
|
||||
QFile file(filename);
|
||||
@@ -60,56 +55,71 @@ void StyleSheetLoader::UpdateStyleSheet(QWidget *widget) {
|
||||
return;
|
||||
}
|
||||
QTextStream stream(&file);
|
||||
QString contents;
|
||||
QString stylesheet;
|
||||
forever {
|
||||
QString line = stream.readLine();
|
||||
contents.append(line);
|
||||
stylesheet.append(line);
|
||||
if (stream.atEnd()) break;
|
||||
}
|
||||
file.close();
|
||||
|
||||
StyleSheetData styledata;
|
||||
styledata.filename_ = filename;
|
||||
styledata.stylesheet_template_ = stylesheet;
|
||||
styledata.stylesheet_current_ = widget->styleSheet();
|
||||
styledata_[widget] = styledata;
|
||||
|
||||
widget->installEventFilter(this);
|
||||
UpdateStyleSheet(widget, styledata);
|
||||
|
||||
}
|
||||
|
||||
void StyleSheetLoader::UpdateStyleSheet(QWidget *widget, StyleSheetData styledata) {
|
||||
|
||||
QString stylesheet = styledata.stylesheet_template_;
|
||||
|
||||
// Replace %palette-role with actual colours
|
||||
QPalette p(widget->palette());
|
||||
|
||||
QColor alt = p.color(QPalette::AlternateBase);
|
||||
alt.setAlpha(50);
|
||||
contents.replace("%palette-alternate-base", QString("rgba(%1,%2,%3,%4%)")
|
||||
stylesheet.replace("%palette-alternate-base", QString("rgba(%1,%2,%3,%4%)")
|
||||
.arg(alt.red())
|
||||
.arg(alt.green())
|
||||
.arg(alt.blue())
|
||||
.arg(alt.alpha()));
|
||||
|
||||
ReplaceColor(&contents, "Window", p, QPalette::Window);
|
||||
ReplaceColor(&contents, "Background", p, QPalette::Background);
|
||||
ReplaceColor(&contents, "WindowText", p, QPalette::WindowText);
|
||||
ReplaceColor(&contents, "Foreground", p, QPalette::Foreground);
|
||||
ReplaceColor(&contents, "Base", p, QPalette::Base);
|
||||
ReplaceColor(&contents, "AlternateBase", p, QPalette::AlternateBase);
|
||||
ReplaceColor(&contents, "ToolTipBase", p, QPalette::ToolTipBase);
|
||||
ReplaceColor(&contents, "ToolTipText", p, QPalette::ToolTipText);
|
||||
ReplaceColor(&contents, "Text", p, QPalette::Text);
|
||||
ReplaceColor(&contents, "Button", p, QPalette::Button);
|
||||
ReplaceColor(&contents, "ButtonText", p, QPalette::ButtonText);
|
||||
ReplaceColor(&contents, "BrightText", p, QPalette::BrightText);
|
||||
ReplaceColor(&contents, "Light", p, QPalette::Light);
|
||||
ReplaceColor(&contents, "Midlight", p, QPalette::Midlight);
|
||||
ReplaceColor(&contents, "Dark", p, QPalette::Dark);
|
||||
ReplaceColor(&contents, "Mid", p, QPalette::Mid);
|
||||
ReplaceColor(&contents, "Shadow", p, QPalette::Shadow);
|
||||
ReplaceColor(&contents, "Highlight", p, QPalette::Highlight);
|
||||
ReplaceColor(&contents, "HighlightedText", p, QPalette::HighlightedText);
|
||||
ReplaceColor(&contents, "Link", p, QPalette::Link);
|
||||
ReplaceColor(&contents, "LinkVisited", p, QPalette::LinkVisited);
|
||||
ReplaceColor(&stylesheet, "Window", p, QPalette::Window);
|
||||
ReplaceColor(&stylesheet, "Background", p, QPalette::Background);
|
||||
ReplaceColor(&stylesheet, "WindowText", p, QPalette::WindowText);
|
||||
ReplaceColor(&stylesheet, "Foreground", p, QPalette::Foreground);
|
||||
ReplaceColor(&stylesheet, "Base", p, QPalette::Base);
|
||||
ReplaceColor(&stylesheet, "AlternateBase", p, QPalette::AlternateBase);
|
||||
ReplaceColor(&stylesheet, "ToolTipBase", p, QPalette::ToolTipBase);
|
||||
ReplaceColor(&stylesheet, "ToolTipText", p, QPalette::ToolTipText);
|
||||
ReplaceColor(&stylesheet, "Text", p, QPalette::Text);
|
||||
ReplaceColor(&stylesheet, "Button", p, QPalette::Button);
|
||||
ReplaceColor(&stylesheet, "ButtonText", p, QPalette::ButtonText);
|
||||
ReplaceColor(&stylesheet, "BrightText", p, QPalette::BrightText);
|
||||
ReplaceColor(&stylesheet, "Light", p, QPalette::Light);
|
||||
ReplaceColor(&stylesheet, "Midlight", p, QPalette::Midlight);
|
||||
ReplaceColor(&stylesheet, "Dark", p, QPalette::Dark);
|
||||
ReplaceColor(&stylesheet, "Mid", p, QPalette::Mid);
|
||||
ReplaceColor(&stylesheet, "Shadow", p, QPalette::Shadow);
|
||||
ReplaceColor(&stylesheet, "Highlight", p, QPalette::Highlight);
|
||||
ReplaceColor(&stylesheet, "HighlightedText", p, QPalette::HighlightedText);
|
||||
ReplaceColor(&stylesheet, "Link", p, QPalette::Link);
|
||||
ReplaceColor(&stylesheet, "LinkVisited", p, QPalette::LinkVisited);
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
contents.replace("macos", "*");
|
||||
stylesheet.replace("macos", "*");
|
||||
#endif
|
||||
|
||||
if (contents == stylesheet) return;
|
||||
|
||||
widget->setStyleSheet("");
|
||||
widget->setStyleSheet(contents);
|
||||
widgets_[widget] = qMakePair(filename, contents);
|
||||
if (stylesheet != styledata.stylesheet_current_) {
|
||||
widget->setStyleSheet(stylesheet);
|
||||
styledata.stylesheet_current_ = widget->styleSheet();
|
||||
styledata_[widget] = styledata;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -126,9 +136,23 @@ bool StyleSheetLoader::eventFilter(QObject *obj, QEvent *event) {
|
||||
if (event->type() != QEvent::PaletteChange) return false;
|
||||
|
||||
QWidget *widget = qobject_cast<QWidget*>(obj);
|
||||
if (!widget || !widgets_.contains(widget)) return false;
|
||||
if (!widget || !styledata_.contains(widget)) return false;
|
||||
|
||||
UpdateStyleSheet(widget);
|
||||
StyleSheetData styledata = styledata_[widget];
|
||||
++styledata.count_;
|
||||
styledata_[widget] = styledata;
|
||||
timer_reset_counter_->start();
|
||||
if (styledata.count_ < 5) {
|
||||
UpdateStyleSheet(widget, styledata);
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
void StyleSheetLoader::ResetCounters() {
|
||||
|
||||
for (QMap<QWidget*, StyleSheetData>::iterator i = styledata_.begin() ; i != styledata_.end() ; ++i) {
|
||||
i.value().count_ = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,9 +31,12 @@
|
||||
#include <QString>
|
||||
|
||||
class QWidget;
|
||||
class QTimer;
|
||||
class QEvent;
|
||||
|
||||
class StyleSheetLoader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit StyleSheetLoader(QObject *parent = nullptr);
|
||||
|
||||
@@ -46,12 +49,24 @@ class StyleSheetLoader : public QObject {
|
||||
bool eventFilter(QObject *obj, QEvent *event);
|
||||
|
||||
private:
|
||||
void UpdateStyleSheet(QWidget *widget);
|
||||
void ReplaceColor(QString *css, const QString name, const QPalette &palette, QPalette::ColorRole role) const;
|
||||
struct StyleSheetData {
|
||||
StyleSheetData() : count_(0) {}
|
||||
QString filename_;
|
||||
QString stylesheet_template_;
|
||||
QString stylesheet_current_;
|
||||
int count_;
|
||||
};
|
||||
|
||||
private:
|
||||
QMap<QWidget*, QPair<QString, QString>> widgets_;
|
||||
void UpdateStyleSheet(QWidget *widget, StyleSheetData styledata);
|
||||
void ReplaceColor(QString *css, const QString name, const QPalette &palette, QPalette::ColorRole role) const;
|
||||
|
||||
private slots:
|
||||
void ResetCounters();
|
||||
|
||||
private:
|
||||
QMap<QWidget*, StyleSheetData> styledata_;
|
||||
QTimer *timer_reset_counter_;
|
||||
};
|
||||
|
||||
#endif // STYLESHEETLOADER_H
|
||||
|
||||
|
||||
@@ -367,6 +367,10 @@ void OpenInFileManager(const QString &path) {
|
||||
command_params.removeAt(command_params.indexOf("%U"));
|
||||
}
|
||||
|
||||
if (command.startsWith("/usr/bin/")) {
|
||||
command = command.split("/").last();
|
||||
}
|
||||
|
||||
if (command.isEmpty() || command == "exo-open") {
|
||||
QFileInfo info(path);
|
||||
if (!info.exists()) return;
|
||||
@@ -926,6 +930,9 @@ QString ReplaceMessage(const QString &message, const Song &song, const QString &
|
||||
pos += variable_replacer.matchedLength();
|
||||
}
|
||||
|
||||
int index_of = copy.indexOf(QRegExp(" - (>|$)"));
|
||||
if (index_of >= 0) copy = copy.remove(index_of, 3);
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -989,6 +996,16 @@ bool IsColorDark(const QColor &color) {
|
||||
return ((30 * color.red() + 59 * color.green() + 11 * color.blue()) / 100) <= 130;
|
||||
}
|
||||
|
||||
QList<QByteArray> ImageFormatsForMimeType(const QByteArray &mimetype) {
|
||||
|
||||
if (mimetype == "image/bmp") return QList<QByteArray>() << "BMP";
|
||||
else if (mimetype == "image/gif") return QList<QByteArray>() << "GIF";
|
||||
else if (mimetype == "image/jpeg") return QList<QByteArray>() << "JPG";
|
||||
else if (mimetype == "image/png") return QList<QByteArray>() << "PNG";
|
||||
else return QList<QByteArray>();
|
||||
|
||||
}
|
||||
|
||||
} // namespace Utilities
|
||||
|
||||
ScopedWCharArray::ScopedWCharArray(const QString &str)
|
||||
|
||||
@@ -163,6 +163,8 @@ QString ReplaceVariable(const QString &variable, const Song &song, const QString
|
||||
|
||||
bool IsColorDark(const QColor &color);
|
||||
|
||||
QList<QByteArray> ImageFormatsForMimeType(const QByteArray &mimetype);
|
||||
|
||||
} // namespace
|
||||
|
||||
class ScopedWCharArray {
|
||||
|
||||
@@ -387,7 +387,7 @@ QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song *song, cons
|
||||
|
||||
QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QImage &image, const bool overwrite) {
|
||||
|
||||
QString filepath = app_->album_cover_loader()->CoverFilePath(source, artist, album, album_id, album_dir, cover_url);
|
||||
QString filepath = app_->album_cover_loader()->CoverFilePath(source, artist, album, album_id, album_dir, cover_url, "jpg");
|
||||
if (filepath.isEmpty()) return QUrl();
|
||||
|
||||
QUrl new_cover_url(QUrl::fromLocalFile(filepath));
|
||||
|
||||
@@ -40,8 +40,20 @@ AlbumCoverFetcher::AlbumCoverFetcher(CoverProviders *cover_providers, QObject *p
|
||||
network_(network ? network : new NetworkAccessManager(this)),
|
||||
next_id_(0),
|
||||
request_starter_(new QTimer(this)) {
|
||||
|
||||
request_starter_->setInterval(1000);
|
||||
connect(request_starter_, SIGNAL(timeout()), SLOT(StartRequests()));
|
||||
|
||||
}
|
||||
|
||||
AlbumCoverFetcher::~AlbumCoverFetcher() {
|
||||
|
||||
for (AlbumCoverFetcherSearch *search : active_requests_.values()) {
|
||||
search->disconnect();
|
||||
search->deleteLater();
|
||||
}
|
||||
active_requests_.clear();
|
||||
|
||||
}
|
||||
|
||||
quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString &album, const QString &title, bool fetchall) {
|
||||
@@ -125,8 +137,8 @@ void AlbumCoverFetcher::StartRequests() {
|
||||
|
||||
void AlbumCoverFetcher::SingleSearchFinished(const quint64 request_id, const CoverSearchResults results) {
|
||||
|
||||
if (!active_requests_.contains(request_id)) return;
|
||||
AlbumCoverFetcherSearch *search = active_requests_.take(request_id);
|
||||
if (!search) return;
|
||||
|
||||
search->deleteLater();
|
||||
emit SearchFinished(request_id, results, search->statistics());
|
||||
@@ -135,8 +147,8 @@ void AlbumCoverFetcher::SingleSearchFinished(const quint64 request_id, const Cov
|
||||
|
||||
void AlbumCoverFetcher::SingleCoverFetched(const quint64 request_id, const QUrl &cover_url, const QImage &image) {
|
||||
|
||||
if (!active_requests_.contains(request_id)) return;
|
||||
AlbumCoverFetcherSearch *search = active_requests_.take(request_id);
|
||||
if (!search) return;
|
||||
|
||||
search->deleteLater();
|
||||
emit AlbumCoverFetched(request_id, cover_url, image, search->statistics());
|
||||
|
||||
@@ -89,7 +89,7 @@ class AlbumCoverFetcher : public QObject {
|
||||
|
||||
public:
|
||||
explicit AlbumCoverFetcher(CoverProviders *cover_providers, QObject *parent = nullptr, QNetworkAccessManager *network = 0);
|
||||
virtual ~AlbumCoverFetcher() {}
|
||||
~AlbumCoverFetcher();
|
||||
|
||||
static const int kMaxConcurrentRequests;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2020, 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
|
||||
@@ -30,6 +31,7 @@
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QImage>
|
||||
#include <QImageReader>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
@@ -60,6 +62,11 @@ AlbumCoverFetcherSearch::AlbumCoverFetcherSearch(
|
||||
|
||||
}
|
||||
|
||||
AlbumCoverFetcherSearch::~AlbumCoverFetcherSearch() {
|
||||
pending_requests_.clear();
|
||||
Cancel();
|
||||
}
|
||||
|
||||
void AlbumCoverFetcherSearch::TerminateSearch() {
|
||||
|
||||
for (quint64 id : pending_requests_.keys()) {
|
||||
@@ -72,17 +79,29 @@ void AlbumCoverFetcherSearch::TerminateSearch() {
|
||||
|
||||
void AlbumCoverFetcherSearch::Start(CoverProviders *cover_providers) {
|
||||
|
||||
for (CoverProvider *provider : cover_providers->List()) {
|
||||
QList<CoverProvider*> cover_providers_sorted = cover_providers->List();
|
||||
std::stable_sort(cover_providers_sorted.begin(), cover_providers_sorted.end(), ProviderCompareOrder);
|
||||
|
||||
// Skip provider if it does not have fetchall set, and we are doing fetchall - "Fetch Missing Covers".
|
||||
for (CoverProvider *provider : cover_providers_sorted) {
|
||||
|
||||
if (!provider->is_enabled()) continue;
|
||||
|
||||
// Skip any provider that requires authentication but is not authenticated.
|
||||
if (provider->AuthenticationRequired() && !provider->IsAuthenticated()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip provider if it does not have fetchall set and we are doing fetchall - "Fetch Missing Covers".
|
||||
if (!provider->fetchall() && request_.fetchall) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If album is missing, check if we can still use this provider by searching using artist + title.
|
||||
if (!provider->allow_missing_album() && request_.album.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
connect(provider, SIGNAL(SearchResults(int, CoverSearchResults)), SLOT(ProviderSearchResults(int, CoverSearchResults)));
|
||||
connect(provider, SIGNAL(SearchFinished(int, CoverSearchResults)), SLOT(ProviderSearchFinished(int, CoverSearchResults)));
|
||||
const int id = cover_providers->NextId();
|
||||
const bool success = provider->StartSearch(request_.artist, request_.album, request_.title, id);
|
||||
@@ -100,10 +119,15 @@ void AlbumCoverFetcherSearch::Start(CoverProviders *cover_providers) {
|
||||
|
||||
}
|
||||
|
||||
void AlbumCoverFetcherSearch::ProviderSearchFinished(const int id, const CoverSearchResults &results) {
|
||||
void AlbumCoverFetcherSearch::ProviderSearchResults(const int id, const CoverSearchResults &results) {
|
||||
|
||||
if (!pending_requests_.contains(id)) return;
|
||||
CoverProvider *provider = pending_requests_.take(id);
|
||||
CoverProvider *provider = pending_requests_[id];
|
||||
ProviderSearchResults(provider, results);
|
||||
|
||||
}
|
||||
|
||||
void AlbumCoverFetcherSearch::ProviderSearchResults(CoverProvider *provider, const CoverSearchResults &results) {
|
||||
|
||||
CoverSearchResults results_copy(results);
|
||||
for (int i = 0 ; i < results_copy.count() ; ++i) {
|
||||
@@ -124,6 +148,15 @@ void AlbumCoverFetcherSearch::ProviderSearchFinished(const int id, const CoverSe
|
||||
results_.append(results_copy);
|
||||
statistics_.total_images_by_provider_[provider->name()]++;
|
||||
|
||||
}
|
||||
|
||||
void AlbumCoverFetcherSearch::ProviderSearchFinished(const int id, const CoverSearchResults &results) {
|
||||
|
||||
if (!pending_requests_.contains(id)) return;
|
||||
|
||||
CoverProvider *provider = pending_requests_.take(id);
|
||||
ProviderSearchResults(provider, results);
|
||||
|
||||
// Do we have more providers left?
|
||||
if (!pending_requests_.isEmpty()) {
|
||||
return;
|
||||
@@ -193,6 +226,7 @@ void AlbumCoverFetcherSearch::FetchMoreImages() {
|
||||
|
||||
void AlbumCoverFetcherSearch::ProviderCoverFetchFinished(QNetworkReply *reply) {
|
||||
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (!pending_image_loads_.contains(reply)) return;
|
||||
@@ -207,18 +241,24 @@ void AlbumCoverFetcherSearch::ProviderCoverFetchFinished(QNetworkReply *reply) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qLog(Error) << "Error requesting" << reply->url() << reply->errorString();
|
||||
}
|
||||
else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
qLog(Error) << "Error requesting" << reply->url() << "received HTTP code" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
}
|
||||
else {
|
||||
QImage image;
|
||||
if (image.loadFromData(reply->readAll())) {
|
||||
|
||||
result.score += ScoreImage(image);
|
||||
candidate_images_.insertMulti(result.score, CandidateImage(result, image));
|
||||
|
||||
qLog(Debug) << reply->url() << "from" << result.provider << "scored" << result.score;
|
||||
|
||||
QString mimetype = reply->header(QNetworkRequest::ContentTypeHeader).toString();
|
||||
if (QImageReader::supportedMimeTypes().contains(mimetype.toUtf8())) {
|
||||
QImage image;
|
||||
if (image.loadFromData(reply->readAll())) {
|
||||
result.score += ScoreImage(image);
|
||||
candidate_images_.insertMulti(result.score, CandidateImage(result, image));
|
||||
qLog(Debug) << reply->url() << "from" << result.provider << "scored" << result.score;
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Error decoding image data from" << reply->url();
|
||||
}
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Error decoding image data from" << reply->url();
|
||||
qLog(Error) << "Unsupported mimetype for image reader:" << mimetype << "from" << reply->url();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +308,7 @@ void AlbumCoverFetcherSearch::SendBestImage() {
|
||||
cover_url = best_image.first.image_url;
|
||||
image = best_image.second;
|
||||
|
||||
qLog(Info) << "Using " << best_image.first.image_url << "from" << best_image.first.provider << "with score" << best_image.first.score;
|
||||
qLog(Info) << "Using" << best_image.first.image_url << "from" << best_image.first.provider << "with score" << best_image.first.score;
|
||||
|
||||
statistics_.chosen_images_by_provider_[best_image.first.provider]++;
|
||||
statistics_.chosen_images_++;
|
||||
@@ -292,13 +332,19 @@ void AlbumCoverFetcherSearch::Cancel() {
|
||||
}
|
||||
else if (!pending_image_loads_.isEmpty()) {
|
||||
for (QNetworkReply *reply : pending_image_loads_.keys()) {
|
||||
disconnect(reply, &QNetworkReply::finished, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
pending_image_loads_.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool AlbumCoverFetcherSearch::ProviderCompareOrder(CoverProvider *a, CoverProvider *b) {
|
||||
return a->order() < b->order();
|
||||
}
|
||||
|
||||
bool AlbumCoverFetcherSearch::CoverSearchResultCompareScore(const CoverSearchResult &a, const CoverSearchResult &b) {
|
||||
return a.score > b.score;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2020, 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
|
||||
@@ -48,6 +49,7 @@ class AlbumCoverFetcherSearch : public QObject {
|
||||
|
||||
public:
|
||||
explicit AlbumCoverFetcherSearch(const CoverSearchRequest &request, QNetworkAccessManager *network, QObject *parent);
|
||||
~AlbumCoverFetcherSearch();
|
||||
|
||||
void Start(CoverProviders *cover_providers);
|
||||
|
||||
@@ -64,18 +66,22 @@ class AlbumCoverFetcherSearch : public QObject {
|
||||
void AlbumCoverFetched(const quint64, const QUrl &cover_url, const QImage &cover);
|
||||
|
||||
private slots:
|
||||
void ProviderSearchResults(const int id, const CoverSearchResults &results);
|
||||
void ProviderSearchResults(CoverProvider *provider, const CoverSearchResults &results);
|
||||
void ProviderSearchFinished(const int id, const CoverSearchResults &results);
|
||||
void ProviderCoverFetchFinished(QNetworkReply *reply);
|
||||
void TerminateSearch();
|
||||
|
||||
private:
|
||||
static bool CoverSearchResultCompareScore(const CoverSearchResult &a, const CoverSearchResult &b);
|
||||
void AllProvidersFinished();
|
||||
|
||||
void FetchMoreImages();
|
||||
float ScoreImage(const QImage &image) const;
|
||||
void SendBestImage();
|
||||
|
||||
static bool ProviderCompareOrder(CoverProvider *a, CoverProvider *b);
|
||||
static bool CoverSearchResultCompareScore(const CoverSearchResult &a, const CoverSearchResult &b);
|
||||
|
||||
private:
|
||||
static const int kSearchTimeoutMs;
|
||||
static const int kImageLoadTimeoutMs;
|
||||
|
||||
@@ -98,27 +98,32 @@ void AlbumCoverLoader::ReloadSettings() {
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::AlbumCoverFilename(QString artist, QString album) {
|
||||
QString AlbumCoverLoader::AlbumCoverFilename(QString artist, QString album, const QString &extension) {
|
||||
|
||||
artist.remove('/');
|
||||
album.remove('/');
|
||||
|
||||
QString filename = artist + "-" + album + ".jpg";
|
||||
QString filename = artist + "-" + album;
|
||||
filename = Utilities::UnicodeToAscii(filename.toLower());
|
||||
filename = filename.replace(' ', '-');
|
||||
filename = filename.replace("--", "-");
|
||||
filename = filename.remove(OrganiseFormat::kInvalidFatCharacters);
|
||||
filename = filename.trimmed();
|
||||
filename = filename.simplified();
|
||||
|
||||
if (!extension.isEmpty()) {
|
||||
filename.append('.');
|
||||
filename.append(extension);
|
||||
}
|
||||
|
||||
return filename;
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url) {
|
||||
return CoverFilePath(song.source(), song.effective_albumartist(), song.album(), song.album_id(), album_dir, cover_url);
|
||||
QString AlbumCoverLoader::CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension) {
|
||||
return CoverFilePath(song.source(), song.effective_albumartist(), song.album(), song.album_id(), album_dir, cover_url, extension);
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url) {
|
||||
QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension) {
|
||||
|
||||
album.remove(Song::kAlbumRemoveDisc);
|
||||
|
||||
@@ -130,7 +135,7 @@ QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString
|
||||
path = Song::ImageCacheDir(source);
|
||||
}
|
||||
|
||||
if (path.right(1) == QDir::separator()) {
|
||||
if (path.right(1) == QDir::separator() || path.right(1) == "/") {
|
||||
path.chop(1);
|
||||
}
|
||||
|
||||
@@ -142,13 +147,17 @@ QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString
|
||||
|
||||
QString filename;
|
||||
if (source == Song::Source_Collection && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern && !cover_pattern_.isEmpty()) {
|
||||
filename = CoverFilenameFromVariable(artist, album) + ".jpg";
|
||||
filename = CoverFilenameFromVariable(artist, album);
|
||||
filename.remove(OrganiseFormat::kInvalidFatCharacters);
|
||||
if (cover_lowercase_) filename = filename.toLower();
|
||||
if (cover_replace_spaces_) filename.replace(QRegExp("\\s"), "-");
|
||||
if (!extension.isEmpty()) {
|
||||
filename.append('.');
|
||||
filename.append(extension);
|
||||
}
|
||||
}
|
||||
else {
|
||||
filename = CoverFilenameFromSource(source, cover_url, artist, album, album_id);
|
||||
filename = CoverFilenameFromSource(source, cover_url, artist, album, album_id, extension);
|
||||
}
|
||||
|
||||
QString filepath(path + "/" + filename);
|
||||
@@ -157,18 +166,21 @@ QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id) {
|
||||
QString AlbumCoverLoader::CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension) {
|
||||
|
||||
QString filename;
|
||||
|
||||
switch (source) {
|
||||
case Song::Source_Tidal:
|
||||
filename = album_id + "-" + cover_url.fileName();
|
||||
break;
|
||||
if (!album_id.isEmpty()) {
|
||||
filename = album_id + "-" + cover_url.fileName();
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
case Song::Source_Subsonic:
|
||||
case Song::Source_Qobuz:
|
||||
filename = AlbumCoverFilename(artist, album);
|
||||
if (filename.length() > 8 && (filename.length() - 5) >= (artist.length() + album.length() - 2)) {
|
||||
if (!album_id.isEmpty()) {
|
||||
filename = album_id;
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
@@ -178,20 +190,29 @@ QString AlbumCoverLoader::CoverFilenameFromSource(const Song::Source source, con
|
||||
case Song::Source_Device:
|
||||
case Song::Source_Stream:
|
||||
case Song::Source_Unknown:
|
||||
filename = Utilities::Sha1CoverHash(artist, album).toHex() + ".jpg";
|
||||
filename = Utilities::Sha1CoverHash(artist, album).toHex();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!extension.isEmpty()) {
|
||||
filename.append('.');
|
||||
filename.append(extension);
|
||||
}
|
||||
|
||||
return filename;
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::CoverFilenameFromVariable(const QString &artist, const QString &album) {
|
||||
QString AlbumCoverLoader::CoverFilenameFromVariable(const QString &artist, const QString &album, const QString &extension) {
|
||||
|
||||
QString filename(cover_pattern_);
|
||||
filename.replace("%albumartist", artist);
|
||||
filename.replace("%artist", artist);
|
||||
filename.replace("%album", album);
|
||||
if (!extension.isEmpty()) {
|
||||
filename.append('.');
|
||||
filename.append(extension);
|
||||
}
|
||||
return filename;
|
||||
|
||||
}
|
||||
@@ -315,7 +336,7 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) {
|
||||
!task->options.scale_output_image_ &&
|
||||
!task->options.pad_output_image_) {
|
||||
task->song.InitArtManual();
|
||||
if (task->art_manual != task->song.art_manual()) {
|
||||
if (task->song.art_manual_is_valid() && task->art_manual != task->song.art_manual()) {
|
||||
task->art_manual = task->song.art_manual();
|
||||
task->art_updated = true;
|
||||
}
|
||||
@@ -338,12 +359,12 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) {
|
||||
|
||||
if (!cover_url.isEmpty() && !cover_url.path().isEmpty()) {
|
||||
if (cover_url.path() == Song::kManuallyUnsetCover) {
|
||||
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_ManuallyUnset, QUrl(), task->options.default_output_image_);
|
||||
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_ManuallyUnset, cover_url, task->options.default_output_image_);
|
||||
}
|
||||
else if (cover_url.path() == Song::kEmbeddedCover && task->song_url.isLocalFile()) {
|
||||
const QImage taglib_image = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task->song_url.toLocalFile());
|
||||
if (!taglib_image.isNull()) {
|
||||
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, QUrl(), ScaleAndPad(task->options, taglib_image).first);
|
||||
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, cover_url, ScaleAndPad(task->options, taglib_image).first);
|
||||
}
|
||||
}
|
||||
else if (cover_url.isLocalFile()) {
|
||||
|
||||
@@ -61,12 +61,13 @@ class AlbumCoverLoader : public QObject {
|
||||
void ExitAsync();
|
||||
void Stop() { stop_requested_ = true; }
|
||||
|
||||
static QString AlbumCoverFilename(QString artist, QString album);
|
||||
static QString AlbumCoverFilename(QString artist, QString album, const QString &extension);
|
||||
|
||||
QString CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id);
|
||||
QString CoverFilenameFromVariable(const QString &artist, const QString &album);
|
||||
QString CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url);
|
||||
QString CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url);
|
||||
QString CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension);
|
||||
QString CoverFilenameFromVariable(const QString &artist, const QString &album, const QString &extension = QString());
|
||||
QString CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString());
|
||||
|
||||
QString CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString());
|
||||
|
||||
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song);
|
||||
virtual quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QUrl &song_url = QUrl(), const Song song = Song(), const QImage &embedded_image = QImage());
|
||||
|
||||
@@ -250,6 +250,9 @@ void AlbumCoverManager::closeEvent(QCloseEvent *e) {
|
||||
// Cancel any outstanding requests
|
||||
CancelRequests();
|
||||
|
||||
ui_->artists->clear();
|
||||
ui_->albums->clear();
|
||||
|
||||
}
|
||||
|
||||
void AlbumCoverManager::LoadGeometry() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2020, 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
|
||||
@@ -26,5 +27,4 @@
|
||||
#include "core/application.h"
|
||||
#include "coverprovider.h"
|
||||
|
||||
CoverProvider::CoverProvider(const QString &name, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent)
|
||||
: QObject(parent), app_(app), name_(name), quality_(quality), fetchall_(fetchall), allow_missing_album_(allow_missing_album) {}
|
||||
CoverProvider::CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent) : QObject(parent), app_(app), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required), quality_(quality), fetchall_(fetchall), allow_missing_album_(allow_missing_album) {}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2020, 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
|
||||
@@ -25,7 +26,9 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "albumcoverfetcher.h"
|
||||
|
||||
@@ -37,27 +40,45 @@ class CoverProvider : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CoverProvider(const QString &name, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
|
||||
explicit CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
|
||||
|
||||
// A name (very short description) of this provider, like "last.fm".
|
||||
QString name() const { return name_; }
|
||||
bool is_enabled() const { return enabled_; }
|
||||
int order() const { return order_; }
|
||||
bool quality() const { return quality_; }
|
||||
bool fetchall() const { return fetchall_; }
|
||||
bool allow_missing_album() const { return allow_missing_album_; }
|
||||
|
||||
void set_enabled(const bool enabled) { enabled_ = enabled; }
|
||||
void set_order(const int order) { order_ = order; }
|
||||
|
||||
bool AuthenticationRequired() const { return authentication_required_; }
|
||||
virtual bool IsAuthenticated() const { return true; }
|
||||
virtual void Authenticate() {}
|
||||
virtual void Deauthenticate() {}
|
||||
|
||||
// Starts searching for covers matching the given query text.
|
||||
// Returns true if the query has been started, or false if an error occurred.
|
||||
// The provider should remember the ID and emit it along with the result when it finishes.
|
||||
virtual bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) = 0;
|
||||
virtual void CancelSearch(const int id) { Q_UNUSED(id); }
|
||||
|
||||
virtual void CancelSearch(int id) { Q_UNUSED(id); }
|
||||
virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0;
|
||||
|
||||
signals:
|
||||
void SearchFinished(int id, const CoverSearchResults& results);
|
||||
void AuthenticationComplete(bool, QStringList = QStringList());
|
||||
void AuthenticationSuccess();
|
||||
void AuthenticationFailure(QStringList);
|
||||
void SearchResults(int, CoverSearchResults);
|
||||
void SearchFinished(int, CoverSearchResults);
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
QString name_;
|
||||
bool enabled_;
|
||||
int order_;
|
||||
bool authentication_required_;
|
||||
float quality_;
|
||||
bool fetchall_;
|
||||
bool allow_missing_album_;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2020, 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
|
||||
@@ -22,13 +23,23 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QVariant>
|
||||
#include <QVariantList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QSettings>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "coverprovider.h"
|
||||
#include "coverproviders.h"
|
||||
|
||||
#include "settings/coverssettingspage.h"
|
||||
|
||||
int CoverProviders::NextOrderId = 0;
|
||||
|
||||
CoverProviders::CoverProviders(QObject *parent) : QObject(parent) {}
|
||||
|
||||
CoverProviders::~CoverProviders() {
|
||||
@@ -39,6 +50,48 @@ CoverProviders::~CoverProviders() {
|
||||
|
||||
}
|
||||
|
||||
void CoverProviders::ReloadSettings() {
|
||||
|
||||
QMap<int, QString> all_providers;
|
||||
for (CoverProvider *provider : cover_providers_.keys()) {
|
||||
if (!provider->is_enabled()) continue;
|
||||
all_providers.insert(provider->order(), provider->name());
|
||||
}
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(CoversSettingsPage::kSettingsGroup);
|
||||
QStringList providers_enabled = s.value("providers", QStringList() << all_providers.values()).toStringList();
|
||||
s.endGroup();
|
||||
|
||||
int i = 0;
|
||||
QList<CoverProvider*> providers;
|
||||
for (const QString &name : providers_enabled) {
|
||||
CoverProvider *provider = ProviderByName(name);
|
||||
if (provider) {
|
||||
provider->set_enabled(true);
|
||||
provider->set_order(++i);
|
||||
providers << provider;
|
||||
}
|
||||
}
|
||||
|
||||
for (CoverProvider *provider : cover_providers_.keys()) {
|
||||
if (!providers.contains(provider)) {
|
||||
provider->set_enabled(false);
|
||||
provider->set_order(++i);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CoverProvider *CoverProviders::ProviderByName(const QString &name) const {
|
||||
|
||||
for (CoverProvider *provider : cover_providers_.keys()) {
|
||||
if (provider->name() == name) return provider;
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
void CoverProviders::AddProvider(CoverProvider *provider) {
|
||||
|
||||
{
|
||||
@@ -47,6 +100,8 @@ void CoverProviders::AddProvider(CoverProvider *provider) {
|
||||
connect(provider, SIGNAL(destroyed()), SLOT(ProviderDestroyed()));
|
||||
}
|
||||
|
||||
provider->set_order(++NextOrderId);
|
||||
|
||||
qLog(Debug) << "Registered cover provider" << provider->name();
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2020, 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
|
||||
@@ -42,6 +43,10 @@ class CoverProviders : public QObject {
|
||||
explicit CoverProviders(QObject *parent = nullptr);
|
||||
~CoverProviders();
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
CoverProvider *ProviderByName(const QString &name) const;
|
||||
|
||||
// Lets a cover provider register itself in the repository.
|
||||
void AddProvider(CoverProvider *provider);
|
||||
void RemoveProvider(CoverProvider *provider);
|
||||
@@ -60,6 +65,8 @@ class CoverProviders : public QObject {
|
||||
private:
|
||||
Q_DISABLE_COPY(CoverProviders)
|
||||
|
||||
static int NextOrderId;
|
||||
|
||||
QMap<CoverProvider*, QString> cover_providers_;
|
||||
QMutex mutex_;
|
||||
|
||||
|
||||
@@ -45,13 +45,24 @@
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "deezercoverprovider.h"
|
||||
|
||||
const char *DeezerCoverProvider::kApiUrl = "https://api.deezer.com";
|
||||
const int DeezerCoverProvider::kLimit = 10;
|
||||
|
||||
DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): CoverProvider("Deezer", 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): JsonCoverProvider("Deezer", true, false, 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
DeezerCoverProvider::~DeezerCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
@@ -83,6 +94,7 @@ bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &albu
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); });
|
||||
|
||||
return true;
|
||||
@@ -139,36 +151,6 @@ QByteArray DeezerCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
QJsonObject DeezerCoverProvider::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.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;
|
||||
|
||||
}
|
||||
|
||||
QJsonValue DeezerCoverProvider::ExtractData(const QByteArray &data) {
|
||||
|
||||
@@ -204,6 +186,9 @@ QJsonValue DeezerCoverProvider::ExtractData(const QByteArray &data) {
|
||||
|
||||
void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
|
||||
@@ -23,23 +23,26 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
|
||||
class DeezerCoverProvider : public CoverProvider {
|
||||
class DeezerCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeezerCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~DeezerCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
@@ -48,7 +51,6 @@ class DeezerCoverProvider : public CoverProvider {
|
||||
|
||||
private:
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
QJsonValue ExtractData(const QByteArray &data);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
@@ -57,6 +59,7 @@ class DeezerCoverProvider : public CoverProvider {
|
||||
static const int kLimit;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -32,11 +32,13 @@
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QMap>
|
||||
#include <QQueue>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QTimer>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
@@ -50,41 +52,53 @@
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/utilities.h"
|
||||
#include "coverprovider.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "discogscoverprovider.h"
|
||||
|
||||
const char *DiscogsCoverProvider::kUrlSearch = "https://api.discogs.com/database/search";
|
||||
const char *DiscogsCoverProvider::kAccessKeyB64 = "dGh6ZnljUGJlZ1NEeXBuSFFxSVk=";
|
||||
const char *DiscogsCoverProvider::kSecretKeyB64 = "ZkFIcmlaSER4aHhRSlF2U3d0bm5ZVmdxeXFLWUl0UXI=";
|
||||
const int DiscogsCoverProvider::kRequestsDelay = 1000;
|
||||
|
||||
DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) : CoverProvider("Discogs", 0.0, false, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) :
|
||||
JsonCoverProvider("Discogs", false, false, 0.0, false, false, app, parent),
|
||||
network_(new NetworkAccessManager(this)), timer_flush_requests_(new QTimer(this)) {
|
||||
|
||||
timer_flush_requests_->setInterval(kRequestsDelay);
|
||||
timer_flush_requests_->setSingleShot(false);
|
||||
connect(timer_flush_requests_, SIGNAL(timeout()), this, SLOT(FlushRequests()));
|
||||
|
||||
}
|
||||
|
||||
DiscogsCoverProvider::~DiscogsCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
timer_flush_requests_->stop();
|
||||
queue_search_requests_.clear();
|
||||
queue_release_requests_.clear();
|
||||
requests_search_.clear();
|
||||
|
||||
}
|
||||
|
||||
bool DiscogsCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
|
||||
std::shared_ptr<DiscogsCoverSearchContext> search = std::make_shared<DiscogsCoverSearchContext>();
|
||||
std::shared_ptr<DiscogsCoverSearchContext> search = std::make_shared<DiscogsCoverSearchContext>(id, artist, album);
|
||||
|
||||
search->id = id;
|
||||
search->artist = artist;
|
||||
search->album = album;
|
||||
requests_search_.insert(id, search);
|
||||
requests_search_.insert(search->id, search);
|
||||
queue_search_requests_.enqueue(search);
|
||||
|
||||
ParamList params = ParamList() << Param("type", "release");
|
||||
if (!search->artist.isEmpty()) {
|
||||
params.append(Param("artist", search->artist.toLower()));
|
||||
if (!timer_flush_requests_->isActive()) {
|
||||
timer_flush_requests_->start();
|
||||
}
|
||||
if (!search->album.isEmpty()) {
|
||||
params.append(Param("release_title", search->album.toLower()));
|
||||
}
|
||||
|
||||
QNetworkReply *reply = CreateRequest(QUrl(kUrlSearch), params);
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); });
|
||||
|
||||
return true;
|
||||
|
||||
@@ -96,6 +110,42 @@ void DiscogsCoverProvider::CancelSearch(const int id) {
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::FlushRequests() {
|
||||
|
||||
if (!queue_release_requests_.isEmpty()) {
|
||||
SendReleaseRequest(queue_release_requests_.dequeue());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!queue_search_requests_.isEmpty()) {
|
||||
SendSearchRequest(queue_search_requests_.dequeue());
|
||||
return;
|
||||
}
|
||||
|
||||
timer_flush_requests_->stop();
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::SendSearchRequest(std::shared_ptr<DiscogsCoverSearchContext> search) {
|
||||
|
||||
ParamList params = ParamList() << Param("format", "album")
|
||||
<< Param("artist", search->artist.toLower())
|
||||
<< Param("release_title", search->album.toLower());
|
||||
|
||||
switch (search->type) {
|
||||
case DiscogsCoverType_Master:
|
||||
params << Param("type", "master");
|
||||
break;
|
||||
case DiscogsCoverType_Release:
|
||||
params << Param("type", "release");
|
||||
break;
|
||||
}
|
||||
|
||||
QNetworkReply *reply = CreateRequest(QUrl(kUrlSearch), params);
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, search->id); });
|
||||
|
||||
}
|
||||
|
||||
QNetworkReply *DiscogsCoverProvider::CreateRequest(QUrl url, const ParamList ¶ms_provided) {
|
||||
|
||||
ParamList params = ParamList() << Param("key", QByteArray::fromBase64(kAccessKeyB64))
|
||||
@@ -124,6 +174,9 @@ QNetworkReply *DiscogsCoverProvider::CreateRequest(QUrl url, const ParamList &pa
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
|
||||
qLog(Debug) << "Discogs: Sending request" << url;
|
||||
|
||||
return reply;
|
||||
|
||||
@@ -171,40 +224,14 @@ QByteArray DiscogsCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
}
|
||||
|
||||
QJsonObject DiscogsCoverProvider::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.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;
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(id)) {
|
||||
return;
|
||||
}
|
||||
if (!requests_search_.contains(id)) return;
|
||||
std::shared_ptr<DiscogsCoverSearchContext> search = requests_search_.value(id);
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
@@ -235,18 +262,13 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value_results.isArray()) {
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray array_results = value_results.toArray();
|
||||
if (array_results.isEmpty()) {
|
||||
EndSearch(search);
|
||||
return;
|
||||
QJsonArray array_results;
|
||||
if (value_results.isArray()) {
|
||||
array_results = value_results.toArray();
|
||||
}
|
||||
|
||||
for (const QJsonValue &value_result : array_results) {
|
||||
|
||||
if (!value_result.isObject()) {
|
||||
Error("Invalid Json reply, results value is not a object.", value_result);
|
||||
continue;
|
||||
@@ -258,72 +280,95 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
}
|
||||
quint64 release_id = obj_result["id"].toDouble();
|
||||
QUrl resource_url(obj_result["resource_url"].toString());
|
||||
if (!resource_url.isValid()) {
|
||||
continue;
|
||||
QString title = obj_result["title"].toString();
|
||||
|
||||
if (title.contains(" - ")) {
|
||||
QStringList title_splitted = title.split(" - ");
|
||||
if (title_splitted.count() == 2) {
|
||||
QString artist = title_splitted.first();
|
||||
title = title_splitted.last();
|
||||
if (artist.toLower() != search->artist.toLower() && title.toLower() != search->album.toLower()) continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resource_url.isValid()) continue;
|
||||
if (search->requests_release_.contains(release_id)) {
|
||||
continue;
|
||||
}
|
||||
StartRelease(search, release_id, resource_url);
|
||||
StartReleaseRequest(search, release_id, resource_url);
|
||||
}
|
||||
|
||||
if (search->requests_release_.count() <= 0) {
|
||||
EndSearch(search);
|
||||
if (search->requests_release_.count() == 0) {
|
||||
if (search->type == DiscogsCoverType_Master) {
|
||||
search->type = DiscogsCoverType_Release;
|
||||
queue_search_requests_.enqueue(search);
|
||||
}
|
||||
else {
|
||||
EndSearch(search);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::StartRelease(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id, const QUrl &url) {
|
||||
void DiscogsCoverProvider::StartReleaseRequest(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id, const QUrl &url) {
|
||||
|
||||
DiscogsCoverReleaseContext release(release_id, url);
|
||||
DiscogsCoverReleaseContext release(search->id, release_id, url);
|
||||
search->requests_release_.insert(release_id, release);
|
||||
queue_release_requests_.enqueue(release);
|
||||
|
||||
if (!timer_flush_requests_->isActive()) {
|
||||
timer_flush_requests_->start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::SendReleaseRequest(const DiscogsCoverReleaseContext release) {
|
||||
|
||||
QNetworkReply *reply = CreateRequest(release.url);
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleReleaseReply(reply, search->id, release.id); });
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleReleaseReply(reply, release.search_id, release.id); });
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int search_id, const quint64 release_id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(search_id)) {
|
||||
return;
|
||||
}
|
||||
if (!requests_search_.contains(search_id)) return;
|
||||
std::shared_ptr<DiscogsCoverSearchContext> search = requests_search_.value(search_id);
|
||||
|
||||
if (!search->requests_release_.contains(release_id)) {
|
||||
return;
|
||||
}
|
||||
if (!search->requests_release_.contains(release_id)) return;
|
||||
const DiscogsCoverReleaseContext &release = search->requests_release_.value(release_id);
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (data.isEmpty()) {
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) {
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("artists") || !json_obj.contains("title")) {
|
||||
Error("Json reply object is missing artists or title.", json_obj);
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("images")) {
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_artists = json_obj["artists"];
|
||||
if (!value_artists.isArray()) {
|
||||
Error("Json reply object artists is not a array.", value_artists);
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
QJsonArray array_artists = value_artists.toArray();
|
||||
@@ -345,28 +390,28 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
|
||||
}
|
||||
|
||||
if (artist.isEmpty()) {
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
if (i > 1 && artist != search->artist) artist = "Various artists";
|
||||
|
||||
QString album = json_obj["title"].toString();
|
||||
if (artist != search->artist && album != search->album) {
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_images = json_obj["images"];
|
||||
if (!value_images.isArray()) {
|
||||
Error("Json images is not an array.");
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
QJsonArray array_images = value_images.toArray();
|
||||
|
||||
if (array_images.isEmpty()) {
|
||||
Error("Invalid Json reply, images array is empty.");
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -390,28 +435,35 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
|
||||
if (width < 300 || height < 300) continue;
|
||||
const float aspect_score = 1.0 - float(std::max(width, height) - std::min(width, height)) / std::max(height, width);
|
||||
if (aspect_score < 0.85) continue;
|
||||
CoverSearchResult cover_result;
|
||||
cover_result.artist = artist;
|
||||
cover_result.album = album;
|
||||
cover_result.image_url = QUrl(obj_image["resource_url"].toString());
|
||||
if (cover_result.image_url.isEmpty()) continue;
|
||||
search->results.append(cover_result);
|
||||
CoverSearchResult result;
|
||||
result.artist = artist;
|
||||
result.album = album;
|
||||
result.image_url = QUrl(obj_image["resource_url"].toString());
|
||||
if (result.image_url.isEmpty()) continue;
|
||||
search->results.append(result);
|
||||
}
|
||||
|
||||
EndSearch(search, release);
|
||||
emit SearchResults(search->id, search->results);
|
||||
search->results.clear();
|
||||
|
||||
EndSearch(search, release.id);
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::EndSearch(std::shared_ptr<DiscogsCoverSearchContext> search, const DiscogsCoverReleaseContext &release) {
|
||||
void DiscogsCoverProvider::EndSearch(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id) {
|
||||
|
||||
if (search->requests_release_.contains(release.id)) {
|
||||
search->requests_release_.remove(release.id);
|
||||
if (search->requests_release_.contains(release_id)) {
|
||||
search->requests_release_.remove(release_id);
|
||||
}
|
||||
if (search->requests_release_.count() <= 0) {
|
||||
requests_search_.remove(search->id);
|
||||
emit SearchFinished(search->id, search->results);
|
||||
}
|
||||
|
||||
if (queue_release_requests_.isEmpty() && queue_search_requests_.isEmpty()) {
|
||||
timer_flush_requests_->stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
@@ -28,20 +28,24 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QMetaType>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QQueue>
|
||||
#include <QMap>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class QTimer;
|
||||
class Application;
|
||||
|
||||
class DiscogsCoverProvider : public CoverProvider {
|
||||
class DiscogsCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
@@ -49,24 +53,25 @@ class DiscogsCoverProvider : public CoverProvider {
|
||||
~DiscogsCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
|
||||
void CancelSearch(const int id);
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id);
|
||||
void HandleReleaseReply(QNetworkReply *reply, const int id, const quint64 release_id);
|
||||
enum DiscogsCoverType {
|
||||
DiscogsCoverType_Master,
|
||||
DiscogsCoverType_Release,
|
||||
};
|
||||
|
||||
public:
|
||||
struct DiscogsCoverReleaseContext {
|
||||
explicit DiscogsCoverReleaseContext(const quint64 _id = 0, const QUrl &_url = QUrl()) : id(_id), url(_url) {}
|
||||
explicit DiscogsCoverReleaseContext(const quint64 _search_id = 0, const quint64 _id = 0, const QUrl &_url = QUrl()) : search_id(_search_id), id(_id), url(_url) {}
|
||||
quint64 search_id;
|
||||
quint64 id;
|
||||
QUrl url;
|
||||
};
|
||||
struct DiscogsCoverSearchContext {
|
||||
explicit DiscogsCoverSearchContext() : id(-1) {}
|
||||
explicit DiscogsCoverSearchContext(const int _id = 0, const QString &_artist = QString(), const QString &_album = QString(), const DiscogsCoverType _type = DiscogsCoverType_Master) : id(_id), artist(_artist), album(_album), type(_type) {}
|
||||
int id;
|
||||
QString artist;
|
||||
QString album;
|
||||
DiscogsCoverType type;
|
||||
QMap<quint64, DiscogsCoverReleaseContext> requests_release_;
|
||||
CoverSearchResults results;
|
||||
};
|
||||
@@ -75,20 +80,31 @@ class DiscogsCoverProvider : public CoverProvider {
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
void SendSearchRequest(std::shared_ptr<DiscogsCoverSearchContext> search);
|
||||
void SendReleaseRequest(const DiscogsCoverReleaseContext release);
|
||||
QNetworkReply *CreateRequest(QUrl url, const ParamList ¶ms_provided = ParamList());
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
void StartRelease(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id, const QUrl &url);
|
||||
void EndSearch(std::shared_ptr<DiscogsCoverSearchContext> search, const DiscogsCoverReleaseContext &release = DiscogsCoverReleaseContext());
|
||||
void StartReleaseRequest(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id, const QUrl &url);
|
||||
void EndSearch(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id = 0);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private slots:
|
||||
void FlushRequests();
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id);
|
||||
void HandleReleaseReply(QNetworkReply *reply, const int id, const quint64 release_id);
|
||||
|
||||
private:
|
||||
static const char *kUrlSearch;
|
||||
static const char *kAccessKeyB64;
|
||||
static const char *kSecretKeyB64;
|
||||
static const int kRequestsDelay;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
QTimer *timer_flush_requests_;
|
||||
QQueue<std::shared_ptr<DiscogsCoverSearchContext>> queue_search_requests_;
|
||||
QQueue<DiscogsCoverReleaseContext> queue_release_requests_;
|
||||
QMap<int, std::shared_ptr<DiscogsCoverSearchContext>> requests_search_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
64
src/covermanager/jsoncoverprovider.cpp
Normal file
64
src/covermanager/jsoncoverprovider.cpp
Normal 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
JsonCoverProvider::JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent) : CoverProvider(name, enabled, authentication_required, quality, fetchall, allow_missing_album, app, parent) {}
|
||||
|
||||
QJsonObject JsonCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
Error(QString("Failed to parse json data: %1").arg(json_error.errorString()));
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", data);
|
||||
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;
|
||||
|
||||
}
|
||||
44
src/covermanager/jsoncoverprovider.h
Normal file
44
src/covermanager/jsoncoverprovider.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 JSONCOVERPROVIDER_H
|
||||
#define JSONCOVERPROVIDER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
|
||||
class Application;
|
||||
|
||||
class JsonCoverProvider : public CoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
|
||||
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
|
||||
};
|
||||
|
||||
#endif // JSONCOVERPROVIDER_H
|
||||
@@ -45,7 +45,7 @@
|
||||
#include "core/network.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "lastfmcoverprovider.h"
|
||||
|
||||
@@ -53,7 +53,18 @@ const char *LastFmCoverProvider::kUrl = "https://ws.audioscrobbler.com/2.0/";
|
||||
const char *LastFmCoverProvider::kApiKey = "211990b4c96782c05d1536e7219eb56e";
|
||||
const char *LastFmCoverProvider::kSecret = "80fd738f49596e9709b1bf9319c444a8";
|
||||
|
||||
LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : CoverProvider("last.fm", 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : JsonCoverProvider("Last.fm", true, false, 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
LastFmCoverProvider::~LastFmCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
@@ -100,6 +111,7 @@ bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &albu
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8());
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { QueryFinished(reply, id, type); });
|
||||
|
||||
return true;
|
||||
@@ -108,6 +120,9 @@ bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &albu
|
||||
|
||||
void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, const QString &type) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
@@ -303,36 +318,9 @@ QByteArray LastFmCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
}
|
||||
|
||||
QJsonObject LastFmCoverProvider::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.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;
|
||||
|
||||
}
|
||||
|
||||
void LastFmCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "LastFm:" << error;
|
||||
qLog(Error) << "Last.fm:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
|
||||
@@ -23,22 +23,25 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
|
||||
class LastFmCoverProvider : public CoverProvider {
|
||||
class LastFmCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LastFmCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~LastFmCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
|
||||
private slots:
|
||||
@@ -54,7 +57,6 @@ class LastFmCoverProvider : public CoverProvider {
|
||||
};
|
||||
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
LastFmImageSize ImageSizeFromString(const QString &size);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
@@ -64,6 +66,7 @@ class LastFmCoverProvider : public CoverProvider {
|
||||
static const char *kSecret;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -23,11 +23,13 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QQueue>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QTimer>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
@@ -41,20 +43,51 @@
|
||||
#include "core/network.h"
|
||||
#include "core/logging.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "musicbrainzcoverprovider.h"
|
||||
|
||||
const char *MusicbrainzCoverProvider::kReleaseSearchUrl = "https://musicbrainz.org/ws/2/release/";
|
||||
const char *MusicbrainzCoverProvider::kAlbumCoverUrl = "https://coverartarchive.org/release/%1/front";
|
||||
const int MusicbrainzCoverProvider::kLimit = 8;
|
||||
const int MusicbrainzCoverProvider::kRequestsDelay = 1000;
|
||||
|
||||
MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): CoverProvider("MusicBrainz", 1.5, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): JsonCoverProvider("MusicBrainz", true, false, 1.5, true, false, app, parent), network_(new NetworkAccessManager(this)), timer_flush_requests_(new QTimer(this)) {
|
||||
|
||||
timer_flush_requests_->setInterval(kRequestsDelay);
|
||||
timer_flush_requests_->setSingleShot(false);
|
||||
connect(timer_flush_requests_, SIGNAL(timeout()), this, SLOT(FlushRequests()));
|
||||
|
||||
}
|
||||
|
||||
MusicbrainzCoverProvider::~MusicbrainzCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
|
||||
QString query = QString("release:\"%1\" AND artist:\"%2\"").arg(album.trimmed().replace('"', "\\\"")).arg(artist.trimmed().replace('"', "\\\""));
|
||||
SearchRequest request(id, artist, album);
|
||||
queue_search_requests_ << request;
|
||||
|
||||
if (!timer_flush_requests_->isActive()) {
|
||||
timer_flush_requests_->start();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void MusicbrainzCoverProvider::SendSearchRequest(const SearchRequest &request) {
|
||||
|
||||
QString query = QString("release:\"%1\" AND artist:\"%2\"").arg(request.album.trimmed().replace('"', "\\\"")).arg(request.artist.trimmed().replace('"', "\\\""));
|
||||
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem("query", query);
|
||||
@@ -66,14 +99,27 @@ bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); });
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, request.id); });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MusicbrainzCoverProvider::FlushRequests() {
|
||||
|
||||
if (!queue_search_requests_.isEmpty()) {
|
||||
SendSearchRequest(queue_search_requests_.dequeue());
|
||||
return;
|
||||
}
|
||||
|
||||
timer_flush_requests_->stop();
|
||||
|
||||
}
|
||||
|
||||
void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int search_id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
@@ -218,33 +264,6 @@ QByteArray MusicbrainzCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
}
|
||||
|
||||
QJsonObject MusicbrainzCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error("Reply from server is missing Json data.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
if (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;
|
||||
|
||||
}
|
||||
|
||||
void MusicbrainzCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "Musicbrainz:" << error;
|
||||
|
||||
@@ -23,38 +23,55 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QQueue>
|
||||
#include <QByteArray>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class QTimer;
|
||||
class Application;
|
||||
|
||||
class MusicbrainzCoverProvider : public CoverProvider {
|
||||
class MusicbrainzCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MusicbrainzCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~MusicbrainzCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
|
||||
private slots:
|
||||
void FlushRequests();
|
||||
void HandleSearchReply(QNetworkReply *reply, const int search_id);
|
||||
|
||||
private:
|
||||
struct SearchRequest {
|
||||
explicit SearchRequest(const int _id, const QString &_artist, const QString &_album) : id(_id), artist(_artist), album(_album) {}
|
||||
int id;
|
||||
QString artist;
|
||||
QString album;
|
||||
};
|
||||
|
||||
void SendSearchRequest(const SearchRequest &request);
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private:
|
||||
static const char *kReleaseSearchUrl;
|
||||
static const char *kAlbumCoverUrl;
|
||||
static const int kLimit;
|
||||
static const int kRequestsDelay;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
QTimer *timer_flush_requests_;
|
||||
QQueue<SearchRequest> queue_search_requests_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
239
src/covermanager/musixmatchcoverprovider.cpp
Normal file
239
src/covermanager/musixmatchcoverprovider.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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 <QByteArray>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QTextCodec>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonObject>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "musixmatchcoverprovider.h"
|
||||
|
||||
MusixmatchCoverProvider::MusixmatchCoverProvider(Application *app, QObject *parent): JsonCoverProvider("Musixmatch", true, false, 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
MusixmatchCoverProvider::~MusixmatchCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool MusixmatchCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
|
||||
QString artist_stripped = artist;
|
||||
QString album_stripped = album;
|
||||
|
||||
artist_stripped = artist_stripped.replace('/', '-');
|
||||
artist_stripped = artist_stripped.remove(QRegExp("[^A-Za-z0-9\\- ]"));
|
||||
artist_stripped = artist_stripped.simplified();
|
||||
artist_stripped = artist_stripped.replace(' ', '-');
|
||||
artist_stripped = artist_stripped.replace(QRegExp("(-)\\1+"), "-");
|
||||
artist_stripped = artist_stripped.toLower();
|
||||
|
||||
album_stripped = album_stripped.replace('/', '-');
|
||||
album_stripped = album_stripped.remove(QRegExp("[^a-zA-Z0-9\\- ]"));
|
||||
album_stripped = album_stripped.simplified();
|
||||
album_stripped = album_stripped.replace(' ', '-').toLower();
|
||||
album_stripped = album_stripped.replace(QRegExp("(-)\\1+"), "-");
|
||||
album_stripped = album_stripped.toLower();
|
||||
|
||||
if (artist_stripped.isEmpty() || album_stripped.isEmpty()) return false;
|
||||
|
||||
QUrl url(QString("https://www.musixmatch.com/album/%1/%2").arg(artist_stripped).arg(album_stripped));
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, artist, album); });
|
||||
|
||||
//qLog(Debug) << "Musixmatch: Sending request for" << artist_stripped << album_stripped << url;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void MusixmatchCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); }
|
||||
|
||||
void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id, const QString &artist, const QString &album) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
Error("Empty reply received from server.");
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCodec *codec = QTextCodec::codecForName("utf-8");
|
||||
if (!codec) {
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
QString content = codec->toUnicode(data);
|
||||
|
||||
QString data_begin = "var __mxmState = ";
|
||||
QString data_end = ";</script>";
|
||||
int begin_idx = content.indexOf(data_begin);
|
||||
QString content_json;
|
||||
if (begin_idx > 0) {
|
||||
begin_idx += data_begin.length();
|
||||
int end_idx = content.indexOf(data_end, begin_idx);
|
||||
if (end_idx > begin_idx) {
|
||||
content_json = content.mid(begin_idx, end_idx - begin_idx);
|
||||
}
|
||||
}
|
||||
|
||||
if (content_json.isEmpty()) {
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
if (content_json.contains(QRegExp("<[^>]*>"))) { // Make sure it's not HTML code.
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(content_json.toUtf8(), &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error(QString("Failed to parse json data: %1").arg(error.errorString()));
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", data);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error("Json document is not an object.", json_doc);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error("Received empty Json object.", json_doc);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("page") || !json_obj["page"].isObject()) {
|
||||
Error("Json reply is missing page object.", json_obj);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
json_obj = json_obj["page"].toObject();
|
||||
|
||||
if (!json_obj.contains("album") || !json_obj["album"].isObject()) {
|
||||
Error("Json page object is missing album object.", json_obj);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_album = json_obj["album"].toObject();
|
||||
|
||||
if (!obj_album.contains("artistName") || !obj_album.contains("name")) {
|
||||
Error("Json album object is missing artistName or name.", obj_album);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QString cover;
|
||||
|
||||
if (obj_album.contains("coverart800x800")) {
|
||||
cover = obj_album["coverart800x800"].toString();
|
||||
}
|
||||
else if (obj_album.contains("coverart500x500")) {
|
||||
cover = obj_album["coverart500x500"].toString();
|
||||
}
|
||||
else if (obj_album.contains("coverart350x350")) {
|
||||
cover = obj_album["coverart350x350"].toString();
|
||||
}
|
||||
|
||||
if (cover.isEmpty()) {
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
QUrl cover_url(cover);
|
||||
if (!cover_url.isValid()) {
|
||||
Error("Received cover url is not valid.", cover);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
CoverSearchResult result;
|
||||
result.artist = obj_album["artistName"].toString();
|
||||
result.album = obj_album["name"].toString();
|
||||
result.image_url = cover_url;
|
||||
|
||||
if (artist.toLower() == result.artist.toLower() || album.toLower() == result.album.toLower()) {
|
||||
results.append(result);
|
||||
}
|
||||
|
||||
emit SearchFinished(id, results);
|
||||
|
||||
}
|
||||
|
||||
void MusixmatchCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "Musixmatch:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
58
src/covermanager/musixmatchcoverprovider.h
Normal file
58
src/covermanager/musixmatchcoverprovider.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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 MUSIXMATCHCOVERPROVIDER_H
|
||||
#define MUSIXMATCHCOVERPROVIDER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
class MusixmatchCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MusixmatchCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~MusixmatchCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
private:
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id, const QString &artist, const QString &album);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // MUSIXMATCHCOVERPROVIDER_H
|
||||
@@ -44,13 +44,25 @@
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "qobuzcoverprovider.h"
|
||||
|
||||
const char *QobuzCoverProvider::kApiUrl = "https://www.qobuz.com/api.json/0.2";
|
||||
const char *QobuzCoverProvider::kAppID = "OTQyODUyNTY3";
|
||||
const int QobuzCoverProvider::kLimit = 10;
|
||||
|
||||
QobuzCoverProvider::QobuzCoverProvider(Application *app, QObject *parent) : CoverProvider("Qobuz", 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
QobuzCoverProvider::QobuzCoverProvider(Application *app, QObject *parent) : JsonCoverProvider("Qobuz", true, false, 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
QobuzCoverProvider::~QobuzCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
@@ -87,6 +99,7 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
req.setRawHeader("X-App-Id", kAppID);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); });
|
||||
|
||||
return true;
|
||||
@@ -139,38 +152,11 @@ QByteArray QobuzCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
}
|
||||
|
||||
QJsonObject QobuzCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
Error("Reply from server missing Json data.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", data);
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
|
||||
@@ -23,22 +23,25 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
|
||||
class QobuzCoverProvider : public CoverProvider {
|
||||
class QobuzCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QobuzCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~QobuzCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
@@ -47,7 +50,6 @@ class QobuzCoverProvider : public CoverProvider {
|
||||
|
||||
private:
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private:
|
||||
@@ -56,6 +58,7 @@ class QobuzCoverProvider : public CoverProvider {
|
||||
static const int kLimit;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
551
src/covermanager/spotifycoverprovider.cpp
Normal file
551
src/covermanager/spotifycoverprovider.cpp
Normal file
@@ -0,0 +1,551 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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 <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QDateTime>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QSslError>
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDesktopServices>
|
||||
#include <QMessageBox>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/network.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "internet/localredirectserver.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "spotifycoverprovider.h"
|
||||
|
||||
const char *SpotifyCoverProvider::kSettingsGroup = "Spotify";
|
||||
const char *SpotifyCoverProvider::kOAuthAuthorizeUrl = "https://accounts.spotify.com/authorize";
|
||||
const char *SpotifyCoverProvider::kOAuthAccessTokenUrl = "https://accounts.spotify.com/api/token";
|
||||
const char *SpotifyCoverProvider::kOAuthRedirectUrl = "http://localhost:63111/";
|
||||
const char *SpotifyCoverProvider::kClientIDB64 = "ZTZjY2Y2OTQ5NzY1NGE3NThjOTAxNWViYzdiMWQzMTc=";
|
||||
const char *SpotifyCoverProvider::kClientSecretB64 = "N2ZlMDMxODk1NTBlNDE3ZGI1ZWQ1MzE3ZGZlZmU2MTE=";
|
||||
const char *SpotifyCoverProvider::kApiUrl = "https://api.spotify.com/v1";
|
||||
const int SpotifyCoverProvider::kLimit = 10;
|
||||
|
||||
SpotifyCoverProvider::SpotifyCoverProvider(Application *app, QObject *parent) : JsonCoverProvider("Spotify", true, true, 2.5, true, true, app, parent), network_(new NetworkAccessManager(this)), server_(nullptr), expires_in_(0), login_time_(0) {
|
||||
|
||||
refresh_login_timer_.setSingleShot(true);
|
||||
connect(&refresh_login_timer_, SIGNAL(timeout()), SLOT(RequestAccessToken()));
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
access_token_ = s.value("access_token").toString();
|
||||
refresh_token_ = s.value("refresh_token").toString();
|
||||
expires_in_ = s.value("expires_in").toLongLong();
|
||||
login_time_ = s.value("login_time").toLongLong();
|
||||
s.endGroup();
|
||||
|
||||
if (!refresh_token_.isEmpty()) {
|
||||
qint64 time = expires_in_ - (QDateTime::currentDateTime().toTime_t() - login_time_);
|
||||
if (time < 6) time = 6;
|
||||
refresh_login_timer_.setInterval(time * kMsecPerSec);
|
||||
refresh_login_timer_.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SpotifyCoverProvider::~SpotifyCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::Authenticate() {
|
||||
|
||||
QUrl redirect_url(kOAuthRedirectUrl);
|
||||
|
||||
if (!server_) {
|
||||
server_ = new LocalRedirectServer(this);
|
||||
server_->set_https(false);
|
||||
int port = redirect_url.port();
|
||||
int port_max = port + 10;
|
||||
bool success = false;
|
||||
forever {
|
||||
server_->set_port(port);
|
||||
if (server_->Listen()) { success = true; break; }
|
||||
++port;
|
||||
if (port > port_max) break;
|
||||
}
|
||||
if (!success) {
|
||||
AuthError(server_->error());
|
||||
server_->deleteLater();
|
||||
server_ = nullptr;
|
||||
return;
|
||||
}
|
||||
connect(server_, SIGNAL(Finished()), this, SLOT(RedirectArrived()));
|
||||
}
|
||||
|
||||
code_verifier_ = Utilities::CryptographicRandomString(44);
|
||||
code_challenge_ = QString(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding));
|
||||
if (code_challenge_.lastIndexOf(QChar('=')) == code_challenge_.length() - 1) {
|
||||
code_challenge_.chop(1);
|
||||
}
|
||||
|
||||
const ParamList params = ParamList() << Param("client_id", QByteArray::fromBase64(kClientIDB64))
|
||||
<< Param("response_type", "code")
|
||||
<< Param("redirect_uri", redirect_url.toString())
|
||||
<< Param("state", code_challenge_);
|
||||
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl url(kOAuthAuthorizeUrl);
|
||||
url.setQuery(url_query);
|
||||
|
||||
const bool result = QDesktopServices::openUrl(url);
|
||||
if (!result) {
|
||||
QMessageBox messagebox(QMessageBox::Information, tr("Spotify Authentication"), tr("Please open this URL in your browser") + QString(":<br /><a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
|
||||
messagebox.setTextFormat(Qt::RichText);
|
||||
messagebox.exec();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::Deauthenticate() {
|
||||
|
||||
access_token_.clear();
|
||||
refresh_token_.clear();
|
||||
expires_in_ = 0;
|
||||
login_time_ = 0;
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.remove("access_token");
|
||||
s.remove("refresh_token");
|
||||
s.remove("expires_in");
|
||||
s.remove("login_time");
|
||||
s.endGroup();
|
||||
|
||||
refresh_login_timer_.stop();
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::RedirectArrived() {
|
||||
|
||||
if (!server_) return;
|
||||
|
||||
if (server_->error().isEmpty()) {
|
||||
QUrl url = server_->request_url();
|
||||
if (url.isValid()) {
|
||||
QUrlQuery url_query(url);
|
||||
if (url_query.hasQueryItem("error")) {
|
||||
AuthError(QUrlQuery(url).queryItemValue("error"));
|
||||
}
|
||||
else if (url_query.hasQueryItem("code") && url_query.hasQueryItem("state")) {
|
||||
qLog(Debug) << "Spotify: Authorization URL Received" << url;
|
||||
QString code = url_query.queryItemValue("code");
|
||||
QString state = url_query.queryItemValue("state");
|
||||
QUrl redirect_url(kOAuthRedirectUrl);
|
||||
redirect_url.setPort(server_->url().port());
|
||||
RequestAccessToken(code, redirect_url);
|
||||
}
|
||||
else {
|
||||
AuthError(tr("Redirect missing token code or state!"));
|
||||
}
|
||||
}
|
||||
else {
|
||||
AuthError(tr("Received invalid reply from web browser."));
|
||||
}
|
||||
}
|
||||
else {
|
||||
AuthError(server_->error());
|
||||
}
|
||||
|
||||
server_->close();
|
||||
server_->deleteLater();
|
||||
server_ = nullptr;
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::RequestAccessToken(const QString code, const QUrl redirect_url) {
|
||||
|
||||
refresh_login_timer_.stop();
|
||||
|
||||
ParamList params = ParamList() << Param("client_id", QByteArray::fromBase64(kClientIDB64))
|
||||
<< Param("client_secret", QByteArray::fromBase64(kClientSecretB64));
|
||||
|
||||
if (!code.isEmpty() && !redirect_url.isEmpty()) {
|
||||
params << Param("grant_type", "authorization_code");
|
||||
params << Param("code", code);
|
||||
params << Param("redirect_uri", redirect_url.toString());
|
||||
}
|
||||
else if (!refresh_token_.isEmpty() && is_enabled()) {
|
||||
params << Param("grant_type", "refresh_token");
|
||||
params << Param("refresh_token", refresh_token_);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl new_url(kOAuthAccessTokenUrl);
|
||||
QNetworkRequest req = QNetworkRequest(new_url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
QString auth_header_data = QByteArray::fromBase64(kClientIDB64) + QString(":") + QByteArray::fromBase64(kClientSecretB64);
|
||||
req.setRawHeader("Authorization", "Basic " + auth_header_data.toUtf8().toBase64());
|
||||
|
||||
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
|
||||
|
||||
QNetworkReply *reply = network_->post(req, query);
|
||||
replies_ << reply;
|
||||
connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(HandleLoginSSLErrors(QList<QSslError>)));
|
||||
connect(reply, &QNetworkReply::finished, [=] { AccessTokenRequestFinished(reply); });
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::HandleLoginSSLErrors(QList<QSslError> ssl_errors) {
|
||||
|
||||
for (const QSslError &ssl_error : ssl_errors) {
|
||||
login_errors_ += ssl_error.errorString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::AccessTokenRequestFinished(QNetworkReply *reply) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
AuthError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "error" and "error_description" then use that instead.
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("error") && json_obj.contains("error_description")) {
|
||||
QString error = json_obj["error"].toString();
|
||||
QString error_description = json_obj["error_description"].toString();
|
||||
login_errors_ << QString("Authentication failure: %1 (%2)").arg(error).arg(error_description);
|
||||
}
|
||||
}
|
||||
if (login_errors_.isEmpty()) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
else {
|
||||
login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||
}
|
||||
}
|
||||
AuthError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
Error(QString("Failed to parse Json data in authentication reply: %1").arg(json_error.errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
AuthError("Authentication reply from server has empty Json document.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
AuthError("Authentication reply from server has Json document that is not an object.", json_doc);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
AuthError("Authentication reply from server has empty Json object.", json_doc);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("access_token") || !json_obj.contains("expires_in")) {
|
||||
AuthError("Authentication reply from server is missing access token or expires in.", json_obj);
|
||||
return;
|
||||
}
|
||||
|
||||
access_token_ = json_obj["access_token"].toString();
|
||||
if (json_obj.contains("refresh_token")) {
|
||||
refresh_token_ = json_obj["refresh_token"].toString();
|
||||
}
|
||||
expires_in_ = json_obj["expires_in"].toInt();
|
||||
login_time_ = QDateTime::currentDateTime().toTime_t();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("access_token", access_token_);
|
||||
s.setValue("refresh_token", refresh_token_);
|
||||
s.setValue("expires_in", expires_in_);
|
||||
s.setValue("login_time", login_time_);
|
||||
s.endGroup();
|
||||
|
||||
if (expires_in_ > 0) {
|
||||
refresh_login_timer_.setInterval(expires_in_ * kMsecPerSec);
|
||||
refresh_login_timer_.start();
|
||||
}
|
||||
|
||||
qLog(Debug) << "Spotify: Authentication was successful, got access token" << access_token_ << "expires in" << expires_in_;
|
||||
|
||||
emit AuthenticationComplete(true);
|
||||
emit AuthenticationSuccess();
|
||||
|
||||
}
|
||||
|
||||
bool SpotifyCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
if (access_token_.isEmpty()) return false;
|
||||
|
||||
QString type;
|
||||
QString query;
|
||||
QString extract;
|
||||
if (album.isEmpty()) {
|
||||
type = "track";
|
||||
query = artist + " " + title;
|
||||
extract = "tracks";
|
||||
}
|
||||
else {
|
||||
type = "album";
|
||||
query = artist + " " + album;
|
||||
extract = "albums";
|
||||
}
|
||||
|
||||
ParamList params = ParamList() << Param("q", query)
|
||||
<< Param("type", type)
|
||||
<< Param("limit", QString::number(kLimit));
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl url(kApiUrl + QString("/search"));
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
req.setRawHeader("Authorization", "Bearer " + access_token_.toUtf8());
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, extract); });
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); }
|
||||
|
||||
QByteArray SpotifyCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
QByteArray data;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
|
||||
data = reply->readAll();
|
||||
}
|
||||
else {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
else {
|
||||
data = reply->readAll();
|
||||
QJsonParseError parse_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
|
||||
QString error;
|
||||
if (parse_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("error") && json_obj["error"].isObject()) {
|
||||
QJsonObject obj_error = json_obj["error"].toObject();
|
||||
if (obj_error.contains("status") && obj_error.contains("message")) {
|
||||
int status = obj_error["status"].toInt();
|
||||
QString message = obj_error["message"].toString();
|
||||
error = QString("%1 (%2)").arg(message).arg(status);
|
||||
if (status == 401) access_token_.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (error.isEmpty()) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
if (reply->error() == 204) access_token_.clear();
|
||||
error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
else {
|
||||
error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||
}
|
||||
}
|
||||
Error(error);
|
||||
}
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id, const QString &extract) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (data.isEmpty()) {
|
||||
emit SearchFinished(id, CoverSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) {
|
||||
emit SearchFinished(id, CoverSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains(extract) || !json_obj[extract].isObject()) {
|
||||
Error(QString("Json object is missing %1 object.").arg(extract), json_obj);
|
||||
emit SearchFinished(id, CoverSearchResults());
|
||||
return;
|
||||
}
|
||||
json_obj = json_obj[extract].toObject();
|
||||
|
||||
if (!json_obj.contains("items") || !json_obj["items"].isArray()) {
|
||||
Error(QString("%1 object is missing items array.").arg(extract), json_obj);
|
||||
emit SearchFinished(id, CoverSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray array_items = json_obj["items"].toArray();
|
||||
if (array_items.isEmpty()) {
|
||||
emit SearchFinished(id, CoverSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
CoverSearchResults results;
|
||||
for (const QJsonValue &value_item : array_items) {
|
||||
|
||||
if (!value_item.isObject()) {
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_item = value_item.toObject();
|
||||
|
||||
QJsonObject obj_album = obj_item;
|
||||
if (obj_item.contains("album") && obj_item["album"].isObject()) {
|
||||
obj_album = obj_item["album"].toObject();
|
||||
}
|
||||
|
||||
if (!obj_album.contains("artists") || !obj_album.contains("name") || !obj_album.contains("images") || !obj_album["artists"].isArray() || !obj_album["images"].isArray()) {
|
||||
continue;
|
||||
}
|
||||
QJsonArray array_artists = obj_album["artists"].toArray();
|
||||
QJsonArray array_images = obj_album["images"].toArray();
|
||||
QString album = obj_album["name"].toString();
|
||||
|
||||
QStringList artists;
|
||||
for (const QJsonValue &value_artist : array_artists) {
|
||||
if (!value_artist.isObject()) continue;
|
||||
QJsonObject obj_artist = value_artist.toObject();
|
||||
if (!obj_artist.contains("name")) continue;
|
||||
artists << obj_artist["name"].toString();
|
||||
}
|
||||
|
||||
for (const QJsonValue &value_image : array_images) {
|
||||
if (!value_image.isObject()) continue;
|
||||
QJsonObject obj_image = value_image.toObject();
|
||||
if (!obj_image.contains("url") || !obj_image.contains("width") || !obj_image.contains("height")) continue;
|
||||
int width = obj_image["width"].toInt();
|
||||
int height = obj_image["height"].toInt();
|
||||
if (width < 300 || height < 300) continue;
|
||||
QUrl url(obj_image["url"].toString());
|
||||
CoverSearchResult result;
|
||||
result.album = album;
|
||||
result.image_url = url;
|
||||
if (!artists.isEmpty()) result.artist = artists.first();
|
||||
results << result;
|
||||
}
|
||||
|
||||
}
|
||||
emit SearchFinished(id, results);
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::AuthError(const QString &error, const QVariant &debug) {
|
||||
|
||||
if (!error.isEmpty()) login_errors_ << error;
|
||||
|
||||
for (const QString &e : login_errors_) Error(e);
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
emit AuthenticationFailure(login_errors_);
|
||||
emit AuthenticationComplete(false, login_errors_);
|
||||
|
||||
login_errors_.clear();
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "Spotify:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
97
src/covermanager/spotifycoverprovider.h
Normal file
97
src/covermanager/spotifycoverprovider.h
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 SPOTIFYCOVERPROVIDER_H
|
||||
#define SPOTIFYCOVERPROVIDER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QSslError>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
class LocalRedirectServer;
|
||||
|
||||
class SpotifyCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SpotifyCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~SpotifyCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
void Authenticate();
|
||||
void Deauthenticate();
|
||||
bool IsAuthenticated() const { return !access_token_.isEmpty(); }
|
||||
|
||||
private slots:
|
||||
void HandleLoginSSLErrors(QList<QSslError> ssl_errors);
|
||||
void RedirectArrived();
|
||||
void RequestAccessToken(const QString code = QString(), const QUrl redirect_url = QUrl());
|
||||
void AccessTokenRequestFinished(QNetworkReply *reply);
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id, const QString &extract);
|
||||
|
||||
private:
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
void AuthError(const QString &error = QString(), const QVariant &debug = QVariant());
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private:
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
static const char *kClientIDB64;
|
||||
static const char *kClientSecretB64;
|
||||
static const char *kOAuthAuthorizeUrl;
|
||||
static const char *kOAuthAccessTokenUrl;
|
||||
static const char *kOAuthRedirectUrl;
|
||||
static const char *kApiUrl;
|
||||
static const int kLimit;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
LocalRedirectServer *server_;
|
||||
QStringList login_errors_;
|
||||
QString code_verifier_;
|
||||
QString code_challenge_;
|
||||
QString access_token_;
|
||||
QString refresh_token_;
|
||||
quint64 expires_in_;
|
||||
quint64 login_time_;
|
||||
QTimer refresh_login_timer_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // SPOTIFYCOVERPROVIDER_H
|
||||
@@ -45,7 +45,7 @@
|
||||
#include "internet/internetservices.h"
|
||||
#include "tidal/tidalservice.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "tidalcoverprovider.h"
|
||||
|
||||
const char *TidalCoverProvider::kApiUrl = "https://api.tidalhifi.com/v1";
|
||||
@@ -53,12 +53,23 @@ const char *TidalCoverProvider::kResourcesUrl = "https://resources.tidal.com";
|
||||
const int TidalCoverProvider::kLimit = 10;
|
||||
|
||||
TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) :
|
||||
CoverProvider("Tidal", 2.5, true, true, app, parent),
|
||||
JsonCoverProvider("Tidal", true, true, 2.5, true, true, app, parent),
|
||||
service_(app->internet_services()->Service<TidalService>()),
|
||||
network_(new NetworkAccessManager(this)) {
|
||||
|
||||
}
|
||||
|
||||
TidalCoverProvider::~TidalCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
@@ -95,6 +106,7 @@ bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album
|
||||
if (!service_->session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", service_->session_id().toUtf8());
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); });
|
||||
|
||||
return true;
|
||||
@@ -152,38 +164,11 @@ QByteArray TidalCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
}
|
||||
|
||||
QJsonObject TidalCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
Error("Reply from server missing Json data.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", data);
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
|
||||
@@ -32,27 +32,31 @@
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "tidal/tidalservice.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
class TidalService;
|
||||
|
||||
class TidalCoverProvider : public CoverProvider {
|
||||
class TidalCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TidalCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~TidalCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
bool IsAuthenticated() const { return service_ && service_->authenticated(); }
|
||||
void Deauthenticate() { if (service_) service_->Logout(); }
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id);
|
||||
|
||||
private:
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private:
|
||||
@@ -62,6 +66,7 @@ class TidalCoverProvider : public CoverProvider {
|
||||
|
||||
TidalService *service_;
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -77,9 +77,7 @@ void CddaSongLoader::LoadSongs() {
|
||||
if (error) {
|
||||
Error(QString("%1: %2").arg(error->code).arg(error->message));
|
||||
}
|
||||
if (cdda_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!cdda_) return;
|
||||
|
||||
if (!url_.isEmpty()) {
|
||||
g_object_set(cdda_, "device", g_strdup(url_.path().toLocal8Bit().constData()), nullptr);
|
||||
@@ -242,7 +240,7 @@ void CddaSongLoader::AudioCDTagsLoaded(const QString &artist, const QString &alb
|
||||
|
||||
bool CddaSongLoader::HasChanged() {
|
||||
|
||||
if ((cdio_ && cdda_) && cdio_get_media_changed(cdio_) != 1) {
|
||||
if (cdio_ && cdio_get_media_changed(cdio_) != 1) {
|
||||
return false;
|
||||
}
|
||||
// Check if mutex is already token (i.e. init is already taking place)
|
||||
@@ -250,6 +248,7 @@ bool CddaSongLoader::HasChanged() {
|
||||
return false;
|
||||
}
|
||||
mutex_load_.unlock();
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ ConnectedDevice::ConnectedDevice(const QUrl &url, DeviceLister *lister, const QS
|
||||
backend_->moveToThread(app_->database()->thread());
|
||||
qLog(Debug) << backend_ << "for device" << unique_id_ << "moved to thread" << app_->database()->thread();
|
||||
|
||||
connect(backend_, SIGNAL(TotalSongCountUpdated(int)), SLOT(BackendTotalSongCountUpdated(int)));
|
||||
if (url_.scheme() != "cdda") {
|
||||
connect(backend_, SIGNAL(TotalSongCountUpdated(int)), SLOT(BackendTotalSongCountUpdated(int)));
|
||||
}
|
||||
|
||||
backend_->Init(app_->database(),
|
||||
Song::Source_Device,
|
||||
|
||||
@@ -49,11 +49,6 @@ About::About(QWidget *parent):QDialog(parent) {
|
||||
<< Person("Gavin D. Howard", "yzena.tech@gmail.com")
|
||||
<< Person("Martin Delille", "martin@delille.org");
|
||||
|
||||
strawberry_thanks_ \
|
||||
<< Person("Robert-André Mauchin", "eclipseo@fedoraproject.org")
|
||||
<< Person("Thomas Pierson", "contact@thomaspierson.fr")
|
||||
<< Person("Fabio Loli", "fabio.lolix@gmail.com");
|
||||
|
||||
clementine_authors_
|
||||
<< Person("David Sansome", "me@davidsansome.com")
|
||||
<< Person("John Maguire", "john.maguire@gmail.com")
|
||||
@@ -61,13 +56,10 @@ About::About(QWidget *parent):QDialog(parent) {
|
||||
<< Person("Arnaud Bienner", "arnaud.bienner@gmail.com");
|
||||
|
||||
clementine_contributors_ \
|
||||
<< Person("Mark Kretschmann", "kretschmann@kde.org")
|
||||
<< Person("Max Howell", "max.howell@methylblue.com")
|
||||
<< Person("Jakub Stachowski", "qbast@go2.pl")
|
||||
<< Person("Paul Cifarelli", "paul@cifarelli.net")
|
||||
<< Person("Felipe Rivera", "liebremx@users.sourceforge.net")
|
||||
<< Person("Alexander Peitz")
|
||||
<< Person("Artur Rona", "artur.rona@gmail.com")
|
||||
<< Person("Andreas Muttscheller", "asfa194@gmail.com")
|
||||
<< Person("Mark Furneaux", "mark@furneaux.ca")
|
||||
<< Person("Florian Bigard", "florian.bigard@gmail.com")
|
||||
@@ -85,6 +77,14 @@ About::About(QWidget *parent):QDialog(parent) {
|
||||
<< Person("Valeriy Malov", "jazzvoid@gmail.com")
|
||||
<< Person("Nick Lanham", "nick@afternight.org");
|
||||
|
||||
strawberry_thanks_ \
|
||||
<< Person("Mark Kretschmann", "kretschmann@kde.org")
|
||||
<< Person("Max Howell", "max.howell@methylblue.com")
|
||||
<< Person("Artur Rona", "artur.rona@gmail.com")
|
||||
<< Person("Robert-André Mauchin", "eclipseo@fedoraproject.org")
|
||||
<< Person("Thomas Pierson", "contact@thomaspierson.fr")
|
||||
<< Person("Fabio Loli", "fabio.lolix@gmail.com");
|
||||
|
||||
QString Title(tr("About Strawberry"));
|
||||
|
||||
QFont title_font;
|
||||
@@ -95,16 +95,28 @@ About::About(QWidget *parent):QDialog(parent) {
|
||||
|
||||
ui_.label_title->setFont(title_font);
|
||||
ui_.label_title->setText(Title);
|
||||
ui_.label_title->adjustSize();
|
||||
|
||||
ui_.label_text->setText(MainHtml());
|
||||
ui_.label_text->adjustSize();
|
||||
|
||||
ui_.text_contributors->setText(ContributorsHtml());
|
||||
ui_.text_contributors->updateGeometry();
|
||||
updateGeometry();
|
||||
|
||||
ui_.buttonBox->button(QDialogButtonBox::Close)->setShortcut(QKeySequence::Close);
|
||||
|
||||
}
|
||||
|
||||
void About::showEvent(QShowEvent*) {
|
||||
|
||||
setMinimumHeight(0);
|
||||
setMaximumHeight(9000);
|
||||
adjustSize();
|
||||
// Set fixed height and workaround bottom spacer taking up to much space.
|
||||
setFixedHeight(height() - ui_.spacer_bottom->geometry().height() + 15);
|
||||
adjustSize();
|
||||
|
||||
}
|
||||
|
||||
QString About::MainHtml() const {
|
||||
|
||||
QString ret;
|
||||
@@ -116,12 +128,19 @@ QString About::MainHtml() const {
|
||||
ret += "<p>";
|
||||
ret += tr("Strawberry is a music player and music collection organizer.");
|
||||
ret += "<br />";
|
||||
ret += tr("It is a fork of Clementine released in 2018 aimed at music collectors, audio enthusiasts and audiophiles.");
|
||||
ret += "<br />";
|
||||
ret += tr("The name is inspired by the band Strawbs. It's based on a heavily modified version of Clementine created in 2012-2013. It's written in C++ and Qt 5.");
|
||||
ret += tr("It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles.");
|
||||
ret += "</p>";
|
||||
|
||||
ret += "<p>";
|
||||
ret += tr("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.");
|
||||
ret += tr("Strawberry is free software released under GPL. The source code is available on %1").arg("<a href=\"https://github.com/strawberrymusicplayer/strawberry\">GitHub</a>.");
|
||||
ret += "<br />";
|
||||
ret += tr("You should have received a copy of the GNU General Public License along with this program. If not, see %1").arg("<a href=\"http://www.gnu.org/licenses/\">http://www.gnu.org/licenses/</a>");
|
||||
ret += "</p>";
|
||||
|
||||
ret += "<p>";
|
||||
ret += tr("If you like this Strawberry and can make use of it, consider sponsoring or donating.");
|
||||
ret += "<br />";
|
||||
ret += tr("You can sponsor the author on %1. You can also make a one-time payment through %2.").arg("<a href=\"https://github.com/sponsors/jonaski\">GitHub sponsors</a>.").arg("<a href=\"https://paypal.me/jonaskvinge\">paypal.me/jonaskvinge</a>");
|
||||
ret += "</p>";
|
||||
|
||||
return ret;
|
||||
@@ -134,7 +153,7 @@ QString About::ContributorsHtml() const {
|
||||
|
||||
ret += "<p>";
|
||||
ret += "<b>";
|
||||
ret += tr("Maintainer");
|
||||
ret += tr("Author and maintainer");
|
||||
ret += "</b>";
|
||||
for (const Person &person : strawberry_authors_) {
|
||||
ret += "<br />" + PersonToHtml(person);
|
||||
@@ -150,15 +169,6 @@ QString About::ContributorsHtml() const {
|
||||
}
|
||||
ret += "</p>";
|
||||
|
||||
ret += "<p>";
|
||||
ret += "<b>";
|
||||
ret += tr("Thanks to");
|
||||
ret += "</b>";
|
||||
for (const Person &person : strawberry_thanks_) {
|
||||
ret += "<br />" + PersonToHtml(person);
|
||||
}
|
||||
ret += "</p>";
|
||||
|
||||
ret += "<p>";
|
||||
ret += "<b>";
|
||||
ret += tr("Clementine authors");
|
||||
@@ -177,6 +187,15 @@ QString About::ContributorsHtml() const {
|
||||
}
|
||||
ret += "</p>";
|
||||
|
||||
ret += "<p>";
|
||||
ret += "<b>";
|
||||
ret += tr("Thanks to");
|
||||
ret += "</b>";
|
||||
for (const Person &person : strawberry_thanks_) {
|
||||
ret += "<br />" + PersonToHtml(person);
|
||||
}
|
||||
ret += "</p>";
|
||||
|
||||
ret += "<p>";
|
||||
ret += tr("Thanks to all the other Amarok and Clementine contributors.");
|
||||
ret += "</p>";
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "ui_about.h"
|
||||
|
||||
class QWidget;
|
||||
class QShowEvent;
|
||||
|
||||
class About : public QDialog {
|
||||
Q_OBJECT
|
||||
@@ -39,6 +40,10 @@ class About : public QDialog {
|
||||
public:
|
||||
explicit About(QWidget *parent = nullptr);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent*);
|
||||
|
||||
private:
|
||||
struct Person {
|
||||
Person(const QString &n, const QString &e = QString()) : name(n), email(e) {}
|
||||
bool operator<(const Person &other) const { return name < other.name; }
|
||||
@@ -46,7 +51,6 @@ class About : public QDialog {
|
||||
QString email;
|
||||
};
|
||||
|
||||
private:
|
||||
QString MainHtml() const;
|
||||
QString ContributorsHtml() const;
|
||||
QString PersonToHtml(const Person& person) const;
|
||||
|
||||
@@ -7,13 +7,19 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>500</width>
|
||||
<height>600</height>
|
||||
<height>500</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>500</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>600</width>
|
||||
<height>700</height>
|
||||
<width>500</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
@@ -118,13 +124,29 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="spacer_middle">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="text_contributors">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
|
||||
@@ -803,13 +803,7 @@ void InternetSearchView::LazyLoadAlbumCover(const QModelIndex &proxy_index) {
|
||||
|
||||
// Is this an album?
|
||||
const CollectionModel::GroupBy container_type = CollectionModel::GroupBy(proxy_index.data(CollectionModel::Role_ContainerType).toInt());
|
||||
if (container_type != CollectionModel::GroupBy_Album &&
|
||||
container_type != CollectionModel::GroupBy_AlbumDisc &&
|
||||
container_type != CollectionModel::GroupBy_YearAlbum &&
|
||||
container_type != CollectionModel::GroupBy_YearAlbumDisc &&
|
||||
container_type != CollectionModel::GroupBy_OriginalYearAlbum) {
|
||||
return;
|
||||
}
|
||||
if (!CollectionModel::IsAlbumGrouping(container_type)) return;
|
||||
|
||||
// Mark the item as loading art
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ void InternetSongsView::SongsFinished(const SongList &songs, const QString &erro
|
||||
service_->songs_collection_backend()->DeleteAll();
|
||||
ui_->stacked->setCurrentWidget(ui_->internetcollection_page);
|
||||
ui_->status->clear();
|
||||
service_->songs_collection_backend()->AddOrUpdateSongs(songs);
|
||||
service_->songs_collection_backend()->AddOrUpdateSongsAsync(songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ void InternetTabsView::ArtistsFinished(const SongList &songs, const QString &err
|
||||
service_->artists_collection_backend()->DeleteAll();
|
||||
ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->internetcollection_page());
|
||||
ui_->artists_collection->status()->clear();
|
||||
service_->artists_collection_backend()->AddOrUpdateSongs(songs);
|
||||
service_->artists_collection_backend()->AddOrUpdateSongsAsync(songs);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -274,7 +274,7 @@ void InternetTabsView::AlbumsFinished(const SongList &songs, const QString &erro
|
||||
service_->albums_collection_backend()->DeleteAll();
|
||||
ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->internetcollection_page());
|
||||
ui_->albums_collection->status()->clear();
|
||||
service_->albums_collection_backend()->AddOrUpdateSongs(songs);
|
||||
service_->albums_collection_backend()->AddOrUpdateSongsAsync(songs);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -317,7 +317,7 @@ void InternetTabsView::SongsFinished(const SongList &songs, const QString &error
|
||||
service_->songs_collection_backend()->DeleteAll();
|
||||
ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->internetcollection_page());
|
||||
ui_->songs_collection->status()->clear();
|
||||
service_->songs_collection_backend()->AddOrUpdateSongs(songs);
|
||||
service_->songs_collection_backend()->AddOrUpdateSongsAsync(songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,9 +49,10 @@
|
||||
#include <QSslSocket>
|
||||
#include <QDateTime>
|
||||
|
||||
LocalRedirectServer::LocalRedirectServer(const bool https, QObject *parent)
|
||||
LocalRedirectServer::LocalRedirectServer(QObject *parent)
|
||||
: QTcpServer(parent),
|
||||
https_(https),
|
||||
https_(false),
|
||||
port_(0),
|
||||
socket_(nullptr)
|
||||
{}
|
||||
|
||||
@@ -232,7 +233,7 @@ bool LocalRedirectServer::Listen() {
|
||||
if (https_) {
|
||||
if (!GenerateCertificate()) return false;
|
||||
}
|
||||
if (!listen(QHostAddress::LocalHost)) {
|
||||
if (!listen(QHostAddress::LocalHost, port_)) {
|
||||
error_ = errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -38,9 +38,11 @@ class LocalRedirectServer : public QTcpServer {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LocalRedirectServer(const bool https, QObject* parent = nullptr);
|
||||
explicit LocalRedirectServer(QObject *parent = nullptr);
|
||||
~LocalRedirectServer();
|
||||
|
||||
void set_https(const bool https) { https_ = https; }
|
||||
void set_port(const int port) { port_ = port; }
|
||||
bool Listen();
|
||||
const QUrl &url() const { return url_; }
|
||||
const QUrl &request_url() const { return request_url_; }
|
||||
@@ -65,6 +67,7 @@ class LocalRedirectServer : public QTcpServer {
|
||||
|
||||
private:
|
||||
bool https_;
|
||||
int port_;
|
||||
QUrl url_;
|
||||
QUrl request_url_;
|
||||
QSslCertificate ssl_certificate_;
|
||||
|
||||
@@ -46,7 +46,18 @@ const char *AuddLyricsProvider::kUrlSearch = "https://api.audd.io/findLyrics/";
|
||||
const char *AuddLyricsProvider::kAPITokenB64 = "ZjA0NjQ4YjgyNDM3ZTc1MjY3YjJlZDI5ZDBlMzQxZjk=";
|
||||
const int AuddLyricsProvider::kMaxLength = 6000;
|
||||
|
||||
AuddLyricsProvider::AuddLyricsProvider(QObject *parent) : JsonLyricsProvider("AudD", parent), network_(new NetworkAccessManager(this)) {}
|
||||
AuddLyricsProvider::AuddLyricsProvider(QObject *parent) : JsonLyricsProvider("AudD", true, false, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
AuddLyricsProvider::~AuddLyricsProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool AuddLyricsProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) {
|
||||
|
||||
@@ -65,6 +76,7 @@ bool AuddLyricsProvider::StartSearch(const QString &artist, const QString &album
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, artist, title); });
|
||||
|
||||
//qLog(Debug) << "AudDLyrics: Sending request for" << url;
|
||||
@@ -77,10 +89,14 @@ void AuddLyricsProvider::CancelSearch(const quint64 id) { Q_UNUSED(id); }
|
||||
|
||||
void AuddLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonArray json_result = ExtractResult(reply, id, artist, title);
|
||||
QJsonArray json_result = ExtractResult(reply, artist, title);
|
||||
if (json_result.isEmpty()) {
|
||||
emit SearchFinished(id, LyricsSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,10 +126,7 @@ void AuddLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i
|
||||
result.lyrics = json_obj["lyrics"].toString();
|
||||
if (result.lyrics.length() > kMaxLength) continue;
|
||||
if (result.lyrics == "error") continue;
|
||||
result.score = 0.0;
|
||||
if (result.artist.toLower() == artist.toLower()) result.score += 1.0;
|
||||
if (result.title.toLower() == title.toLower()) result.score += 1.0;
|
||||
if (result.lyrics.length() > LyricsFetcher::kGoodLyricsLength) result.score += 1.0;
|
||||
|
||||
//qLog(Debug) << "AudDLyrics:" << result.artist << result.title << result.lyrics.length();
|
||||
|
||||
results << result;
|
||||
@@ -126,40 +139,40 @@ void AuddLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i
|
||||
|
||||
}
|
||||
|
||||
QJsonArray AuddLyricsProvider::ExtractResult(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title) {
|
||||
QJsonArray AuddLyricsProvider::ExtractResult(QNetworkReply *reply, const QString &artist, const QString &title) {
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(reply, id);
|
||||
QJsonObject json_obj = ExtractJsonObj(reply);
|
||||
if (json_obj.isEmpty()) return QJsonArray();
|
||||
|
||||
if (!json_obj.contains("status")) {
|
||||
Error(id, "Json reply is missing status.", json_obj);
|
||||
Error("Json reply is missing status.", json_obj);
|
||||
return QJsonArray();
|
||||
}
|
||||
|
||||
if (json_obj["status"].toString() == "error") {
|
||||
if (!json_obj.contains("error")) {
|
||||
Error(id, "Json reply is missing error status.", json_obj);
|
||||
Error("Json reply is missing error status.", json_obj);
|
||||
return QJsonArray();
|
||||
}
|
||||
QJsonObject json_error = json_obj["error"].toObject();
|
||||
if (!json_error.contains("error_code") || !json_error.contains("error_message")) {
|
||||
Error(id, "Json reply is missing error code or message.", json_error);
|
||||
Error("Json reply is missing error code or message.", json_error);
|
||||
return QJsonArray();
|
||||
}
|
||||
QString error_code(json_error["error_code"].toString());
|
||||
QString error_message(json_error["error_message"].toString());
|
||||
Error(id, error_message);
|
||||
Error(error_message);
|
||||
return QJsonArray();
|
||||
}
|
||||
|
||||
if (!json_obj.contains("result")) {
|
||||
Error(id, "Json reply is missing result.", json_obj);
|
||||
Error("Json reply is missing result.", json_obj);
|
||||
return QJsonArray();
|
||||
}
|
||||
|
||||
QJsonArray json_result = json_obj["result"].toArray();
|
||||
if (json_result.isEmpty()) {
|
||||
Error(id, QString("No lyrics for %1 %2").arg(artist).arg(title));
|
||||
Error(QString("No lyrics for %1 %2").arg(artist).arg(title));
|
||||
return QJsonArray();
|
||||
}
|
||||
|
||||
@@ -167,8 +180,9 @@ QJsonArray AuddLyricsProvider::ExtractResult(QNetworkReply *reply, const quint64
|
||||
|
||||
}
|
||||
|
||||
void AuddLyricsProvider::Error(const quint64 id, const QString &error, const QVariant &debug) {
|
||||
void AuddLyricsProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "AudDLyrics:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
emit SearchFinished(id, LyricsSearchResults());
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QJsonArray>
|
||||
@@ -39,10 +40,15 @@ class AuddLyricsProvider : public JsonLyricsProvider {
|
||||
|
||||
public:
|
||||
explicit AuddLyricsProvider(QObject *parent = nullptr);
|
||||
~AuddLyricsProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, quint64 id);
|
||||
void CancelSearch(const quint64 id);
|
||||
|
||||
private:
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
QJsonArray ExtractResult(QNetworkReply *reply, const QString &artist, const QString &title);
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title);
|
||||
|
||||
@@ -51,9 +57,7 @@ class AuddLyricsProvider : public JsonLyricsProvider {
|
||||
static const char *kAPITokenB64;
|
||||
static const int kMaxLength;
|
||||
QNetworkAccessManager *network_;
|
||||
void Error(const quint64 id, const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
QJsonArray ExtractResult(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title);
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
143
src/lyrics/chartlyricsprovider.cpp
Normal file
143
src/lyrics/chartlyricsprovider.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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 <QByteArray>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/utilities.h"
|
||||
#include "lyricsprovider.h"
|
||||
#include "lyricsfetcher.h"
|
||||
#include "chartlyricsprovider.h"
|
||||
|
||||
const char *ChartLyricsProvider::kUrlSearch = "http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect";
|
||||
|
||||
ChartLyricsProvider::ChartLyricsProvider(QObject *parent) : LyricsProvider("ChartLyrics", false, false, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
ChartLyricsProvider::~ChartLyricsProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool ChartLyricsProvider::StartSearch(const QString &artist, const QString&, const QString &title, const quint64 id) {
|
||||
|
||||
const ParamList params = ParamList() << Param("artist", artist)
|
||||
<< Param("song", title);
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl url(kUrlSearch);
|
||||
url.setQuery(url_query);
|
||||
QNetworkReply *reply = network_->get(QNetworkRequest(url));
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, artist, title); });
|
||||
|
||||
//qLog(Debug) << "ChartLyrics: Sending request for" << url;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void ChartLyricsProvider::CancelSearch(const quint64) {}
|
||||
|
||||
void ChartLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
emit SearchFinished(id, LyricsSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
|
||||
emit SearchFinished(id, LyricsSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
QXmlStreamReader reader(reply);
|
||||
LyricsSearchResults results;
|
||||
LyricsSearchResult result;
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader.readNext();
|
||||
QStringRef name = reader.name();
|
||||
if (type == QXmlStreamReader::StartElement) {
|
||||
if (name == "GetLyricResult") {
|
||||
result = LyricsSearchResult();
|
||||
}
|
||||
if (name == "LyricArtist") {
|
||||
result.artist = reader.readElementText();
|
||||
}
|
||||
else if (name == "LyricSong") {
|
||||
result.title = reader.readElementText();
|
||||
}
|
||||
else if (name == "Lyric") {
|
||||
result.lyrics = reader.readElementText();
|
||||
}
|
||||
}
|
||||
else if (type == QXmlStreamReader::EndElement) {
|
||||
if (name == "GetLyricResult") {
|
||||
if (!result.artist.isEmpty() && !result.title.isEmpty() && !result.lyrics.isEmpty() && (result.artist.toLower() == artist.toLower() || result.title.toLower() == title.toLower())) {
|
||||
results << result;
|
||||
}
|
||||
result = LyricsSearchResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (results.isEmpty()) qLog(Debug) << "ChartLyrics: No lyrics for" << artist << title;
|
||||
else qLog(Debug) << "ChartLyrics: Got lyrics for" << artist << title;
|
||||
|
||||
emit SearchFinished(id, results);
|
||||
|
||||
}
|
||||
|
||||
void ChartLyricsProvider::Error(const QString &error, QVariant debug) {
|
||||
|
||||
qLog(Error) << "ChartLyrics:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
60
src/lyrics/chartlyricsprovider.h
Normal file
60
src/lyrics/chartlyricsprovider.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 CHARTLYRICSPROVIDER_H
|
||||
#define CHARTLYRICSPROVIDER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
#include "lyricsprovider.h"
|
||||
#include "lyricsfetcher.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
class ChartLyricsProvider : public LyricsProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChartLyricsProvider(QObject *parent = nullptr);
|
||||
~ChartLyricsProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id);
|
||||
void CancelSearch(quint64 id);
|
||||
|
||||
private:
|
||||
void Error(const QString &error, QVariant debug = QVariant());
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title);
|
||||
|
||||
private:
|
||||
static const char *kUrlSearch;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // CHARTLYRICSPROVIDER_H
|
||||
557
src/lyrics/geniuslyricsprovider.cpp
Normal file
557
src/lyrics/geniuslyricsprovider.cpp
Normal file
@@ -0,0 +1,557 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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 <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QSslError>
|
||||
#include <QTextCodec>
|
||||
#include <QDesktopServices>
|
||||
#include <QCryptographicHash>
|
||||
#include <QSettings>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonParseError>
|
||||
#include <QMessageBox>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/network.h"
|
||||
#include "internet/localredirectserver.h"
|
||||
#include "jsonlyricsprovider.h"
|
||||
#include "lyricsfetcher.h"
|
||||
#include "lyricsprovider.h"
|
||||
#include "geniuslyricsprovider.h"
|
||||
|
||||
const char *GeniusLyricsProvider::kSettingsGroup = "GeniusLyrics";
|
||||
const char *GeniusLyricsProvider::kOAuthAuthorizeUrl = "https://api.genius.com/oauth/authorize";
|
||||
const char *GeniusLyricsProvider::kOAuthAccessTokenUrl = "https://api.genius.com/oauth/token";
|
||||
const char *GeniusLyricsProvider::kOAuthRedirectUrl = "http://localhost:63111/"; // Genius does not accept a random port number. This port must match the the URL of the ClientID.
|
||||
const char *GeniusLyricsProvider::kUrlSearch = "https://api.genius.com/search/";
|
||||
const char *GeniusLyricsProvider::kClientIDB64 = "RUNTNXU4U1VyMU1KUU5hdTZySEZteUxXY2hkanFiY3lfc2JjdXBpNG5WMU9SNUg4dTBZelEtZTZCdFg2dl91SQ==";
|
||||
const char *GeniusLyricsProvider::kClientSecretB64 = "VE9pMU9vUjNtTXZ3eFR3YVN0QVRyUjVoUlhVWDI1Ylp5X240eEt1M0ZkYlNwRG5JUnd0LXFFbHdGZkZkRWY2VzJ1S011UnQzM3c2Y3hqY0tVZ3NGN2c=";
|
||||
|
||||
GeniusLyricsProvider::GeniusLyricsProvider(QObject *parent) : JsonLyricsProvider("Genius", true, true, parent), network_(new NetworkAccessManager(this)), server_(nullptr) {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
if (s.contains("access_token")) {
|
||||
access_token_ = s.value("access_token").toString();
|
||||
}
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
GeniusLyricsProvider::~GeniusLyricsProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GeniusLyricsProvider::Authenticate() {
|
||||
|
||||
QUrl redirect_url(kOAuthRedirectUrl);
|
||||
|
||||
if (!server_) {
|
||||
server_ = new LocalRedirectServer(this);
|
||||
server_->set_https(false);
|
||||
server_->set_port(redirect_url.port());
|
||||
if (!server_->Listen()) {
|
||||
AuthError(server_->error());
|
||||
server_->deleteLater();
|
||||
server_ = nullptr;
|
||||
return;
|
||||
}
|
||||
connect(server_, SIGNAL(Finished()), this, SLOT(RedirectArrived()));
|
||||
}
|
||||
|
||||
code_verifier_ = Utilities::CryptographicRandomString(44);
|
||||
code_challenge_ = QString(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding));
|
||||
if (code_challenge_.lastIndexOf(QChar('=')) == code_challenge_.length() - 1) {
|
||||
code_challenge_.chop(1);
|
||||
}
|
||||
|
||||
const ParamList params = ParamList() << Param("client_id", QByteArray::fromBase64(kClientIDB64))
|
||||
<< Param("redirect_uri", redirect_url.toString())
|
||||
<< Param("scope", "me")
|
||||
<< Param("state", code_challenge_)
|
||||
<< Param("response_type", "code");
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl url(kOAuthAuthorizeUrl);
|
||||
url.setQuery(url_query);
|
||||
|
||||
const bool result = QDesktopServices::openUrl(url);
|
||||
if (!result) {
|
||||
QMessageBox messagebox(QMessageBox::Information, tr("Genius Authentication"), tr("Please open this URL in your browser") + QString(":<br /><a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
|
||||
messagebox.setTextFormat(Qt::RichText);
|
||||
messagebox.exec();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GeniusLyricsProvider::RedirectArrived() {
|
||||
|
||||
if (!server_) return;
|
||||
|
||||
if (server_->error().isEmpty()) {
|
||||
QUrl url = server_->request_url();
|
||||
if (url.isValid()) {
|
||||
QUrlQuery url_query(url);
|
||||
if (url_query.hasQueryItem("error")) {
|
||||
AuthError(QUrlQuery(url).queryItemValue("error"));
|
||||
}
|
||||
else if (url_query.hasQueryItem("code")) {
|
||||
QUrl redirect_url(kOAuthRedirectUrl);
|
||||
redirect_url.setPort(server_->url().port());
|
||||
RequestAccessToken(url, redirect_url);
|
||||
}
|
||||
else {
|
||||
AuthError(tr("Redirect missing token code!"));
|
||||
}
|
||||
}
|
||||
else {
|
||||
AuthError(tr("Received invalid reply from web browser."));
|
||||
}
|
||||
}
|
||||
else {
|
||||
AuthError(server_->error());
|
||||
}
|
||||
|
||||
server_->close();
|
||||
server_->deleteLater();
|
||||
server_ = nullptr;
|
||||
|
||||
}
|
||||
|
||||
void GeniusLyricsProvider::RequestAccessToken(const QUrl &url, const QUrl &redirect_url) {
|
||||
|
||||
qLog(Debug) << "GeniusLyrics: Authorization URL Received" << url;
|
||||
|
||||
QUrlQuery url_query(url);
|
||||
|
||||
if (url.hasQuery() && url_query.hasQueryItem("code") && url_query.hasQueryItem("state")) {
|
||||
|
||||
QString code = url_query.queryItemValue("code");
|
||||
QString state = url_query.queryItemValue("state");
|
||||
|
||||
const ParamList params = ParamList() << Param("code", code)
|
||||
<< Param("client_id", QByteArray::fromBase64(kClientIDB64))
|
||||
<< Param("client_secret", QByteArray::fromBase64(kClientSecretB64))
|
||||
<< Param("redirect_uri", redirect_url.toString())
|
||||
<< Param("grant_type", "authorization_code")
|
||||
<< Param("response_type", "code");
|
||||
|
||||
QUrlQuery new_url_query;
|
||||
for (const Param ¶m : params) {
|
||||
new_url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl new_url(kOAuthAccessTokenUrl);
|
||||
QNetworkRequest req = QNetworkRequest(new_url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QByteArray query = new_url_query.toString(QUrl::FullyEncoded).toUtf8();
|
||||
|
||||
QNetworkReply *reply = network_->post(req, query);
|
||||
replies_ << reply;
|
||||
connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(HandleLoginSSLErrors(QList<QSslError>)));
|
||||
connect(reply, &QNetworkReply::finished, [=] { AccessTokenRequestFinished(reply); });
|
||||
|
||||
}
|
||||
|
||||
else {
|
||||
AuthError(tr("Redirect from Genius is missing query items code or state."));
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GeniusLyricsProvider::HandleLoginSSLErrors(QList<QSslError> ssl_errors) {
|
||||
|
||||
for (const QSslError &ssl_error : ssl_errors) {
|
||||
login_errors_ += ssl_error.errorString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GeniusLyricsProvider::AccessTokenRequestFinished(QNetworkReply *reply) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
AuthError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "status" and "userMessage" then use that instead.
|
||||
QByteArray data(reply->readAll());
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("error") && json_obj.contains("error_description")) {
|
||||
QString error = json_obj["error"].toString();
|
||||
QString error_description = json_obj["error_description"].toString();
|
||||
login_errors_ << QString("Authentication failure: %1 (%2)").arg(error).arg(error_description);
|
||||
}
|
||||
}
|
||||
if (login_errors_.isEmpty()) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
else {
|
||||
login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||
}
|
||||
}
|
||||
AuthError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray data(reply->readAll());
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
Error(QString("Failed to parse Json data in authentication reply: %1").arg(json_error.errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
AuthError("Authentication reply from server has empty Json document.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
AuthError("Authentication reply from server has Json document that is not an object.", json_doc);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
AuthError("Authentication reply from server has empty Json object.", json_doc);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("access_token")) {
|
||||
AuthError("Authentication reply from server is missing access token.", json_obj);
|
||||
return;
|
||||
}
|
||||
|
||||
access_token_ = json_obj["access_token"].toString();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("access_token", access_token_);
|
||||
s.endGroup();
|
||||
|
||||
qLog(Debug) << "Genius: Authentication was successful, got access token" << access_token_;
|
||||
|
||||
emit AuthenticationComplete(true);
|
||||
emit AuthenticationSuccess();
|
||||
|
||||
}
|
||||
|
||||
bool GeniusLyricsProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) {
|
||||
|
||||
Q_UNUSED(album);
|
||||
|
||||
if (access_token_.isEmpty()) return false;
|
||||
|
||||
std::shared_ptr<GeniusLyricsSearchContext> search = std::make_shared<GeniusLyricsSearchContext>();
|
||||
|
||||
search->id = id;
|
||||
search->artist = artist;
|
||||
search->title = title;
|
||||
requests_search_.insert(id, search);
|
||||
|
||||
const ParamList params = ParamList() << Param("q", QString(artist + " " + title));
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl url(kUrlSearch);
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
req.setRawHeader("Authorization", "Bearer " + access_token_.toUtf8());
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); });
|
||||
|
||||
//qLog(Debug) << "GeniusLyrics: Sending request for" << url;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void GeniusLyricsProvider::CancelSearch(const quint64 id) { Q_UNUSED(id); }
|
||||
|
||||
void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(id)) return;
|
||||
std::shared_ptr<GeniusLyricsSearchContext> search = requests_search_.value(id);
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(reply);
|
||||
if (json_obj.isEmpty()) {
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("meta")) {
|
||||
Error("Json reply is missing meta object.", json_obj);
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
if (!json_obj["meta"].isObject()) {
|
||||
Error("Json reply meta is not an object.", json_obj);
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_meta = json_obj["meta"].toObject();
|
||||
if (!obj_meta.contains("status")) {
|
||||
Error("Json reply meta object is missing status.", obj_meta);
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
int status = obj_meta["status"].toInt();
|
||||
if (status != 200) {
|
||||
if (obj_meta.contains("message")) {
|
||||
Error(QString("Received error %1: %2.").arg(status).arg(obj_meta["message"].toString()));
|
||||
}
|
||||
else {
|
||||
Error(QString("Received error %1.").arg(status));
|
||||
}
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("response")) {
|
||||
Error("Json reply is missing response.", json_obj);
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
if (!json_obj["response"].isObject()) {
|
||||
Error("Json response is not an object.", json_obj);
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_response = json_obj["response"].toObject();
|
||||
if (!obj_response.contains("hits")) {
|
||||
Error("Json response is missing hits.", obj_response);
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
if (!obj_response["hits"].isArray()) {
|
||||
Error("Json hits is not an array.", obj_response);
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
QJsonArray array_hits = obj_response["hits"].toArray();
|
||||
|
||||
for (QJsonValue value_hit : array_hits) {
|
||||
if (!value_hit.isObject()) {
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_hit = value_hit.toObject();
|
||||
if (!obj_hit.contains("result")) {
|
||||
continue;
|
||||
}
|
||||
if (!obj_hit["result"].isObject()) {
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_result = obj_hit["result"].toObject();
|
||||
if (!obj_result.contains("title") || !obj_result.contains("primary_artist") || !obj_result.contains("url") || !obj_result["primary_artist"].isObject()) {
|
||||
Error("Missing one or more values in result object", obj_result);
|
||||
continue;
|
||||
}
|
||||
QJsonObject primary_artist = obj_result["primary_artist"].toObject();
|
||||
if (!primary_artist.contains("name")) continue;
|
||||
|
||||
QString artist = primary_artist["name"].toString();
|
||||
QString title = obj_result["title"].toString();
|
||||
|
||||
// Ignore results where both the artist and title don't match.
|
||||
if (artist.toLower() != search->artist.toLower() && title.toLower() != search->title.toLower()) continue;
|
||||
|
||||
QUrl url(obj_result["url"].toString());
|
||||
if (!url.isValid()) continue;
|
||||
if (search->requests_lyric_.contains(url)) continue;
|
||||
|
||||
GeniusLyricsLyricContext lyric;
|
||||
lyric.artist = artist;
|
||||
lyric.title = title;
|
||||
lyric.url = url;
|
||||
|
||||
search->requests_lyric_.insert(url, lyric);
|
||||
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *new_reply = network_->get(req);
|
||||
replies_ << new_reply;
|
||||
connect(new_reply, &QNetworkReply::finished, [=] { HandleLyricReply(new_reply, search->id, url); });
|
||||
|
||||
}
|
||||
|
||||
EndSearch(search);
|
||||
|
||||
}
|
||||
|
||||
void GeniusLyricsProvider::HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(search_id)) return;
|
||||
std::shared_ptr<GeniusLyricsSearchContext> search = requests_search_.value(search_id);
|
||||
|
||||
if (!search->requests_lyric_.contains(url)) {
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
const GeniusLyricsLyricContext lyric = search->requests_lyric_.value(url);
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
EndSearch(search, lyric);
|
||||
return;
|
||||
}
|
||||
else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
|
||||
EndSearch(search, lyric);
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
Error("Empty reply received from server.");
|
||||
EndSearch(search, lyric);
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCodec *codec = QTextCodec::codecForName("utf-8");
|
||||
if (!codec) {
|
||||
EndSearch(search, lyric);
|
||||
return;
|
||||
}
|
||||
QString content = codec->toUnicode(data);
|
||||
|
||||
// Extract the lyrics from HTML.
|
||||
|
||||
QString tag_begin = "<div class=\"lyrics\">";
|
||||
QString tag_end = "</div>";
|
||||
int begin_idx = content.indexOf(tag_begin);
|
||||
QString lyrics;
|
||||
if (begin_idx > 0) {
|
||||
begin_idx += tag_begin.length();
|
||||
int end_idx = content.indexOf(tag_end, begin_idx);
|
||||
lyrics = content.mid(begin_idx, end_idx - begin_idx);
|
||||
lyrics = lyrics.remove(QRegExp("<[^>]*>"));
|
||||
lyrics = lyrics.trimmed();
|
||||
}
|
||||
|
||||
if (!lyrics.isEmpty()) {
|
||||
LyricsSearchResult result;
|
||||
result.artist = lyric.artist;
|
||||
result.title = lyric.title;
|
||||
result.lyrics = lyrics;
|
||||
search->results.append(result);
|
||||
}
|
||||
|
||||
EndSearch(search, lyric);
|
||||
|
||||
}
|
||||
|
||||
void GeniusLyricsProvider::AuthError(const QString &error, const QVariant &debug) {
|
||||
|
||||
if (!error.isEmpty()) login_errors_ << error;
|
||||
|
||||
for (const QString &e : login_errors_) Error(e);
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
emit AuthenticationFailure(login_errors_);
|
||||
emit AuthenticationComplete(false, login_errors_);
|
||||
|
||||
login_errors_.clear();
|
||||
|
||||
}
|
||||
|
||||
void GeniusLyricsProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "GeniusLyrics:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
|
||||
void GeniusLyricsProvider::EndSearch(std::shared_ptr<GeniusLyricsSearchContext> search, const GeniusLyricsLyricContext lyric) {
|
||||
|
||||
if (search->requests_lyric_.contains(lyric.url)) {
|
||||
search->requests_lyric_.remove(lyric.url);
|
||||
}
|
||||
if (search->requests_lyric_.count() == 0) {
|
||||
requests_search_.remove(search->id);
|
||||
if (search->results.isEmpty()) {
|
||||
qLog(Debug) << "GeniusLyrics: No lyrics for" << search->artist << search->title;
|
||||
}
|
||||
else {
|
||||
qLog(Debug) << "GeniusLyrics: Got lyrics for" << search->artist << search->title;
|
||||
}
|
||||
emit SearchFinished(search->id, search->results);
|
||||
}
|
||||
|
||||
}
|
||||
110
src/lyrics/geniuslyricsprovider.h
Normal file
110
src/lyrics/geniuslyricsprovider.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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 GENIUSLYRICSPROVIDER_H
|
||||
#define GENIUSLYRICSPROVIDER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QSslError>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include "jsonlyricsprovider.h"
|
||||
#include "lyricsfetcher.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
class LocalRedirectServer;
|
||||
|
||||
class GeniusLyricsProvider : public JsonLyricsProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GeniusLyricsProvider(QObject *parent = nullptr);
|
||||
~GeniusLyricsProvider();
|
||||
|
||||
bool IsAuthenticated() { return !access_token_.isEmpty(); }
|
||||
void Authenticate();
|
||||
void Deauthenticate() { access_token_.clear(); }
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, quint64 id);
|
||||
void CancelSearch(const quint64 id);
|
||||
|
||||
public:
|
||||
struct GeniusLyricsLyricContext {
|
||||
explicit GeniusLyricsLyricContext() {}
|
||||
QString artist;
|
||||
QString title;
|
||||
QUrl url;
|
||||
};
|
||||
struct GeniusLyricsSearchContext {
|
||||
explicit GeniusLyricsSearchContext() : id(-1) {}
|
||||
int id;
|
||||
QString artist;
|
||||
QString title;
|
||||
QMap<QUrl, GeniusLyricsLyricContext> requests_lyric_;
|
||||
LyricsSearchResults results;
|
||||
};
|
||||
|
||||
private:
|
||||
void RequestAccessToken(const QUrl &url, const QUrl &redirect_url);
|
||||
void AuthError(const QString &error = QString(), const QVariant &debug = QVariant());
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
void EndSearch(std::shared_ptr<GeniusLyricsSearchContext> search, const GeniusLyricsLyricContext lyric = GeniusLyricsLyricContext());
|
||||
|
||||
private slots:
|
||||
void HandleLoginSSLErrors(QList<QSslError> ssl_errors);
|
||||
void RedirectArrived();
|
||||
void AccessTokenRequestFinished(QNetworkReply *reply);
|
||||
void HandleSearchReply(QNetworkReply *reply, const quint64 id);
|
||||
void HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url);
|
||||
|
||||
private:
|
||||
static const char *kSettingsGroup;
|
||||
static const char *kClientIDB64;
|
||||
static const char *kClientSecretB64;
|
||||
static const char *kOAuthAuthorizeUrl;
|
||||
static const char *kOAuthAccessTokenUrl;
|
||||
static const char *kOAuthRedirectUrl;
|
||||
static const char *kUrlSearch;
|
||||
|
||||
private:
|
||||
QNetworkAccessManager *network_;
|
||||
LocalRedirectServer *server_;
|
||||
QString code_verifier_;
|
||||
QString code_challenge_;
|
||||
QString access_token_;
|
||||
QStringList login_errors_;
|
||||
QMap<int, std::shared_ptr<GeniusLyricsSearchContext>> requests_search_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // GENIUSLYRICSPROVIDER_H
|
||||
@@ -30,53 +30,60 @@
|
||||
#include "lyricsprovider.h"
|
||||
#include "jsonlyricsprovider.h"
|
||||
|
||||
JsonLyricsProvider::JsonLyricsProvider(const QString &name, QObject *parent) : LyricsProvider(name, parent) {}
|
||||
JsonLyricsProvider::JsonLyricsProvider(const QString &name, const bool enabled, const bool authentication_required, QObject *parent) : LyricsProvider(name, enabled, authentication_required, parent) {}
|
||||
|
||||
QJsonObject JsonLyricsProvider::ExtractJsonObj(QNetworkReply *reply, const quint64 id) {
|
||||
QByteArray JsonLyricsProvider::ExtractData(QNetworkReply *reply) {
|
||||
|
||||
QString failure_reason;
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
if (reply->error() < 200) {
|
||||
Error(id, failure_reason);
|
||||
return QJsonObject();
|
||||
return QByteArray();
|
||||
}
|
||||
}
|
||||
else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
failure_reason = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||
Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
return reply->readAll();
|
||||
|
||||
}
|
||||
|
||||
QJsonObject JsonLyricsProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
if (data.isEmpty()) {
|
||||
if (failure_reason.isEmpty()) failure_reason = "Empty reply received from server.";
|
||||
Error(id, failure_reason);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error(id, "Reply from server missing Json data.");
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
Error(QString("Failed to parse json data: %1").arg(json_error.errorString()));
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isNull() || json_doc.isEmpty()) {
|
||||
Error(id, "Received empty Json document.");
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error(id, "Json document is not an object.");
|
||||
Error("Json document is not an object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error(id, "Received empty Json object.");
|
||||
Error("Received empty Json object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
QJsonObject JsonLyricsProvider::ExtractJsonObj(QNetworkReply *reply) {
|
||||
|
||||
return ExtractJsonObj(ExtractData(reply));
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
@@ -36,11 +37,14 @@ class JsonLyricsProvider : public LyricsProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit JsonLyricsProvider(const QString &name, QObject *parent = nullptr);
|
||||
QJsonObject ExtractJsonObj(QNetworkReply *reply, const quint64 id);
|
||||
explicit JsonLyricsProvider(const QString &name, const bool enabled = true, const bool authentication_required = false, QObject *parent = nullptr);
|
||||
|
||||
QByteArray ExtractData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
QJsonObject ExtractJsonObj(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
virtual void Error(const quint64 id, const QString &error, const QVariant &debug = QVariant()) = 0;
|
||||
virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,18 @@
|
||||
|
||||
const char *LoloLyricsProvider::kUrlSearch = "http://api.lololyrics.com/0.5/getLyric";
|
||||
|
||||
LoloLyricsProvider::LoloLyricsProvider(QObject *parent) : LyricsProvider("LoloLyrics", parent), network_(new NetworkAccessManager(this)) {}
|
||||
LoloLyricsProvider::LoloLyricsProvider(QObject *parent) : LyricsProvider("LoloLyrics", true, false, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
LoloLyricsProvider::~LoloLyricsProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool LoloLyricsProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) {
|
||||
|
||||
@@ -60,6 +71,7 @@ bool LoloLyricsProvider::StartSearch(const QString &artist, const QString &album
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, artist, title); });
|
||||
|
||||
//qLog(Debug) << "LoloLyrics: Sending request for" << url;
|
||||
@@ -72,15 +84,17 @@ void LoloLyricsProvider::CancelSearch(const quint64 id) { Q_UNUSED(id); }
|
||||
|
||||
void LoloLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray data;
|
||||
QString failure_reason;
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
if (reply->error() < 200) {
|
||||
Error(id, failure_reason);
|
||||
Error(failure_reason);
|
||||
emit SearchFinished(id, LyricsSearchResults());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -88,7 +102,7 @@ void LoloLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i
|
||||
failure_reason = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||
}
|
||||
|
||||
data = reply->readAll();
|
||||
QByteArray data = reply->readAll();
|
||||
LyricsSearchResults results;
|
||||
|
||||
if (!data.isEmpty()) {
|
||||
@@ -119,12 +133,6 @@ void LoloLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i
|
||||
else if (type == QXmlStreamReader::EndElement) {
|
||||
if (name == "result") {
|
||||
if (!result.lyrics.isEmpty()) {
|
||||
if (result.artist.toLower() == artist.toLower())
|
||||
result.score += 1.0;
|
||||
if (result.title.toLower() == title.toLower())
|
||||
result.score += 1.0;
|
||||
if (result.lyrics.length() > LyricsFetcher::kGoodLyricsLength)
|
||||
result.score += 1.0;
|
||||
results << result;
|
||||
}
|
||||
result = LyricsSearchResult();
|
||||
@@ -140,10 +148,9 @@ void LoloLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i
|
||||
|
||||
}
|
||||
|
||||
void LoloLyricsProvider::Error(const quint64 id, const QString &error, const QVariant &debug) {
|
||||
void LoloLyricsProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "LoloLyrics:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
emit SearchFinished(id, LyricsSearchResults());
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
@@ -37,17 +38,21 @@ class LoloLyricsProvider : public LyricsProvider {
|
||||
|
||||
public:
|
||||
explicit LoloLyricsProvider(QObject *parent = nullptr);
|
||||
~LoloLyricsProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id);
|
||||
void CancelSearch(const quint64 id);
|
||||
|
||||
private:
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title);
|
||||
|
||||
private:
|
||||
static const char *kUrlSearch;
|
||||
QNetworkAccessManager *network_;
|
||||
void Error(const quint64 id, const QString &error, const QVariant &debug = QVariant());
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
#include "lyricsfetchersearch.h"
|
||||
|
||||
const int LyricsFetcher::kMaxConcurrentRequests = 5;
|
||||
const int LyricsFetcher::kGoodLyricsLength = 60;
|
||||
|
||||
LyricsFetcher::LyricsFetcher(LyricsProviders *lyrics_providers, QObject *parent)
|
||||
: QObject(parent),
|
||||
@@ -104,8 +103,9 @@ void LyricsFetcher::StartRequests() {
|
||||
|
||||
void LyricsFetcher::SingleSearchFinished(const quint64 request_id, const LyricsSearchResults &results) {
|
||||
|
||||
if (!active_requests_.contains(request_id)) return;
|
||||
|
||||
LyricsFetcherSearch *search = active_requests_.take(request_id);
|
||||
if (!search) return;
|
||||
search->deleteLater();
|
||||
emit SearchFinished(request_id, results);
|
||||
|
||||
@@ -113,8 +113,9 @@ void LyricsFetcher::SingleSearchFinished(const quint64 request_id, const LyricsS
|
||||
|
||||
void LyricsFetcher::SingleLyricsFetched(const quint64 request_id, const QString &provider, const QString &lyrics) {
|
||||
|
||||
if (!active_requests_.contains(request_id)) return;
|
||||
|
||||
LyricsFetcherSearch *search = active_requests_.take(request_id);
|
||||
if (!search) return;
|
||||
search->deleteLater();
|
||||
emit LyricsFetched(request_id, provider, lyrics);
|
||||
|
||||
|
||||
@@ -53,9 +53,9 @@ struct LyricsSearchResult {
|
||||
QString lyrics;
|
||||
float score;
|
||||
};
|
||||
Q_DECLARE_METATYPE(LyricsSearchResult)
|
||||
|
||||
typedef QList<LyricsSearchResult> LyricsSearchResults;
|
||||
|
||||
Q_DECLARE_METATYPE(LyricsSearchResult)
|
||||
Q_DECLARE_METATYPE(QList<LyricsSearchResult>)
|
||||
|
||||
class LyricsFetcher : public QObject {
|
||||
@@ -65,13 +65,13 @@ class LyricsFetcher : public QObject {
|
||||
explicit LyricsFetcher(LyricsProviders *lyrics_providers, QObject *parent = nullptr);
|
||||
virtual ~LyricsFetcher() {}
|
||||
|
||||
static const int kMaxConcurrentRequests;
|
||||
static const int kGoodLyricsLength;
|
||||
|
||||
quint64 Search(const QString &artist, const QString &album, const QString &title);
|
||||
void Clear();
|
||||
|
||||
signals:
|
||||
private:
|
||||
void AddRequest(const LyricsSearchRequest &req);
|
||||
|
||||
signals:
|
||||
void LyricsFetched(const quint64 request_id, const QString &provider, const QString &lyrics);
|
||||
void SearchFinished(const quint64 request_id, const LyricsSearchResults &results);
|
||||
|
||||
@@ -81,7 +81,7 @@ signals:
|
||||
void StartRequests();
|
||||
|
||||
private:
|
||||
void AddRequest(const LyricsSearchRequest &req);
|
||||
static const int kMaxConcurrentRequests;
|
||||
|
||||
LyricsProviders *lyrics_providers_;
|
||||
quint64 next_id_;
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
#include <QList>
|
||||
@@ -30,10 +32,11 @@
|
||||
#include "lyricsprovider.h"
|
||||
#include "lyricsproviders.h"
|
||||
|
||||
const int LyricsFetcherSearch::kSearchTimeoutMs = 6000;
|
||||
const int LyricsFetcherSearch::kSearchTimeoutMs = 3000;
|
||||
const int LyricsFetcherSearch::kGoodLyricsLength = 60;
|
||||
const float LyricsFetcherSearch::kHighScore = 2.5;
|
||||
|
||||
LyricsFetcherSearch::LyricsFetcherSearch(
|
||||
const LyricsSearchRequest &request, QObject *parent)
|
||||
LyricsFetcherSearch::LyricsFetcherSearch(const LyricsSearchRequest &request, QObject *parent)
|
||||
: QObject(parent),
|
||||
request_(request),
|
||||
cancel_requested_(false) {
|
||||
@@ -53,7 +56,11 @@ void LyricsFetcherSearch::TerminateSearch() {
|
||||
|
||||
void LyricsFetcherSearch::Start(LyricsProviders *lyrics_providers) {
|
||||
|
||||
for (LyricsProvider *provider : lyrics_providers->List()) {
|
||||
QList<LyricsProvider*> lyrics_providers_sorted = lyrics_providers->List();
|
||||
std::stable_sort(lyrics_providers_sorted.begin(), lyrics_providers_sorted.end(), ProviderCompareOrder);
|
||||
|
||||
for (LyricsProvider *provider : lyrics_providers_sorted) {
|
||||
if (!provider->is_enabled() || !provider->IsAuthenticated()) continue;
|
||||
connect(provider, SIGNAL(SearchFinished(quint64, LyricsSearchResults)), SLOT(ProviderSearchFinished(quint64, LyricsSearchResults)));
|
||||
const int id = lyrics_providers->NextId();
|
||||
const bool success = provider->StartSearch(request_.artist, request_.album, request_.title, id);
|
||||
@@ -70,13 +77,37 @@ void LyricsFetcherSearch::ProviderSearchFinished(const quint64 id, const LyricsS
|
||||
LyricsProvider *provider = pending_requests_.take(id);
|
||||
|
||||
LyricsSearchResults results_copy(results);
|
||||
for (int i = 0; i < results_copy.count(); ++i) {
|
||||
float higest_score = 0.0;
|
||||
for (int i = 0 ; i < results_copy.count() ; ++i) {
|
||||
results_copy[i].provider = provider->name();
|
||||
results_copy[i].score = 0.0;
|
||||
if (results_copy[i].artist.toLower() == request_.artist.toLower()) {
|
||||
results_copy[i].score += 0.5;
|
||||
}
|
||||
if (results_copy[i].album.toLower() == request_.album.toLower()) {
|
||||
results_copy[i].score += 0.5;
|
||||
}
|
||||
if (results_copy[i].title.toLower() == request_.title.toLower()) {
|
||||
results_copy[i].score += 0.5;
|
||||
}
|
||||
if (results_copy[i].artist.toLower() != request_.artist.toLower() && results_copy[i].title.toLower() != request_.title.toLower()) {
|
||||
results_copy[i].score -= 1.5;
|
||||
}
|
||||
if (results_copy[i].lyrics.length() > kGoodLyricsLength) results_copy[i].score += 1.0;
|
||||
if (results_copy[i].score > higest_score) higest_score = results_copy[i].score;
|
||||
}
|
||||
|
||||
results_.append(results_copy);
|
||||
std::stable_sort(results_.begin(), results_.end(), LyricsSearchResultCompareScore);
|
||||
|
||||
if (!pending_requests_.isEmpty()) {
|
||||
if (!results_.isEmpty() && higest_score >= kHighScore) { // Highest score, no need to wait for other providers.
|
||||
qLog(Debug) << "Got lyrics with high score from" << results_.last().provider << "for" << request_.artist << request_.title << "score" << results_.last().score << "finishing search.";
|
||||
TerminateSearch();
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,14 +120,10 @@ void LyricsFetcherSearch::AllProvidersFinished() {
|
||||
if (cancel_requested_) return;
|
||||
|
||||
if (!results_.isEmpty()) {
|
||||
LyricsSearchResult result_use;
|
||||
result_use.score = 0.0;
|
||||
for (LyricsSearchResult result : results_) {
|
||||
if (result_use.lyrics.isEmpty() || result.score > result_use.score) result_use = result;
|
||||
}
|
||||
qLog(Debug) << "Using lyrics from" << result_use.provider << "for" << request_.artist << request_.title << "with score" << result_use.score;
|
||||
emit LyricsFetched(request_.id, result_use.provider, result_use.lyrics);
|
||||
qLog(Debug) << "Using lyrics from" << results_.last().provider << "for" << request_.artist << request_.title << "with score" << results_.last().score;
|
||||
emit LyricsFetched(request_.id, results_.last().provider, results_.last().lyrics);
|
||||
}
|
||||
|
||||
emit SearchFinished(request_.id, results_);
|
||||
|
||||
}
|
||||
@@ -111,3 +138,11 @@ void LyricsFetcherSearch::Cancel() {
|
||||
|
||||
}
|
||||
|
||||
bool LyricsFetcherSearch::ProviderCompareOrder(LyricsProvider *a, LyricsProvider *b) {
|
||||
return a->order() < b->order();
|
||||
}
|
||||
|
||||
bool LyricsFetcherSearch::LyricsSearchResultCompareScore(const LyricsSearchResult &a, const LyricsSearchResult &b) {
|
||||
return a.score < b.score;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,11 +51,13 @@ class LyricsFetcherSearch : public QObject {
|
||||
|
||||
private:
|
||||
void AllProvidersFinished();
|
||||
|
||||
void SendBestImage();
|
||||
static bool ProviderCompareOrder(LyricsProvider *a, LyricsProvider *b);
|
||||
static bool LyricsSearchResultCompareScore(const LyricsSearchResult &a, const LyricsSearchResult &b);
|
||||
|
||||
private:
|
||||
static const int kSearchTimeoutMs;
|
||||
static const int kGoodLyricsLength;
|
||||
static const float kHighScore;
|
||||
|
||||
LyricsSearchRequest request_;
|
||||
LyricsSearchResults results_;
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
|
||||
#include "lyricsprovider.h"
|
||||
|
||||
LyricsProvider::LyricsProvider(const QString &name, QObject *parent)
|
||||
: QObject(parent), name_(name) {}
|
||||
LyricsProvider::LyricsProvider(const QString &name, const bool enabled, const bool authentication_required, QObject *parent)
|
||||
: QObject(parent), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required) {}
|
||||
|
||||
@@ -34,22 +34,36 @@ class LyricsProvider : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LyricsProvider(const QString &name, QObject *parent);
|
||||
explicit LyricsProvider(const QString &name, const bool enabled, const bool authentication_required, QObject *parent);
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
QString name() const { return name_; }
|
||||
bool is_enabled() const { return enabled_; }
|
||||
int order() const { return order_; }
|
||||
|
||||
void set_enabled(const bool enabled) { enabled_ = enabled; }
|
||||
void set_order(const int order) { order_ = order; }
|
||||
|
||||
virtual bool StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) = 0;
|
||||
virtual void CancelSearch(const quint64 id) { Q_UNUSED(id); }
|
||||
virtual bool AuthenticationRequired() { return authentication_required_; }
|
||||
virtual void Authenticate() {}
|
||||
virtual bool IsAuthenticated() { return !authentication_required_; }
|
||||
virtual void Deauthenticate() {}
|
||||
|
||||
signals:
|
||||
void AuthenticationComplete(bool, QStringList = QStringList());
|
||||
void AuthenticationSuccess();
|
||||
void AuthenticationFailure(QStringList);
|
||||
void SearchFinished(const quint64 id, const LyricsSearchResults &results);
|
||||
|
||||
private:
|
||||
QString name_;
|
||||
|
||||
bool enabled_;
|
||||
int order_;
|
||||
bool authentication_required_;
|
||||
};
|
||||
|
||||
#endif // LYRICSPROVIDER_H
|
||||
|
||||
@@ -21,13 +21,23 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QVariant>
|
||||
#include <QVariantList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QSettings>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "lyricsprovider.h"
|
||||
#include "lyricsproviders.h"
|
||||
|
||||
#include "settings/lyricssettingspage.h"
|
||||
|
||||
int LyricsProviders::NextOrderId = 0;
|
||||
|
||||
LyricsProviders::LyricsProviders(QObject *parent) : QObject(parent) {}
|
||||
|
||||
LyricsProviders::~LyricsProviders() {
|
||||
@@ -38,6 +48,48 @@ LyricsProviders::~LyricsProviders() {
|
||||
|
||||
}
|
||||
|
||||
void LyricsProviders::ReloadSettings() {
|
||||
|
||||
QMap<int, QString> all_providers;
|
||||
for (LyricsProvider *provider : lyrics_providers_.keys()) {
|
||||
if (!provider->is_enabled()) continue;
|
||||
all_providers.insert(provider->order(), provider->name());
|
||||
}
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(LyricsSettingsPage::kSettingsGroup);
|
||||
QStringList providers_enabled = s.value("providers", QStringList() << all_providers.values()).toStringList();
|
||||
s.endGroup();
|
||||
|
||||
int i = 0;
|
||||
QList<LyricsProvider*> providers;
|
||||
for (const QString &name : providers_enabled) {
|
||||
LyricsProvider *provider = ProviderByName(name);
|
||||
if (provider) {
|
||||
provider->set_enabled(true);
|
||||
provider->set_order(++i);
|
||||
providers << provider;
|
||||
}
|
||||
}
|
||||
|
||||
for (LyricsProvider *provider : lyrics_providers_.keys()) {
|
||||
if (!providers.contains(provider)) {
|
||||
provider->set_enabled(false);
|
||||
provider->set_order(++i);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LyricsProvider *LyricsProviders::ProviderByName(const QString &name) const {
|
||||
|
||||
for (LyricsProvider *provider : lyrics_providers_.keys()) {
|
||||
if (provider->name() == name) return provider;
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
void LyricsProviders::AddProvider(LyricsProvider *provider) {
|
||||
|
||||
{
|
||||
@@ -46,6 +98,8 @@ void LyricsProviders::AddProvider(LyricsProvider *provider) {
|
||||
connect(provider, SIGNAL(destroyed()), SLOT(ProviderDestroyed()));
|
||||
}
|
||||
|
||||
provider->set_order(++NextOrderId);
|
||||
|
||||
qLog(Debug) << "Registered lyrics provider" << provider->name();
|
||||
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ class LyricsProviders : public QObject {
|
||||
explicit LyricsProviders(QObject *parent = nullptr);
|
||||
~LyricsProviders();
|
||||
|
||||
void ReloadSettings();
|
||||
LyricsProvider *ProviderByName(const QString &name) const;
|
||||
|
||||
void AddProvider(LyricsProvider *provider);
|
||||
void RemoveProvider(LyricsProvider *provider);
|
||||
QList<LyricsProvider*> List() const { return lyrics_providers_.keys(); }
|
||||
@@ -51,7 +54,10 @@ class LyricsProviders : public QObject {
|
||||
private:
|
||||
Q_DISABLE_COPY(LyricsProviders)
|
||||
|
||||
QMap<LyricsProvider *, QString> lyrics_providers_;
|
||||
static int NextOrderId;
|
||||
|
||||
QMap<LyricsProvider*, QString> lyrics_providers_;
|
||||
QList<LyricsProvider*> ordered_providers_;
|
||||
QMutex mutex_;
|
||||
|
||||
QAtomicInt next_id_;
|
||||
|
||||
221
src/lyrics/musixmatchlyricsprovider.cpp
Normal file
221
src/lyrics/musixmatchlyricsprovider.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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 <QByteArray>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QTextCodec>
|
||||
#include <QJsonObject>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "jsonlyricsprovider.h"
|
||||
#include "lyricsfetcher.h"
|
||||
#include "lyricsprovider.h"
|
||||
#include "musixmatchlyricsprovider.h"
|
||||
|
||||
MusixmatchLyricsProvider::MusixmatchLyricsProvider(QObject *parent) : JsonLyricsProvider("Musixmatch", true, false, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
MusixmatchLyricsProvider::~MusixmatchLyricsProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool MusixmatchLyricsProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) {
|
||||
|
||||
QString artist_stripped = artist;
|
||||
QString title_stripped = title;
|
||||
|
||||
artist_stripped = artist_stripped.replace('/', '-');
|
||||
artist_stripped = artist_stripped.remove(QRegExp("[^A-Za-z0-9\\- ]"));
|
||||
artist_stripped = artist_stripped.simplified();
|
||||
artist_stripped = artist_stripped.replace(' ', '-');
|
||||
artist_stripped = artist_stripped.replace(QRegExp("(-)\\1+"), "-");
|
||||
artist_stripped = artist_stripped.toLower();
|
||||
|
||||
title_stripped = title_stripped.replace('/', '-');
|
||||
title_stripped = title_stripped.remove(QRegExp("[^a-zA-Z0-9\\- ]"));
|
||||
title_stripped = title_stripped.simplified();
|
||||
title_stripped = title_stripped.replace(' ', '-').toLower();
|
||||
title_stripped = title_stripped.replace(QRegExp("(-)\\1+"), "-");
|
||||
title_stripped = title_stripped.toLower();
|
||||
|
||||
if (artist_stripped.isEmpty() || title_stripped.isEmpty()) return false;
|
||||
|
||||
QUrl url(QString("https://www.musixmatch.com/lyrics/%1/%2").arg(artist_stripped).arg(title_stripped));
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, artist, album, title); });
|
||||
|
||||
qLog(Debug) << "MusixmatchLyrics: Sending request for" << artist_stripped << title_stripped << url;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void MusixmatchLyricsProvider::CancelSearch(const quint64 id) { Q_UNUSED(id); }
|
||||
|
||||
void MusixmatchLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &album, const QString &title) {
|
||||
|
||||
Q_UNUSED(album);
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
LyricsSearchResults results;
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
Error("Empty reply received from server.");
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCodec *codec = QTextCodec::codecForName("utf-8");
|
||||
if (!codec) {
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
QString content = codec->toUnicode(data);
|
||||
|
||||
QString data_begin = "var __mxmState = ";
|
||||
QString data_end = ";</script>";
|
||||
int begin_idx = content.indexOf(data_begin);
|
||||
QString content_json;
|
||||
if (begin_idx > 0) {
|
||||
begin_idx += data_begin.length();
|
||||
int end_idx = content.indexOf(data_end, begin_idx);
|
||||
if (end_idx > begin_idx) {
|
||||
content_json = content.mid(begin_idx, end_idx - begin_idx);
|
||||
}
|
||||
}
|
||||
|
||||
if (content_json.isEmpty()) {
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
if (content_json.contains(QRegExp("<[^>]*>"))) { // Make sure it's not HTML code.
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(content_json.toUtf8());
|
||||
if (json_obj.isEmpty()) {
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("page") || !json_obj["page"].isObject()) {
|
||||
Error("Json reply is missing page.", json_obj);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
json_obj = json_obj["page"].toObject();
|
||||
|
||||
|
||||
if (!json_obj.contains("track") || !json_obj["track"].isObject()) {
|
||||
Error("Json reply is missing track.", json_obj);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_track = json_obj["track"].toObject();
|
||||
|
||||
if (!obj_track.contains("artistName") || !obj_track.contains("albumName") || !obj_track.contains("name")) {
|
||||
Error("Json track is missing artistName, albumName or name.", json_obj);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("lyrics") || !json_obj["lyrics"].isObject()) {
|
||||
Error("Json reply is missing lyrics.", json_obj);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_lyrics = json_obj["lyrics"].toObject();
|
||||
|
||||
if (!obj_lyrics.contains("lyrics") || !obj_lyrics["lyrics"].isObject()) {
|
||||
Error("Json reply is missing lyrics.", obj_lyrics);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
obj_lyrics = obj_lyrics["lyrics"].toObject();
|
||||
|
||||
if (!obj_lyrics.contains("body")) {
|
||||
Error("Json lyrics is missing body.", obj_lyrics);
|
||||
emit SearchFinished(id, results);
|
||||
}
|
||||
|
||||
LyricsSearchResult result;
|
||||
result.artist = obj_track["artistName"].toString();
|
||||
result.album = obj_track["albumName"].toString();
|
||||
result.title = obj_track["name"].toString();
|
||||
result.lyrics = obj_lyrics["body"].toString();
|
||||
|
||||
if (!result.lyrics.isEmpty() && (artist.toLower() == result.artist.toLower() || title.toLower() == result.title.toLower())) {
|
||||
results.append(result);
|
||||
}
|
||||
|
||||
if (results.isEmpty()) {
|
||||
qLog(Debug) << "MusixmatchLyrics: No lyrics for" << artist << title;
|
||||
}
|
||||
else {
|
||||
qLog(Debug) << "MusixmatchLyrics: Got lyrics for" << artist << title;
|
||||
}
|
||||
|
||||
emit SearchFinished(id, results);
|
||||
|
||||
}
|
||||
|
||||
void MusixmatchLyricsProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "MusixmatchLyrics:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
59
src/lyrics/musixmatchlyricsprovider.h
Normal file
59
src/lyrics/musixmatchlyricsprovider.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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 MUSIXMATCHLYRICSPROVIDER_H
|
||||
#define MUSIXMATCHLYRICSPROVIDER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
#include "jsonlyricsprovider.h"
|
||||
#include "lyricsfetcher.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
class MusixmatchLyricsProvider : public JsonLyricsProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MusixmatchLyricsProvider(QObject *parent = nullptr);
|
||||
~MusixmatchLyricsProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id);
|
||||
void CancelSearch(const quint64 id);
|
||||
|
||||
private:
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &album, const QString &title);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // MUSIXMATCHLYRICSPROVIDER_H
|
||||
@@ -38,7 +38,18 @@
|
||||
|
||||
const char *OVHLyricsProvider::kUrlSearch = "https://api.lyrics.ovh/v1/";
|
||||
|
||||
OVHLyricsProvider::OVHLyricsProvider(QObject *parent) : JsonLyricsProvider("Lyrics.ovh", parent), network_(new NetworkAccessManager(this)) {}
|
||||
OVHLyricsProvider::OVHLyricsProvider(QObject *parent) : JsonLyricsProvider("Lyrics.ovh", true, false, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
OVHLyricsProvider::~OVHLyricsProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool OVHLyricsProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) {
|
||||
|
||||
@@ -48,6 +59,7 @@ bool OVHLyricsProvider::StartSearch(const QString &artist, const QString &album,
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, artist, title); });
|
||||
|
||||
//qLog(Debug) << "OVHLyrics: Sending request for" << url;
|
||||
@@ -60,16 +72,19 @@ void OVHLyricsProvider::CancelSearch(const quint64 id) { Q_UNUSED(id); }
|
||||
|
||||
void OVHLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(reply, id);
|
||||
QJsonObject json_obj = ExtractJsonObj(reply);
|
||||
if (json_obj.isEmpty()) {
|
||||
emit SearchFinished(id, LyricsSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_obj.contains("error")) {
|
||||
Error(id, json_obj["error"].toString());
|
||||
Error(json_obj["error"].toString());
|
||||
qLog(Debug) << "OVHLyrics: No lyrics for" << artist << title;
|
||||
emit SearchFinished(id, LyricsSearchResults());
|
||||
return;
|
||||
@@ -82,19 +97,17 @@ void OVHLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id
|
||||
|
||||
LyricsSearchResult result;
|
||||
result.lyrics = json_obj["lyrics"].toString();
|
||||
if (result.lyrics.length() > LyricsFetcher::kGoodLyricsLength)
|
||||
result.score += 1.0;
|
||||
|
||||
qLog(Debug) << "OVHLyrics: Got lyrics for" << artist << title;
|
||||
|
||||
emit SearchFinished(id, LyricsSearchResults() << result);
|
||||
|
||||
}
|
||||
|
||||
|
||||
void OVHLyricsProvider::Error(const quint64 id, const QString &error, const QVariant &debug) {
|
||||
void OVHLyricsProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "OVHLyrics:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
emit SearchFinished(id, LyricsSearchResults());
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
@@ -37,17 +38,21 @@ class OVHLyricsProvider : public JsonLyricsProvider {
|
||||
|
||||
public:
|
||||
explicit OVHLyricsProvider(QObject *parent = nullptr);
|
||||
~OVHLyricsProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id);
|
||||
void CancelSearch(const quint64 id);
|
||||
|
||||
private:
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title);
|
||||
|
||||
private:
|
||||
static const char *kUrlSearch;
|
||||
QNetworkAccessManager *network_;
|
||||
void Error(const quint64 id, const QString &error, const QVariant &debug = QVariant());
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -175,11 +175,8 @@ QByteArray MoodbarBuilder::Finish(int width) {
|
||||
|
||||
for (int i = 0; i < width; ++i) {
|
||||
Rgb rgb;
|
||||
int start = i * frames_.count() / width;
|
||||
int end = (i + 1) * frames_.count() / width;
|
||||
if (start == end) {
|
||||
end = start + 1;
|
||||
}
|
||||
const int start = i * frames_.count() / width;
|
||||
const int end = std::max((i + 1) * frames_.count() / width, start + 1);
|
||||
|
||||
for (int j = start; j < end; j++) {
|
||||
const Rgb& frame = frames_[j];
|
||||
|
||||
@@ -57,6 +57,12 @@ AcoustidClient::AcoustidClient(QObject *parent)
|
||||
network_(new NetworkAccessManager(this)),
|
||||
timeouts_(new NetworkTimeouts(kDefaultTimeout, this)) {}
|
||||
|
||||
AcoustidClient::~AcoustidClient() {
|
||||
|
||||
CancelAll();
|
||||
|
||||
}
|
||||
|
||||
void AcoustidClient::SetTimeout(const int msec) { timeouts_->SetTimeout(msec); }
|
||||
|
||||
void AcoustidClient::Start(const int id, const QString &fingerprint, int duration_msec) {
|
||||
@@ -82,10 +88,13 @@ void AcoustidClient::Start(const int id, const QString &fingerprint, int duratio
|
||||
requests_[id] = reply;
|
||||
|
||||
timeouts_->AddReply(reply);
|
||||
|
||||
}
|
||||
|
||||
void AcoustidClient::Cancel(const int id) {
|
||||
|
||||
if (requests_.contains(id)) delete requests_.take(id);
|
||||
|
||||
}
|
||||
|
||||
void AcoustidClient::CancelAll() {
|
||||
@@ -113,6 +122,7 @@ struct IdSource {
|
||||
|
||||
void AcoustidClient::RequestFinished(QNetworkReply *reply, const int request_id) {
|
||||
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
requests_.remove(request_id);
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ class AcoustidClient : public QObject {
|
||||
|
||||
public:
|
||||
explicit AcoustidClient(QObject *parent = nullptr);
|
||||
~AcoustidClient();
|
||||
|
||||
// Network requests will be aborted after this interval.
|
||||
void SetTimeout(const int msec);
|
||||
@@ -70,6 +71,7 @@ class AcoustidClient : public QObject {
|
||||
QNetworkAccessManager *network_;
|
||||
NetworkTimeouts *timeouts_;
|
||||
QMap<int, QNetworkReply*> requests_;
|
||||
|
||||
};
|
||||
|
||||
#endif // ACOUSTIDCLIENT_H
|
||||
|
||||
@@ -69,6 +69,12 @@ MusicBrainzClient::MusicBrainzClient(QObject *parent, QNetworkAccessManager *net
|
||||
|
||||
}
|
||||
|
||||
MusicBrainzClient::~MusicBrainzClient() {
|
||||
|
||||
CancelAll();
|
||||
|
||||
}
|
||||
|
||||
QByteArray MusicBrainzClient::GetReplyData(QNetworkReply *reply, QString &error) {
|
||||
|
||||
QByteArray data;
|
||||
@@ -114,7 +120,7 @@ void MusicBrainzClient::Cancel(int id) {
|
||||
|
||||
while (!requests_.isEmpty() && requests_.contains(id)) {
|
||||
QNetworkReply *reply = requests_.take(id);
|
||||
disconnect(reply, 0, this, 0);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
if (reply->isRunning()) reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
@@ -187,6 +193,7 @@ void MusicBrainzClient::FlushRequests() {
|
||||
|
||||
void MusicBrainzClient::RequestFinished(QNetworkReply *reply, const int id, const int request_number) {
|
||||
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
const int nb_removed = requests_.remove(id, reply);
|
||||
@@ -232,6 +239,7 @@ void MusicBrainzClient::RequestFinished(QNetworkReply *reply, const int id, cons
|
||||
|
||||
void MusicBrainzClient::DiscIdRequestFinished(const QString &discid, QNetworkReply *reply) {
|
||||
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
ResultList ret;
|
||||
|
||||
@@ -51,6 +51,7 @@ class MusicBrainzClient : public QObject {
|
||||
// The second argument allows for specifying a custom network access manager.
|
||||
// It is used in tests. The ownership of network is not transferred.
|
||||
explicit MusicBrainzClient(QObject *parent = nullptr, QNetworkAccessManager *network = nullptr);
|
||||
~MusicBrainzClient();
|
||||
|
||||
struct Result {
|
||||
Result() : duration_msec_(0), track_(0), year_(-1) {}
|
||||
|
||||
@@ -2029,9 +2029,9 @@ void Playlist::UpdateScrobblePoint(const qint64 seek_point_nanosec) {
|
||||
void Playlist::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result) {
|
||||
|
||||
// Update art_manual for local songs that are not in the collection.
|
||||
if (result.type == AlbumCoverLoaderResult::Type_Manual && result.cover_url.isLocalFile() && (song.source() == Song::Source_LocalFile || song.source() == Song::Source_CDDA || song.source() == Song::Source_Device)) {
|
||||
if (((result.type == AlbumCoverLoaderResult::Type_Manual && result.cover_url.isLocalFile()) || result.type == AlbumCoverLoaderResult::Type_ManuallyUnset) && (song.source() == Song::Source_LocalFile || song.source() == Song::Source_CDDA || song.source() == Song::Source_Device)) {
|
||||
PlaylistItemPtr item = current_item();
|
||||
if (item && item->Metadata() == song && !item->Metadata().art_manual_is_valid()) {
|
||||
if (item && item->Metadata() == song && (!item->Metadata().art_manual_is_valid() || (result.type == AlbumCoverLoaderResult::Type_ManuallyUnset && !item->Metadata().has_manually_unset_cover()))) {
|
||||
qLog(Debug) << "Updating art manual for local song" << song.title() << song.album() << song.title() << "to" << result.cover_url << "in playlist.";
|
||||
item->SetArtManual(result.cover_url);
|
||||
Save();
|
||||
|
||||
@@ -189,6 +189,7 @@ class Playlist : public QAbstractListModel {
|
||||
|
||||
int current_row() const;
|
||||
int last_played_row() const;
|
||||
void reset_last_played() { last_played_item_index_ = QPersistentModelIndex(); }
|
||||
int next_row(bool ignore_repeat_track = false) const;
|
||||
int previous_row(bool ignore_repeat_track = false) const;
|
||||
|
||||
|
||||
@@ -46,7 +46,9 @@ AudioScrobbler::AudioScrobbler(Application *app, QObject *parent) :
|
||||
offline_(false),
|
||||
scrobble_button_(false),
|
||||
love_button_(false),
|
||||
submit_delay_(0)
|
||||
submit_delay_(0),
|
||||
prefer_albumartist_(false),
|
||||
show_auth_error_(false)
|
||||
{
|
||||
|
||||
scrobbler_services_->AddService(new LastFMScrobbler(app_, scrobbler_services_));
|
||||
@@ -73,6 +75,7 @@ void AudioScrobbler::ReloadSettings() {
|
||||
love_button_ = s.value("love_button", false).toBool();
|
||||
submit_delay_ = s.value("submit", 0).toInt();
|
||||
prefer_albumartist_ = s.value("albumartist", false).toBool();
|
||||
show_auth_error_ = s.value("show_auth_error", true).toBool();
|
||||
s.endGroup();
|
||||
|
||||
emit ScrobblingEnabledChanged(enabled_);
|
||||
|
||||
@@ -46,6 +46,7 @@ class AudioScrobbler : public QObject {
|
||||
bool LoveButton() const { return love_button_; }
|
||||
int SubmitDelay() const { return submit_delay_; }
|
||||
bool PreferAlbumArtist() const { return prefer_albumartist_; }
|
||||
bool ShowAuthError() const { return show_auth_error_; }
|
||||
|
||||
void UpdateNowPlaying(const Song &song);
|
||||
void ClearPlaying();
|
||||
@@ -85,6 +86,7 @@ class AudioScrobbler : public QObject {
|
||||
bool love_button_;
|
||||
int submit_delay_;
|
||||
bool prefer_albumartist_;
|
||||
bool show_auth_error_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <QTimer>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/application.h"
|
||||
@@ -53,12 +54,12 @@
|
||||
|
||||
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::kOAuthAuthorizeUrl = "https://musicbrainz.org/oauth2/authorize";
|
||||
const char *ListenBrainzScrobbler::kOAuthAccessTokenUrl = "https://musicbrainz.org/oauth2/token";
|
||||
const char *ListenBrainzScrobbler::kOAuthRedirectUrl = "http://localhost";
|
||||
const char *ListenBrainzScrobbler::kApiUrl = "https://api.listenbrainz.org";
|
||||
const char *ListenBrainzScrobbler::kClientID = "oeAUNwqSQer0er09Fiqi0Q";
|
||||
const char *ListenBrainzScrobbler::kClientSecret = "ROFghkeQ3F3oPyEhqiyWPA";
|
||||
const char *ListenBrainzScrobbler::kClientIDB64 = "b2VBVU53cVNRZXIwZXIwOUZpcWkwUQ==";
|
||||
const char *ListenBrainzScrobbler::kClientSecretB64 = "Uk9GZ2hrZVEzRjNvUHlFaHFpeVdQQQ==";
|
||||
const char *ListenBrainzScrobbler::kCacheFile = "listenbrainzscrobbler.cache";
|
||||
const int ListenBrainzScrobbler::kScrobblesPerRequest = 10;
|
||||
|
||||
@@ -69,16 +70,35 @@ ListenBrainzScrobbler::ListenBrainzScrobbler(Application *app, QObject *parent)
|
||||
server_(nullptr),
|
||||
enabled_(false),
|
||||
expires_in_(-1),
|
||||
login_time_(0),
|
||||
submitted_(false),
|
||||
scrobbled_(false),
|
||||
timestamp_(0) {
|
||||
|
||||
refresh_login_timer_.setSingleShot(true);
|
||||
connect(&refresh_login_timer_, SIGNAL(timeout()), SLOT(RequestAccessToken()));
|
||||
|
||||
ReloadSettings();
|
||||
LoadSession();
|
||||
|
||||
}
|
||||
|
||||
ListenBrainzScrobbler::~ListenBrainzScrobbler() {}
|
||||
ListenBrainzScrobbler::~ListenBrainzScrobbler() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
if (server_) {
|
||||
disconnect(server_, nullptr, this, nullptr);
|
||||
if (server_->isListening()) server_->close();
|
||||
server_->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ListenBrainzScrobbler::ReloadSettings() {
|
||||
|
||||
@@ -98,16 +118,25 @@ void ListenBrainzScrobbler::LoadSession() {
|
||||
expires_in_ = s.value("expires_in", -1).toInt();
|
||||
token_type_ = s.value("token_type").toString();
|
||||
refresh_token_ = s.value("refresh_token").toString();
|
||||
login_time_ = s.value("login_time").toLongLong();
|
||||
s.endGroup();
|
||||
|
||||
if (!refresh_token_.isEmpty()) {
|
||||
qint64 time = expires_in_ - (QDateTime::currentDateTime().toTime_t() - login_time_);
|
||||
if (time < 6) time = 6;
|
||||
refresh_login_timer_.setInterval(time * kMsecPerSec);
|
||||
refresh_login_timer_.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ListenBrainzScrobbler::Logout() {
|
||||
|
||||
access_token_.clear();
|
||||
expires_in_ = -1;
|
||||
token_type_.clear();
|
||||
refresh_token_.clear();
|
||||
expires_in_ = -1;
|
||||
login_time_ = 0;
|
||||
|
||||
QSettings settings;
|
||||
settings.beginGroup(kSettingsGroup);
|
||||
@@ -122,7 +151,8 @@ void ListenBrainzScrobbler::Logout() {
|
||||
void ListenBrainzScrobbler::Authenticate(const bool https) {
|
||||
|
||||
if (!server_) {
|
||||
server_ = new LocalRedirectServer(https, this);
|
||||
server_ = new LocalRedirectServer(this);
|
||||
server_->set_https(https);
|
||||
if (!server_->Listen()) {
|
||||
AuthError(server_->error());
|
||||
delete server_;
|
||||
@@ -132,15 +162,15 @@ void ListenBrainzScrobbler::Authenticate(const bool https) {
|
||||
connect(server_, SIGNAL(Finished()), this, SLOT(RedirectArrived()));
|
||||
}
|
||||
|
||||
QUrl redirect_url(kRedirectUrl);
|
||||
QUrl redirect_url(kOAuthRedirectUrl);
|
||||
redirect_url.setPort(server_->url().port());
|
||||
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem("response_type", "code");
|
||||
url_query.addQueryItem("client_id", kClientID);
|
||||
url_query.addQueryItem("client_id", QByteArray::fromBase64(kClientIDB64));
|
||||
url_query.addQueryItem("redirect_uri", redirect_url.toString());
|
||||
url_query.addQueryItem("scope", "profile;email;tag;rating;collection;submit_isrc;submit_barcode");
|
||||
QUrl url(kAuthUrl);
|
||||
QUrl url(kOAuthAuthorizeUrl);
|
||||
url.setQuery(url_query);
|
||||
|
||||
bool result = QDesktopServices::openUrl(url);
|
||||
@@ -164,7 +194,7 @@ void ListenBrainzScrobbler::RedirectArrived() {
|
||||
AuthError(QUrlQuery(url).queryItemValue("error"));
|
||||
}
|
||||
else if (url_query.hasQueryItem("code")) {
|
||||
RequestSession(url, url_query.queryItemValue("code"));
|
||||
RequestAccessToken(url, url_query.queryItemValue("code"));
|
||||
}
|
||||
else {
|
||||
AuthError(tr("Redirect missing token code!"));
|
||||
@@ -184,27 +214,48 @@ void ListenBrainzScrobbler::RedirectArrived() {
|
||||
|
||||
}
|
||||
|
||||
void ListenBrainzScrobbler::RequestSession(const QUrl &url, const QString &token) {
|
||||
void ListenBrainzScrobbler::RequestAccessToken(const QUrl &redirect_url, const QString &code) {
|
||||
|
||||
refresh_login_timer_.stop();
|
||||
|
||||
ParamList params = ParamList() << Param("client_id", QByteArray::fromBase64(kClientIDB64))
|
||||
<< Param("client_secret", QByteArray::fromBase64(kClientSecretB64));
|
||||
|
||||
if (!code.isEmpty() && !redirect_url.isEmpty()) {
|
||||
params << Param("grant_type", "authorization_code");
|
||||
params << Param("code", code);
|
||||
params << Param("redirect_uri", redirect_url.toString());
|
||||
}
|
||||
else if (!refresh_token_.isEmpty() && enabled_) {
|
||||
params << Param("grant_type", "refresh_token");
|
||||
params << Param("refresh_token", refresh_token_);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
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());
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl session_url(kOAuthAccessTokenUrl);
|
||||
|
||||
QNetworkRequest req(session_url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
|
||||
QNetworkReply *reply = network_->post(req, query);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { AuthenticateReplyFinished(reply); });
|
||||
|
||||
}
|
||||
|
||||
void ListenBrainzScrobbler::AuthenticateReplyFinished(QNetworkReply *reply) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray data;
|
||||
@@ -256,7 +307,7 @@ void ListenBrainzScrobbler::AuthenticateReplyFinished(QNetworkReply *reply) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("access_token") || !json_obj.contains("expires_in") || !json_obj.contains("token_type") || !json_obj.contains("refresh_token")) {
|
||||
if (!json_obj.contains("access_token") || !json_obj.contains("expires_in") || !json_obj.contains("token_type")) {
|
||||
AuthError("Json access_token, expires_in or token_type is missing.");
|
||||
return;
|
||||
}
|
||||
@@ -264,7 +315,10 @@ void ListenBrainzScrobbler::AuthenticateReplyFinished(QNetworkReply *reply) {
|
||||
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();
|
||||
if (json_obj.contains("refresh_token")) {
|
||||
refresh_token_ = json_obj["refresh_token"].toString();
|
||||
}
|
||||
login_time_ = QDateTime::currentDateTime().toTime_t();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
@@ -272,10 +326,18 @@ void ListenBrainzScrobbler::AuthenticateReplyFinished(QNetworkReply *reply) {
|
||||
s.setValue("expires_in", expires_in_);
|
||||
s.setValue("token_type", token_type_);
|
||||
s.setValue("refresh_token", refresh_token_);
|
||||
s.setValue("login_time", login_time_);
|
||||
s.endGroup();
|
||||
|
||||
if (expires_in_ > 0) {
|
||||
refresh_login_timer_.setInterval(expires_in_ * kMsecPerSec);
|
||||
refresh_login_timer_.start();
|
||||
}
|
||||
|
||||
emit AuthenticationComplete(true);
|
||||
|
||||
qLog(Debug) << "ListenBrainz: Authentication was successful, got access token" << access_token_ << "expires in" << expires_in_;
|
||||
|
||||
DoSubmit();
|
||||
|
||||
}
|
||||
@@ -287,6 +349,7 @@ QNetworkReply *ListenBrainzScrobbler::CreateRequest(const QUrl &url, const QJson
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
req.setRawHeader("Authorization", QString("Token %1").arg(user_token_).toUtf8());
|
||||
QNetworkReply *reply = network_->post(req, json_doc.toJson());
|
||||
replies_ << reply;
|
||||
|
||||
//qLog(Debug) << "ListenBrainz: Sending request" << json_doc.toJson();
|
||||
|
||||
@@ -390,6 +453,9 @@ void ListenBrainzScrobbler::UpdateNowPlaying(const Song &song) {
|
||||
|
||||
void ListenBrainzScrobbler::UpdateNowPlayingRequestFinished(QNetworkReply *reply) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
@@ -438,12 +504,7 @@ void ListenBrainzScrobbler::Scrobble(const Song &song) {
|
||||
|
||||
cache_->Add(song, timestamp_);
|
||||
|
||||
if (app_->scrobbler()->IsOffline()) return;
|
||||
|
||||
if (!IsAuthenticated()) {
|
||||
emit ErrorMessage("ListenBrainz is not authenticated!");
|
||||
return;
|
||||
}
|
||||
if (app_->scrobbler()->IsOffline() || !IsAuthenticated()) return;
|
||||
|
||||
if (!submitted_) {
|
||||
submitted_ = true;
|
||||
@@ -471,7 +532,7 @@ void ListenBrainzScrobbler::DoSubmit() {
|
||||
|
||||
void ListenBrainzScrobbler::Submit() {
|
||||
|
||||
qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
qLog(Debug) << "ListenBrainz: Submitting scrobbles.";
|
||||
|
||||
submitted_ = false;
|
||||
|
||||
@@ -480,7 +541,7 @@ void ListenBrainzScrobbler::Submit() {
|
||||
QJsonArray array;
|
||||
int i(0);
|
||||
QList<quint64> list;
|
||||
for (ScrobblerCacheItem *item : cache_->List()) {
|
||||
for (ScrobblerCacheItemPtr item : cache_->List()) {
|
||||
if (item->sent_) continue;
|
||||
item->sent_ = true;
|
||||
++i;
|
||||
@@ -517,6 +578,9 @@ void ListenBrainzScrobbler::Submit() {
|
||||
|
||||
void ListenBrainzScrobbler::ScrobbleRequestFinished(QNetworkReply *reply, QList<quint64> list) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QJsonDocument>
|
||||
#include <QTimer>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "scrobblerservice.h"
|
||||
@@ -77,6 +78,7 @@ class ListenBrainzScrobbler : public ScrobblerService {
|
||||
private slots:
|
||||
void RedirectArrived();
|
||||
void AuthenticateReplyFinished(QNetworkReply *reply);
|
||||
void RequestAccessToken(const QUrl &redirect_url = QUrl(), const QString &code = QString());
|
||||
void UpdateNowPlayingRequestFinished(QNetworkReply *reply);
|
||||
void ScrobbleRequestFinished(QNetworkReply *reply, QList<quint64>);
|
||||
|
||||
@@ -84,19 +86,18 @@ class ListenBrainzScrobbler : public ScrobblerService {
|
||||
QNetworkReply *CreateRequest(const QUrl &url, const QJsonDocument &json_doc);
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
|
||||
void RequestSession(const QUrl &url, const QString &token);
|
||||
void AuthError(const QString &error);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
void DoSubmit();
|
||||
void CheckScrobblePrevSong();
|
||||
|
||||
static const char *kAuthUrl;
|
||||
static const char *kAuthTokenUrl;
|
||||
static const char *kOAuthAuthorizeUrl;
|
||||
static const char *kOAuthAccessTokenUrl;
|
||||
static const char *kOAuthRedirectUrl;
|
||||
static const char *kApiUrl;
|
||||
static const char *kClientID;
|
||||
static const char *kClientSecret;
|
||||
static const char *kClientIDB64;
|
||||
static const char *kClientSecretB64;
|
||||
static const char *kCacheFile;
|
||||
static const char *kRedirectUrl;
|
||||
static const int kScrobblesPerRequest;
|
||||
|
||||
Application *app_;
|
||||
@@ -109,10 +110,14 @@ class ListenBrainzScrobbler : public ScrobblerService {
|
||||
qint64 expires_in_;
|
||||
QString token_type_;
|
||||
QString refresh_token_;
|
||||
quint64 login_time_;
|
||||
bool submitted_;
|
||||
Song song_playing_;
|
||||
bool scrobbled_;
|
||||
quint64 timestamp_;
|
||||
QTimer refresh_login_timer_;
|
||||
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "memory"
|
||||
|
||||
#include <QObject>
|
||||
#include <QStandardPaths>
|
||||
#include <QHash>
|
||||
@@ -48,7 +50,9 @@ ScrobblerCache::ScrobblerCache(const QString &filename, QObject *parent) :
|
||||
loaded_ = true;
|
||||
}
|
||||
|
||||
ScrobblerCache::~ScrobblerCache() {}
|
||||
ScrobblerCache::~ScrobblerCache() {
|
||||
scrobbler_cache_.clear();
|
||||
}
|
||||
|
||||
void ScrobblerCache::ReadCache() {
|
||||
|
||||
@@ -130,8 +134,7 @@ void ScrobblerCache::ReadCache() {
|
||||
continue;
|
||||
}
|
||||
if (scrobbler_cache_.contains(timestamp)) continue;
|
||||
ScrobblerCacheItem *item = new ScrobblerCacheItem(artist, album, song, albumartist, track, duration, timestamp);
|
||||
scrobbler_cache_.insert(timestamp, item);
|
||||
scrobbler_cache_.insert(timestamp, std::make_shared<ScrobblerCacheItem>(artist, album, song, albumartist, track, duration, timestamp));
|
||||
|
||||
}
|
||||
|
||||
@@ -143,11 +146,17 @@ void ScrobblerCache::WriteCache() {
|
||||
|
||||
qLog(Debug) << "Writing scrobbler cache file" << filename_;
|
||||
|
||||
if (scrobbler_cache_.isEmpty()) {
|
||||
QFile file(filename_);
|
||||
if (file.exists()) file.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray array;
|
||||
|
||||
QHash <quint64, ScrobblerCacheItem*> ::iterator i;
|
||||
QHash <quint64, std::shared_ptr<ScrobblerCacheItem>> ::iterator i;
|
||||
for (i = scrobbler_cache_.begin() ; i != scrobbler_cache_.end() ; ++i) {
|
||||
ScrobblerCacheItem *item = i.value();
|
||||
ScrobblerCacheItemPtr item = i.value();
|
||||
QJsonObject object;
|
||||
object.insert("timestamp", QJsonValue::fromVariant(item->timestamp_));
|
||||
object.insert("artist", QJsonValue::fromVariant(item->artist_));
|
||||
@@ -176,7 +185,7 @@ void ScrobblerCache::WriteCache() {
|
||||
|
||||
}
|
||||
|
||||
ScrobblerCacheItem *ScrobblerCache::Add(const Song &song, const quint64 ×tamp) {
|
||||
ScrobblerCacheItemPtr ScrobblerCache::Add(const Song &song, const quint64 ×tamp) {
|
||||
|
||||
if (scrobbler_cache_.contains(timestamp)) return nullptr;
|
||||
|
||||
@@ -187,7 +196,7 @@ ScrobblerCacheItem *ScrobblerCache::Add(const Song &song, const quint64 ×ta
|
||||
album.remove(Song::kAlbumRemoveMisc);
|
||||
title.remove(Song::kTitleRemoveMisc);
|
||||
|
||||
ScrobblerCacheItem *item = new ScrobblerCacheItem(song.artist(), album, title, song.albumartist(), song.track(), song.length_nanosec(), timestamp);
|
||||
ScrobblerCacheItemPtr item = std::make_shared<ScrobblerCacheItem>(song.artist(), album, title, song.albumartist(), song.track(), song.length_nanosec(), timestamp);
|
||||
scrobbler_cache_.insert(timestamp, item);
|
||||
|
||||
if (loaded_) DoInAMinuteOrSo(this, SLOT(WriteCache()));
|
||||
@@ -196,7 +205,7 @@ ScrobblerCacheItem *ScrobblerCache::Add(const Song &song, const quint64 ×ta
|
||||
|
||||
}
|
||||
|
||||
ScrobblerCacheItem *ScrobblerCache::Get(const quint64 hash) {
|
||||
ScrobblerCacheItemPtr ScrobblerCache::Get(const quint64 hash) {
|
||||
|
||||
if (scrobbler_cache_.contains(hash)) { return scrobbler_cache_.value(hash); }
|
||||
else return nullptr;
|
||||
@@ -210,27 +219,29 @@ void ScrobblerCache::Remove(const quint64 hash) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete scrobbler_cache_.take(hash);
|
||||
scrobbler_cache_.remove(hash);
|
||||
|
||||
}
|
||||
|
||||
void ScrobblerCache::Remove(ScrobblerCacheItem &item) {
|
||||
delete scrobbler_cache_.take(item.timestamp_);
|
||||
void ScrobblerCache::Remove(ScrobblerCacheItemPtr item) {
|
||||
scrobbler_cache_.remove(item->timestamp_);
|
||||
}
|
||||
|
||||
void ScrobblerCache::ClearSent(const QList<quint64> list) {
|
||||
for (quint64 timestamp : list) {
|
||||
void ScrobblerCache::ClearSent(const QList<quint64> &list) {
|
||||
|
||||
for (const quint64 timestamp : list) {
|
||||
if (!scrobbler_cache_.contains(timestamp)) continue;
|
||||
ScrobblerCacheItem *item = scrobbler_cache_.take(timestamp);
|
||||
ScrobblerCacheItemPtr item = scrobbler_cache_.take(timestamp);
|
||||
item->sent_ = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ScrobblerCache::Flush(const QList<quint64> list) {
|
||||
void ScrobblerCache::Flush(const QList<quint64> &list) {
|
||||
|
||||
for (quint64 timestamp : list) {
|
||||
for (const quint64 timestamp : list) {
|
||||
if (!scrobbler_cache_.contains(timestamp)) continue;
|
||||
delete scrobbler_cache_.take(timestamp);
|
||||
scrobbler_cache_.remove(timestamp);
|
||||
}
|
||||
DoInAMinuteOrSo(this, SLOT(WriteCache()));
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user