Compare commits

...

129 Commits

Author SHA1 Message Date
Jonas Kvinge
783cf7f1b0 Release 0.6.12 2020-06-07 23:49:11 +02:00
Jonas Kvinge
92d6fc3fad Change default grouping to album disc 2020-06-07 23:36:37 +02:00
Jonas Kvinge
436cdea4fd Add libgstfaac.dll 2020-06-06 15:08:30 +02:00
Jonas Kvinge
6b144b3b1f Add faac to nsi 2020-06-06 14:56:31 +02:00
Jonas Kvinge
7820082eb8 Add windows builder 2020-06-06 01:50:40 +02:00
Strawbs Bot
e732f921a3 Update translations 2020-06-06 01:02:52 +02:00
Jonas Kvinge
8b7c5d8585 Fix saving OSD pretty settings 2020-06-05 23:46:07 +02:00
Jonas Kvinge
9e959b189c Save settings when tabbar colors are changed 2020-06-05 23:39:22 +02:00
Jonas Kvinge
5c7a4cdc22 Fix deprecated use of QProcess::startDetached 2020-06-05 22:15:58 +02:00
Strawbs Bot
756e619e07 Update translations 2020-06-05 01:02:01 +02:00
Jonas Kvinge
eea6194b90 Add liborc-0.4-0.dll to nsi 2020-06-04 07:41:49 +02:00
Strawbs Bot
a09c1fa154 Update translations 2020-06-04 01:01:40 +02:00
Strawbs Bot
38c742328c Update translations 2020-05-31 01:01:44 +02:00
Jonas Kvinge
1d5db1446d Sort folders added from file view
Fixes #449
2020-05-30 21:59:55 +02:00
Jonas Kvinge
3f5f3d143f Fix compile without gstreamer 2020-05-30 21:39:16 +02:00
Jonas Kvinge
d297a7198a Update CI 2020-05-30 03:49:37 +02:00
Strawbs Bot
e5bd99dee4 Update translations 2020-05-30 01:04:25 +02:00
Jonas Kvinge
2720e13e88 Update libprotobuf DLL 2020-05-30 00:20:16 +02:00
Jonas Kvinge
0e9c1789ff Dont append disc to album title
Fixes #438
2020-05-29 20:38:19 +02:00
Jonas Kvinge
281cb10f84 Fix shadowing member 2020-05-29 18:30:27 +02:00
Jonas Kvinge
69a92ffe72 Remove mistankely added include 2020-05-29 18:30:09 +02:00
Jonas Kvinge
6447f159e5 Fix endl 2020-05-29 18:06:50 +02:00
Jonas Kvinge
94430883ad Use Qt::endl with Qt 5.14 and higher 2020-05-29 18:04:31 +02:00
Jonas Kvinge
046512eb3d Use Qt::endl 2020-05-29 17:47:26 +02:00
Jonas Kvinge
4479d97e90 Change use of Qt::KeyboardModifiers 2020-05-29 17:47:10 +02:00
Jonas Kvinge
bf5fea8951 Replace use of QMultiMap::insertMulti with QMultiMap::insert 2020-05-29 17:46:41 +02:00
Jonas Kvinge
07282e3de6 Change use of QLabel::pixmap 2020-05-29 17:45:00 +02:00
Jonas Kvinge
c2f90a20df Replace hex with Qt::hex 2020-05-29 17:43:44 +02:00
Jonas Kvinge
481d2d699e Replace QWheelEvent::delta with QWheelEvent::angleDelta 2020-05-29 17:42:40 +02:00
Jonas Kvinge
cf9a7e6ed3 Dont do qsrand on Qt versions higher than Qt 5.10 2020-05-29 17:40:34 +02:00
Jonas Kvinge
c35235371a Replace QString::SkipEmptyParts with Qt::SkipEmptyParts on Qt 5.14.0 or higher 2020-05-29 17:40:11 +02:00
Jonas Kvinge
5d5723ad58 Replace qrand with QRandomGenerator when using Qt 5.10 or higher 2020-05-29 17:37:46 +02:00
Jonas Kvinge
6c77294a86 Fixes to imobiledeviceconnection support 2020-05-29 17:36:01 +02:00
Strawbs Bot
823f65f1ca Add Czech 2020-05-29 01:52:17 +02:00
Strawbs Bot
0c378c1642 Update translations 2020-05-29 01:05:58 +02:00
Jonas Kvinge
bbb4162867 Make it possible to maximize console dialog 2020-05-26 18:27:01 +02:00
Jonas Kvinge
5dbdde3f2b Make scrobbler show error dialog for all errors when option is on 2020-05-26 17:51:23 +02:00
Jonas Kvinge
2521954bd9 Change layout name 2020-05-26 00:02:16 +02:00
Jonas Kvinge
5f1002894e Only save settings that has been changed 2020-05-25 23:56:54 +02:00
Jonas Kvinge
0489b312a3 Fix CircleCI 2020-05-25 23:56:29 +02:00
Jonas Kvinge
732be5a34f Use g_free 2020-05-24 23:22:18 +02:00
Jonas Kvinge
1f45c78ebb Unmap buffer references in error cases 2020-05-24 23:21:26 +02:00
Strawbs Bot
8b86c79bf9 Update translations 2020-05-18 01:01:33 +02:00
Jonas Kvinge
710ed81067 Require sqlite on centos too 2020-05-17 19:36:37 +02:00
Jonas Kvinge
24ac0e7b9b Fix CircleCI tumbleweed build 2020-05-17 18:34:25 +02:00
Jonas Kvinge
15b2bfbb29 Set library paths for Linux too when USE_BUNDLE is set 2020-05-17 17:01:35 +02:00
Jonas Kvinge
b7494eb381 Increase about dialog height 2020-05-17 16:25:18 +02:00
Strawbs Bot
27ac590250 Update translations 2020-05-17 01:01:49 +02:00
Jonas Kvinge
972076edab Move ClearDiskCache connect 2020-05-16 19:35:25 +02:00
Jonas Kvinge
bfa9a1eb8a Fix tests
Fixes #440
2020-05-16 19:17:06 +02:00
Jonas Kvinge
b0966f14e6 Only connect ClearPixmapDiskCache if app is set 2020-05-16 18:25:13 +02:00
Jonas Kvinge
37cf0c2fb6 Turn back git revision 2020-05-16 18:24:51 +02:00
Jonas Kvinge
4eb11c32b0 Release 0.6.11 2020-05-16 14:40:35 +02:00
Jonas Kvinge
25457bc09a Release 0.6.11 2020-05-16 14:39:53 +02:00
Jonas Kvinge
d5cfb5f733 Merge branch 'master' of github.com:strawberrymusicplayer/strawberry 2020-05-16 14:14:34 +02:00
Jonas Kvinge
79ba6e628e Use art id from the API as cover filename for Subsonic
Fixes #433
2020-05-16 14:13:22 +02:00
King_DuckZ
ef73add05a Warning fix on gcc 8.3.0 (#439)
Fixes warning:
assuming signed overflow does not occur when
simplifying conditional to constant [-Wstrict-overflow]

Signed-off-by: Michele Santullo <m.santullo@posteo.net>

Co-authored-by: Michele Santullo <m.santullo@posteo.net>
2020-05-16 13:34:42 +02:00
Strawbs Bot
ec3d11fb27 Update translations 2020-05-16 01:03:55 +02:00
Jonas Kvinge
3fbc7031b5 Update about dialog 2020-05-16 00:45:15 +02:00
Jonas Kvinge
40beb5e428 Update Changelog 2020-05-15 23:54:15 +02:00
Jonas Kvinge
0f608c8ef0 Update debian/copyright 2020-05-15 23:48:44 +02:00
Jonas Kvinge
8509cb4743 Spotify: Fix clearing access token 2020-05-15 23:36:01 +02:00
Jonas Kvinge
f4429e8c4a Make Musicbrainz cover provider respect rate limiting 2020-05-15 22:53:21 +02:00
Jonas Kvinge
e9e0829cdc Remove end dash from title 2020-05-15 22:15:52 +02:00
Strawbs Bot
93f0230423 Update translations 2020-05-15 01:03:01 +02:00
plonibarploni
f26a0df4a4 strip directory from OpenInFileManager command (#436) 2020-05-14 22:19:26 +02:00
Jonas Kvinge
c7d4624282 Update README.md 2020-05-14 22:12:55 +02:00
Jonas Kvinge
b03eee2a22 Update Changelog 2020-05-14 22:10:18 +02:00
Jonas Kvinge
7d4d72e706 Make Discogs provider respect rate limiting 2020-05-14 19:31:40 +02:00
Jonas Kvinge
e3c367984b Make it possible to receive SearchResults before SearchFinished 2020-05-14 19:30:29 +02:00
Jonas Kvinge
0ebfa10d32 Update details in playing widget 2020-05-14 19:29:34 +02:00
Jonas Kvinge
16d9a077f0 emit SearchCoverInProgress before SearchCoverAutomatically 2020-05-14 19:29:07 +02:00
Strawbs Bot
a9d8bbad42 Update translations 2020-05-14 01:03:53 +02:00
Jonas Kvinge
d78bb94af3 Fix Tidal OAuth login 2020-05-13 21:56:11 +02:00
Jonas Kvinge
b139c0a824 Dont use song count from backend for CDDA devices 2020-05-13 19:42:13 +02:00
Jonas Kvinge
5b0b924d34 Fix crash in CD songloader 2020-05-13 19:00:57 +02:00
Strawbs Bot
f75acf820c Update translations 2020-05-13 01:05:26 +02:00
Jonas Kvinge
43a47f33ac Dont link chromaprint unless its enabled
Fixes #432
2020-05-12 22:39:56 +02:00
Jonas Kvinge
fcea3a0877 Add option to scrobbler setting for showing login error
Fixes #430
2020-05-12 22:25:00 +02:00
Jonas Kvinge
a950ec3bd5 Adjust login state widget placement for covers and lyrics settings 2020-05-12 22:15:53 +02:00
Jonas Kvinge
e35501ff0a Delete remaining network replies and local redirct server in destructor 2020-05-12 21:28:42 +02:00
Jonas Kvinge
4bfad9dad8 Fix use of QString::right() 2020-05-12 21:12:08 +02:00
Jonas Kvinge
c5c7a07c12 Add QImageReader::imageFormatsForMimeType replacement function 2020-05-12 19:48:37 +02:00
Jonas Kvinge
7e22e0e552 Use original image format when saving images from Subsonic and Tidal
Fixes #435
2020-05-12 18:50:57 +02:00
Jonas Kvinge
84ec4bdc79 Check content type for image in album cover fetcher search 2020-05-12 18:47:32 +02:00
Jonas Kvinge
2bcad9b637 Do AddOrUpdateSongs in database thread 2020-05-12 18:45:24 +02:00
Jonas Kvinge
c8d5f03070 Dont use reference in AlbumSongsReplyReceived 2020-05-12 15:58:36 +02:00
Jonas Kvinge
168e101a5a Subsonic: Disconnect signal/slots 2020-05-12 15:55:13 +02:00
Jonas Kvinge
b4bc7333d9 Use album id as cover filename for Subsonic
Fixes #433
2020-05-12 15:53:15 +02:00
Strawbs Bot
e8b58c940e Update translations 2020-05-11 01:03:48 +02:00
Jonas Kvinge
ec7202e3f6 Use refresh token for ListenBrainz 2020-05-11 00:51:18 +02:00
Jonas Kvinge
9a740f7962 Change variable name 2020-05-11 00:49:54 +02:00
Jonas Kvinge
9210fdee0d Make spotify refresh login 2020-05-10 17:10:20 +02:00
Jonas Kvinge
d7661f0964 Fix possible crash in album cover fetcher 2020-05-10 16:54:14 +02:00
Jonas Kvinge
139e148912 Use shared_ptr for scrobbler cache items 2020-05-10 14:59:04 +02:00
Jonas Kvinge
1b8dedb4ed Clear access token when login is expired 2020-05-10 14:53:40 +02:00
Jonas Kvinge
5d6b0fa329 Reset last played song when playlist is finished 2020-05-10 13:08:29 +02:00
Jonas Kvinge
f35bbd89c9 Initialize QNetworkReply pointer 2020-05-10 12:56:12 +02:00
Jonas Kvinge
538a9e42f4 Remove these 2020-05-10 12:50:37 +02:00
Jonas Kvinge
623147dea7 Add Json cover provider class 2020-05-10 12:49:11 +02:00
Jonas Kvinge
dfecd0cd12 Show Json parse error 2020-05-10 12:48:48 +02:00
Jonas Kvinge
fe3af3a676 Clear albums on close in cover manager 2020-05-10 11:50:05 +02:00
Strawbs Bot
25f60331ed Update translations 2020-05-10 01:04:15 +02:00
Jonas Kvinge
d4860a3426 Use defaults from context UI 2020-05-09 18:37:43 +02:00
Jonas Kvinge
e7e77ed86b Add automatically search for album cover to context settings 2020-05-09 18:31:10 +02:00
Jonas Kvinge
dc80459c59 Remove debug print 2020-05-09 02:30:32 +02:00
Jonas Kvinge
2f2de59234 Fix AuthError function 2020-05-09 02:07:51 +02:00
Jonas Kvinge
7bccc21878 Add setting for cover providers 2020-05-09 01:48:08 +02:00
Strawbs Bot
40f9dafa44 Update translations 2020-05-09 01:04:23 +02:00
Jonas Kvinge
355d436d29 Sort settings pages 2020-05-08 20:25:02 +02:00
Jonas Kvinge
079b684388 Remove duplicate include 2020-05-08 20:17:33 +02:00
Jonas Kvinge
fd11f46d30 Add album cover provider from Musixmatch 2020-05-08 20:14:16 +02:00
Jonas Kvinge
cb7099199a Fix memory include 2020-05-08 18:47:55 +02:00
Jonas Kvinge
8566d91e89 Remove some unneeded includes, etc 2020-05-08 18:44:07 +02:00
Jonas Kvinge
f44ce49ea7 Add setting for lyric providers and add more providers
Fixes #335
2020-05-08 18:35:36 +02:00
Jonas Kvinge
6ef69f6b32 Format code 2020-05-08 18:34:33 +02:00
Strawbs Bot
f5983d5f10 Update translations 2020-05-07 01:03:30 +02:00
Jonas Kvinge
54cce5e089 Use album grouping function 2020-05-06 22:35:55 +02:00
Jonas Kvinge
4e4e596a1e Change some parameters to const 2020-05-06 22:26:29 +02:00
Jonas Kvinge
727a1f5ad1 Sort songs in collection by song title instead of track if previous
grouping is not the album.

Fixes #295
2020-05-06 22:14:59 +02:00
Jonas Kvinge
85fa86625b Fix infinite loop in stylesheetloader
Fixes #361
2020-05-06 21:43:44 +02:00
Jonas Kvinge
2c91877f83 Add option to show/hide sidebar
Fixes #393
2020-05-06 18:15:17 +02:00
Jonas Kvinge
7d1fac44e9 Update non collection songs with manually unset cover 2020-05-05 23:57:37 +02:00
Jonas Kvinge
2e34abfc0d Fix mpris:artUrl when using embedded cover
Fixes #426
2020-05-04 23:23:24 +02:00
Jonas Kvinge
81ba63e247 Turn on git revision 2020-05-02 15:18:39 +02:00
Strawbs Bot
8b11a65522 Update translations 2020-05-02 01:01:45 +02:00
Jonas Kvinge
7190ad1d15 Remove styles directory when uninstalling 2020-05-01 19:55:45 +02:00
Jonas Kvinge
1c9bae5df5 Update libnettle dll in nsi 2020-05-01 18:41:36 +02:00
Jonas Kvinge
cc7fd73916 Update libhogweed dll in nsi 2020-05-01 16:55:58 +02:00
224 changed files with 16499 additions and 4723 deletions

View File

@@ -142,7 +142,6 @@ commands:
pulseaudio-libs-devel
libnotify-devel
gnutls-devel
qt5-devel
qt5-qtbase-devel
qt5-qtx11extras-devel
qt5-qttools-devel
@@ -161,6 +160,79 @@ commands:
hicolor-icon-theme
install_centos_dependencies:
description: Install CentOS dependencies
steps:
- run:
name: Install epel-release
command: dnf install -y epel-release
- run:
name: Install epel-release-latest-8.noarch.rpm
command: dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
- run:
name: Install config-manager
command: dnf install -y 'dnf-command(config-manager)'
- run:
name: PowerTools
command: dnf config-manager --set-enabled PowerTools
- run:
name: DNF Clean All
command: dnf clean all
- run:
name: Update packages
command: dnf update -y
- run:
name: Install CentOS dependencies
command: >
dnf install -y
glibc
gcc-c++
make
libtool
cmake3
rpmdevtools
redhat-lsb-core
git
man
tar
gettext
boost-devel
fuse-devel
dbus-devel
libnotify-devel
gnutls-devel
sqlite-devel
protobuf-devel
protobuf-compiler
alsa-lib-devel
pulseaudio-libs-devel
qt5-devel
qt5-qtbase-devel
qt5-qtx11extras-devel
qt5-qttools-devel
fftw-devel
libchromaprint-devel
libcdio-devel
libgpod-devel
libplist-devel
libusbmuxd-devel
libmtp-devel
libjpeg-devel
cairo-devel
dbus-x11
xorg-x11-server-Xvfb
xorg-x11-xauth
vim-common
desktop-file-utils
libappstream-glib
appstream-data
hicolor-icon-theme
python3-pip
python3-devel
gstreamer1-devel
gstreamer1-plugins-base-devel
install_mageia_dependencies:
description: Install Mageia dependencies
steps:
@@ -381,9 +453,9 @@ jobs:
- build_rpm
build_fedora_30:
build_fedora_31:
docker:
- image: fedora:30
- image: fedora:31
environment:
RPM_BUILD_NCPUS: "2"
steps:
@@ -393,9 +465,9 @@ jobs:
- build_source
- build_rpm
build_fedora_31:
build_fedora_32:
docker:
- image: fedora:31
- image: fedora:32
environment:
RPM_BUILD_NCPUS: "2"
steps:
@@ -405,6 +477,31 @@ jobs:
- build_source
- build_rpm
build_fedora_33:
docker:
- image: fedora:33
environment:
RPM_BUILD_NCPUS: "2"
steps:
- install_fedora_dependencies
- checkout
- cmake
- build_source
- build_rpm
build_centos_8:
docker:
- image: centos:8
environment:
RPM_BUILD_NCPUS: "2"
steps:
- install_centos_dependencies
- checkout
- cmake
- build_source
- build_rpm
build_mageia_7:
docker:
@@ -475,6 +572,15 @@ jobs:
- cmake
- build_deb
build_ubuntu_groovy:
docker:
- image: ubuntu:groovy
steps:
- install_ubuntu_dependencies
- checkout
- cmake
- build_deb
workflows:
version: 2
build_all:
@@ -500,11 +606,15 @@ workflows:
only: /.*/
- build_fedora_30:
- build_fedora_31:
filters:
tags:
only: /.*/
- build_fedora_31:
- build_fedora_32:
filters:
tags:
only: /.*/
- build_fedora_33:
filters:
tags:
only: /.*/
@@ -516,6 +626,12 @@ workflows:
only: /.*/
- build_centos_8:
filters:
tags:
only: /.*/
- build_debian_stretch:
filters:
tags:
@@ -542,3 +658,7 @@ workflows:
filters:
tags:
only: /.*/
- build_ubuntu_groovy:
filters:
tags:
only: /.*/

File diff suppressed because it is too large Load Diff

View File

@@ -40,9 +40,13 @@
#include <QSharedMemory>
#include <QLocalSocket>
#include <QByteArray>
#include <QDateTime>
#include <QElapsedTimer>
#include <QtDebug>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#else
# include <QDateTime>
#endif
#include "singleapplication.h"
#include "singleapplication_p.h"
@@ -110,8 +114,12 @@ SingleApplication::SingleApplication(int &argc, char *argv[], bool allowSecondar
d->memory->unlock();
// Random sleep here limits the probability of a collision between two racing apps
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
QThread::sleep(QRandomGenerator::global()->bounded(8u, 18u));
#else
qsrand(QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max());
QThread::sleep(8 + static_cast <unsigned long>(static_cast <float>(qrand()) / RAND_MAX * 10));
QThread::sleep(8 + static_cast<unsigned long>(static_cast <float>(qrand()) / RAND_MAX * 10));
#endif
}
if (inst->primary == false) {

View File

@@ -40,9 +40,13 @@
#include <QSharedMemory>
#include <QLocalSocket>
#include <QByteArray>
#include <QDateTime>
#include <QElapsedTimer>
#include <QtDebug>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#else
# include <QDateTime>
#endif
#include "singlecoreapplication.h"
#include "singlecoreapplication_p.h"
@@ -62,8 +66,7 @@ SingleCoreApplication::SingleCoreApplication(int &argc, char *argv[], bool allow
// Store the current mode of the program
d->options = options;
// Generating an application ID used for identifying the shared memory
// block and QLocalServer
// Generating an application ID used for identifying the shared memory block and QLocalServer
d->genBlockServerName();
#ifdef Q_OS_UNIX
@@ -111,8 +114,12 @@ SingleCoreApplication::SingleCoreApplication(int &argc, char *argv[], bool allow
d->memory->unlock();
// Random sleep here limits the probability of a collision between two racing apps
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
QThread::sleep(QRandomGenerator::global()->bounded(8u, 18u));
#else
qsrand(QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max());
QThread::sleep(8 + static_cast <unsigned long>(static_cast <float>(qrand()) / RAND_MAX * 10));
QThread::sleep(8 + static_cast<unsigned long>(static_cast <float>(qrand()) / RAND_MAX * 10));
#endif
}
if (inst->primary == false) {

View File

@@ -2,6 +2,45 @@ Strawberry Music Player
=======================
ChangeLog
0.6.12:
Bugfixes:
* Fixed height of about dialog.
Enhancements:
* Only save settings for pages that actually has been changed.
* Replaced use of deprecated Qt functionality as of 5.15.
* Made scrobbler show error dialog for all errors when show error dialog option is on.
* Dont append disc to album titles for Subsonic and Tidal.
* Sort folders added from file view.
* Changed default collection grouping to album - disc.
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:

View File

@@ -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

View File

@@ -1,6 +1,6 @@
set(STRAWBERRY_VERSION_MAJOR 0)
set(STRAWBERRY_VERSION_MINOR 6)
set(STRAWBERRY_VERSION_PATCH 10)
set(STRAWBERRY_VERSION_PATCH 12)
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
set(INCLUDE_GIT_REVISION OFF)

4
debian/control vendored
View File

@@ -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
View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -44,9 +44,7 @@ BuildRequires: pkgconfig(dbus-1)
BuildRequires: pkgconfig(gnutls)
BuildRequires: pkgconfig(alsa)
BuildRequires: pkgconfig(protobuf)
%if ! 0%{?centos}
BuildRequires: pkgconfig(sqlite3) >= 3.9
%endif
%if ! 0%{?centos} && ! 0%{?mageia}
BuildRequires: pkgconfig(taglib)
%endif
@@ -104,8 +102,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

View File

@@ -158,6 +158,7 @@ Section "Strawberry" Strawberry
File "libbz2.dll"
File "libcdio-19.dll"
File "libchromaprint.dll"
File "libfaac-0.dll"
File "libfaad-2.dll"
File "libffi-7.dll"
File "libfftw3-3.dll"
@@ -183,20 +184,20 @@ 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"
File "libpcre2-16-0.dll"
File "libpng16-16.dll"
File "libprotobuf-22.dll"
File "libprotobuf-23.dll"
File "libsoup-2.4-1.dll"
File "libspeex-1.dll"
File "libsqlite3-0.dll"
@@ -221,6 +222,7 @@ Section "Strawberry" Strawberry
File "libbrotlicommon.dll"
File "libbrotlidec.dll"
File "libpsl-5.dll"
File "liborc-0.4-0.dll"
!ifdef arch_x86
File "libgcc_s_sjlj-1.dll"
@@ -341,6 +343,7 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=libgstspeex.dll" "gstreamer-plugins\libgstspeex.dll"
File "/oname=libgstlame.dll" "gstreamer-plugins\libgstlame.dll"
File "/oname=libgstaiff.dll" "gstreamer-plugins\libgstaiff.dll"
File "/oname=libgstfaac.dll" "gstreamer-plugins\libgstfaac.dll"
File "/oname=libgstfaad.dll" "gstreamer-plugins\libgstfaad.dll"
File "/oname=libgstisomp4.dll" "gstreamer-plugins\libgstisomp4.dll"
File "/oname=libgstasf.dll" "gstreamer-plugins\libgstasf.dll"
@@ -422,6 +425,7 @@ Section "Uninstall"
Delete "$INSTDIR\libbz2.dll"
Delete "$INSTDIR\libcdio-19.dll"
Delete "$INSTDIR\libchromaprint.dll"
Delete "$INSTDIR\libfaac-0.dll"
Delete "$INSTDIR\libfaad-2.dll"
Delete "$INSTDIR\libffi-7.dll"
Delete "$INSTDIR\libfftw3-3.dll"
@@ -447,20 +451,20 @@ 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"
Delete "$INSTDIR\libpcre2-16-0.dll"
Delete "$INSTDIR\libpng16-16.dll"
Delete "$INSTDIR\libprotobuf-22.dll"
Delete "$INSTDIR\libprotobuf-23.dll"
Delete "$INSTDIR\libsoup-2.4-1.dll"
Delete "$INSTDIR\libspeex-1.dll"
Delete "$INSTDIR\libsqlite3-0.dll"
@@ -485,6 +489,7 @@ Section "Uninstall"
Delete "$INSTDIR\libbrotlicommon.dll"
Delete "$INSTDIR\libbrotlidec.dll"
Delete "$INSTDIR\libpsl-5.dll"
Delete "$INSTDIR\liborc-0.4-0.dll"
!ifdef arch_x86
Delete "$INSTDIR\libgcc_s_sjlj-1.dll"
@@ -550,6 +555,7 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\libgstspeex.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstlame.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaiff.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstfaac.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstfaad.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstisomp4.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstasf.dll"
@@ -592,6 +598,7 @@ Section "Uninstall"
; Remove the installation folders.
RMDir "$INSTDIR\platforms"
RMDir "$INSTDIR\styles"
RMDir "$INSTDIR\sqldrivers"
RMDir "$INSTDIR\imageformats"
RMDir "$INSTDIR\gio-modules"

View File

@@ -19,6 +19,9 @@
#include <QList>
#include <QTimer>
#include <QGenericArgument>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#endif
#include "closure.h"
@@ -72,6 +75,13 @@ void DoAfter(QObject *receiver, const char *slot, int msec) {
}
void DoInAMinuteOrSo(QObject *receiver, const char *slot) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
int msec = (60 + QRandomGenerator::global()->bounded(1, 60)) * kMsecPerSec;
#else
int msec = (60 + (qrand() % 60)) * kMsecPerSec;
#endif
DoAfter(receiver, slot, msec);
}

View File

@@ -292,7 +292,11 @@ QString DarwinDemangle(const QString &symbol);
QString DarwinDemangle(const QString &symbol) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList split = symbol.split(' ', Qt::SkipEmptyParts);
#else
QStringList split = symbol.split(' ', QString::SkipEmptyParts);
#endif
QString mangled_function = split[3];
return CXXDemangle(mangled_function);

View File

@@ -33,6 +33,9 @@
#include <QString>
#include <QStringList>
#include <QAtomicInt>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#endif
#include "core/logging.h"
@@ -264,7 +267,11 @@ void WorkerPool<HandlerType>::StartOneWorker(Worker *worker) {
// Create a server, find an unused name and start listening
forever {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
const int unique_number = QRandomGenerator::global()->bounded((int)(quint64(this) & 0xFFFFFFFF));
#else
const int unique_number = qrand() ^ ((int)(quint64(this) & 0xFFFFFFFF));
#endif
const QString name = QString("%1_%2").arg(local_server_name_).arg(unique_number);
if (worker->local_server_->listen(name)) {

View File

@@ -46,9 +46,11 @@ int main(int argc, char **argv) {
}
// Seed random number generator
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
timeval time;
gettimeofday(&time, nullptr);
qsrand((time.tv_sec * 1000) + (time.tv_usec / 1000));
#endif
logging::Init();
qLog(Info) << "TagReader worker connecting to" << args[1];
@@ -61,7 +63,9 @@ int main(int argc, char **argv) {
return 1;
}
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
QSslSocket::addDefaultCaCertificates(QSslCertificate::fromPath(":/certs/godaddy-root.pem", QSsl::Pem));
#endif
TagReaderWorker worker(&socket);

View File

@@ -35,14 +35,6 @@ void TagReaderWorker::MessageArrived(const pb::tagreader::Message& message) {
pb::tagreader::Message reply;
#if 0
// Crash every few requests
if (qrand() % 10 == 0) {
qLog(Debug) << "Crashing on request ID" << message.id();
abort();
}
#endif
if (message.has_read_file_request()) {
tag_reader_.ReadFile(QStringFromStdString(message.read_file_request().filename()), reply.mutable_read_file_response()->mutable_metadata());
}

View File

@@ -1,5 +1,5 @@
name: strawberry
version: '0.6.10+git'
version: '0.6.12+git'
summary: music player and collection organizer
description: |
Strawberry is a music player and collection organizer.

View File

@@ -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()

View File

@@ -112,7 +112,7 @@ void AnalyzerContainer::ShowPopupMenu() {
}
void AnalyzerContainer::wheelEvent(QWheelEvent *e) {
emit WheelEvent(e->delta());
emit WheelEvent(e->angleDelta().y());
}
void AnalyzerContainer::SetEngine(EngineBase *engine) {

View File

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

View File

@@ -186,6 +186,8 @@ class CollectionBackend : public CollectionBackendInterface {
Song::Source Source() const;
void AddOrUpdateSongsAsync(const SongList &songs);
public slots:
void Exit();
void LoadDirectories();

View File

@@ -290,7 +290,7 @@ void CollectionFilterWidget::SetCollectionModel(CollectionModel *model) {
s.beginGroup(settings_group_);
model_->SetGroupBy(CollectionModel::Grouping(
CollectionModel::GroupBy(s.value(group_by(1), int(CollectionModel::GroupBy_AlbumArtist)).toInt()),
CollectionModel::GroupBy(s.value(group_by(2), int(CollectionModel::GroupBy_Album)).toInt()),
CollectionModel::GroupBy(s.value(group_by(2), int(CollectionModel::GroupBy_AlbumDisc)).toInt()),
CollectionModel::GroupBy(s.value(group_by(3), int(CollectionModel::GroupBy_None)).toInt())));
}

View File

@@ -107,7 +107,7 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q
root_->lazy_loaded = true;
group_by_[0] = GroupBy_AlbumArtist;
group_by_[1] = GroupBy_Album;
group_by_[1] = GroupBy_AlbumDisc;
group_by_[2] = GroupBy_None;
cover_loader_options_.desired_height_ = kPrettyCoverSize;
@@ -119,12 +119,14 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q
}
QIcon nocover = IconLoader::Load("cdcase");
no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
//no_cover_icon_ = QPixmap(":/pictures/noalbumart.png").scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
if (!nocover.isNull()) {
no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
if (sIconCache == nullptr) {
if (app_ && !sIconCache) {
sIconCache = new QNetworkDiskCache(this);
sIconCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + kPixmapDiskCacheDir);
connect(app_, SIGNAL(ClearPixmapDiskCache()), SLOT(ClearDiskCache()));
}
connect(backend_, SIGNAL(SongsDiscovered(SongList)), SLOT(SongsDiscovered(SongList)));
@@ -139,8 +141,6 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q
backend_->UpdateTotalArtistCountAsync();
backend_->UpdateTotalAlbumCountAsync();
connect(app_, SIGNAL(ClearPixmapDiskCache()), SLOT(ClearDiskCache()));
ReloadSettings();
}
@@ -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;
@@ -189,7 +189,9 @@ void CollectionModel::ReloadSettings() {
QPixmapCache::setCacheLimit(MaximumCacheSize(&s, CollectionSettingsPage::kSettingsCacheSize, CollectionSettingsPage::kSettingsCacheSizeUnit, CollectionSettingsPage::kSettingsCacheSizeDefault) / 1024);
sIconCache->setMaximumCacheSize(MaximumCacheSize(&s, CollectionSettingsPage::kSettingsDiskCacheSize, CollectionSettingsPage::kSettingsDiskCacheSizeUnit, CollectionSettingsPage::kSettingsDiskCacheSizeDefault));
if (sIconCache) {
sIconCache->setMaximumCacheSize(MaximumCacheSize(&s, CollectionSettingsPage::kSettingsDiskCacheSize, CollectionSettingsPage::kSettingsDiskCacheSizeUnit, CollectionSettingsPage::kSettingsDiskCacheSizeDefault));
}
s.endGroup();
@@ -199,7 +201,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 +340,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 +356,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 +410,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.
@@ -510,7 +512,7 @@ void CollectionModel::SongsDeleted(const SongList &songs) {
// Remove from pixmap cache
const QString cache_key = AlbumIconPixmapCacheKey(ItemToIndex(node));
QPixmapCache::remove(cache_key);
if (use_disk_cache_) sIconCache->remove(QUrl(cache_key));
if (use_disk_cache_ && sIconCache) sIconCache->remove(QUrl(cache_key));
if (pending_cache_keys_.contains(cache_key)) {
pending_cache_keys_.remove(cache_key);
}
@@ -585,7 +587,7 @@ QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) {
}
// Try to load it from the disk cache
if (use_disk_cache_) {
if (use_disk_cache_ && sIconCache) {
std::unique_ptr<QIODevice> cache(sIconCache->data(QUrl(cache_key)));
if (cache) {
QImage cached_image;
@@ -637,7 +639,7 @@ void CollectionModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderR
}
// If we have a valid cover not already in the disk cache
if (use_disk_cache_) {
if (use_disk_cache_ && sIconCache) {
std::unique_ptr<QIODevice> cached_img(sIconCache->data(QUrl(cache_key)));
if (!cached_img && !result.image_scaled.isNull()) {
QNetworkCacheMetaData item_metadata;
@@ -658,7 +660,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 +672,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 +684,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 +824,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 +848,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 +916,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 +984,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 +1077,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 +1092,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 +1215,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 +1230,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 +1361,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 +1377,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 +1469,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;
@@ -1550,6 +1558,7 @@ int CollectionModel::MaximumCacheSize(QSettings *s, const char *size_id, const c
} while (unit > 0);
return size;
}
void CollectionModel::GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const {
@@ -1597,7 +1606,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 +1641,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 +1653,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 +1667,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);
@@ -1680,7 +1689,7 @@ void CollectionModel::TotalAlbumCountUpdatedSlot(int count) {
}
void CollectionModel::ClearDiskCache() {
sIconCache->clear();
if (sIconCache) sIconCache->clear();
}
QDataStream &operator<<(QDataStream &s, const CollectionModel::Grouping &g) {

View File

@@ -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;

View File

@@ -45,7 +45,11 @@ CollectionQuery::CollectionQuery(const QueryOptions &options)
// 3) Remove colons which don't correspond to column names.
// Split on whitespace
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList tokens(options.filter().split(QRegExp("\\s+"), Qt::SkipEmptyParts));
#else
QStringList tokens(options.filter().split(QRegExp("\\s+"), QString::SkipEmptyParts));
#endif
QString query;
for (QString token : tokens) {
token.remove('(');

View File

@@ -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_([=]() {

View File

@@ -53,6 +53,7 @@ class DeviceManager;
class CoverProviders;
class AlbumCoverLoader;
class CurrentAlbumCoverLoader;
class CoverProviders;
class LyricsProviders;
class AudioScrobbler;
class InternetServices;

View File

@@ -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"
@@ -224,10 +226,12 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
dialog->SetDestinationModel(app->collection()->model()->directory_model());
return dialog;
}),
#ifdef HAVE_GSTREAMER
transcode_dialog_([=]() {
TranscodeDialog *dialog = new TranscodeDialog(this);
return dialog;
}),
#endif
add_stream_dialog_([=]() {
AddStreamDialog *add_stream_dialog = new AddStreamDialog;
connect(add_stream_dialog, SIGNAL(accepted()), this, SLOT(AddStreamAccepted()));
@@ -262,15 +266,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 +423,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 +543,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 +596,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 +764,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 +808,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 +826,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 +851,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 +868,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 +879,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 +913,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 +926,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 +955,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 +968,6 @@ void MainWindow::ReloadAllSettings() {
void MainWindow::RefreshStyleSheet() {
QString contents(styleSheet());
setStyleSheet("");
setStyleSheet(contents);
}
@@ -973,10 +980,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 +1164,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 +1173,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 +1908,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 +1935,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 +2075,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 +2673,8 @@ void MainWindow::GetCoverAutomatically() {
!song_.effective_album().isEmpty();
if (search) {
album_cover_choice_controller_->SearchCoverAutomatically(song_);
emit SearchCoverInProgress();
album_cover_choice_controller_->SearchCoverAutomatically(song_);
}
}

View File

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

View File

@@ -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>

View File

@@ -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_);

View File

@@ -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;

View File

@@ -93,7 +93,7 @@ bool QtSystemTrayIcon::eventFilter(QObject *object, QEvent *event) {
if (event->type() == QEvent::Wheel) {
QWheelEvent *e = static_cast<QWheelEvent*>(event);
if (e->modifiers() == Qt::ShiftModifier) {
if (e->delta() > 0) {
if (e->angleDelta().y() > 0) {
emit SeekForward();
}
else {
@@ -101,7 +101,7 @@ bool QtSystemTrayIcon::eventFilter(QObject *object, QEvent *event) {
}
}
else if (e->modifiers() == Qt::ControlModifier) {
if (e->delta() < 0) {
if (e->angleDelta().y() < 0) {
emit NextTrack();
}
else {
@@ -114,7 +114,7 @@ bool QtSystemTrayIcon::eventFilter(QObject *object, QEvent *event) {
bool prev_next_track = s.value("scrolltrayicon").toBool();
s.endGroup();
if (prev_next_track) {
if (e->delta() < 0) {
if (e->angleDelta().y() < 0) {
emit NextTrack();
}
else {
@@ -122,7 +122,7 @@ bool QtSystemTrayIcon::eventFilter(QObject *object, QEvent *event) {
}
}
else {
emit ChangeVolume(e->delta());
emit ChangeVolume(e->angleDelta().y());
}
}
return true;

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -64,6 +64,9 @@
#include <QMessageBox>
#include <QNetworkInterface>
#include <QtDebug>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#endif
#include <stdio.h>
@@ -367,6 +370,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;
@@ -536,7 +543,11 @@ bool IsMouseEventInWidget(const QMouseEvent *e, const QWidget *widget) {
quint16 PickUnusedPort() {
forever {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
const quint64 port = QRandomGenerator::global()->bounded(49152, 65535);
#else
const quint16 port = 49152 + qrand() % 16384;
#endif
QTcpServer server;
if (server.listen(QHostAddress::Any, port)) {
@@ -817,8 +828,12 @@ QString CryptographicRandomString(const int len) {
QString GetRandomString(const int len, const QString &UseCharacters) {
QString randstr;
for(int i=0 ; i < len ; ++i) {
int index = qrand() % UseCharacters.length();
for(int i = 0 ; i < len ; ++i) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
const int index = QRandomGenerator::global()->bounded(0, UseCharacters.length());
#else
const int index = qrand() % UseCharacters.length();
#endif
QChar nextchar = UseCharacters.at(index);
randstr.append(nextchar);
}
@@ -926,6 +941,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 +1007,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)

View File

@@ -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 {

View File

@@ -108,14 +108,14 @@ void Windows7ThumbBar::HandleWinEvent(MSG *msg) {
// Create the taskbar list
hr = CoCreateInstance(CLSID_ITaskbarList, nullptr, CLSCTX_ALL, IID_ITaskbarList3, (void**)&taskbar_list_);
if (hr != S_OK) {
qLog(Warning) << "Error creating the ITaskbarList3 interface" << hex << DWORD (hr);
qLog(Warning) << "Error creating the ITaskbarList3 interface" << Qt::hex << DWORD (hr);
return;
}
ITaskbarList3 *taskbar_list = reinterpret_cast<ITaskbarList3*>(taskbar_list_);
hr = taskbar_list->HrInit();
if (hr != S_OK) {
qLog(Warning) << "Error initialising taskbar list" << hex << DWORD (hr);
qLog(Warning) << "Error initialising taskbar list" << Qt::hex << DWORD (hr);
taskbar_list->Release();
taskbar_list_ = nullptr;
return;
@@ -134,7 +134,7 @@ void Windows7ThumbBar::HandleWinEvent(MSG *msg) {
qLog(Debug) << "Adding buttons";
hr = taskbar_list->ThumbBarAddButtons((HWND)widget_->winId(), actions_.count(), buttons);
if (hr != S_OK)
qLog(Debug) << "Failed to add buttons" << hex << DWORD (hr);
qLog(Debug) << "Failed to add buttons" << Qt::hex << DWORD (hr);
for (int i = 0; i < actions_.count(); i++) {
if (buttons[i].hIcon)
DestroyIcon (buttons[i].hIcon);

View File

@@ -280,7 +280,7 @@ void AlbumCoverChoiceController::ShowCover(const Song &song, const QPixmap &pixm
label->setPixmap(pixmap);
// Add (WxHpx) to the title before possibly resizing
title_text += " (" + QString::number(label->pixmap()->width()) + "x" + QString::number(label->pixmap()->height()) + "px)";
title_text += " (" + QString::number(pixmap.width()) + "x" + QString::number(pixmap.height()) + "px)";
// If the cover is larger than the screen, resize the window 85% seems to be enough to account for title bar and taskbar etc.
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
@@ -295,19 +295,23 @@ void AlbumCoverChoiceController::ShowCover(const Song &song, const QPixmap &pixm
// Resize differently if monitor is in portrait mode
if (desktop_width < desktop_height) {
const int new_width = (double)desktop_width * 0.95;
if (new_width < label->pixmap()->width()) {
label->setPixmap(label->pixmap()->scaledToWidth(new_width, Qt::SmoothTransformation));
if (new_width < pixmap.width()) {
label->setPixmap(pixmap.scaledToWidth(new_width, Qt::SmoothTransformation));
}
}
else {
const int new_height = (double)desktop_height * 0.85;
if (new_height < label->pixmap()->height()) {
label->setPixmap(label->pixmap()->scaledToHeight(new_height, Qt::SmoothTransformation));
if (new_height < pixmap.height()) {
label->setPixmap(pixmap.scaledToHeight(new_height, Qt::SmoothTransformation));
}
}
dialog->setWindowTitle(title_text);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
dialog->setFixedSize(label->pixmap(Qt::ReturnByValue).size());
#else
dialog->setFixedSize(label->pixmap()->size());
#endif
dialog->show();
}
@@ -387,7 +391,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));

View File

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

View File

@@ -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;

View File

@@ -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
@@ -27,9 +28,12 @@
#include <QCoreApplication>
#include <QTimer>
#include <QList>
#include <QMap>
#include <QMultiMap>
#include <QString>
#include <QUrl>
#include <QImage>
#include <QImageReader>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
@@ -60,6 +64,11 @@ AlbumCoverFetcherSearch::AlbumCoverFetcherSearch(
}
AlbumCoverFetcherSearch::~AlbumCoverFetcherSearch() {
pending_requests_.clear();
Cancel();
}
void AlbumCoverFetcherSearch::TerminateSearch() {
for (quint64 id : pending_requests_.keys()) {
@@ -72,17 +81,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 +121,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 +150,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 +228,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 +243,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_.insert(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 +310,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 +334,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;
}

View File

@@ -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
@@ -27,6 +28,7 @@
#include <QObject>
#include <QPair>
#include <QMap>
#include <QMultiMap>
#include <QString>
#include <QUrl>
#include <QImage>
@@ -48,6 +50,7 @@ class AlbumCoverFetcherSearch : public QObject {
public:
explicit AlbumCoverFetcherSearch(const CoverSearchRequest &request, QNetworkAccessManager *network, QObject *parent);
~AlbumCoverFetcherSearch();
void Start(CoverProviders *cover_providers);
@@ -64,18 +67,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;
@@ -96,7 +103,7 @@ class AlbumCoverFetcherSearch : public QObject {
// QMap is sorted by key (score). Values are (result, image)
typedef QPair<CoverSearchResult, QImage> CandidateImage;
QMap<float, CandidateImage> candidate_images_;
QMultiMap<float, CandidateImage> candidate_images_;
QNetworkAccessManager *network_;

View File

@@ -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()) {

View File

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

View File

@@ -250,6 +250,9 @@ void AlbumCoverManager::closeEvent(QCloseEvent *e) {
// Cancel any outstanding requests
CancelRequests();
ui_->artists->clear();
ui_->albums->clear();
}
void AlbumCoverManager::LoadGeometry() {

View File

@@ -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) {}

View File

@@ -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_;

View File

@@ -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();
}

View File

@@ -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_;

View File

@@ -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;

View File

@@ -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_;
};

View File

@@ -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 &params_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) {

View File

@@ -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 &params_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_;
};

View File

@@ -0,0 +1,64 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#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;
}

View 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

View File

@@ -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;
}

View File

@@ -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_;
};

View File

@@ -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;

View File

@@ -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_;
};

View 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;
}

View 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

View File

@@ -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;

View File

@@ -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_;
};

View 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 &param : 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 &param : 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 &param : 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;
}

View 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

View File

@@ -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;

View File

@@ -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_;
};

View File

@@ -20,6 +20,8 @@
#include "config.h"
#include <memory>
#include <QThread>
#include <QFile>
#include <QList>
@@ -28,6 +30,7 @@
#include "core/application.h"
#include "core/utilities.h"
#include "core/song.h"
#include "afcdevice.h"
#include "afcfile.h"
#include "afctransfer.h"
@@ -35,13 +38,23 @@
#include "gpodloader.h"
#include "imobiledeviceconnection.h"
AfcDevice::AfcDevice(const QUrl &url, DeviceLister* lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time)
: GPodDevice(url, lister, unique_id, manager, app, database_id, first_time), transfer_(nullptr)
{
}
AfcDevice::AfcDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, const int database_id, const bool first_time) : GPodDevice(url, lister, unique_id, manager, app, database_id, first_time), transfer_(nullptr) {}
AfcDevice::~AfcDevice() {
Utilities::RemoveRecursive(local_path_);
if (loader_) {
loader_->deleteLater();
loader_ = nullptr;
}
if (loader_thread_) {
loader_thread_->exit();
loader_thread_->deleteLater();
loader_thread_ = nullptr;
}
}
bool AfcDevice::Init() {
@@ -51,11 +64,16 @@ bool AfcDevice::Init() {
InitBackendDirectory(local_path_, first_time_, false);
model_->Init();
if (!loader_thread_) loader_thread_ = new QThread();
if (url_.isEmpty() || url_.path().isEmpty()) return false;
transfer_ = new AfcTransfer(url_.host(), local_path_, app_->task_manager(), shared_from_this());
transfer_->moveToThread(loader_thread_);
connect(transfer_, SIGNAL(TaskStarted(int)), SIGNAL(TaskStarted(int)));
connect(transfer_, SIGNAL(CopyFinished(bool)), SLOT(CopyFinished(bool)));
connect(loader_thread_, SIGNAL(started()), transfer_, SLOT(CopyFromDevice()));
loader_thread_->start();
@@ -63,7 +81,7 @@ bool AfcDevice::Init() {
}
void AfcDevice::CopyFinished(bool success) {
void AfcDevice::CopyFinished(const bool success) {
transfer_->deleteLater();
transfer_ = nullptr;
@@ -76,12 +94,12 @@ void AfcDevice::CopyFinished(bool success) {
// Now load the songs from the local database
loader_ = new GPodLoader(local_path_, app_->task_manager(), backend_, shared_from_this());
loader_->set_music_path_prefix("afc://" + url_.host());
//loader_->set_song_type(Song::Type_Stream);
loader_->set_song_type(Song::FileType_Stream);
loader_->moveToThread(loader_thread_);
connect(loader_, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
connect(loader_, SIGNAL(Error(QString)), SLOT(LoaderError(QString)));
connect(loader_, SIGNAL(TaskStarted(int)), SIGNAL(TaskStarted(int)));
connect(loader_, SIGNAL(LoadFinished(Itdb_iTunesDB*)), SLOT(LoadFinished(Itdb_iTunesDB*)));
connect(loader_, SIGNAL(LoadFinished(Itdb_iTunesDB*, bool)), SLOT(LoadFinished(Itdb_iTunesDB*, bool)));
QMetaObject::invokeMethod(loader_, "LoadDatabase");
}

View File

@@ -23,6 +23,8 @@
#include "config.h"
#include <memory>
#include <gpod/itdb.h>
#include <QObject>
@@ -41,7 +43,7 @@ class AfcDevice : public GPodDevice {
Q_OBJECT
public:
Q_INVOKABLE AfcDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time);
Q_INVOKABLE AfcDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, const int database_id, const bool first_time);
~AfcDevice();
bool Init();
@@ -50,20 +52,20 @@ public:
bool StartCopy(QList<Song::FileType> *supported_types);
bool CopyToStorage(const CopyJob &job);
void FinishCopy(bool success);
void FinishCopy(const bool success);
bool DeleteFromStorage(const DeleteJob &job);
protected:
protected:
void FinaliseDatabase();
private slots:
private slots:
void CopyFinished(bool success);
private:
private:
void RemoveRecursive(const QString &path);
private:
private:
AfcTransfer *transfer_;
std::shared_ptr<iMobileDeviceConnection> connection_;

View File

@@ -45,9 +45,6 @@ AfcTransfer::AfcTransfer(const QString &uuid, const QString &local_destination,
}
AfcTransfer::~AfcTransfer() {
}
void AfcTransfer::CopyFromDevice() {
int task_id = 0;
@@ -57,6 +54,7 @@ void AfcTransfer::CopyFromDevice() {
}
// Connect to the device
iMobileDeviceConnection c(uuid_);
// Copy directories. If one fails we stop.

View File

@@ -40,7 +40,6 @@ class AfcTransfer : public QObject {
public:
explicit AfcTransfer(const QString &uuid, const QString &local_destination, TaskManager *task_manager, std::shared_ptr<ConnectedDevice> device);
~AfcTransfer();
bool CopyToDevice(iMobileDeviceConnection *connection);

View File

@@ -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;
}

View File

@@ -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,
@@ -76,7 +78,7 @@ ConnectedDevice::~ConnectedDevice() {
backend_->deleteLater();
}
void ConnectedDevice::InitBackendDirectory(const QString &mount_point, bool first_time, bool rewrite_path) {
void ConnectedDevice::InitBackendDirectory(const QString &mount_point, const bool first_time, const bool rewrite_path) {
if (first_time || backend_->GetAllDirectories().isEmpty()) {
backend_->AddDirectory(mount_point);

View File

@@ -79,7 +79,7 @@ class ConnectedDevice : public QObject, public virtual MusicStorage, public std:
void CloseFinished(const QString& id);
protected:
void InitBackendDirectory(const QString &mount_point, bool first_time, bool rewrite_path = true);
void InitBackendDirectory(const QString &mount_point, const bool first_time, const bool rewrite_path = true);
protected:
Application *app_;

View File

@@ -353,9 +353,9 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
if (info->database_id_ == -1 && !info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) {
if (info->BestBackend()->lister_->AskForScan(info->BestBackend()->unique_id_)) {
std::unique_ptr<QMessageBox> dialog(new QMessageBox(QMessageBox::Information, tr("Connect device"), tr("This is the first time you have connected this device. Strawberry will now scan the device to find music files - this may take some time."), QMessageBox::Cancel));
QPushButton *connect = dialog->addButton(tr("Connect device"), QMessageBox::AcceptRole);
QPushButton *pushbutton = dialog->addButton(tr("Connect device"), QMessageBox::AcceptRole);
dialog->exec();
if (dialog->clickedButton() != connect) return QVariant();
if (dialog->clickedButton() != pushbutton) return QVariant();
}
}
const_cast<DeviceManager*>(this)->Connect(info);

View File

@@ -283,6 +283,10 @@ void GioLister::VolumeAdded(GVolume *volume) {
DeviceInfo info;
info.ReadVolumeInfo(volume);
if (info.volume_root_uri.startsWith("afc://") || info.volume_root_uri.startsWith("gphoto2://")) {
// Handled by iLister.
return;
}
#ifdef HAVE_AUDIOCD
if (info.volume_root_uri.startsWith("cdda"))
// Audio CD devices are already handled by CDDA lister
@@ -322,6 +326,10 @@ void GioLister::MountAdded(GMount *mount) {
DeviceInfo info;
info.ReadVolumeInfo(g_mount_get_volume(mount));
if (info.volume_root_uri.startsWith("afc://") || info.volume_root_uri.startsWith("gphoto2://")) {
// Handled by iLister.
return;
}
#ifdef HAVE_AUDIOCD
if (info.volume_root_uri.startsWith("cdda"))
// Audio CD devices are already handled by CDDA lister
@@ -566,7 +574,7 @@ void GioLister::UpdateDeviceFreeSpace(const QString &id) {
bool GioLister::DeviceNeedsMount(const QString &id) {
QMutexLocker l(&mutex_);
return devices_.contains(id) && !devices_[id].mount_ptr && !devices_[id].volume_root_uri.startsWith("mtp://");
return devices_.contains(id) && !devices_[id].mount_ptr && !devices_[id].volume_root_uri.startsWith("mtp://") && !devices_[id].volume_root_uri.startsWith("gphoto2://");
}

View File

@@ -46,7 +46,7 @@
class DeviceLister;
class DeviceManager;
GPodDevice::GPodDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time)
GPodDevice::GPodDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, const int database_id, const bool first_time)
: ConnectedDevice(url, lister, unique_id, manager, app, database_id, first_time),
loader_(nullptr),
loader_thread_(nullptr),
@@ -72,11 +72,15 @@ bool GPodDevice::Init() {
}
GPodDevice::~GPodDevice() {
if (loader_) {
loader_thread_->exit();
loader_->deleteLater();
loader_thread_->deleteLater();
loader_ = nullptr;
loader_thread_ = nullptr;
}
}
void GPodDevice::ConnectAsync() {

View File

@@ -47,13 +47,7 @@ class GPodDevice : public ConnectedDevice, public virtual MusicStorage {
Q_OBJECT
public:
Q_INVOKABLE GPodDevice(
const QUrl &url, DeviceLister *lister,
const QString &unique_id,
DeviceManager *manager,
Application *app,
int database_id,
bool first_time);
Q_INVOKABLE GPodDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, const int database_id, const bool first_time);
~GPodDevice();
bool Init();

View File

@@ -52,7 +52,7 @@ class GPodLoader : public QObject {
void LoadDatabase();
signals:
void Error(const QString &message);
void Error(QString message);
void TaskStarted(int task_id);
void LoadFinished(Itdb_iTunesDB *db, bool success);

View File

@@ -36,15 +36,17 @@ iLister::iLister() {}
iLister::~iLister() {}
bool iLister::Init() {
idevice_event_subscribe(&EventCallback, reinterpret_cast<void*>(this));
return true;
}
void iLister::EventCallback(const idevice_event_t *event, void *context) {
iLister *me = reinterpret_cast<iLister*>(context);
const char *uuid = event->udid;
QString uuid = QString::fromUtf8(event->udid);
switch (event->event) {
case IDEVICE_DEVICE_ADD:
@@ -60,7 +62,7 @@ void iLister::EventCallback(const idevice_event_t *event, void *context) {
}
void iLister::DeviceAddedCallback(const char *uuid) {
void iLister::DeviceAddedCallback(const QString uuid) {
DeviceInfo info = ReadDeviceInfo(uuid);
if (!info.valid) return;
@@ -82,9 +84,10 @@ void iLister::DeviceAddedCallback(const char *uuid) {
}
void iLister::DeviceRemovedCallback(const char *uuid) {
void iLister::DeviceRemovedCallback(const QString uuid) {
QString id = UniqueId(uuid);
{
QMutexLocker l(&mutex_);
if (!devices_.contains(id))
@@ -97,8 +100,8 @@ void iLister::DeviceRemovedCallback(const char *uuid) {
}
QString iLister::UniqueId(const char *uuid) {
return "ithing/" + QString::fromUtf8(uuid);
QString iLister::UniqueId(const QString uuid) {
return "ithing/" + uuid;
}
QStringList iLister::DeviceUniqueIDs() {
@@ -191,12 +194,13 @@ QList<QUrl> iLister::MakeDeviceUrls(const QString &id) {
}
iLister::DeviceInfo iLister::ReadDeviceInfo(const char *uuid) {
iLister::DeviceInfo iLister::ReadDeviceInfo(const QString uuid) {
DeviceInfo ret;
iMobileDeviceConnection conn(uuid);
if (!conn.is_valid()) return ret;
ret.valid = conn.is_valid();
ret.uuid = uuid;
ret.product_type = conn.GetProperty("ProductType").toString();

View File

@@ -37,6 +37,7 @@
class iLister : public DeviceLister {
Q_OBJECT
public:
explicit iLister();
~iLister();
@@ -58,7 +59,7 @@ class iLister : public DeviceLister {
private:
struct DeviceInfo {
DeviceInfo() : valid(false), free_bytes(0), total_bytes(0) {}
DeviceInfo() : valid(false), free_bytes(0), total_bytes(0), password_protected(false) {}
bool valid;
@@ -83,16 +84,16 @@ class iLister : public DeviceLister {
static void EventCallback(const idevice_event_t *event, void *context);
void DeviceAddedCallback(const char *uuid);
void DeviceRemovedCallback(const char *uuid);
void DeviceAddedCallback(const QString uuid);
void DeviceRemovedCallback(const QString uuid);
DeviceInfo ReadDeviceInfo(const char *uuid);
static QString UniqueId(const char *uuid);
DeviceInfo ReadDeviceInfo(const QString uuid);
static QString UniqueId(const QString uuid);
template <typename T>
T LockAndGetDeviceInfo(const QString &id, T DeviceInfo::*field);
private:
private:
QMutex mutex_;
QMap<QString, DeviceInfo> devices_;
};

View File

@@ -22,6 +22,10 @@
#include <plist/plist.h>
#include <libimobiledevice/afc.h>
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
#include <QCoreApplication>
#include <QDir>
#include <QByteArray>
@@ -29,15 +33,18 @@
#include <QStringList>
#include <QUrl>
#include <QtDebug>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#endif
#include "core/logging.h"
#include "imobiledeviceconnection.h"
iMobileDeviceConnection::iMobileDeviceConnection(const QString &uuid) : device_(nullptr), afc_(nullptr) {
iMobileDeviceConnection::iMobileDeviceConnection(const QString uuid) : device_(nullptr), afc_(nullptr) {
idevice_error_t err = idevice_new(&device_, uuid.toUtf8().constData());
if (err != IDEVICE_E_SUCCESS) {
qLog(Warning) << "idevice error:" << err;
qLog(Warning) << "idevice_new error:" << err;
return;
}
@@ -47,27 +54,25 @@ iMobileDeviceConnection::iMobileDeviceConnection(const QString &uuid) : device_(
lockdownd_client_t lockdown;
lockdownd_error_t lockdown_err = lockdownd_client_new_with_handshake(device_, &lockdown, label);
if (lockdown_err != LOCKDOWN_E_SUCCESS) {
qLog(Warning) << "lockdown error:" << lockdown_err;
qLog(Warning) << "lockdownd_client_new_with_handshake error:" << lockdown_err;
return;
}
lockdownd_service_descriptor_t lockdown_service_desc;
lockdown_err = lockdownd_start_service(lockdown, "com.apple.afc", &lockdown_service_desc);
if (lockdown_err != LOCKDOWN_E_SUCCESS) {
qLog(Warning) << "lockdown error:" << lockdown_err;
qLog(Warning) << "lockdownd_start_service error:" << lockdown_err;
lockdownd_client_free(lockdown);
return;
}
afc_error_t afc_err = afc_client_new(device_, lockdown_service_desc, &afc_);
if (afc_err != AFC_E_SUCCESS) {
qLog(Warning) << "afc error:" << afc_err;
lockdownd_service_descriptor_free(lockdown_service_desc);
qLog(Warning) << "afc_client_new error:" << afc_err;
lockdownd_client_free(lockdown);
return;
}
lockdownd_service_descriptor_free(lockdown_service_desc);
lockdownd_client_free(lockdown);
}
@@ -187,6 +192,7 @@ QString iMobileDeviceConnection::GetFileInfo(const QString &path, const QString
char **infolist = nullptr;
afc_error_t err = afc_get_file_info(afc_, path.toUtf8().constData(), &infolist);
if (err != AFC_E_SUCCESS || !infolist) {
qLog(Debug) << "afc_get_file_info error:" << path << err;
return ret;
}
@@ -232,7 +238,12 @@ QString iMobileDeviceConnection::GetUnusedFilename(Itdb_iTunesDB *itdb, const So
}
// Pick one at random
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
const int dir_num = QRandomGenerator::global()->bounded(total_musicdirs);
#else
const int dir_num = qrand() % total_musicdirs;
#endif
QString dir = QString::asprintf("/iTunes_Control/Music/F%02d", dir_num);
if (!Exists(dir)) {

View File

@@ -38,7 +38,7 @@
class iMobileDeviceConnection {
public:
explicit iMobileDeviceConnection(const QString &uuid);
explicit iMobileDeviceConnection(const QString uuid);
~iMobileDeviceConnection();
afc_client_t afc() { return afc_; }

View File

@@ -51,7 +51,7 @@ class MtpDevice : public ConnectedDevice {
Q_INVOKABLE MtpDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time);
~MtpDevice();
static QStringList url_schemes() { return QStringList() << "mtp" << "gphoto2"; }
static QStringList url_schemes() { return QStringList() << "mtp"; }
bool Init();
void ConnectAsync();

View File

@@ -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,24 @@ 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();
setFixedHeight(height() + 40);
}
QString About::MainHtml() const {
QString ret;
@@ -116,12 +124,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 +149,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 +165,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 +183,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>";

View File

@@ -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;

View File

@@ -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>

View File

@@ -42,6 +42,9 @@
Console::Console(Application *app, QWidget *parent) : QDialog(parent), app_(app) {
ui_.setupUi(this);
setWindowFlags(windowFlags()|Qt::WindowMaximizeButtonHint);
connect(ui_.run, SIGNAL(clicked()), SLOT(RunQuery()));
QFont font("Monospace");

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>545</width>
<height>347</height>
<width>600</width>
<height>360</height>
</rect>
</property>
<property name="windowTitle">

View File

@@ -419,6 +419,7 @@ void GstEngine::ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QS
// Schedule this to run in the GUI thread. The buffer gets added to the queue and unreffed by UpdateScope.
if (!QMetaObject::invokeMethod(this, "AddBufferToScope", Q_ARG(GstBuffer*, buffer), Q_ARG(int, pipeline_id), Q_ARG(QString, format))) {
qLog(Warning) << "Failed to invoke AddBufferToScope on GstEngine";
gst_buffer_unref(buffer);
}
}
@@ -802,6 +803,7 @@ void GstEngine::UpdateScope(const int chunk_length) {
// In case a buffer doesn't arrive in time
if (scope_chunk_ >= scope_chunks_) {
scope_chunk_ = 0;
gst_buffer_unmap(latest_buffer_, &map);
return;
}

View File

@@ -799,7 +799,7 @@ void GstEnginePipeline::ErrorMessageReceived(GstMessage *msg) {
int domain = error->domain;
int code = error->code;
g_error_free(error);
free(debugs);
g_free(debugs);
if (state() == GST_STATE_PLAYING && pipeline_is_initialised_ && next_uri_set_ && (domain == (int)GST_RESOURCE_ERROR || domain == (int)GST_STREAM_ERROR)) {
// A track is still playing and the next uri is not playable. We ignore the error here so it can play until the end.

View File

@@ -122,7 +122,7 @@ bool GlobalShortcut::unsetShortcut() {
}
qt_key_ = Qt::Key(0);
qt_mods_ = Qt::KeyboardModifiers(0);
qt_mods_ = Qt::KeyboardModifiers();
native_key_ = 0;
native_mods_ = 0;

View File

@@ -48,7 +48,7 @@ InternetSearchModel::InternetSearchModel(InternetService *service, QObject *pare
{
group_by_[0] = CollectionModel::GroupBy_AlbumArtist;
group_by_[1] = CollectionModel::GroupBy_Album;
group_by_[1] = CollectionModel::GroupBy_AlbumDisc;
group_by_[2] = CollectionModel::GroupBy_None;
no_cover_icon_ = album_icon_.pixmap(album_icon_.availableSizes().last()).scaled(CollectionModel::kPrettyCoverSize, CollectionModel::kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);

View File

@@ -241,7 +241,7 @@ void InternetSearchView::ReloadSettings() {
SetGroupBy(CollectionModel::Grouping(
CollectionModel::GroupBy(s.value("search_group_by1", int(CollectionModel::GroupBy_AlbumArtist)).toInt()),
CollectionModel::GroupBy(s.value("search_group_by2", int(CollectionModel::GroupBy_Album)).toInt()),
CollectionModel::GroupBy(s.value("search_group_by2", int(CollectionModel::GroupBy_AlbumDisc)).toInt()),
CollectionModel::GroupBy(s.value("search_group_by3", int(CollectionModel::GroupBy_None)).toInt())));
s.endGroup();
@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -48,10 +48,14 @@
#include <QTcpSocket>
#include <QSslSocket>
#include <QDateTime>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#endif
LocalRedirectServer::LocalRedirectServer(const bool https, QObject *parent)
LocalRedirectServer::LocalRedirectServer(QObject *parent)
: QTcpServer(parent),
https_(https),
https_(false),
port_(0),
socket_(nullptr)
{}
@@ -151,7 +155,12 @@ bool LocalRedirectServer::GenerateCertificate() {
return false;
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
quint64 serial = 9999999 + QRandomGenerator::global()->bounded(1000000);
#else
quint64 serial = (9999999 + qrand() % 1000000);
#endif
QByteArray q_serial;
q_serial.setNum(serial);
@@ -232,7 +241,7 @@ bool LocalRedirectServer::Listen() {
if (https_) {
if (!GenerateCertificate()) return false;
}
if (!listen(QHostAddress::LocalHost)) {
if (!listen(QHostAddress::LocalHost, port_)) {
error_ = errorString();
return false;
}

Some files were not shown because too many files have changed in this diff Show More