Compare commits

...

109 Commits

Author SHA1 Message Date
Clementine Buildbot 650bd81508 Automatic merge of translations from Transifex 2024-05-17 02:32:22 +00:00
Clementine Buildbot 7607ddcb96 Automatic merge of translations from Transifex 2024-05-12 02:33:06 +00:00
Clementine Buildbot e249911937 Automatic merge of translations from Transifex 2024-05-05 02:32:03 +00:00
Clementine Buildbot 4ae57a4b5d Automatic merge of translations from Transifex 2024-05-03 02:32:14 +00:00
Clementine Buildbot 2f3464403b Automatic merge of translations from Transifex 2024-04-25 02:31:15 +00:00
Clementine Buildbot f76dbffa6b Automatic merge of translations from Transifex 2024-03-24 02:30:19 +00:00
Clementine Buildbot fbb266adc2 Automatic merge of translations from Transifex 2024-03-15 02:29:14 +00:00
Clementine Buildbot 9638ac70b3 Automatic merge of translations from Transifex 2024-03-13 02:29:55 +00:00
Clementine Buildbot c93b4e1149 Automatic merge of translations from Transifex 2024-02-27 02:27:58 +00:00
Clementine Buildbot d014a315c9 Automatic merge of translations from Transifex 2024-02-23 02:28:36 +00:00
Isaiah W df4181940d oops (:
this is what I meant lol
2024-02-08 14:04:03 +00:00
Isaiah W ebe3c45476 Fix Instructions™
uses all of your cores if you have more than 8, or doesn't try to use more than you have if you have less (:
2024-02-08 14:04:03 +00:00
Clementine Buildbot 634910238d Automatic merge of translations from Transifex 2024-01-03 02:31:14 +00:00
Clementine Buildbot 62ed69fa3d Automatic merge of translations from Transifex 2023-12-19 02:32:39 +00:00
Clementine Buildbot dd0a94e8a6 Automatic merge of translations from Transifex 2023-12-09 02:30:35 +00:00
Clementine Buildbot 1566148c50 Automatic merge of translations from Transifex 2023-11-29 02:32:39 +00:00
Clementine Buildbot 98a520552b Automatic merge of translations from Transifex 2023-11-26 02:32:33 +00:00
Clementine Buildbot 5968648aa1 Automatic merge of translations from Transifex 2023-11-01 02:31:10 +00:00
Clementine Buildbot f3ddd7eee4 Automatic merge of translations from Transifex 2023-10-22 02:30:09 +00:00
Clementine Buildbot 19b44fb831 Automatic merge of translations from Transifex 2023-10-16 02:30:12 +00:00
Robert-André Mauchin 994d16effa Fix missing QTSINGLECOREAPPLICATION_LIBRARIES
In f3837f95db, QTSINGLECOREAPPLICATION_LIBRARIES was mistakenly removed, which prevents building with USE_SYSTEM_QTSINGLEAPPLICATION enabled.
2023-10-15 13:40:14 +01:00
xoza 4768cb9efb Skip subsonic multi-genre tags 2023-10-12 12:27:43 +01:00
Clementine Buildbot 7b678f26e0 Automatic merge of translations from Transifex 2023-10-02 02:29:29 +00:00
Marcus Müller 3f572a4139 RPM & CI: Build rpm packages against native qtsingleapplication
Signed-off-by: Marcus Müller <marcus_clementine@baseband.digital>
2023-09-20 17:52:55 +01:00
Marcus Müller f3837f95db CMake: Re-enable usability of system QtSingleApplication
This seem to have gone broken over time.
As far as I can tell, upstream QtSingleApplication works fine!

Signed-off-by: Marcus Müller <marcus_clementine@baseband.digital>
2023-09-20 17:52:55 +01:00
Marcus Müller 6820a0a58d 3rdparty: remove unused libmygpo-qt
The -qt5 library is still there, and seems to be used.

Signed-off-by: Marcus Müller <marcus_clementine@baseband.digital>
2023-09-17 16:41:15 +01:00
Marcus Müller cfcddf7c0f src: remove unused variable
Signed-off-by: Marcus Müller <marcus_clementine@baseband.digital>
2023-09-17 12:43:13 +01:00
Marcus Müller 98e24f626b library: use boolean, not bitwise, operator on bools
Signed-off-by: Marcus Müller <marcus_clementine@baseband.digital>
2023-09-17 12:43:13 +01:00
Marcus Müller 8e47ab59e5 internet services: consistently use 'override'
Signed-off-by: Marcus Müller <marcus_clementine@baseband.digital>
2023-09-17 12:43:13 +01:00
Marcus Müller 63208b4e1f core/organisefmt: use same visibility for fwd decl as in def
Signed-off-by: Marcus Müller <marcus_clementine@baseband.digital>
2023-09-17 12:43:13 +01:00
Marcus Müller 20773dee29 CMake: Check for minimum version before setting the project name
Signed-off-by: Marcus Müller <marcus_clementine@baseband.digital>
2023-09-17 12:43:13 +01:00
John Maguire c2a5b9b07e Remove Kinetic build 2023-09-14 14:51:42 +01:00
Quentin Snow de7455eebd Adjusted MainWindow::TrackSkipped to only count song skips if listened to for 5 seconds. 2023-09-13 11:53:37 +01:00
Marcus Müller 2a14ec9d4d Lyrics Providers: Remove unreachable ones
This was determined programmatically by means of trying to do a simple
HTTP request to / of any of the URLs given:

```python
from lxml import etree
from urllib import parse
import requests
doc = etree.parse("ultimate_providers.xml")
root = doc.getroot()
for provider in root:
    parsed_url = parse.urlparse(provider.get("url"))
    url = f"{parsed_url.scheme}://{parsed_url.netloc}/"
    try:
        requests.head(url, timeout=5)
    except Exception as e:
        print(parsed_url.netloc)
```

Note that these were also removed from songinfoview as present, and from
outgoingdatacreator. The two lists there were found to be inconsistent,
but this isn't subject of this PR.

Signed-off-by: Marcus Müller <marcus_clementine@baseband.digital>
2023-09-12 13:33:01 +01:00
Clementine Buildbot 86e81cea05 Automatic merge of translations from Transifex 2023-08-26 02:27:18 +00:00
Clementine Buildbot 10570316dd Automatic merge of translations from Transifex 2023-07-22 02:34:48 +00:00
Clementine Buildbot ad8fd81ba9 Automatic merge of translations from Transifex 2023-07-18 02:50:36 +00:00
Clementine Buildbot 6ff5768634 Automatic merge of translations from Transifex 2023-07-15 02:51:38 +00:00
Clementine Buildbot 08bfb88912 Automatic merge of translations from Transifex 2023-07-14 02:50:44 +00:00
Clementine Buildbot d3108b32e8 Automatic merge of translations from Transifex 2023-07-13 02:50:56 +00:00
Clementine Buildbot 0701bef103 Automatic merge of translations from Transifex 2023-07-12 02:49:20 +00:00
Clementine Buildbot bf4ac0cb46 Automatic merge of translations from Transifex 2023-07-11 02:44:50 +00:00
John Maguire baf05335f9 Only push translations from master 2023-07-11 01:22:17 +01:00
John Maguire d21e9697d0 Disable mac builds temporarily 2023-07-10 23:42:29 +01:00
John Maguire ab057f8275 Migrate `tx pull` to new CLI 2023-07-10 20:52:39 +01:00
John Maguire 58325e45a7 Migrate `tx push` to new CLI 2023-07-10 20:52:29 +01:00
John Maguire 1d0cbc0ebb Remove support for Debian Stretch 2023-07-10 19:25:22 +01:00
Clementine Buildbot c83a0ac25f Automatic merge of translations from Transifex 2023-07-10 17:53:14 +00:00
John Maguire 351a5e2547 Build for Fedora 38 2023-04-07 11:59:53 +01:00
John Maguire 8773e8fe0a Bump runner for translations 2023-04-07 11:59:45 +01:00
Clementine Buildbot 3471134d52 Automatic merge of translations from Transifex 2023-02-10 03:06:03 +00:00
Clementine Buildbot 26192c3469 Automatic merge of translations from Transifex 2023-02-04 02:38:09 +00:00
Clementine Buildbot 982d8fbb63 Automatic merge of translations from Transifex 2023-01-16 02:39:34 +00:00
Clementine Buildbot 20cf7f793b Automatic merge of translations from Transifex 2023-01-15 02:39:29 +00:00
Clementine Buildbot ccf4f75c3d Automatic merge of translations from Transifex 2022-12-31 02:36:31 +00:00
Alexey Sokolov 65319d4952 Fix build: add zlib to deps where it's used
Ref https://bugs.gentoo.org/887105
2022-12-25 20:55:23 +00:00
John Maguire 9ef681b0e9 Build for Fedora 37 2022-12-09 18:32:11 +00:00
Clementine Buildbot dfeb1182f9 Automatic merge of translations from Transifex 2022-11-23 02:52:42 +00:00
Clementine Buildbot 384a8850d9 Automatic merge of translations from Transifex 2022-11-20 03:03:02 +00:00
adem 0fab612784 Replace play and pause with the single play/pause action in desktop file 2022-10-27 14:34:46 +01:00
Clementine Buildbot 336770bb95 Automatic merge of translations from Transifex 2022-10-27 03:08:37 +00:00
Andrei Stepanov 101f450aaa Update Russian translation in desktop file 2022-10-26 10:58:39 +01:00
Clementine Buildbot 6a440fe397 Automatic merge of translations from Transifex 2022-10-21 03:07:56 +00:00
John Maguire 3a506e0917 Add builder for Ubuntu 22.10 2022-10-20 21:56:08 +01:00
Clementine Buildbot c716ddb722 Automatic merge of translations from Transifex 2022-10-12 03:21:27 +00:00
Clementine Buildbot 519b33ed81 Automatic merge of translations from Transifex 2022-10-08 03:06:42 +00:00
Clementine Buildbot f2011e7e26 Automatic merge of translations from Transifex 2022-09-30 03:31:30 +00:00
Clementine Buildbot 42853b7b52 Automatic merge of translations from Transifex 2022-09-29 03:23:15 +00:00
Clementine Buildbot 1c69e343b9 Automatic merge of translations from Transifex 2022-09-26 03:25:40 +00:00
Clementine Buildbot 770080b80b Automatic merge of translations from Transifex 2022-09-24 03:24:44 +00:00
Clementine Buildbot 3e9e251e90 Automatic merge of translations from Transifex 2022-09-23 03:23:03 +00:00
Jason Freidman 72c2336d94 Fix ClassicalRadio.com and ROCKRADIO.com #5616
These two audio addict feeds do support premium -- Perhaps they didn't previously. This changes fixes the playlist not loading for me on a local build.

This was changed in https://github.com/clementine-player/Clementine/issues/4582
2022-09-22 12:45:13 +01:00
Clementine Buildbot 495803ab99 Automatic merge of translations from Transifex 2022-09-20 03:21:35 +00:00
Clementine Buildbot 7da98fbbcc Automatic merge of translations from Transifex 2022-09-13 03:23:41 +00:00
Clementine Buildbot 2055fd51fa Automatic merge of translations from Transifex 2022-09-12 03:25:17 +00:00
Clementine Buildbot 39124eda38 Automatic merge of translations from Transifex 2022-09-10 03:20:31 +00:00
Clementine Buildbot b567760ae1 Automatic merge of translations from Transifex 2022-09-08 03:21:44 +00:00
Clementine Buildbot 2d3a604b85 Automatic merge of translations from Transifex 2022-09-07 03:23:14 +00:00
Clementine Buildbot ce4a22bd5f Automatic merge of translations from Transifex 2022-09-06 03:23:45 +00:00
John Maguire e6a7539480 Fix translations pull workflow 2022-09-05 19:37:47 +01:00
John Maguire a551c40c4e Remove Spotify playback support
libspotify is dead
2022-09-01 22:55:44 +01:00
John Maguire 99029ed643 Bump github actions runner 2022-09-01 20:49:32 +01:00
John Maguire cf8047b4ce Fix translations push job 2022-09-01 19:55:53 +01:00
Andrew Reading f59c9f4b2b Rewrite the Block Analyzer to improve performance.
The block analyzer was doing lots of repeated, out-of-order blits to the
widget's canvas. To improve performance and reduce CPU usage, this has
been rewritten to generate the canvas contents using only a single buffer.
Cache thrashing has been greatly reduced by writing to memory only
sequentially and in one single write pass. Further, the raw format is
now guaranteed to be in a format efficient for Qt.

The results are visually identical to what they were previously, but
result in a CPU usage reduction between 2 and 6 percent depending on refresh
rate and Psychadelic Mode value. In particular, there used to be a ~3 percent
overhead for Psychadelic Mode, and this has been eliminated.

The specific details of the block analyzer and explanations for how it works
(and used to work) have been documented via fairly extensive comments
in blockanalyzer.cpp.
2022-09-01 19:36:03 +01:00
John Maguire 71eac9bb3b Remove support for FC34 2022-09-01 19:35:27 +01:00
John Maguire 3fd467591a Force GIT_REV in Fedora RPM builds 2022-09-01 19:00:51 +01:00
John Maguire a0ae9210dd Try using %{version} for RPM builds 2022-09-01 19:00:51 +01:00
John Maguire c1fa38120d Add git hackery for all builds 2022-09-01 19:00:51 +01:00
John Maguire 13352c5802 Git hackery to make git describe work 2022-09-01 19:00:51 +01:00
John Maguire 5e5b888d41 Make non-zero from `git describe` fatal 2022-09-01 19:00:51 +01:00
John Maguire 662ac60eb1 Add debug messages for git rev versioning 2022-09-01 19:00:51 +01:00
John Maguire 9be5b9805d Fix typo 2022-09-01 19:00:51 +01:00
John Maguire 9de903d42d Remove useless config 2022-09-01 19:00:51 +01:00
John Maguire 454678256e Try `dh_installgsettings` 2022-09-01 19:00:51 +01:00
John Maguire d3c847b38c Build for Ubuntu 22.04 Jammy Jellyfish
Deprecate support for Ubuntu 21.04 Hirsute Hippo
2022-09-01 19:00:51 +01:00
John Maguire 398893117e Remove obsolete Ubuntu builds 2022-09-01 19:00:51 +01:00
Lorenz Bausch bbda59a5f3 Build RPMs for Fedora 36 2022-05-18 14:23:11 +01:00
John Maguire bebd0b5d3c Remove mms plugin from mac build
Removed from upstream gstreamer
2022-05-18 14:21:58 +01:00
Clementine Buildbot 250024e117 Automatic merge of translations from Transifex 2022-04-11 02:56:58 +00:00
Clementine Buildbot 9168299c0f Automatic merge of translations from Transifex 2022-04-10 02:49:22 +00:00
Clementine Buildbot 24d4b6e7f2 Automatic merge of translations from Transifex 2022-04-09 02:44:35 +00:00
Clementine Buildbot 644405ec7a Automatic merge of translations from Transifex 2022-04-06 02:49:01 +00:00
Clementine Buildbot 2fb964fc29 Automatic merge of translations from Transifex 2022-04-05 02:47:59 +00:00
Clementine Buildbot cf31624836 Automatic merge of translations from Transifex 2022-03-30 03:10:46 +00:00
Clementine Buildbot d05616e37c Automatic merge of translations from Transifex 2022-03-29 02:51:19 +00:00
Clementine Buildbot 0b5faa7550 Automatic merge of translations from Transifex 2022-03-28 02:51:09 +00:00
Clementine Buildbot c0b42ace6d Automatic merge of translations from Transifex 2022-03-27 02:40:50 +00:00
Clementine Buildbot 810f0b0acb Automatic merge of translations from Transifex 2022-03-26 02:41:48 +00:00
Clementine Buildbot c2b8a35642 Automatic merge of translations from Transifex 2022-03-25 02:40:45 +00:00
137 changed files with 33382 additions and 235749 deletions

View File

@ -10,7 +10,7 @@ on:
jobs:
lint:
name: Lint
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
@ -25,10 +25,15 @@ jobs:
push_translations:
name: Push translation sources to Transifex
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
container:
image: ubuntu:bionic
steps:
- uses: supplypike/setup-bin@v3
with:
uri: https://github.com/transifex/cli/releases/download/v1.6.7/tx-linux-amd64.tar.gz
name: tx
version: 1.6.7
- name: Install dependencies
env:
DEBIAN_FRONTEND: noninteractive
@ -64,14 +69,14 @@ jobs:
qttools5-dev
libsparsehash-dev
ssh
- name: Install tx
run: pip3 install transifex-client==0.13.9
- name: Checkout
uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: tx init
env:
TX_TOKEN: ${{ secrets.TX_TOKEN }}
run: tx init --no-interactive --force
run: tx init
- name: cmake
working-directory: bin
run: cmake ..
@ -81,7 +86,7 @@ jobs:
- name: tx config
env:
TX_TOKEN: ${{ secrets.TX_TOKEN }}
run: tx config mapping --execute -r clementine.clementineplayer -f src/translations/translations.pot -s en -t PO --expression 'src/translations/<lang>.po'
run: tx add --organization davidsansome --project clementine --resource clementineplayer --file-filter 'src/translations/<lang>.po' --type PO src/translations/en.po
- name: tx push
env:
TX_TOKEN: ${{ secrets.TX_TOKEN }}
@ -89,21 +94,20 @@ jobs:
create_release:
name: Create GitHub Release
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
if: github.ref == 'refs/heads/master'
needs:
- build_bionic_64
- build_bullseye_64
- build_buster_64
- build_fedora_34
- build_fedora_35
- build_fedora_36
- build_fedora_37
- build_fedora_38
- build_focal_64
- build_hirsute_64
- build_impish_64
- build_mac
- build_jammy_64
# - build_mac
- build_mingw
- build_source
- build_stretch_64
steps:
- uses: actions/checkout@v1.2.0
- uses: actions/download-artifact@v2
@ -125,7 +129,7 @@ jobs:
build_source:
name: Build source tarball
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
container:
image: ubuntu:focal
steps:
@ -170,6 +174,8 @@ jobs:
qttools5-dev
ssh
- uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: cmake
working-directory: bin
run: cmake ..
@ -181,11 +187,11 @@ jobs:
name: release_source
path: bin/clementine-*.tar.xz
build_fedora_35:
name: Build Fedora 35 RPM
runs-on: ubuntu-18.04
build_fedora_36:
name: Build Fedora 36 RPM
runs-on: ubuntu-22.04
container:
image: fedora:35
image: fedora:36
env:
RPM_BUILD_NCPUS: "2"
steps:
@ -225,6 +231,8 @@ jobs:
qt5-qtbase-devel
qt5-qtx11extras-devel
qt5-rpm-macros
qtsingleapplication-qt5-devel
qtsinglecoreapplication-qt5-devel
rpmdevtools
sha2-devel
sparsehash-devel
@ -232,9 +240,11 @@ jobs:
taglib-devel
tar
- uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: cmake
working-directory: bin
run: cmake ..
run: cmake -DUSE_SYSTEM_QTSINGLEAPPLICATION=On ..
- name: Build source tarball
working-directory: bin
run: ../dist/maketarball.sh
@ -248,14 +258,14 @@ jobs:
run: rpmbuild -ba ../dist/clementine.spec
- uses: actions/upload-artifact@v2
with:
name: release_fedora_35
name: release_fedora_36
path: ~/rpmbuild/RPMS/*/clementine-*.rpm
build_fedora_34:
name: Build Fedora 34 RPM
runs-on: ubuntu-18.04
build_fedora_37:
name: Build Fedora 37 RPM
runs-on: ubuntu-22.04
container:
image: fedora:34
image: fedora:37
env:
RPM_BUILD_NCPUS: "2"
steps:
@ -295,6 +305,8 @@ jobs:
qt5-qtbase-devel
qt5-qtx11extras-devel
qt5-rpm-macros
qtsingleapplication-qt5-devel
qtsinglecoreapplication-qt5-devel
rpmdevtools
sha2-devel
sparsehash-devel
@ -302,9 +314,11 @@ jobs:
taglib-devel
tar
- uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: cmake
working-directory: bin
run: cmake ..
run: cmake -DUSE_SYSTEM_QTSINGLEAPPLICATION=On ..
- name: Build source tarball
working-directory: bin
run: ../dist/maketarball.sh
@ -318,12 +332,87 @@ jobs:
run: rpmbuild -ba ../dist/clementine.spec
- uses: actions/upload-artifact@v2
with:
name: release_fedora_34
name: release_fedora_37
path: ~/rpmbuild/RPMS/*/clementine-*.rpm
build_fedora_38:
name: Build Fedora 38 RPM
runs-on: ubuntu-22.04
container:
image: fedora:38
env:
RPM_BUILD_NCPUS: "2"
steps:
- name: Install dependencies
run: >
dnf install --assumeyes
@development-tools
alsa-lib-devel
boost-devel
cmake
cryptopp-devel
dbus-devel
desktop-file-utils
fftw-devel
gcc-c++
gettext
git
glew-devel
gstreamer1-devel
gstreamer1-plugins-base-devel
hicolor-icon-theme
libappstream-glib
libcdio-devel
libchromaprint-devel
libgpod-devel
liblastfm-qt5-devel
libmtp-devel
libnotify-devel
openssh
pkgconfig
protobuf-compiler
protobuf-devel
pulseaudio-libs-devel
qca-qt5-devel
qca-qt5-ossl
qt5-linguist
qt5-qtbase-devel
qt5-qtx11extras-devel
qt5-rpm-macros
qtsingleapplication-qt5-devel
qtsinglecoreapplication-qt5-devel
rpmdevtools
sha2-devel
sparsehash-devel
sqlite-devel
taglib-devel
tar
- uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: cmake
working-directory: bin
run: cmake -DUSE_SYSTEM_QTSINGLEAPPLICATION=On ..
- name: Build source tarball
working-directory: bin
run: ../dist/maketarball.sh
- name: Create rpmbuild directory
run: mkdir -p ~/rpmbuild/SOURCES
- name: Move source tarball
working-directory: bin
run: mv clementine-*.tar.xz ~/rpmbuild/SOURCES
- name: Build RPM
working-directory: bin
run: rpmbuild -ba ../dist/clementine.spec
- uses: actions/upload-artifact@v2
with:
name: release_fedora_38
path: ~/rpmbuild/RPMS/*/clementine-*.rpm
build_mingw:
name: Build Windows Installer
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
container:
image: eu.gcr.io/clementine-data/mingw-w64:latest
env:
@ -332,6 +421,8 @@ jobs:
- name: Fix liblastfm includes
run: ln -s /target/include/lastfm /target/include/lastfm5
- uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: cmake
working-directory: bin
run: >
@ -512,67 +603,9 @@ jobs:
name: release_mingw
path: dist/windows/ClementineSetup*.exe
build_stretch_64:
name: Build Debian Stretch 64-bit deb
runs-on: ubuntu-18.04
container:
image: debian:stretch
steps:
- name: Install dependencies
run: >
apt-get update && apt-get install -y
build-essential
cmake
gettext
git
libasound2-dev
libboost-dev
libcdio-dev
libchromaprint-dev
libcrypto++-dev
libdbus-1-dev
libfftw3-dev
libglew1.5-dev
libglib2.0-dev
libgpod-dev
libgstreamer-plugins-base1.0-dev
libgstreamer1.0-dev
liblastfm5-dev
libmtp-dev
libmygpo-qt-dev
libprotobuf-dev
libpulse-dev
libqt5x11extras5-dev
libsparsehash-dev
libsqlite3-dev
libtag1-dev
pkg-config
protobuf-compiler
qtbase5-dev
qttools5-dev-tools
qttools5-dev
ssh
- uses: actions/checkout@v1.2.0
- name: cmake
working-directory: bin
run: >
cmake ..
-DWITH_DEBIAN=ON
-DDEB_ARCH=amd64
-DDEB_DIST=stretch
-DFORCE_GIT_VERSION=
-DENABLE_SPOTIFY_BLOB=OFF
- name: make
working-directory: bin
run : make -j2 deb
- uses: actions/upload-artifact@v2
with:
name: release_stretch_64
path: bin/clementine_*.deb
build_bionic_64:
name: Build Ubuntu Bionic 64-bit deb
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
container:
image: ubuntu:bionic
steps:
@ -611,6 +644,8 @@ jobs:
libsparsehash-dev
ssh
- uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: cmake
working-directory: bin
run: >
@ -618,7 +653,6 @@ jobs:
-DWITH_DEBIAN=ON
-DDEB_ARCH=amd64
-DDEB_DIST=bionic
-DFORCE_GIT_VERSION=
-DENABLE_SPOTIFY_BLOB=OFF
- name: make
working-directory: bin
@ -630,7 +664,7 @@ jobs:
build_bullseye_64:
name: Build Debian Bullseye 64-bit deb
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
container:
image: debian:bullseye
steps:
@ -670,6 +704,8 @@ jobs:
qttools5-dev
ssh
- uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: cmake
working-directory: bin
run: >
@ -677,7 +713,6 @@ jobs:
-DWITH_DEBIAN=ON
-DDEB_ARCH=amd64
-DDEB_DIST=bullseye
-DFORCE_GIT_VERSION=
-DENABLE_SPOTIFY_BLOB=OFF
- name: make
working-directory: bin
@ -689,7 +724,7 @@ jobs:
build_buster_64:
name: Build Debian Buster 64-bit deb
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
container:
image: debian:buster
steps:
@ -728,6 +763,8 @@ jobs:
qttools5-dev
ssh
- uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: cmake
working-directory: bin
run: >
@ -735,7 +772,6 @@ jobs:
-DWITH_DEBIAN=ON
-DDEB_ARCH=amd64
-DDEB_DIST=buster
-DFORCE_GIT_VERSION=
-DENABLE_SPOTIFY_BLOB=OFF
- name: make
working-directory: bin
@ -747,7 +783,7 @@ jobs:
build_focal_64:
name: Build Ubuntu Focal 64-bit deb
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
container:
image: ubuntu:focal
steps:
@ -792,6 +828,8 @@ jobs:
qttools5-dev
ssh
- uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: cmake
working-directory: bin
run: >
@ -799,7 +837,6 @@ jobs:
-DWITH_DEBIAN=ON
-DDEB_ARCH=amd64
-DDEB_DIST=focal
-DFORCE_GIT_VERSION=
-DENABLE_SPOTIFY_BLOB=OFF
- name: make
working-directory: bin
@ -809,11 +846,11 @@ jobs:
name: release_focal_64
path: bin/clementine_*.deb
build_hirsute_64:
name: Build Ubuntu Hirsute 64-bit deb
runs-on: ubuntu-18.04
build_jammy_64:
name: Build Ubuntu Jammy 64-bit deb
runs-on: ubuntu-22.04
container:
image: ubuntu:hirsute
image: ubuntu:jammy
steps:
- name: Install dependencies
env:
@ -836,7 +873,7 @@ jobs:
libcrypto++-dev
libdbus-1-dev
libfftw3-dev
libglew1.5-dev
libglew-dev
libgpod-dev
libgstreamer-plugins-base1.0-dev
libgstreamer1.0-dev
@ -859,95 +896,32 @@ jobs:
qttools5-dev
ssh
- uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: cmake
working-directory: bin
run: >
cmake ..
-DWITH_DEBIAN=ON
-DDEB_ARCH=amd64
-DDEB_DIST=hirsute
-DFORCE_GIT_VERSION=
-DDEB_DIST=jammy
-DENABLE_SPOTIFY_BLOB=OFF
- name: make
working-directory: bin
run : make -j2 deb
- uses: actions/upload-artifact@v2
with:
name: release_hirsute_64
path: bin/clementine_*.deb
build_impish_64:
name: Build Ubuntu Impish 64-bit deb
runs-on: ubuntu-18.04
container:
image: ubuntu:impish
steps:
- name: Install dependencies
env:
DEBIAN_FRONTEND: noninteractive
run: >
apt-get update && apt-get install -y
cmake
dpkg-dev
debhelper
fakeroot
g++
gettext
git
libasound2-dev
libboost-dev
libboost-serialization-dev
libcdio-cdda2
libcdio-dev
libchromaprint-dev
libcrypto++-dev
libdbus-1-dev
libfftw3-dev
libglew1.5-dev
libgpod-dev
libgstreamer-plugins-base1.0-dev
libgstreamer1.0-dev
liblastfm5-dev
libmtp-dev
libmygpo-qt-dev
libplist-dev
libprotobuf-dev
libpulse-dev
libqca-qt5-2-dev
libqca-qt5-2-plugins
libqt5x11extras5-dev
libsparsehash-dev
libsqlite3-dev
libtag1-dev
libusbmuxd-dev
protobuf-compiler
qtbase5-dev
qttools5-dev-tools
qttools5-dev
ssh
- uses: actions/checkout@v1.2.0
- name: cmake
working-directory: bin
run: >
cmake ..
-DWITH_DEBIAN=ON
-DDEB_ARCH=amd64
-DDEB_DIST=impish
-DFORCE_GIT_VERSION=
-DENABLE_SPOTIFY_BLOB=OFF
- name: make
working-directory: bin
run : make -j2 deb
- uses: actions/upload-artifact@v2
with:
name: release_impish_64
name: release_jammy_64
path: bin/clementine_*.deb
build_mac:
if: false
name: Build Mac DMG
runs-on: macos-10.15
steps:
- uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
run: brew bundle

View File

@ -6,36 +6,40 @@ on:
jobs:
pull_translations:
name: Pull translations from Transifex
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
container:
image: ubuntu:bionic
image: ubuntu:jammy
steps:
- uses: supplypike/setup-bin@v3
with:
uri: https://github.com/transifex/cli/releases/download/v1.6.7/tx-linux-amd64.tar.gz
name: tx
version: 1.6.7
- name: Install dependencies
env:
DEBIAN_FRONTEND: noninteractive
run: >
apt-get update && apt-get install -y
git
python3-pip
ssh
- name: Install tx
run: pip3 install transifex-client==0.13.9
- name: Checkout
uses: actions/checkout@v1.2.0
- name: git hackery
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: Switch to master
run: git checkout master
- name: tx init
env:
TX_TOKEN: ${{ secrets.TX_TOKEN }}
run: tx init --no-interactive --force
run: tx init
- name: tx config
env:
TX_TOKEN: ${{ secrets.TX_TOKEN }}
run: tx config mapping --execute -r clementine.clementineplayer -s en -t PO --expression 'src/translations/<lang>.po'
run: tx add --organization davidsansome --project clementine --resource clementineplayer --file-filter 'src/translations/<lang>.po' --type PO src/translations/en.po
- name: tx pull
env:
TX_TOKEN: ${{ secrets.TX_TOKEN }}
run: tx pull --all -f --no-interactive
run: tx pull -f -a
- name: Setup git SSH
uses: webfactory/ssh-agent@v0.4.1
with:

View File

@ -1,128 +0,0 @@
/* Copyright 2014, Uwe L. Korn <uwelk@xhochy.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "Json.h"
// Qt version specific includes
#if QT_VERSION >= QT_VERSION_CHECK( 5, 0, 0 )
#include <QJsonDocument>
#include <QMetaProperty>
#else
#include <qjson/parser.h>
#include <qjson/qobjecthelper.h>
#include <qjson/serializer.h>
#endif
namespace QJsonWrapper
{
QVariantMap
qobject2qvariant( const QObject* object )
{
#if QT_VERSION >= QT_VERSION_CHECK( 5, 0, 0 )
QVariantMap map;
if ( object == NULL )
{
return map;
}
const QMetaObject* metaObject = object->metaObject();
for ( int i = 0; i < metaObject->propertyCount(); ++i )
{
QMetaProperty metaproperty = metaObject->property( i );
if ( metaproperty.isReadable() )
{
map[ QLatin1String( metaproperty.name() ) ] = object->property( metaproperty.name() );
}
}
return map;
#else
return QJson::QObjectHelper::qobject2qvariant( object );
#endif
}
void
qvariant2qobject( const QVariantMap& variant, QObject* object )
{
#if QT_VERSION >= QT_VERSION_CHECK( 5, 0, 0 )
for ( QVariantMap::const_iterator iter = variant.begin(); iter != variant.end(); ++iter )
{
QVariant property = object->property( iter.key().toLatin1() );
Q_ASSERT( property.isValid() );
if ( property.isValid() )
{
QVariant value = iter.value();
if ( value.canConvert( property.type() ) )
{
value.convert( property.type() );
object->setProperty( iter.key().toLatin1(), value );
} else if ( QString( QLatin1String("QVariant") ).compare( QLatin1String( property.typeName() ) ) == 0 ) {
object->setProperty( iter.key().toLatin1(), value );
}
}
}
#else
QJson::QObjectHelper::qvariant2qobject( variant, object );
#endif
}
QVariant
parseJson( const QByteArray& jsonData, bool* ok )
{
#if QT_VERSION >= QT_VERSION_CHECK( 5, 0, 0 )
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson( jsonData, &error );
if ( ok != NULL )
{
*ok = ( error.error == QJsonParseError::NoError );
}
return doc.toVariant();
#else
QJson::Parser p;
return p.parse( jsonData, ok );
#endif
}
QByteArray
toJson( const QVariant &variant, bool* ok )
{
#if QT_VERSION >= QT_VERSION_CHECK( 5, 0, 0 )
QJsonDocument doc = QJsonDocument::fromVariant( variant );
if ( ok != NULL )
{
*ok = !doc.isNull();
}
return doc.toJson( QJsonDocument::Compact );
#else
QJson::Serializer serializer;
QByteArray ret = serializer.serialize(variant);
if ( ok != NULL )
{
*ok = !ret.isNull();
}
return ret;
#endif
}
}

View File

@ -1,36 +0,0 @@
/* Copyright 2014, Uwe L. Korn <uwelk@xhochy.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#ifndef QJSONWRAPPER_JSON_H
#define QJSONWRAPPER_JSON_H
#include <QVariant>
namespace QJsonWrapper
{
QVariantMap qobject2qvariant( const QObject* object );
void qvariant2qobject( const QVariantMap& variant, QObject* object );
QVariant parseJson( const QByteArray& jsonData, bool* ok = 0 );
QByteArray toJson( const QVariant &variant, bool* ok = 0 );
}
#endif // QJSONWRAPPER_JSON_H

View File

@ -15,4 +15,4 @@ ADD_LIBRARY(qtiocompressor STATIC
${IOCOMPRESSOR-SOURCES-MOC}
)
target_link_libraries(qtiocompressor Qt5::Core)
target_link_libraries(qtiocompressor Qt5::Core ${ZLIB_LIBRARIES})

View File

@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.0.0)
project(clementine)
cmake_minimum_required(VERSION 3.0.0)
cmake_policy(SET CMP0053 OLD)
include(CheckCXXCompilerFlag)
@ -60,7 +60,6 @@ find_library(PROTOBUF_STATIC_LIBRARY libprotobuf.a libprotobuf)
pkg_check_modules(CDIO libcdio)
pkg_check_modules(CHROMAPRINT REQUIRED libchromaprint)
pkg_search_module(CRYPTOPP cryptopp libcrypto++)
pkg_check_modules(GIO gio-2.0)
pkg_check_modules(GLIB REQUIRED glib-2.0)
pkg_check_modules(GOBJECT REQUIRED gobject-2.0)
@ -75,12 +74,9 @@ pkg_check_modules(LIBMTP libmtp>=1.0)
pkg_check_modules(LIBMYGPO_QT5 libmygpo-qt5>=1.0.9)
pkg_check_modules(LIBPULSE libpulse)
pkg_check_modules(LIBXML libxml-2.0)
pkg_check_modules(LIBSPOTIFY libspotify>=12.1.45)
pkg_check_modules(TAGLIB taglib)
if (WIN32)
find_package(ZLIB REQUIRED)
endif (WIN32)
find_package(ZLIB REQUIRED)
find_library(LASTFM5_LIBRARIES lastfm5)
find_path(LASTFM5_INCLUDE_DIRS lastfm5/ws.h)
@ -167,12 +163,6 @@ endif()
if (APPLE)
find_library(SPARKLE Sparkle)
find_library(LIBSPOTIFY libspotify)
if(LIBSPOTIFY_FOUND)
set(LIBSPOTIFY_INCLUDE_DIRS ${LIBSPOTIFY})
set(LIBSPOTIFY_LIBRARIES ${LIBSPOTIFY})
endif(LIBSPOTIFY_FOUND)
add_subdirectory(3rdparty/SPMediaKeyTap)
set(SPMEDIAKEYTAP_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/SPMediaKeyTap)
@ -296,19 +286,6 @@ optional_component(UDISKS2 ON "Devices: UDisks2 backend"
DEPENDS "D-Bus support" Qt5DBus_FOUND
)
optional_component(SPOTIFY_BLOB ON "Spotify support: non-GPL binary helper"
DEPENDS "protobuf" PROTOBUF_FOUND PROTOBUF_PROTOC_EXECUTABLE
DEPENDS "libspotify" LIBSPOTIFY_FOUND
)
if (CRYPTOPP_FOUND OR HAVE_SPOTIFY_BLOB)
set(CRYPTOPP_OR_HAVE_SPOTIFY_BLOB ON)
endif()
optional_component(SPOTIFY ON "Spotify support"
DEPENDS "cryptopp or spotify blob" CRYPTOPP_OR_HAVE_SPOTIFY_BLOB
)
optional_component(MOODBAR ON "Moodbar support"
DEPENDS "fftw3" FFTW3_FOUND
)
@ -340,13 +317,6 @@ if (APPLE AND USE_BUNDLE AND NOT USE_BUNDLE_DIR)
set(USE_BUNDLE_DIR "../PlugIns")
endif()
if(CRYPTOPP_FOUND)
set(HAVE_CRYPTOPP ON)
if(HAVE_SPOTIFY)
set(HAVE_SPOTIFY_DOWNLOADER ON)
endif(HAVE_SPOTIFY)
endif(CRYPTOPP_FOUND)
# Remove GLU and GL from the link line - they're not really required
# and don't exist on my mingw toolchain
list(REMOVE_ITEM QT_LIBRARIES "-lGLU -lGL")
@ -377,11 +347,18 @@ include_directories("3rdparty/qsqlite")
# When/if upstream accepts our patches then these options can be used to link
# to system installed qtsingleapplication instead.
option(USE_SYSTEM_QTSINGLEAPPLICATION "Don't set this option unless your system QtSingleApplication library has been compiled with the Clementine patches in 3rdparty" OFF)
option(USE_SYSTEM_QTSINGLEAPPLICATION "Use the system-provided QtSingleApplication library (needs to have clementine patches, but these seem to be in Qt5)" OFF)
if(USE_SYSTEM_QTSINGLEAPPLICATION)
find_path(QTSINGLEAPPLICATION_INCLUDE_DIRS qtsingleapplication.h PATH_SUFFIXES qt5/QtSolutions)
find_library(QTSINGLEAPPLICATION_LIBRARIES Qt5Solutions_SingleApplication-2.6)
find_library(QTSINGLECOREAPPLICATION_LIBRARIES Qt5Solutions_SingleCoreApplication-2.6)
find_path(QTSINGLEAPPLICATION_INCLUDE_DIRS qtsingleapplication.h PATH_SUFFIXES qt5/QtSolutions REQUIRED)
find_library(QTSINGLEAPPLICATION_LIBRARIES Qt5Solutions_SingleApplication-2.6 REQUIRED)
add_library(qtsingleapplication INTERFACE)
target_link_libraries(qtsingleapplication INTERFACE QTSINGLEAPPLICATION_LIBRARIES)
target_include_directories(qtsingleapplication INTERFACE QTSINGLEAPPLICATION_INCLUDE_DIRS)
find_path(QTSINGLECOREAPPLICATION_INCLUDE_DIRS qtsinglecoreapplication.h PATH_SUFFIXES qt5/QtSolutions REQUIRED)
find_library(QTSINGLECOREAPPLICATION_LIBRARIES Qt5Solutions_SingleCoreApplication-2.6 REQUIRED)
add_library(qtsinglecoreapplication INTERFACE)
target_link_libraries(qtsinglecoreapplication INTERFACE QTSINGLECOREAPPLICATION_LIBRARIES)
target_include_directories(qtsinglecoreapplication INTERFACE QTSINGLECOREAPPLICATION_INCLUDE_DIRS)
else(USE_SYSTEM_QTSINGLEAPPLICATION)
add_subdirectory(3rdparty/qtsingleapplication)
set(QTSINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/qtsingleapplication)
@ -454,9 +431,6 @@ add_subdirectory(ext/libclementine-common)
add_subdirectory(ext/libclementine-tagreader)
add_subdirectory(ext/clementine-tagreader)
add_subdirectory(ext/libclementine-remote)
if(HAVE_SPOTIFY)
add_subdirectory(ext/libclementine-spotifyblob)
endif(HAVE_SPOTIFY)
option(WITH_DEBIAN OFF)
if(WITH_DEBIAN)
@ -467,10 +441,6 @@ if(HAVE_BREAKPAD)
add_subdirectory(3rdparty/google-breakpad)
endif(HAVE_BREAKPAD)
if(HAVE_SPOTIFY_BLOB)
add_subdirectory(ext/clementine-spotifyblob)
endif(HAVE_SPOTIFY_BLOB)
if(HAVE_MOODBAR)
add_subdirectory(gst/moodbar)
endif()

View File

@ -41,7 +41,7 @@ Compile and install:
cd bin
cmake ..
make -j8
make -j$(nproc)
sudo make install
See the Wiki for more instructions and a list of dependencies:

View File

@ -124,42 +124,43 @@ if(FORCE_GIT_REVISION)
set(GIT_INFO_RESULT 0)
else(FORCE_GIT_REVISION)
find_program(GIT_EXECUTABLE git)
message(STATUS "Found git: ${GIT_EXECUTABLE}")
if(NOT GIT_EXECUTABLE-NOTFOUND)
execute_process(COMMAND ${GIT_EXECUTABLE} describe
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
RESULT_VARIABLE GIT_INFO_RESULT
OUTPUT_VARIABLE GIT_REV
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)
if(NOT ${GIT_INFO_RESULT} EQUAL 0)
message(SEND_ERROR "git describe failed with code ${GIT_INFO_RESULT}: ${GIT_REV}")
endif()
endif()
endif()
if(${GIT_INFO_RESULT} EQUAL 0)
string(REGEX REPLACE "^(.+)-([0-9]+)-(g[a-f0-9]+)$" "\\1;\\2;\\3"
GIT_PARTS ${GIT_REV})
string(REGEX REPLACE "^(.+)-([0-9]+)-(g[a-f0-9]+)$" "\\1;\\2;\\3"
GIT_PARTS ${GIT_REV})
if(NOT GIT_PARTS)
message(FATAL_ERROR "Failed to parse git revision string '${GIT_REV}'")
endif(NOT GIT_PARTS)
if(NOT GIT_PARTS)
message(FATAL_ERROR "Failed to parse git revision string '${GIT_REV}'")
endif(NOT GIT_PARTS)
list(LENGTH GIT_PARTS GIT_PARTS_LENGTH)
if(GIT_PARTS_LENGTH EQUAL 3)
list(GET GIT_PARTS 0 GIT_TAGNAME)
list(GET GIT_PARTS 1 GIT_COMMITCOUNT)
list(GET GIT_PARTS 2 GIT_SHA1)
set(HAS_GET_REVISION ON)
endif(GIT_PARTS_LENGTH EQUAL 3)
endif(${GIT_INFO_RESULT} EQUAL 0)
list(LENGTH GIT_PARTS GIT_PARTS_LENGTH)
if(GIT_PARTS_LENGTH EQUAL 3)
list(GET GIT_PARTS 0 GIT_TAGNAME)
list(GET GIT_PARTS 1 GIT_COMMITCOUNT)
list(GET GIT_PARTS 2 GIT_SHA1)
set(HAS_GIT_REVISION ON)
endif(GIT_PARTS_LENGTH EQUAL 3)
if(INCLUDE_GIT_REVISION AND HAS_GET_REVISION)
if(INCLUDE_GIT_REVISION AND HAS_GIT_REVISION)
set(CLEMENTINE_VERSION_DISPLAY "${GIT_REV}")
set(CLEMENTINE_VERSION_DEB "${GIT_REV}")
set(CLEMENTINE_VERSION_RPM_V "${GIT_TAGNAME}")
set(CLEMENTINE_VERSION_RPM_R "2.${GIT_COMMITCOUNT}.${GIT_SHA1}")
set(CLEMENTINE_VERSION_SPARKLE "${GIT_REV}")
set(CLEMENTINE_VERSION_PLIST "4096.${GIT_TAGNAME}.2.${GIT_COMMITCOUNT}")
endif(INCLUDE_GIT_REVISION AND HAS_GET_REVISION)
endif(INCLUDE_GIT_REVISION AND HAS_GIT_REVISION)
if(0)
message(STATUS "Display: ${CLEMENTINE_VERSION_DISPLAY}")

View File

@ -88,17 +88,6 @@
<item begin="&lt;/a" end="&gt;"/>
</exclude>
</provider>
<provider name="hindilyrics.net (Bollywood songs)" title="{title} ({album})" charset="utf-8" url="http://www.hindilyrics.net/lyrics/of-{Title}.html">
<urlFormat replace=" _@;\/&quot;'()[]" with="%20"/>
<urlFormat replace="?" with=""/>
<extract>
<item begin="&lt;div class=nm&gt;Movie&lt;/div&gt;:" end="&lt;/pre&gt;"/>
</extract>
<exclude>
<item begin="&lt;span class=" end="&quot;&gt;"/>
</exclude>
<invalidIndicator value="Couldn't find that page."/>
</provider>
<provider name="letras.mus.br" title="" charset="utf-8" url="https://www.letras.mus.br/winamp.php?musica={title}&amp;artista={artist}">
<urlFormat replace="_@,;&amp;\/&quot;" with="_"/>
<urlFormat replace=" " with="+"/>
@ -114,13 +103,6 @@
</extract>
<invalidIndicator value="ERROR"/>
</provider>
<provider name="loudson.gs" title="" charset="utf-8" url="http://www.loudson.gs/{a}/{artist}/{album}/{title}">
<urlFormat replace=" _@,;&amp;\/&quot;" with="-"/>
<urlFormat replace="." with=""/>
<extract>
<item tag="&lt;div class=&quot;middle_col_TracksLyrics &quot;&gt;"/>
</extract>
</provider>
<provider name="lyrics.com" title="{artist} - {title} Lyrics" charset="utf-8" url="http://www.lyrics.com/lyrics/{artist}/{title}.html">
<urlFormat replace=" _@,;&amp;\/&quot;" with="-"/>
<urlFormat replace="'." with=""/>
@ -194,14 +176,6 @@
</extract>
<invalidIndicator value="Page not Found"/>
</provider>
<provider name="lyricstime.com" title="{artist} - {title} Lyrics" charset="iso-8859-1" url="http://www.lyricstime.com/{artist}-{title}-lyrics.html">
<urlFormat replace=" _@,;&amp;\/&quot;'" with="-"/>
<urlFormat replace="." with=""/>
<extract>
<item tag="&lt;div id=&quot;songlyrics&quot; &gt;"/>
<item tag="&lt;p&gt;"/>
</extract>
</provider>
<provider name="lyriki.com" title="" charset="utf-8" url="http://www.lyriki.com/{artist}:{title}">
<urlFormat replace=" _@,;&amp;\/&quot;" with="_"/>
<urlFormat replace="." with=""/>
@ -210,20 +184,6 @@
<item tag="&lt;p&gt;"/>
</extract>
</provider>
<provider name="metrolyrics.com" title="{artist} - {title} LYRICS" charset="utf-8" url="http://www.metrolyrics.com/{title}-lyrics-{artist}.html">
<urlFormat replace=" _@,;&amp;\/&quot;" with="-"/>
<urlFormat replace="'." with=""/>
<extract>
<item tag="&lt;span id=&quot;lyrics&quot;&gt;"/>
</extract>
<extract>
<item tag="&lt;div id=&quot;lyrics&quot;&gt;"/>
</extract>
<exclude>
<item tag="&lt;h5&gt;"/>
</exclude>
<invalidIndicator value="These lyrics are missing"/>
</provider>
<provider name="mp3lyrics.org" title="{artist} &amp;quot;{title}&amp;quot; Lyrics" charset="utf-8" url="http://www.mp3lyrics.org/{a}/{artist}/{title}/">
<urlFormat replace=" _@,;&amp;\/&quot;" with="-"/>
<urlFormat replace="'." with=""/>
@ -251,13 +211,6 @@
</exclude>
<invalidIndicator value="We couldn't find that page."/>
</provider>
<provider name="seeklyrics.com" title="{artist} - {title} Lyrics" charset="iso-8859-1" url="http://www.seeklyrics.com/lyrics/{Artist}/{Title}.html">
<urlFormat replace=" _@,;&amp;\/'&quot;" with="-"/>
<urlFormat replace="." with=""/>
<extract>
<item tag="&lt;div id=&quot;songlyrics&quot;&gt;"/>
</extract>
</provider>
<provider name="songlyrics.com" title="{title} LYRICS - {artist}" charset="utf-8" url="http://www.songlyrics.com/{artist}/{title}-lyrics/">
<urlFormat replace=" ._@,;&amp;\/&quot;" with="-"/>
<urlFormat replace="'" with="_"/>

2
debian/rules.in vendored
View File

@ -48,7 +48,7 @@ binary-arch: install
dh_installchangelogs
dh_installmenu
dh_installdocs
dh_gconf
dh_installgsettings
dh_link
dh_strip
dh_compress

View File

@ -18,6 +18,7 @@ BuildRequires: liblastfm-qt5-devel
BuildRequires: desktop-file-utils
BuildRequires: hicolor-icon-theme
BuildRequires: libappstream-glib
BuildRequires: qtsingleapplication-qt5-devel
BuildRequires: pkgconfig
BuildRequires: pkgconfig(glib-2.0)
BuildRequires: pkgconfig(gio-2.0)
@ -97,7 +98,7 @@ Features include:
%build
cd bin
%{cmake} .. -DUSE_INSTALL_PREFIX=OFF -DBUNDLE_PROJECTM_PRESETS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON
%{cmake} .. -DUSE_INSTALL_PREFIX=OFF -DBUNDLE_PROJECTM_PRESETS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DFORCE_GIT_REVISION=@GIT_REV@
%{cmake_build}
%install

1
dist/macdeploy.py vendored
View File

@ -66,7 +66,6 @@ GSTREAMER_PLUGINS = [
'libgstisomp4.dylib',
'libgstlame.dylib',
'libgstlibav.dylib',
'libgstmms.dylib',
# TODO: Bring back Musepack support.
'libgstogg.dylib',
'libgstopus.dylib',

View File

@ -15,6 +15,7 @@ GenericName[hi]=क्लेमेंटैन् संगीत वादक
GenericName[is]=Clementine tónlistarspilarinn
GenericName[pl]=Odtwarzacz muzyki Clementine
GenericName[pt]=Reprodutor de músicas Clementine
GenericName[ru]=Музыкальный проигрыватель Clementine
GenericName[sl]=Predvajalnik glasbe Clementine
GenericName[sr]=Клементина музички плејер
GenericName[sr@ijekavian]=Клементина музички плејер
@ -26,6 +27,7 @@ Comment[es]=Reproducción de música y flujos de Last.fm
Comment[is]=Spilar tónlist og streymi frá last.fm
Comment[pl]=Odtwarzanie muzyki i strumieni last.fm
Comment[pt]=Reprodução de músicas e emissões last.fm
Comment[ru]=Прослушивание музыки и потоков last.fm
Comment[sl]=Predvaja glasbo in pretoke last.fm
Comment[sr]=Репродукује музику и last.fm токове
Comment[sr@ijekavian]=Репродукује музику и last.fm токове
@ -38,106 +40,58 @@ Terminal=false
Categories=AudioVideo;Player;Qt;Audio;
StartupNotify=false
MimeType=application/ogg;application/x-ogg;application/x-ogm-audio;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/ogg;audio/vnd.rn-realaudio;audio/vorbis;audio/x-flac;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-oggflac;audio/x-pn-realaudio;audio/x-scpls;audio/x-speex;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-wav;video/x-ms-asf;x-content/audio-player;x-scheme-handler/zune;x-scheme-handler/itpc;x-scheme-handler/itms;x-scheme-handler/feed;
Actions=Play;Pause;Stop;StopAfterCurrent;Previous;Next;
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;
[Desktop Action Play]
Name=Play
Exec=clementine --play
Name[af]=Speel
Name[be]=Прайграць
Name[bg]=Възпроизвеждане
Name[br]=Lenn
Name[ca]=Reprodueix
Name[cs]=Přehrát
Name[da]=Afspil
Name[de]=Wiedergabe
Name[el]=Αναπαραγωγή
Name[es]=Reproducir
Name[et]=Mängi
Name[eu]=Erreproduzitu
Name[fa]=پخش
Name[fi]=Toista
Name[fr]=Lecture
Name[ga]=Seinn
Name[gl]=Reproducir
Name[he]=נגינה
Name[hi]=गाना बजाएं
Name[hr]=Pokreni reprodukciju
Name[hu]=Lejátszás
Name[is]=Spila
Name[it]=Riproduci
Name[ja]=再生
Name[kk]=Ойнату
Name[lt]=Groti
Name[lv]=Atskaņot
Name[ms]=Mainkan
Name[nb]=Spill
Name[nl]=Afspelen
Name[oc]=Lectura
Name[pl]=Odtwarzaj
Name[pt]=Reproduzir
Name[pt_BR]=Reproduzir
Name[ro]=Redă
Name[ru]=Воспроизвести
Name[sk]=Hrať
Name[sl]=Predvajaj
Name[sr]=Пусти
Name[sr@ijekavian]=Пусти
Name[sr@ijekavianlatin]=Pusti
Name[sr@latin]=Pusti
Name[sv]=Spela upp
Name[tr]=Çal
Name[uk]=Відтворити
Name[vi]=Phát
Name[zh_CN]=播放
Name[zh_TW]=播放
[Desktop Action Pause]
Name=Pause
Exec=clementine --pause
Name[be]=Прыпыніць
Name[bg]=Пауза
Name[br]=Ehan
Name[ca]=Pausa
Name[cs]=Pozastavit
Name[el]=Παύση
Name[es]=Pausar
Name[et]=Paus
Name[eu]=Pausarazi
Name[fa]=ایست
Name[fi]=Keskeytä
Name[ga]=Cuir ar sos
Name[gl]=Pausa
Name[he]=השהייה
Name[hi]=गाना रोकें
Name[hr]=Pauza
Name[hu]=Szünet
Name[is]=Setja í bið
Name[it]=Pausa
Name[ja]=一時停止
Name[kk]=Аялдату
Name[ko]=일시중지
Name[lt]=Pristabdyti
Name[lv]=Pauze
Name[nl]=Pauze
Name[oc]=Pausa
Name[pl]=Pauza
Name[pt]=Pausa
Name[pt_BR]=Pausar
Name[ro]=Pauză
Name[ru]=Приостановить
Name[sk]=Pozastaviť
Name[sl]=Začasno ustavi
Name[sr]=Паузирај
Name[sr@ijekavian]=Паузирај
Name[sr@ijekavianlatin]=Pauziraj
Name[sr@latin]=Pauziraj
Name[sv]=Gör paus
Name[tr]=Duraklat
Name[uk]=Призупинити
Name[vi]=Tạm dừng
Name[zh_CN]=暂停
Name[zh_TW]=暫停
[Desktop Action Play-Pause]
Name=Play/Pause
Exec=clementine --play-pause
Name[be]=Прайграць/Прыпыніць
Name[bg]=Възпроизвеждане/Пауза
Name[br]=Lenn/Ehan
Name[ca]=Reprodueix/Pausa
Name[cs]=Přehrát/Pozastavit
Name[da]=Afspil/Pause
Name[de]=Wiedergabe/Pause
Name[el]=Αναπαραγωγή/Παύση
Name[es]=Reproducir/Pausar
Name[et]=Mängi/Paus
Name[eu]=Erreproduzitu/Pausarazi
Name[fa]=پخش/مکث
Name[fi]=Toista/Keskeytä
Name[fr]=Lecture/Pause
Name[ga]=Seinn/Cuir ar sos
Name[gl]=Reproducir/Pausa
Name[he]=נגן/השהה
Name[hi]=गाना बजाएं/गाना रोकें
Name[hr]=Pokreni reprodukciju/Pauza
Name[hu]=Lejátszás/Szünet
Name[is]=Spila/Setja í bið
Name[it]=Riproduci/Pausa
Name[ja]=再生/一時停止
Name[kk]=Ойнату/Аялдату
Name[ko]=재생/일시 중지
Name[lt]=Groti/Pristabdyti
Name[lv]=Atskaņot/Pauze
Name[nb]=Spill/Pause
Name[nl]=Afspelen/Pauze
Name[oc]=Lectura/Pausa
Name[pl]=Odtwarzaj/Pauza
Name[pt]=Reproduzir/Pausa
Name[pt_BR]=Reproduzir/Pausar
Name[ro]=Redă/Pauză
Name[ru]=Играть/пауза
Name[sk]=Hrať/Pozastaviť
Name[sl]=Predvajaj/Začasno ustavi
Name[sr]=Пусти/Паузирај
Name[sr@ijekavian]=Пусти/Паузирај
Name[sr@ijekavianlatin]=Pusti/Pauziraj
Name[sr@latin]=Pusti/Pauziraj
Name[sv]=Spela upp/Gör paus
Name[tr]=Çal/Duraklat
Name[uk]=Відтворити/Призупинити
Name[vi]=Phát/Tạm dừng
Name[zh_CN]=播放/暂停
Name[zh_TW]=播放/暫停
[Desktop Action Stop]
Name=Stop
@ -322,7 +276,7 @@ Name[pl]=Dalej
Name[pt]=Seguinte
Name[pt_BR]=Próximo
Name[ro]=Următoarea
Name[ru]=Дальше
Name[ru]=Следующий
Name[sk]=Ďalšia
Name[sl]=Naslednji
Name[sr]=Следећа

View File

@ -302,7 +302,6 @@ Section "Clementine" Clementine
File "clementine.exe"
File "clementine-tagreader.exe"
File "clementine-spotifyblob.exe"
File "clementine.ico"
File "glew32.dll"
File "libcdio-19.dll"
@ -355,7 +354,6 @@ Section "Clementine" Clementine
File "libpsl-5.dll"
File "libsoup-2.4-1.dll"
File "libspeex-1.dll"
File "libspotify.dll"
File "libssl-1_1.dll"
File "libsqlite3-0.dll"
File "libstdc++-6.dll"

View File

@ -1,79 +0,0 @@
include_directories(${LIBSPOTIFY_INCLUDE_DIRS})
include_directories(${PROTOBUF_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-spotifyblob)
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-spotifyblob)
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-common)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Woverloaded-virtual -Wall -Wno-sign-compare -Wno-deprecated-declarations -Wno-unused-local-typedefs -Wno-unused-private-field -Wno-unknown-warning-option")
link_directories(${LIBSPOTIFY_LIBRARY_DIRS})
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
set(SOURCES
main.cpp
mediapipeline.cpp
spotifyclient.cpp
spotify_utilities.cpp
)
set(HEADERS
spotifyclient.h
)
if(APPLE)
list(APPEND SOURCES spotify_utilities.mm)
endif(APPLE)
qt5_wrap_cpp(MOC ${HEADERS})
if(WIN32 AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT ENABLE_WIN32_CONSOLE)
set(win32_build_flag WIN32)
endif(WIN32 AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT ENABLE_WIN32_CONSOLE)
add_executable(clementine-spotifyblob
${win32_build_flag}
${SOURCES}
${MOC}
)
target_link_libraries(clementine-spotifyblob
${LIBSPOTIFY_LIBRARIES} ${LIBSPOTIFY_LDFLAGS}
${QT_QTCORE_LIBRARY}
${QT_QTNETWORK_LIBRARY}
${GSTREAMER_BASE_LIBRARIES}
${GSTREAMER_APP_LIBRARIES}
${PROTOBUF_STATIC_LIBRARY}
clementine-spotifyblob-messages
libclementine-common
)
if(APPLE)
target_link_libraries(clementine-spotifyblob
"-framework Foundation"
)
endif(APPLE)
if(NOT APPLE)
# macdeploy.py takes care of this on mac
install(TARGETS clementine-spotifyblob
RUNTIME DESTINATION bin
)
endif(NOT APPLE)
if(LINUX)
# Versioned name of the blob
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
set(SPOTIFY_BLOB_ARCH 32)
else(CMAKE_SIZEOF_VOID_P EQUAL 4)
set(SPOTIFY_BLOB_ARCH 64)
endif(CMAKE_SIZEOF_VOID_P EQUAL 4)
install(
FILES ${CMAKE_BINARY_DIR}/clementine-spotifyblob
DESTINATION ${CMAKE_BINARY_DIR}/spotify/version${SPOTIFY_BLOB_VERSION}-${SPOTIFY_BLOB_ARCH}bit/
RENAME blob
)
endif(LINUX)

View File

@ -1,49 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL because
// it is used by the Spotify blob which links against libspotify and is not GPL
// compatible.
#include <gst/gst.h>
#include <QCoreApplication>
#include <QStringList>
#include "core/logging.h"
#include "spotifyclient.h"
int main(int argc, char** argv) {
QCoreApplication a(argc, argv);
QCoreApplication::setApplicationName("Clementine");
QCoreApplication::setOrganizationName("Clementine");
QCoreApplication::setOrganizationDomain("clementine-player.org");
logging::Init();
gst_init(nullptr, nullptr);
const QStringList arguments(a.arguments());
if (arguments.length() != 2) {
qFatal("Usage: %s port", argv[0]);
}
SpotifyClient client;
client.Init(arguments[1].toInt());
return a.exec();
}

View File

@ -1,169 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL because
// it is used by the Spotify blob which links against libspotify and is not GPL
// compatible.
#include "mediapipeline.h"
#include <cstring>
#include "core/logging.h"
#include "core/timeconstants.h"
MediaPipeline::MediaPipeline(int port, quint64 length_msec)
: port_(port),
length_msec_(length_msec),
accepting_data_(true),
pipeline_(nullptr),
appsrc_(nullptr),
byte_rate_(1),
offset_bytes_(0) {}
MediaPipeline::~MediaPipeline() {
if (pipeline_) {
gst_element_set_state(pipeline_, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(pipeline_));
}
}
bool MediaPipeline::Init(int sample_rate, int channels) {
if (is_initialised()) return false;
pipeline_ = gst_pipeline_new("pipeline");
// Create elements
appsrc_ = GST_APP_SRC(gst_element_factory_make("appsrc", nullptr));
GstElement* gdppay = gst_element_factory_make("gdppay", nullptr);
tcpsink_ = gst_element_factory_make("tcpclientsink", nullptr);
if (!pipeline_ || !appsrc_ || !tcpsink_) {
if (pipeline_) {
gst_object_unref(GST_OBJECT(pipeline_));
pipeline_ = nullptr;
}
if (appsrc_) {
gst_object_unref(GST_OBJECT(appsrc_));
appsrc_ = nullptr;
}
if (gdppay) {
gst_object_unref(GST_OBJECT(gdppay));
}
if (tcpsink_) {
gst_object_unref(GST_OBJECT(tcpsink_));
tcpsink_ = nullptr;
}
return false;
}
// Add elements to the pipeline and link them
gst_bin_add(GST_BIN(pipeline_), GST_ELEMENT(appsrc_));
gst_bin_add(GST_BIN(pipeline_), gdppay);
gst_bin_add(GST_BIN(pipeline_), tcpsink_);
gst_element_link_many(GST_ELEMENT(appsrc_), gdppay, tcpsink_, nullptr);
// Set the sink's port
g_object_set(G_OBJECT(tcpsink_), "host", "127.0.0.1", nullptr);
g_object_set(G_OBJECT(tcpsink_), "port", port_, nullptr);
// Try to send 5 seconds of audio in advance to initially fill Clementine's
// buffer.
g_object_set(G_OBJECT(tcpsink_), "ts-offset", qint64(-5 * kNsecPerSec),
nullptr);
// We know the time of each buffer
g_object_set(G_OBJECT(appsrc_), "format", GST_FORMAT_TIME, nullptr);
// Spotify only pushes data to us every 100ms, so keep the appsrc half full
// to prevent tiny stalls.
g_object_set(G_OBJECT(appsrc_), "min-percent", 50, nullptr);
// Set callbacks for when to start/stop pushing data
GstAppSrcCallbacks callbacks;
callbacks.enough_data = EnoughDataCallback;
callbacks.need_data = NeedDataCallback;
callbacks.seek_data = SeekDataCallback;
gst_app_src_set_callbacks(appsrc_, &callbacks, this, nullptr);
#if Q_BYTE_ORDER == Q_BIG_ENDIAN
static const char* format = "S16BE";
#elif Q_BYTE_ORDER == Q_LITTLE_ENDIAN
static const char* format = "S16LE";
#endif
// Set caps
GstCaps* caps = gst_caps_new_simple(
"audio/x-raw", "format", G_TYPE_STRING, format, "rate", G_TYPE_INT,
sample_rate, "channels", G_TYPE_INT, channels, "layout", G_TYPE_STRING,
"interleaved", nullptr);
gst_app_src_set_caps(appsrc_, caps);
gst_caps_unref(caps);
// Set size
byte_rate_ = quint64(sample_rate) * channels * 2;
const quint64 bytes = byte_rate_ * length_msec_ / 1000;
gst_app_src_set_size(appsrc_, bytes);
// Ready to go
return gst_element_set_state(pipeline_, GST_STATE_PLAYING) !=
GST_STATE_CHANGE_FAILURE;
}
void MediaPipeline::WriteData(const char* data, qint64 length) {
if (!is_initialised()) return;
GstBuffer* buffer = gst_buffer_new_allocate(nullptr, length, nullptr);
GstMapInfo map_info;
gst_buffer_map(buffer, &map_info, GST_MAP_WRITE);
memcpy(map_info.data, data, length);
gst_buffer_unmap(buffer, &map_info);
GST_BUFFER_PTS(buffer) = offset_bytes_ * kNsecPerSec / byte_rate_;
GST_BUFFER_DURATION(buffer) = length * kNsecPerSec / byte_rate_;
offset_bytes_ += length;
gst_app_src_push_buffer(appsrc_, buffer);
}
void MediaPipeline::EndStream() {
if (!is_initialised()) return;
gst_app_src_end_of_stream(appsrc_);
}
void MediaPipeline::NeedDataCallback(GstAppSrc* src, guint length, void* data) {
MediaPipeline* me = reinterpret_cast<MediaPipeline*>(data);
me->accepting_data_ = true;
}
void MediaPipeline::EnoughDataCallback(GstAppSrc* src, void* data) {
MediaPipeline* me = reinterpret_cast<MediaPipeline*>(data);
me->accepting_data_ = false;
}
gboolean MediaPipeline::SeekDataCallback(GstAppSrc* src, guint64 offset,
void* data) {
// MediaPipeline* me = reinterpret_cast<MediaPipeline*>(data);
qLog(Debug) << "Gstreamer wants seek to" << offset;
return false;
}

View File

@ -1,62 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL because
// it is used by the Spotify blob which links against libspotify and is not GPL
// compatible.
#ifndef MEDIAPIPELINE_H
#define MEDIAPIPELINE_H
#include <QtGlobal>
#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
class MediaPipeline {
public:
MediaPipeline(int port, quint64 length_msec);
~MediaPipeline();
bool is_initialised() const { return pipeline_; }
bool is_accepting_data() const { return accepting_data_; }
bool Init(int sample_rate, int channels);
void WriteData(const char* data, qint64 length);
void EndStream();
private:
static void NeedDataCallback(GstAppSrc* src, guint length, void* data);
static void EnoughDataCallback(GstAppSrc* src, void* data);
static gboolean SeekDataCallback(GstAppSrc* src, guint64 offset, void* data);
private:
Q_DISABLE_COPY(MediaPipeline)
const int port_;
const quint64 length_msec_;
bool accepting_data_;
GstElement* pipeline_;
GstAppSrc* appsrc_;
GstElement* tcpsink_;
quint64 byte_rate_;
quint64 offset_bytes_;
};
#endif // MEDIAPIPELINE_H

View File

@ -1,66 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL because
// it is used by the Spotify blob which links against libspotify and is not GPL
// compatible.
#include "spotify_utilities.h"
#include <stdlib.h>
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QSettings>
namespace utilities {
QString GetCacheDirectory() {
QString user_cache = GetUserDataDirectory();
return user_cache + "/" + QCoreApplication::applicationName() +
"/spotify-cache";
}
#ifndef Q_OS_DARWIN // See spotify_utilities.mm for Mac implementation.
QString GetUserDataDirectory() {
const char* xdg_cache_dir = getenv("XDG_CACHE_HOME");
if (!xdg_cache_dir) {
return QDir::homePath() + "/.config";
}
return QString::fromLocal8Bit(xdg_cache_dir);
}
QString GetSettingsDirectory() {
QString ret;
#ifdef Q_OS_WIN32
ret = GetUserDataDirectory() + "/" + QCoreApplication::applicationName() +
"/spotify-settings";
#else
ret = QFileInfo(QSettings().fileName()).absolutePath() + "/spotify-settings";
#endif // Q_OS_WIN32
// Create the directory
QDir dir;
dir.mkpath(ret);
return ret;
}
#endif // Q_OS_DARWIN
} // namespace utilities

View File

@ -1,37 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL because
// it is used by the Spotify blob which links against libspotify and is not GPL
// compatible.
#ifndef SPOTIFY_UTILITIES_H
#define SPOTIFY_UTILITIES_H
#include <QString>
namespace utilities {
// Get the path to the current user's data directory for all apps.
QString GetUserDataDirectory();
// Get the path for Clementine's cache.
QString GetCacheDirectory();
QString GetSettingsDirectory();
}
#endif

View File

@ -1,44 +0,0 @@
#include "spotify_utilities.h"
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSFileManager.h>
#import <Foundation/NSPathUtilities.h>
#import "core/scoped_nsautorelease_pool.h"
namespace utilities {
QString GetUserDataDirectory() {
ScopedNSAutoreleasePool pool;
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
QString ret;
if ([paths count] > 0) {
NSString* user_path = [paths objectAtIndex:0];
ret = QString::fromUtf8([user_path UTF8String]);
} else {
ret = "~/Library/Caches";
}
return ret;
}
QString GetSettingsDirectory() {
ScopedNSAutoreleasePool pool;
NSArray* paths =
NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSString* ret;
if ([paths count] > 0) {
ret = [paths objectAtIndex:0];
} else {
ret = @"~/Library/Application Support";
}
ret = [ret stringByAppendingString:@"/Clementine/spotify-settings"];
NSFileManager* file_manager = [NSFileManager defaultManager];
[file_manager createDirectoryAtPath:ret withIntermediateDirectories:YES attributes:nil error:nil];
QString path = QString::fromUtf8([ret UTF8String]);
return path;
}
} // namespace utilities

File diff suppressed because it is too large Load Diff

View File

@ -1,204 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL because
// it is used by the Spotify blob which links against libspotify and is not GPL
// compatible.
#ifndef SPOTIFYCLIENT_H
#define SPOTIFYCLIENT_H
#include "spotifymessages.pb.h"
#include "core/messagehandler.h"
#include <QMap>
#include <QObject>
#include <libspotify/api.h>
class QTcpSocket;
class QTimer;
class MediaPipeline;
class ResponseMessage;
class SpotifyClient : public AbstractMessageHandler<cpb::spotify::Message> {
Q_OBJECT
public:
SpotifyClient(QObject* parent = nullptr);
~SpotifyClient();
static const int kSpotifyImageIDSize;
static const int kWaveHeaderSize;
void Init(quint16 port);
protected:
void MessageArrived(const cpb::spotify::Message& message);
void DeviceClosed();
private slots:
void ProcessEvents();
private:
void SendLoginCompleted(bool success, const QString& error,
cpb::spotify::LoginResponse_Error error_code);
void SendPlaybackError(const QString& error);
void SendSearchResponse(sp_search* result);
// Spotify session callbacks.
static void SP_CALLCONV LoggedInCallback(sp_session* session, sp_error error);
static void SP_CALLCONV NotifyMainThreadCallback(sp_session* session);
static void SP_CALLCONV
LogMessageCallback(sp_session* session, const char* data);
static void SP_CALLCONV
SearchCompleteCallback(sp_search* result, void* userdata);
static void SP_CALLCONV MetadataUpdatedCallback(sp_session* session);
static int SP_CALLCONV
MusicDeliveryCallback(sp_session* session, const sp_audioformat* format,
const void* frames, int num_frames);
static void SP_CALLCONV EndOfTrackCallback(sp_session* session);
static void SP_CALLCONV
StreamingErrorCallback(sp_session* session, sp_error error);
static void SP_CALLCONV OfflineStatusUpdatedCallback(sp_session* session);
static void SP_CALLCONV
ConnectionErrorCallback(sp_session* session, sp_error error);
static void SP_CALLCONV
UserMessageCallback(sp_session* session, const char* message);
static void SP_CALLCONV StartPlaybackCallback(sp_session* session);
static void SP_CALLCONV StopPlaybackCallback(sp_session* session);
// Spotify playlist container callbacks.
static void SP_CALLCONV PlaylistAddedCallback(sp_playlistcontainer* pc,
sp_playlist* playlist,
int position, void* userdata);
static void SP_CALLCONV PlaylistRemovedCallback(sp_playlistcontainer* pc,
sp_playlist* playlist,
int position, void* userdata);
static void SP_CALLCONV
PlaylistMovedCallback(sp_playlistcontainer* pc, sp_playlist* playlist,
int position, int new_position, void* userdata);
static void SP_CALLCONV
PlaylistContainerLoadedCallback(sp_playlistcontainer* pc, void* userdata);
// Spotify playlist callbacks - when loading the list of playlists
// initially
static void SP_CALLCONV
PlaylistStateChangedForGetPlaylists(sp_playlist* pl, void* userdata);
// Spotify playlist callbacks - when loading a playlist
static void SP_CALLCONV
PlaylistStateChangedForLoadPlaylist(sp_playlist* pl, void* userdata);
// Spotify image callbacks.
static void SP_CALLCONV ImageLoaded(sp_image* image, void* userdata);
// Spotify album browse callbacks.
static void SP_CALLCONV
SearchAlbumBrowseComplete(sp_albumbrowse* result, void* userdata);
static void SP_CALLCONV
AlbumBrowseComplete(sp_albumbrowse* result, void* userdata);
// Spotify toplist browse callbacks.
static void SP_CALLCONV
ToplistBrowseComplete(sp_toplistbrowse* result, void* userdata);
// Request handlers.
void Login(const cpb::spotify::LoginRequest& req);
void Search(const cpb::spotify::SearchRequest& req);
void LoadPlaylist(const cpb::spotify::LoadPlaylistRequest& req);
void SyncPlaylist(const cpb::spotify::SyncPlaylistRequest& req);
void AddTracksToPlaylist(const cpb::spotify::AddTracksToPlaylistRequest& req);
void RemoveTracksFromPlaylist(
const cpb::spotify::RemoveTracksFromPlaylistRequest& req);
void StartPlayback(const cpb::spotify::PlaybackRequest& req);
void Seek(qint64 offset_nsec);
void LoadImage(const QString& id_b64);
void BrowseAlbum(const QString& uri);
void BrowseToplist(const cpb::spotify::BrowseToplistRequest& req);
void SetPlaybackSettings(const cpb::spotify::PlaybackSettings& req);
void SetPaused(const cpb::spotify::PauseRequest& req);
void SendPlaylistList();
void ConvertTrack(sp_track* track, cpb::spotify::Track* pb);
void ConvertAlbum(sp_album* album, cpb::spotify::Track* pb);
void ConvertAlbumBrowse(sp_albumbrowse* browse, cpb::spotify::Track* pb);
// Gets the appropriate sp_playlist* but does not load it.
sp_playlist* GetPlaylist(cpb::spotify::PlaylistType type, int user_index);
private:
struct PendingLoadPlaylist {
cpb::spotify::LoadPlaylistRequest request_;
sp_playlist* playlist_;
QList<sp_track*> tracks_;
bool offline_sync;
};
struct PendingPlaybackRequest {
cpb::spotify::PlaybackRequest request_;
sp_link* link_;
sp_track* track_;
bool operator==(const PendingPlaybackRequest& other) const {
return request_.track_uri() == other.request_.track_uri() &&
request_.media_port() == other.request_.media_port();
}
};
struct PendingImageRequest {
QString id_b64_;
QByteArray id_;
sp_image* image_;
};
void TryPlaybackAgain(const PendingPlaybackRequest& req);
void TryImageAgain(sp_image* image);
int GetDownloadProgress(sp_playlist* playlist);
void SendDownloadProgress(cpb::spotify::PlaylistType type, int index,
int download_progress);
QByteArray api_key_;
QTcpSocket* protocol_socket_;
sp_session_config spotify_config_;
sp_session_callbacks spotify_callbacks_;
sp_playlistcontainer_callbacks playlistcontainer_callbacks_;
sp_playlist_callbacks get_playlists_callbacks_;
sp_playlist_callbacks load_playlist_callbacks_;
sp_session* session_;
QTimer* events_timer_;
QList<PendingLoadPlaylist> pending_load_playlists_;
QList<PendingPlaybackRequest> pending_playback_requests_;
QList<PendingImageRequest> pending_image_requests_;
QMap<sp_image*, int> image_callbacks_registered_;
QMap<sp_search*, cpb::spotify::SearchRequest> pending_searches_;
QMap<sp_albumbrowse*, QString> pending_album_browses_;
QMap<sp_toplistbrowse*, cpb::spotify::BrowseToplistRequest>
pending_toplist_browses_;
QMap<sp_search*, QList<sp_albumbrowse*>> pending_search_album_browses_;
QMap<sp_albumbrowse*, sp_search*> pending_search_album_browse_responses_;
QScopedPointer<MediaPipeline> media_pipeline_;
};
#endif // SPOTIFYCLIENT_H

View File

@ -1,37 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL because
// it is used by the Spotify blob which links against libspotify and is not GPL
// compatible.
// The Spotify terms of service require that application keys are not
// accessible to third parties. Therefore this application key is heavily
// encrypted here in the source to prevent third parties from viewing it.
// It is most definitely not base64 encoded.
static const char* kSpotifyApiKey =
"AVlOrvJkKx8T+LEsCk+Kyl24I0MSsjohZAtMFzm2O5Lms1bmAWFWgdZaHkpypzSJPmSd+"
"Wi50wwg"
"JwVCU0sq4Lep1zB4t6Z8h26NK6+z8gmkHVkV9DRPkRgebcUkWTDTflwVPKWF4+"
"gdRjUwprsqBw6O"
"iofRLJzeKaxbmaUGqkSkxVLOiXC9lxylNq6ju7Q7uY8u8XkDUsVM3YIxiWy2+EM7I/"
"lhatzT9xrq"
"rxHe2lg7CzOwF5kuFdwgmi8MQ72xTYXIKnNlOry/"
"hJDlN9lKxkbUBLh+pzbYvO92S2fYKK5PAHvX"
"5+SmSBGbh6dlpHeCGqb8MPdaeZ5I1YxMcDkxa2+tbLA/Muat7gKA9u57TFCtYjun/u/i/"
"ONwdBIQ"
"rePzXZjipO32kYmQAiCkN1p8sgQEcF43QxaVwXGo2X0rRnJf";

View File

@ -1,18 +0,0 @@
include_directories(${PROTOBUF_INCLUDE_DIRS})
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/blobversion.h.in
${CMAKE_CURRENT_BINARY_DIR}/blobversion.h)
set(MESSAGES
spotifymessages.proto
)
protobuf_generate_cpp(PROTO_SOURCES PROTO_HEADERS ${MESSAGES})
add_library(clementine-spotifyblob-messages STATIC
${PROTO_SOURCES}
)
target_link_libraries(clementine-spotifyblob-messages
libclementine-common
)

View File

@ -1,23 +0,0 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SPOTIFY_BLOBVERSION_H_IN
#define SPOTIFY_BLOBVERSION_H_IN
#define SPOTIFY_BLOB_VERSION ${SPOTIFY_BLOB_VERSION}
#endif // SPOTIFY_BLOBVERSION_H_IN

View File

@ -1,236 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL because
// it is used by the Spotify blob which links against libspotify and is not GPL
// compatible.
syntax = "proto2";
package cpb.spotify;
message LoginRequest {
required string username = 1;
optional string password = 2;
optional PlaybackSettings playback_settings = 3;
}
message LoginResponse {
enum Error {
BadUsernameOrPassword = 1;
UserBanned = 2;
UserNeedsPremium = 3;
Other = 4;
ReloginFailed = 5;
}
required bool success = 1;
required string error = 2;
optional Error error_code = 3 [default = Other];
}
message Playlists {
message Playlist {
required int32 index = 1;
required string name = 2;
required int32 nb_tracks = 3;
required bool is_mine = 4;
required string owner= 5;
required bool is_offline = 6;
required string uri = 7;
// Offline sync progress between 0-100.
optional int32 download_progress = 8;
}
repeated Playlist playlist = 1;
}
message Track {
required bool starred = 1;
required string title = 2;
repeated string artist = 3;
required string album = 4;
required int32 duration_msec = 5;
required int32 popularity = 6;
required int32 disc = 7;
required int32 track = 8;
required int32 year = 9;
required string uri = 10;
required string album_art_id = 11;
}
message Album {
required Track metadata = 1;
repeated Track track = 2;
}
enum PlaylistType {
Starred = 1;
Inbox = 2;
UserPlaylist = 3;
}
message LoadPlaylistRequest {
required PlaylistType type = 1;
optional int32 user_playlist_index = 2;
}
message LoadPlaylistResponse {
required LoadPlaylistRequest request = 1;
repeated Track track = 2;
}
message SyncPlaylistRequest {
required LoadPlaylistRequest request = 1;
required bool offline_sync = 2;
}
message SyncPlaylistProgress {
required LoadPlaylistRequest request = 1;
required int32 sync_progress = 2;
}
message PlaybackRequest {
required string track_uri = 1;
required int32 media_port = 2;
}
message PlaybackError {
required string error = 1;
}
message SearchRequest {
required string query = 1;
optional int32 limit = 2 [default = 250];
optional int32 limit_album = 3 [default = 0];
}
message SearchResponse {
required SearchRequest request = 1;
repeated Track result = 2;
optional int32 total_tracks = 3;
optional string did_you_mean = 4;
optional string error = 5;
// field 6 is deprecated
repeated Album album = 7;
}
message ImageRequest {
required string id = 1;
}
message ImageResponse {
required string id = 1;
optional bytes data = 2;
}
message BrowseAlbumRequest {
required string uri = 1;
}
message BrowseAlbumResponse {
required string uri = 1;
repeated Track track = 2;
}
message BrowseToplistRequest {
enum ToplistType {
Artists = 1;
Albums = 2;
Tracks = 3;
};
enum Region {
Everywhere = 1;
User = 2;
};
required ToplistType type = 1;
optional Region region = 2 [default=Everywhere];
// Username to use if region is User.
optional string username = 3;
}
message BrowseToplistResponse {
required BrowseToplistRequest request = 1;
repeated Track track = 2;
repeated Album album = 3;
}
message SeekRequest {
optional int64 offset_nsec = 1;
}
enum Bitrate {
Bitrate96k = 1;
Bitrate160k = 2;
Bitrate320k = 3;
}
message PlaybackSettings {
optional Bitrate bitrate = 1 [default = Bitrate320k];
optional bool volume_normalisation = 2 [default = false];
}
message PauseRequest {
optional bool paused = 1 [default = false];
}
message AddTracksToPlaylistRequest {
required PlaylistType playlist_type = 1;
optional int64 playlist_index = 2; // Used if playlist_index == UserPlaylist
repeated string track_uri = 3;
}
message RemoveTracksFromPlaylistRequest {
required PlaylistType playlist_type = 1;
optional int64 playlist_index = 2; // Used if playlist_index == UserPlaylist
repeated int64 track_index = 3;
}
// NEXT_ID: 25
message Message {
// Not currently used
optional int32 id = 18;
optional LoginRequest login_request = 1;
optional LoginResponse login_response = 2;
optional Playlists playlists_updated = 3;
optional LoadPlaylistRequest load_playlist_request = 4;
optional LoadPlaylistResponse load_playlist_response = 5;
optional PlaybackRequest playback_request = 6;
optional PlaybackError playback_error = 7;
optional SearchRequest search_request = 8;
optional SearchResponse search_response = 9;
optional ImageRequest image_request = 10;
optional ImageResponse image_response = 11;
optional SyncPlaylistRequest sync_playlist_request = 12;
optional SyncPlaylistProgress sync_playlist_progress = 13;
optional BrowseAlbumRequest browse_album_request = 14;
optional BrowseAlbumResponse browse_album_response = 15;
optional SeekRequest seek_request = 16;
optional PlaybackSettings set_playback_settings_request = 17;
optional BrowseToplistRequest browse_toplist_request = 19;
optional BrowseToplistResponse browse_toplist_response = 20;
optional PauseRequest pause_request = 21;
// ID 22 unused.
optional AddTracksToPlaylistRequest add_tracks_to_playlist = 23;
optional RemoveTracksFromPlaylistRequest remove_tracks_from_playlist = 24;
}

View File

@ -47,10 +47,6 @@ include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-tagreader)
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-tagreader)
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-remote)
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-remote)
if(HAVE_SPOTIFY)
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-spotifyblob)
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-spotifyblob)
endif(HAVE_SPOTIFY)
include(../cmake/ParseArguments.cmake)
@ -885,37 +881,6 @@ optional_source(HAVE_LIBLASTFM
internet/lastfm/lastfmsettingspage.ui
)
# Spotify support
optional_source(HAVE_SPOTIFY
SOURCES
internet/spotify/spotifyserver.cpp
internet/spotify/spotifyservice.cpp
internet/spotify/spotifysettingspage.cpp
internet/spotifywebapi/spotifywebapiservice.cpp
globalsearch/spotifysearchprovider.cpp
globalsearch/spotifywebapisearchprovider.cpp
HEADERS
globalsearch/spotifysearchprovider.h
globalsearch/spotifywebapisearchprovider.h
internet/spotify/spotifyserver.h
internet/spotify/spotifyservice.h
internet/spotify/spotifysettingspage.h
internet/spotifywebapi/spotifywebapiservice.h
UI
internet/spotify/spotifysettingspage.ui
)
if(HAVE_SPOTIFY)
optional_source(HAVE_SPOTIFY_DOWNLOADER
SOURCES
internet/spotify/spotifyblobdownloader.cpp
HEADERS
internet/spotify/spotifyblobdownloader.h
INCLUDE_DIRECTORIES
${CRYPTOPP_INCLUDE_DIRS}
)
endif(HAVE_SPOTIFY)
# Platform specific - OS X
optional_source(APPLE
INCLUDE_DIRECTORIES
@ -1303,7 +1268,6 @@ target_link_libraries(clementine_lib
${SQLITE_LIBRARIES}
Qocoa
z
)
link_directories(
@ -1353,17 +1317,6 @@ if(HAVE_BREAKPAD)
endif (LINUX)
endif(HAVE_BREAKPAD)
if(HAVE_SPOTIFY)
target_link_libraries(clementine_lib clementine-spotifyblob-messages)
endif(HAVE_SPOTIFY)
if(HAVE_SPOTIFY_DOWNLOADER)
target_link_libraries(clementine_lib
${CRYPTOPP_LIBRARIES}
)
link_directories(${CRYPTOPP_LIBRARY_DIRS})
endif(HAVE_SPOTIFY_DOWNLOADER)
if(HAVE_LIBPULSE)
target_link_libraries(clementine_lib ${LIBPULSE_LIBRARIES})
endif()
@ -1397,7 +1350,6 @@ target_link_libraries(clementine_lib qsqlite)
if (WIN32)
target_link_libraries(clementine_lib
protobuf
${ZLIB_LIBRARIES}
tinysvcmdns
dsound
)
@ -1451,9 +1403,6 @@ target_link_libraries(clementine
)
# macdeploy.py relies on the blob being built first.
if(HAVE_SPOTIFY_BLOB)
add_dependencies(clementine clementine-spotifyblob)
endif(HAVE_SPOTIFY_BLOB)
add_dependencies(clementine clementine-tagreader)
set_target_properties(clementine PROPERTIES

View File

@ -65,13 +65,15 @@ Analyzer::Base::Base(QWidget* parent, uint scopeSize)
timeout_(40), // msec
fht_(new FHT(scopeSize)),
engine_(nullptr),
lastScope_(512),
lastScope_(),
new_frame_(false),
is_playing_(false),
barkband_table_(),
prev_color_index_(0),
bands_(0),
psychedelic_enabled_(false) {}
psychedelic_enabled_(false) {
lastScope_.resize(fht_->size());
}
void Analyzer::Base::hideEvent(QHideEvent*) { timer_.stop(); }

View File

@ -5,6 +5,7 @@
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2022, Andrew Reading <andrew@areading.me>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -34,32 +35,32 @@
const uint BlockAnalyzer::kHeight = 2;
const uint BlockAnalyzer::kWidth = 4;
const uint BlockAnalyzer::kMinRows = 3; // arbituary
const uint BlockAnalyzer::kMinColumns = 32; // arbituary
const uint BlockAnalyzer::kMinRows = 3; // arbitrary
const uint BlockAnalyzer::kMaxRows = 256; // arbitrary
const uint BlockAnalyzer::kMinColumns = 32; // arbitrary
const uint BlockAnalyzer::kMaxColumns = 256; // must be 2**n
const uint BlockAnalyzer::kFadeSize = 90;
const uint BlockAnalyzer::kFadeInitial = 32;
const char* BlockAnalyzer::kName =
QT_TRANSLATE_NOOP("AnalyzerContainer", "Block analyzer");
BlockAnalyzer::BlockAnalyzer(QWidget* parent)
: Analyzer::Base(parent, 9),
scope_(kMinColumns),
columns_(0),
rows_(0),
y_(0),
barPixmap_(1, 1),
topBarPixmap_(kWidth, kHeight),
scope_(kMinColumns),
store_(1 << 8, 0),
fade_bars_(kFadeSize),
fade_pos_(1 << 8, 50),
fade_intensity_(1 << 8, 32) {
canvas_(),
rthresh_(kMaxRows + 1, 0.f),
bg_grad_(kMaxRows + 1, 0),
fade_bars_(kFadeSize, 0),
bandinfo_(kMaxColumns) {
// Right and bottom edges are 1px padding.
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1);
// -1 is padding, no drawing takes place there
setMaximumWidth(kMaxColumns * (kWidth + 1) - 1);
// mxcl says null pixmaps cause crashes, so let's play it safe
for (uint i = 0; i < kFadeSize; ++i) fade_bars_[i] = QPixmap(1, 1);
setAttribute(Qt::WA_OpaquePaintEvent, true);
}
BlockAnalyzer::~BlockAnalyzer() {}
@ -67,56 +68,53 @@ BlockAnalyzer::~BlockAnalyzer() {}
void BlockAnalyzer::resizeEvent(QResizeEvent* e) {
QWidget::resizeEvent(e);
background_ = QPixmap(size());
canvas_ = QPixmap(size());
const uint oldRows = rows_;
uint newRows, newCols;
// all is explained in analyze()..
// +1 to counter -1 in maxSizes, trust me we need this!
columns_ = qMin(
static_cast<uint>(static_cast<double>(width() + 1) / (kWidth + 1)) + 1,
kMaxColumns);
rows_ = static_cast<uint>(static_cast<double>(height() + 1) / (kHeight + 1));
newCols = 1 + (width() + 1) / (kWidth + 1);
newRows = 0 + (height() + 1) / (kHeight + 1);
newCols = qMin(kMaxColumns, qMax(kMinColumns, newCols));
newRows = qMin(kMaxRows, qMax(kMinRows, newRows));
// this is the y-offset for drawing from the top of the widget
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
if (newCols != columns_) {
columns_ = newCols;
scope_.resize(columns_);
scope_.resize(columns_);
updateBandSize(columns_);
bandinfo_.fill(FHTBand());
}
if (rows_ != oldRows) {
barPixmap_ = QPixmap(kWidth, rows_ * (kHeight + 1));
if (rows_ != newRows) {
rows_ = newRows;
for (uint i = 0; i < kFadeSize; ++i)
fade_bars_[i] = QPixmap(kWidth, rows_ * (kHeight + 1));
// this is the y-offset for drawing from the top of the widget
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
yscale_.resize(rows_ + 1);
const uint PRE = 1,
PRO = 1; // PRE and PRO allow us to restrict the range somewhat
const float PRE = 1.f,
PRO =
1.f, // PRE and PRO allow us to restrict the range somewhat
SCL = log10f(PRE + PRO + (1.f * rows_));
for (uint z = 0; z < rows_; ++z)
yscale_[z] = 1 - (log10(PRE + z) / log10(PRE + rows_ + PRO));
rthresh_[z] = 1.f - log10f(PRE + (1.f * z)) / SCL;
yscale_[rows_] = 0;
rthresh_[rows_] = 0.f;
determineStep();
paletteChange(palette());
}
updateBandSize(columns_);
drawBackground();
canvas_ = QImage(columns_ * (kWidth + 1), rows_ * (kHeight + 1),
QImage::Format_ARGB32_Premultiplied);
canvas_.fill(pad_color_);
}
void BlockAnalyzer::determineStep() {
// falltime is dependent on rowcount due to our digital resolution (ie we have
// boxes/blocks of pixels)
// I calculated the value 30 based on some trial and error
// falltime is dependent on rowcount
// the fall time of 30 is too slow on framerates above 50fps
const double fallTime = timeout() < 20 ? 20 * rows_ : 30 * rows_;
step_ = static_cast<double>(rows_ * timeout()) / fallTime;
const float rFallTime = 1.f / (timeout() < 20 ? 20.f : 30.f);
step_ = timeout() * rFallTime;
}
void BlockAnalyzer::framerateChanged() { // virtual
@ -124,10 +122,10 @@ void BlockAnalyzer::framerateChanged() { // virtual
}
void BlockAnalyzer::transform(Analyzer::Scope& s) {
for (uint x = 0; x < s.size(); ++x) s[x] *= 2;
for (uint x = 0; x < s.size(); ++x) s[x] *= 2.f;
fht_->spectrum(s.data());
fht_->scale(s.data(), 1.0 / 20);
fht_->scale(s.data(), 1.f / 20.f);
// the second half is pretty dull, so only show it if the user has a large
// analyzer
@ -138,77 +136,225 @@ void BlockAnalyzer::transform(Analyzer::Scope& s) {
void BlockAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
bool new_frame) {
// y = 2 3 2 1 0 2
// . . . . # .
// . . . # # .
// # . # # # #
// # # # # # #
//
// visual aid for how this analyzer works.
// y represents the number of blanks
// y starts from the top and increases in units of blocks
float yf;
uint x, y;
// yscale_ looks similar to: { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
// if it contains 6 elements there are 5 rows in the analyzer
if (p.paintEngine() == 0) return;
if (canvas_.isNull()) return;
p.setCompositionMode(QPainter::CompositionMode_Source);
if (!new_frame) {
p.drawPixmap(0, 0, canvas_);
p.drawImage(0, 0, canvas_, 0, 0, width(), height(), Qt::NoFormatConversion);
return;
}
QPainter canvas_painter(&canvas_);
Analyzer::interpolate(s, scope_);
// update the graphics with the new colour
if (psychedelic_enabled_) {
paletteChange(QPalette());
}
// Update the color palettes.
if (psychedelic_enabled_) paletteChange(QPalette());
// Paint the background
canvas_painter.drawPixmap(0, 0, background_);
// Visual Aid
//
// This analyzer maintains a list of intensity thresholds for each row of
// the analyzer. For each frequency band (represented column-wise, one per
// band), the spectral power calculation obtained from the analyzer scope
// output is compared against these thresholds to determine the row indices
// at which the regions become active. While inactive regions are dark,
// active regions and all those below the corresponding transition region
// are "lit up".
//
// So, where
// . indicates block is inactive/dark
// # indicates block is active /lit,
// what is drawn is (for example)
//
// COLUMNS/Bands
// . . . . # . R
// . . . # # . O
// # . # # # # W
// # # # # # # S
//
// y = 2 3 2 1 0 2
//
// Here y is the row index for which the intensity threshold is met, with
// 0 indicating the topmost row. The nRows+1 intensity values are stored
// in rthresh_[], sorted in decreasing order (the top, y=0 region would
// be the most spectrally intense); the additional, final value is always
// zero and exists mostly as a sort of loop optimization.
//
// For the above illustration, rthresh_[] might have values similar to
// { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
//
// Now, consider two "frames" that occur sequentially after each other. Where
// . indicates block is inactive/dark
// o indicates block is inactive/dark and fading out (was active)
// # indicates block is active /lit,
// [ ] indicates block is the bar topper
//
// frame 1 ====> frame 2
// COLUMNS/Bands COLUMNS/Bands
// . . . . [#] . R . . . . o .
// . . . [#] # . O . . [#] o [#] .
// [#] . [#] # # [#] W o . # o # [#]
// # [#] # # # # S [#] o # [#] # #
//
// 2 3 2 1 0 2 = B_y = 3 4 1 1 1 2
//
// After a previously active region becomes inactive, for a period of time
// it is drawn in a color that darkens over time. These are based upon the
// the current color scheme and get stored within fade_bars_[].
// Additionally, a rowwise gradient is applied to active bands to help keep
// the spectrum display visually interesting, with colors darkening as
// intensities decrease -- that is, as rthresh_[] values decrease. The
// inactive-active transition area is drawn with the brightest color and
// acts as a "bar topper"; this topper should visually rise and fall over
// time.
//
// As in the transition example above, bands (columns) are drawn vertically
// from top to bottom, progressing from left to right. Supposing Y_r is the
// row coordinate, B_y is the band coordinate, and
// 0 <= Y_r,B_y < nRows <= kMaxRows,
// the drawing procedure for each band can be described as follows:
// a. Y_r < B_y
// First the '.' regions that have not been recently active are
// darkened (background). This is determined via the band's
// fade_intensity and fade_row values.
// b. Y_r < B_y
// Recently active areas are drawn using a special darkening-fade
// color, until either some number of frames have elapsed or they
// became active since the countdown began.
// c. Y_r = B_y
// The transition region is drawn as a bar topper.
// d. Y_r > B_y < nRows
// Each subsequent region below the transition region should be active.
// Draw these using a gradient that darkens as Y_r -> nRows.
// The logic for these can be found in the colorFromRowAndBand() function.
//
for (uint y, x = 0; x < scope_.size(); ++x) {
// determine y
for (y = 0; scope_[x] < yscale_[y]; ++y) continue;
// Update band information.
for (x = 0; x < scope_.size(); ++x) {
const float& bandthr = scope_[x];
FHTBand& band = bandinfo_[x];
// this is opposite to what you'd think, higher than y
// means the bar is lower than y (physically)
if (static_cast<float>(y) > store_[x])
y = static_cast<int>(store_[x] += step_);
else
store_[x] = y;
// if y is lower than fade_pos_, then the bar has exceeded the kHeight of
// the fadeout
// if the fadeout is quite faded now, then display the new one
if (y <= fade_pos_[x] /*|| fade_intensity_[x] < kFadeSize / 3*/) {
fade_pos_[x] = y;
fade_intensity_[x] = kFadeSize;
// Calculate activity transition row values.
// Note: rows_ < rthresh_.size()
for (y = 0; y < rows_; ++y) {
if (bandthr >= rthresh_[y]) break;
}
if (fade_intensity_[x] > 0) {
const uint offset = --fade_intensity_[x];
const uint y = y_ + (fade_pos_[x] * (kHeight + 1));
canvas_painter.drawPixmap(x * (kWidth + 1), y, fade_bars_[offset], 0, 0,
kWidth, height() - y);
// y <= band height :: band matches or exceeds power from last frame.
// y > band height :: band lost power since last frame.
if ((yf = 1.f * y) <= band.height) {
band.height = yf;
band.row = y;
} else {
// This band has lost power since the last-recorded maximal threshold
// value. Gradually decrease this until it meets the current value.
band.height += step_;
band.row = y = static_cast<uint>(band.height);
}
if (fade_intensity_[x] == 0) fade_pos_[x] = rows_;
// y <= band fade_row :: the current threshold exceeds the previously-
// marked position in which to begin fade-out. Use the current position
// as a new marker and start/restart fade_intensity, the fade-out period
// counter.
if (y <= band.fade_row) {
band.fade_row = y;
band.fade_intensity = kFadeSize;
}
// REMEMBER: y is a number from 0 to rows_, 0 means all blocks are glowing,
// rows_ means none are
canvas_painter.drawPixmap(x * (kWidth + 1), y * (kHeight + 1) + y_, *bar(),
0, y * (kHeight + 1), bar()->width(),
bar()->height());
// Check the fade-out period counter. If expired (i.e., <= 0), the
// fade-out effect is complete. Otherwise, continue downcounting and
// select the next color for the fade-out sequence.
if (band.fade_intensity <= 0) {
// fade_intensity <= 0: Done with fade out effect (time expired).
band.fade_row = rows_;
band.fade_coloridx = 0;
} else {
// fade_intensity > 0: Continue effect; continue color change.
band.fade_coloridx = --band.fade_intensity;
}
}
for (uint x = 0; x < store_.size(); ++x)
canvas_painter.drawPixmap(x * (kWidth + 1),
static_cast<int>(store_[x]) * (kHeight + 1) + y_,
topBarPixmap_);
// A block will be drawn and colored according to each band (column) of
// the FHT spectrum data. This block is a kWidth x kHeight region, along
// with 1-px of padding on its right and bottom.
//
// Conditional (FHTBand) Block State / Color
// ===================== ===================
// 0 < y < fade_row & fade-out : Inactive / BG color
// fade_row < y < row & fade-out : Fade-out / darkening
// 0 < y < row & no fade-out : Inactive / BG color
// row == y : Threshold / FG color
// row < y < rows_ : Active / Vert. gradient
// {1-px padding region} : Padding / Pad color
//
p.drawPixmap(0, 0, canvas_);
//
// Paint the canvas in one go in order to mimize cache thrashing.
//
QRgb* line; // Current scanline.
uint px_w, px_h; // Current width and height in pixels (just to avoid cast).
uint to_x; // [0, width()) Current and ending x pixel coordinate.
uint to_y; // [0, height()) Current and ending y pixel coordinate.
uint blk_r; // [0, rows_) Current block's row.
uint blk_c; // [0, columns_) Current block's column.
quint32 padcolor = pad_color_.rgba();
quint32 blkcolor;
px_w = static_cast<uint>(width());
px_h = static_cast<uint>(height());
// Draw empty top padding, if needed (when y_ > 0. weird window size?).
for (y = 0; y < y_; ++y) {
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y));
for (x = 0; x < px_w; line[x++] = padcolor)
;
}
// Draw the texture in one shot, iterating in a row-major fashion.
for (blk_r = 0; blk_r < rows_; ++blk_r) {
to_y = qMin(y + kHeight, px_h);
// This block may take several 1-px high scanlines. Each column needs
// to be filled accordingly for each of these rows.
for (; y < to_y; ++y) {
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y));
for (x = 0, blk_c = 0; blk_c < columns_; ++blk_c) {
to_x = qMin(x + kWidth, px_w);
// Draw [x, to_x], then padding on the right.
blkcolor = colorFromRowAndBand(blk_r, bandinfo_[blk_c]);
for (; x < to_x; line[x++] = blkcolor)
;
if (x < px_w) line[x++] = padcolor;
}
// If extra space remains in line, fill to the right edge.
for (; x < px_w; line[x++] = padcolor)
;
}
// Draw a full line of padding below the just-drawn region (if in bounds).
if (y < px_h) {
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y++));
for (x = 0; x < px_w; line[x++] = padcolor)
;
}
}
// If not at bottom boundary yet, pad remaining lines.
while (y < px_h) {
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y++));
for (x = 0; x < px_w; line[x++] = padcolor)
;
}
p.drawImage(0, 0, canvas_, 0, 0, width(), height(), Qt::NoFormatConversion);
}
static inline void adjustToLimits(int& b, int& f, uint& amount) {
@ -248,7 +394,8 @@ void BlockAnalyzer::psychedelicModeChanged(bool enabled) {
* It won't modify the hue of fg unless absolutely necessary
* @return the adjusted form of fg
*/
QColor ensureContrast(const QColor& bg, const QColor& fg, uint _amount = 150) {
static QColor ensureContrast(const QColor& bg, const QColor& fg,
uint _amount = 150) {
class OutputOnExit {
public:
explicit OutputOnExit(const QColor& color) : c(color) {}
@ -335,85 +482,58 @@ QColor ensureContrast(const QColor& bg, const QColor& fg, uint _amount = 150) {
}
void BlockAnalyzer::paletteChange(const QPalette&) {
const QColor bg = palette().color(QPalette::Background);
QColor fg;
QColor bg, bgdark, fg;
if (psychedelic_enabled_) {
bg = palette().color(QPalette::Background);
bgdark = bg.darker(112);
if (psychedelic_enabled_)
fg = getPsychedelicColor(scope_, 10, 75);
} else {
else
fg = ensureContrast(bg, palette().color(QPalette::Highlight));
fg_color_ = fg;
bg_color_ = bgdark;
pad_color_ = bg;
// Calculate background gradient colors.
{
const float dr = 15.f * (bg.red() - fg.red()) / (16.f * rows_);
const float dg = 15.f * (bg.green() - fg.green()) / (16.f * rows_);
const float db = 15.f * (bg.blue() - fg.blue()) / (16.f * rows_);
for (uint y = 0; y < rows_; ++y) {
bg_grad_[y] = qRgba(fg.red() + static_cast<int>(dr * y),
fg.green() + static_cast<int>(dg * y),
fg.blue() + static_cast<int>(db * y), 255);
}
bg_grad_[rows_] = bg.rgba();
}
topBarPixmap_.fill(fg);
const double dr =
15 * static_cast<double>(bg.red() - fg.red()) / (rows_ * 16);
const double dg =
15 * static_cast<double>(bg.green() - fg.green()) / (rows_ * 16);
const double db =
15 * static_cast<double>(bg.blue() - fg.blue()) / (rows_ * 16);
const int r = fg.red(), g = fg.green(), b = fg.blue();
bar()->fill(bg);
QPainter p(bar());
for (int y = 0; static_cast<uint>(y) < rows_; ++y)
// graduate the fg color
p.fillRect(
0, y * (kHeight + 1), kWidth, kHeight,
QColor(r + static_cast<int>(dr * y), g + static_cast<int>(dg * y),
b + static_cast<int>(db * y)));
// make a complimentary fadebar colour
// TODO(John Maguire): dark is not always correct, dumbo!
{
const QColor bg = palette().color(QPalette::Background).darker(112);
// make a complimentary fadebar colour
// TODO(John Maguire): dark is not always correct, dumbo!
int h, s, v;
palette().color(QPalette::Background).darker(150).getHsv(&h, &s, &v);
const QColor fg(QColor::fromHsv(h + 120, s, v));
const double dr = fg.red() - bg.red();
const double dg = fg.green() - bg.green();
const double db = fg.blue() - bg.blue();
const int r = bg.red(), g = bg.green(), b = bg.blue();
bg.darker(150).getHsv(&h, &s, &v);
fg = QColor::fromHsv(h + 120, s, v);
const float r = 1.f * bgdark.red();
const float g = 1.f * bgdark.green();
const float b = 1.f * bgdark.blue();
const float dr = 1.f * fg.red() - r;
const float dg = 1.f * fg.green() - g;
const float db = 1.f * fg.blue() - b;
const float fFscl = 1. * kFadeSize;
const float frlogFscl = 1.f / log10f(fFscl);
// Precalculate all fade-bar pixmaps
for (uint y = 0; y < kFadeSize; ++y) {
fade_bars_[y].fill(palette().color(QPalette::Background));
QPainter f(&fade_bars_[y]);
for (int z = 0; static_cast<uint>(z) < rows_; ++z) {
const double Y = 1.0 - (log10(kFadeSize - y) / log10(kFadeSize));
f.fillRect(
0, z * (kHeight + 1), kWidth, kHeight,
QColor(r + static_cast<int>(dr * Y), g + static_cast<int>(dg * Y),
b + static_cast<int>(db * Y)));
}
const float lrY = 1.f - (frlogFscl * log10f(fFscl - y));
fade_bars_[y] =
qRgba(static_cast<int>(r + lrY * dr), static_cast<int>(g + lrY * dg),
static_cast<int>(b + lrY * db), 255);
}
}
drawBackground();
}
void BlockAnalyzer::drawBackground() {
if (background_.isNull()) {
return;
}
const QColor bg = palette().color(QPalette::Background);
const QColor bgdark = bg.darker(112);
background_.fill(bg);
QPainter p(&background_);
if (p.paintEngine() == 0) {
return;
}
for (int x = 0; (uint)x < columns_; ++x)
for (int y = 0; (uint)y < rows_; ++y)
p.fillRect(x * (kWidth + 1), y * (kHeight + 1) + y_, kWidth, kHeight,
bgdark);
}

View File

@ -4,6 +4,7 @@
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof A. Sobiecki <sobkas@gmail.com>
Copyright 2022, Andrew Reading <andrew@areading.me>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -43,9 +44,11 @@ class BlockAnalyzer : public Analyzer::Base {
static const uint kHeight;
static const uint kWidth;
static const uint kMinRows;
static const uint kMaxRows;
static const uint kMinColumns;
static const uint kMaxColumns;
static const uint kFadeSize;
static const uint kFadeInitial;
static const char* kName;
@ -57,27 +60,58 @@ class BlockAnalyzer : public Analyzer::Base {
virtual void framerateChanged();
virtual void psychedelicModeChanged(bool);
void drawBackground();
void determineStep();
private:
QPixmap* bar() { return &barPixmap_; }
struct FHTBand {
FHTBand()
: height(0.f),
row(0),
fade_row(0),
fade_coloridx(kMaxRows),
fade_intensity(kFadeInitial) {}
uint columns_, rows_; // number of rows and columns of blocks
uint y_; // y-offset from top of widget
QPixmap barPixmap_;
QPixmap topBarPixmap_;
QPixmap background_;
QPixmap canvas_;
Analyzer::Scope scope_; // so we don't create a vector every frame
QVector<float> store_; // current bar kHeights
QVector<float> yscale_;
// Top of the spectral activity bar.
float height; // Foreground-Background transition row.
uint row; // Integer floor of the height value.
QVector<QPixmap> fade_bars_;
QVector<uint> fade_pos_;
QVector<int> fade_intensity_;
// Vertical color fade effect (a sort of hysteresis).
uint fade_row; // Row in which to begin showing BG gradient.
uint fade_coloridx; // Current fade_bars_[] offset.
int fade_intensity; // Current intensity frame counter value.
};
float step_; // rows to fall per frame
inline quint32 colorFromRowAndBand(uint cur_r, const FHTBand& band);
Analyzer::Scope scope_;
uint columns_; // Number of columns of blocks.
uint rows_; // Number of rows of blocks.
uint y_; // y-offset from top of widget.
float step_; // Rows to fall per frame (during inactivity).
QColor fg_color_; // Foreground/Active block color.
QColor bg_color_; // Background/Inactive block color.
QColor pad_color_; // Color of 'lines' dividing the blocks.
QImage canvas_; // Drawable canvas of widget.
QVector<float> rthresh_; // [rows_+1] Rowwise intensity thresholds.
QVector<quint32> bg_grad_; // [rows_+1] Vertical background gradient.
QVector<quint32> fade_bars_; // [kFadeSize] Block colors per fade level.
QVector<FHTBand> bandinfo_; // [columns_] FHT band info.
};
inline quint32 BlockAnalyzer::colorFromRowAndBand(uint r, const FHTBand& band) {
// Calculate the block color given band info and the current row.
// Note: 0 <= r <= rows_.
if (r == band.row)
return fg_color_.rgba();
else if (r > band.row)
return bg_grad_[r];
else if ((band.fade_intensity > 0) && (r >= band.fade_row))
return fade_bars_[band.fade_coloridx];
else
return bg_color_.rgba();
}
#endif // ANALYZERS_BLOCKANALYZER_H_

View File

@ -27,7 +27,7 @@
#include "core/song.h"
class TranscoderPreset;
struct TranscoderPreset;
class OrganiseFormat {
public:

View File

@ -34,9 +34,6 @@
#include "core/tagreaderclient.h"
#include "core/utilities.h"
#include "internet/core/internetmodel.h"
#ifdef HAVE_SPOTIFY
#include "internet/spotify/spotifyservice.h"
#endif
AlbumCoverLoader::AlbumCoverLoader(QObject* parent)
: QObject(parent),
@ -180,31 +177,7 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(
remote_tasks_.insert(reply, task);
return TryLoadResult(true, false, QImage());
}
#ifdef HAVE_SPOTIFY
else if (filename.toLower().startsWith("spotify://image/")) {
// HACK: we should add generic image URL handlers
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
if (!connected_spotify_) {
connect(spotify, SIGNAL(ImageLoaded(QString, QImage)),
SLOT(SpotifyImageLoaded(QString, QImage)));
connected_spotify_ = true;
}
QString id = QUrl(filename).path();
if (id.startsWith('/')) {
id.remove(0, 1);
}
remote_spotify_tasks_.insert(id, task);
// Need to schedule this in the spotify service's thread
QMetaObject::invokeMethod(spotify, "LoadImage", Qt::QueuedConnection,
Q_ARG(QString, id));
return TryLoadResult(true, false, QImage());
}
#endif
else if (filename.isEmpty()) {
} else if (filename.isEmpty()) {
// Avoid "QFSFileEngine::open: No file name specified" messages if we know
// that the filename is empty
return TryLoadResult(false, false, task.options.default_output_image_);
@ -216,18 +189,6 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(
image.isNull() ? task.options.default_output_image_ : image);
}
#ifdef HAVE_SPOTIFY
void AlbumCoverLoader::SpotifyImageLoaded(const QString& id,
const QImage& image) {
if (!remote_spotify_tasks_.contains(id)) return;
Task task = remote_spotify_tasks_.take(id);
QImage scaled = ScaleAndPad(task.options, image);
emit ImageLoaded(task.id, scaled);
emit ImageLoaded(task.id, scaled, image);
}
#endif
void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply* reply) {
reply->deleteLater();

View File

@ -67,9 +67,6 @@ class AlbumCoverLoader : public QObject {
protected slots:
void ProcessTasks();
void RemoteFetchFinished(QNetworkReply* reply);
#ifdef HAVE_SPOTIFY
void SpotifyImageLoaded(const QString& url, const QImage& image);
#endif
protected:
enum State {

View File

@ -39,10 +39,6 @@
#include "devices/cddadevice.h"
#endif
#include "internet/core/internetmodel.h"
#ifdef HAVE_SPOTIFY
#include "internet/spotify/spotifyserver.h"
#include "internet/spotify/spotifyservice.h"
#endif
const int GstEnginePipeline::kGstStateTimeoutNanosecs = 10000000;
const int GstEnginePipeline::kFaderFudgeMsec = 2000;
@ -171,58 +167,14 @@ QByteArray GstEnginePipeline::GstUriFromUrl(const QUrl& url) {
GstElement* GstEnginePipeline::CreateDecodeBinFromUrl(const QUrl& url) {
GstElement* new_bin = nullptr;
#ifdef HAVE_SPOTIFY
if (url.scheme() == "spotify") {
new_bin = gst_bin_new("spotify_bin");
if (!new_bin) return nullptr;
// Create elements
GstElement* src = engine_->CreateElement("tcpserversrc", new_bin);
if (!src) {
gst_object_unref(GST_OBJECT(new_bin));
return nullptr;
}
GstElement* gdp = engine_->CreateElement("gdpdepay", new_bin);
if (!gdp) {
gst_object_unref(GST_OBJECT(new_bin));
return nullptr;
}
// Pick a port number
const int port = Utilities::PickUnusedPort();
g_object_set(G_OBJECT(src), "host", "127.0.0.1", nullptr);
g_object_set(G_OBJECT(src), "port", port, nullptr);
// Link the elements
gst_element_link(src, gdp);
// Add a ghost pad
GstPad* pad = gst_element_get_static_pad(gdp, "src");
gst_element_add_pad(GST_ELEMENT(new_bin), gst_ghost_pad_new("src", pad));
gst_object_unref(GST_OBJECT(pad));
// Tell spotify to start sending data to us.
SpotifyServer* spotify_server =
InternetModel::Service<SpotifyService>()->server();
// Need to schedule this in the spotify server's thread
QMetaObject::invokeMethod(
spotify_server, "StartPlayback", Qt::QueuedConnection,
Q_ARG(QString, url.toString()), Q_ARG(quint16, port));
} else {
#endif
QByteArray uri = GstUriFromUrl(url);
new_bin = engine_->CreateElement("uridecodebin");
if (!new_bin) return nullptr;
g_object_set(G_OBJECT(new_bin), "uri", uri.constData(), nullptr);
CHECKED_GCONNECT(G_OBJECT(new_bin), "drained", &SourceDrainedCallback,
this);
CHECKED_GCONNECT(G_OBJECT(new_bin), "pad-added", &NewPadCallback, this);
CHECKED_GCONNECT(G_OBJECT(new_bin), "notify::source", &SourceSetupCallback,
this);
#ifdef HAVE_SPOTIFY
}
#endif
QByteArray uri = GstUriFromUrl(url);
new_bin = engine_->CreateElement("uridecodebin");
if (!new_bin) return nullptr;
g_object_set(G_OBJECT(new_bin), "uri", uri.constData(), nullptr);
CHECKED_GCONNECT(G_OBJECT(new_bin), "drained", &SourceDrainedCallback, this);
CHECKED_GCONNECT(G_OBJECT(new_bin), "pad-added", &NewPadCallback, this);
CHECKED_GCONNECT(G_OBJECT(new_bin), "notify::source", &SourceSetupCallback,
this);
return new_bin;
}
@ -1199,26 +1151,6 @@ GstState GstEnginePipeline::state() const {
}
QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(GstState state) {
#ifdef HAVE_SPOTIFY
if (current_.url_.scheme() == "spotify" && !buffering_) {
const GstState current_state = this->state();
if (state == GST_STATE_PAUSED && current_state == GST_STATE_PLAYING) {
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
// Need to schedule this in the spotify service's thread
QMetaObject::invokeMethod(spotify, "SetPaused", Qt::QueuedConnection,
Q_ARG(bool, true));
} else if (state == GST_STATE_PLAYING &&
current_state == GST_STATE_PAUSED) {
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
// Need to schedule this in the spotify service's thread
QMetaObject::invokeMethod(spotify, "SetPaused", Qt::QueuedConnection,
Q_ARG(bool, false));
}
}
#endif
return ConcurrentRun::Run<GstStateChangeReturn, GstElement*, GstState>(
&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
}

View File

@ -1,280 +0,0 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "spotifysearchprovider.h"
#include <ctime>
#include <random>
#include "core/logging.h"
#include "internet/core/internetmodel.h"
#include "internet/spotify/spotifyserver.h"
#include "playlist/songmimedata.h"
#include "ui/iconloader.h"
namespace {
const int kSearchSongLimit = 5;
const int kSearchAlbumLimit = 20;
} // namespace
SpotifySearchProvider::SpotifySearchProvider(Application* app, QObject* parent)
: SearchProvider(app, parent), server_(nullptr), service_(nullptr) {
Init("Spotify", "spotify", IconLoader::Load("spotify", IconLoader::Provider),
WantsDelayedQueries | WantsSerialisedArtQueries | ArtIsProbablyRemote |
CanShowConfig | CanGiveSuggestions);
}
SpotifyServer* SpotifySearchProvider::server() {
if (server_) return server_;
if (!service_) service_ = InternetModel::Service<SpotifyService>();
if (service_->login_state() != SpotifyService::LoginState_LoggedIn)
return nullptr;
if (!service_->IsBlobInstalled()) return nullptr;
server_ = service_->server();
connect(server_, SIGNAL(SearchResults(cpb::spotify::SearchResponse)),
SLOT(SearchFinishedSlot(cpb::spotify::SearchResponse)));
connect(server_, SIGNAL(ImageLoaded(QString, QImage)),
SLOT(ArtLoadedSlot(QString, QImage)));
connect(server_, SIGNAL(destroyed()), SLOT(ServerDestroyed()));
connect(server_, SIGNAL(StarredLoaded(cpb::spotify::LoadPlaylistResponse)),
SLOT(SuggestionsLoaded(cpb::spotify::LoadPlaylistResponse)));
connect(server_,
SIGNAL(ToplistBrowseResults(cpb::spotify::BrowseToplistResponse)),
SLOT(SuggestionsLoaded(cpb::spotify::BrowseToplistResponse)));
return server_;
}
void SpotifySearchProvider::ServerDestroyed() { server_ = nullptr; }
void SpotifySearchProvider::SearchAsync(int id, const QString& query) {
SpotifyServer* s = server();
if (!s) {
emit SearchFinished(id);
return;
}
PendingState state;
state.orig_id_ = id;
state.tokens_ = TokenizeQuery(query);
const QString query_string = state.tokens_.join(" ");
s->Search(query_string, kSearchSongLimit, kSearchAlbumLimit);
queries_[query_string] = state;
}
void SpotifySearchProvider::SearchFinishedSlot(
const cpb::spotify::SearchResponse& response) {
QString query_string = QString::fromUtf8(response.request().query().c_str());
QMap<QString, PendingState>::iterator it = queries_.find(query_string);
if (it == queries_.end()) return;
PendingState state = it.value();
queries_.erase(it);
/* Here we clean up Spotify's results for our purposes
*
* Since Spotify doesn't give us an album artist,
* we pick one, so there's a single album artist
* per album to use for sorting.
*
* We also drop any of the single tracks returned
* if they are already represented in an album
*
* This eliminates frequent duplicates from the
* "Top Tracks" results that Spotify sometimes
* returns
*/
QMap<std::string, std::string> album_dedup;
ResultList ret;
for (int i = 0; i < response.album_size(); ++i) {
const cpb::spotify::Album& album = response.album(i);
QHash<QString, int> artist_count;
QString majority_artist;
int majority_count = 0;
/* We go through and find the artist that is
* represented most frequently in the artist
*
* For most albums this will just be one artist,
* but this ensures we have a consistent album artist for
* soundtracks, compilations, contributing artists, etc
*/
for (int j = 0; j < album.track_size(); ++j) {
// Each track can have multiple artists attributed, check them all
for (int k = 0; k < album.track(j).artist_size(); ++k) {
QString artist = QStringFromStdString(album.track(j).artist(k));
if (artist_count.contains(artist)) {
artist_count[artist]++;
} else {
artist_count[artist] = 1;
}
if (artist_count[artist] > majority_count) {
majority_count = artist_count[artist];
majority_artist = artist;
}
}
}
for (int j = 0; j < album.track_size(); ++j) {
// Insert the album/track title into the dedup map
// so we can check tracks against it below
album_dedup.insertMulti(album.track(j).album(), album.track(j).title());
Result result(this);
SpotifyService::SongFromProtobuf(album.track(j), &result.metadata_);
// Just use the album index as an id.
result.metadata_.set_album_id(i);
result.metadata_.set_albumartist(majority_artist);
ret << result;
}
}
for (int i = 0; i < response.result_size(); ++i) {
const cpb::spotify::Track& track = response.result(i);
// Check this track/album against tracks we've already seen
// in the album results, and skip if it's a duplicate
if (album_dedup.contains(track.album()) &&
album_dedup.values(track.album()).contains(track.title())) {
continue;
}
Result result(this);
SpotifyService::SongFromProtobuf(track, &result.metadata_);
ret << result;
}
emit ResultsAvailable(state.orig_id_, ret);
emit SearchFinished(state.orig_id_);
}
void SpotifySearchProvider::LoadArtAsync(int id, const Result& result) {
SpotifyServer* s = server();
if (!s) {
emit ArtLoaded(id, QImage());
return;
}
QString image_id = QUrl(result.metadata_.art_automatic()).path();
if (image_id.startsWith('/')) image_id.remove(0, 1);
pending_art_[image_id] = id;
s->LoadImage(image_id);
}
void SpotifySearchProvider::ArtLoadedSlot(const QString& id,
const QImage& image) {
QMap<QString, int>::iterator it = pending_art_.find(id);
if (it == pending_art_.end()) return;
const int orig_id = it.value();
pending_art_.erase(it);
emit ArtLoaded(orig_id, ScaleAndPad(image));
}
bool SpotifySearchProvider::IsLoggedIn() {
if (server()) {
return service_->IsLoggedIn();
}
return false;
}
void SpotifySearchProvider::ShowConfig() {
if (service_) {
return service_->ShowConfig();
}
}
void SpotifySearchProvider::AddSuggestionFromTrack(
const cpb::spotify::Track& track) {
if (!track.title().empty()) {
suggestions_.insert(QString::fromUtf8(track.title().c_str()));
}
for (int j = 0; j < track.artist_size(); ++j) {
if (!track.artist(j).empty()) {
suggestions_.insert(QString::fromUtf8(track.artist(j).c_str()));
}
}
if (!track.album().empty()) {
suggestions_.insert(QString::fromUtf8(track.album().c_str()));
}
}
void SpotifySearchProvider::AddSuggestionFromAlbum(
const cpb::spotify::Album& album) {
AddSuggestionFromTrack(album.metadata());
for (int i = 0; i < album.track_size(); ++i) {
AddSuggestionFromTrack(album.track(i));
}
}
void SpotifySearchProvider::SuggestionsLoaded(
const cpb::spotify::LoadPlaylistResponse& playlist) {
for (int i = 0; i < playlist.track_size(); ++i) {
AddSuggestionFromTrack(playlist.track(i));
}
}
void SpotifySearchProvider::SuggestionsLoaded(
const cpb::spotify::BrowseToplistResponse& response) {
for (int i = 0; i < response.track_size(); ++i) {
AddSuggestionFromTrack(response.track(i));
}
for (int i = 0; i < response.album_size(); ++i) {
AddSuggestionFromAlbum(response.album(i));
}
}
void SpotifySearchProvider::LoadSuggestions() {
if (!server()) {
return;
}
server()->LoadStarred();
server()->LoadToplist();
}
QStringList SpotifySearchProvider::GetSuggestions(int count) {
if (suggestions_.empty()) {
LoadSuggestions();
return QStringList();
}
QStringList all_suggestions = suggestions_.values();
std::mt19937 gen(std::time(0));
std::uniform_int_distribution<> random(0, all_suggestions.size() - 1);
QSet<QString> candidates;
const int max = qMin(count, all_suggestions.size());
while (candidates.size() < max) {
const int index = random(gen);
candidates.insert(all_suggestions[index]);
}
return candidates.toList();
}

View File

@ -1,67 +0,0 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SPOTIFYSEARCHPROVIDER_H
#define SPOTIFYSEARCHPROVIDER_H
#include "internet/spotify/spotifyservice.h"
#include "searchprovider.h"
#include "spotifymessages.pb.h"
class SpotifyServer;
class SpotifySearchProvider : public SearchProvider {
Q_OBJECT
public:
SpotifySearchProvider(Application* app, QObject* parent = nullptr);
void SearchAsync(int id, const QString& query) override;
void LoadArtAsync(int id, const Result& result) override;
QStringList GetSuggestions(int count) override;
// SearchProvider
bool IsLoggedIn() override;
void ShowConfig() override;
InternetService* internet_service() override { return service_; }
private slots:
void ServerDestroyed();
void SearchFinishedSlot(const cpb::spotify::SearchResponse& response);
void ArtLoadedSlot(const QString& id, const QImage& image);
void SuggestionsLoaded(const cpb::spotify::LoadPlaylistResponse& response);
void SuggestionsLoaded(const cpb::spotify::BrowseToplistResponse& response);
private:
SpotifyServer* server();
void LoadSuggestions();
void AddSuggestionFromTrack(const cpb::spotify::Track& track);
void AddSuggestionFromAlbum(const cpb::spotify::Album& album);
private:
SpotifyServer* server_;
SpotifyService* service_;
QMap<QString, PendingState> queries_;
QMap<QString, int> pending_art_;
QMap<QString, int> pending_tracks_;
QSet<QString> suggestions_;
};
#endif // SPOTIFYSEARCHPROVIDER_H

View File

@ -1,97 +0,0 @@
/* This file is part of Clementine.
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "spotifywebapisearchprovider.h"
#include <qurl.h>
#include <iostream>
#include "internet/spotifywebapi/spotifywebapiservice.h"
#include "ui/iconloader.h"
namespace {
static constexpr int kNoRunningSearch = -1;
}
SpotifyWebApiSearchProvider::SpotifyWebApiSearchProvider(
Application* app, SpotifyWebApiService* parent)
: SearchProvider(app, parent),
parent_{parent},
last_search_id_{kNoRunningSearch} {
Init("Spotify (Experimential)", "spotify_web_api",
IconLoader::Load("spotify", IconLoader::Provider),
WantsDelayedQueries | WantsSerialisedArtQueries | ArtIsProbablyRemote |
CanGiveSuggestions);
connect(parent, &SpotifyWebApiService::SearchFinished, this,
&SpotifyWebApiSearchProvider::SearchFinishedSlot);
}
void SpotifyWebApiSearchProvider::SearchAsync(int id, const QString& query) {
if (last_search_id_ != kNoRunningSearch) {
// Cancel last pending search
emit SearchFinished(last_search_id_);
// Set the pending query
last_search_id_ = id;
last_query_ = query;
// And wait for the current search to be completed
return;
}
last_search_id_ = id;
last_query_ = query;
parent_->Search(last_search_id_, last_query_);
}
void SpotifyWebApiSearchProvider::SearchFinishedSlot(
int searchId, const QList<Song>& apiResult) {
ResultList ret;
for (auto&& item : apiResult) {
Result result{this};
result.group_automatically_ = true;
result.metadata_ = item;
ret += result;
}
emit ResultsAvailable(searchId, ret);
emit SearchFinished(searchId);
// Search again if we have a pending query
if (searchId != last_search_id_) {
parent_->Search(last_search_id_, last_query_);
} else {
last_search_id_ = kNoRunningSearch;
}
}
void SpotifyWebApiSearchProvider::LoadArtAsync(int id, const Result& result) {
// TODO
}
void SpotifyWebApiSearchProvider::ShowConfig() {}
InternetService* SpotifyWebApiSearchProvider::internet_service() {
return parent_;
}
QStringList SpotifyWebApiSearchProvider::GetSuggestions(int count) {
// TODO
return QStringList{};
}

View File

@ -1,48 +0,0 @@
/* This file is part of Clementine.
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SPOTIFYWEBAPISEARCHPROVIDER_H
#define SPOTIFYWEBAPISEARCHPROVIDER_H
#include "core/song.h"
#include "searchprovider.h"
class SpotifyWebApiService;
class SpotifyWebApiSearchProvider : public SearchProvider {
Q_OBJECT
public:
SpotifyWebApiSearchProvider(Application* app, SpotifyWebApiService* parent);
void SearchAsync(int id, const QString& query) override;
void LoadArtAsync(int id, const Result& result) override;
QStringList GetSuggestions(int count) override;
void ShowConfig() override;
InternetService* internet_service() override;
private slots:
void SearchFinishedSlot(int id, const QList<Song>&);
private:
SpotifyWebApiService* parent_;
int last_search_id_;
QString last_query_;
};
#endif // SPOTIFYWEBAPISEARCHPROVIDER_H

View File

@ -60,10 +60,6 @@
#ifdef HAVE_SEAFILE
#include "internet/seafile/seafileservice.h"
#endif
#ifdef HAVE_SPOTIFY
#include "internet/spotify/spotifyservice.h"
#include "internet/spotifywebapi/spotifywebapiservice.h"
#endif
using smart_playlists::Generator;
using smart_playlists::GeneratorMimeData;
@ -99,10 +95,6 @@ InternetModel::InternetModel(Application* app, QObject* parent)
AddService(new SomaFMService(app, this));
AddService(new IntergalacticFMService(app, this));
AddService(new RadioBrowserService(app, this));
#ifdef HAVE_SPOTIFY
AddService(new SpotifyService(app, this));
AddService(new SpotifyWebApiService(app, this));
#endif
AddService(new SubsonicService(app, this));
#ifdef HAVE_BOX
AddService(new BoxService(app, this));

View File

@ -259,7 +259,7 @@ RockRadioService::RockRadioService(Application* app, InternetModel* model,
: DigitallyImportedServiceBase(
"RockRadio", "ROCKRADIO.com", QUrl("http://www.rockradio.com"),
IconLoader::Load("rockradio", IconLoader::Provider), "rockradio", app,
model, false, parent) {}
model, true, parent) {}
ClassicalRadioService::ClassicalRadioService(Application* app,
InternetModel* model,
@ -268,4 +268,4 @@ ClassicalRadioService::ClassicalRadioService(Application* app,
"ClassicalRadio", "ClassicalRadio.com",
QUrl("http://www.classicalradio.com"),
IconLoader::Load("digitallyimported", IconLoader::Provider),
"classicalradio", app, model, false, parent) {}
"classicalradio", app, model, true, parent) {}

View File

@ -41,16 +41,16 @@ class DigitallyImportedServiceBase : public InternetService {
const QString& api_service_name,
Application* app, InternetModel* model,
bool has_premium, QObject* parent = nullptr);
~DigitallyImportedServiceBase();
~DigitallyImportedServiceBase() override;
static const char* kSettingsGroup;
static const int kStreamsCacheDurationSecs;
QStandardItem* CreateRootItem();
void LazyPopulate(QStandardItem* parent);
void ShowContextMenu(const QPoint& global_pos);
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* parent) override;
void ShowContextMenu(const QPoint& global_pos) override;
void ReloadSettings();
void ReloadSettings() override;
bool is_premium_account() const;

View File

@ -40,7 +40,7 @@ class IcecastService : public InternetService {
public:
IcecastService(Application* app, InternetModel* parent);
~IcecastService();
~IcecastService() override;
static const char* kServiceName;
static const char* kDirectoryUrl;
@ -51,12 +51,12 @@ class IcecastService : public InternetService {
Type_Genre,
};
QStandardItem* CreateRootItem();
void LazyPopulate(QStandardItem* item);
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* item) override;
void ShowContextMenu(const QPoint& global_pos);
void ShowContextMenu(const QPoint& global_pos) override;
QWidget* HeaderWidget() const;
QWidget* HeaderWidget() const override;
private slots:
void LoadDirectory();

View File

@ -39,7 +39,7 @@ class IntergalacticFMServiceBase : public InternetService {
const QString& name, const QUrl& channel_list_url,
const QUrl& homepage_url,
const QUrl& donate_page_url, const QIcon& icon);
~IntergalacticFMServiceBase();
~IntergalacticFMServiceBase() override;
enum ItemType {
Type_Stream = 2000,
@ -59,14 +59,14 @@ class IntergalacticFMServiceBase : public InternetService {
const QString& url_scheme() const { return url_scheme_; }
const QIcon& icon() const { return icon_; }
QStandardItem* CreateRootItem();
void LazyPopulate(QStandardItem* item);
void ShowContextMenu(const QPoint& global_pos);
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* item) override;
void ShowContextMenu(const QPoint& global_pos) override;
PlaylistItem::Options playlistitem_options() const;
PlaylistItem::Options playlistitem_options() const override;
QNetworkAccessManager* network() const { return network_; }
void ReloadSettings();
void ReloadSettings() override;
bool IsStreamListStale() const { return streams_.IsStale(); }
StreamList Streams();

View File

@ -34,7 +34,7 @@ class SavedRadio : public InternetService {
public:
SavedRadio(Application* app, InternetModel* parent);
~SavedRadio();
~SavedRadio() override;
enum ItemType {
Type_Stream = 2000,
@ -57,10 +57,10 @@ class SavedRadio : public InternetService {
static const char* kServiceName;
static const char* kSettingsGroup;
QStandardItem* CreateRootItem();
void LazyPopulate(QStandardItem* item);
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* item) override;
void ShowContextMenu(const QPoint& global_pos);
void ShowContextMenu(const QPoint& global_pos) override;
void Add(const QUrl& url, const QString& name = QString(),
const QUrl& url_logo = QUrl());

View File

@ -43,14 +43,14 @@ class JamendoService : public InternetService {
public:
JamendoService(Application* app, InternetModel* parent);
~JamendoService();
~JamendoService() override;
QStandardItem* CreateRootItem();
void LazyPopulate(QStandardItem* item);
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* item) override;
void ShowContextMenu(const QPoint& global_pos);
void ShowContextMenu(const QPoint& global_pos) override;
QWidget* HeaderWidget() const;
QWidget* HeaderWidget() const override;
LibraryBackend* library_backend() const { return library_backend_.get(); }

View File

@ -38,7 +38,7 @@ class MagnatuneService : public InternetService {
public:
MagnatuneService(Application* app, InternetModel* parent);
~MagnatuneService();
~MagnatuneService() override;
// Values are saved in QSettings and are indices into the combo box in
// MagnatuneConfig
@ -71,14 +71,14 @@ class MagnatuneService : public InternetService {
static QString ReadElementText(QXmlStreamReader& reader);
QStandardItem* CreateRootItem();
void LazyPopulate(QStandardItem* item);
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* item) override;
void ShowContextMenu(const QPoint& global_pos);
void ShowContextMenu(const QPoint& global_pos) override;
QWidget* HeaderWidget() const;
QWidget* HeaderWidget() const override;
void ReloadSettings();
void ReloadSettings() override;
// Magnatune specific stuff
MembershipType membership_type() const { return membership_; }
@ -99,7 +99,7 @@ class MagnatuneService : public InternetService {
void Download();
void Homepage();
void ShowConfig();
void ShowConfig() override;
private:
void EnsureMenuCreated();

View File

@ -44,7 +44,7 @@ class PodcastService : public InternetService {
public:
PodcastService(Application* app, InternetModel* parent);
~PodcastService();
~PodcastService() override;
static const char* kServiceName;
static const char* kSettingsGroup;
@ -57,12 +57,12 @@ class PodcastService : public InternetService {
enum Role { Role_Podcast = InternetModel::RoleCount, Role_Episode };
QStandardItem* CreateRootItem();
void LazyPopulate(QStandardItem* parent);
bool has_initial_load_settings() const { return true; }
void ShowContextMenu(const QPoint& global_pos);
void ReloadSettings();
void InitialLoadSettings();
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* parent) override;
bool has_initial_load_settings() const override { return true; }
void ShowContextMenu(const QPoint& global_pos) override;
void ReloadSettings() override;
void InitialLoadSettings() override;
// Called by SongLoader when the user adds a Podcast URL directly. Adds a
// subscription to the podcast and displays it in the UI. If the QVariant
// contains an OPML file then this displays it in the Add Podcast dialog.
@ -81,7 +81,7 @@ class PodcastService : public InternetService {
void DeleteDownloadedData();
void SetNew();
void SetListened();
void ShowConfig();
void ShowConfig() override;
void SubscriptionAdded(const Podcast& podcast);
void SubscriptionRemoved(const Podcast& podcast);

View File

@ -35,7 +35,7 @@ class RadioBrowserService : public InternetService {
public:
RadioBrowserService(Application* app, InternetModel* parent);
~RadioBrowserService(){};
~RadioBrowserService() override{};
enum ItemType {
Type_Stream = 2000,

View File

@ -39,7 +39,7 @@ class SomaFMServiceBase : public InternetService {
const QString& name, const QUrl& channel_list_url,
const QUrl& homepage_url, const QUrl& donate_page_url,
const QIcon& icon);
~SomaFMServiceBase();
~SomaFMServiceBase() override;
enum ItemType {
Type_Stream = 2000,
@ -59,14 +59,14 @@ class SomaFMServiceBase : public InternetService {
const QString& url_scheme() const { return url_scheme_; }
const QIcon& icon() const { return icon_; }
QStandardItem* CreateRootItem();
void LazyPopulate(QStandardItem* item);
void ShowContextMenu(const QPoint& global_pos);
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* item) override;
void ShowContextMenu(const QPoint& global_pos) override;
PlaylistItem::Options playlistitem_options() const;
PlaylistItem::Options playlistitem_options() const override;
QNetworkAccessManager* network() const { return network_; }
void ReloadSettings();
void ReloadSettings() override;
bool IsStreamListStale() const { return streams_.IsStale(); }
StreamList Streams();

View File

@ -1,281 +0,0 @@
/* This file is part of Clementine.
Copyright 2011-2012, David Sansome <me@davidsansome.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "spotifyblobdownloader.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QIODevice>
#include <QMessageBox>
#include <QNetworkReply>
#include <QProgressDialog>
#include <QSslKey>
#include "config.h"
#include "core/arraysize.h"
#include "core/logging.h"
#include "core/network.h"
#include "core/utilities.h"
#include "spotifyservice.h"
#ifdef Q_OS_UNIX
#include <unistd.h>
#endif
#ifdef HAVE_CRYPTOPP
#include <cryptopp/pkcspad.h>
#include <cryptopp/rsa.h>
// Compatibility with cryptocpp >= 6.0.0
namespace CryptoPP {
typedef unsigned char byte;
}
#endif // HAVE_CRYPTOPP
const char* SpotifyBlobDownloader::kSignatureSuffix = ".sha512";
SpotifyBlobDownloader::SpotifyBlobDownloader(const QString& version,
const QString& path,
QObject* parent)
: QObject(parent),
version_(version),
path_(path),
network_(new NetworkAccessManager(this)),
progress_(new QProgressDialog(tr("Downloading Spotify plugin"),
tr("Cancel"), 0, 0)) {
progress_->setWindowTitle(QCoreApplication::applicationName());
connect(progress_, SIGNAL(canceled()), SLOT(Cancel()));
}
SpotifyBlobDownloader::~SpotifyBlobDownloader() {
qDeleteAll(replies_);
replies_.clear();
delete progress_;
}
bool SpotifyBlobDownloader::Prompt() {
QMessageBox::StandardButton ret = QMessageBox::question(
nullptr, tr("Spotify plugin not installed"),
tr("An additional plugin is required to use Spotify in Clementine. "
"Would you like to download and install it now?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
return ret == QMessageBox::Yes;
}
void SpotifyBlobDownloader::Start() {
qDeleteAll(replies_);
replies_.clear();
const QStringList filenames =
QStringList() << "blob"
<< "blob" + QString(kSignatureSuffix)
<< "libspotify.so.12.1.51"
<< "libspotify.so.12.1.51" + QString(kSignatureSuffix);
for (const QString& filename : filenames) {
const QUrl url(SpotifyService::kBlobDownloadUrl + version_ + "/" +
filename);
qLog(Info) << "Downloading" << url;
QNetworkRequest req(url);
// This policy will work as long as there isn't a redirect from https to
// http. This is a legacy attribute that should be changed to use
// RedirectPolicyAttribute when Qt 5.9 is the lowest supported version.
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
QNetworkReply* reply = network_->get(req);
connect(reply, SIGNAL(finished()), SLOT(ReplyFinished()));
connect(reply, SIGNAL(downloadProgress(qint64, qint64)),
SLOT(ReplyProgress()));
replies_ << reply;
}
progress_->show();
}
void SpotifyBlobDownloader::ReplyFinished() {
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
if (reply->error() != QNetworkReply::NoError) {
// Handle network errors
ShowError(reply->errorString());
return;
}
// Is everything finished?
for (QNetworkReply* reply : replies_) {
if (!reply->isFinished()) {
return;
}
}
// Read files into memory first.
QMap<QString, QByteArray> file_data;
QStringList signature_filenames;
for (QNetworkReply* reply : replies_) {
const QString filename = reply->url().path().section('/', -1, -1);
if (filename.endsWith(kSignatureSuffix)) {
signature_filenames << filename;
}
file_data[filename] = reply->readAll();
}
if (!CheckSignature(file_data, signature_filenames)) {
qLog(Warning) << "Signature checks failed";
return;
}
// Make the destination directory and write the files into it
QDir().mkpath(path_);
for (const QString& filename : file_data.keys()) {
const QString dest_path = path_ + "/" + filename;
if (filename.endsWith(kSignatureSuffix)) continue;
qLog(Info) << "Writing" << dest_path;
QFile file(dest_path);
if (!file.open(QIODevice::WriteOnly)) {
ShowError("Failed to open " + dest_path + " for writing");
return;
}
file.write(file_data[filename]);
file.close();
file.setPermissions(QFile::Permissions(0x7755));
#ifdef Q_OS_UNIX
const int so_pos = filename.lastIndexOf(".so.");
if (so_pos != -1) {
QString link_path = path_ + "/" + filename.left(so_pos + 3);
QStringList version_parts = filename.mid(so_pos + 4).split('.');
while (!version_parts.isEmpty()) {
qLog(Debug) << "Linking" << dest_path << "to" << link_path;
int ret = symlink(dest_path.toLocal8Bit().constData(),
link_path.toLocal8Bit().constData());
if (ret != 0) {
qLog(Warning) << "Creating symlink failed with return code" << ret;
}
link_path += "." + version_parts.takeFirst();
}
}
#endif // Q_OS_UNIX
}
EmitFinished();
}
bool SpotifyBlobDownloader::CheckSignature(
const QMap<QString, QByteArray>& file_data,
const QStringList& signature_filenames) {
#ifdef HAVE_CRYPTOPP
QFile public_key_file(":/clementine-spotify-public.pem");
public_key_file.open(QIODevice::ReadOnly);
const QByteArray public_key_data = ConvertPEMToDER(public_key_file.readAll());
try {
CryptoPP::ByteQueue bytes;
bytes.Put(
reinterpret_cast<const CryptoPP::byte*>(public_key_data.constData()),
public_key_data.size());
bytes.MessageEnd();
CryptoPP::RSA::PublicKey public_key;
public_key.Load(bytes);
CryptoPP::RSASS<CryptoPP::PKCS1v15, CryptoPP::SHA512>::Verifier verifier(
public_key);
for (const QString& signature_filename : signature_filenames) {
QString actual_filename = signature_filename;
actual_filename.remove(kSignatureSuffix);
const bool result =
verifier.VerifyMessage(reinterpret_cast<const CryptoPP::byte*>(
file_data[actual_filename].constData()),
file_data[actual_filename].size(),
reinterpret_cast<const CryptoPP::byte*>(
file_data[signature_filename].constData()),
file_data[signature_filename].size());
qLog(Debug) << "Verifying" << actual_filename << "against"
<< signature_filename << result;
if (!result) {
ShowError("Invalid signature: " + actual_filename);
return false;
}
}
} catch (std::exception& e) {
// This should only happen if we fail to parse our own key.
qLog(Debug) << "Verifying spotify blob signature failed:" << e.what();
return false;
}
return true;
#else
return false;
#endif // HAVE_CRYPTOPP
}
QByteArray SpotifyBlobDownloader::ConvertPEMToDER(const QByteArray& pem) {
QSslKey key(pem, QSsl::Rsa, QSsl::Pem, QSsl::PublicKey);
Q_ASSERT(!key.isNull());
return key.toDer();
}
void SpotifyBlobDownloader::ReplyProgress() {
int progress = 0;
int total = 0;
for (QNetworkReply* reply : replies_) {
progress += reply->bytesAvailable();
total += reply->rawHeader("Content-Length").toInt();
}
progress_->setMaximum(total);
progress_->setValue(progress);
}
void SpotifyBlobDownloader::Cancel() { deleteLater(); }
void SpotifyBlobDownloader::ShowError(const QString& message) {
// Stop any remaining replies before showing the dialog so they don't
// carry on in the background
for (QNetworkReply* reply : replies_) {
disconnect(reply, 0, this, 0);
reply->abort();
}
qLog(Warning) << message;
QMessageBox::warning(nullptr, tr("Error downloading Spotify plugin"), message,
QMessageBox::Close);
deleteLater();
}
void SpotifyBlobDownloader::EmitFinished() {
emit Finished();
deleteLater();
}

View File

@ -1,70 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_
#define INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_
#include <QMap>
#include <QObject>
class QNetworkAccessManager;
class QNetworkReply;
class QProgressDialog;
class SpotifyBlobDownloader : public QObject {
Q_OBJECT
public:
SpotifyBlobDownloader(const QString& version, const QString& path,
QObject* parent = nullptr);
~SpotifyBlobDownloader();
static const char* kSignatureSuffix;
static bool Prompt();
void Start();
signals:
void Finished();
private slots:
void ReplyFinished();
void ReplyProgress();
void Cancel();
private:
void ShowError(const QString& message);
void EmitFinished();
bool CheckSignature(const QMap<QString, QByteArray>& file_data,
const QStringList& signature_filenames);
static QByteArray ConvertPEMToDER(const QByteArray& pem);
private:
QString version_;
QString path_;
QNetworkAccessManager* network_;
QList<QNetworkReply*> replies_;
QProgressDialog* progress_;
};
#endif // INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_

View File

@ -1,321 +0,0 @@
/* This file is part of Clementine.
Copyright 2011-2012, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2011-2012, 2014, David Sansome <me@davidsansome.com>
Copyright 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
Copyright 2014, pie.or.paj <pie.or.paj@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "spotifyserver.h"
#include <QTcpServer>
#include <QTcpSocket>
#include <QTimer>
#include <QUrl>
#include "core/closure.h"
#include "core/logging.h"
#include "spotifymessages.pb.h"
SpotifyServer::SpotifyServer(QObject* parent)
: AbstractMessageHandler<cpb::spotify::Message>(nullptr, parent),
server_(new QTcpServer(this)),
logged_in_(false) {
connect(server_, SIGNAL(newConnection()), SLOT(NewConnection()));
}
void SpotifyServer::Init() {
if (!server_->listen(QHostAddress::LocalHost)) {
qLog(Error) << "Couldn't open server socket" << server_->errorString();
}
}
int SpotifyServer::server_port() const { return server_->serverPort(); }
void SpotifyServer::NewConnection() {
QTcpSocket* socket = server_->nextPendingConnection();
SetDevice(socket);
qLog(Info) << "Connection from port" << socket->peerPort();
// Send any login messages that were queued before the client connected
for (const cpb::spotify::Message& message : queued_login_messages_) {
SendOrQueueMessage(message);
}
queued_login_messages_.clear();
// Don't take any more connections from clients
disconnect(server_, SIGNAL(newConnection()), this, 0);
}
void SpotifyServer::SendOrQueueMessage(const cpb::spotify::Message& message) {
const bool is_login_message = message.has_login_request();
QList<cpb::spotify::Message>* queue =
is_login_message ? &queued_login_messages_ : &queued_messages_;
if (!device_ || (!is_login_message && !logged_in_)) {
queue->append(message);
} else {
SendMessage(message);
}
}
void SpotifyServer::Login(const QString& username, const QString& password,
cpb::spotify::Bitrate bitrate,
bool volume_normalisation) {
cpb::spotify::Message message;
cpb::spotify::LoginRequest* request = message.mutable_login_request();
request->set_username(DataCommaSizeFromQString(username));
if (!password.isEmpty()) {
request->set_password(DataCommaSizeFromQString(password));
}
request->mutable_playback_settings()->set_bitrate(bitrate);
request->mutable_playback_settings()->set_volume_normalisation(
volume_normalisation);
SendOrQueueMessage(message);
}
void SpotifyServer::SetPlaybackSettings(cpb::spotify::Bitrate bitrate,
bool volume_normalisation) {
cpb::spotify::Message message;
cpb::spotify::PlaybackSettings* request =
message.mutable_set_playback_settings_request();
request->set_bitrate(bitrate);
request->set_volume_normalisation(volume_normalisation);
SendOrQueueMessage(message);
}
void SpotifyServer::MessageArrived(const cpb::spotify::Message& message) {
if (message.has_login_response()) {
const cpb::spotify::LoginResponse& response = message.login_response();
logged_in_ = response.success();
if (response.success()) {
// Send any messages that were queued before the client logged in
for (const cpb::spotify::Message& message : queued_messages_) {
SendOrQueueMessage(message);
}
queued_messages_.clear();
}
emit LoginCompleted(response.success(),
QStringFromStdString(response.error()),
response.error_code());
} else if (message.has_playlists_updated()) {
emit PlaylistsUpdated(message.playlists_updated());
} else if (message.has_load_playlist_response()) {
const cpb::spotify::LoadPlaylistResponse& response =
message.load_playlist_response();
switch (response.request().type()) {
case cpb::spotify::Inbox:
emit InboxLoaded(response);
break;
case cpb::spotify::Starred:
emit StarredLoaded(response);
break;
case cpb::spotify::UserPlaylist:
emit UserPlaylistLoaded(response);
break;
}
} else if (message.has_playback_error()) {
emit PlaybackError(QStringFromStdString(message.playback_error().error()));
} else if (message.has_search_response()) {
emit SearchResults(message.search_response());
} else if (message.has_image_response()) {
const cpb::spotify::ImageResponse& response = message.image_response();
const QString id = QStringFromStdString(response.id());
if (response.has_data()) {
emit ImageLoaded(
id, QImage::fromData(
QByteArray(response.data().data(), response.data().size())));
} else {
emit ImageLoaded(id, QImage());
}
} else if (message.has_sync_playlist_progress()) {
emit SyncPlaylistProgress(message.sync_playlist_progress());
} else if (message.has_browse_album_response()) {
emit AlbumBrowseResults(message.browse_album_response());
} else if (message.has_browse_toplist_response()) {
emit ToplistBrowseResults(message.browse_toplist_response());
}
}
void SpotifyServer::LoadPlaylist(cpb::spotify::PlaylistType type, int index) {
cpb::spotify::Message message;
cpb::spotify::LoadPlaylistRequest* req =
message.mutable_load_playlist_request();
req->set_type(type);
if (index != -1) {
req->set_user_playlist_index(index);
}
SendOrQueueMessage(message);
}
void SpotifyServer::SyncPlaylist(cpb::spotify::PlaylistType type, int index,
bool offline) {
cpb::spotify::Message message;
cpb::spotify::SyncPlaylistRequest* req =
message.mutable_sync_playlist_request();
req->mutable_request()->set_type(type);
if (index != -1) {
req->mutable_request()->set_user_playlist_index(index);
}
req->set_offline_sync(offline);
SendOrQueueMessage(message);
}
void SpotifyServer::SyncInbox() { SyncPlaylist(cpb::spotify::Inbox, -1, true); }
void SpotifyServer::SyncStarred() {
SyncPlaylist(cpb::spotify::Starred, -1, true);
}
void SpotifyServer::SyncUserPlaylist(int index) {
Q_ASSERT(index >= 0);
SyncPlaylist(cpb::spotify::UserPlaylist, index, true);
}
void SpotifyServer::LoadInbox() { LoadPlaylist(cpb::spotify::Inbox); }
void SpotifyServer::LoadStarred() { LoadPlaylist(cpb::spotify::Starred); }
void SpotifyServer::LoadUserPlaylist(int index) {
Q_ASSERT(index >= 0);
LoadPlaylist(cpb::spotify::UserPlaylist, index);
}
void SpotifyServer::AddSongsToStarred(const QList<QUrl>& songs_urls) {
AddSongsToPlaylist(cpb::spotify::Starred, songs_urls);
}
void SpotifyServer::AddSongsToUserPlaylist(int playlist_index,
const QList<QUrl>& songs_urls) {
AddSongsToPlaylist(cpb::spotify::UserPlaylist, songs_urls, playlist_index);
}
void SpotifyServer::AddSongsToPlaylist(
const cpb::spotify::PlaylistType playlist_type,
const QList<QUrl>& songs_urls, int playlist_index) {
cpb::spotify::Message message;
cpb::spotify::AddTracksToPlaylistRequest* req =
message.mutable_add_tracks_to_playlist();
req->set_playlist_type(playlist_type);
req->set_playlist_index(playlist_index);
for (const QUrl& song_url : songs_urls) {
req->add_track_uri(DataCommaSizeFromQString(song_url.toString()));
}
SendOrQueueMessage(message);
}
void SpotifyServer::RemoveSongsFromStarred(
const QList<int>& songs_indices_to_remove) {
RemoveSongsFromPlaylist(cpb::spotify::Starred, songs_indices_to_remove);
}
void SpotifyServer::RemoveSongsFromUserPlaylist(
int playlist_index, const QList<int>& songs_indices_to_remove) {
RemoveSongsFromPlaylist(cpb::spotify::UserPlaylist, songs_indices_to_remove,
playlist_index);
}
void SpotifyServer::RemoveSongsFromPlaylist(
const cpb::spotify::PlaylistType playlist_type,
const QList<int>& songs_indices_to_remove, int playlist_index) {
cpb::spotify::Message message;
cpb::spotify::RemoveTracksFromPlaylistRequest* req =
message.mutable_remove_tracks_from_playlist();
req->set_playlist_type(playlist_type);
if (playlist_type == cpb::spotify::UserPlaylist) {
req->set_playlist_index(playlist_index);
}
for (int song_index : songs_indices_to_remove) {
req->add_track_index(song_index);
}
SendOrQueueMessage(message);
}
void SpotifyServer::StartPlayback(const QString& uri, quint16 port) {
cpb::spotify::Message message;
cpb::spotify::PlaybackRequest* req = message.mutable_playback_request();
req->set_track_uri(DataCommaSizeFromQString(uri));
req->set_media_port(port);
SendOrQueueMessage(message);
}
void SpotifyServer::Seek(qint64 offset_nsec) {
cpb::spotify::Message message;
cpb::spotify::SeekRequest* req = message.mutable_seek_request();
req->set_offset_nsec(offset_nsec);
SendOrQueueMessage(message);
}
void SpotifyServer::Search(const QString& text, int limit, int limit_album) {
cpb::spotify::Message message;
cpb::spotify::SearchRequest* req = message.mutable_search_request();
req->set_query(DataCommaSizeFromQString(text));
req->set_limit(limit);
req->set_limit_album(limit_album);
SendOrQueueMessage(message);
}
void SpotifyServer::LoadImage(const QString& id) {
cpb::spotify::Message message;
cpb::spotify::ImageRequest* req = message.mutable_image_request();
req->set_id(DataCommaSizeFromQString(id));
SendOrQueueMessage(message);
}
void SpotifyServer::AlbumBrowse(const QString& uri) {
cpb::spotify::Message message;
cpb::spotify::BrowseAlbumRequest* req =
message.mutable_browse_album_request();
req->set_uri(DataCommaSizeFromQString(uri));
SendOrQueueMessage(message);
}
void SpotifyServer::LoadToplist() {
cpb::spotify::Message message;
cpb::spotify::BrowseToplistRequest* req =
message.mutable_browse_toplist_request();
req->set_type(cpb::spotify::BrowseToplistRequest::Tracks);
req->set_region(cpb::spotify::BrowseToplistRequest::Everywhere);
SendOrQueueMessage(message);
}
void SpotifyServer::SetPaused(const bool paused) {
cpb::spotify::Message message;
cpb::spotify::PauseRequest* req = message.mutable_pause_request();
req->set_paused(paused);
SendOrQueueMessage(message);
}

View File

@ -1,112 +0,0 @@
/* This file is part of Clementine.
Copyright 2011-2012, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2011-2012, 2014, David Sansome <me@davidsansome.com>
Copyright 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
Copyright 2014, pie.or.paj <pie.or.paj@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INTERNET_SPOTIFY_SPOTIFYSERVER_H_
#define INTERNET_SPOTIFY_SPOTIFYSERVER_H_
#include <QImage>
#include <QObject>
#include "core/messagehandler.h"
#include "spotifymessages.pb.h"
class QTcpServer;
class QTcpSocket;
class SpotifyServer : public AbstractMessageHandler<cpb::spotify::Message> {
Q_OBJECT
public:
explicit SpotifyServer(QObject* parent = nullptr);
void Init();
void Login(const QString& username, const QString& password,
cpb::spotify::Bitrate bitrate, bool volume_normalisation);
void LoadStarred();
void SyncStarred();
void LoadInbox();
void SyncInbox();
void LoadUserPlaylist(int index);
void SyncUserPlaylist(int index);
void AddSongsToStarred(const QList<QUrl>& songs_urls);
void AddSongsToUserPlaylist(int playlist_index,
const QList<QUrl>& songs_urls);
void RemoveSongsFromUserPlaylist(int playlist_index,
const QList<int>& songs_indices_to_remove);
void RemoveSongsFromStarred(const QList<int>& songs_indices_to_remove);
void Search(const QString& text, int limit, int limit_album = 0);
void LoadImage(const QString& id);
void AlbumBrowse(const QString& uri);
void SetPlaybackSettings(cpb::spotify::Bitrate bitrate,
bool volume_normalisation);
void LoadToplist();
void SetPaused(const bool paused);
int server_port() const;
public slots:
void StartPlayback(const QString& uri, quint16 port);
void Seek(qint64 offset_nsec);
signals:
void LoginCompleted(bool success, const QString& error,
cpb::spotify::LoginResponse_Error error_code);
void PlaylistsUpdated(const cpb::spotify::Playlists& playlists);
void StarredLoaded(const cpb::spotify::LoadPlaylistResponse& response);
void InboxLoaded(const cpb::spotify::LoadPlaylistResponse& response);
void UserPlaylistLoaded(const cpb::spotify::LoadPlaylistResponse& response);
void PlaybackError(const QString& message);
void SearchResults(const cpb::spotify::SearchResponse& response);
void ImageLoaded(const QString& id, const QImage& image);
void SyncPlaylistProgress(const cpb::spotify::SyncPlaylistProgress& progress);
void AlbumBrowseResults(const cpb::spotify::BrowseAlbumResponse& response);
void ToplistBrowseResults(
const cpb::spotify::BrowseToplistResponse& response);
protected:
void MessageArrived(const cpb::spotify::Message& message);
private slots:
void NewConnection();
private:
void LoadPlaylist(cpb::spotify::PlaylistType type, int index = -1);
void SyncPlaylist(cpb::spotify::PlaylistType type, int index, bool offline);
void AddSongsToPlaylist(const cpb::spotify::PlaylistType playlist_type,
const QList<QUrl>& songs_urls,
// Used iff type is user_playlist
int playlist_index = -1);
void RemoveSongsFromPlaylist(const cpb::spotify::PlaylistType playlist_type,
const QList<int>& songs_indices_to_remove,
// Used iff type is user_playlist
int playlist_index = -1);
void SendOrQueueMessage(const cpb::spotify::Message& message);
QTcpServer* server_;
bool logged_in_;
QList<cpb::spotify::Message> queued_login_messages_;
QList<cpb::spotify::Message> queued_messages_;
};
#endif // INTERNET_SPOTIFY_SPOTIFYSERVER_H_

View File

@ -1,993 +0,0 @@
/* This file is part of Clementine.
Copyright 2011-2014, David Sansome <me@davidsansome.com>
Copyright 2011, Tyler Rhodes <tyler.s.rhodes@gmail.com>
Copyright 2011-2012, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2012, 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
Copyright 2014, Chocobozzz <florian.bigard@gmail.com>
Copyright 2014, pie.or.paj <pie.or.paj@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "spotifyservice.h"
#include <QCoreApplication>
#include <QFile>
#include <QFileInfo>
#include <QMenu>
#include <QMessageBox>
#include <QMimeData>
#include <QProcess>
#include <QSettings>
#include <QVariant>
#include "blobversion.h"
#include "config.h"
#include "core/application.h"
#include "core/database.h"
#include "core/logging.h"
#include "core/mergedproxymodel.h"
#include "core/player.h"
#include "core/taskmanager.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "globalsearch/globalsearch.h"
#include "globalsearch/spotifysearchprovider.h"
#include "internet/core/internetmodel.h"
#include "internet/core/searchboxwidget.h"
#include "playlist/playlist.h"
#include "playlist/playlistcontainer.h"
#include "playlist/playlistmanager.h"
#include "spotifyserver.h"
#include "ui/iconloader.h"
#include "widgets/didyoumean.h"
#ifdef HAVE_SPOTIFY_DOWNLOADER
#include "spotifyblobdownloader.h"
#endif
Q_DECLARE_METATYPE(QStandardItem*)
const char* SpotifyService::kServiceName = "Spotify";
const char* SpotifyService::kSettingsGroup = "Spotify";
const char* SpotifyService::kBlobDownloadUrl =
"https://spotify.clementine-player.org/";
const int SpotifyService::kSearchDelayMsec = 400;
SpotifyService::SpotifyService(Application* app, InternetModel* parent)
: InternetService(kServiceName, app, parent, parent),
server_(nullptr),
blob_process_(nullptr),
root_(nullptr),
search_(nullptr),
starred_(nullptr),
inbox_(nullptr),
toplist_(nullptr),
login_task_id_(0),
playlist_context_menu_(nullptr),
song_context_menu_(nullptr),
playlist_sync_action_(nullptr),
get_url_to_share_playlist_(nullptr),
remove_from_playlist_(nullptr),
search_box_(new SearchBoxWidget(this)),
search_delay_(new QTimer(this)),
login_state_(LoginState_OtherError),
bitrate_(cpb::spotify::Bitrate320k),
volume_normalisation_(false) {
// Build the search path for the binary blob.
// Look for one distributed alongside clementine first, then check in the
// user's home directory for any that have been downloaded.
#if defined(Q_OS_MACOS) && defined(USE_BUNDLE)
system_blob_path_ = QCoreApplication::applicationDirPath() + "/" +
USE_BUNDLE_DIR + "/clementine-spotifyblob";
#else
system_blob_path_ = QCoreApplication::applicationDirPath() +
"/clementine-spotifyblob" CMAKE_EXECUTABLE_SUFFIX;
#endif
local_blob_version_ = QString("version%1-%2bit")
.arg(SPOTIFY_BLOB_VERSION)
.arg(sizeof(void*) * 8);
local_blob_path_ =
Utilities::GetConfigPath(Utilities::Path_LocalSpotifyBlob) + "/" +
local_blob_version_ + "/blob";
qLog(Debug) << "Spotify system blob path:" << system_blob_path_;
qLog(Debug) << "Spotify local blob path:" << local_blob_path_;
app_->global_search()->AddProvider(new SpotifySearchProvider(app_, this));
search_delay_->setInterval(kSearchDelayMsec);
search_delay_->setSingleShot(true);
connect(search_delay_, SIGNAL(timeout()), SLOT(DoSearch()));
connect(search_box_, SIGNAL(TextChanged(QString)), SLOT(Search(QString)));
}
SpotifyService::~SpotifyService() {
if (blob_process_ && blob_process_->state() == QProcess::Running) {
qLog(Info) << "Terminating blob process...";
blob_process_->terminate();
blob_process_->waitForFinished(1000);
}
}
QStandardItem* SpotifyService::CreateRootItem() {
root_ = new QStandardItem(IconLoader::Load("spotify", IconLoader::Provider),
kServiceName);
root_->setData(true, InternetModel::Role_CanLazyLoad);
return root_;
}
void SpotifyService::LazyPopulate(QStandardItem* item) {
switch (item->data(InternetModel::Role_Type).toInt()) {
case InternetModel::Type_Service:
EnsureServerCreated();
break;
case Type_SearchResults:
break;
case Type_InboxPlaylist:
EnsureServerCreated();
server_->LoadInbox();
break;
case Type_StarredPlaylist:
EnsureServerCreated();
server_->LoadStarred();
break;
case InternetModel::Type_UserPlaylist:
EnsureServerCreated();
server_->LoadUserPlaylist(item->data(Role_UserPlaylistIndex).toInt());
break;
case Type_Toplist:
EnsureServerCreated();
server_->LoadToplist();
break;
default:
break;
}
return;
}
void SpotifyService::Login(const QString& username, const QString& password) {
Logout();
EnsureServerCreated(username, password);
}
void SpotifyService::LoginCompleted(
bool success, const QString& error,
cpb::spotify::LoginResponse_Error error_code) {
if (login_task_id_) {
app_->task_manager()->SetTaskFinished(login_task_id_);
login_task_id_ = 0;
}
if (!success) {
bool show_error_dialog = true;
QString error_copy(error);
switch (error_code) {
case cpb::spotify::LoginResponse_Error_BadUsernameOrPassword:
login_state_ = LoginState_BadCredentials;
break;
case cpb::spotify::LoginResponse_Error_UserBanned:
login_state_ = LoginState_Banned;
break;
case cpb::spotify::LoginResponse_Error_UserNeedsPremium:
login_state_ = LoginState_NoPremium;
break;
case cpb::spotify::LoginResponse_Error_ReloginFailed:
if (login_state_ == LoginState_LoggedIn) {
// This is the first time the relogin has failed - show a message this
// time only.
error_copy =
tr("You have been logged out of Spotify, please re-enter your "
"password in the Settings dialog.");
} else {
show_error_dialog = false;
}
login_state_ = LoginState_ReloginFailed;
break;
default:
login_state_ = LoginState_OtherError;
break;
}
if (show_error_dialog) {
QMessageBox::warning(nullptr, tr("Spotify login error"), error_copy,
QMessageBox::Close);
}
} else {
login_state_ = LoginState_LoggedIn;
}
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("login_state", login_state_);
emit LoginFinished(success);
}
void SpotifyService::BlobProcessError(QProcess::ProcessError error) {
qLog(Error) << "Spotify blob process failed:" << error;
blob_process_->deleteLater();
blob_process_ = nullptr;
if (login_task_id_) {
app_->task_manager()->SetTaskFinished(login_task_id_);
}
}
void SpotifyService::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
login_state_ =
LoginState(s.value("login_state", LoginState_OtherError).toInt());
bitrate_ = static_cast<cpb::spotify::Bitrate>(
s.value("bitrate", cpb::spotify::Bitrate320k).toInt());
volume_normalisation_ = s.value("volume_normalisation", false).toBool();
if (server_ && blob_process_) {
server_->SetPlaybackSettings(bitrate_, volume_normalisation_);
}
}
void SpotifyService::EnsureServerCreated(const QString& username,
const QString& password) {
if (server_ && blob_process_) {
return;
}
delete server_;
server_ = new SpotifyServer(this);
connect(
server_,
SIGNAL(LoginCompleted(bool, QString, cpb::spotify::LoginResponse_Error)),
SLOT(LoginCompleted(bool, QString, cpb::spotify::LoginResponse_Error)));
connect(server_, SIGNAL(PlaylistsUpdated(cpb::spotify::Playlists)),
SLOT(PlaylistsUpdated(cpb::spotify::Playlists)));
connect(server_, SIGNAL(InboxLoaded(cpb::spotify::LoadPlaylistResponse)),
SLOT(InboxLoaded(cpb::spotify::LoadPlaylistResponse)));
connect(server_, SIGNAL(StarredLoaded(cpb::spotify::LoadPlaylistResponse)),
SLOT(StarredLoaded(cpb::spotify::LoadPlaylistResponse)));
connect(server_,
SIGNAL(UserPlaylistLoaded(cpb::spotify::LoadPlaylistResponse)),
SLOT(UserPlaylistLoaded(cpb::spotify::LoadPlaylistResponse)));
connect(server_, SIGNAL(PlaybackError(QString)),
SIGNAL(StreamError(QString)));
connect(server_, SIGNAL(SearchResults(cpb::spotify::SearchResponse)),
SLOT(SearchResults(cpb::spotify::SearchResponse)));
connect(server_, SIGNAL(ImageLoaded(QString, QImage)),
SIGNAL(ImageLoaded(QString, QImage)));
connect(server_,
SIGNAL(SyncPlaylistProgress(cpb::spotify::SyncPlaylistProgress)),
SLOT(SyncPlaylistProgress(cpb::spotify::SyncPlaylistProgress)));
connect(server_,
SIGNAL(ToplistBrowseResults(cpb::spotify::BrowseToplistResponse)),
SLOT(ToplistLoaded(cpb::spotify::BrowseToplistResponse)));
server_->Init();
login_task_id_ = app_->task_manager()->StartTask(tr("Connecting to Spotify"));
QString login_username = username;
QString login_password = password;
if (username.isEmpty()) {
QSettings s;
s.beginGroup(kSettingsGroup);
login_username = s.value("username").toString();
login_password = QString();
}
server_->Login(login_username, login_password, bitrate_,
volume_normalisation_);
StartBlobProcess();
}
void SpotifyService::StartBlobProcess() {
// Try to find an executable to run
QString blob_path;
QProcessEnvironment env(QProcessEnvironment::systemEnvironment());
// Look in the system search path first
if (QFile::exists(system_blob_path_)) {
blob_path = system_blob_path_;
}
// Next look in the local path
if (blob_path.isEmpty()) {
if (QFile::exists(local_blob_path_)) {
blob_path = local_blob_path_;
env.insert("LD_LIBRARY_PATH", QFileInfo(local_blob_path_).path());
}
}
if (blob_path.isEmpty()) {
// If the blob still wasn't found then we'll prompt the user to download one
if (login_task_id_) {
app_->task_manager()->SetTaskFinished(login_task_id_);
}
#ifdef HAVE_SPOTIFY_DOWNLOADER
if (SpotifyBlobDownloader::Prompt()) {
InstallBlob();
}
#endif
return;
}
delete blob_process_;
blob_process_ = new QProcess(this);
blob_process_->setProcessChannelMode(QProcess::ForwardedChannels);
blob_process_->setProcessEnvironment(env);
connect(blob_process_, SIGNAL(error(QProcess::ProcessError)),
SLOT(BlobProcessError(QProcess::ProcessError)));
qLog(Info) << "Starting" << blob_path;
blob_process_->start(
blob_path, QStringList() << QString::number(server_->server_port()));
}
bool SpotifyService::IsBlobInstalled() const {
return QFile::exists(system_blob_path_) || QFile::exists(local_blob_path_);
}
void SpotifyService::InstallBlob() {
#ifdef HAVE_SPOTIFY_DOWNLOADER
// The downloader deletes itself when it finishes
SpotifyBlobDownloader* downloader = new SpotifyBlobDownloader(
local_blob_version_, QFileInfo(local_blob_path_).path(), this);
connect(downloader, SIGNAL(Finished()), SLOT(BlobDownloadFinished()));
connect(downloader, SIGNAL(Finished()), SIGNAL(BlobStateChanged()));
downloader->Start();
#endif // HAVE_SPOTIFY_DOWNLOADER
}
void SpotifyService::BlobDownloadFinished() { EnsureServerCreated(); }
void SpotifyService::AddCurrentSongToUserPlaylist(QAction* action) {
int playlist_index = action->data().toInt();
AddSongsToUserPlaylist(playlist_index, QList<QUrl>() << current_song_url_);
}
void SpotifyService::AddSongsToUserPlaylist(int playlist_index,
const QList<QUrl>& songs_urls) {
EnsureServerCreated();
server_->AddSongsToUserPlaylist(playlist_index, songs_urls);
}
void SpotifyService::AddCurrentSongToStarredPlaylist() {
AddSongsToStarred(QList<QUrl>() << current_song_url_);
}
void SpotifyService::AddSongsToStarred(const QList<QUrl>& songs_urls) {
EnsureMenuCreated();
server_->AddSongsToStarred(songs_urls);
}
void SpotifyService::InitSearch() {
search_ = new QStandardItem(IconLoader::Load("edit-find", IconLoader::Base),
tr("Search results"));
search_->setToolTip(
tr("Start typing something on the search box above to "
"fill this search results list"));
search_->setData(Type_SearchResults, InternetModel::Role_Type);
search_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
starred_ = new QStandardItem(IconLoader::Load("star-on", IconLoader::Other),
tr("Starred"));
starred_->setData(Type_StarredPlaylist, InternetModel::Role_Type);
starred_->setData(true, InternetModel::Role_CanLazyLoad);
starred_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
starred_->setData(true, InternetModel::Role_CanBeModified);
inbox_ = new QStandardItem(IconLoader::Load("mail-message", IconLoader::Base),
tr("Inbox"));
inbox_->setData(Type_InboxPlaylist, InternetModel::Role_Type);
inbox_->setData(true, InternetModel::Role_CanLazyLoad);
inbox_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
toplist_ = new QStandardItem(QIcon(), tr("Top tracks"));
toplist_->setData(Type_Toplist, InternetModel::Role_Type);
toplist_->setData(true, InternetModel::Role_CanLazyLoad);
toplist_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
root_->appendRow(search_);
root_->appendRow(toplist_);
root_->appendRow(starred_);
root_->appendRow(inbox_);
}
void SpotifyService::PlaylistsUpdated(const cpb::spotify::Playlists& response) {
if (login_task_id_) {
app_->task_manager()->SetTaskFinished(login_task_id_);
login_task_id_ = 0;
}
// Create starred and inbox playlists if they're not here already
if (!search_) {
InitSearch();
} else {
// Always reset starred playlist
// TODO: might be improved by including starred playlist in the response,
// and reloading it only when needed, like other playlists.
starred_->removeRows(0, starred_->rowCount());
LazyPopulate(starred_);
}
// Don't do anything if the playlists haven't changed since last time.
if (!DoPlaylistsDiffer(response)) {
qLog(Debug) << "Playlists haven't changed - not updating";
return;
}
qLog(Debug) << "Playlist have changed: updating";
// Remove and recreate the other playlists
for (QStandardItem* item : playlists_) {
item->parent()->removeRow(item->row());
}
playlists_.clear();
for (int i = 0; i < response.playlist_size(); ++i) {
const cpb::spotify::Playlists::Playlist& msg = response.playlist(i);
QString playlist_title = QStringFromStdString(msg.name());
if (!msg.is_mine()) {
const std::string& owner = msg.owner();
playlist_title +=
tr(", by ") + QString::fromUtf8(owner.c_str(), owner.size());
}
QStandardItem* item = new QStandardItem(playlist_title);
item->setData(InternetModel::Type_UserPlaylist, InternetModel::Role_Type);
item->setData(true, InternetModel::Role_CanLazyLoad);
item->setData(msg.index(), Role_UserPlaylistIndex);
item->setData(msg.is_mine(), InternetModel::Role_CanBeModified);
item->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
item->setData(QUrl(QStringFromStdString(msg.uri())),
InternetModel::Role_Url);
root_->appendRow(item);
playlists_ << item;
// Preload the playlist items so that drag & drop works immediately.
LazyPopulate(item);
}
}
bool SpotifyService::DoPlaylistsDiffer(
const cpb::spotify::Playlists& response) const {
if (playlists_.count() != response.playlist_size()) {
return true;
}
for (int i = 0; i < response.playlist_size(); ++i) {
const cpb::spotify::Playlists::Playlist& msg = response.playlist(i);
const QStandardItem* item = PlaylistBySpotifyIndex(msg.index());
if (!item) {
return true;
}
if (QStringFromStdString(msg.name()) != item->text()) {
return true;
}
if (msg.nb_tracks() != item->rowCount()) {
return true;
}
}
return false;
}
void SpotifyService::InboxLoaded(
const cpb::spotify::LoadPlaylistResponse& response) {
if (inbox_) {
FillPlaylist(inbox_, response);
}
}
void SpotifyService::StarredLoaded(
const cpb::spotify::LoadPlaylistResponse& response) {
if (starred_) {
FillPlaylist(starred_, response);
}
}
void SpotifyService::ToplistLoaded(
const cpb::spotify::BrowseToplistResponse& response) {
if (toplist_) {
FillPlaylist(toplist_, response.track());
}
}
QStandardItem* SpotifyService::PlaylistBySpotifyIndex(int index) const {
for (QStandardItem* item : playlists_) {
if (item->data(Role_UserPlaylistIndex).toInt() == index) {
return item;
}
}
return nullptr;
}
void SpotifyService::UserPlaylistLoaded(
const cpb::spotify::LoadPlaylistResponse& response) {
// Find a playlist with this index
QStandardItem* item =
PlaylistBySpotifyIndex(response.request().user_playlist_index());
if (item) {
FillPlaylist(item, response);
}
}
void SpotifyService::FillPlaylist(
QStandardItem* item,
const google::protobuf::RepeatedPtrField<cpb::spotify::Track>& tracks) {
if (item->hasChildren()) item->removeRows(0, item->rowCount());
for (int i = 0; i < tracks.size(); ++i) {
Song song;
SongFromProtobuf(tracks.Get(i), &song);
QStandardItem* child = CreateSongItem(song);
item->appendRow(child);
}
}
void SpotifyService::FillPlaylist(
QStandardItem* item, const cpb::spotify::LoadPlaylistResponse& response) {
qLog(Debug) << "Filling playlist:" << item->text();
FillPlaylist(item, response.track());
}
void SpotifyService::SongFromProtobuf(const cpb::spotify::Track& track,
Song* song) {
song->set_rating(track.starred() ? 1.0 : 0.0);
song->set_title(QStringFromStdString(track.title()));
song->set_album(QStringFromStdString(track.album()));
song->set_length_nanosec(track.duration_msec() * kNsecPerMsec);
song->set_score(track.popularity());
song->set_disc(track.disc());
song->set_track(track.track());
song->set_year(track.year());
song->set_url(QUrl(QStringFromStdString(track.uri())));
song->set_art_automatic("spotify://image/" +
QStringFromStdString(track.album_art_id()));
QStringList artists;
for (int i = 0; i < track.artist_size(); ++i) {
artists << QStringFromStdString(track.artist(i));
}
song->set_artist(artists.join(", "));
song->set_filetype(Song::Type_Stream);
song->set_valid(true);
song->set_directory_id(0);
song->set_mtime(0);
song->set_ctime(0);
song->set_filesize(0);
}
QList<QAction*> SpotifyService::playlistitem_actions(const Song& song) {
// Clear previous actions
while (!playlistitem_actions_.isEmpty()) {
QAction* action = playlistitem_actions_.takeFirst();
delete action->menu();
delete action;
}
QAction* add_to_starred =
new QAction(IconLoader::Load("star-on", IconLoader::Other),
tr("Add to Spotify starred"), this);
connect(add_to_starred, SIGNAL(triggered()),
SLOT(AddCurrentSongToStarredPlaylist()));
playlistitem_actions_.append(add_to_starred);
// Create a menu with 'add to playlist' actions for each Spotify playlist
QAction* add_to_playlists =
new QAction(IconLoader::Load("list-add", IconLoader::Base),
tr("Add to Spotify playlists"), this);
QMenu* playlists_menu = new QMenu();
for (const QStandardItem* playlist_item : playlists_) {
if (!playlist_item->data(InternetModel::Role_CanBeModified).toBool()) {
continue;
}
QAction* add_to_playlist = new QAction(playlist_item->text(), this);
add_to_playlist->setData(playlist_item->data(Role_UserPlaylistIndex));
playlists_menu->addAction(add_to_playlist);
}
connect(playlists_menu, SIGNAL(triggered(QAction*)),
SLOT(AddCurrentSongToUserPlaylist(QAction*)));
add_to_playlists->setMenu(playlists_menu);
playlistitem_actions_.append(add_to_playlists);
QAction* share_song =
new QAction(tr("Get a URL to share this Spotify song"), this);
connect(share_song, SIGNAL(triggered()), SLOT(GetCurrentSongUrlToShare()));
playlistitem_actions_.append(share_song);
// Keep in mind the current song URL
current_song_url_ = song.url();
return playlistitem_actions_;
}
PlaylistItem::Options SpotifyService::playlistitem_options() const {
return PlaylistItem::SeekDisabled;
}
QWidget* SpotifyService::HeaderWidget() const {
if (IsLoggedIn()) return search_box_;
return nullptr;
}
void SpotifyService::EnsureMenuCreated() {
if (context_menu_) return;
context_menu_.reset(new QMenu);
context_menu_->addAction(GetNewShowConfigAction());
playlist_context_menu_ = new QMenu;
playlist_context_menu_->addActions(GetPlaylistActions());
playlist_context_menu_->addSeparator();
playlist_sync_action_ = playlist_context_menu_->addAction(
IconLoader::Load("view-refresh", IconLoader::Base),
tr("Make playlist available offline"), this, SLOT(SyncPlaylist()));
get_url_to_share_playlist_ = playlist_context_menu_->addAction(
tr("Get a URL to share this playlist"), this,
SLOT(GetCurrentPlaylistUrlToShare()));
playlist_context_menu_->addSeparator();
playlist_context_menu_->addAction(GetNewShowConfigAction());
song_context_menu_ = new QMenu;
song_context_menu_->addActions(GetPlaylistActions());
song_context_menu_->addSeparator();
remove_from_playlist_ = song_context_menu_->addAction(
IconLoader::Load("list-remove", IconLoader::Base),
tr("Remove from playlist"), this, SLOT(RemoveCurrentFromPlaylist()));
song_context_menu_->addAction(tr("Get a URL to share this Spotify song"),
this, SLOT(GetCurrentSongUrlToShare()));
song_context_menu_->addSeparator();
song_context_menu_->addAction(GetNewShowConfigAction());
}
void SpotifyService::ClearSearchResults() {
if (search_) search_->removeRows(0, search_->rowCount());
}
void SpotifyService::SyncPlaylist() {
QStandardItem* item = playlist_sync_action_->data().value<QStandardItem*>();
Q_ASSERT(item);
switch (item->data(InternetModel::Role_Type).toInt()) {
case InternetModel::Type_UserPlaylist: {
int index = item->data(Role_UserPlaylistIndex).toInt();
server_->SyncUserPlaylist(index);
playlist_sync_ids_[index] =
app_->task_manager()->StartTask(tr("Syncing Spotify playlist"));
break;
}
case Type_InboxPlaylist:
server_->SyncInbox();
inbox_sync_id_ =
app_->task_manager()->StartTask(tr("Syncing Spotify inbox"));
break;
case Type_StarredPlaylist:
server_->SyncStarred();
starred_sync_id_ =
app_->task_manager()->StartTask(tr("Syncing Spotify starred tracks"));
break;
default:
break;
}
}
void SpotifyService::Search(const QString& text, bool now) {
EnsureServerCreated();
pending_search_ = text;
// If there is no text (e.g. user cleared search box), we don't need to do a
// real query that will return nothing: we can clear the playlist now
if (text.isEmpty()) {
search_delay_->stop();
ClearSearchResults();
return;
}
if (now) {
search_delay_->stop();
DoSearch();
} else {
search_delay_->start();
}
}
void SpotifyService::DoSearch() {
if (!pending_search_.isEmpty()) {
server_->Search(pending_search_, 200);
}
}
void SpotifyService::SearchResults(
const cpb::spotify::SearchResponse& response) {
if (QStringFromStdString(response.request().query()) != pending_search_) {
qLog(Debug) << "Old search result for"
<< QStringFromStdString(response.request().query())
<< "expecting" << pending_search_;
return;
}
pending_search_.clear();
SongList songs;
for (int i = 0; i < response.result_size(); ++i) {
Song song;
SongFromProtobuf(response.result(i), &song);
songs << song;
}
qLog(Debug) << "Got" << songs.count() << "results";
ClearSearchResults();
// Must initialize search pointer if it is nullptr
if (!search_) {
InitSearch();
}
// Fill results list
for (const Song& song : songs) {
QStandardItem* child = CreateSongItem(song);
search_->appendRow(child);
}
const QString did_you_mean_suggestion =
QStringFromStdString(response.did_you_mean());
qLog(Debug) << "Did you mean suggestion: " << did_you_mean_suggestion;
if (!did_you_mean_suggestion.isEmpty()) {
search_box_->did_you_mean()->Show(did_you_mean_suggestion);
} else {
// In case something else was previously displayed
search_box_->did_you_mean()->hide();
}
QModelIndex index = model()->merged_model()->mapFromSource(search_->index());
ScrollToIndex(index);
}
SpotifyServer* SpotifyService::server() const {
SpotifyService* nonconst_this = const_cast<SpotifyService*>(this);
if (QThread::currentThread() != thread()) {
metaObject()->invokeMethod(nonconst_this, "EnsureServerCreated",
Qt::BlockingQueuedConnection);
} else {
nonconst_this->EnsureServerCreated();
}
return server_;
}
void SpotifyService::ShowContextMenu(const QPoint& global_pos) {
EnsureMenuCreated();
QStandardItem* item = model()->itemFromIndex(model()->current_index());
if (item) {
int type = item->data(InternetModel::Role_Type).toInt();
if (type == Type_InboxPlaylist || type == Type_StarredPlaylist ||
type == InternetModel::Type_UserPlaylist) {
playlist_sync_action_->setData(qVariantFromValue(item));
playlist_context_menu_->popup(global_pos);
current_playlist_url_ = item->data(InternetModel::Role_Url).toUrl();
get_url_to_share_playlist_->setVisible(type ==
InternetModel::Type_UserPlaylist);
return;
} else if (type == InternetModel::Type_Track) {
current_song_url_ = item->data(InternetModel::Role_Url).toUrl();
// Is this track contained in a playlist we can modify?
bool is_playlist_modifiable =
item->parent() &&
item->parent()->data(InternetModel::Role_CanBeModified).toBool();
remove_from_playlist_->setVisible(is_playlist_modifiable);
song_context_menu_->popup(global_pos);
return;
}
}
context_menu_->popup(global_pos);
}
void SpotifyService::GetCurrentSongUrlToShare() const {
QString url = current_song_url_.toEncoded();
// URLs we use can be opened with Spotify application, but I believe it's
// better to give website links instead.
url.replace("spotify:track:", "https://play.spotify.com/track/");
InternetService::ShowUrlBox(tr("Spotify song's URL"), url);
}
void SpotifyService::GetCurrentPlaylistUrlToShare() const {
QString url = current_playlist_url_.toEncoded();
// URLs we use can be opened with Spotify application, but I believe it's
// better to give website links instead.
url.replace(QRegExp("spotify:user:([^:]*):playlist:([^:]*)"),
"https://play.spotify.com/user/\\1/playlist/\\2");
InternetService::ShowUrlBox(tr("Spotify playlist's URL"), url);
}
void SpotifyService::DropMimeData(const QMimeData* data,
const QModelIndex& index) {
QModelIndex playlist_root_index = index;
QVariant q_playlist_type = playlist_root_index.data(InternetModel::Role_Type);
if (!q_playlist_type.isValid() ||
q_playlist_type.toInt() == InternetModel::Type_Track) {
// In case song was dropped on a playlist item, not on the playlist
// title/root element
playlist_root_index = index.parent();
q_playlist_type = playlist_root_index.data(InternetModel::Role_Type);
}
if (!q_playlist_type.isValid()) return;
int playlist_type = q_playlist_type.toInt();
if (playlist_type == Type_StarredPlaylist) {
AddSongsToStarred(data->urls());
} else if (playlist_type == InternetModel::Type_UserPlaylist) {
QVariant q_playlist_index =
playlist_root_index.data(Role_UserPlaylistIndex);
if (!q_playlist_index.isValid()) return;
AddSongsToUserPlaylist(q_playlist_index.toInt(), data->urls());
}
}
void SpotifyService::LoadImage(const QString& id) {
EnsureServerCreated();
server_->LoadImage(id);
}
void SpotifyService::SetPaused(bool paused) {
EnsureServerCreated();
server_->SetPaused(paused);
}
void SpotifyService::SyncPlaylistProgress(
const cpb::spotify::SyncPlaylistProgress& progress) {
qLog(Debug) << "Sync progress:" << progress.sync_progress();
int task_id = -1;
switch (progress.request().type()) {
case cpb::spotify::Inbox:
task_id = inbox_sync_id_;
break;
case cpb::spotify::Starred:
task_id = starred_sync_id_;
break;
case cpb::spotify::UserPlaylist: {
QMap<int, int>::const_iterator it = playlist_sync_ids_.constFind(
progress.request().user_playlist_index());
if (it != playlist_sync_ids_.constEnd()) {
task_id = it.value();
}
break;
}
default:
break;
}
if (task_id == -1) {
qLog(Warning) << "Received sync progress for unknown playlist";
return;
}
app_->task_manager()->SetTaskProgress(task_id, progress.sync_progress(), 100);
if (progress.sync_progress() == 100) {
app_->task_manager()->SetTaskFinished(task_id);
if (progress.request().type() == cpb::spotify::UserPlaylist) {
playlist_sync_ids_.remove(task_id);
}
}
}
QAction* SpotifyService::GetNewShowConfigAction() {
QAction* action = new QAction(IconLoader::Load("configure", IconLoader::Base),
tr("Configure Spotify..."), this);
connect(action, SIGNAL(triggered()), this, SLOT(ShowConfig()));
return action;
}
void SpotifyService::ShowConfig() {
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Spotify);
}
void SpotifyService::RemoveCurrentFromPlaylist() {
const QModelIndexList& indexes(model()->selected_indexes());
QMap<int, QList<int>> playlists_songs_indices;
QList<int> starred_songs_indices;
for (const QModelIndex& index : indexes) {
bool is_starred = false;
if (index.parent().data(InternetModel::Role_Type).toInt() ==
Type_StarredPlaylist) {
is_starred = true;
} else if (index.parent().data(InternetModel::Role_Type).toInt() !=
InternetModel::Type_UserPlaylist) {
continue;
}
if (index.data(InternetModel::Role_Type).toInt() !=
InternetModel::Type_Track) {
continue;
}
int song_index = index.row();
if (is_starred) {
starred_songs_indices << song_index;
} else {
int playlist_index = index.parent().data(Role_UserPlaylistIndex).toInt();
playlists_songs_indices[playlist_index] << song_index;
}
}
for (QMap<int, QList<int>>::const_iterator it =
playlists_songs_indices.constBegin();
it != playlists_songs_indices.constEnd(); ++it) {
RemoveSongsFromUserPlaylist(it.key(), it.value());
}
if (!starred_songs_indices.isEmpty()) {
RemoveSongsFromStarred(starred_songs_indices);
}
}
void SpotifyService::RemoveSongsFromUserPlaylist(
int playlist_index, const QList<int>& songs_indices_to_remove) {
server_->RemoveSongsFromUserPlaylist(playlist_index, songs_indices_to_remove);
}
void SpotifyService::RemoveSongsFromStarred(
const QList<int>& songs_indices_to_remove) {
server_->RemoveSongsFromStarred(songs_indices_to_remove);
}
void SpotifyService::Logout() {
delete server_;
delete blob_process_;
server_ = nullptr;
blob_process_ = nullptr;
login_state_ = LoginState_OtherError;
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("login_state", login_state_);
}

View File

@ -1,195 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, Tyler Rhodes <tyler.s.rhodes@gmail.com>
Copyright 2011-2012, 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
Copyright 2011-2012, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2011-2012, 2014, David Sansome <me@davidsansome.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INTERNET_SPOTIFY_SPOTIFYSERVICE_H_
#define INTERNET_SPOTIFY_SPOTIFYSERVICE_H_
#include <QProcess>
#include <QTimer>
#include "internet/core/internetmodel.h"
#include "internet/core/internetservice.h"
#include "spotifymessages.pb.h"
class Playlist;
class SearchBoxWidget;
class SpotifyServer;
class QMenu;
class SpotifyService : public InternetService {
Q_OBJECT
public:
SpotifyService(Application* app, InternetModel* parent);
~SpotifyService();
enum Type {
Type_SearchResults = InternetModel::TypeCount,
Type_StarredPlaylist,
Type_InboxPlaylist,
Type_Toplist,
};
enum Role {
Role_UserPlaylistIndex = InternetModel::RoleCount,
};
// Values are persisted - don't change.
enum LoginState {
LoginState_LoggedIn = 1,
LoginState_Banned = 2,
LoginState_BadCredentials = 3,
LoginState_NoPremium = 4,
LoginState_OtherError = 5,
LoginState_ReloginFailed = 6
};
static const char* kServiceName;
static const char* kSettingsGroup;
static const char* kBlobDownloadUrl;
static const int kSearchDelayMsec;
void ReloadSettings() override;
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* parent) override;
void ShowContextMenu(const QPoint& global_pos) override;
void DropMimeData(const QMimeData* data, const QModelIndex& index) override;
QList<QAction*> playlistitem_actions(const Song& song) override;
PlaylistItem::Options playlistitem_options() const override;
QWidget* HeaderWidget() const override;
void Logout();
void Login(const QString& username, const QString& password);
Q_INVOKABLE void LoadImage(const QString& id);
Q_INVOKABLE void SetPaused(bool paused);
SpotifyServer* server() const;
bool IsBlobInstalled() const;
void InstallBlob();
// Persisted in the settings and updated on each Login().
LoginState login_state() const { return login_state_; }
bool IsLoggedIn() const { return login_state_ == LoginState_LoggedIn; }
bool ConfigRequired() override { return !IsLoggedIn(); }
static void SongFromProtobuf(const cpb::spotify::Track& track, Song* song);
signals:
void BlobStateChanged();
void LoginFinished(bool success);
void ImageLoaded(const QString& id, const QImage& image);
public slots:
void Search(const QString& text, bool now = false);
void ShowConfig() override;
void RemoveCurrentFromPlaylist();
private:
void StartBlobProcess();
void FillPlaylist(
QStandardItem* item,
const google::protobuf::RepeatedPtrField<cpb::spotify::Track>& tracks);
void FillPlaylist(QStandardItem* item,
const cpb::spotify::LoadPlaylistResponse& response);
void AddSongsToUserPlaylist(int playlist_index,
const QList<QUrl>& songs_urls);
void AddSongsToStarred(const QList<QUrl>& songs_urls);
void EnsureMenuCreated();
// Create a new "show config" action. The caller is responsible for deleting
// the pointer (or adding it to menu or anything else that will take ownership
// of it)
QAction* GetNewShowConfigAction();
void InitSearch();
void ClearSearchResults();
QStandardItem* PlaylistBySpotifyIndex(int index) const;
bool DoPlaylistsDiffer(const cpb::spotify::Playlists& response) const;
private slots:
void EnsureServerCreated(const QString& username = QString(),
const QString& password = QString());
void BlobProcessError(QProcess::ProcessError error);
void LoginCompleted(bool success, const QString& error,
cpb::spotify::LoginResponse_Error error_code);
void AddCurrentSongToUserPlaylist(QAction* action);
void AddCurrentSongToStarredPlaylist();
void RemoveSongsFromUserPlaylist(int playlist_index,
const QList<int>& songs_indices_to_remove);
void RemoveSongsFromStarred(const QList<int>& songs_indices_to_remove);
void PlaylistsUpdated(const cpb::spotify::Playlists& response);
void InboxLoaded(const cpb::spotify::LoadPlaylistResponse& response);
void StarredLoaded(const cpb::spotify::LoadPlaylistResponse& response);
void UserPlaylistLoaded(const cpb::spotify::LoadPlaylistResponse& response);
void SearchResults(const cpb::spotify::SearchResponse& response);
void SyncPlaylistProgress(const cpb::spotify::SyncPlaylistProgress& progress);
void ToplistLoaded(const cpb::spotify::BrowseToplistResponse& response);
void GetCurrentSongUrlToShare() const;
void GetCurrentPlaylistUrlToShare() const;
void DoSearch();
void SyncPlaylist();
void BlobDownloadFinished();
private:
SpotifyServer* server_;
QString system_blob_path_;
QString local_blob_version_;
QString local_blob_path_;
QProcess* blob_process_;
QStandardItem* root_;
QStandardItem* search_;
QStandardItem* starred_;
QStandardItem* inbox_;
QStandardItem* toplist_;
QList<QStandardItem*> playlists_;
int login_task_id_;
QString pending_search_;
QMenu* playlist_context_menu_;
QMenu* song_context_menu_;
QAction* playlist_sync_action_;
QAction* get_url_to_share_playlist_;
QList<QAction*> playlistitem_actions_;
QAction* remove_from_playlist_;
QUrl current_song_url_;
QUrl current_playlist_url_;
SearchBoxWidget* search_box_;
QTimer* search_delay_;
int inbox_sync_id_;
int starred_sync_id_;
QMap<int, int> playlist_sync_ids_;
LoginState login_state_;
cpb::spotify::Bitrate bitrate_;
bool volume_normalisation_;
};
#endif // INTERNET_SPOTIFY_SPOTIFYSERVICE_H_

View File

@ -1,176 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, Andrea Decorte <adecorte@gmail.com>
Copyright 2011-2013, David Sansome <me@davidsansome.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "spotifysettingspage.h"
#include <QMessageBox>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSettings>
#include <QtDebug>
#include "config.h"
#include "core/network.h"
#include "internet/core/internetmodel.h"
#include "spotifymessages.pb.h"
#include "spotifyservice.h"
#include "ui/iconloader.h"
#include "ui_spotifysettingspage.h"
SpotifySettingsPage::SpotifySettingsPage(SettingsDialog* dialog)
: SettingsPage(dialog),
ui_(new Ui_SpotifySettingsPage),
service_(InternetModel::Service<SpotifyService>()),
validated_(false) {
ui_->setupUi(this);
setWindowIcon(IconLoader::Load("spotify", IconLoader::Provider));
QFont bold_font(font());
bold_font.setBold(true);
ui_->blob_status->setFont(bold_font);
connect(ui_->download_blob, SIGNAL(clicked()), SLOT(DownloadBlob()));
connect(ui_->login, SIGNAL(clicked()), SLOT(Login()));
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout()));
connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(Login()));
connect(service_, SIGNAL(LoginFinished(bool)), SLOT(LoginFinished(bool)));
connect(service_, SIGNAL(BlobStateChanged()), SLOT(BlobStateChanged()));
ui_->login_state->AddCredentialField(ui_->username);
ui_->login_state->AddCredentialField(ui_->password);
ui_->login_state->AddCredentialGroup(ui_->account_group);
ui_->bitrate->addItem("96 " + tr("kbps"), cpb::spotify::Bitrate96k);
ui_->bitrate->addItem("160 " + tr("kbps"), cpb::spotify::Bitrate160k);
ui_->bitrate->addItem("320 " + tr("kbps"), cpb::spotify::Bitrate320k);
BlobStateChanged();
}
SpotifySettingsPage::~SpotifySettingsPage() { delete ui_; }
void SpotifySettingsPage::BlobStateChanged() {
const bool installed = service_->IsBlobInstalled();
ui_->account_group->setEnabled(installed);
ui_->blob_status->setText(installed ? tr("Installed") : tr("Not installed"));
#ifdef HAVE_SPOTIFY_DOWNLOADER
ui_->download_blob->setEnabled(!installed);
#else
ui_->download_blob->hide();
#endif
}
void SpotifySettingsPage::DownloadBlob() { service_->InstallBlob(); }
void SpotifySettingsPage::Login() {
if (!service_->IsBlobInstalled()) {
return;
}
if (ui_->username->text() == original_username_ &&
ui_->password->text() == original_password_ &&
service_->login_state() == SpotifyService::LoginState_LoggedIn) {
return;
}
ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
service_->Login(ui_->username->text(), ui_->password->text());
}
void SpotifySettingsPage::Load() {
QSettings s;
s.beginGroup(SpotifyService::kSettingsGroup);
original_username_ = s.value("username").toString();
ui_->username->setText(original_username_);
validated_ = false;
ui_->bitrate->setCurrentIndex(ui_->bitrate->findData(
s.value("bitrate", cpb::spotify::Bitrate320k).toInt()));
ui_->volume_normalisation->setChecked(
s.value("volume_normalisation", false).toBool());
UpdateLoginState();
}
void SpotifySettingsPage::Save() {
QSettings s;
s.beginGroup(SpotifyService::kSettingsGroup);
s.setValue("username", ui_->username->text());
s.setValue("password", ui_->password->text());
s.setValue("bitrate",
ui_->bitrate->itemData(ui_->bitrate->currentIndex()).toInt());
s.setValue("volume_normalisation", ui_->volume_normalisation->isChecked());
}
void SpotifySettingsPage::LoginFinished(bool success) {
validated_ = success;
Save();
UpdateLoginState();
}
void SpotifySettingsPage::UpdateLoginState() {
const bool logged_in =
service_->login_state() == SpotifyService::LoginState_LoggedIn;
ui_->login_state->SetLoggedIn(
logged_in ? LoginStateWidget::LoggedIn : LoginStateWidget::LoggedOut,
ui_->username->text());
ui_->login_state->SetAccountTypeVisible(!logged_in);
switch (service_->login_state()) {
case SpotifyService::LoginState_NoPremium:
ui_->login_state->SetAccountTypeText(
tr("You do not have a Spotify Premium account."));
break;
case SpotifyService::LoginState_Banned:
case SpotifyService::LoginState_BadCredentials:
ui_->login_state->SetAccountTypeText(
tr("Your username or password was incorrect."));
break;
case SpotifyService::LoginState_ReloginFailed:
ui_->login_state->SetAccountTypeText(
tr("You have been logged out of Spotify, please re-enter your "
"password."));
break;
default:
ui_->login_state->SetAccountTypeText(
tr("A Spotify Premium account is required."));
break;
}
}
void SpotifySettingsPage::Logout() {
service_->Logout();
UpdateLoginState();
ui_->username->clear();
}

View File

@ -1,60 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INTERNET_SPOTIFY_SPOTIFYSETTINGSPAGE_H_
#define INTERNET_SPOTIFY_SPOTIFYSETTINGSPAGE_H_
#include "ui/settingspage.h"
class NetworkAccessManager;
class Ui_SpotifySettingsPage;
class SpotifyService;
class SpotifySettingsPage : public SettingsPage {
Q_OBJECT
public:
explicit SpotifySettingsPage(SettingsDialog* dialog);
~SpotifySettingsPage();
void Load();
void Save();
public slots:
void BlobStateChanged();
void DownloadBlob();
private slots:
void Login();
void LoginFinished(bool success);
void Logout();
private:
void UpdateLoginState();
private:
Ui_SpotifySettingsPage* ui_;
SpotifyService* service_;
bool validated_;
QString original_username_;
QString original_password_;
};
#endif // INTERNET_SPOTIFY_SPOTIFYSETTINGSPAGE_H_

View File

@ -1,211 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SpotifySettingsPage</class>
<widget class="QWidget" name="SpotifySettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>545</width>
<height>458</height>
</rect>
</property>
<property name="windowTitle">
<string>Spotify</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="LoginStateWidget" name="login_state" native="true"/>
</item>
<item>
<widget class="QGroupBox" name="account_group">
<property name="title">
<string>Account details</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QWidget" name="login_container" native="true">
<property name="enabled">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="margin">
<number>0</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="username_label">
<property name="text">
<string>Username</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="username"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="password_label">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="login">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Spotify plugin</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>For licensing reasons Spotify support is in a separate plugin.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Plugin status:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="blob_status"/>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="download_blob">
<property name="text">
<string>Download...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Preferences</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Preferred bitrate</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="bitrate"/>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="volume_normalisation">
<property name="text">
<string>Use volume normalisation</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>30</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="minimumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="pixmap">
<pixmap resource="../../../data/data.qrc">:/spotify-attribution.png</pixmap>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LoginStateWidget</class>
<extends>QWidget</extends>
<header>widgets/loginstatewidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../../../data/data.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -1,198 +0,0 @@
/* This file is part of Clementine.
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "spotifywebapiservice.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QtDebug>
#include <utility>
#include "3rdparty/qtiocompressor/qtiocompressor.h"
#include "core/application.h"
#include "core/network.h"
#include "core/timeconstants.h"
#include "globalsearch/globalsearch.h"
#include "globalsearch/spotifywebapisearchprovider.h"
#include "ui/iconloader.h"
namespace {
static constexpr const char* kServiceName = "SpotifyWebApi";
static constexpr const char* kGetAccessTokenUrl =
"https://open.spotify.com/"
"get_access_token?reason=transport&productType=web_player";
static constexpr const char* kSearchUrl =
"https://api.spotify.com/v1/search?q=%1&type=track&limit=50";
template <typename... Args>
inline QJsonValue Get(QJsonValue obj, Args&&... args) {
std::array<const char*, sizeof...(Args)> names = {
std::forward<Args>(args)...};
for (auto&& name : names) {
Q_ASSERT(obj.isObject());
obj = obj.toObject()[name];
}
return obj;
}
template <typename... Args>
inline QJsonValue Get(const QJsonDocument& obj, Args&&... args) {
return Get(obj.object(), std::forward<Args>(args)...);
}
QString concat(const QJsonArray& array, const char* name) {
QStringList ret;
for (auto&& item : array) {
ret << Get(item, name).toString();
}
return ret.join(", ");
}
} // namespace
SpotifyWebApiService::SpotifyWebApiService(Application* app,
InternetModel* parent)
: InternetService(kServiceName, app, parent, parent),
network_(new NetworkAccessManager{this}),
token_expiration_ms_{0} {
app_->global_search()->AddProvider(
new SpotifyWebApiSearchProvider(app_, this));
}
SpotifyWebApiService::~SpotifyWebApiService() {}
QStandardItem* SpotifyWebApiService::CreateRootItem() {
root_ = new QStandardItem(IconLoader::Load("spotify", IconLoader::Provider),
kServiceName);
return root_;
}
void SpotifyWebApiService::LazyPopulate(QStandardItem* item) {}
void SpotifyWebApiService::Search(int searchId, QString queryStr) {
if (QDateTime::currentDateTime().toMSecsSinceEpoch() >=
token_expiration_ms_) {
QNetworkRequest request{QUrl{kGetAccessTokenUrl}};
request.setRawHeader("Accept-Encoding", "gzip");
QNetworkReply* reply = network_->get(request);
connect(reply, &QNetworkReply::finished, [=]() {
reply->deleteLater();
OnTokenReady(ParseJsonReplyWithGzip(reply), searchId, queryStr);
});
} else {
OnReadyToSearch(searchId, queryStr);
}
}
void SpotifyWebApiService::OnTokenReady(const QJsonDocument& json, int searchId,
QString queryStr) {
if (!json.isEmpty()) {
token_ = Get(json, "accessToken").toString();
token_expiration_ms_ = static_cast<qint64>(
Get(json, "accessTokenExpirationTimestampMs").toDouble());
qLog(Debug) << "Spotify API Token:" << token_;
OnReadyToSearch(searchId, queryStr);
}
}
void SpotifyWebApiService::OnReadyToSearch(int searchId, QString queryStr) {
qLog(Debug) << "Spotify API Searching: " << queryStr;
QNetworkRequest request{
QUrl{QString(kSearchUrl).arg(queryStr.toHtmlEscaped())}};
request.setRawHeader("Accept-Encoding", "gzip");
request.setRawHeader("Authorization", ("Bearer " + token_).toUtf8());
QNetworkReply* reply = network_->get(request);
connect(reply, &QNetworkReply::finished, [=] {
reply->deleteLater();
BuildResultList(ParseJsonReplyWithGzip(reply), searchId);
});
}
void SpotifyWebApiService::BuildResultList(const QJsonDocument& json,
int searchId) {
QList<Song> result;
for (auto&& item : Get(json, "tracks", "items").toArray()) {
Song song;
song.set_albumartist(
concat(Get(item, "album", "artists").toArray(), "name"));
song.set_album(Get(item, "album", "name").toString());
song.set_artist(concat(Get(item, "artists").toArray(), "name"));
song.set_disc(Get(item, "disc_number").toInt());
song.set_length_nanosec(Get(item, "duration_ms").toInt() * kNsecPerMsec);
song.set_title(Get(item, "name").toString());
song.set_track(Get(item, "track_number").toInt());
song.set_url(QUrl{Get(item, "uri").toString()});
song.set_filetype(Song::Type_Stream);
song.set_valid(true);
song.set_directory_id(0);
song.set_mtime(0);
song.set_ctime(0);
song.set_filesize(0);
result += song;
}
emit SearchFinished(searchId, result);
}
QJsonDocument SpotifyWebApiService::ParseJsonReplyWithGzip(
QNetworkReply* reply) {
if (reply->error() != QNetworkReply::NoError) {
app_->AddError(tr("%1 request failed:\n%2")
.arg(kServiceName)
.arg(reply->errorString()));
return QJsonDocument();
}
QByteArray output;
if (reply->hasRawHeader("content-encoding") &&
reply->rawHeader("content-encoding") == "gzip") {
QtIOCompressor gzip(reply);
gzip.setStreamFormat(QtIOCompressor::GzipFormat);
if (!gzip.open(QIODevice::ReadOnly)) {
app_->AddError(tr("%1 failed to decode as gzip stream:\n%2")
.arg(kServiceName)
.arg(gzip.errorString()));
return QJsonDocument();
}
output = gzip.readAll();
} else {
output = reply->readAll();
}
QJsonParseError error;
QJsonDocument document = QJsonDocument::fromJson(output, &error);
if (error.error != QJsonParseError::NoError) {
app_->AddError(tr("Failed to parse %1 response:\n%2")
.arg(kServiceName)
.arg(error.errorString()));
return QJsonDocument();
}
return document;
}

View File

@ -1,60 +0,0 @@
/* This file is part of Clementine.
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SPOTIFYWEBAPISERVICE_H
#define SPOTIFYWEBAPISERVICE_H
#include <chrono>
#include "internet/core/internetmodel.h"
#include "internet/core/internetservice.h"
class NetworkAccessManager;
class SpotifyWebApiService : public InternetService {
Q_OBJECT
public:
SpotifyWebApiService(Application* app, InternetModel* parent);
~SpotifyWebApiService();
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* parent) override;
public:
void Search(int searchId, QString queryStr);
private:
void OnTokenReady(const QJsonDocument&, int searchId, QString queryStr);
void OnReadyToSearch(int searchId, QString queryStr);
void BuildResultList(const QJsonDocument&, int searchId);
signals:
void SearchFinished(int searchId, const QList<Song>&);
private:
QJsonDocument ParseJsonReplyWithGzip(QNetworkReply* reply);
private:
QStandardItem* root_;
NetworkAccessManager* network_;
QString token_;
qint64 token_expiration_ms_;
};
#endif // SPOTIFYWEBAPISERVICE_H

View File

@ -530,6 +530,11 @@ void SubsonicLibraryScanner::OnGetAlbumFinished(QNetworkReply* reply) {
// Read song information
while (reader.readNextStartElement()) {
// skip multi-artist and multi-genre tags
if ((reader.name() == "artists") || (reader.name() == "genres")) {
reader.skipCurrentElement();
continue;
}
if (reader.name() != "song") {
ParsingError("song tag expected. Aborting scan.");
return;

View File

@ -42,7 +42,7 @@ class SubsonicService : public InternetService {
public:
SubsonicService(Application* app, InternetModel* parent);
~SubsonicService();
~SubsonicService() override;
enum LoginState {
LoginState_Loggedin,
@ -90,11 +90,11 @@ class SubsonicService : public InternetService {
bool IsConfigured() const;
bool IsAmpache() const;
QStandardItem* CreateRootItem();
void LazyPopulate(QStandardItem* item);
void ShowContextMenu(const QPoint& global_pos);
QWidget* HeaderWidget() const;
void ReloadSettings();
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* item) override;
void ShowContextMenu(const QPoint& global_pos) override;
QWidget* HeaderWidget() const override;
void ReloadSettings() override;
void Login();
void Login(const QString& server, const QString& username,
@ -175,7 +175,7 @@ class SubsonicService : public InternetService {
void OnLoginStateChanged(SubsonicService::LoginState newstate);
void OnPingFinished(QNetworkReply* reply);
void ShowConfig();
void ShowConfig() override;
};
class SubsonicLibraryScanner : public QObject {
@ -184,7 +184,7 @@ class SubsonicLibraryScanner : public QObject {
public:
explicit SubsonicLibraryScanner(SubsonicService* service,
QObject* parent = nullptr);
~SubsonicLibraryScanner();
~SubsonicLibraryScanner() override;
void Scan();
const SongList& GetSongs() const { return songs_; }

View File

@ -896,7 +896,7 @@ LibraryBackend::AlbumList LibraryBackend::GetAlbums(const QString& artist,
QString last_artist;
QString last_album_artist;
while (query.Next()) {
bool compilation = query.Value(3).toBool() | query.Value(4).toBool();
bool compilation = query.Value(3).toBool() || query.Value(4).toBool();
Album info;
info.artist = compilation ? QString() : query.Value(1).toString();

View File

@ -92,12 +92,10 @@ void MoodbarBuilder::Normalize(QList<Rgb>* vals, double Rgb::*member) {
}
double avg = 0;
int t = 0;
for (const Rgb& rgb : *vals) {
const double value = rgb.*member;
if (value != mini && value != maxi) {
avg += value / vals->count();
t++;
}
}

View File

@ -98,7 +98,6 @@ void OutgoingDataCreator::CheckEnabledProviders() {
<< "lyricstime.com"
<< "lyricsreg.com"
<< "lyricsmania.com"
<< "metrolyrics.com"
<< "azlyrics.com"
<< "songlyrics.com"
<< "elyrics.net"
@ -106,7 +105,6 @@ void OutgoingDataCreator::CheckEnabledProviders() {
<< "lyrics.com"
<< "lyricsbay.com"
<< "directlyrics.com"
<< "loudson.gs"
<< "teksty.org"
<< "tekstowo.pl (Polish translations)"
<< "vagalume.uol.com.br"

View File

@ -1729,13 +1729,11 @@ PlaylistItemList Playlist::RemoveItemsWithoutUndo(int row, int count) {
endRemoveRows();
QList<int>::iterator it = virtual_items_.begin();
int i = 0;
while (it != virtual_items_.end()) {
if (*it >= items_.count())
it = virtual_items_.erase(it);
else
++it;
++i;
}
// Reset current_virtual_index_

View File

@ -34,12 +34,6 @@
#undef AddJob
#endif
namespace {
const char kWavHeaderRiffMarker[] = "RIFF";
const char kWavFileTypeFormatChunk[] = "WAVEfmt ";
const char kWavDataString[] = "data";
} // namespace
Ripper::Ripper(int track_count, QObject* parent)
: QObject(parent),
track_count_(track_count),

View File

@ -90,10 +90,8 @@ void SongInfoView::ReloadSettings() {
QVariantList default_order;
default_order << "lyrics.wikia.com"
<< "lyricstime.com"
<< "lyricsreg.com"
<< "lyricsmania.com"
<< "metrolyrics.com"
<< "azlyrics.com"
<< "songlyrics.com"
<< "elyrics.net"
@ -101,7 +99,6 @@ void SongInfoView::ReloadSettings() {
<< "lyrics.com"
<< "lyricsbay.com"
<< "directlyrics.com"
<< "loudson.gs"
<< "teksty.org"
<< "tekstowo.pl (Polish translations)"
<< "vagalume.uol.com.br"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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