Compare commits
129 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
783cf7f1b0 | ||
|
|
92d6fc3fad | ||
|
|
436cdea4fd | ||
|
|
6b144b3b1f | ||
|
|
7820082eb8 | ||
|
|
e732f921a3 | ||
|
|
8b7c5d8585 | ||
|
|
9e959b189c | ||
|
|
5c7a4cdc22 | ||
|
|
756e619e07 | ||
|
|
eea6194b90 | ||
|
|
a09c1fa154 | ||
|
|
38c742328c | ||
|
|
1d5db1446d | ||
|
|
3f5f3d143f | ||
|
|
d297a7198a | ||
|
|
e5bd99dee4 | ||
|
|
2720e13e88 | ||
|
|
0e9c1789ff | ||
|
|
281cb10f84 | ||
|
|
69a92ffe72 | ||
|
|
6447f159e5 | ||
|
|
94430883ad | ||
|
|
046512eb3d | ||
|
|
4479d97e90 | ||
|
|
bf5fea8951 | ||
|
|
07282e3de6 | ||
|
|
c2f90a20df | ||
|
|
481d2d699e | ||
|
|
cf9a7e6ed3 | ||
|
|
c35235371a | ||
|
|
5d5723ad58 | ||
|
|
6c77294a86 | ||
|
|
823f65f1ca | ||
|
|
0c378c1642 | ||
|
|
bbb4162867 | ||
|
|
5dbdde3f2b | ||
|
|
2521954bd9 | ||
|
|
5f1002894e | ||
|
|
0489b312a3 | ||
|
|
732be5a34f | ||
|
|
1f45c78ebb | ||
|
|
8b86c79bf9 | ||
|
|
710ed81067 | ||
|
|
24ac0e7b9b | ||
|
|
15b2bfbb29 | ||
|
|
b7494eb381 | ||
|
|
27ac590250 | ||
|
|
972076edab | ||
|
|
bfa9a1eb8a | ||
|
|
b0966f14e6 | ||
|
|
37cf0c2fb6 | ||
|
|
4eb11c32b0 | ||
|
|
25457bc09a | ||
|
|
d5cfb5f733 | ||
|
|
79ba6e628e | ||
|
|
ef73add05a | ||
|
|
ec3d11fb27 | ||
|
|
3fbc7031b5 | ||
|
|
40beb5e428 | ||
|
|
0f608c8ef0 | ||
|
|
8509cb4743 | ||
|
|
f4429e8c4a | ||
|
|
e9e0829cdc | ||
|
|
93f0230423 | ||
|
|
f26a0df4a4 | ||
|
|
c7d4624282 | ||
|
|
b03eee2a22 | ||
|
|
7d4d72e706 | ||
|
|
e3c367984b | ||
|
|
0ebfa10d32 | ||
|
|
16d9a077f0 | ||
|
|
a9d8bbad42 | ||
|
|
d78bb94af3 | ||
|
|
b139c0a824 | ||
|
|
5b0b924d34 | ||
|
|
f75acf820c | ||
|
|
43a47f33ac | ||
|
|
fcea3a0877 | ||
|
|
a950ec3bd5 | ||
|
|
e35501ff0a | ||
|
|
4bfad9dad8 | ||
|
|
c5c7a07c12 | ||
|
|
7e22e0e552 | ||
|
|
84ec4bdc79 | ||
|
|
2bcad9b637 | ||
|
|
c8d5f03070 | ||
|
|
168e101a5a | ||
|
|
b4bc7333d9 | ||
|
|
e8b58c940e | ||
|
|
ec7202e3f6 | ||
|
|
9a740f7962 | ||
|
|
9210fdee0d | ||
|
|
d7661f0964 | ||
|
|
139e148912 | ||
|
|
1b8dedb4ed | ||
|
|
5d6b0fa329 | ||
|
|
f35bbd89c9 | ||
|
|
538a9e42f4 | ||
|
|
623147dea7 | ||
|
|
dfecd0cd12 | ||
|
|
fe3af3a676 | ||
|
|
25f60331ed | ||
|
|
d4860a3426 | ||
|
|
e7e77ed86b | ||
|
|
dc80459c59 | ||
|
|
2f2de59234 | ||
|
|
7bccc21878 | ||
|
|
40f9dafa44 | ||
|
|
355d436d29 | ||
|
|
079b684388 | ||
|
|
fd11f46d30 | ||
|
|
cb7099199a | ||
|
|
8566d91e89 | ||
|
|
f44ce49ea7 | ||
|
|
6ef69f6b32 | ||
|
|
f5983d5f10 | ||
|
|
54cce5e089 | ||
|
|
4e4e596a1e | ||
|
|
727a1f5ad1 | ||
|
|
85fa86625b | ||
|
|
2c91877f83 | ||
|
|
7d1fac44e9 | ||
|
|
2e34abfc0d | ||
|
|
81ba63e247 | ||
|
|
8b11a65522 | ||
|
|
7190ad1d15 | ||
|
|
1c9bae5df5 | ||
|
|
cc7fd73916 |
@@ -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: /.*/
|
||||
|
||||
1331
.github/workflows/ccpp.yml
vendored
1331
.github/workflows/ccpp.yml
vendored
File diff suppressed because it is too large
Load Diff
12
3rdparty/singleapplication/singleapplication.cpp
vendored
12
3rdparty/singleapplication/singleapplication.cpp
vendored
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
39
Changelog
39
Changelog
@@ -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:
|
||||
|
||||
10
README.md
10
README.md
@@ -17,7 +17,7 @@ Resources:
|
||||
* PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
||||
* Translations: https://translate.zanata.org/iteration/view/strawberry/master
|
||||
|
||||
The program is free software, released under GPL. If you like this program and can make use of it, consider sponsoring or donating to help funding the project.
|
||||
The program is free software, released under GPL. If you like this program and can make use of it, consider sponsoring or donating to help fund the project.
|
||||
To sponsor, visit [my GitHub sponsors profile](https://github.com/sponsors/jonaski).
|
||||
Funding developers through GitHub Sponsors is one more way to contribute to open source projects you appreciate, it helps developers get the resources they need, and recognize contributors working behind the scenes to make open source better for everyone.
|
||||
You can also make a one-time payment through [paypal.me/jonaskvinge](https://paypal.me/jonaskvinge)
|
||||
@@ -32,19 +32,19 @@ You can also make a one-time payment through [paypal.me/jonaskvinge](https://pay
|
||||
* Advanced audio output and device configuration for bit-perfect playback on Linux
|
||||
* Edit tags on music files
|
||||
* Fetch tags from MusicBrainz
|
||||
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/) and [Deezer](https://www.deezer.com/)
|
||||
* Song lyrics from [AudD](https://audd.io/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/)
|
||||
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
|
||||
* Song lyrics from [AudD](https://audd.io/), [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/)
|
||||
* Support for multiple backends
|
||||
* Audio analyzer
|
||||
* Audio equalizer
|
||||
* Transfer music to iPod, iPhone, MTP or mass-storage USB player
|
||||
* Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
||||
* Subsonic streaming support
|
||||
* Subsonic and Tidal streaming support
|
||||
|
||||
|
||||
It has so far been tested to work on Linux, OpenBSD and Windows.
|
||||
|
||||
**We currently do not provide releases for macOS because there aren't any macOS developers actively working on this project. It is still possible to compile by following the instructions in the Wiki**
|
||||
**We currently do not provide releases for macOS because there aren't any macOS developers actively working on this project. Development builds are available**
|
||||
|
||||
### :heavy_exclamation_mark: Requirements
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 0)
|
||||
set(STRAWBERRY_VERSION_MINOR 6)
|
||||
set(STRAWBERRY_VERSION_PATCH 10)
|
||||
set(STRAWBERRY_VERSION_PATCH 12)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
|
||||
4
debian/control
vendored
4
debian/control
vendored
@@ -55,8 +55,8 @@ Description: Audio player and music collection organizer
|
||||
- Advanced audio output and device configuration for bit-perfect playback on Linux
|
||||
- Edit tags on music files
|
||||
- Fetch tags from MusicBrainz
|
||||
- Album cover art from Lastfm, Musicbrainz, Discogs and Deezer
|
||||
- Song lyrics from AudD, lyrics.ovh and lololyrics.com
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
- Support for multiple backends
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
|
||||
39
debian/copyright
vendored
39
debian/copyright
vendored
@@ -24,6 +24,8 @@ Files: src/core/main.h
|
||||
src/version.h.in
|
||||
src/context/contextview.cpp
|
||||
src/context/contextview.h
|
||||
src/context/contextalbum.cpp
|
||||
src/context/contextalbum.h
|
||||
src/engine/enginetype.cpp
|
||||
src/engine/enginetype.h
|
||||
src/engine/alsadevicefinder.cpp
|
||||
@@ -38,16 +40,36 @@ Files: src/core/main.h
|
||||
src/internet/internetservice.h
|
||||
src/internet/internettabsview.cpp
|
||||
src/internet/internettabsview.h
|
||||
src/internet/internetsongsview.cpp
|
||||
src/internet/internetsongsview.h
|
||||
src/settings/backendsettingspage.cpp
|
||||
src/settings/backendsettingspage.h
|
||||
src/settings/coverssettingspage.cpp
|
||||
src/settings/coverssettingspage.h
|
||||
src/settings/lyricssettingspage.cpp
|
||||
src/settings/lyricssettingspage.h
|
||||
src/settings/scrobblersettingspage.cpp
|
||||
src/settings/scrobblersettingspage.h
|
||||
src/settings/subsonicsettingspage.cpp
|
||||
src/settings/subsonicsettingspage.h
|
||||
src/settings/tidalsettingspage.cpp
|
||||
src/settings/tidalsettingspage.h
|
||||
src/covermanager/jsoncoverprovider.cpp
|
||||
src/covermanager/jsoncoverprovider.h
|
||||
src/covermanager/lastfmcoverprovider.cpp
|
||||
src/covermanager/lastfmcoverprovider.h
|
||||
src/covermanager/musicbrainzcoverprovider.cpp
|
||||
src/covermanager/musicbrainzcoverprovider.h
|
||||
src/covermanager/deezercoverprovider.cpp
|
||||
src/covermanager/deezercoverprovider.h
|
||||
src/covermanager/tidalcoverprovider.cpp
|
||||
src/covermanager/tidalcoverprovider.h
|
||||
src/covermanager/qobuzcoverprovider.cpp
|
||||
src/covermanager/qobuzcoverprovider.h
|
||||
src/covermanager/spotifycoverprovider.cpp
|
||||
src/covermanager/spotifycoverprovider.h
|
||||
src/covermanager/musixmatchcoverprovider.cpp
|
||||
src/covermanager/musixmatchcoverprovider.h
|
||||
src/globalshortcuts/globalshortcutbackend-system.cpp
|
||||
src/globalshortcuts/globalshortcutbackend-system.h
|
||||
src/globalshortcuts/globalshortcut.cpp
|
||||
@@ -59,9 +81,10 @@ Files: src/core/main.h
|
||||
src/lyrics/*
|
||||
src/scrobbler/*
|
||||
src/subsonic/*
|
||||
src/tidal/*
|
||||
src/transcoder/transcoderoptionswavpack.cpp
|
||||
src/transcoder/transcoderoptionswavpack.h
|
||||
Copyright: 2012-2014, 2017-2019, Jonas Kvinge <jonas@jkvinge.net>
|
||||
Copyright: 2012-2014, 2017-2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/core/main.cpp
|
||||
@@ -158,8 +181,20 @@ Files: src/core/main.cpp
|
||||
src/transcoder/transcoder.h
|
||||
src/musicbrainz/musicbrainzclient.cpp
|
||||
src/musicbrainz/musicbrainzclient.h
|
||||
src/covermanager/albumcoverloader.cpp
|
||||
src/covermanager/albumcoverloader.h
|
||||
src/covermanager/currentalbumcoverloader.cpp
|
||||
src/covermanager/currentalbumcoverloader.h
|
||||
src/covermanager/albumcoverchoicecontroller.cpp
|
||||
src/covermanager/albumcoverchoicecontroller.h
|
||||
src/covermanager/albumcoverfetchersearch.cpp
|
||||
src/covermanager/albumcoverfetchersearch.h
|
||||
src/covermanager/coverproviders.cpp
|
||||
src/covermanager/coverproviders.h
|
||||
src/covermanager/coverprovider.cpp
|
||||
src/covermanager/coverprovider.h
|
||||
Copyright: 2010, 2012-2014 David Sansome <me@davidsansome.com>
|
||||
2012-2014, 2017-2019 Jonas Kvinge <jonas@jkvinge.net>
|
||||
2012-2014, 2017-2020 Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/engine/enginebase.cpp
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
<li>Advanced audio output and device configuration for bit-perfect playback on Linux</li>
|
||||
<li>Edit tags on music files</li>
|
||||
<li>Fetch tags from MusicBrainz</li>
|
||||
<li>Album cover art from Last.fm, Musicbrainz and Discogs</li>
|
||||
<li>Song lyrics from AudD, lyrics.ovh and lololyrics.com</li>
|
||||
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
||||
<li>Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com</li>
|
||||
<li>Support for multiple backends</li>
|
||||
<li>Audio analyzer and equalizer</li>
|
||||
<li>Transfer music to iPod, iPhone, MTP or mass-storage USB player</li>
|
||||
|
||||
4
dist/unix/strawberry.1
vendored
4
dist/unix/strawberry.1
vendored
@@ -25,9 +25,9 @@ Features:
|
||||
.br
|
||||
- Fetch tags from MusicBrainz
|
||||
.br
|
||||
- Album cover art from Lastfm, Musicbrainz, Discogs and Deezer
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
.br
|
||||
- Song lyrics from AudD, lyrics.ovh and lololyrics.com
|
||||
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
.br
|
||||
- Support for multiple backends
|
||||
.br
|
||||
|
||||
6
dist/unix/strawberry.spec.in
vendored
6
dist/unix/strawberry.spec.in
vendored
@@ -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
|
||||
|
||||
19
dist/windows/strawberry.nsi.in
vendored
19
dist/windows/strawberry.nsi.in
vendored
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -411,6 +411,10 @@ void CollectionBackend::AddOrUpdateSubdirs(const SubdirectoryList &subdirs) {
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::AddOrUpdateSongsAsync(const SongList &songs) {
|
||||
metaObject()->invokeMethod(this, "AddOrUpdateSongs", Qt::QueuedConnection, Q_ARG(SongList, songs));
|
||||
}
|
||||
|
||||
void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
|
||||
@@ -186,6 +186,8 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
|
||||
Song::Source Source() const;
|
||||
|
||||
void AddOrUpdateSongsAsync(const SongList &songs);
|
||||
|
||||
public slots:
|
||||
void Exit();
|
||||
void LoadDirectories();
|
||||
|
||||
@@ -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())));
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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('(');
|
||||
|
||||
@@ -57,11 +57,16 @@
|
||||
#include "covermanager/musicbrainzcoverprovider.h"
|
||||
#include "covermanager/deezercoverprovider.h"
|
||||
#include "covermanager/qobuzcoverprovider.h"
|
||||
#include "covermanager/musixmatchcoverprovider.h"
|
||||
#include "covermanager/spotifycoverprovider.h"
|
||||
|
||||
#include "lyrics/lyricsproviders.h"
|
||||
#include "lyrics/auddlyricsprovider.h"
|
||||
#include "lyrics/geniuslyricsprovider.h"
|
||||
#include "lyrics/ovhlyricsprovider.h"
|
||||
#include "lyrics/lololyricsprovider.h"
|
||||
#include "lyrics/musixmatchlyricsprovider.h"
|
||||
#include "lyrics/chartlyricsprovider.h"
|
||||
|
||||
#include "scrobbler/audioscrobbler.h"
|
||||
|
||||
@@ -118,9 +123,12 @@ class ApplicationImpl {
|
||||
cover_providers->AddProvider(new DiscogsCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new DeezerCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new QobuzCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new MusixmatchCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new SpotifyCoverProvider(app, app));
|
||||
#ifdef HAVE_TIDAL
|
||||
cover_providers->AddProvider(new TidalCoverProvider(app, app));
|
||||
#endif
|
||||
cover_providers->ReloadSettings();
|
||||
return cover_providers;
|
||||
}),
|
||||
album_cover_loader_([=]() {
|
||||
@@ -131,9 +139,14 @@ class ApplicationImpl {
|
||||
current_albumcover_loader_([=]() { return new CurrentAlbumCoverLoader(app, app); }),
|
||||
lyrics_providers_([=]() {
|
||||
LyricsProviders *lyrics_providers = new LyricsProviders(app);
|
||||
// Initialize the repository of lyrics providers.
|
||||
lyrics_providers->AddProvider(new AuddLyricsProvider(app));
|
||||
lyrics_providers->AddProvider(new GeniusLyricsProvider(app));
|
||||
lyrics_providers->AddProvider(new OVHLyricsProvider(app));
|
||||
lyrics_providers->AddProvider(new LoloLyricsProvider(app));
|
||||
lyrics_providers->AddProvider(new MusixmatchLyricsProvider(app));
|
||||
lyrics_providers->AddProvider(new ChartLyricsProvider(app));
|
||||
lyrics_providers->ReloadSettings();
|
||||
return lyrics_providers;
|
||||
}),
|
||||
internet_services_([=]() {
|
||||
|
||||
@@ -53,6 +53,7 @@ class DeviceManager;
|
||||
class CoverProviders;
|
||||
class AlbumCoverLoader;
|
||||
class CurrentAlbumCoverLoader;
|
||||
class CoverProviders;
|
||||
class LyricsProviders;
|
||||
class AudioScrobbler;
|
||||
class InternetServices;
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
#include "covermanager/albumcoverchoicecontroller.h"
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
#include "covermanager/currentalbumcoverloader.h"
|
||||
#include "covermanager/coverproviders.h"
|
||||
#include "lyrics/lyricsproviders.h"
|
||||
#ifndef Q_OS_WIN
|
||||
# include "device/devicemanager.h"
|
||||
# include "device/devicestatefiltermodel.h"
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
|
||||
void IntroPointReached();
|
||||
|
||||
void AuthorisationUrlReceived(const QUrl &url);
|
||||
void AuthorizationUrlReceived(const QUrl &url);
|
||||
|
||||
private slots:
|
||||
void FilePathChanged(const QString& path);
|
||||
@@ -232,6 +232,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
void OpenSettingsDialogAtPage(SettingsDialog::Page page);
|
||||
|
||||
void TabSwitched();
|
||||
void ToggleSidebar(const bool checked);
|
||||
void ToggleSearchCoverAuto(const bool checked);
|
||||
void SaveGeometry();
|
||||
void SavePlaybackStatus();
|
||||
void LoadPlaybackStatus();
|
||||
|
||||
@@ -506,6 +506,8 @@
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_settings"/>
|
||||
<addaction name="action_console"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_toggle_show_sidebar"/>
|
||||
</widget>
|
||||
<addaction name="menu_music"/>
|
||||
<addaction name="menu_playlist"/>
|
||||
@@ -833,6 +835,14 @@
|
||||
<string>Add stream...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_toggle_show_sidebar">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Show sidebar</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<customwidgets>
|
||||
|
||||
@@ -396,13 +396,13 @@ void Mpris2::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &re
|
||||
AddMetadata("mpris:trackid", current_track_id(), &last_metadata_);
|
||||
|
||||
QUrl cover_url;
|
||||
if (result.cover_url.isValid() && result.cover_url.isLocalFile()) {
|
||||
if (result.cover_url.isValid() && result.cover_url.isLocalFile() && QFile(result.cover_url.toLocalFile()).exists()) {
|
||||
cover_url = result.cover_url;
|
||||
}
|
||||
else if (result.temp_cover_url.isValid() && result.temp_cover_url.isLocalFile()) {
|
||||
cover_url = result.temp_cover_url;
|
||||
}
|
||||
if (cover_url.isValid()) AddMetadata("mpris:artUrl", result.cover_url.toString(), &last_metadata_);
|
||||
if (cover_url.isValid()) AddMetadata("mpris:artUrl", cover_url.toString(), &last_metadata_);
|
||||
|
||||
AddMetadata("year", song.year(), &last_metadata_);
|
||||
AddMetadata("bitrate", song.bitrate(), &last_metadata_);
|
||||
|
||||
@@ -395,6 +395,7 @@ void Player::NextItem(Engine::TrackChangeFlags change) {
|
||||
int i = active_playlist->next_row(ignore_repeat_track);
|
||||
if (i == -1) {
|
||||
app_->playlist_manager()->active()->set_current_row(i);
|
||||
app_->playlist_manager()->active()->reset_last_played();
|
||||
emit PlaylistFinished();
|
||||
Stop();
|
||||
return;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QTimer>
|
||||
#include <QIODevice>
|
||||
#include <QTextStream>
|
||||
#include <QFile>
|
||||
@@ -36,22 +37,16 @@
|
||||
#include "core/logging.h"
|
||||
#include "stylesheetloader.h"
|
||||
|
||||
StyleSheetLoader::StyleSheetLoader(QObject *parent) : QObject(parent) {}
|
||||
StyleSheetLoader::StyleSheetLoader(QObject *parent) : QObject(parent), timer_reset_counter_(new QTimer(this)) {
|
||||
|
||||
void StyleSheetLoader::SetStyleSheet(QWidget *widget, const QString &filename) {
|
||||
timer_reset_counter_->setSingleShot(true);
|
||||
timer_reset_counter_->setInterval(1000);
|
||||
|
||||
widgets_[widget] = qMakePair(filename, QString());
|
||||
widget->installEventFilter(this);
|
||||
UpdateStyleSheet(widget);
|
||||
connect(timer_reset_counter_, SIGNAL(timeout()), this, SLOT(ResetCounters()));
|
||||
|
||||
}
|
||||
|
||||
void StyleSheetLoader::UpdateStyleSheet(QWidget *widget) {
|
||||
|
||||
if (!widget || !widgets_.contains(widget)) return;
|
||||
|
||||
QString filename(widgets_[widget].first);
|
||||
QString stylesheet(widgets_[widget].second);
|
||||
void StyleSheetLoader::SetStyleSheet(QWidget *widget, const QString &filename) {
|
||||
|
||||
// Load the file
|
||||
QFile file(filename);
|
||||
@@ -60,56 +55,71 @@ void StyleSheetLoader::UpdateStyleSheet(QWidget *widget) {
|
||||
return;
|
||||
}
|
||||
QTextStream stream(&file);
|
||||
QString contents;
|
||||
QString stylesheet;
|
||||
forever {
|
||||
QString line = stream.readLine();
|
||||
contents.append(line);
|
||||
stylesheet.append(line);
|
||||
if (stream.atEnd()) break;
|
||||
}
|
||||
file.close();
|
||||
|
||||
StyleSheetData styledata;
|
||||
styledata.filename_ = filename;
|
||||
styledata.stylesheet_template_ = stylesheet;
|
||||
styledata.stylesheet_current_ = widget->styleSheet();
|
||||
styledata_[widget] = styledata;
|
||||
|
||||
widget->installEventFilter(this);
|
||||
UpdateStyleSheet(widget, styledata);
|
||||
|
||||
}
|
||||
|
||||
void StyleSheetLoader::UpdateStyleSheet(QWidget *widget, StyleSheetData styledata) {
|
||||
|
||||
QString stylesheet = styledata.stylesheet_template_;
|
||||
|
||||
// Replace %palette-role with actual colours
|
||||
QPalette p(widget->palette());
|
||||
|
||||
QColor alt = p.color(QPalette::AlternateBase);
|
||||
alt.setAlpha(50);
|
||||
contents.replace("%palette-alternate-base", QString("rgba(%1,%2,%3,%4%)")
|
||||
stylesheet.replace("%palette-alternate-base", QString("rgba(%1,%2,%3,%4%)")
|
||||
.arg(alt.red())
|
||||
.arg(alt.green())
|
||||
.arg(alt.blue())
|
||||
.arg(alt.alpha()));
|
||||
|
||||
ReplaceColor(&contents, "Window", p, QPalette::Window);
|
||||
ReplaceColor(&contents, "Background", p, QPalette::Background);
|
||||
ReplaceColor(&contents, "WindowText", p, QPalette::WindowText);
|
||||
ReplaceColor(&contents, "Foreground", p, QPalette::Foreground);
|
||||
ReplaceColor(&contents, "Base", p, QPalette::Base);
|
||||
ReplaceColor(&contents, "AlternateBase", p, QPalette::AlternateBase);
|
||||
ReplaceColor(&contents, "ToolTipBase", p, QPalette::ToolTipBase);
|
||||
ReplaceColor(&contents, "ToolTipText", p, QPalette::ToolTipText);
|
||||
ReplaceColor(&contents, "Text", p, QPalette::Text);
|
||||
ReplaceColor(&contents, "Button", p, QPalette::Button);
|
||||
ReplaceColor(&contents, "ButtonText", p, QPalette::ButtonText);
|
||||
ReplaceColor(&contents, "BrightText", p, QPalette::BrightText);
|
||||
ReplaceColor(&contents, "Light", p, QPalette::Light);
|
||||
ReplaceColor(&contents, "Midlight", p, QPalette::Midlight);
|
||||
ReplaceColor(&contents, "Dark", p, QPalette::Dark);
|
||||
ReplaceColor(&contents, "Mid", p, QPalette::Mid);
|
||||
ReplaceColor(&contents, "Shadow", p, QPalette::Shadow);
|
||||
ReplaceColor(&contents, "Highlight", p, QPalette::Highlight);
|
||||
ReplaceColor(&contents, "HighlightedText", p, QPalette::HighlightedText);
|
||||
ReplaceColor(&contents, "Link", p, QPalette::Link);
|
||||
ReplaceColor(&contents, "LinkVisited", p, QPalette::LinkVisited);
|
||||
ReplaceColor(&stylesheet, "Window", p, QPalette::Window);
|
||||
ReplaceColor(&stylesheet, "Background", p, QPalette::Background);
|
||||
ReplaceColor(&stylesheet, "WindowText", p, QPalette::WindowText);
|
||||
ReplaceColor(&stylesheet, "Foreground", p, QPalette::Foreground);
|
||||
ReplaceColor(&stylesheet, "Base", p, QPalette::Base);
|
||||
ReplaceColor(&stylesheet, "AlternateBase", p, QPalette::AlternateBase);
|
||||
ReplaceColor(&stylesheet, "ToolTipBase", p, QPalette::ToolTipBase);
|
||||
ReplaceColor(&stylesheet, "ToolTipText", p, QPalette::ToolTipText);
|
||||
ReplaceColor(&stylesheet, "Text", p, QPalette::Text);
|
||||
ReplaceColor(&stylesheet, "Button", p, QPalette::Button);
|
||||
ReplaceColor(&stylesheet, "ButtonText", p, QPalette::ButtonText);
|
||||
ReplaceColor(&stylesheet, "BrightText", p, QPalette::BrightText);
|
||||
ReplaceColor(&stylesheet, "Light", p, QPalette::Light);
|
||||
ReplaceColor(&stylesheet, "Midlight", p, QPalette::Midlight);
|
||||
ReplaceColor(&stylesheet, "Dark", p, QPalette::Dark);
|
||||
ReplaceColor(&stylesheet, "Mid", p, QPalette::Mid);
|
||||
ReplaceColor(&stylesheet, "Shadow", p, QPalette::Shadow);
|
||||
ReplaceColor(&stylesheet, "Highlight", p, QPalette::Highlight);
|
||||
ReplaceColor(&stylesheet, "HighlightedText", p, QPalette::HighlightedText);
|
||||
ReplaceColor(&stylesheet, "Link", p, QPalette::Link);
|
||||
ReplaceColor(&stylesheet, "LinkVisited", p, QPalette::LinkVisited);
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
contents.replace("macos", "*");
|
||||
stylesheet.replace("macos", "*");
|
||||
#endif
|
||||
|
||||
if (contents == stylesheet) return;
|
||||
|
||||
widget->setStyleSheet("");
|
||||
widget->setStyleSheet(contents);
|
||||
widgets_[widget] = qMakePair(filename, contents);
|
||||
if (stylesheet != styledata.stylesheet_current_) {
|
||||
widget->setStyleSheet(stylesheet);
|
||||
styledata.stylesheet_current_ = widget->styleSheet();
|
||||
styledata_[widget] = styledata;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -126,9 +136,23 @@ bool StyleSheetLoader::eventFilter(QObject *obj, QEvent *event) {
|
||||
if (event->type() != QEvent::PaletteChange) return false;
|
||||
|
||||
QWidget *widget = qobject_cast<QWidget*>(obj);
|
||||
if (!widget || !widgets_.contains(widget)) return false;
|
||||
if (!widget || !styledata_.contains(widget)) return false;
|
||||
|
||||
UpdateStyleSheet(widget);
|
||||
StyleSheetData styledata = styledata_[widget];
|
||||
++styledata.count_;
|
||||
styledata_[widget] = styledata;
|
||||
timer_reset_counter_->start();
|
||||
if (styledata.count_ < 5) {
|
||||
UpdateStyleSheet(widget, styledata);
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
void StyleSheetLoader::ResetCounters() {
|
||||
|
||||
for (QMap<QWidget*, StyleSheetData>::iterator i = styledata_.begin() ; i != styledata_.end() ; ++i) {
|
||||
i.value().count_ = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,9 +31,12 @@
|
||||
#include <QString>
|
||||
|
||||
class QWidget;
|
||||
class QTimer;
|
||||
class QEvent;
|
||||
|
||||
class StyleSheetLoader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit StyleSheetLoader(QObject *parent = nullptr);
|
||||
|
||||
@@ -46,12 +49,24 @@ class StyleSheetLoader : public QObject {
|
||||
bool eventFilter(QObject *obj, QEvent *event);
|
||||
|
||||
private:
|
||||
void UpdateStyleSheet(QWidget *widget);
|
||||
void ReplaceColor(QString *css, const QString name, const QPalette &palette, QPalette::ColorRole role) const;
|
||||
struct StyleSheetData {
|
||||
StyleSheetData() : count_(0) {}
|
||||
QString filename_;
|
||||
QString stylesheet_template_;
|
||||
QString stylesheet_current_;
|
||||
int count_;
|
||||
};
|
||||
|
||||
private:
|
||||
QMap<QWidget*, QPair<QString, QString>> widgets_;
|
||||
void UpdateStyleSheet(QWidget *widget, StyleSheetData styledata);
|
||||
void ReplaceColor(QString *css, const QString name, const QPalette &palette, QPalette::ColorRole role) const;
|
||||
|
||||
private slots:
|
||||
void ResetCounters();
|
||||
|
||||
private:
|
||||
QMap<QWidget*, StyleSheetData> styledata_;
|
||||
QTimer *timer_reset_counter_;
|
||||
};
|
||||
|
||||
#endif // STYLESHEETLOADER_H
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -40,8 +40,20 @@ AlbumCoverFetcher::AlbumCoverFetcher(CoverProviders *cover_providers, QObject *p
|
||||
network_(network ? network : new NetworkAccessManager(this)),
|
||||
next_id_(0),
|
||||
request_starter_(new QTimer(this)) {
|
||||
|
||||
request_starter_->setInterval(1000);
|
||||
connect(request_starter_, SIGNAL(timeout()), SLOT(StartRequests()));
|
||||
|
||||
}
|
||||
|
||||
AlbumCoverFetcher::~AlbumCoverFetcher() {
|
||||
|
||||
for (AlbumCoverFetcherSearch *search : active_requests_.values()) {
|
||||
search->disconnect();
|
||||
search->deleteLater();
|
||||
}
|
||||
active_requests_.clear();
|
||||
|
||||
}
|
||||
|
||||
quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString &album, const QString &title, bool fetchall) {
|
||||
@@ -125,8 +137,8 @@ void AlbumCoverFetcher::StartRequests() {
|
||||
|
||||
void AlbumCoverFetcher::SingleSearchFinished(const quint64 request_id, const CoverSearchResults results) {
|
||||
|
||||
if (!active_requests_.contains(request_id)) return;
|
||||
AlbumCoverFetcherSearch *search = active_requests_.take(request_id);
|
||||
if (!search) return;
|
||||
|
||||
search->deleteLater();
|
||||
emit SearchFinished(request_id, results, search->statistics());
|
||||
@@ -135,8 +147,8 @@ void AlbumCoverFetcher::SingleSearchFinished(const quint64 request_id, const Cov
|
||||
|
||||
void AlbumCoverFetcher::SingleCoverFetched(const quint64 request_id, const QUrl &cover_url, const QImage &image) {
|
||||
|
||||
if (!active_requests_.contains(request_id)) return;
|
||||
AlbumCoverFetcherSearch *search = active_requests_.take(request_id);
|
||||
if (!search) return;
|
||||
|
||||
search->deleteLater();
|
||||
emit AlbumCoverFetched(request_id, cover_url, image, search->statistics());
|
||||
|
||||
@@ -89,7 +89,7 @@ class AlbumCoverFetcher : public QObject {
|
||||
|
||||
public:
|
||||
explicit AlbumCoverFetcher(CoverProviders *cover_providers, QObject *parent = nullptr, QNetworkAccessManager *network = 0);
|
||||
virtual ~AlbumCoverFetcher() {}
|
||||
~AlbumCoverFetcher();
|
||||
|
||||
static const int kMaxConcurrentRequests;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -98,27 +98,32 @@ void AlbumCoverLoader::ReloadSettings() {
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::AlbumCoverFilename(QString artist, QString album) {
|
||||
QString AlbumCoverLoader::AlbumCoverFilename(QString artist, QString album, const QString &extension) {
|
||||
|
||||
artist.remove('/');
|
||||
album.remove('/');
|
||||
|
||||
QString filename = artist + "-" + album + ".jpg";
|
||||
QString filename = artist + "-" + album;
|
||||
filename = Utilities::UnicodeToAscii(filename.toLower());
|
||||
filename = filename.replace(' ', '-');
|
||||
filename = filename.replace("--", "-");
|
||||
filename = filename.remove(OrganiseFormat::kInvalidFatCharacters);
|
||||
filename = filename.trimmed();
|
||||
filename = filename.simplified();
|
||||
|
||||
if (!extension.isEmpty()) {
|
||||
filename.append('.');
|
||||
filename.append(extension);
|
||||
}
|
||||
|
||||
return filename;
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url) {
|
||||
return CoverFilePath(song.source(), song.effective_albumartist(), song.album(), song.album_id(), album_dir, cover_url);
|
||||
QString AlbumCoverLoader::CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension) {
|
||||
return CoverFilePath(song.source(), song.effective_albumartist(), song.album(), song.album_id(), album_dir, cover_url, extension);
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url) {
|
||||
QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension) {
|
||||
|
||||
album.remove(Song::kAlbumRemoveDisc);
|
||||
|
||||
@@ -130,7 +135,7 @@ QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString
|
||||
path = Song::ImageCacheDir(source);
|
||||
}
|
||||
|
||||
if (path.right(1) == QDir::separator()) {
|
||||
if (path.right(1) == QDir::separator() || path.right(1) == "/") {
|
||||
path.chop(1);
|
||||
}
|
||||
|
||||
@@ -142,13 +147,17 @@ QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString
|
||||
|
||||
QString filename;
|
||||
if (source == Song::Source_Collection && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern && !cover_pattern_.isEmpty()) {
|
||||
filename = CoverFilenameFromVariable(artist, album) + ".jpg";
|
||||
filename = CoverFilenameFromVariable(artist, album);
|
||||
filename.remove(OrganiseFormat::kInvalidFatCharacters);
|
||||
if (cover_lowercase_) filename = filename.toLower();
|
||||
if (cover_replace_spaces_) filename.replace(QRegExp("\\s"), "-");
|
||||
if (!extension.isEmpty()) {
|
||||
filename.append('.');
|
||||
filename.append(extension);
|
||||
}
|
||||
}
|
||||
else {
|
||||
filename = CoverFilenameFromSource(source, cover_url, artist, album, album_id);
|
||||
filename = CoverFilenameFromSource(source, cover_url, artist, album, album_id, extension);
|
||||
}
|
||||
|
||||
QString filepath(path + "/" + filename);
|
||||
@@ -157,18 +166,21 @@ QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id) {
|
||||
QString AlbumCoverLoader::CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension) {
|
||||
|
||||
QString filename;
|
||||
|
||||
switch (source) {
|
||||
case Song::Source_Tidal:
|
||||
filename = album_id + "-" + cover_url.fileName();
|
||||
break;
|
||||
if (!album_id.isEmpty()) {
|
||||
filename = album_id + "-" + cover_url.fileName();
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
case Song::Source_Subsonic:
|
||||
case Song::Source_Qobuz:
|
||||
filename = AlbumCoverFilename(artist, album);
|
||||
if (filename.length() > 8 && (filename.length() - 5) >= (artist.length() + album.length() - 2)) {
|
||||
if (!album_id.isEmpty()) {
|
||||
filename = album_id;
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
@@ -178,20 +190,29 @@ QString AlbumCoverLoader::CoverFilenameFromSource(const Song::Source source, con
|
||||
case Song::Source_Device:
|
||||
case Song::Source_Stream:
|
||||
case Song::Source_Unknown:
|
||||
filename = Utilities::Sha1CoverHash(artist, album).toHex() + ".jpg";
|
||||
filename = Utilities::Sha1CoverHash(artist, album).toHex();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!extension.isEmpty()) {
|
||||
filename.append('.');
|
||||
filename.append(extension);
|
||||
}
|
||||
|
||||
return filename;
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::CoverFilenameFromVariable(const QString &artist, const QString &album) {
|
||||
QString AlbumCoverLoader::CoverFilenameFromVariable(const QString &artist, const QString &album, const QString &extension) {
|
||||
|
||||
QString filename(cover_pattern_);
|
||||
filename.replace("%albumartist", artist);
|
||||
filename.replace("%artist", artist);
|
||||
filename.replace("%album", album);
|
||||
if (!extension.isEmpty()) {
|
||||
filename.append('.');
|
||||
filename.append(extension);
|
||||
}
|
||||
return filename;
|
||||
|
||||
}
|
||||
@@ -315,7 +336,7 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) {
|
||||
!task->options.scale_output_image_ &&
|
||||
!task->options.pad_output_image_) {
|
||||
task->song.InitArtManual();
|
||||
if (task->art_manual != task->song.art_manual()) {
|
||||
if (task->song.art_manual_is_valid() && task->art_manual != task->song.art_manual()) {
|
||||
task->art_manual = task->song.art_manual();
|
||||
task->art_updated = true;
|
||||
}
|
||||
@@ -338,12 +359,12 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) {
|
||||
|
||||
if (!cover_url.isEmpty() && !cover_url.path().isEmpty()) {
|
||||
if (cover_url.path() == Song::kManuallyUnsetCover) {
|
||||
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_ManuallyUnset, QUrl(), task->options.default_output_image_);
|
||||
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_ManuallyUnset, cover_url, task->options.default_output_image_);
|
||||
}
|
||||
else if (cover_url.path() == Song::kEmbeddedCover && task->song_url.isLocalFile()) {
|
||||
const QImage taglib_image = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task->song_url.toLocalFile());
|
||||
if (!taglib_image.isNull()) {
|
||||
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, QUrl(), ScaleAndPad(task->options, taglib_image).first);
|
||||
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, cover_url, ScaleAndPad(task->options, taglib_image).first);
|
||||
}
|
||||
}
|
||||
else if (cover_url.isLocalFile()) {
|
||||
|
||||
@@ -61,12 +61,13 @@ class AlbumCoverLoader : public QObject {
|
||||
void ExitAsync();
|
||||
void Stop() { stop_requested_ = true; }
|
||||
|
||||
static QString AlbumCoverFilename(QString artist, QString album);
|
||||
static QString AlbumCoverFilename(QString artist, QString album, const QString &extension);
|
||||
|
||||
QString CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id);
|
||||
QString CoverFilenameFromVariable(const QString &artist, const QString &album);
|
||||
QString CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url);
|
||||
QString CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url);
|
||||
QString CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension);
|
||||
QString CoverFilenameFromVariable(const QString &artist, const QString &album, const QString &extension = QString());
|
||||
QString CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString());
|
||||
|
||||
QString CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString());
|
||||
|
||||
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song);
|
||||
virtual quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QUrl &song_url = QUrl(), const Song song = Song(), const QImage &embedded_image = QImage());
|
||||
|
||||
@@ -250,6 +250,9 @@ void AlbumCoverManager::closeEvent(QCloseEvent *e) {
|
||||
// Cancel any outstanding requests
|
||||
CancelRequests();
|
||||
|
||||
ui_->artists->clear();
|
||||
ui_->albums->clear();
|
||||
|
||||
}
|
||||
|
||||
void AlbumCoverManager::LoadGeometry() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -26,5 +27,4 @@
|
||||
#include "core/application.h"
|
||||
#include "coverprovider.h"
|
||||
|
||||
CoverProvider::CoverProvider(const QString &name, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent)
|
||||
: QObject(parent), app_(app), name_(name), quality_(quality), fetchall_(fetchall), allow_missing_album_(allow_missing_album) {}
|
||||
CoverProvider::CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent) : QObject(parent), app_(app), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required), quality_(quality), fetchall_(fetchall), allow_missing_album_(allow_missing_album) {}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -25,7 +26,9 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "albumcoverfetcher.h"
|
||||
|
||||
@@ -37,27 +40,45 @@ class CoverProvider : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CoverProvider(const QString &name, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
|
||||
explicit CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
|
||||
|
||||
// A name (very short description) of this provider, like "last.fm".
|
||||
QString name() const { return name_; }
|
||||
bool is_enabled() const { return enabled_; }
|
||||
int order() const { return order_; }
|
||||
bool quality() const { return quality_; }
|
||||
bool fetchall() const { return fetchall_; }
|
||||
bool allow_missing_album() const { return allow_missing_album_; }
|
||||
|
||||
void set_enabled(const bool enabled) { enabled_ = enabled; }
|
||||
void set_order(const int order) { order_ = order; }
|
||||
|
||||
bool AuthenticationRequired() const { return authentication_required_; }
|
||||
virtual bool IsAuthenticated() const { return true; }
|
||||
virtual void Authenticate() {}
|
||||
virtual void Deauthenticate() {}
|
||||
|
||||
// Starts searching for covers matching the given query text.
|
||||
// Returns true if the query has been started, or false if an error occurred.
|
||||
// The provider should remember the ID and emit it along with the result when it finishes.
|
||||
virtual bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) = 0;
|
||||
virtual void CancelSearch(const int id) { Q_UNUSED(id); }
|
||||
|
||||
virtual void CancelSearch(int id) { Q_UNUSED(id); }
|
||||
virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0;
|
||||
|
||||
signals:
|
||||
void SearchFinished(int id, const CoverSearchResults& results);
|
||||
void AuthenticationComplete(bool, QStringList = QStringList());
|
||||
void AuthenticationSuccess();
|
||||
void AuthenticationFailure(QStringList);
|
||||
void SearchResults(int, CoverSearchResults);
|
||||
void SearchFinished(int, CoverSearchResults);
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
QString name_;
|
||||
bool enabled_;
|
||||
int order_;
|
||||
bool authentication_required_;
|
||||
float quality_;
|
||||
bool fetchall_;
|
||||
bool allow_missing_album_;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,13 +23,23 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QVariant>
|
||||
#include <QVariantList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QSettings>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "coverprovider.h"
|
||||
#include "coverproviders.h"
|
||||
|
||||
#include "settings/coverssettingspage.h"
|
||||
|
||||
int CoverProviders::NextOrderId = 0;
|
||||
|
||||
CoverProviders::CoverProviders(QObject *parent) : QObject(parent) {}
|
||||
|
||||
CoverProviders::~CoverProviders() {
|
||||
@@ -39,6 +50,48 @@ CoverProviders::~CoverProviders() {
|
||||
|
||||
}
|
||||
|
||||
void CoverProviders::ReloadSettings() {
|
||||
|
||||
QMap<int, QString> all_providers;
|
||||
for (CoverProvider *provider : cover_providers_.keys()) {
|
||||
if (!provider->is_enabled()) continue;
|
||||
all_providers.insert(provider->order(), provider->name());
|
||||
}
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(CoversSettingsPage::kSettingsGroup);
|
||||
QStringList providers_enabled = s.value("providers", QStringList() << all_providers.values()).toStringList();
|
||||
s.endGroup();
|
||||
|
||||
int i = 0;
|
||||
QList<CoverProvider*> providers;
|
||||
for (const QString &name : providers_enabled) {
|
||||
CoverProvider *provider = ProviderByName(name);
|
||||
if (provider) {
|
||||
provider->set_enabled(true);
|
||||
provider->set_order(++i);
|
||||
providers << provider;
|
||||
}
|
||||
}
|
||||
|
||||
for (CoverProvider *provider : cover_providers_.keys()) {
|
||||
if (!providers.contains(provider)) {
|
||||
provider->set_enabled(false);
|
||||
provider->set_order(++i);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CoverProvider *CoverProviders::ProviderByName(const QString &name) const {
|
||||
|
||||
for (CoverProvider *provider : cover_providers_.keys()) {
|
||||
if (provider->name() == name) return provider;
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
void CoverProviders::AddProvider(CoverProvider *provider) {
|
||||
|
||||
{
|
||||
@@ -47,6 +100,8 @@ void CoverProviders::AddProvider(CoverProvider *provider) {
|
||||
connect(provider, SIGNAL(destroyed()), SLOT(ProviderDestroyed()));
|
||||
}
|
||||
|
||||
provider->set_order(++NextOrderId);
|
||||
|
||||
qLog(Debug) << "Registered cover provider" << provider->name();
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -42,6 +43,10 @@ class CoverProviders : public QObject {
|
||||
explicit CoverProviders(QObject *parent = nullptr);
|
||||
~CoverProviders();
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
CoverProvider *ProviderByName(const QString &name) const;
|
||||
|
||||
// Lets a cover provider register itself in the repository.
|
||||
void AddProvider(CoverProvider *provider);
|
||||
void RemoveProvider(CoverProvider *provider);
|
||||
@@ -60,6 +65,8 @@ class CoverProviders : public QObject {
|
||||
private:
|
||||
Q_DISABLE_COPY(CoverProviders)
|
||||
|
||||
static int NextOrderId;
|
||||
|
||||
QMap<CoverProvider*, QString> cover_providers_;
|
||||
QMutex mutex_;
|
||||
|
||||
|
||||
@@ -45,13 +45,24 @@
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "deezercoverprovider.h"
|
||||
|
||||
const char *DeezerCoverProvider::kApiUrl = "https://api.deezer.com";
|
||||
const int DeezerCoverProvider::kLimit = 10;
|
||||
|
||||
DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): CoverProvider("Deezer", 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): JsonCoverProvider("Deezer", true, false, 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
DeezerCoverProvider::~DeezerCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
@@ -83,6 +94,7 @@ bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &albu
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); });
|
||||
|
||||
return true;
|
||||
@@ -139,36 +151,6 @@ QByteArray DeezerCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
QJsonObject DeezerCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error("Reply from server missing Json data.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error("Json document is not an object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error("Received empty Json object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
QJsonValue DeezerCoverProvider::ExtractData(const QByteArray &data) {
|
||||
|
||||
@@ -204,6 +186,9 @@ QJsonValue DeezerCoverProvider::ExtractData(const QByteArray &data) {
|
||||
|
||||
void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
|
||||
@@ -23,23 +23,26 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
|
||||
class DeezerCoverProvider : public CoverProvider {
|
||||
class DeezerCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeezerCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~DeezerCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
@@ -48,7 +51,6 @@ class DeezerCoverProvider : public CoverProvider {
|
||||
|
||||
private:
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
QJsonValue ExtractData(const QByteArray &data);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
@@ -57,6 +59,7 @@ class DeezerCoverProvider : public CoverProvider {
|
||||
static const int kLimit;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -32,11 +32,13 @@
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QMap>
|
||||
#include <QQueue>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QTimer>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
@@ -50,41 +52,53 @@
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/utilities.h"
|
||||
#include "coverprovider.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "discogscoverprovider.h"
|
||||
|
||||
const char *DiscogsCoverProvider::kUrlSearch = "https://api.discogs.com/database/search";
|
||||
const char *DiscogsCoverProvider::kAccessKeyB64 = "dGh6ZnljUGJlZ1NEeXBuSFFxSVk=";
|
||||
const char *DiscogsCoverProvider::kSecretKeyB64 = "ZkFIcmlaSER4aHhRSlF2U3d0bm5ZVmdxeXFLWUl0UXI=";
|
||||
const int DiscogsCoverProvider::kRequestsDelay = 1000;
|
||||
|
||||
DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) : CoverProvider("Discogs", 0.0, false, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) :
|
||||
JsonCoverProvider("Discogs", false, false, 0.0, false, false, app, parent),
|
||||
network_(new NetworkAccessManager(this)), timer_flush_requests_(new QTimer(this)) {
|
||||
|
||||
timer_flush_requests_->setInterval(kRequestsDelay);
|
||||
timer_flush_requests_->setSingleShot(false);
|
||||
connect(timer_flush_requests_, SIGNAL(timeout()), this, SLOT(FlushRequests()));
|
||||
|
||||
}
|
||||
|
||||
DiscogsCoverProvider::~DiscogsCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
timer_flush_requests_->stop();
|
||||
queue_search_requests_.clear();
|
||||
queue_release_requests_.clear();
|
||||
requests_search_.clear();
|
||||
|
||||
}
|
||||
|
||||
bool DiscogsCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
|
||||
std::shared_ptr<DiscogsCoverSearchContext> search = std::make_shared<DiscogsCoverSearchContext>();
|
||||
std::shared_ptr<DiscogsCoverSearchContext> search = std::make_shared<DiscogsCoverSearchContext>(id, artist, album);
|
||||
|
||||
search->id = id;
|
||||
search->artist = artist;
|
||||
search->album = album;
|
||||
requests_search_.insert(id, search);
|
||||
requests_search_.insert(search->id, search);
|
||||
queue_search_requests_.enqueue(search);
|
||||
|
||||
ParamList params = ParamList() << Param("type", "release");
|
||||
if (!search->artist.isEmpty()) {
|
||||
params.append(Param("artist", search->artist.toLower()));
|
||||
if (!timer_flush_requests_->isActive()) {
|
||||
timer_flush_requests_->start();
|
||||
}
|
||||
if (!search->album.isEmpty()) {
|
||||
params.append(Param("release_title", search->album.toLower()));
|
||||
}
|
||||
|
||||
QNetworkReply *reply = CreateRequest(QUrl(kUrlSearch), params);
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); });
|
||||
|
||||
return true;
|
||||
|
||||
@@ -96,6 +110,42 @@ void DiscogsCoverProvider::CancelSearch(const int id) {
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::FlushRequests() {
|
||||
|
||||
if (!queue_release_requests_.isEmpty()) {
|
||||
SendReleaseRequest(queue_release_requests_.dequeue());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!queue_search_requests_.isEmpty()) {
|
||||
SendSearchRequest(queue_search_requests_.dequeue());
|
||||
return;
|
||||
}
|
||||
|
||||
timer_flush_requests_->stop();
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::SendSearchRequest(std::shared_ptr<DiscogsCoverSearchContext> search) {
|
||||
|
||||
ParamList params = ParamList() << Param("format", "album")
|
||||
<< Param("artist", search->artist.toLower())
|
||||
<< Param("release_title", search->album.toLower());
|
||||
|
||||
switch (search->type) {
|
||||
case DiscogsCoverType_Master:
|
||||
params << Param("type", "master");
|
||||
break;
|
||||
case DiscogsCoverType_Release:
|
||||
params << Param("type", "release");
|
||||
break;
|
||||
}
|
||||
|
||||
QNetworkReply *reply = CreateRequest(QUrl(kUrlSearch), params);
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, search->id); });
|
||||
|
||||
}
|
||||
|
||||
QNetworkReply *DiscogsCoverProvider::CreateRequest(QUrl url, const ParamList ¶ms_provided) {
|
||||
|
||||
ParamList params = ParamList() << Param("key", QByteArray::fromBase64(kAccessKeyB64))
|
||||
@@ -124,6 +174,9 @@ QNetworkReply *DiscogsCoverProvider::CreateRequest(QUrl url, const ParamList &pa
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
|
||||
qLog(Debug) << "Discogs: Sending request" << url;
|
||||
|
||||
return reply;
|
||||
|
||||
@@ -171,40 +224,14 @@ QByteArray DiscogsCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
}
|
||||
|
||||
QJsonObject DiscogsCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error("Reply from server missing Json data.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
if (!json_doc.isObject()) {
|
||||
Error("Json document is not an object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error("Received empty Json object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(id)) {
|
||||
return;
|
||||
}
|
||||
if (!requests_search_.contains(id)) return;
|
||||
std::shared_ptr<DiscogsCoverSearchContext> search = requests_search_.value(id);
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
@@ -235,18 +262,13 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value_results.isArray()) {
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray array_results = value_results.toArray();
|
||||
if (array_results.isEmpty()) {
|
||||
EndSearch(search);
|
||||
return;
|
||||
QJsonArray array_results;
|
||||
if (value_results.isArray()) {
|
||||
array_results = value_results.toArray();
|
||||
}
|
||||
|
||||
for (const QJsonValue &value_result : array_results) {
|
||||
|
||||
if (!value_result.isObject()) {
|
||||
Error("Invalid Json reply, results value is not a object.", value_result);
|
||||
continue;
|
||||
@@ -258,72 +280,95 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
}
|
||||
quint64 release_id = obj_result["id"].toDouble();
|
||||
QUrl resource_url(obj_result["resource_url"].toString());
|
||||
if (!resource_url.isValid()) {
|
||||
continue;
|
||||
QString title = obj_result["title"].toString();
|
||||
|
||||
if (title.contains(" - ")) {
|
||||
QStringList title_splitted = title.split(" - ");
|
||||
if (title_splitted.count() == 2) {
|
||||
QString artist = title_splitted.first();
|
||||
title = title_splitted.last();
|
||||
if (artist.toLower() != search->artist.toLower() && title.toLower() != search->album.toLower()) continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resource_url.isValid()) continue;
|
||||
if (search->requests_release_.contains(release_id)) {
|
||||
continue;
|
||||
}
|
||||
StartRelease(search, release_id, resource_url);
|
||||
StartReleaseRequest(search, release_id, resource_url);
|
||||
}
|
||||
|
||||
if (search->requests_release_.count() <= 0) {
|
||||
EndSearch(search);
|
||||
if (search->requests_release_.count() == 0) {
|
||||
if (search->type == DiscogsCoverType_Master) {
|
||||
search->type = DiscogsCoverType_Release;
|
||||
queue_search_requests_.enqueue(search);
|
||||
}
|
||||
else {
|
||||
EndSearch(search);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::StartRelease(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id, const QUrl &url) {
|
||||
void DiscogsCoverProvider::StartReleaseRequest(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id, const QUrl &url) {
|
||||
|
||||
DiscogsCoverReleaseContext release(release_id, url);
|
||||
DiscogsCoverReleaseContext release(search->id, release_id, url);
|
||||
search->requests_release_.insert(release_id, release);
|
||||
queue_release_requests_.enqueue(release);
|
||||
|
||||
if (!timer_flush_requests_->isActive()) {
|
||||
timer_flush_requests_->start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::SendReleaseRequest(const DiscogsCoverReleaseContext release) {
|
||||
|
||||
QNetworkReply *reply = CreateRequest(release.url);
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleReleaseReply(reply, search->id, release.id); });
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleReleaseReply(reply, release.search_id, release.id); });
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int search_id, const quint64 release_id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(search_id)) {
|
||||
return;
|
||||
}
|
||||
if (!requests_search_.contains(search_id)) return;
|
||||
std::shared_ptr<DiscogsCoverSearchContext> search = requests_search_.value(search_id);
|
||||
|
||||
if (!search->requests_release_.contains(release_id)) {
|
||||
return;
|
||||
}
|
||||
if (!search->requests_release_.contains(release_id)) return;
|
||||
const DiscogsCoverReleaseContext &release = search->requests_release_.value(release_id);
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (data.isEmpty()) {
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) {
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("artists") || !json_obj.contains("title")) {
|
||||
Error("Json reply object is missing artists or title.", json_obj);
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("images")) {
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_artists = json_obj["artists"];
|
||||
if (!value_artists.isArray()) {
|
||||
Error("Json reply object artists is not a array.", value_artists);
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
QJsonArray array_artists = value_artists.toArray();
|
||||
@@ -345,28 +390,28 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
|
||||
}
|
||||
|
||||
if (artist.isEmpty()) {
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
if (i > 1 && artist != search->artist) artist = "Various artists";
|
||||
|
||||
QString album = json_obj["title"].toString();
|
||||
if (artist != search->artist && album != search->album) {
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_images = json_obj["images"];
|
||||
if (!value_images.isArray()) {
|
||||
Error("Json images is not an array.");
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
QJsonArray array_images = value_images.toArray();
|
||||
|
||||
if (array_images.isEmpty()) {
|
||||
Error("Invalid Json reply, images array is empty.");
|
||||
EndSearch(search, release);
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -390,28 +435,35 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
|
||||
if (width < 300 || height < 300) continue;
|
||||
const float aspect_score = 1.0 - float(std::max(width, height) - std::min(width, height)) / std::max(height, width);
|
||||
if (aspect_score < 0.85) continue;
|
||||
CoverSearchResult cover_result;
|
||||
cover_result.artist = artist;
|
||||
cover_result.album = album;
|
||||
cover_result.image_url = QUrl(obj_image["resource_url"].toString());
|
||||
if (cover_result.image_url.isEmpty()) continue;
|
||||
search->results.append(cover_result);
|
||||
CoverSearchResult result;
|
||||
result.artist = artist;
|
||||
result.album = album;
|
||||
result.image_url = QUrl(obj_image["resource_url"].toString());
|
||||
if (result.image_url.isEmpty()) continue;
|
||||
search->results.append(result);
|
||||
}
|
||||
|
||||
EndSearch(search, release);
|
||||
emit SearchResults(search->id, search->results);
|
||||
search->results.clear();
|
||||
|
||||
EndSearch(search, release.id);
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::EndSearch(std::shared_ptr<DiscogsCoverSearchContext> search, const DiscogsCoverReleaseContext &release) {
|
||||
void DiscogsCoverProvider::EndSearch(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id) {
|
||||
|
||||
if (search->requests_release_.contains(release.id)) {
|
||||
search->requests_release_.remove(release.id);
|
||||
if (search->requests_release_.contains(release_id)) {
|
||||
search->requests_release_.remove(release_id);
|
||||
}
|
||||
if (search->requests_release_.count() <= 0) {
|
||||
requests_search_.remove(search->id);
|
||||
emit SearchFinished(search->id, search->results);
|
||||
}
|
||||
|
||||
if (queue_release_requests_.isEmpty() && queue_search_requests_.isEmpty()) {
|
||||
timer_flush_requests_->stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DiscogsCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
@@ -28,20 +28,24 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QMetaType>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QQueue>
|
||||
#include <QMap>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class QTimer;
|
||||
class Application;
|
||||
|
||||
class DiscogsCoverProvider : public CoverProvider {
|
||||
class DiscogsCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
@@ -49,24 +53,25 @@ class DiscogsCoverProvider : public CoverProvider {
|
||||
~DiscogsCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
|
||||
void CancelSearch(const int id);
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id);
|
||||
void HandleReleaseReply(QNetworkReply *reply, const int id, const quint64 release_id);
|
||||
enum DiscogsCoverType {
|
||||
DiscogsCoverType_Master,
|
||||
DiscogsCoverType_Release,
|
||||
};
|
||||
|
||||
public:
|
||||
struct DiscogsCoverReleaseContext {
|
||||
explicit DiscogsCoverReleaseContext(const quint64 _id = 0, const QUrl &_url = QUrl()) : id(_id), url(_url) {}
|
||||
explicit DiscogsCoverReleaseContext(const quint64 _search_id = 0, const quint64 _id = 0, const QUrl &_url = QUrl()) : search_id(_search_id), id(_id), url(_url) {}
|
||||
quint64 search_id;
|
||||
quint64 id;
|
||||
QUrl url;
|
||||
};
|
||||
struct DiscogsCoverSearchContext {
|
||||
explicit DiscogsCoverSearchContext() : id(-1) {}
|
||||
explicit DiscogsCoverSearchContext(const int _id = 0, const QString &_artist = QString(), const QString &_album = QString(), const DiscogsCoverType _type = DiscogsCoverType_Master) : id(_id), artist(_artist), album(_album), type(_type) {}
|
||||
int id;
|
||||
QString artist;
|
||||
QString album;
|
||||
DiscogsCoverType type;
|
||||
QMap<quint64, DiscogsCoverReleaseContext> requests_release_;
|
||||
CoverSearchResults results;
|
||||
};
|
||||
@@ -75,20 +80,31 @@ class DiscogsCoverProvider : public CoverProvider {
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
void SendSearchRequest(std::shared_ptr<DiscogsCoverSearchContext> search);
|
||||
void SendReleaseRequest(const DiscogsCoverReleaseContext release);
|
||||
QNetworkReply *CreateRequest(QUrl url, const ParamList ¶ms_provided = ParamList());
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
void StartRelease(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id, const QUrl &url);
|
||||
void EndSearch(std::shared_ptr<DiscogsCoverSearchContext> search, const DiscogsCoverReleaseContext &release = DiscogsCoverReleaseContext());
|
||||
void StartReleaseRequest(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id, const QUrl &url);
|
||||
void EndSearch(std::shared_ptr<DiscogsCoverSearchContext> search, const quint64 release_id = 0);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private slots:
|
||||
void FlushRequests();
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id);
|
||||
void HandleReleaseReply(QNetworkReply *reply, const int id, const quint64 release_id);
|
||||
|
||||
private:
|
||||
static const char *kUrlSearch;
|
||||
static const char *kAccessKeyB64;
|
||||
static const char *kSecretKeyB64;
|
||||
static const int kRequestsDelay;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
QTimer *timer_flush_requests_;
|
||||
QQueue<std::shared_ptr<DiscogsCoverSearchContext>> queue_search_requests_;
|
||||
QQueue<DiscogsCoverReleaseContext> queue_release_requests_;
|
||||
QMap<int, std::shared_ptr<DiscogsCoverSearchContext>> requests_search_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
64
src/covermanager/jsoncoverprovider.cpp
Normal file
64
src/covermanager/jsoncoverprovider.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
JsonCoverProvider::JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent) : CoverProvider(name, enabled, authentication_required, quality, fetchall, allow_missing_album, app, parent) {}
|
||||
|
||||
QJsonObject JsonCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
Error(QString("Failed to parse json data: %1").arg(json_error.errorString()));
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error("Json document is not an object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error("Received empty Json object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
44
src/covermanager/jsoncoverprovider.h
Normal file
44
src/covermanager/jsoncoverprovider.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef JSONCOVERPROVIDER_H
|
||||
#define JSONCOVERPROVIDER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
|
||||
class Application;
|
||||
|
||||
class JsonCoverProvider : public CoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
|
||||
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
|
||||
};
|
||||
|
||||
#endif // JSONCOVERPROVIDER_H
|
||||
@@ -45,7 +45,7 @@
|
||||
#include "core/network.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "lastfmcoverprovider.h"
|
||||
|
||||
@@ -53,7 +53,18 @@ const char *LastFmCoverProvider::kUrl = "https://ws.audioscrobbler.com/2.0/";
|
||||
const char *LastFmCoverProvider::kApiKey = "211990b4c96782c05d1536e7219eb56e";
|
||||
const char *LastFmCoverProvider::kSecret = "80fd738f49596e9709b1bf9319c444a8";
|
||||
|
||||
LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : CoverProvider("last.fm", 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : JsonCoverProvider("Last.fm", true, false, 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
LastFmCoverProvider::~LastFmCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
@@ -100,6 +111,7 @@ bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &albu
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8());
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { QueryFinished(reply, id, type); });
|
||||
|
||||
return true;
|
||||
@@ -108,6 +120,9 @@ bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &albu
|
||||
|
||||
void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, const QString &type) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
@@ -303,36 +318,9 @@ QByteArray LastFmCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
}
|
||||
|
||||
QJsonObject LastFmCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error("Reply from server missing Json data.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
if (!json_doc.isObject()) {
|
||||
Error("Json document is not an object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error("Received empty Json object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
void LastFmCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "LastFm:" << error;
|
||||
qLog(Error) << "Last.fm:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
|
||||
@@ -23,22 +23,25 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
|
||||
class LastFmCoverProvider : public CoverProvider {
|
||||
class LastFmCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LastFmCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~LastFmCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
|
||||
private slots:
|
||||
@@ -54,7 +57,6 @@ class LastFmCoverProvider : public CoverProvider {
|
||||
};
|
||||
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
LastFmImageSize ImageSizeFromString(const QString &size);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
@@ -64,6 +66,7 @@ class LastFmCoverProvider : public CoverProvider {
|
||||
static const char *kSecret;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -23,11 +23,13 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QQueue>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QTimer>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
@@ -41,20 +43,51 @@
|
||||
#include "core/network.h"
|
||||
#include "core/logging.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "musicbrainzcoverprovider.h"
|
||||
|
||||
const char *MusicbrainzCoverProvider::kReleaseSearchUrl = "https://musicbrainz.org/ws/2/release/";
|
||||
const char *MusicbrainzCoverProvider::kAlbumCoverUrl = "https://coverartarchive.org/release/%1/front";
|
||||
const int MusicbrainzCoverProvider::kLimit = 8;
|
||||
const int MusicbrainzCoverProvider::kRequestsDelay = 1000;
|
||||
|
||||
MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): CoverProvider("MusicBrainz", 1.5, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): JsonCoverProvider("MusicBrainz", true, false, 1.5, true, false, app, parent), network_(new NetworkAccessManager(this)), timer_flush_requests_(new QTimer(this)) {
|
||||
|
||||
timer_flush_requests_->setInterval(kRequestsDelay);
|
||||
timer_flush_requests_->setSingleShot(false);
|
||||
connect(timer_flush_requests_, SIGNAL(timeout()), this, SLOT(FlushRequests()));
|
||||
|
||||
}
|
||||
|
||||
MusicbrainzCoverProvider::~MusicbrainzCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
|
||||
QString query = QString("release:\"%1\" AND artist:\"%2\"").arg(album.trimmed().replace('"', "\\\"")).arg(artist.trimmed().replace('"', "\\\""));
|
||||
SearchRequest request(id, artist, album);
|
||||
queue_search_requests_ << request;
|
||||
|
||||
if (!timer_flush_requests_->isActive()) {
|
||||
timer_flush_requests_->start();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void MusicbrainzCoverProvider::SendSearchRequest(const SearchRequest &request) {
|
||||
|
||||
QString query = QString("release:\"%1\" AND artist:\"%2\"").arg(request.album.trimmed().replace('"', "\\\"")).arg(request.artist.trimmed().replace('"', "\\\""));
|
||||
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem("query", query);
|
||||
@@ -66,14 +99,27 @@ bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); });
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, request.id); });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MusicbrainzCoverProvider::FlushRequests() {
|
||||
|
||||
if (!queue_search_requests_.isEmpty()) {
|
||||
SendSearchRequest(queue_search_requests_.dequeue());
|
||||
return;
|
||||
}
|
||||
|
||||
timer_flush_requests_->stop();
|
||||
|
||||
}
|
||||
|
||||
void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int search_id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
@@ -218,33 +264,6 @@ QByteArray MusicbrainzCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
}
|
||||
|
||||
QJsonObject MusicbrainzCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error("Reply from server is missing Json data.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
if (!json_doc.isObject()) {
|
||||
Error("Json document is not an object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error("Received empty Json object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
void MusicbrainzCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "Musicbrainz:" << error;
|
||||
|
||||
@@ -23,38 +23,55 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QQueue>
|
||||
#include <QByteArray>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class QTimer;
|
||||
class Application;
|
||||
|
||||
class MusicbrainzCoverProvider : public CoverProvider {
|
||||
class MusicbrainzCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MusicbrainzCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~MusicbrainzCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
|
||||
private slots:
|
||||
void FlushRequests();
|
||||
void HandleSearchReply(QNetworkReply *reply, const int search_id);
|
||||
|
||||
private:
|
||||
struct SearchRequest {
|
||||
explicit SearchRequest(const int _id, const QString &_artist, const QString &_album) : id(_id), artist(_artist), album(_album) {}
|
||||
int id;
|
||||
QString artist;
|
||||
QString album;
|
||||
};
|
||||
|
||||
void SendSearchRequest(const SearchRequest &request);
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private:
|
||||
static const char *kReleaseSearchUrl;
|
||||
static const char *kAlbumCoverUrl;
|
||||
static const int kLimit;
|
||||
static const int kRequestsDelay;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
QTimer *timer_flush_requests_;
|
||||
QQueue<SearchRequest> queue_search_requests_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
239
src/covermanager/musixmatchcoverprovider.cpp
Normal file
239
src/covermanager/musixmatchcoverprovider.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QTextCodec>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonObject>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "musixmatchcoverprovider.h"
|
||||
|
||||
MusixmatchCoverProvider::MusixmatchCoverProvider(Application *app, QObject *parent): JsonCoverProvider("Musixmatch", true, false, 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
MusixmatchCoverProvider::~MusixmatchCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool MusixmatchCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
|
||||
QString artist_stripped = artist;
|
||||
QString album_stripped = album;
|
||||
|
||||
artist_stripped = artist_stripped.replace('/', '-');
|
||||
artist_stripped = artist_stripped.remove(QRegExp("[^A-Za-z0-9\\- ]"));
|
||||
artist_stripped = artist_stripped.simplified();
|
||||
artist_stripped = artist_stripped.replace(' ', '-');
|
||||
artist_stripped = artist_stripped.replace(QRegExp("(-)\\1+"), "-");
|
||||
artist_stripped = artist_stripped.toLower();
|
||||
|
||||
album_stripped = album_stripped.replace('/', '-');
|
||||
album_stripped = album_stripped.remove(QRegExp("[^a-zA-Z0-9\\- ]"));
|
||||
album_stripped = album_stripped.simplified();
|
||||
album_stripped = album_stripped.replace(' ', '-').toLower();
|
||||
album_stripped = album_stripped.replace(QRegExp("(-)\\1+"), "-");
|
||||
album_stripped = album_stripped.toLower();
|
||||
|
||||
if (artist_stripped.isEmpty() || album_stripped.isEmpty()) return false;
|
||||
|
||||
QUrl url(QString("https://www.musixmatch.com/album/%1/%2").arg(artist_stripped).arg(album_stripped));
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, artist, album); });
|
||||
|
||||
//qLog(Debug) << "Musixmatch: Sending request for" << artist_stripped << album_stripped << url;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void MusixmatchCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); }
|
||||
|
||||
void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id, const QString &artist, const QString &album) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
Error("Empty reply received from server.");
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCodec *codec = QTextCodec::codecForName("utf-8");
|
||||
if (!codec) {
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
QString content = codec->toUnicode(data);
|
||||
|
||||
QString data_begin = "var __mxmState = ";
|
||||
QString data_end = ";</script>";
|
||||
int begin_idx = content.indexOf(data_begin);
|
||||
QString content_json;
|
||||
if (begin_idx > 0) {
|
||||
begin_idx += data_begin.length();
|
||||
int end_idx = content.indexOf(data_end, begin_idx);
|
||||
if (end_idx > begin_idx) {
|
||||
content_json = content.mid(begin_idx, end_idx - begin_idx);
|
||||
}
|
||||
}
|
||||
|
||||
if (content_json.isEmpty()) {
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
if (content_json.contains(QRegExp("<[^>]*>"))) { // Make sure it's not HTML code.
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(content_json.toUtf8(), &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error(QString("Failed to parse json data: %1").arg(error.errorString()));
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", data);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error("Json document is not an object.", json_doc);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error("Received empty Json object.", json_doc);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("page") || !json_obj["page"].isObject()) {
|
||||
Error("Json reply is missing page object.", json_obj);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
json_obj = json_obj["page"].toObject();
|
||||
|
||||
if (!json_obj.contains("album") || !json_obj["album"].isObject()) {
|
||||
Error("Json page object is missing album object.", json_obj);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_album = json_obj["album"].toObject();
|
||||
|
||||
if (!obj_album.contains("artistName") || !obj_album.contains("name")) {
|
||||
Error("Json album object is missing artistName or name.", obj_album);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QString cover;
|
||||
|
||||
if (obj_album.contains("coverart800x800")) {
|
||||
cover = obj_album["coverart800x800"].toString();
|
||||
}
|
||||
else if (obj_album.contains("coverart500x500")) {
|
||||
cover = obj_album["coverart500x500"].toString();
|
||||
}
|
||||
else if (obj_album.contains("coverart350x350")) {
|
||||
cover = obj_album["coverart350x350"].toString();
|
||||
}
|
||||
|
||||
if (cover.isEmpty()) {
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
QUrl cover_url(cover);
|
||||
if (!cover_url.isValid()) {
|
||||
Error("Received cover url is not valid.", cover);
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
CoverSearchResult result;
|
||||
result.artist = obj_album["artistName"].toString();
|
||||
result.album = obj_album["name"].toString();
|
||||
result.image_url = cover_url;
|
||||
|
||||
if (artist.toLower() == result.artist.toLower() || album.toLower() == result.album.toLower()) {
|
||||
results.append(result);
|
||||
}
|
||||
|
||||
emit SearchFinished(id, results);
|
||||
|
||||
}
|
||||
|
||||
void MusixmatchCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "Musixmatch:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
58
src/covermanager/musixmatchcoverprovider.h
Normal file
58
src/covermanager/musixmatchcoverprovider.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MUSIXMATCHCOVERPROVIDER_H
|
||||
#define MUSIXMATCHCOVERPROVIDER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
class MusixmatchCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MusixmatchCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~MusixmatchCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
private:
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id, const QString &artist, const QString &album);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // MUSIXMATCHCOVERPROVIDER_H
|
||||
@@ -44,13 +44,25 @@
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "qobuzcoverprovider.h"
|
||||
|
||||
const char *QobuzCoverProvider::kApiUrl = "https://www.qobuz.com/api.json/0.2";
|
||||
const char *QobuzCoverProvider::kAppID = "OTQyODUyNTY3";
|
||||
const int QobuzCoverProvider::kLimit = 10;
|
||||
|
||||
QobuzCoverProvider::QobuzCoverProvider(Application *app, QObject *parent) : CoverProvider("Qobuz", 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
QobuzCoverProvider::QobuzCoverProvider(Application *app, QObject *parent) : JsonCoverProvider("Qobuz", true, false, 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
QobuzCoverProvider::~QobuzCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
@@ -87,6 +99,7 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
req.setRawHeader("X-App-Id", kAppID);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); });
|
||||
|
||||
return true;
|
||||
@@ -139,38 +152,11 @@ QByteArray QobuzCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
}
|
||||
|
||||
QJsonObject QobuzCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
Error("Reply from server missing Json data.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error("Json document is not an object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error("Received empty Json object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
|
||||
@@ -23,22 +23,25 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
|
||||
class QobuzCoverProvider : public CoverProvider {
|
||||
class QobuzCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QobuzCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~QobuzCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
@@ -47,7 +50,6 @@ class QobuzCoverProvider : public CoverProvider {
|
||||
|
||||
private:
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private:
|
||||
@@ -56,6 +58,7 @@ class QobuzCoverProvider : public CoverProvider {
|
||||
static const int kLimit;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
551
src/covermanager/spotifycoverprovider.cpp
Normal file
551
src/covermanager/spotifycoverprovider.cpp
Normal file
@@ -0,0 +1,551 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QDateTime>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QSslError>
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDesktopServices>
|
||||
#include <QMessageBox>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/network.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "internet/localredirectserver.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "spotifycoverprovider.h"
|
||||
|
||||
const char *SpotifyCoverProvider::kSettingsGroup = "Spotify";
|
||||
const char *SpotifyCoverProvider::kOAuthAuthorizeUrl = "https://accounts.spotify.com/authorize";
|
||||
const char *SpotifyCoverProvider::kOAuthAccessTokenUrl = "https://accounts.spotify.com/api/token";
|
||||
const char *SpotifyCoverProvider::kOAuthRedirectUrl = "http://localhost:63111/";
|
||||
const char *SpotifyCoverProvider::kClientIDB64 = "ZTZjY2Y2OTQ5NzY1NGE3NThjOTAxNWViYzdiMWQzMTc=";
|
||||
const char *SpotifyCoverProvider::kClientSecretB64 = "N2ZlMDMxODk1NTBlNDE3ZGI1ZWQ1MzE3ZGZlZmU2MTE=";
|
||||
const char *SpotifyCoverProvider::kApiUrl = "https://api.spotify.com/v1";
|
||||
const int SpotifyCoverProvider::kLimit = 10;
|
||||
|
||||
SpotifyCoverProvider::SpotifyCoverProvider(Application *app, QObject *parent) : JsonCoverProvider("Spotify", true, true, 2.5, true, true, app, parent), network_(new NetworkAccessManager(this)), server_(nullptr), expires_in_(0), login_time_(0) {
|
||||
|
||||
refresh_login_timer_.setSingleShot(true);
|
||||
connect(&refresh_login_timer_, SIGNAL(timeout()), SLOT(RequestAccessToken()));
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
access_token_ = s.value("access_token").toString();
|
||||
refresh_token_ = s.value("refresh_token").toString();
|
||||
expires_in_ = s.value("expires_in").toLongLong();
|
||||
login_time_ = s.value("login_time").toLongLong();
|
||||
s.endGroup();
|
||||
|
||||
if (!refresh_token_.isEmpty()) {
|
||||
qint64 time = expires_in_ - (QDateTime::currentDateTime().toTime_t() - login_time_);
|
||||
if (time < 6) time = 6;
|
||||
refresh_login_timer_.setInterval(time * kMsecPerSec);
|
||||
refresh_login_timer_.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SpotifyCoverProvider::~SpotifyCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::Authenticate() {
|
||||
|
||||
QUrl redirect_url(kOAuthRedirectUrl);
|
||||
|
||||
if (!server_) {
|
||||
server_ = new LocalRedirectServer(this);
|
||||
server_->set_https(false);
|
||||
int port = redirect_url.port();
|
||||
int port_max = port + 10;
|
||||
bool success = false;
|
||||
forever {
|
||||
server_->set_port(port);
|
||||
if (server_->Listen()) { success = true; break; }
|
||||
++port;
|
||||
if (port > port_max) break;
|
||||
}
|
||||
if (!success) {
|
||||
AuthError(server_->error());
|
||||
server_->deleteLater();
|
||||
server_ = nullptr;
|
||||
return;
|
||||
}
|
||||
connect(server_, SIGNAL(Finished()), this, SLOT(RedirectArrived()));
|
||||
}
|
||||
|
||||
code_verifier_ = Utilities::CryptographicRandomString(44);
|
||||
code_challenge_ = QString(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding));
|
||||
if (code_challenge_.lastIndexOf(QChar('=')) == code_challenge_.length() - 1) {
|
||||
code_challenge_.chop(1);
|
||||
}
|
||||
|
||||
const ParamList params = ParamList() << Param("client_id", QByteArray::fromBase64(kClientIDB64))
|
||||
<< Param("response_type", "code")
|
||||
<< Param("redirect_uri", redirect_url.toString())
|
||||
<< Param("state", code_challenge_);
|
||||
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl url(kOAuthAuthorizeUrl);
|
||||
url.setQuery(url_query);
|
||||
|
||||
const bool result = QDesktopServices::openUrl(url);
|
||||
if (!result) {
|
||||
QMessageBox messagebox(QMessageBox::Information, tr("Spotify Authentication"), tr("Please open this URL in your browser") + QString(":<br /><a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
|
||||
messagebox.setTextFormat(Qt::RichText);
|
||||
messagebox.exec();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::Deauthenticate() {
|
||||
|
||||
access_token_.clear();
|
||||
refresh_token_.clear();
|
||||
expires_in_ = 0;
|
||||
login_time_ = 0;
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.remove("access_token");
|
||||
s.remove("refresh_token");
|
||||
s.remove("expires_in");
|
||||
s.remove("login_time");
|
||||
s.endGroup();
|
||||
|
||||
refresh_login_timer_.stop();
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::RedirectArrived() {
|
||||
|
||||
if (!server_) return;
|
||||
|
||||
if (server_->error().isEmpty()) {
|
||||
QUrl url = server_->request_url();
|
||||
if (url.isValid()) {
|
||||
QUrlQuery url_query(url);
|
||||
if (url_query.hasQueryItem("error")) {
|
||||
AuthError(QUrlQuery(url).queryItemValue("error"));
|
||||
}
|
||||
else if (url_query.hasQueryItem("code") && url_query.hasQueryItem("state")) {
|
||||
qLog(Debug) << "Spotify: Authorization URL Received" << url;
|
||||
QString code = url_query.queryItemValue("code");
|
||||
QString state = url_query.queryItemValue("state");
|
||||
QUrl redirect_url(kOAuthRedirectUrl);
|
||||
redirect_url.setPort(server_->url().port());
|
||||
RequestAccessToken(code, redirect_url);
|
||||
}
|
||||
else {
|
||||
AuthError(tr("Redirect missing token code or state!"));
|
||||
}
|
||||
}
|
||||
else {
|
||||
AuthError(tr("Received invalid reply from web browser."));
|
||||
}
|
||||
}
|
||||
else {
|
||||
AuthError(server_->error());
|
||||
}
|
||||
|
||||
server_->close();
|
||||
server_->deleteLater();
|
||||
server_ = nullptr;
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::RequestAccessToken(const QString code, const QUrl redirect_url) {
|
||||
|
||||
refresh_login_timer_.stop();
|
||||
|
||||
ParamList params = ParamList() << Param("client_id", QByteArray::fromBase64(kClientIDB64))
|
||||
<< Param("client_secret", QByteArray::fromBase64(kClientSecretB64));
|
||||
|
||||
if (!code.isEmpty() && !redirect_url.isEmpty()) {
|
||||
params << Param("grant_type", "authorization_code");
|
||||
params << Param("code", code);
|
||||
params << Param("redirect_uri", redirect_url.toString());
|
||||
}
|
||||
else if (!refresh_token_.isEmpty() && is_enabled()) {
|
||||
params << Param("grant_type", "refresh_token");
|
||||
params << Param("refresh_token", refresh_token_);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl new_url(kOAuthAccessTokenUrl);
|
||||
QNetworkRequest req = QNetworkRequest(new_url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
QString auth_header_data = QByteArray::fromBase64(kClientIDB64) + QString(":") + QByteArray::fromBase64(kClientSecretB64);
|
||||
req.setRawHeader("Authorization", "Basic " + auth_header_data.toUtf8().toBase64());
|
||||
|
||||
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
|
||||
|
||||
QNetworkReply *reply = network_->post(req, query);
|
||||
replies_ << reply;
|
||||
connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(HandleLoginSSLErrors(QList<QSslError>)));
|
||||
connect(reply, &QNetworkReply::finished, [=] { AccessTokenRequestFinished(reply); });
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::HandleLoginSSLErrors(QList<QSslError> ssl_errors) {
|
||||
|
||||
for (const QSslError &ssl_error : ssl_errors) {
|
||||
login_errors_ += ssl_error.errorString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::AccessTokenRequestFinished(QNetworkReply *reply) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
AuthError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "error" and "error_description" then use that instead.
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("error") && json_obj.contains("error_description")) {
|
||||
QString error = json_obj["error"].toString();
|
||||
QString error_description = json_obj["error_description"].toString();
|
||||
login_errors_ << QString("Authentication failure: %1 (%2)").arg(error).arg(error_description);
|
||||
}
|
||||
}
|
||||
if (login_errors_.isEmpty()) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
else {
|
||||
login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||
}
|
||||
}
|
||||
AuthError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
Error(QString("Failed to parse Json data in authentication reply: %1").arg(json_error.errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
AuthError("Authentication reply from server has empty Json document.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
AuthError("Authentication reply from server has Json document that is not an object.", json_doc);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
AuthError("Authentication reply from server has empty Json object.", json_doc);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("access_token") || !json_obj.contains("expires_in")) {
|
||||
AuthError("Authentication reply from server is missing access token or expires in.", json_obj);
|
||||
return;
|
||||
}
|
||||
|
||||
access_token_ = json_obj["access_token"].toString();
|
||||
if (json_obj.contains("refresh_token")) {
|
||||
refresh_token_ = json_obj["refresh_token"].toString();
|
||||
}
|
||||
expires_in_ = json_obj["expires_in"].toInt();
|
||||
login_time_ = QDateTime::currentDateTime().toTime_t();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("access_token", access_token_);
|
||||
s.setValue("refresh_token", refresh_token_);
|
||||
s.setValue("expires_in", expires_in_);
|
||||
s.setValue("login_time", login_time_);
|
||||
s.endGroup();
|
||||
|
||||
if (expires_in_ > 0) {
|
||||
refresh_login_timer_.setInterval(expires_in_ * kMsecPerSec);
|
||||
refresh_login_timer_.start();
|
||||
}
|
||||
|
||||
qLog(Debug) << "Spotify: Authentication was successful, got access token" << access_token_ << "expires in" << expires_in_;
|
||||
|
||||
emit AuthenticationComplete(true);
|
||||
emit AuthenticationSuccess();
|
||||
|
||||
}
|
||||
|
||||
bool SpotifyCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
if (access_token_.isEmpty()) return false;
|
||||
|
||||
QString type;
|
||||
QString query;
|
||||
QString extract;
|
||||
if (album.isEmpty()) {
|
||||
type = "track";
|
||||
query = artist + " " + title;
|
||||
extract = "tracks";
|
||||
}
|
||||
else {
|
||||
type = "album";
|
||||
query = artist + " " + album;
|
||||
extract = "albums";
|
||||
}
|
||||
|
||||
ParamList params = ParamList() << Param("q", query)
|
||||
<< Param("type", type)
|
||||
<< Param("limit", QString::number(kLimit));
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl url(kApiUrl + QString("/search"));
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
req.setRawHeader("Authorization", "Bearer " + access_token_.toUtf8());
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, extract); });
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); }
|
||||
|
||||
QByteArray SpotifyCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
QByteArray data;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
|
||||
data = reply->readAll();
|
||||
}
|
||||
else {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
else {
|
||||
data = reply->readAll();
|
||||
QJsonParseError parse_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
|
||||
QString error;
|
||||
if (parse_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("error") && json_obj["error"].isObject()) {
|
||||
QJsonObject obj_error = json_obj["error"].toObject();
|
||||
if (obj_error.contains("status") && obj_error.contains("message")) {
|
||||
int status = obj_error["status"].toInt();
|
||||
QString message = obj_error["message"].toString();
|
||||
error = QString("%1 (%2)").arg(message).arg(status);
|
||||
if (status == 401) access_token_.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (error.isEmpty()) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
if (reply->error() == 204) access_token_.clear();
|
||||
error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
else {
|
||||
error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||
}
|
||||
}
|
||||
Error(error);
|
||||
}
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id, const QString &extract) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (data.isEmpty()) {
|
||||
emit SearchFinished(id, CoverSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) {
|
||||
emit SearchFinished(id, CoverSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains(extract) || !json_obj[extract].isObject()) {
|
||||
Error(QString("Json object is missing %1 object.").arg(extract), json_obj);
|
||||
emit SearchFinished(id, CoverSearchResults());
|
||||
return;
|
||||
}
|
||||
json_obj = json_obj[extract].toObject();
|
||||
|
||||
if (!json_obj.contains("items") || !json_obj["items"].isArray()) {
|
||||
Error(QString("%1 object is missing items array.").arg(extract), json_obj);
|
||||
emit SearchFinished(id, CoverSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray array_items = json_obj["items"].toArray();
|
||||
if (array_items.isEmpty()) {
|
||||
emit SearchFinished(id, CoverSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
CoverSearchResults results;
|
||||
for (const QJsonValue &value_item : array_items) {
|
||||
|
||||
if (!value_item.isObject()) {
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_item = value_item.toObject();
|
||||
|
||||
QJsonObject obj_album = obj_item;
|
||||
if (obj_item.contains("album") && obj_item["album"].isObject()) {
|
||||
obj_album = obj_item["album"].toObject();
|
||||
}
|
||||
|
||||
if (!obj_album.contains("artists") || !obj_album.contains("name") || !obj_album.contains("images") || !obj_album["artists"].isArray() || !obj_album["images"].isArray()) {
|
||||
continue;
|
||||
}
|
||||
QJsonArray array_artists = obj_album["artists"].toArray();
|
||||
QJsonArray array_images = obj_album["images"].toArray();
|
||||
QString album = obj_album["name"].toString();
|
||||
|
||||
QStringList artists;
|
||||
for (const QJsonValue &value_artist : array_artists) {
|
||||
if (!value_artist.isObject()) continue;
|
||||
QJsonObject obj_artist = value_artist.toObject();
|
||||
if (!obj_artist.contains("name")) continue;
|
||||
artists << obj_artist["name"].toString();
|
||||
}
|
||||
|
||||
for (const QJsonValue &value_image : array_images) {
|
||||
if (!value_image.isObject()) continue;
|
||||
QJsonObject obj_image = value_image.toObject();
|
||||
if (!obj_image.contains("url") || !obj_image.contains("width") || !obj_image.contains("height")) continue;
|
||||
int width = obj_image["width"].toInt();
|
||||
int height = obj_image["height"].toInt();
|
||||
if (width < 300 || height < 300) continue;
|
||||
QUrl url(obj_image["url"].toString());
|
||||
CoverSearchResult result;
|
||||
result.album = album;
|
||||
result.image_url = url;
|
||||
if (!artists.isEmpty()) result.artist = artists.first();
|
||||
results << result;
|
||||
}
|
||||
|
||||
}
|
||||
emit SearchFinished(id, results);
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::AuthError(const QString &error, const QVariant &debug) {
|
||||
|
||||
if (!error.isEmpty()) login_errors_ << error;
|
||||
|
||||
for (const QString &e : login_errors_) Error(e);
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
emit AuthenticationFailure(login_errors_);
|
||||
emit AuthenticationComplete(false, login_errors_);
|
||||
|
||||
login_errors_.clear();
|
||||
|
||||
}
|
||||
|
||||
void SpotifyCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "Spotify:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
97
src/covermanager/spotifycoverprovider.h
Normal file
97
src/covermanager/spotifycoverprovider.h
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef SPOTIFYCOVERPROVIDER_H
|
||||
#define SPOTIFYCOVERPROVIDER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QSslError>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
class LocalRedirectServer;
|
||||
|
||||
class SpotifyCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SpotifyCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~SpotifyCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
void Authenticate();
|
||||
void Deauthenticate();
|
||||
bool IsAuthenticated() const { return !access_token_.isEmpty(); }
|
||||
|
||||
private slots:
|
||||
void HandleLoginSSLErrors(QList<QSslError> ssl_errors);
|
||||
void RedirectArrived();
|
||||
void RequestAccessToken(const QString code = QString(), const QUrl redirect_url = QUrl());
|
||||
void AccessTokenRequestFinished(QNetworkReply *reply);
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id, const QString &extract);
|
||||
|
||||
private:
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
void AuthError(const QString &error = QString(), const QVariant &debug = QVariant());
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private:
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
static const char *kClientIDB64;
|
||||
static const char *kClientSecretB64;
|
||||
static const char *kOAuthAuthorizeUrl;
|
||||
static const char *kOAuthAccessTokenUrl;
|
||||
static const char *kOAuthRedirectUrl;
|
||||
static const char *kApiUrl;
|
||||
static const int kLimit;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
LocalRedirectServer *server_;
|
||||
QStringList login_errors_;
|
||||
QString code_verifier_;
|
||||
QString code_challenge_;
|
||||
QString access_token_;
|
||||
QString refresh_token_;
|
||||
quint64 expires_in_;
|
||||
quint64 login_time_;
|
||||
QTimer refresh_login_timer_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // SPOTIFYCOVERPROVIDER_H
|
||||
@@ -45,7 +45,7 @@
|
||||
#include "internet/internetservices.h"
|
||||
#include "tidal/tidalservice.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "tidalcoverprovider.h"
|
||||
|
||||
const char *TidalCoverProvider::kApiUrl = "https://api.tidalhifi.com/v1";
|
||||
@@ -53,12 +53,23 @@ const char *TidalCoverProvider::kResourcesUrl = "https://resources.tidal.com";
|
||||
const int TidalCoverProvider::kLimit = 10;
|
||||
|
||||
TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) :
|
||||
CoverProvider("Tidal", 2.5, true, true, app, parent),
|
||||
JsonCoverProvider("Tidal", true, true, 2.5, true, true, app, parent),
|
||||
service_(app->internet_services()->Service<TidalService>()),
|
||||
network_(new NetworkAccessManager(this)) {
|
||||
|
||||
}
|
||||
|
||||
TidalCoverProvider::~TidalCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
@@ -95,6 +106,7 @@ bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album
|
||||
if (!service_->session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", service_->session_id().toUtf8());
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); });
|
||||
|
||||
return true;
|
||||
@@ -152,38 +164,11 @@ QByteArray TidalCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
}
|
||||
|
||||
QJsonObject TidalCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
Error("Reply from server missing Json data.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error("Json document is not an object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error("Received empty Json object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverSearchResults results;
|
||||
|
||||
@@ -32,27 +32,31 @@
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "coverprovider.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "tidal/tidalservice.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
class TidalService;
|
||||
|
||||
class TidalCoverProvider : public CoverProvider {
|
||||
class TidalCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TidalCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
~TidalCoverProvider();
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
bool IsAuthenticated() const { return service_ && service_->authenticated(); }
|
||||
void Deauthenticate() { if (service_) service_->Logout(); }
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id);
|
||||
|
||||
private:
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private:
|
||||
@@ -62,6 +66,7 @@ class TidalCoverProvider : public CoverProvider {
|
||||
|
||||
TidalService *service_;
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -77,9 +77,7 @@ void CddaSongLoader::LoadSongs() {
|
||||
if (error) {
|
||||
Error(QString("%1: %2").arg(error->code).arg(error->message));
|
||||
}
|
||||
if (cdda_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!cdda_) return;
|
||||
|
||||
if (!url_.isEmpty()) {
|
||||
g_object_set(cdda_, "device", g_strdup(url_.path().toLocal8Bit().constData()), nullptr);
|
||||
@@ -242,7 +240,7 @@ void CddaSongLoader::AudioCDTagsLoaded(const QString &artist, const QString &alb
|
||||
|
||||
bool CddaSongLoader::HasChanged() {
|
||||
|
||||
if ((cdio_ && cdda_) && cdio_get_media_changed(cdio_) != 1) {
|
||||
if (cdio_ && cdio_get_media_changed(cdio_) != 1) {
|
||||
return false;
|
||||
}
|
||||
// Check if mutex is already token (i.e. init is already taking place)
|
||||
@@ -250,6 +248,7 @@ bool CddaSongLoader::HasChanged() {
|
||||
return false;
|
||||
}
|
||||
mutex_load_.unlock();
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ ConnectedDevice::ConnectedDevice(const QUrl &url, DeviceLister *lister, const QS
|
||||
backend_->moveToThread(app_->database()->thread());
|
||||
qLog(Debug) << backend_ << "for device" << unique_id_ << "moved to thread" << app_->database()->thread();
|
||||
|
||||
connect(backend_, SIGNAL(TotalSongCountUpdated(int)), SLOT(BackendTotalSongCountUpdated(int)));
|
||||
if (url_.scheme() != "cdda") {
|
||||
connect(backend_, SIGNAL(TotalSongCountUpdated(int)), SLOT(BackendTotalSongCountUpdated(int)));
|
||||
}
|
||||
|
||||
backend_->Init(app_->database(),
|
||||
Song::Source_Device,
|
||||
@@ -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);
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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://");
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
class iMobileDeviceConnection {
|
||||
public:
|
||||
explicit iMobileDeviceConnection(const QString &uuid);
|
||||
explicit iMobileDeviceConnection(const QString uuid);
|
||||
~iMobileDeviceConnection();
|
||||
|
||||
afc_client_t afc() { return afc_; }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>";
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "ui_about.h"
|
||||
|
||||
class QWidget;
|
||||
class QShowEvent;
|
||||
|
||||
class About : public QDialog {
|
||||
Q_OBJECT
|
||||
@@ -39,6 +40,10 @@ class About : public QDialog {
|
||||
public:
|
||||
explicit About(QWidget *parent = nullptr);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent*);
|
||||
|
||||
private:
|
||||
struct Person {
|
||||
Person(const QString &n, const QString &e = QString()) : name(n), email(e) {}
|
||||
bool operator<(const Person &other) const { return name < other.name; }
|
||||
@@ -46,7 +51,6 @@ class About : public QDialog {
|
||||
QString email;
|
||||
};
|
||||
|
||||
private:
|
||||
QString MainHtml() const;
|
||||
QString ContributorsHtml() const;
|
||||
QString PersonToHtml(const Person& person) const;
|
||||
|
||||
@@ -7,13 +7,19 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>500</width>
|
||||
<height>600</height>
|
||||
<height>500</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>500</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>600</width>
|
||||
<height>700</height>
|
||||
<width>500</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
@@ -118,13 +124,29 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="spacer_middle">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="text_contributors">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ void InternetSongsView::SongsFinished(const SongList &songs, const QString &erro
|
||||
service_->songs_collection_backend()->DeleteAll();
|
||||
ui_->stacked->setCurrentWidget(ui_->internetcollection_page);
|
||||
ui_->status->clear();
|
||||
service_->songs_collection_backend()->AddOrUpdateSongs(songs);
|
||||
service_->songs_collection_backend()->AddOrUpdateSongsAsync(songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ void InternetTabsView::ArtistsFinished(const SongList &songs, const QString &err
|
||||
service_->artists_collection_backend()->DeleteAll();
|
||||
ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->internetcollection_page());
|
||||
ui_->artists_collection->status()->clear();
|
||||
service_->artists_collection_backend()->AddOrUpdateSongs(songs);
|
||||
service_->artists_collection_backend()->AddOrUpdateSongsAsync(songs);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -274,7 +274,7 @@ void InternetTabsView::AlbumsFinished(const SongList &songs, const QString &erro
|
||||
service_->albums_collection_backend()->DeleteAll();
|
||||
ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->internetcollection_page());
|
||||
ui_->albums_collection->status()->clear();
|
||||
service_->albums_collection_backend()->AddOrUpdateSongs(songs);
|
||||
service_->albums_collection_backend()->AddOrUpdateSongsAsync(songs);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -317,7 +317,7 @@ void InternetTabsView::SongsFinished(const SongList &songs, const QString &error
|
||||
service_->songs_collection_backend()->DeleteAll();
|
||||
ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->internetcollection_page());
|
||||
ui_->songs_collection->status()->clear();
|
||||
service_->songs_collection_backend()->AddOrUpdateSongs(songs);
|
||||
service_->songs_collection_backend()->AddOrUpdateSongsAsync(songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user