Compare commits
256 Commits
1.4.0rc1-6
...
master
Author | SHA1 | Date |
---|---|---|
|
1506c27696 | |
|
d99cbb269b | |
|
3cca9bb98a | |
|
650bd81508 | |
|
7607ddcb96 | |
|
e249911937 | |
|
4ae57a4b5d | |
|
2f3464403b | |
|
f76dbffa6b | |
|
fbb266adc2 | |
|
9638ac70b3 | |
|
c93b4e1149 | |
|
d014a315c9 | |
|
df4181940d | |
|
ebe3c45476 | |
|
634910238d | |
|
62ed69fa3d | |
|
dd0a94e8a6 | |
|
1566148c50 | |
|
98a520552b | |
|
5968648aa1 | |
|
f3ddd7eee4 | |
|
19b44fb831 | |
|
994d16effa | |
|
4768cb9efb | |
|
7b678f26e0 | |
|
3f572a4139 | |
|
f3837f95db | |
|
6820a0a58d | |
|
cfcddf7c0f | |
|
98e24f626b | |
|
8e47ab59e5 | |
|
63208b4e1f | |
|
20773dee29 | |
|
c2a5b9b07e | |
|
de7455eebd | |
|
2a14ec9d4d | |
|
86e81cea05 | |
|
10570316dd | |
|
ad8fd81ba9 | |
|
6ff5768634 | |
|
08bfb88912 | |
|
d3108b32e8 | |
|
0701bef103 | |
|
bf4ac0cb46 | |
|
baf05335f9 | |
|
d21e9697d0 | |
|
ab057f8275 | |
|
58325e45a7 | |
|
1d0cbc0ebb | |
|
c83a0ac25f | |
|
351a5e2547 | |
|
8773e8fe0a | |
|
3471134d52 | |
|
26192c3469 | |
|
982d8fbb63 | |
|
20cf7f793b | |
|
ccf4f75c3d | |
|
65319d4952 | |
|
9ef681b0e9 | |
|
dfeb1182f9 | |
|
384a8850d9 | |
|
0fab612784 | |
|
336770bb95 | |
|
101f450aaa | |
|
6a440fe397 | |
|
3a506e0917 | |
|
c716ddb722 | |
|
519b33ed81 | |
|
f2011e7e26 | |
|
42853b7b52 | |
|
1c69e343b9 | |
|
770080b80b | |
|
3e9e251e90 | |
|
72c2336d94 | |
|
495803ab99 | |
|
7da98fbbcc | |
|
2055fd51fa | |
|
39124eda38 | |
|
b567760ae1 | |
|
2d3a604b85 | |
|
ce4a22bd5f | |
|
e6a7539480 | |
|
a551c40c4e | |
|
99029ed643 | |
|
cf8047b4ce | |
|
f59c9f4b2b | |
|
71eac9bb3b | |
|
3fd467591a | |
|
a0ae9210dd | |
|
c1fa38120d | |
|
13352c5802 | |
|
5e5b888d41 | |
|
662ac60eb1 | |
|
9be5b9805d | |
|
9de903d42d | |
|
454678256e | |
|
d3c847b38c | |
|
398893117e | |
|
bbda59a5f3 | |
|
bebd0b5d3c | |
|
250024e117 | |
|
9168299c0f | |
|
24d4b6e7f2 | |
|
644405ec7a | |
|
2fb964fc29 | |
|
cf31624836 | |
|
d05616e37c | |
|
0b5faa7550 | |
|
c0b42ace6d | |
|
810f0b0acb | |
|
c2b8a35642 | |
|
2b340da79f | |
|
6698723991 | |
|
7175ee4d37 | |
|
20c6ae6c14 | |
|
59d1c94b90 | |
|
9d143334e2 | |
|
4797edbc8a | |
|
01f72b575d | |
|
dcbb3f8a58 | |
|
3acf26015b | |
|
333203c972 | |
|
63b806dbb7 | |
|
a8d529ca14 | |
|
111379dfd0 | |
|
4821bd50c2 | |
|
c3a0bd69fd | |
|
5487d0632c | |
|
98b68afc28 | |
|
15b819fea3 | |
|
e2f6ec8e12 | |
|
efa0530ed9 | |
|
a504c1d391 | |
|
794c1b8c92 | |
|
f35e1b543d | |
|
497552aab2 | |
|
9487f67f64 | |
|
19a86ba2e4 | |
|
1aaf74788c | |
|
7ce9928779 | |
|
af890f0736 | |
|
09ccf93b98 | |
|
f237795850 | |
|
d79f837ddb | |
|
e69ceb25df | |
|
ab37de5e8f | |
|
dd1393ea3a | |
|
6b6547095a | |
|
24a766d0e5 | |
|
cefe81d0c1 | |
|
0895297297 | |
|
3a40be6706 | |
|
bb618efc5d | |
|
245f64a882 | |
|
0be314337d | |
|
63eb7aa743 | |
|
9dd008da2c | |
|
b1e750c52c | |
|
41539d0c02 | |
|
44dbc95554 | |
|
1d8139e462 | |
|
2d0518a5a8 | |
|
d5986a4820 | |
|
590ab22f8d | |
|
b747423b5a | |
|
15e45c9ec6 | |
|
d033b38c4b | |
|
2469763b9b | |
|
e7b1c06341 | |
|
a25887be5c | |
|
568ff1f9da | |
|
174fc515ee | |
|
7b8b477d07 | |
|
b9dbcb78db | |
|
c29b1e10d2 | |
|
f8c167c9c6 | |
|
e5023535d2 | |
|
1b3b621957 | |
|
2dc8df7e23 | |
|
4eebf5747d | |
|
c24927a03b | |
|
424dbd44e8 | |
|
68bc9d9ebb | |
|
294620fe66 | |
|
21f038c156 | |
|
5705d4fd85 | |
|
86b958015b | |
|
f8f849e49c | |
|
69fd49b977 | |
|
62922147e6 | |
|
2e133f7ce4 | |
|
0820035b84 | |
|
679a0ee544 | |
|
8715815452 | |
|
b762aeb1ba | |
|
fd585e8aa4 | |
|
2936578fa4 | |
|
62b5a0e77b | |
|
50404a967b | |
|
6b03b8f5d1 | |
|
90ec6f6a24 | |
|
b020171da7 | |
|
c969bf9783 | |
|
082f941bb9 | |
|
cd72cf3390 | |
|
ba29b0e3ba | |
|
ab6a480131 | |
|
f548884f57 | |
|
1535e78aa0 | |
|
2cca75d930 | |
|
e556a59aea | |
|
e187a68e9f | |
|
c58335c6c9 | |
|
769d8bbe6d | |
|
3b7d5880f9 | |
|
7eb62b6266 | |
|
628ff65828 | |
|
c8c110efaf | |
|
a72e252ec6 | |
|
b0704331d7 | |
|
5c8ca3754f | |
|
83d961f808 | |
|
a6fef97cac | |
|
922afe506f | |
|
8682d4de48 | |
|
03e13c69e7 | |
|
327d5fdac3 | |
|
b55e54388f | |
|
cddc08e148 | |
|
bf424ce986 | |
|
e2d6759d55 | |
|
102317e5c8 | |
|
ac3a0d33f7 | |
|
224c475b50 | |
|
dbe15e5e9f | |
|
c0c9037677 | |
|
6982b47819 | |
|
98dd3e48a6 | |
|
1e39ce29a4 | |
|
daa2f25e3c | |
|
f379ad84d4 | |
|
1d1d3b157f | |
|
320a1b81c9 | |
|
0c1b6a2a44 | |
|
598e660aeb | |
|
4d34748401 | |
|
b3b769f0e7 | |
|
2902a8786e | |
|
4acfdae740 | |
|
6b2918ee92 | |
|
a5e84bbe98 | |
|
7cb5f5c804 | |
|
db8de64abb | |
|
a5fd484a61 | |
|
25b537cf26 |
|
@ -10,7 +10,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
@ -25,10 +25,15 @@ jobs:
|
||||||
push_translations:
|
push_translations:
|
||||||
name: Push translation sources to Transifex
|
name: Push translation sources to Transifex
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: ubuntu:bionic
|
image: ubuntu:bionic
|
||||||
steps:
|
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
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
@ -58,20 +63,20 @@ jobs:
|
||||||
libtag1-dev
|
libtag1-dev
|
||||||
pkg-config
|
pkg-config
|
||||||
protobuf-compiler
|
protobuf-compiler
|
||||||
python-pip
|
python3-pip
|
||||||
qtbase5-dev
|
qtbase5-dev
|
||||||
qttools5-dev-tools
|
qttools5-dev-tools
|
||||||
qttools5-dev
|
qttools5-dev
|
||||||
libsparsehash-dev
|
libsparsehash-dev
|
||||||
ssh
|
ssh
|
||||||
- name: Install tx
|
|
||||||
run: pip install transifex-client==0.13.9
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v1.2.0
|
uses: actions/checkout@v1.2.0
|
||||||
|
- name: git hackery
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
- name: tx init
|
- name: tx init
|
||||||
env:
|
env:
|
||||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||||
run: tx init --no-interactive --force
|
run: tx init
|
||||||
- name: cmake
|
- name: cmake
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run: cmake ..
|
run: cmake ..
|
||||||
|
@ -81,7 +86,7 @@ jobs:
|
||||||
- name: tx config
|
- name: tx config
|
||||||
env:
|
env:
|
||||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
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
|
- name: tx push
|
||||||
env:
|
env:
|
||||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||||
|
@ -89,20 +94,20 @@ jobs:
|
||||||
|
|
||||||
create_release:
|
create_release:
|
||||||
name: Create GitHub Release
|
name: Create GitHub Release
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
needs:
|
needs:
|
||||||
- build_bionic_64
|
- build_bionic_64
|
||||||
|
- build_bullseye_64
|
||||||
- build_buster_64
|
- build_buster_64
|
||||||
- build_fedora_33
|
- build_fedora_36
|
||||||
- build_fedora_34
|
- build_fedora_37
|
||||||
|
- build_fedora_38
|
||||||
- build_focal_64
|
- build_focal_64
|
||||||
- build_groovy_64
|
- build_jammy_64
|
||||||
- build_hirsute_64
|
# - build_mac
|
||||||
- build_mac
|
|
||||||
- build_mingw
|
- build_mingw
|
||||||
- build_source
|
- build_source
|
||||||
- build_stretch_64
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1.2.0
|
- uses: actions/checkout@v1.2.0
|
||||||
- uses: actions/download-artifact@v2
|
- uses: actions/download-artifact@v2
|
||||||
|
@ -124,7 +129,7 @@ jobs:
|
||||||
|
|
||||||
build_source:
|
build_source:
|
||||||
name: Build source tarball
|
name: Build source tarball
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: ubuntu:focal
|
image: ubuntu:focal
|
||||||
steps:
|
steps:
|
||||||
|
@ -169,6 +174,8 @@ jobs:
|
||||||
qttools5-dev
|
qttools5-dev
|
||||||
ssh
|
ssh
|
||||||
- uses: actions/checkout@v1.2.0
|
- uses: actions/checkout@v1.2.0
|
||||||
|
- name: git hackery
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
- name: cmake
|
- name: cmake
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run: cmake ..
|
run: cmake ..
|
||||||
|
@ -180,11 +187,11 @@ jobs:
|
||||||
name: release_source
|
name: release_source
|
||||||
path: bin/clementine-*.tar.xz
|
path: bin/clementine-*.tar.xz
|
||||||
|
|
||||||
build_fedora_34:
|
build_fedora_36:
|
||||||
name: Build Fedora 34 RPM
|
name: Build Fedora 36 RPM
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: fedora:34
|
image: fedora:36
|
||||||
env:
|
env:
|
||||||
RPM_BUILD_NCPUS: "2"
|
RPM_BUILD_NCPUS: "2"
|
||||||
steps:
|
steps:
|
||||||
|
@ -224,6 +231,8 @@ jobs:
|
||||||
qt5-qtbase-devel
|
qt5-qtbase-devel
|
||||||
qt5-qtx11extras-devel
|
qt5-qtx11extras-devel
|
||||||
qt5-rpm-macros
|
qt5-rpm-macros
|
||||||
|
qtsingleapplication-qt5-devel
|
||||||
|
qtsinglecoreapplication-qt5-devel
|
||||||
rpmdevtools
|
rpmdevtools
|
||||||
sha2-devel
|
sha2-devel
|
||||||
sparsehash-devel
|
sparsehash-devel
|
||||||
|
@ -231,9 +240,11 @@ jobs:
|
||||||
taglib-devel
|
taglib-devel
|
||||||
tar
|
tar
|
||||||
- uses: actions/checkout@v1.2.0
|
- uses: actions/checkout@v1.2.0
|
||||||
|
- name: git hackery
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
- name: cmake
|
- name: cmake
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run: cmake ..
|
run: cmake -DUSE_SYSTEM_QTSINGLEAPPLICATION=On ..
|
||||||
- name: Build source tarball
|
- name: Build source tarball
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run: ../dist/maketarball.sh
|
run: ../dist/maketarball.sh
|
||||||
|
@ -247,14 +258,14 @@ jobs:
|
||||||
run: rpmbuild -ba ../dist/clementine.spec
|
run: rpmbuild -ba ../dist/clementine.spec
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: release_fedora_34
|
name: release_fedora_36
|
||||||
path: ~/rpmbuild/RPMS/*/clementine-*.rpm
|
path: ~/rpmbuild/RPMS/*/clementine-*.rpm
|
||||||
|
|
||||||
build_fedora_33:
|
build_fedora_37:
|
||||||
name: Build Fedora 33 RPM
|
name: Build Fedora 37 RPM
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: fedora:33
|
image: fedora:37
|
||||||
env:
|
env:
|
||||||
RPM_BUILD_NCPUS: "2"
|
RPM_BUILD_NCPUS: "2"
|
||||||
steps:
|
steps:
|
||||||
|
@ -294,6 +305,8 @@ jobs:
|
||||||
qt5-qtbase-devel
|
qt5-qtbase-devel
|
||||||
qt5-qtx11extras-devel
|
qt5-qtx11extras-devel
|
||||||
qt5-rpm-macros
|
qt5-rpm-macros
|
||||||
|
qtsingleapplication-qt5-devel
|
||||||
|
qtsinglecoreapplication-qt5-devel
|
||||||
rpmdevtools
|
rpmdevtools
|
||||||
sha2-devel
|
sha2-devel
|
||||||
sparsehash-devel
|
sparsehash-devel
|
||||||
|
@ -301,9 +314,11 @@ jobs:
|
||||||
taglib-devel
|
taglib-devel
|
||||||
tar
|
tar
|
||||||
- uses: actions/checkout@v1.2.0
|
- uses: actions/checkout@v1.2.0
|
||||||
|
- name: git hackery
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
- name: cmake
|
- name: cmake
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run: cmake ..
|
run: cmake -DUSE_SYSTEM_QTSINGLEAPPLICATION=On ..
|
||||||
- name: Build source tarball
|
- name: Build source tarball
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run: ../dist/maketarball.sh
|
run: ../dist/maketarball.sh
|
||||||
|
@ -317,12 +332,87 @@ jobs:
|
||||||
run: rpmbuild -ba ../dist/clementine.spec
|
run: rpmbuild -ba ../dist/clementine.spec
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: release_fedora_33
|
name: release_fedora_37
|
||||||
path: ~/rpmbuild/RPMS/*/clementine-*.rpm
|
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:
|
build_mingw:
|
||||||
name: Build Windows Installer
|
name: Build Windows Installer
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: eu.gcr.io/clementine-data/mingw-w64:latest
|
image: eu.gcr.io/clementine-data/mingw-w64:latest
|
||||||
env:
|
env:
|
||||||
|
@ -331,6 +421,8 @@ jobs:
|
||||||
- name: Fix liblastfm includes
|
- name: Fix liblastfm includes
|
||||||
run: ln -s /target/include/lastfm /target/include/lastfm5
|
run: ln -s /target/include/lastfm /target/include/lastfm5
|
||||||
- uses: actions/checkout@v1.2.0
|
- uses: actions/checkout@v1.2.0
|
||||||
|
- name: git hackery
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
- name: cmake
|
- name: cmake
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run: >
|
run: >
|
||||||
|
@ -511,67 +603,9 @@ jobs:
|
||||||
name: release_mingw
|
name: release_mingw
|
||||||
path: dist/windows/ClementineSetup*.exe
|
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:
|
build_bionic_64:
|
||||||
name: Build Ubuntu Bionic 64-bit deb
|
name: Build Ubuntu Bionic 64-bit deb
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: ubuntu:bionic
|
image: ubuntu:bionic
|
||||||
steps:
|
steps:
|
||||||
|
@ -610,6 +644,8 @@ jobs:
|
||||||
libsparsehash-dev
|
libsparsehash-dev
|
||||||
ssh
|
ssh
|
||||||
- uses: actions/checkout@v1.2.0
|
- uses: actions/checkout@v1.2.0
|
||||||
|
- name: git hackery
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
- name: cmake
|
- name: cmake
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run: >
|
run: >
|
||||||
|
@ -617,7 +653,6 @@ jobs:
|
||||||
-DWITH_DEBIAN=ON
|
-DWITH_DEBIAN=ON
|
||||||
-DDEB_ARCH=amd64
|
-DDEB_ARCH=amd64
|
||||||
-DDEB_DIST=bionic
|
-DDEB_DIST=bionic
|
||||||
-DFORCE_GIT_VERSION=
|
|
||||||
-DENABLE_SPOTIFY_BLOB=OFF
|
-DENABLE_SPOTIFY_BLOB=OFF
|
||||||
- name: make
|
- name: make
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
|
@ -627,9 +662,69 @@ jobs:
|
||||||
name: release_bionic_64
|
name: release_bionic_64
|
||||||
path: bin/clementine_*.deb
|
path: bin/clementine_*.deb
|
||||||
|
|
||||||
|
build_bullseye_64:
|
||||||
|
name: Build Debian Bullseye 64-bit deb
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
container:
|
||||||
|
image: debian:bullseye
|
||||||
|
steps:
|
||||||
|
- name: Install dependencies
|
||||||
|
run: >
|
||||||
|
apt-get update && apt-get install -y
|
||||||
|
build-essential
|
||||||
|
cmake
|
||||||
|
debhelper
|
||||||
|
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: 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=bullseye
|
||||||
|
-DENABLE_SPOTIFY_BLOB=OFF
|
||||||
|
- name: make
|
||||||
|
working-directory: bin
|
||||||
|
run : make -j2 deb
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: release_bullseye_64
|
||||||
|
path: bin/clementine_*.deb
|
||||||
|
|
||||||
build_buster_64:
|
build_buster_64:
|
||||||
name: Build Debian Buster 64-bit deb
|
name: Build Debian Buster 64-bit deb
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: debian:buster
|
image: debian:buster
|
||||||
steps:
|
steps:
|
||||||
|
@ -668,6 +763,8 @@ jobs:
|
||||||
qttools5-dev
|
qttools5-dev
|
||||||
ssh
|
ssh
|
||||||
- uses: actions/checkout@v1.2.0
|
- uses: actions/checkout@v1.2.0
|
||||||
|
- name: git hackery
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
- name: cmake
|
- name: cmake
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run: >
|
run: >
|
||||||
|
@ -675,7 +772,6 @@ jobs:
|
||||||
-DWITH_DEBIAN=ON
|
-DWITH_DEBIAN=ON
|
||||||
-DDEB_ARCH=amd64
|
-DDEB_ARCH=amd64
|
||||||
-DDEB_DIST=buster
|
-DDEB_DIST=buster
|
||||||
-DFORCE_GIT_VERSION=
|
|
||||||
-DENABLE_SPOTIFY_BLOB=OFF
|
-DENABLE_SPOTIFY_BLOB=OFF
|
||||||
- name: make
|
- name: make
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
|
@ -687,7 +783,7 @@ jobs:
|
||||||
|
|
||||||
build_focal_64:
|
build_focal_64:
|
||||||
name: Build Ubuntu Focal 64-bit deb
|
name: Build Ubuntu Focal 64-bit deb
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: ubuntu:focal
|
image: ubuntu:focal
|
||||||
steps:
|
steps:
|
||||||
|
@ -732,6 +828,8 @@ jobs:
|
||||||
qttools5-dev
|
qttools5-dev
|
||||||
ssh
|
ssh
|
||||||
- uses: actions/checkout@v1.2.0
|
- uses: actions/checkout@v1.2.0
|
||||||
|
- name: git hackery
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
- name: cmake
|
- name: cmake
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run: >
|
run: >
|
||||||
|
@ -739,7 +837,6 @@ jobs:
|
||||||
-DWITH_DEBIAN=ON
|
-DWITH_DEBIAN=ON
|
||||||
-DDEB_ARCH=amd64
|
-DDEB_ARCH=amd64
|
||||||
-DDEB_DIST=focal
|
-DDEB_DIST=focal
|
||||||
-DFORCE_GIT_VERSION=
|
|
||||||
-DENABLE_SPOTIFY_BLOB=OFF
|
-DENABLE_SPOTIFY_BLOB=OFF
|
||||||
- name: make
|
- name: make
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
|
@ -749,11 +846,11 @@ jobs:
|
||||||
name: release_focal_64
|
name: release_focal_64
|
||||||
path: bin/clementine_*.deb
|
path: bin/clementine_*.deb
|
||||||
|
|
||||||
build_groovy_64:
|
build_jammy_64:
|
||||||
name: Build Ubuntu Groovy 64-bit deb
|
name: Build Ubuntu Jammy 64-bit deb
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: ubuntu:groovy
|
image: ubuntu:jammy
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
|
@ -776,7 +873,7 @@ jobs:
|
||||||
libcrypto++-dev
|
libcrypto++-dev
|
||||||
libdbus-1-dev
|
libdbus-1-dev
|
||||||
libfftw3-dev
|
libfftw3-dev
|
||||||
libglew1.5-dev
|
libglew-dev
|
||||||
libgpod-dev
|
libgpod-dev
|
||||||
libgstreamer-plugins-base1.0-dev
|
libgstreamer-plugins-base1.0-dev
|
||||||
libgstreamer1.0-dev
|
libgstreamer1.0-dev
|
||||||
|
@ -799,95 +896,32 @@ jobs:
|
||||||
qttools5-dev
|
qttools5-dev
|
||||||
ssh
|
ssh
|
||||||
- uses: actions/checkout@v1.2.0
|
- uses: actions/checkout@v1.2.0
|
||||||
|
- name: git hackery
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
- name: cmake
|
- name: cmake
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run: >
|
run: >
|
||||||
cmake ..
|
cmake ..
|
||||||
-DWITH_DEBIAN=ON
|
-DWITH_DEBIAN=ON
|
||||||
-DDEB_ARCH=amd64
|
-DDEB_ARCH=amd64
|
||||||
-DDEB_DIST=groovy
|
-DDEB_DIST=jammy
|
||||||
-DFORCE_GIT_VERSION=
|
|
||||||
-DENABLE_SPOTIFY_BLOB=OFF
|
-DENABLE_SPOTIFY_BLOB=OFF
|
||||||
- name: make
|
- name: make
|
||||||
working-directory: bin
|
working-directory: bin
|
||||||
run : make -j2 deb
|
run : make -j2 deb
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: release_groovy_64
|
name: release_jammy_64
|
||||||
path: bin/clementine_*.deb
|
|
||||||
|
|
||||||
build_hirsute_64:
|
|
||||||
name: Build Ubuntu Hirsute 64-bit deb
|
|
||||||
runs-on: ubuntu-18.04
|
|
||||||
container:
|
|
||||||
image: ubuntu:hirsute
|
|
||||||
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=hirsute
|
|
||||||
-DFORCE_GIT_VERSION=
|
|
||||||
-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
|
path: bin/clementine_*.deb
|
||||||
|
|
||||||
build_mac:
|
build_mac:
|
||||||
|
if: false
|
||||||
name: Build Mac DMG
|
name: Build Mac DMG
|
||||||
runs-on: macos-10.15
|
runs-on: macos-10.15
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1.2.0
|
- uses: actions/checkout@v1.2.0
|
||||||
|
- name: git hackery
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: brew bundle
|
run: brew bundle
|
||||||
|
|
||||||
|
|
|
@ -6,36 +6,40 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
pull_translations:
|
pull_translations:
|
||||||
name: Pull translations from Transifex
|
name: Pull translations from Transifex
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: ubuntu:bionic
|
image: ubuntu:jammy
|
||||||
steps:
|
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
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
run: >
|
run: >
|
||||||
apt-get update && apt-get install -y
|
apt-get update && apt-get install -y
|
||||||
git
|
git
|
||||||
python-pip
|
|
||||||
ssh
|
ssh
|
||||||
- name: Install tx
|
|
||||||
run: pip install transifex-client==0.13.9
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v1.2.0
|
uses: actions/checkout@v1.2.0
|
||||||
|
- name: git hackery
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
- name: Switch to master
|
- name: Switch to master
|
||||||
run: git checkout master
|
run: git checkout master
|
||||||
- name: tx init
|
- name: tx init
|
||||||
env:
|
env:
|
||||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||||
run: tx init --no-interactive --force
|
run: tx init
|
||||||
- name: tx config
|
- name: tx config
|
||||||
env:
|
env:
|
||||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
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
|
- name: tx pull
|
||||||
env:
|
env:
|
||||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||||
run: tx pull --all -f --no-interactive
|
run: tx pull -f -a
|
||||||
- name: Setup git SSH
|
- name: Setup git SSH
|
||||||
uses: webfactory/ssh-agent@v0.4.1
|
uses: webfactory/ssh-agent@v0.4.1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -44,3 +44,4 @@ dist/windows/Python27.zip
|
||||||
src/translations/translations.pot
|
src/translations/translations.pot
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
|
*.kdev4
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -15,4 +15,4 @@ ADD_LIBRARY(qtiocompressor STATIC
|
||||||
${IOCOMPRESSOR-SOURCES-MOC}
|
${IOCOMPRESSOR-SOURCES-MOC}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(qtiocompressor Qt5::Core)
|
target_link_libraries(qtiocompressor Qt5::Core ${ZLIB_LIBRARIES})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
cmake_minimum_required(VERSION 3.0.0)
|
||||||
project(clementine)
|
project(clementine)
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.0.0)
|
|
||||||
cmake_policy(SET CMP0053 OLD)
|
cmake_policy(SET CMP0053 OLD)
|
||||||
|
|
||||||
include(CheckCXXCompilerFlag)
|
include(CheckCXXCompilerFlag)
|
||||||
|
@ -13,6 +13,7 @@ include(cmake/SpotifyVersion.cmake)
|
||||||
include(cmake/OptionalSource.cmake)
|
include(cmake/OptionalSource.cmake)
|
||||||
include(cmake/Format.cmake)
|
include(cmake/Format.cmake)
|
||||||
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
|
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "")
|
||||||
|
|
||||||
if (CMAKE_CXX_COMPILER MATCHES ".*clang")
|
if (CMAKE_CXX_COMPILER MATCHES ".*clang")
|
||||||
set(CMAKE_COMPILER_IS_CLANGXX 1)
|
set(CMAKE_COMPILER_IS_CLANGXX 1)
|
||||||
|
@ -59,7 +60,6 @@ find_library(PROTOBUF_STATIC_LIBRARY libprotobuf.a libprotobuf)
|
||||||
|
|
||||||
pkg_check_modules(CDIO libcdio)
|
pkg_check_modules(CDIO libcdio)
|
||||||
pkg_check_modules(CHROMAPRINT REQUIRED libchromaprint)
|
pkg_check_modules(CHROMAPRINT REQUIRED libchromaprint)
|
||||||
pkg_search_module(CRYPTOPP cryptopp libcrypto++)
|
|
||||||
pkg_check_modules(GIO gio-2.0)
|
pkg_check_modules(GIO gio-2.0)
|
||||||
pkg_check_modules(GLIB REQUIRED glib-2.0)
|
pkg_check_modules(GLIB REQUIRED glib-2.0)
|
||||||
pkg_check_modules(GOBJECT REQUIRED gobject-2.0)
|
pkg_check_modules(GOBJECT REQUIRED gobject-2.0)
|
||||||
|
@ -74,12 +74,9 @@ pkg_check_modules(LIBMTP libmtp>=1.0)
|
||||||
pkg_check_modules(LIBMYGPO_QT5 libmygpo-qt5>=1.0.9)
|
pkg_check_modules(LIBMYGPO_QT5 libmygpo-qt5>=1.0.9)
|
||||||
pkg_check_modules(LIBPULSE libpulse)
|
pkg_check_modules(LIBPULSE libpulse)
|
||||||
pkg_check_modules(LIBXML libxml-2.0)
|
pkg_check_modules(LIBXML libxml-2.0)
|
||||||
pkg_check_modules(LIBSPOTIFY libspotify>=12.1.45)
|
|
||||||
pkg_check_modules(TAGLIB taglib)
|
pkg_check_modules(TAGLIB taglib)
|
||||||
|
|
||||||
if (WIN32)
|
find_package(ZLIB REQUIRED)
|
||||||
find_package(ZLIB REQUIRED)
|
|
||||||
endif (WIN32)
|
|
||||||
|
|
||||||
find_library(LASTFM5_LIBRARIES lastfm5)
|
find_library(LASTFM5_LIBRARIES lastfm5)
|
||||||
find_path(LASTFM5_INCLUDE_DIRS lastfm5/ws.h)
|
find_path(LASTFM5_INCLUDE_DIRS lastfm5/ws.h)
|
||||||
|
@ -166,12 +163,6 @@ endif()
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
find_library(SPARKLE Sparkle)
|
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)
|
add_subdirectory(3rdparty/SPMediaKeyTap)
|
||||||
set(SPMEDIAKEYTAP_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/SPMediaKeyTap)
|
set(SPMEDIAKEYTAP_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/SPMediaKeyTap)
|
||||||
|
@ -295,19 +286,6 @@ optional_component(UDISKS2 ON "Devices: UDisks2 backend"
|
||||||
DEPENDS "D-Bus support" Qt5DBus_FOUND
|
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"
|
optional_component(MOODBAR ON "Moodbar support"
|
||||||
DEPENDS "fftw3" FFTW3_FOUND
|
DEPENDS "fftw3" FFTW3_FOUND
|
||||||
)
|
)
|
||||||
|
@ -339,13 +317,6 @@ if (APPLE AND USE_BUNDLE AND NOT USE_BUNDLE_DIR)
|
||||||
set(USE_BUNDLE_DIR "../PlugIns")
|
set(USE_BUNDLE_DIR "../PlugIns")
|
||||||
endif()
|
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
|
# Remove GLU and GL from the link line - they're not really required
|
||||||
# and don't exist on my mingw toolchain
|
# and don't exist on my mingw toolchain
|
||||||
list(REMOVE_ITEM QT_LIBRARIES "-lGLU -lGL")
|
list(REMOVE_ITEM QT_LIBRARIES "-lGLU -lGL")
|
||||||
|
@ -376,11 +347,18 @@ include_directories("3rdparty/qsqlite")
|
||||||
|
|
||||||
# When/if upstream accepts our patches then these options can be used to link
|
# When/if upstream accepts our patches then these options can be used to link
|
||||||
# to system installed qtsingleapplication instead.
|
# 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)
|
if(USE_SYSTEM_QTSINGLEAPPLICATION)
|
||||||
find_path(QTSINGLEAPPLICATION_INCLUDE_DIRS qtsingleapplication.h PATH_SUFFIXES qt5/QtSolutions)
|
find_path(QTSINGLEAPPLICATION_INCLUDE_DIRS qtsingleapplication.h PATH_SUFFIXES qt5/QtSolutions REQUIRED)
|
||||||
find_library(QTSINGLEAPPLICATION_LIBRARIES Qt5Solutions_SingleApplication-2.6)
|
find_library(QTSINGLEAPPLICATION_LIBRARIES Qt5Solutions_SingleApplication-2.6 REQUIRED)
|
||||||
find_library(QTSINGLECOREAPPLICATION_LIBRARIES Qt5Solutions_SingleCoreApplication-2.6)
|
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)
|
else(USE_SYSTEM_QTSINGLEAPPLICATION)
|
||||||
add_subdirectory(3rdparty/qtsingleapplication)
|
add_subdirectory(3rdparty/qtsingleapplication)
|
||||||
set(QTSINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/qtsingleapplication)
|
set(QTSINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/qtsingleapplication)
|
||||||
|
@ -453,9 +431,6 @@ add_subdirectory(ext/libclementine-common)
|
||||||
add_subdirectory(ext/libclementine-tagreader)
|
add_subdirectory(ext/libclementine-tagreader)
|
||||||
add_subdirectory(ext/clementine-tagreader)
|
add_subdirectory(ext/clementine-tagreader)
|
||||||
add_subdirectory(ext/libclementine-remote)
|
add_subdirectory(ext/libclementine-remote)
|
||||||
if(HAVE_SPOTIFY)
|
|
||||||
add_subdirectory(ext/libclementine-spotifyblob)
|
|
||||||
endif(HAVE_SPOTIFY)
|
|
||||||
|
|
||||||
option(WITH_DEBIAN OFF)
|
option(WITH_DEBIAN OFF)
|
||||||
if(WITH_DEBIAN)
|
if(WITH_DEBIAN)
|
||||||
|
@ -466,10 +441,6 @@ if(HAVE_BREAKPAD)
|
||||||
add_subdirectory(3rdparty/google-breakpad)
|
add_subdirectory(3rdparty/google-breakpad)
|
||||||
endif(HAVE_BREAKPAD)
|
endif(HAVE_BREAKPAD)
|
||||||
|
|
||||||
if(HAVE_SPOTIFY_BLOB)
|
|
||||||
add_subdirectory(ext/clementine-spotifyblob)
|
|
||||||
endif(HAVE_SPOTIFY_BLOB)
|
|
||||||
|
|
||||||
if(HAVE_MOODBAR)
|
if(HAVE_MOODBAR)
|
||||||
add_subdirectory(gst/moodbar)
|
add_subdirectory(gst/moodbar)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -41,8 +41,8 @@ Compile and install:
|
||||||
|
|
||||||
cd bin
|
cd bin
|
||||||
cmake ..
|
cmake ..
|
||||||
make -j8
|
make -j$(nproc)
|
||||||
sudo make install
|
sudo make install
|
||||||
|
|
||||||
See the Wiki for more instructions and a list of dependencies:
|
See the Wiki for more instructions and a list of dependencies:
|
||||||
https://github.com/clementine-player/Clementine/wiki/Compiling-from-Source
|
https://github.com/clementine-player/Clementine/wiki#compiling-and-installing-clementine
|
||||||
|
|
|
@ -124,42 +124,43 @@ if(FORCE_GIT_REVISION)
|
||||||
set(GIT_INFO_RESULT 0)
|
set(GIT_INFO_RESULT 0)
|
||||||
else(FORCE_GIT_REVISION)
|
else(FORCE_GIT_REVISION)
|
||||||
find_program(GIT_EXECUTABLE git)
|
find_program(GIT_EXECUTABLE git)
|
||||||
|
message(STATUS "Found git: ${GIT_EXECUTABLE}")
|
||||||
|
|
||||||
if(NOT GIT_EXECUTABLE-NOTFOUND)
|
if(NOT GIT_EXECUTABLE-NOTFOUND)
|
||||||
execute_process(COMMAND ${GIT_EXECUTABLE} describe
|
execute_process(COMMAND ${GIT_EXECUTABLE} describe
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
RESULT_VARIABLE GIT_INFO_RESULT
|
RESULT_VARIABLE GIT_INFO_RESULT
|
||||||
OUTPUT_VARIABLE GIT_REV
|
OUTPUT_VARIABLE GIT_REV
|
||||||
ERROR_QUIET
|
|
||||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
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()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(${GIT_INFO_RESULT} EQUAL 0)
|
string(REGEX REPLACE "^(.+)-([0-9]+)-(g[a-f0-9]+)$" "\\1;\\2;\\3"
|
||||||
string(REGEX REPLACE "^(.+)-([0-9]+)-(g[a-f0-9]+)$" "\\1;\\2;\\3"
|
GIT_PARTS ${GIT_REV})
|
||||||
GIT_PARTS ${GIT_REV})
|
|
||||||
|
|
||||||
if(NOT GIT_PARTS)
|
if(NOT GIT_PARTS)
|
||||||
message(FATAL_ERROR "Failed to parse git revision string '${GIT_REV}'")
|
message(FATAL_ERROR "Failed to parse git revision string '${GIT_REV}'")
|
||||||
endif(NOT GIT_PARTS)
|
endif(NOT GIT_PARTS)
|
||||||
|
|
||||||
list(LENGTH GIT_PARTS GIT_PARTS_LENGTH)
|
list(LENGTH GIT_PARTS GIT_PARTS_LENGTH)
|
||||||
if(GIT_PARTS_LENGTH EQUAL 3)
|
if(GIT_PARTS_LENGTH EQUAL 3)
|
||||||
list(GET GIT_PARTS 0 GIT_TAGNAME)
|
list(GET GIT_PARTS 0 GIT_TAGNAME)
|
||||||
list(GET GIT_PARTS 1 GIT_COMMITCOUNT)
|
list(GET GIT_PARTS 1 GIT_COMMITCOUNT)
|
||||||
list(GET GIT_PARTS 2 GIT_SHA1)
|
list(GET GIT_PARTS 2 GIT_SHA1)
|
||||||
set(HAS_GET_REVISION ON)
|
set(HAS_GIT_REVISION ON)
|
||||||
endif(GIT_PARTS_LENGTH EQUAL 3)
|
endif(GIT_PARTS_LENGTH EQUAL 3)
|
||||||
endif(${GIT_INFO_RESULT} EQUAL 0)
|
|
||||||
|
|
||||||
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_DISPLAY "${GIT_REV}")
|
||||||
set(CLEMENTINE_VERSION_DEB "${GIT_REV}")
|
set(CLEMENTINE_VERSION_DEB "${GIT_REV}")
|
||||||
set(CLEMENTINE_VERSION_RPM_V "${GIT_TAGNAME}")
|
set(CLEMENTINE_VERSION_RPM_V "${GIT_TAGNAME}")
|
||||||
set(CLEMENTINE_VERSION_RPM_R "2.${GIT_COMMITCOUNT}.${GIT_SHA1}")
|
set(CLEMENTINE_VERSION_RPM_R "2.${GIT_COMMITCOUNT}.${GIT_SHA1}")
|
||||||
set(CLEMENTINE_VERSION_SPARKLE "${GIT_REV}")
|
set(CLEMENTINE_VERSION_SPARKLE "${GIT_REV}")
|
||||||
set(CLEMENTINE_VERSION_PLIST "4096.${GIT_TAGNAME}.2.${GIT_COMMITCOUNT}")
|
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)
|
if(0)
|
||||||
message(STATUS "Display: ${CLEMENTINE_VERSION_DISPLAY}")
|
message(STATUS "Display: ${CLEMENTINE_VERSION_DISPLAY}")
|
||||||
|
|
|
@ -88,17 +88,6 @@
|
||||||
<item begin="</a" end=">"/>
|
<item begin="</a" end=">"/>
|
||||||
</exclude>
|
</exclude>
|
||||||
</provider>
|
</provider>
|
||||||
<provider name="hindilyrics.net (Bollywood songs)" title="{title} ({album})" charset="utf-8" url="http://www.hindilyrics.net/lyrics/of-{Title}.html">
|
|
||||||
<urlFormat replace=" _@;\/"'()[]" with="%20"/>
|
|
||||||
<urlFormat replace="?" with=""/>
|
|
||||||
<extract>
|
|
||||||
<item begin="<div class=nm>Movie</div>:" end="</pre>"/>
|
|
||||||
</extract>
|
|
||||||
<exclude>
|
|
||||||
<item begin="<span class=" end="">"/>
|
|
||||||
</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}&artista={artist}">
|
<provider name="letras.mus.br" title="" charset="utf-8" url="https://www.letras.mus.br/winamp.php?musica={title}&artista={artist}">
|
||||||
<urlFormat replace="_@,;&\/"" with="_"/>
|
<urlFormat replace="_@,;&\/"" with="_"/>
|
||||||
<urlFormat replace=" " with="+"/>
|
<urlFormat replace=" " with="+"/>
|
||||||
|
@ -114,13 +103,6 @@
|
||||||
</extract>
|
</extract>
|
||||||
<invalidIndicator value="ERROR"/>
|
<invalidIndicator value="ERROR"/>
|
||||||
</provider>
|
</provider>
|
||||||
<provider name="loudson.gs" title="" charset="utf-8" url="http://www.loudson.gs/{a}/{artist}/{album}/{title}">
|
|
||||||
<urlFormat replace=" _@,;&\/"" with="-"/>
|
|
||||||
<urlFormat replace="." with=""/>
|
|
||||||
<extract>
|
|
||||||
<item tag="<div class="middle_col_TracksLyrics ">"/>
|
|
||||||
</extract>
|
|
||||||
</provider>
|
|
||||||
<provider name="lyrics.com" title="{artist} - {title} Lyrics" charset="utf-8" url="http://www.lyrics.com/lyrics/{artist}/{title}.html">
|
<provider name="lyrics.com" title="{artist} - {title} Lyrics" charset="utf-8" url="http://www.lyrics.com/lyrics/{artist}/{title}.html">
|
||||||
<urlFormat replace=" _@,;&\/"" with="-"/>
|
<urlFormat replace=" _@,;&\/"" with="-"/>
|
||||||
<urlFormat replace="'." with=""/>
|
<urlFormat replace="'." with=""/>
|
||||||
|
@ -194,14 +176,6 @@
|
||||||
</extract>
|
</extract>
|
||||||
<invalidIndicator value="Page not Found"/>
|
<invalidIndicator value="Page not Found"/>
|
||||||
</provider>
|
</provider>
|
||||||
<provider name="lyricstime.com" title="{artist} - {title} Lyrics" charset="iso-8859-1" url="http://www.lyricstime.com/{artist}-{title}-lyrics.html">
|
|
||||||
<urlFormat replace=" _@,;&\/"'" with="-"/>
|
|
||||||
<urlFormat replace="." with=""/>
|
|
||||||
<extract>
|
|
||||||
<item tag="<div id="songlyrics" >"/>
|
|
||||||
<item tag="<p>"/>
|
|
||||||
</extract>
|
|
||||||
</provider>
|
|
||||||
<provider name="lyriki.com" title="" charset="utf-8" url="http://www.lyriki.com/{artist}:{title}">
|
<provider name="lyriki.com" title="" charset="utf-8" url="http://www.lyriki.com/{artist}:{title}">
|
||||||
<urlFormat replace=" _@,;&\/"" with="_"/>
|
<urlFormat replace=" _@,;&\/"" with="_"/>
|
||||||
<urlFormat replace="." with=""/>
|
<urlFormat replace="." with=""/>
|
||||||
|
@ -210,20 +184,6 @@
|
||||||
<item tag="<p>"/>
|
<item tag="<p>"/>
|
||||||
</extract>
|
</extract>
|
||||||
</provider>
|
</provider>
|
||||||
<provider name="metrolyrics.com" title="{artist} - {title} LYRICS" charset="utf-8" url="http://www.metrolyrics.com/{title}-lyrics-{artist}.html">
|
|
||||||
<urlFormat replace=" _@,;&\/"" with="-"/>
|
|
||||||
<urlFormat replace="'." with=""/>
|
|
||||||
<extract>
|
|
||||||
<item tag="<span id="lyrics">"/>
|
|
||||||
</extract>
|
|
||||||
<extract>
|
|
||||||
<item tag="<div id="lyrics">"/>
|
|
||||||
</extract>
|
|
||||||
<exclude>
|
|
||||||
<item tag="<h5>"/>
|
|
||||||
</exclude>
|
|
||||||
<invalidIndicator value="These lyrics are missing"/>
|
|
||||||
</provider>
|
|
||||||
<provider name="mp3lyrics.org" title="{artist} &quot;{title}&quot; Lyrics" charset="utf-8" url="http://www.mp3lyrics.org/{a}/{artist}/{title}/">
|
<provider name="mp3lyrics.org" title="{artist} &quot;{title}&quot; Lyrics" charset="utf-8" url="http://www.mp3lyrics.org/{a}/{artist}/{title}/">
|
||||||
<urlFormat replace=" _@,;&\/"" with="-"/>
|
<urlFormat replace=" _@,;&\/"" with="-"/>
|
||||||
<urlFormat replace="'." with=""/>
|
<urlFormat replace="'." with=""/>
|
||||||
|
@ -251,13 +211,6 @@
|
||||||
</exclude>
|
</exclude>
|
||||||
<invalidIndicator value="We couldn't find that page."/>
|
<invalidIndicator value="We couldn't find that page."/>
|
||||||
</provider>
|
</provider>
|
||||||
<provider name="seeklyrics.com" title="{artist} - {title} Lyrics" charset="iso-8859-1" url="http://www.seeklyrics.com/lyrics/{Artist}/{Title}.html">
|
|
||||||
<urlFormat replace=" _@,;&\/'"" with="-"/>
|
|
||||||
<urlFormat replace="." with=""/>
|
|
||||||
<extract>
|
|
||||||
<item tag="<div id="songlyrics">"/>
|
|
||||||
</extract>
|
|
||||||
</provider>
|
|
||||||
<provider name="songlyrics.com" title="{title} LYRICS - {artist}" charset="utf-8" url="http://www.songlyrics.com/{artist}/{title}-lyrics/">
|
<provider name="songlyrics.com" title="{title} LYRICS - {artist}" charset="utf-8" url="http://www.songlyrics.com/{artist}/{title}-lyrics/">
|
||||||
<urlFormat replace=" ._@,;&\/"" with="-"/>
|
<urlFormat replace=" ._@,;&\/"" with="-"/>
|
||||||
<urlFormat replace="'" with="_"/>
|
<urlFormat replace="'" with="_"/>
|
||||||
|
|
|
@ -48,7 +48,7 @@ binary-arch: install
|
||||||
dh_installchangelogs
|
dh_installchangelogs
|
||||||
dh_installmenu
|
dh_installmenu
|
||||||
dh_installdocs
|
dh_installdocs
|
||||||
dh_gconf
|
dh_installgsettings
|
||||||
dh_link
|
dh_link
|
||||||
dh_strip
|
dh_strip
|
||||||
dh_compress
|
dh_compress
|
||||||
|
|
|
@ -22,7 +22,7 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/clementine.nsi.in
|
||||||
|
|
||||||
|
|
||||||
# windows/windres.rc is done by src/CMakeLists.txt
|
# windows/windres.rc is done by src/CMakeLists.txt
|
||||||
|
set(APP_ID "org.clementine_player.Clementine")
|
||||||
if(EXISTS /etc/lsb-release)
|
if(EXISTS /etc/lsb-release)
|
||||||
file(READ "/etc/lsb-release" LSB_RELEASE_CONTENTS)
|
file(READ "/etc/lsb-release" LSB_RELEASE_CONTENTS)
|
||||||
string(REGEX MATCH "DISTRIB_ID=Ubuntu" IS_UBUNTU ${LSB_RELEASE_CONTENTS})
|
string(REGEX MATCH "DISTRIB_ID=Ubuntu" IS_UBUNTU ${LSB_RELEASE_CONTENTS})
|
||||||
|
@ -40,20 +40,20 @@ option(INSTALL_UBUNTU_ICONS "Install the Ubuntu themed monochrome panel icons" $
|
||||||
if (NOT APPLE)
|
if (NOT APPLE)
|
||||||
install(FILES clementine_64.png
|
install(FILES clementine_64.png
|
||||||
DESTINATION share/icons/hicolor/64x64/apps/
|
DESTINATION share/icons/hicolor/64x64/apps/
|
||||||
RENAME clementine.png
|
RENAME ${APP_ID}.png
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES clementine_128.png
|
install(FILES clementine_128.png
|
||||||
DESTINATION share/icons/hicolor/128x128/apps/
|
DESTINATION share/icons/hicolor/128x128/apps/
|
||||||
RENAME clementine.png
|
RENAME ${APP_ID}.png
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES ../data/icon.svg
|
install(FILES ../data/icon.svg
|
||||||
DESTINATION share/icons/hicolor/scalable/apps/
|
DESTINATION share/icons/hicolor/scalable/apps/
|
||||||
RENAME clementine.svg
|
RENAME ${APP_ID}.svg
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES clementine.desktop
|
install(FILES ${APP_ID}.desktop
|
||||||
DESTINATION share/applications
|
DESTINATION share/applications
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ if (NOT APPLE)
|
||||||
DESTINATION share/kservices5
|
DESTINATION share/kservices5
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES clementine.appdata.xml
|
install(FILES ${APP_ID}.appdata.xml
|
||||||
DESTINATION share/metainfo
|
DESTINATION share/metainfo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ BuildRequires: liblastfm-qt5-devel
|
||||||
BuildRequires: desktop-file-utils
|
BuildRequires: desktop-file-utils
|
||||||
BuildRequires: hicolor-icon-theme
|
BuildRequires: hicolor-icon-theme
|
||||||
BuildRequires: libappstream-glib
|
BuildRequires: libappstream-glib
|
||||||
|
BuildRequires: qtsingleapplication-qt5-devel
|
||||||
BuildRequires: pkgconfig
|
BuildRequires: pkgconfig
|
||||||
BuildRequires: pkgconfig(glib-2.0)
|
BuildRequires: pkgconfig(glib-2.0)
|
||||||
BuildRequires: pkgconfig(gio-2.0)
|
BuildRequires: pkgconfig(gio-2.0)
|
||||||
|
@ -97,7 +98,7 @@ Features include:
|
||||||
|
|
||||||
%build
|
%build
|
||||||
cd bin
|
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}
|
%{cmake_build}
|
||||||
|
|
||||||
%install
|
%install
|
||||||
|
@ -112,16 +113,16 @@ rm -f $RPM_BUILD_ROOT/usr/share/icons/ubuntu-mono-{dark,light}/apps/24/clementin
|
||||||
%{_bindir}/clementine
|
%{_bindir}/clementine
|
||||||
%{_bindir}/clementine-tagreader
|
%{_bindir}/clementine-tagreader
|
||||||
%dir %{_datadir}/metainfo/
|
%dir %{_datadir}/metainfo/
|
||||||
%{_datadir}/metainfo/clementine.appdata.xml
|
%{_datadir}/metainfo/org.clementine_player.Clementine.appdata.xml
|
||||||
%{_datadir}/applications/clementine.desktop
|
%{_datadir}/applications/org.clementine_player.Clementine.desktop
|
||||||
%{_datadir}/clementine/projectm-presets
|
%{_datadir}/clementine/projectm-presets
|
||||||
%{_datadir}/kservices5/clementine-itms.protocol
|
%{_datadir}/kservices5/clementine-itms.protocol
|
||||||
%{_datadir}/kservices5/clementine-itpc.protocol
|
%{_datadir}/kservices5/clementine-itpc.protocol
|
||||||
%{_datadir}/kservices5/clementine-feed.protocol
|
%{_datadir}/kservices5/clementine-feed.protocol
|
||||||
%{_datadir}/kservices5/clementine-zune.protocol
|
%{_datadir}/kservices5/clementine-zune.protocol
|
||||||
%{_datadir}/icons/hicolor/64x64/apps/clementine.png
|
%{_datadir}/icons/hicolor/64x64/apps/org.clementine_player.Clementine.png
|
||||||
%{_datadir}/icons/hicolor/128x128/apps/clementine.png
|
%{_datadir}/icons/hicolor/128x128/apps/org.clementine_player.Clementine.png
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/clementine.svg
|
%{_datadir}/icons/hicolor/scalable/apps/org.clementine_player.Clementine.svg
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* @RPM_DATE@ David Sansome <me@davidsansome.com> - @CLEMENTINE_VERSION_RPM_V@
|
* @RPM_DATE@ David Sansome <me@davidsansome.com> - @CLEMENTINE_VERSION_RPM_V@
|
||||||
|
|
|
@ -66,7 +66,6 @@ GSTREAMER_PLUGINS = [
|
||||||
'libgstisomp4.dylib',
|
'libgstisomp4.dylib',
|
||||||
'libgstlame.dylib',
|
'libgstlame.dylib',
|
||||||
'libgstlibav.dylib',
|
'libgstlibav.dylib',
|
||||||
'libgstmms.dylib',
|
|
||||||
# TODO: Bring back Musepack support.
|
# TODO: Bring back Musepack support.
|
||||||
'libgstogg.dylib',
|
'libgstogg.dylib',
|
||||||
'libgstopus.dylib',
|
'libgstopus.dylib',
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<!-- Copyright 2014 David Sansome <me@davidsansome.com> -->
|
<!-- Copyright 2014 David Sansome <me@davidsansome.com> -->
|
||||||
<application>
|
<application>
|
||||||
<id>org.clementine_player.Clementine</id>
|
<id>org.clementine_player.Clementine</id>
|
||||||
<id type="desktop">clementine.desktop</id>
|
<id type="desktop">org.clementine_player.Clementine.desktop</id>
|
||||||
<metadata_license>CC0-1.0</metadata_license>
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
<project_license>GPL-3.0+</project_license>
|
<project_license>GPL-3.0+</project_license>
|
||||||
<name>Clementine Music Player</name>
|
<name>Clementine Music Player</name>
|
|
@ -15,6 +15,7 @@ GenericName[hi]=क्लेमेंटैन् संगीत वादक
|
||||||
GenericName[is]=Clementine tónlistarspilarinn
|
GenericName[is]=Clementine tónlistarspilarinn
|
||||||
GenericName[pl]=Odtwarzacz muzyki Clementine
|
GenericName[pl]=Odtwarzacz muzyki Clementine
|
||||||
GenericName[pt]=Reprodutor de músicas Clementine
|
GenericName[pt]=Reprodutor de músicas Clementine
|
||||||
|
GenericName[ru]=Музыкальный проигрыватель Clementine
|
||||||
GenericName[sl]=Predvajalnik glasbe Clementine
|
GenericName[sl]=Predvajalnik glasbe Clementine
|
||||||
GenericName[sr]=Клементина музички плејер
|
GenericName[sr]=Клементина музички плејер
|
||||||
GenericName[sr@ijekavian]=Клементина музички плејер
|
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[is]=Spilar tónlist og streymi frá last.fm
|
||||||
Comment[pl]=Odtwarzanie muzyki i strumieni last.fm
|
Comment[pl]=Odtwarzanie muzyki i strumieni last.fm
|
||||||
Comment[pt]=Reprodução de músicas e emissões 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[sl]=Predvaja glasbo in pretoke last.fm
|
||||||
Comment[sr]=Репродукује музику и last.fm токове
|
Comment[sr]=Репродукује музику и last.fm токове
|
||||||
Comment[sr@ijekavian]=Репродукује музику и last.fm токове
|
Comment[sr@ijekavian]=Репродукује музику и last.fm токове
|
||||||
|
@ -33,111 +35,63 @@ Comment[sr@ijekavianlatin]=Reprodukuje muziku i last.fm tokove
|
||||||
Comment[sr@latin]=Reprodukuje muziku i last.fm tokove
|
Comment[sr@latin]=Reprodukuje muziku i last.fm tokove
|
||||||
Exec=clementine %U
|
Exec=clementine %U
|
||||||
TryExec=clementine
|
TryExec=clementine
|
||||||
Icon=clementine
|
Icon=org.clementine_player.Clementine
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=AudioVideo;Player;Qt;Audio;
|
Categories=AudioVideo;Player;Qt;Audio;
|
||||||
StartupNotify=false
|
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;
|
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]
|
[Desktop Action Play-Pause]
|
||||||
Name=Play
|
Name=Play/Pause
|
||||||
Exec=clementine --play
|
Exec=clementine --play-pause
|
||||||
Name[af]=Speel
|
Name[be]=Прайграць/Прыпыніць
|
||||||
Name[be]=Прайграць
|
Name[bg]=Възпроизвеждане/Пауза
|
||||||
Name[bg]=Възпроизвеждане
|
Name[br]=Lenn/Ehan
|
||||||
Name[br]=Lenn
|
Name[ca]=Reprodueix/Pausa
|
||||||
Name[ca]=Reprodueix
|
Name[cs]=Přehrát/Pozastavit
|
||||||
Name[cs]=Přehrát
|
Name[da]=Afspil/Pause
|
||||||
Name[da]=Afspil
|
Name[de]=Wiedergabe/Pause
|
||||||
Name[de]=Wiedergabe
|
Name[el]=Αναπαραγωγή/Παύση
|
||||||
Name[el]=Αναπαραγωγή
|
Name[es]=Reproducir/Pausar
|
||||||
Name[es]=Reproducir
|
Name[et]=Mängi/Paus
|
||||||
Name[et]=Mängi
|
Name[eu]=Erreproduzitu/Pausarazi
|
||||||
Name[eu]=Erreproduzitu
|
Name[fa]=پخش/مکث
|
||||||
Name[fa]=پخش
|
Name[fi]=Toista/Keskeytä
|
||||||
Name[fi]=Toista
|
Name[fr]=Lecture/Pause
|
||||||
Name[fr]=Lecture
|
Name[ga]=Seinn/Cuir ar sos
|
||||||
Name[ga]=Seinn
|
Name[gl]=Reproducir/Pausa
|
||||||
Name[gl]=Reproducir
|
Name[he]=נגן/השהה
|
||||||
Name[he]=נגינה
|
Name[hi]=गाना बजाएं/गाना रोकें
|
||||||
Name[hi]=गाना बजाएं
|
Name[hr]=Pokreni reprodukciju/Pauza
|
||||||
Name[hr]=Pokreni reprodukciju
|
Name[hu]=Lejátszás/Szünet
|
||||||
Name[hu]=Lejátszás
|
Name[is]=Spila/Setja í bið
|
||||||
Name[is]=Spila
|
Name[it]=Riproduci/Pausa
|
||||||
Name[it]=Riproduci
|
Name[ja]=再生/一時停止
|
||||||
Name[ja]=再生
|
Name[kk]=Ойнату/Аялдату
|
||||||
Name[kk]=Ойнату
|
Name[ko]=재생/일시 중지
|
||||||
Name[lt]=Groti
|
Name[lt]=Groti/Pristabdyti
|
||||||
Name[lv]=Atskaņot
|
Name[lv]=Atskaņot/Pauze
|
||||||
Name[ms]=Mainkan
|
Name[nb]=Spill/Pause
|
||||||
Name[nb]=Spill
|
Name[nl]=Afspelen/Pauze
|
||||||
Name[nl]=Afspelen
|
Name[oc]=Lectura/Pausa
|
||||||
Name[oc]=Lectura
|
Name[pl]=Odtwarzaj/Pauza
|
||||||
Name[pl]=Odtwarzaj
|
Name[pt]=Reproduzir/Pausa
|
||||||
Name[pt]=Reproduzir
|
Name[pt_BR]=Reproduzir/Pausar
|
||||||
Name[pt_BR]=Reproduzir
|
Name[ro]=Redă/Pauză
|
||||||
Name[ro]=Redă
|
Name[ru]=Играть/пауза
|
||||||
Name[ru]=Воспроизвести
|
Name[sk]=Hrať/Pozastaviť
|
||||||
Name[sk]=Hrať
|
Name[sl]=Predvajaj/Začasno ustavi
|
||||||
Name[sl]=Predvajaj
|
Name[sr]=Пусти/Паузирај
|
||||||
Name[sr]=Пусти
|
Name[sr@ijekavian]=Пусти/Паузирај
|
||||||
Name[sr@ijekavian]=Пусти
|
Name[sr@ijekavianlatin]=Pusti/Pauziraj
|
||||||
Name[sr@ijekavianlatin]=Pusti
|
Name[sr@latin]=Pusti/Pauziraj
|
||||||
Name[sr@latin]=Pusti
|
Name[sv]=Spela upp/Gör paus
|
||||||
Name[sv]=Spela upp
|
Name[tr]=Çal/Duraklat
|
||||||
Name[tr]=Çal
|
Name[uk]=Відтворити/Призупинити
|
||||||
Name[uk]=Відтворити
|
Name[vi]=Phát/Tạm dừng
|
||||||
Name[vi]=Phát
|
Name[zh_CN]=播放/暂停
|
||||||
Name[zh_CN]=播放
|
Name[zh_TW]=播放/暫停
|
||||||
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 Stop]
|
[Desktop Action Stop]
|
||||||
Name=Stop
|
Name=Stop
|
||||||
|
@ -322,7 +276,7 @@ Name[pl]=Dalej
|
||||||
Name[pt]=Seguinte
|
Name[pt]=Seguinte
|
||||||
Name[pt_BR]=Próximo
|
Name[pt_BR]=Próximo
|
||||||
Name[ro]=Următoarea
|
Name[ro]=Următoarea
|
||||||
Name[ru]=Дальше
|
Name[ru]=Следующий
|
||||||
Name[sk]=Ďalšia
|
Name[sk]=Ďalšia
|
||||||
Name[sl]=Naslednji
|
Name[sl]=Naslednji
|
||||||
Name[sr]=Следећа
|
Name[sr]=Следећа
|
|
@ -7,7 +7,7 @@ import polib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
PO_GLOB = 'src/translations/*.po'
|
PO_GLOB = 'src/translations/*.po'
|
||||||
DESKTOP_PATH = 'dist/clementine.desktop'
|
DESKTOP_PATH = 'dist/org.clementine_player.Clementine.desktop'
|
||||||
|
|
||||||
class ConfigParser(object):
|
class ConfigParser(object):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -302,7 +302,6 @@ Section "Clementine" Clementine
|
||||||
|
|
||||||
File "clementine.exe"
|
File "clementine.exe"
|
||||||
File "clementine-tagreader.exe"
|
File "clementine-tagreader.exe"
|
||||||
File "clementine-spotifyblob.exe"
|
|
||||||
File "clementine.ico"
|
File "clementine.ico"
|
||||||
File "glew32.dll"
|
File "glew32.dll"
|
||||||
File "libcdio-19.dll"
|
File "libcdio-19.dll"
|
||||||
|
@ -355,7 +354,6 @@ Section "Clementine" Clementine
|
||||||
File "libpsl-5.dll"
|
File "libpsl-5.dll"
|
||||||
File "libsoup-2.4-1.dll"
|
File "libsoup-2.4-1.dll"
|
||||||
File "libspeex-1.dll"
|
File "libspeex-1.dll"
|
||||||
File "libspotify.dll"
|
|
||||||
File "libssl-1_1.dll"
|
File "libssl-1_1.dll"
|
||||||
File "libsqlite3-0.dll"
|
File "libsqlite3-0.dll"
|
||||||
File "libstdc++-6.dll"
|
File "libstdc++-6.dll"
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
include_directories(${SPOTIFY_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(${SPOTIFY_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)
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
@ -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
|
|
|
@ -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";
|
|
|
@ -39,3 +39,6 @@ target_link_libraries(libclementine-common
|
||||||
${TAGLIB_LIBRARIES}
|
${TAGLIB_LIBRARIES}
|
||||||
${CMAKE_THREAD_LIBS_INIT}
|
${CMAKE_THREAD_LIBS_INIT}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
find_package(Backtrace)
|
||||||
|
configure_file(core/conf_backtrace.h.in conf_backtrace.h)
|
||||||
|
|
|
@ -202,8 +202,8 @@ template <typename T, typename... Args>
|
||||||
_detail::ClosureBase* NewClosure(QFuture<T> future, QObject* receiver,
|
_detail::ClosureBase* NewClosure(QFuture<T> future, QObject* receiver,
|
||||||
const char* slot, const Args&... args) {
|
const char* slot, const Args&... args) {
|
||||||
QFutureWatcher<T>* watcher = new QFutureWatcher<T>;
|
QFutureWatcher<T>* watcher = new QFutureWatcher<T>;
|
||||||
watcher->setFuture(future);
|
|
||||||
QObject::connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()));
|
QObject::connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()));
|
||||||
|
watcher->setFuture(future);
|
||||||
return NewClosure(watcher, SIGNAL(finished()), receiver, slot, args...);
|
return NewClosure(watcher, SIGNAL(finished()), receiver, slot, args...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,8 +211,8 @@ template <typename T, typename F, typename... Args>
|
||||||
_detail::ClosureBase* NewClosure(QFuture<T> future, const F& callback,
|
_detail::ClosureBase* NewClosure(QFuture<T> future, const F& callback,
|
||||||
const Args&... args) {
|
const Args&... args) {
|
||||||
QFutureWatcher<T>* watcher = new QFutureWatcher<T>;
|
QFutureWatcher<T>* watcher = new QFutureWatcher<T>;
|
||||||
watcher->setFuture(future);
|
|
||||||
QObject::connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()));
|
QObject::connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()));
|
||||||
|
watcher->setFuture(future);
|
||||||
return NewClosure(watcher, SIGNAL(finished()), callback, args...);
|
return NewClosure(watcher, SIGNAL(finished()), callback, args...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
#cmakedefine Backtrace_FOUND
|
||||||
|
#ifdef Backtrace_FOUND
|
||||||
|
#include <@Backtrace_HEADER@>
|
||||||
|
#endif
|
|
@ -21,9 +21,7 @@
|
||||||
#include <cxxabi.h>
|
#include <cxxabi.h>
|
||||||
|
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#ifdef Q_OS_UNIX
|
#include "conf_backtrace.h"
|
||||||
#include <execinfo.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
|
|
||||||
|
@ -325,7 +323,7 @@ QString DemangleSymbol(const QString& symbol) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void DumpStackTrace() {
|
void DumpStackTrace() {
|
||||||
#ifdef Q_OS_UNIX
|
#ifdef Backtrace_FOUND
|
||||||
void* callstack[128];
|
void* callstack[128];
|
||||||
int callstack_size =
|
int callstack_size =
|
||||||
backtrace(reinterpret_cast<void**>(&callstack), sizeof(callstack));
|
backtrace(reinterpret_cast<void**>(&callstack), sizeof(callstack));
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -72,8 +72,8 @@ parts:
|
||||||
cmake ../src -DCMAKE_INSTALL_PREFIX=/usr
|
cmake ../src -DCMAKE_INSTALL_PREFIX=/usr
|
||||||
make -j $(getconf _NPROCESSORS_ONLN)
|
make -j $(getconf _NPROCESSORS_ONLN)
|
||||||
make DESTDIR=$SNAPCRAFT_PART_INSTALL install
|
make DESTDIR=$SNAPCRAFT_PART_INSTALL install
|
||||||
sed -i 's|Icon=clementine|Icon=/usr/share/icons/hicolor/scalable/apps/clementine\.svg|' $SNAPCRAFT_PART_INSTALL/usr/share/applications/clementine.desktop
|
sed -i 's|Icon=clementine|Icon=/usr/share/icons/hicolor/scalable/apps/clementine\.svg|' $SNAPCRAFT_PART_INSTALL/usr/share/applications/org.clementine_player.Clementine.desktop
|
||||||
sed -i 's|TryExec=.*|TryExec=/snap/bin/clementine|' $SNAPCRAFT_PART_INSTALL/usr/share/applications/clementine.desktop
|
sed -i 's|TryExec=.*|TryExec=/snap/bin/clementine|' $SNAPCRAFT_PART_INSTALL/usr/share/applications/org.clementine_player.Clementine.desktop
|
||||||
|
|
||||||
build-packages:
|
build-packages:
|
||||||
- cmake
|
- cmake
|
||||||
|
@ -170,7 +170,7 @@ parts:
|
||||||
apps:
|
apps:
|
||||||
clementine:
|
clementine:
|
||||||
command: desktop-launch $SNAP/usr/bin/clementine
|
command: desktop-launch $SNAP/usr/bin/clementine
|
||||||
desktop: usr/share/applications/clementine.desktop
|
desktop: usr/share/applications/org.clementine_player.Clementine.desktop
|
||||||
environment:
|
environment:
|
||||||
ALSA_CONFIG_PATH: /snap/$SNAPCRAFT_PROJECT_NAME/current/usr/share/alsa/alsa.conf
|
ALSA_CONFIG_PATH: /snap/$SNAPCRAFT_PROJECT_NAME/current/usr/share/alsa/alsa.conf
|
||||||
LD_LIBRARY_PATH: $LD_LIBRARY_PATH:$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/pulseaudio
|
LD_LIBRARY_PATH: $LD_LIBRARY_PATH:$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/pulseaudio
|
||||||
|
|
|
@ -47,10 +47,6 @@ include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-tagreader)
|
||||||
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-tagreader)
|
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-tagreader)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-remote)
|
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-remote)
|
||||||
include_directories(${CMAKE_BINARY_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)
|
include(../cmake/ParseArguments.cmake)
|
||||||
|
|
||||||
|
@ -385,6 +381,7 @@ set(SOURCES
|
||||||
widgets/errordialog.cpp
|
widgets/errordialog.cpp
|
||||||
widgets/fancytabwidget.cpp
|
widgets/fancytabwidget.cpp
|
||||||
widgets/favoritewidget.cpp
|
widgets/favoritewidget.cpp
|
||||||
|
widgets/filenameformatwidget.cpp
|
||||||
widgets/fileview.cpp
|
widgets/fileview.cpp
|
||||||
widgets/fileviewlist.cpp
|
widgets/fileviewlist.cpp
|
||||||
widgets/forcescrollperpixel.cpp
|
widgets/forcescrollperpixel.cpp
|
||||||
|
@ -683,6 +680,7 @@ set(HEADERS
|
||||||
widgets/errordialog.h
|
widgets/errordialog.h
|
||||||
widgets/fancytabwidget.h
|
widgets/fancytabwidget.h
|
||||||
widgets/favoritewidget.h
|
widgets/favoritewidget.h
|
||||||
|
widgets/filenameformatwidget.h
|
||||||
widgets/fileview.h
|
widgets/fileview.h
|
||||||
widgets/fileviewlist.h
|
widgets/fileviewlist.h
|
||||||
widgets/freespacebar.h
|
widgets/freespacebar.h
|
||||||
|
@ -807,6 +805,7 @@ set(UI
|
||||||
|
|
||||||
widgets/equalizerslider.ui
|
widgets/equalizerslider.ui
|
||||||
widgets/errordialog.ui
|
widgets/errordialog.ui
|
||||||
|
widgets/filenameformatwidget.ui
|
||||||
widgets/fileview.ui
|
widgets/fileview.ui
|
||||||
widgets/loginstatewidget.ui
|
widgets/loginstatewidget.ui
|
||||||
widgets/osdpretty.ui
|
widgets/osdpretty.ui
|
||||||
|
@ -882,33 +881,6 @@ optional_source(HAVE_LIBLASTFM
|
||||||
internet/lastfm/lastfmsettingspage.ui
|
internet/lastfm/lastfmsettingspage.ui
|
||||||
)
|
)
|
||||||
|
|
||||||
# Spotify support
|
|
||||||
optional_source(HAVE_SPOTIFY
|
|
||||||
SOURCES
|
|
||||||
internet/spotify/spotifyserver.cpp
|
|
||||||
internet/spotify/spotifyservice.cpp
|
|
||||||
internet/spotify/spotifysettingspage.cpp
|
|
||||||
globalsearch/spotifysearchprovider.cpp
|
|
||||||
HEADERS
|
|
||||||
globalsearch/spotifysearchprovider.h
|
|
||||||
internet/spotify/spotifyserver.h
|
|
||||||
internet/spotify/spotifyservice.h
|
|
||||||
internet/spotify/spotifysettingspage.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
|
# Platform specific - OS X
|
||||||
optional_source(APPLE
|
optional_source(APPLE
|
||||||
INCLUDE_DIRECTORIES
|
INCLUDE_DIRECTORIES
|
||||||
|
@ -1296,7 +1268,6 @@ target_link_libraries(clementine_lib
|
||||||
${SQLITE_LIBRARIES}
|
${SQLITE_LIBRARIES}
|
||||||
|
|
||||||
Qocoa
|
Qocoa
|
||||||
z
|
|
||||||
)
|
)
|
||||||
|
|
||||||
link_directories(
|
link_directories(
|
||||||
|
@ -1346,17 +1317,6 @@ if(HAVE_BREAKPAD)
|
||||||
endif (LINUX)
|
endif (LINUX)
|
||||||
endif(HAVE_BREAKPAD)
|
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)
|
if(HAVE_LIBPULSE)
|
||||||
target_link_libraries(clementine_lib ${LIBPULSE_LIBRARIES})
|
target_link_libraries(clementine_lib ${LIBPULSE_LIBRARIES})
|
||||||
endif()
|
endif()
|
||||||
|
@ -1390,7 +1350,6 @@ target_link_libraries(clementine_lib qsqlite)
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
target_link_libraries(clementine_lib
|
target_link_libraries(clementine_lib
|
||||||
protobuf
|
protobuf
|
||||||
${ZLIB_LIBRARIES}
|
|
||||||
tinysvcmdns
|
tinysvcmdns
|
||||||
dsound
|
dsound
|
||||||
)
|
)
|
||||||
|
@ -1444,9 +1403,6 @@ target_link_libraries(clementine
|
||||||
)
|
)
|
||||||
|
|
||||||
# macdeploy.py relies on the blob being built first.
|
# 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)
|
add_dependencies(clementine clementine-tagreader)
|
||||||
|
|
||||||
set_target_properties(clementine PROPERTIES
|
set_target_properties(clementine PROPERTIES
|
||||||
|
|
|
@ -65,13 +65,15 @@ Analyzer::Base::Base(QWidget* parent, uint scopeSize)
|
||||||
timeout_(40), // msec
|
timeout_(40), // msec
|
||||||
fht_(new FHT(scopeSize)),
|
fht_(new FHT(scopeSize)),
|
||||||
engine_(nullptr),
|
engine_(nullptr),
|
||||||
lastScope_(512),
|
lastScope_(),
|
||||||
new_frame_(false),
|
new_frame_(false),
|
||||||
is_playing_(false),
|
is_playing_(false),
|
||||||
barkband_table_(),
|
barkband_table_(),
|
||||||
prev_color_index_(0),
|
prev_color_index_(0),
|
||||||
bands_(0),
|
bands_(0),
|
||||||
psychedelic_enabled_(false) {}
|
psychedelic_enabled_(false) {
|
||||||
|
lastScope_.resize(fht_->size());
|
||||||
|
}
|
||||||
|
|
||||||
void Analyzer::Base::hideEvent(QHideEvent*) { timer_.stop(); }
|
void Analyzer::Base::hideEvent(QHideEvent*) { timer_.stop(); }
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
|
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
|
||||||
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
|
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
|
||||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
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
|
Clementine is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
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::kHeight = 2;
|
||||||
const uint BlockAnalyzer::kWidth = 4;
|
const uint BlockAnalyzer::kWidth = 4;
|
||||||
const uint BlockAnalyzer::kMinRows = 3; // arbituary
|
const uint BlockAnalyzer::kMinRows = 3; // arbitrary
|
||||||
const uint BlockAnalyzer::kMinColumns = 32; // arbituary
|
const uint BlockAnalyzer::kMaxRows = 256; // arbitrary
|
||||||
|
const uint BlockAnalyzer::kMinColumns = 32; // arbitrary
|
||||||
const uint BlockAnalyzer::kMaxColumns = 256; // must be 2**n
|
const uint BlockAnalyzer::kMaxColumns = 256; // must be 2**n
|
||||||
const uint BlockAnalyzer::kFadeSize = 90;
|
const uint BlockAnalyzer::kFadeSize = 90;
|
||||||
|
const uint BlockAnalyzer::kFadeInitial = 32;
|
||||||
|
|
||||||
const char* BlockAnalyzer::kName =
|
const char* BlockAnalyzer::kName =
|
||||||
QT_TRANSLATE_NOOP("AnalyzerContainer", "Block analyzer");
|
QT_TRANSLATE_NOOP("AnalyzerContainer", "Block analyzer");
|
||||||
|
|
||||||
BlockAnalyzer::BlockAnalyzer(QWidget* parent)
|
BlockAnalyzer::BlockAnalyzer(QWidget* parent)
|
||||||
: Analyzer::Base(parent, 9),
|
: Analyzer::Base(parent, 9),
|
||||||
|
scope_(kMinColumns),
|
||||||
columns_(0),
|
columns_(0),
|
||||||
rows_(0),
|
rows_(0),
|
||||||
y_(0),
|
y_(0),
|
||||||
barPixmap_(1, 1),
|
canvas_(),
|
||||||
topBarPixmap_(kWidth, kHeight),
|
rthresh_(kMaxRows + 1, 0.f),
|
||||||
scope_(kMinColumns),
|
bg_grad_(kMaxRows + 1, 0),
|
||||||
store_(1 << 8, 0),
|
fade_bars_(kFadeSize, 0),
|
||||||
fade_bars_(kFadeSize),
|
bandinfo_(kMaxColumns) {
|
||||||
fade_pos_(1 << 8, 50),
|
// Right and bottom edges are 1px padding.
|
||||||
fade_intensity_(1 << 8, 32) {
|
|
||||||
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1);
|
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1);
|
||||||
// -1 is padding, no drawing takes place there
|
|
||||||
setMaximumWidth(kMaxColumns * (kWidth + 1) - 1);
|
setMaximumWidth(kMaxColumns * (kWidth + 1) - 1);
|
||||||
|
|
||||||
// mxcl says null pixmaps cause crashes, so let's play it safe
|
setAttribute(Qt::WA_OpaquePaintEvent, true);
|
||||||
for (uint i = 0; i < kFadeSize; ++i) fade_bars_[i] = QPixmap(1, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockAnalyzer::~BlockAnalyzer() {}
|
BlockAnalyzer::~BlockAnalyzer() {}
|
||||||
|
@ -67,56 +68,53 @@ BlockAnalyzer::~BlockAnalyzer() {}
|
||||||
void BlockAnalyzer::resizeEvent(QResizeEvent* e) {
|
void BlockAnalyzer::resizeEvent(QResizeEvent* e) {
|
||||||
QWidget::resizeEvent(e);
|
QWidget::resizeEvent(e);
|
||||||
|
|
||||||
background_ = QPixmap(size());
|
uint newRows, newCols;
|
||||||
canvas_ = QPixmap(size());
|
|
||||||
|
|
||||||
const uint oldRows = rows_;
|
|
||||||
|
|
||||||
// all is explained in analyze()..
|
// all is explained in analyze()..
|
||||||
// +1 to counter -1 in maxSizes, trust me we need this!
|
// +1 to counter -1 in maxSizes, trust me we need this!
|
||||||
columns_ = qMin(
|
newCols = 1 + (width() + 1) / (kWidth + 1);
|
||||||
static_cast<uint>(static_cast<double>(width() + 1) / (kWidth + 1)) + 1,
|
newRows = 0 + (height() + 1) / (kHeight + 1);
|
||||||
kMaxColumns);
|
newCols = qMin(kMaxColumns, qMax(kMinColumns, newCols));
|
||||||
rows_ = static_cast<uint>(static_cast<double>(height() + 1) / (kHeight + 1));
|
newRows = qMin(kMaxRows, qMax(kMinRows, newRows));
|
||||||
|
|
||||||
// this is the y-offset for drawing from the top of the widget
|
if (newCols != columns_) {
|
||||||
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
|
columns_ = newCols;
|
||||||
|
scope_.resize(columns_);
|
||||||
|
|
||||||
scope_.resize(columns_);
|
updateBandSize(columns_);
|
||||||
|
bandinfo_.fill(FHTBand());
|
||||||
|
}
|
||||||
|
|
||||||
if (rows_ != oldRows) {
|
if (rows_ != newRows) {
|
||||||
barPixmap_ = QPixmap(kWidth, rows_ * (kHeight + 1));
|
rows_ = newRows;
|
||||||
|
|
||||||
for (uint i = 0; i < kFadeSize; ++i)
|
// this is the y-offset for drawing from the top of the widget
|
||||||
fade_bars_[i] = QPixmap(kWidth, rows_ * (kHeight + 1));
|
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
|
||||||
|
|
||||||
yscale_.resize(rows_ + 1);
|
const float PRE = 1.f,
|
||||||
|
PRO =
|
||||||
const uint PRE = 1,
|
1.f, // PRE and PRO allow us to restrict the range somewhat
|
||||||
PRO = 1; // PRE and PRO allow us to restrict the range somewhat
|
SCL = log10f(PRE + PRO + (1.f * rows_));
|
||||||
|
|
||||||
for (uint z = 0; z < rows_; ++z)
|
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();
|
determineStep();
|
||||||
paletteChange(palette());
|
paletteChange(palette());
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBandSize(columns_);
|
canvas_ = QImage(columns_ * (kWidth + 1), rows_ * (kHeight + 1),
|
||||||
drawBackground();
|
QImage::Format_ARGB32_Premultiplied);
|
||||||
|
canvas_.fill(pad_color_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlockAnalyzer::determineStep() {
|
void BlockAnalyzer::determineStep() {
|
||||||
// falltime is dependent on rowcount due to our digital resolution (ie we have
|
// falltime is dependent on rowcount
|
||||||
// boxes/blocks of pixels)
|
|
||||||
// I calculated the value 30 based on some trial and error
|
|
||||||
|
|
||||||
// the fall time of 30 is too slow on framerates above 50fps
|
// the fall time of 30 is too slow on framerates above 50fps
|
||||||
const double fallTime = timeout() < 20 ? 20 * rows_ : 30 * rows_;
|
const float rFallTime = 1.f / (timeout() < 20 ? 20.f : 30.f);
|
||||||
|
step_ = timeout() * rFallTime;
|
||||||
step_ = static_cast<double>(rows_ * timeout()) / fallTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlockAnalyzer::framerateChanged() { // virtual
|
void BlockAnalyzer::framerateChanged() { // virtual
|
||||||
|
@ -124,10 +122,10 @@ void BlockAnalyzer::framerateChanged() { // virtual
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlockAnalyzer::transform(Analyzer::Scope& s) {
|
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_->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
|
// the second half is pretty dull, so only show it if the user has a large
|
||||||
// analyzer
|
// analyzer
|
||||||
|
@ -138,77 +136,225 @@ void BlockAnalyzer::transform(Analyzer::Scope& s) {
|
||||||
|
|
||||||
void BlockAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
|
void BlockAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
|
||||||
bool new_frame) {
|
bool new_frame) {
|
||||||
// y = 2 3 2 1 0 2
|
float yf;
|
||||||
// . . . . # .
|
uint x, y;
|
||||||
// . . . # # .
|
|
||||||
// # . # # # #
|
|
||||||
// # # # # # #
|
|
||||||
//
|
|
||||||
// visual aid for how this analyzer works.
|
|
||||||
// y represents the number of blanks
|
|
||||||
// y starts from the top and increases in units of blocks
|
|
||||||
|
|
||||||
// yscale_ looks similar to: { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
|
if (p.paintEngine() == 0) return;
|
||||||
// if it contains 6 elements there are 5 rows in the analyzer
|
if (canvas_.isNull()) return;
|
||||||
|
|
||||||
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||||
|
|
||||||
if (!new_frame) {
|
if (!new_frame) {
|
||||||
p.drawPixmap(0, 0, canvas_);
|
p.drawImage(0, 0, canvas_, 0, 0, width(), height(), Qt::NoFormatConversion);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPainter canvas_painter(&canvas_);
|
|
||||||
|
|
||||||
Analyzer::interpolate(s, scope_);
|
Analyzer::interpolate(s, scope_);
|
||||||
|
|
||||||
// update the graphics with the new colour
|
// Update the color palettes.
|
||||||
if (psychedelic_enabled_) {
|
if (psychedelic_enabled_) paletteChange(QPalette());
|
||||||
paletteChange(QPalette());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint the background
|
// Visual Aid
|
||||||
canvas_painter.drawPixmap(0, 0, background_);
|
//
|
||||||
|
// 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) {
|
// Update band information.
|
||||||
// determine y
|
for (x = 0; x < scope_.size(); ++x) {
|
||||||
for (y = 0; scope_[x] < yscale_[y]; ++y) continue;
|
const float& bandthr = scope_[x];
|
||||||
|
FHTBand& band = bandinfo_[x];
|
||||||
|
|
||||||
// this is opposite to what you'd think, higher than y
|
// Calculate activity transition row values.
|
||||||
// means the bar is lower than y (physically)
|
// Note: rows_ < rthresh_.size()
|
||||||
if (static_cast<float>(y) > store_[x])
|
for (y = 0; y < rows_; ++y) {
|
||||||
y = static_cast<int>(store_[x] += step_);
|
if (bandthr >= rthresh_[y]) break;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fade_intensity_[x] > 0) {
|
// y <= band height :: band matches or exceeds power from last frame.
|
||||||
const uint offset = --fade_intensity_[x];
|
// y > band height :: band lost power since last frame.
|
||||||
const uint y = y_ + (fade_pos_[x] * (kHeight + 1));
|
if ((yf = 1.f * y) <= band.height) {
|
||||||
canvas_painter.drawPixmap(x * (kWidth + 1), y, fade_bars_[offset], 0, 0,
|
band.height = yf;
|
||||||
kWidth, height() - y);
|
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,
|
// Check the fade-out period counter. If expired (i.e., <= 0), the
|
||||||
// rows_ means none are
|
// fade-out effect is complete. Otherwise, continue downcounting and
|
||||||
canvas_painter.drawPixmap(x * (kWidth + 1), y * (kHeight + 1) + y_, *bar(),
|
// select the next color for the fade-out sequence.
|
||||||
0, y * (kHeight + 1), bar()->width(),
|
if (band.fade_intensity <= 0) {
|
||||||
bar()->height());
|
// 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)
|
// A block will be drawn and colored according to each band (column) of
|
||||||
canvas_painter.drawPixmap(x * (kWidth + 1),
|
// the FHT spectrum data. This block is a kWidth x kHeight region, along
|
||||||
static_cast<int>(store_[x]) * (kHeight + 1) + y_,
|
// with 1-px of padding on its right and bottom.
|
||||||
topBarPixmap_);
|
//
|
||||||
|
// 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) {
|
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
|
* It won't modify the hue of fg unless absolutely necessary
|
||||||
* @return the adjusted form of fg
|
* @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 {
|
class OutputOnExit {
|
||||||
public:
|
public:
|
||||||
explicit OutputOnExit(const QColor& color) : c(color) {}
|
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&) {
|
void BlockAnalyzer::paletteChange(const QPalette&) {
|
||||||
const QColor bg = palette().color(QPalette::Background);
|
QColor bg, bgdark, fg;
|
||||||
QColor fg;
|
|
||||||
|
|
||||||
if (psychedelic_enabled_) {
|
bg = palette().color(QPalette::Background);
|
||||||
|
bgdark = bg.darker(112);
|
||||||
|
|
||||||
|
if (psychedelic_enabled_)
|
||||||
fg = getPsychedelicColor(scope_, 10, 75);
|
fg = getPsychedelicColor(scope_, 10, 75);
|
||||||
} else {
|
else
|
||||||
fg = ensureContrast(bg, palette().color(QPalette::Highlight));
|
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);
|
// make a complimentary fadebar colour
|
||||||
|
// TODO(John Maguire): dark is not always correct, dumbo!
|
||||||
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)));
|
|
||||||
|
|
||||||
{
|
{
|
||||||
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;
|
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();
|
bg.darker(150).getHsv(&h, &s, &v);
|
||||||
const double dg = fg.green() - bg.green();
|
fg = QColor::fromHsv(h + 120, s, v);
|
||||||
const double db = fg.blue() - bg.blue();
|
|
||||||
const int r = bg.red(), g = bg.green(), b = bg.blue();
|
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) {
|
for (uint y = 0; y < kFadeSize; ++y) {
|
||||||
fade_bars_[y].fill(palette().color(QPalette::Background));
|
const float lrY = 1.f - (frlogFscl * log10f(fFscl - y));
|
||||||
QPainter f(&fade_bars_[y]);
|
fade_bars_[y] =
|
||||||
for (int z = 0; static_cast<uint>(z) < rows_; ++z) {
|
qRgba(static_cast<int>(r + lrY * dr), static_cast<int>(g + lrY * dg),
|
||||||
const double Y = 1.0 - (log10(kFadeSize - y) / log10(kFadeSize));
|
static_cast<int>(b + lrY * db), 255);
|
||||||
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)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
|
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
|
||||||
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
|
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
|
||||||
Copyright 2014, Krzysztof A. Sobiecki <sobkas@gmail.com>
|
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
|
Clementine is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
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 kHeight;
|
||||||
static const uint kWidth;
|
static const uint kWidth;
|
||||||
static const uint kMinRows;
|
static const uint kMinRows;
|
||||||
|
static const uint kMaxRows;
|
||||||
static const uint kMinColumns;
|
static const uint kMinColumns;
|
||||||
static const uint kMaxColumns;
|
static const uint kMaxColumns;
|
||||||
static const uint kFadeSize;
|
static const uint kFadeSize;
|
||||||
|
static const uint kFadeInitial;
|
||||||
|
|
||||||
static const char* kName;
|
static const char* kName;
|
||||||
|
|
||||||
|
@ -57,27 +60,58 @@ class BlockAnalyzer : public Analyzer::Base {
|
||||||
virtual void framerateChanged();
|
virtual void framerateChanged();
|
||||||
virtual void psychedelicModeChanged(bool);
|
virtual void psychedelicModeChanged(bool);
|
||||||
|
|
||||||
void drawBackground();
|
|
||||||
void determineStep();
|
void determineStep();
|
||||||
|
|
||||||
private:
|
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
|
// Top of the spectral activity bar.
|
||||||
uint y_; // y-offset from top of widget
|
float height; // Foreground-Background transition row.
|
||||||
QPixmap barPixmap_;
|
uint row; // Integer floor of the height value.
|
||||||
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_;
|
|
||||||
|
|
||||||
QVector<QPixmap> fade_bars_;
|
// Vertical color fade effect (a sort of hysteresis).
|
||||||
QVector<uint> fade_pos_;
|
uint fade_row; // Row in which to begin showing BG gradient.
|
||||||
QVector<int> fade_intensity_;
|
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_
|
#endif // ANALYZERS_BLOCKANALYZER_H_
|
||||||
|
|
|
@ -48,7 +48,7 @@ void DeleteFiles::Start(const SongList& songs) {
|
||||||
songs_ = songs;
|
songs_ = songs;
|
||||||
|
|
||||||
task_id_ = task_manager_->StartTask(tr("Deleting files"));
|
task_id_ = task_manager_->StartTask(tr("Deleting files"));
|
||||||
task_manager_->SetTaskBlocksLibraryScans(true);
|
task_manager_->SetTaskBlocksLibraryScans(task_id_);
|
||||||
|
|
||||||
thread_ = new QThread;
|
thread_ = new QThread;
|
||||||
connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles()));
|
connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles()));
|
||||||
|
|
|
@ -119,7 +119,7 @@ void GlobalShortcuts::AddRatingShortcut(const QString& id, const QString& name,
|
||||||
const QKeySequence& default_key) {
|
const QKeySequence& default_key) {
|
||||||
Shortcut shortcut = AddShortcut(id, name, default_key);
|
Shortcut shortcut = AddShortcut(id, name, default_key);
|
||||||
connect(shortcut.action, &QAction::triggered,
|
connect(shortcut.action, &QAction::triggered,
|
||||||
[this, rating]() { RateCurrentSong(rating); });
|
[this, rating]() { emit RateCurrentSong(rating); });
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalShortcuts::Shortcut GlobalShortcuts::AddShortcut(
|
GlobalShortcuts::Shortcut GlobalShortcuts::AddShortcut(
|
||||||
|
|
|
@ -70,12 +70,12 @@ void Organise::Start() {
|
||||||
if (thread_) return;
|
if (thread_) return;
|
||||||
|
|
||||||
task_id_ = task_manager_->StartTask(tr("Organising files"));
|
task_id_ = task_manager_->StartTask(tr("Organising files"));
|
||||||
task_manager_->SetTaskBlocksLibraryScans(true);
|
task_manager_->SetTaskBlocksLibraryScans(task_id_);
|
||||||
|
|
||||||
thread_ = new QThread;
|
thread_ = new QThread;
|
||||||
connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles()));
|
connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles()));
|
||||||
connect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)),
|
connect(transcoder_, SIGNAL(JobComplete(QUrl, QString, bool)),
|
||||||
SLOT(FileTranscoded(QString, QString, bool)));
|
SLOT(FileTranscoded(QUrl, QString, bool)));
|
||||||
|
|
||||||
moveToThread(thread_);
|
moveToThread(thread_);
|
||||||
thread_->start();
|
thread_->start();
|
||||||
|
@ -177,7 +177,7 @@ void Organise::ProcessSomeFiles() {
|
||||||
// Start the transcoding - this will happen in the background and
|
// Start the transcoding - this will happen in the background and
|
||||||
// FileTranscoded() will get called when it's done. At that point the
|
// FileTranscoded() will get called when it's done. At that point the
|
||||||
// task will get re-added to the pending queue with the new filename.
|
// task will get re-added to the pending queue with the new filename.
|
||||||
transcoder_->AddJob(task.song_info_.song_.url().toLocalFile(), preset,
|
transcoder_->AddJob(task.song_info_.song_.url(), preset,
|
||||||
task.transcoded_filename_);
|
task.transcoded_filename_);
|
||||||
transcoder_->Start();
|
transcoder_->Start();
|
||||||
continue;
|
continue;
|
||||||
|
@ -262,11 +262,12 @@ void Organise::UpdateProgress() {
|
||||||
const int total = task_count_ * 100;
|
const int total = task_count_ * 100;
|
||||||
|
|
||||||
// Update transcoding progress
|
// Update transcoding progress
|
||||||
QMap<QString, float> transcode_progress = transcoder_->GetProgress();
|
QMap<QUrl, float> transcode_progress = transcoder_->GetProgress();
|
||||||
for (const QString& filename : transcode_progress.keys()) {
|
for (const QUrl& fileurl : transcode_progress.keys()) {
|
||||||
|
QString filename = fileurl.toLocalFile();
|
||||||
if (!tasks_transcoding_.contains(filename)) continue;
|
if (!tasks_transcoding_.contains(filename)) continue;
|
||||||
tasks_transcoding_[filename].transcode_progress_ =
|
tasks_transcoding_[filename].transcode_progress_ =
|
||||||
transcode_progress[filename];
|
transcode_progress[fileurl];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count the progress of all tasks that are in the queue. Files that need
|
// Count the progress of all tasks that are in the queue. Files that need
|
||||||
|
@ -287,14 +288,17 @@ void Organise::UpdateProgress() {
|
||||||
task_manager_->SetTaskProgress(task_id_, progress, total);
|
task_manager_->SetTaskProgress(task_id_, progress, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Organise::FileTranscoded(const QString& input, const QString& output,
|
void Organise::FileTranscoded(const QUrl& input, const QString& output,
|
||||||
bool success) {
|
bool success) {
|
||||||
qLog(Info) << "File finished" << input << success;
|
Q_ASSERT(input.isLocalFile()); // organise only handles local files
|
||||||
|
QString input_file_path = input.toLocalFile();
|
||||||
|
|
||||||
|
qLog(Info) << "File finished" << input_file_path << success;
|
||||||
transcode_progress_timer_.stop();
|
transcode_progress_timer_.stop();
|
||||||
|
|
||||||
Task task = tasks_transcoding_.take(input);
|
Task task = tasks_transcoding_.take(input_file_path);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
files_with_errors_ << input;
|
files_with_errors_ << input_file_path;
|
||||||
} else {
|
} else {
|
||||||
tasks_pending_ << task;
|
tasks_pending_ << task;
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,8 +67,7 @@ class Organise : public QObject {
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void ProcessSomeFiles();
|
void ProcessSomeFiles();
|
||||||
void FileTranscoded(const QString& input, const QString& output,
|
void FileTranscoded(const QUrl& input, const QString& output, bool success);
|
||||||
bool success);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void SetSongProgress(float progress, bool transcoded = false);
|
void SetSongProgress(float progress, bool transcoded = false);
|
||||||
|
|
|
@ -24,12 +24,14 @@
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QHash>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
#include "core/arraysize.h"
|
#include "core/arraysize.h"
|
||||||
#include "core/timeconstants.h"
|
#include "core/timeconstants.h"
|
||||||
#include "core/utilities.h"
|
#include "core/utilities.h"
|
||||||
|
#include "transcoder/transcoder.h"
|
||||||
|
|
||||||
const char* OrganiseFormat::kTagPattern = "\\%([a-zA-Z]*)";
|
const char* OrganiseFormat::kTagPattern = "\\%([a-zA-Z]*)";
|
||||||
const char* OrganiseFormat::kBlockPattern = "\\{([^{}]+)\\}";
|
const char* OrganiseFormat::kBlockPattern = "\\{([^{}]+)\\}";
|
||||||
|
@ -96,7 +98,8 @@ bool OrganiseFormat::IsValid() const {
|
||||||
return v.validate(format_copy, pos) == QValidator::Acceptable;
|
return v.validate(format_copy, pos) == QValidator::Acceptable;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString OrganiseFormat::GetFilenameForSong(const Song& song) const {
|
QString OrganiseFormat::GetFilenameForSong(const Song& song,
|
||||||
|
QString prefix_path) const {
|
||||||
QString filename = ParseBlock(format_, song);
|
QString filename = ParseBlock(format_, song);
|
||||||
|
|
||||||
if (QFileInfo(filename).completeBaseName().isEmpty()) {
|
if (QFileInfo(filename).completeBaseName().isEmpty()) {
|
||||||
|
@ -140,9 +143,41 @@ QString OrganiseFormat::GetFilenameForSong(const Song& song) const {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!prefix_path.isEmpty()) parts.insert(0, prefix_path);
|
||||||
|
|
||||||
return parts.join("/");
|
return parts.join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString OrganiseFormat::GetFilenameForSong(
|
||||||
|
const Song& song, const TranscoderPreset& transcoder_preset,
|
||||||
|
QString prefix_path) const {
|
||||||
|
OrganiseFormat format(*this);
|
||||||
|
format.add_tag_override("extension", transcoder_preset.extension_);
|
||||||
|
|
||||||
|
return format.GetFilenameForSong(song, prefix_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList OrganiseFormat::GetFilenamesForSongs(const SongList& songs) const {
|
||||||
|
// Check if we will have multiple files with the same name.
|
||||||
|
// If so, they will erase each other if the overwrite flag is set.
|
||||||
|
// Better to rename them: e.g. foo.bar -> foo(2).bar
|
||||||
|
QHash<QString, int> filenames;
|
||||||
|
QStringList new_filenames;
|
||||||
|
|
||||||
|
for (const Song& song : songs) {
|
||||||
|
QString new_filename = GetFilenameForSong(song);
|
||||||
|
if (filenames.contains(new_filename)) {
|
||||||
|
QString song_number = QString::number(++filenames[new_filename]);
|
||||||
|
new_filename = Utilities::PathWithoutFilenameExtension(new_filename) +
|
||||||
|
"(" + song_number + ")." +
|
||||||
|
QFileInfo(new_filename).suffix();
|
||||||
|
}
|
||||||
|
filenames.insert(new_filename, 1);
|
||||||
|
new_filenames << new_filename;
|
||||||
|
}
|
||||||
|
return new_filenames;
|
||||||
|
}
|
||||||
|
|
||||||
QString OrganiseFormat::ParseBlock(QString block, const Song& song,
|
QString OrganiseFormat::ParseBlock(QString block, const Song& song,
|
||||||
bool* any_empty) const {
|
bool* any_empty) const {
|
||||||
QRegExp tag_regexp(kTagPattern);
|
QRegExp tag_regexp(kTagPattern);
|
||||||
|
|
|
@ -20,15 +20,19 @@
|
||||||
#ifndef CORE_ORGANISEFORMAT_H_
|
#ifndef CORE_ORGANISEFORMAT_H_
|
||||||
#define CORE_ORGANISEFORMAT_H_
|
#define CORE_ORGANISEFORMAT_H_
|
||||||
|
|
||||||
|
#include <QStringList>
|
||||||
#include <QSyntaxHighlighter>
|
#include <QSyntaxHighlighter>
|
||||||
#include <QTextEdit>
|
#include <QTextEdit>
|
||||||
#include <QValidator>
|
#include <QValidator>
|
||||||
|
|
||||||
#include "core/song.h"
|
#include "core/song.h"
|
||||||
|
|
||||||
|
struct TranscoderPreset;
|
||||||
|
|
||||||
class OrganiseFormat {
|
class OrganiseFormat {
|
||||||
public:
|
public:
|
||||||
explicit OrganiseFormat(const QString& format = QString());
|
explicit OrganiseFormat(const QString& format = QString());
|
||||||
|
OrganiseFormat(const OrganiseFormat& format) = default;
|
||||||
|
|
||||||
static const char* kTagPattern;
|
static const char* kTagPattern;
|
||||||
static const char* kBlockPattern;
|
static const char* kBlockPattern;
|
||||||
|
@ -54,7 +58,11 @@ class OrganiseFormat {
|
||||||
void reset_tag_overrides() { tag_overrides_.clear(); }
|
void reset_tag_overrides() { tag_overrides_.clear(); }
|
||||||
|
|
||||||
bool IsValid() const;
|
bool IsValid() const;
|
||||||
QString GetFilenameForSong(const Song& song) const;
|
QString GetFilenameForSong(const Song& song, QString prefix_path = "") const;
|
||||||
|
QString GetFilenameForSong(const Song& song,
|
||||||
|
const TranscoderPreset& transcoder_preset,
|
||||||
|
QString prefix_path = "") const;
|
||||||
|
QStringList GetFilenamesForSongs(const SongList& songs) const;
|
||||||
|
|
||||||
class Validator : public QValidator {
|
class Validator : public QValidator {
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -156,10 +156,13 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString& filename) {
|
||||||
SongLoader::Result SongLoader::LoadAudioCD() {
|
SongLoader::Result SongLoader::LoadAudioCD() {
|
||||||
#ifdef HAVE_AUDIOCD
|
#ifdef HAVE_AUDIOCD
|
||||||
CddaSongLoader* cdda_song_loader = new CddaSongLoader;
|
CddaSongLoader* cdda_song_loader = new CddaSongLoader;
|
||||||
connect(cdda_song_loader, SIGNAL(SongsDurationLoaded(SongList)), this,
|
connect(cdda_song_loader, &CddaSongLoader::SongsUpdated, this,
|
||||||
SLOT(AudioCDTracksLoadedSlot(SongList)));
|
&SongLoader::AudioCDTracksLoadedSlot);
|
||||||
connect(cdda_song_loader, SIGNAL(SongsMetadataLoaded(SongList)), this,
|
connect(cdda_song_loader, &CddaSongLoader::Finished,
|
||||||
SLOT(AudioCDTracksTagsLoaded(SongList)));
|
[this, cdda_song_loader]() {
|
||||||
|
cdda_song_loader->deleteLater();
|
||||||
|
emit LoadAudioCDFinished(true);
|
||||||
|
});
|
||||||
cdda_song_loader->LoadSongs();
|
cdda_song_loader->LoadSongs();
|
||||||
return Success;
|
return Success;
|
||||||
#else // HAVE_AUDIOCD
|
#else // HAVE_AUDIOCD
|
||||||
|
@ -172,13 +175,6 @@ void SongLoader::AudioCDTracksLoadedSlot(const SongList& songs) {
|
||||||
songs_ = songs;
|
songs_ = songs;
|
||||||
emit AudioCDTracksLoaded();
|
emit AudioCDTracksLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SongLoader::AudioCDTracksTagsLoaded(const SongList& songs) {
|
|
||||||
CddaSongLoader* cdda_song_loader = qobject_cast<CddaSongLoader*>(sender());
|
|
||||||
cdda_song_loader->deleteLater();
|
|
||||||
songs_ = songs;
|
|
||||||
emit LoadAudioCDFinished(true);
|
|
||||||
}
|
|
||||||
#endif // HAVE_AUDIOCD
|
#endif // HAVE_AUDIOCD
|
||||||
|
|
||||||
SongLoader::Result SongLoader::LoadLocal(const QString& filename) {
|
SongLoader::Result SongLoader::LoadLocal(const QString& filename) {
|
||||||
|
@ -421,6 +417,7 @@ SongLoader::Result SongLoader::LoadRemote() {
|
||||||
CHECKED_GCONNECT(typefind, "have-type", &TypeFound, this);
|
CHECKED_GCONNECT(typefind, "have-type", &TypeFound, this);
|
||||||
gst_bus_set_sync_handler(bus, BusCallbackSync, this, NULL);
|
gst_bus_set_sync_handler(bus, BusCallbackSync, this, NULL);
|
||||||
gst_bus_add_watch(bus, BusCallback, this);
|
gst_bus_add_watch(bus, BusCallback, this);
|
||||||
|
gst_object_unref(bus);
|
||||||
|
|
||||||
// Add a probe to the sink so we can capture the data if it's a playlist
|
// Add a probe to the sink so we can capture the data if it's a playlist
|
||||||
GstPad* pad = gst_element_get_static_pad(fakesink, "sink");
|
GstPad* pad = gst_element_get_static_pad(fakesink, "sink");
|
||||||
|
|
|
@ -88,7 +88,6 @@ class SongLoader : public QObject {
|
||||||
void StopTypefind();
|
void StopTypefind();
|
||||||
#ifdef HAVE_AUDIOCD
|
#ifdef HAVE_AUDIOCD
|
||||||
void AudioCDTracksLoadedSlot(const SongList& songs);
|
void AudioCDTracksLoadedSlot(const SongList& songs);
|
||||||
void AudioCDTracksTagsLoaded(const SongList& songs);
|
|
||||||
#endif // HAVE_AUDIOCD
|
#endif // HAVE_AUDIOCD
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -34,9 +34,6 @@
|
||||||
#include "core/tagreaderclient.h"
|
#include "core/tagreaderclient.h"
|
||||||
#include "core/utilities.h"
|
#include "core/utilities.h"
|
||||||
#include "internet/core/internetmodel.h"
|
#include "internet/core/internetmodel.h"
|
||||||
#ifdef HAVE_SPOTIFY
|
|
||||||
#include "internet/spotify/spotifyservice.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
AlbumCoverLoader::AlbumCoverLoader(QObject* parent)
|
AlbumCoverLoader::AlbumCoverLoader(QObject* parent)
|
||||||
: QObject(parent),
|
: QObject(parent),
|
||||||
|
@ -180,31 +177,7 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(
|
||||||
|
|
||||||
remote_tasks_.insert(reply, task);
|
remote_tasks_.insert(reply, task);
|
||||||
return TryLoadResult(true, false, QImage());
|
return TryLoadResult(true, false, QImage());
|
||||||
}
|
} else if (filename.isEmpty()) {
|
||||||
#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()) {
|
|
||||||
// Avoid "QFSFileEngine::open: No file name specified" messages if we know
|
// Avoid "QFSFileEngine::open: No file name specified" messages if we know
|
||||||
// that the filename is empty
|
// that the filename is empty
|
||||||
return TryLoadResult(false, false, task.options.default_output_image_);
|
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);
|
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) {
|
void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply* reply) {
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
|
|
||||||
|
|
|
@ -67,9 +67,6 @@ class AlbumCoverLoader : public QObject {
|
||||||
protected slots:
|
protected slots:
|
||||||
void ProcessTasks();
|
void ProcessTasks();
|
||||||
void RemoteFetchFinished(QNetworkReply* reply);
|
void RemoteFetchFinished(QNetworkReply* reply);
|
||||||
#ifdef HAVE_SPOTIFY
|
|
||||||
void SpotifyImageLoaded(const QString& url, const QImage& image);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
enum State {
|
enum State {
|
||||||
|
|
|
@ -32,12 +32,10 @@ CddaDevice::CddaDevice(const QUrl& url, DeviceLister* lister,
|
||||||
cdio_(nullptr),
|
cdio_(nullptr),
|
||||||
disc_changed_timer_(),
|
disc_changed_timer_(),
|
||||||
cdda_song_loader_(url) {
|
cdda_song_loader_(url) {
|
||||||
connect(&cdda_song_loader_, SIGNAL(SongsLoaded(SongList)), this,
|
connect(&cdda_song_loader_, SIGNAL(SongsUpdated(SongList)), this,
|
||||||
SLOT(SongsLoaded(SongList)));
|
|
||||||
connect(&cdda_song_loader_, SIGNAL(SongsDurationLoaded(SongList)), this,
|
|
||||||
SLOT(SongsLoaded(SongList)));
|
|
||||||
connect(&cdda_song_loader_, SIGNAL(SongsMetadataLoaded(SongList)), this,
|
|
||||||
SLOT(SongsLoaded(SongList)));
|
SLOT(SongsLoaded(SongList)));
|
||||||
|
connect(&cdda_song_loader_, SIGNAL(Finished()), this,
|
||||||
|
SLOT(SongsLoadingFinished()));
|
||||||
connect(this, SIGNAL(SongsDiscovered(SongList)), model_,
|
connect(this, SIGNAL(SongsDiscovered(SongList)), model_,
|
||||||
SLOT(SongsDiscovered(SongList)));
|
SLOT(SongsDiscovered(SongList)));
|
||||||
connect(&disc_changed_timer_, SIGNAL(timeout()), SLOT(CheckDiscChanged()));
|
connect(&disc_changed_timer_, SIGNAL(timeout()), SLOT(CheckDiscChanged()));
|
||||||
|
@ -62,8 +60,6 @@ bool CddaDevice::Init() {
|
||||||
|
|
||||||
CddaSongLoader* CddaDevice::loader() { return &cdda_song_loader_; }
|
CddaSongLoader* CddaDevice::loader() { return &cdda_song_loader_; }
|
||||||
|
|
||||||
CdIo_t* CddaDevice::raw_cdio() { return cdio_; }
|
|
||||||
|
|
||||||
bool CddaDevice::IsValid() const { return (cdio_ != nullptr); }
|
bool CddaDevice::IsValid() const { return (cdio_ != nullptr); }
|
||||||
|
|
||||||
void CddaDevice::WatchForDiscChanges(bool watch) {
|
void CddaDevice::WatchForDiscChanges(bool watch) {
|
||||||
|
@ -73,14 +69,29 @@ void CddaDevice::WatchForDiscChanges(bool watch) {
|
||||||
disc_changed_timer_.stop();
|
disc_changed_timer_.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CddaDevice::LoadSongs() { cdda_song_loader_.LoadSongs(); }
|
void CddaDevice::LoadSongs() {
|
||||||
|
cdda_song_loader_.LoadSongs();
|
||||||
|
disc_changed_timer_.stop();
|
||||||
|
}
|
||||||
|
|
||||||
void CddaDevice::SongsLoaded(const SongList& songs) {
|
void CddaDevice::SongsLoaded(const SongList& songs) {
|
||||||
model_->Reset();
|
model_->Reset();
|
||||||
emit SongsDiscovered(songs);
|
|
||||||
song_count_ = songs.size();
|
song_count_ = songs.size();
|
||||||
|
emit SongsDiscovered(songs);
|
||||||
|
// When a disc is inserted, cdio_get_media_changed will
|
||||||
|
// return true for two times with a bit of delay in between
|
||||||
|
// (at least on linux).
|
||||||
|
// We clear cdio_get_media_changed after songs are
|
||||||
|
// loaded, so we don't potentially re-read the same disc.terminal
|
||||||
|
// There's a slight chance that this hides an actual
|
||||||
|
// media change, but this should be rare enough to not
|
||||||
|
// be a problem in practice and is easily rectified
|
||||||
|
// by user cycling the disc once more.
|
||||||
|
cdio_get_media_changed(cdio_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CddaDevice::SongsLoadingFinished() { disc_changed_timer_.start(); }
|
||||||
|
|
||||||
void CddaDevice::CheckDiscChanged() {
|
void CddaDevice::CheckDiscChanged() {
|
||||||
if (!cdio_) return; // do nothing if not initialized
|
if (!cdio_) return; // do nothing if not initialized
|
||||||
|
|
||||||
|
@ -96,3 +107,5 @@ void CddaDevice::CheckDiscChanged() {
|
||||||
LoadSongs();
|
LoadSongs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SongList CddaDevice::songs() const { return cdda_song_loader_.cached_tracks(); }
|
||||||
|
|
|
@ -49,11 +49,10 @@ class CddaDevice : public ConnectedDevice {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
CddaSongLoader* loader();
|
CddaSongLoader* loader();
|
||||||
// Access to the raw cdio device handle.
|
|
||||||
CdIo_t* raw_cdio(); // TODO: not ideal, but Ripper needs this currently
|
|
||||||
// Check whether a valid device handle was opened.
|
// Check whether a valid device handle was opened.
|
||||||
bool IsValid() const;
|
bool IsValid() const;
|
||||||
void WatchForDiscChanges(bool watch);
|
void WatchForDiscChanges(bool watch);
|
||||||
|
SongList songs() const;
|
||||||
|
|
||||||
static QStringList url_schemes() { return QStringList() << "cdda"; }
|
static QStringList url_schemes() { return QStringList() << "cdda"; }
|
||||||
|
|
||||||
|
@ -74,6 +73,7 @@ class CddaDevice : public ConnectedDevice {
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void SongsLoaded(const SongList& songs);
|
void SongsLoaded(const SongList& songs);
|
||||||
|
void SongsLoadingFinished();
|
||||||
void CheckDiscChanged();
|
void CheckDiscChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* This file is part of Clementine.
|
/* This file is part of Clementine.
|
||||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||||
|
Copyright 2021, Lukas Prediger <lumip@lumip.de>
|
||||||
|
|
||||||
Clementine is free software: you can redistribute it and/or modify
|
Clementine is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -27,9 +28,21 @@
|
||||||
#include "core/timeconstants.h"
|
#include "core/timeconstants.h"
|
||||||
|
|
||||||
CddaSongLoader::CddaSongLoader(const QUrl& url, QObject* parent)
|
CddaSongLoader::CddaSongLoader(const QUrl& url, QObject* parent)
|
||||||
: QObject(parent), url_(url), cdda_(nullptr), may_load_(true) {
|
: QObject(parent), url_(url), cdda_(nullptr), may_load_(true), disc_() {
|
||||||
connect(this, SIGNAL(MusicBrainzDiscIdLoaded(const QString&)),
|
connect(this, &CddaSongLoader::MusicBrainzDiscIdLoaded, this,
|
||||||
SLOT(LoadAudioCDTags(const QString&)));
|
&CddaSongLoader::LoadAudioCDTags);
|
||||||
|
connect(this, &CddaSongLoader::SongsLoaded,
|
||||||
|
[this](const SongList& song_list) {
|
||||||
|
SetDiscTracks(song_list, /*has_titles=*/false);
|
||||||
|
});
|
||||||
|
connect(this, &CddaSongLoader::SongsDurationLoaded,
|
||||||
|
[this](const SongList& song_list) {
|
||||||
|
SetDiscTracks(song_list, /*has_titles=*/false);
|
||||||
|
});
|
||||||
|
connect(this, &CddaSongLoader::SongsMetadataLoaded,
|
||||||
|
[this](const SongList& song_list) {
|
||||||
|
SetDiscTracks(song_list, /*has_titles=*/true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
CddaSongLoader::~CddaSongLoader() {
|
CddaSongLoader::~CddaSongLoader() {
|
||||||
|
@ -55,21 +68,99 @@ bool CddaSongLoader::IsActive() const { return loading_future_.isRunning(); }
|
||||||
void CddaSongLoader::LoadSongs() {
|
void CddaSongLoader::LoadSongs() {
|
||||||
// only dispatch a new thread for loading tracks if not already running.
|
// only dispatch a new thread for loading tracks if not already running.
|
||||||
if (!IsActive()) {
|
if (!IsActive()) {
|
||||||
|
QMutexLocker lock(&disc_mutex_);
|
||||||
|
disc_ = Disc();
|
||||||
loading_future_ =
|
loading_future_ =
|
||||||
QtConcurrent::run(this, &CddaSongLoader::LoadSongsFromCdda);
|
QtConcurrent::run(this, &CddaSongLoader::LoadSongsFromCdda);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CddaSongLoader::LoadSongsFromCdda() {
|
bool CddaSongLoader::ParseSongTags(SongList& songs, GstTagList* tags,
|
||||||
if (!may_load_) return;
|
gint* track_no) {
|
||||||
|
//// cdiocddasrc reads cd-text with following mapping from cdio
|
||||||
|
///
|
||||||
|
/// DISC LEVEL :
|
||||||
|
/// CDTEXT_FIELD_PERFORMER -> GST_TAG_ALBUM_ARTIST
|
||||||
|
/// CDTEXT_FIELD_TITLE -> GST_TAG_ALBUM
|
||||||
|
/// CDTEXT_FIELD_GENRE -> GST_TAG_GENRE
|
||||||
|
///
|
||||||
|
/// TRACK LEVEL :
|
||||||
|
/// CDTEXT_FIELD_PERFORMER -> GST_TAG_ARTIST
|
||||||
|
/// CDTEXT_FIELD_TITLE -> GST_TAG_TITLE
|
||||||
|
|
||||||
|
guint track_number;
|
||||||
|
if (!gst_tag_list_get_uint(tags, GST_TAG_TRACK_NUMBER, &track_number)) {
|
||||||
|
qLog(Error) << "Track tags do not contain track number!";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(track_number != 0u);
|
||||||
|
Q_ASSERT(static_cast<int>(track_number) <= songs.size());
|
||||||
|
Song& song = songs[static_cast<int>(track_number - 1)];
|
||||||
|
*track_no = static_cast<gint>(track_number) - 1;
|
||||||
|
|
||||||
|
// qLog(Debug) << gst_tag_list_to_string(tags);
|
||||||
|
|
||||||
|
bool has_loaded_tags = false;
|
||||||
|
|
||||||
|
gchar* buffer = nullptr;
|
||||||
|
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM, &buffer)) {
|
||||||
|
has_loaded_tags = true;
|
||||||
|
song.set_album(QString::fromUtf8(buffer));
|
||||||
|
g_free(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_ARTIST, &buffer)) {
|
||||||
|
has_loaded_tags = true;
|
||||||
|
song.set_albumartist(QString::fromUtf8(buffer));
|
||||||
|
g_free(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gst_tag_list_get_string(tags, GST_TAG_GENRE, &buffer)) {
|
||||||
|
has_loaded_tags = true;
|
||||||
|
song.set_genre(QString::fromUtf8(buffer));
|
||||||
|
g_free(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gst_tag_list_get_string(tags, GST_TAG_ARTIST, &buffer)) {
|
||||||
|
has_loaded_tags = true;
|
||||||
|
song.set_artist(QString::fromUtf8(buffer));
|
||||||
|
g_free(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gst_tag_list_get_string(tags, GST_TAG_TITLE, &buffer)) {
|
||||||
|
has_loaded_tags = true;
|
||||||
|
song.set_title(QString::fromUtf8(buffer));
|
||||||
|
g_free(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
guint64 duration;
|
||||||
|
if (gst_tag_list_get_uint64(tags, GST_TAG_DURATION, &duration)) {
|
||||||
|
has_loaded_tags = true;
|
||||||
|
song.set_length_nanosec(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
song.set_track(track_number);
|
||||||
|
song.set_id(track_number);
|
||||||
|
song.set_filetype(Song::Type_Cdda);
|
||||||
|
song.set_valid(true);
|
||||||
|
song.set_url(GetUrlFromTrack(track_number));
|
||||||
|
return has_loaded_tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CddaSongLoader::LoadSongsFromCdda() {
|
||||||
|
SongList initial_song_list;
|
||||||
|
|
||||||
|
if (!may_load_) return;
|
||||||
// Create gstreamer cdda element
|
// Create gstreamer cdda element
|
||||||
GError* error = nullptr;
|
GError* error = nullptr;
|
||||||
cdda_ = gst_element_make_from_uri(GST_URI_SRC, "cdda://", nullptr, &error);
|
GstElement* cdda_ = gst_element_factory_make("cdiocddasrc", nullptr);
|
||||||
if (error) {
|
if (error) {
|
||||||
qLog(Error) << error->code << QString::fromLocal8Bit(error->message);
|
qLog(Error) << error->code << QString::fromLocal8Bit(error->message);
|
||||||
}
|
}
|
||||||
if (cdda_ == nullptr) {
|
if (cdda_ == nullptr) {
|
||||||
|
emit SongsLoaded(initial_song_list);
|
||||||
|
emit Finished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +170,7 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
||||||
}
|
}
|
||||||
if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda_),
|
if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda_),
|
||||||
"paranoia-mode")) {
|
"paranoia-mode")) {
|
||||||
g_object_set(cdda_, "paranoia-mode", 0, NULL);
|
g_object_set(cdda_, "paranoia-mode", 0, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the element's state to ready and paused, to be able to query it
|
// Change the element's state to ready and paused, to be able to query it
|
||||||
|
@ -89,21 +180,24 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
||||||
GST_STATE_CHANGE_FAILURE) {
|
GST_STATE_CHANGE_FAILURE) {
|
||||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
gst_element_set_state(cdda_, GST_STATE_NULL);
|
||||||
gst_object_unref(GST_OBJECT(cdda_));
|
gst_object_unref(GST_OBJECT(cdda_));
|
||||||
|
|
||||||
|
emit SongsLoaded(initial_song_list);
|
||||||
|
emit Finished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get number of tracks
|
// Get number of tracks
|
||||||
GstFormat fmt = gst_format_get_by_nick("track");
|
GstFormat track_fmt = gst_format_get_by_nick("track");
|
||||||
GstFormat out_fmt = fmt;
|
|
||||||
gint64 num_tracks = 0;
|
gint64 num_tracks = 0;
|
||||||
if (!gst_element_query_duration(cdda_, out_fmt, &num_tracks) ||
|
if (!gst_element_query_duration(cdda_, track_fmt, &num_tracks)) {
|
||||||
out_fmt != fmt) {
|
qLog(Error) << "Error while querying cdda GstElement for track count";
|
||||||
qLog(Error) << "Error while querying cdda GstElement";
|
|
||||||
gst_object_unref(GST_OBJECT(cdda_));
|
gst_object_unref(GST_OBJECT(cdda_));
|
||||||
|
|
||||||
|
emit SongsLoaded(initial_song_list);
|
||||||
|
emit Finished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SongList songs;
|
|
||||||
for (int track_number = 1; track_number <= num_tracks; track_number++) {
|
for (int track_number = 1; track_number <= num_tracks; track_number++) {
|
||||||
// Init song
|
// Init song
|
||||||
Song song;
|
Song song;
|
||||||
|
@ -113,15 +207,17 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
||||||
song.set_url(GetUrlFromTrack(track_number));
|
song.set_url(GetUrlFromTrack(track_number));
|
||||||
song.set_title(QString("Track %1").arg(track_number));
|
song.set_title(QString("Track %1").arg(track_number));
|
||||||
song.set_track(track_number);
|
song.set_track(track_number);
|
||||||
songs << song;
|
initial_song_list << song;
|
||||||
}
|
}
|
||||||
emit SongsLoaded(songs);
|
emit SongsLoaded(initial_song_list);
|
||||||
|
|
||||||
|
SongList tagged_song_list(initial_song_list);
|
||||||
|
|
||||||
gst_tag_register_musicbrainz_tags();
|
gst_tag_register_musicbrainz_tags();
|
||||||
|
|
||||||
GstElement* pipeline = gst_pipeline_new("pipeline");
|
GstElement* pipeline = gst_pipeline_new("pipeline");
|
||||||
GstElement* sink = gst_element_factory_make("fakesink", NULL);
|
GstElement* sink = gst_element_factory_make("fakesink", NULL);
|
||||||
gst_bin_add_many(GST_BIN(pipeline), cdda_, sink, NULL);
|
gst_bin_add_many(GST_BIN(pipeline), cdda_, sink, nullptr);
|
||||||
gst_element_link(cdda_, sink);
|
gst_element_link(cdda_, sink);
|
||||||
gst_element_set_state(pipeline, GST_STATE_READY);
|
gst_element_set_state(pipeline, GST_STATE_READY);
|
||||||
gst_element_set_state(pipeline, GST_STATE_PAUSED);
|
gst_element_set_state(pipeline, GST_STATE_PAUSED);
|
||||||
|
@ -131,6 +227,7 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
||||||
GstMessageType msg_filter =
|
GstMessageType msg_filter =
|
||||||
static_cast<GstMessageType>(GST_MESSAGE_TOC | GST_MESSAGE_TAG);
|
static_cast<GstMessageType>(GST_MESSAGE_TOC | GST_MESSAGE_TAG);
|
||||||
QString musicbrainz_discid;
|
QString musicbrainz_discid;
|
||||||
|
bool loaded_cd_tags = false;
|
||||||
while (may_load_ && msg_filter &&
|
while (may_load_ && msg_filter &&
|
||||||
(msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline),
|
(msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline),
|
||||||
10 * GST_SECOND, msg_filter))) {
|
10 * GST_SECOND, msg_filter))) {
|
||||||
|
@ -140,7 +237,7 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
||||||
gst_message_parse_toc(msg, &toc, nullptr);
|
gst_message_parse_toc(msg, &toc, nullptr);
|
||||||
if (toc) {
|
if (toc) {
|
||||||
GList* entries = gst_toc_get_entries(toc);
|
GList* entries = gst_toc_get_entries(toc);
|
||||||
if (entries && songs.size() <= g_list_length(entries)) {
|
if (entries && initial_song_list.size() <= g_list_length(entries)) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (GList* node = entries; node != nullptr; node = node->next) {
|
for (GList* node = entries; node != nullptr; node = node->next) {
|
||||||
GstTocEntry* entry = static_cast<GstTocEntry*>(node->data);
|
GstTocEntry* entry = static_cast<GstTocEntry*>(node->data);
|
||||||
|
@ -148,35 +245,84 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
||||||
gint64 start, stop;
|
gint64 start, stop;
|
||||||
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop))
|
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop))
|
||||||
duration = stop - start;
|
duration = stop - start;
|
||||||
songs[i++].set_length_nanosec(duration);
|
initial_song_list[i++].set_length_nanosec(duration);
|
||||||
}
|
}
|
||||||
emit SongsDurationLoaded(songs);
|
emit SongsDurationLoaded(initial_song_list);
|
||||||
msg_filter = static_cast<GstMessageType>(
|
msg_filter = static_cast<GstMessageType>(
|
||||||
static_cast<int>(msg_filter) ^ GST_MESSAGE_TOC);
|
static_cast<int>(msg_filter) ^ GST_MESSAGE_TOC);
|
||||||
}
|
}
|
||||||
gst_toc_unref(toc);
|
gst_toc_unref(toc);
|
||||||
}
|
}
|
||||||
} else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
|
} else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
|
||||||
// Handle TAG message: generate MusicBrainz DiscId
|
// Handle TAG message: generate MusicBrainz DiscId and read CD-TEXT if
|
||||||
|
// present
|
||||||
|
|
||||||
|
gint64
|
||||||
|
track_number_from_query; // track number gstreamer thinks we are at
|
||||||
|
gst_element_query_position(cdda_, track_fmt, &track_number_from_query);
|
||||||
|
|
||||||
GstTagList* tags = nullptr;
|
GstTagList* tags = nullptr;
|
||||||
gst_message_parse_tag(msg, &tags);
|
gst_message_parse_tag(msg, &tags);
|
||||||
char* string_mb = nullptr;
|
char* string_mb = nullptr;
|
||||||
if (gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID,
|
if (musicbrainz_discid.isEmpty() &&
|
||||||
|
gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID,
|
||||||
&string_mb)) {
|
&string_mb)) {
|
||||||
QString musicbrainz_discid = QString::fromUtf8(string_mb);
|
musicbrainz_discid = QString::fromUtf8(string_mb);
|
||||||
g_free(string_mb);
|
g_free(string_mb);
|
||||||
|
|
||||||
qLog(Info) << "MusicBrainz discid: " << musicbrainz_discid;
|
qLog(Info) << "MusicBrainz discid: " << musicbrainz_discid;
|
||||||
emit MusicBrainzDiscIdLoaded(musicbrainz_discid);
|
}
|
||||||
|
|
||||||
|
gint track_number_from_tags; // track number contained in the tag message
|
||||||
|
loaded_cd_tags |=
|
||||||
|
ParseSongTags(tagged_song_list, tags, &track_number_from_tags);
|
||||||
|
gst_tag_list_free(tags);
|
||||||
|
|
||||||
|
// We may receive a tag message for a track we have already seen, not for
|
||||||
|
// the track we seeked to previously, i.e., track_number_from_tags and
|
||||||
|
// track_number_from_query do not agree. If we would just wait now,
|
||||||
|
// nothing else would happen: It seems, gstreamer will for some reason not
|
||||||
|
// pass the tag message for the song we seeked to in this case or it gets
|
||||||
|
// lost somewhere. We can't seek again to the track we want to see,
|
||||||
|
// because gstreamer thinks we are already there and will do nothing. We
|
||||||
|
// therefore seek to the previous track and resume from there.
|
||||||
|
// note(lumip): There's a slight risk of an infinite loop here where if
|
||||||
|
// the above behavior repeats consistently, but in my tests this does not
|
||||||
|
// happen.
|
||||||
|
if (track_number_from_tags < track_number_from_query) {
|
||||||
|
qLog(Debug) << "message query mismatch! : " << track_number_from_tags
|
||||||
|
<< " vs " << track_number_from_query;
|
||||||
|
gst_element_seek_simple(
|
||||||
|
pipeline, track_fmt,
|
||||||
|
static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH |
|
||||||
|
GST_SEEK_FLAG_TRICKMODE),
|
||||||
|
track_number_from_tags);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
gint next_track_number = track_number_from_tags + 1;
|
||||||
|
|
||||||
|
if (next_track_number < num_tracks) {
|
||||||
|
// more to go, seek to next track to get a tag message for it
|
||||||
|
gst_element_seek_simple(
|
||||||
|
pipeline, track_fmt,
|
||||||
|
static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH |
|
||||||
|
GST_SEEK_FLAG_TRICKMODE),
|
||||||
|
next_track_number);
|
||||||
|
} else // we are done with reading track tags: do no longer filter
|
||||||
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^
|
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^
|
||||||
GST_MESSAGE_TAG);
|
GST_MESSAGE_TAG);
|
||||||
}
|
|
||||||
gst_tag_list_free(tags);
|
|
||||||
}
|
}
|
||||||
gst_message_unref(msg);
|
gst_message_unref(msg);
|
||||||
}
|
}
|
||||||
|
if (loaded_cd_tags) emit SongsMetadataLoaded(tagged_song_list);
|
||||||
|
|
||||||
|
if (!musicbrainz_discid.isEmpty())
|
||||||
|
emit MusicBrainzDiscIdLoaded(musicbrainz_discid);
|
||||||
|
else {
|
||||||
|
// no musicbrainz id was loaded, no further udpates will follow
|
||||||
|
emit Finished();
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
gst_element_set_state(pipeline, GST_STATE_NULL);
|
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||||||
// This will also cause cdda_ to be unref'd.
|
// This will also cause cdda_ to be unref'd.
|
||||||
gst_object_unref(pipeline);
|
gst_object_unref(pipeline);
|
||||||
|
@ -187,36 +333,63 @@ void CddaSongLoader::LoadAudioCDTags(const QString& musicbrainz_discid) const {
|
||||||
connect(musicbrainz_client,
|
connect(musicbrainz_client,
|
||||||
SIGNAL(Finished(const QString&, const QString&,
|
SIGNAL(Finished(const QString&, const QString&,
|
||||||
MusicBrainzClient::ResultList)),
|
MusicBrainzClient::ResultList)),
|
||||||
SLOT(AudioCDTagsLoaded(const QString&, const QString&,
|
SLOT(ProcessMusicBrainzResponse(const QString&, const QString&,
|
||||||
MusicBrainzClient::ResultList)));
|
MusicBrainzClient::ResultList)));
|
||||||
|
|
||||||
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
|
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CddaSongLoader::AudioCDTagsLoaded(
|
void CddaSongLoader::ProcessMusicBrainzResponse(
|
||||||
const QString& artist, const QString& album,
|
const QString& artist, const QString& album,
|
||||||
const MusicBrainzClient::ResultList& results) {
|
const MusicBrainzClient::ResultList& results) {
|
||||||
MusicBrainzClient* musicbrainz_client =
|
MusicBrainzClient* musicbrainz_client =
|
||||||
qobject_cast<MusicBrainzClient*>(sender());
|
qobject_cast<MusicBrainzClient*>(sender());
|
||||||
musicbrainz_client->deleteLater();
|
musicbrainz_client->deleteLater();
|
||||||
SongList songs;
|
if (results.empty()) {
|
||||||
if (results.empty()) return;
|
// no real update; signal that no further updates will follow now
|
||||||
int track_number = 1;
|
emit Finished();
|
||||||
for (const MusicBrainzClient::Result& ret : results) {
|
return;
|
||||||
Song song;
|
|
||||||
song.set_artist(artist);
|
|
||||||
song.set_album(album);
|
|
||||||
song.set_title(ret.title_);
|
|
||||||
song.set_length_nanosec(ret.duration_msec_ * kNsecPerMsec);
|
|
||||||
song.set_track(track_number);
|
|
||||||
song.set_year(ret.year_);
|
|
||||||
song.set_id(track_number);
|
|
||||||
song.set_filetype(Song::Type_Cdda);
|
|
||||||
song.set_valid(true);
|
|
||||||
// We need to set url: that's how playlist will find the correct item to
|
|
||||||
// update
|
|
||||||
song.set_url(GetUrlFromTrack(track_number++));
|
|
||||||
songs << song;
|
|
||||||
}
|
}
|
||||||
emit SongsMetadataLoaded(songs);
|
|
||||||
|
{
|
||||||
|
QMutexLocker lock(&disc_mutex_);
|
||||||
|
|
||||||
|
if (disc_.tracks.length() != results.length()) {
|
||||||
|
qLog(Warning) << "Number of tracks in metadata does not match number of "
|
||||||
|
"songs on disc!";
|
||||||
|
// no idea how to recover; signal that no further updates will follow now
|
||||||
|
emit Finished();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < results.length(); ++i) {
|
||||||
|
const MusicBrainzClient::Result& new_song_info = results[i];
|
||||||
|
Song& song = disc_.tracks[i];
|
||||||
|
|
||||||
|
if (!disc_.has_titles) song.set_title(new_song_info.title_);
|
||||||
|
if (song.album().isEmpty()) song.set_album(album);
|
||||||
|
if (song.artist().isEmpty()) song.set_artist(artist);
|
||||||
|
|
||||||
|
if (song.length_nanosec() == -1)
|
||||||
|
song.set_length_nanosec(new_song_info.duration_msec_ * kNsecPerMsec);
|
||||||
|
if (song.track() < 1) song.set_track(new_song_info.track_);
|
||||||
|
if (song.year() == -1) song.set_year(new_song_info.year_);
|
||||||
|
}
|
||||||
|
disc_.has_titles = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit SongsMetadataLoaded(disc_.tracks);
|
||||||
|
emit Finished(); // no further updates will follow
|
||||||
|
}
|
||||||
|
|
||||||
|
void CddaSongLoader::SetDiscTracks(const SongList& songs, bool has_titles) {
|
||||||
|
QMutexLocker lock(&disc_mutex_);
|
||||||
|
disc_.tracks = songs;
|
||||||
|
disc_.has_titles = has_titles;
|
||||||
|
emit SongsUpdated(disc_.tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
SongList CddaSongLoader::cached_tracks() const {
|
||||||
|
QMutexLocker lock(&disc_mutex_);
|
||||||
|
return disc_.tracks;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* This file is part of Clementine.
|
/* This file is part of Clementine.
|
||||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||||
|
Copyright 2021, Lukas Prediger <lumip@lumip.de>
|
||||||
|
|
||||||
Clementine is free software: you can redistribute it and/or modify
|
Clementine is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -42,30 +43,74 @@ class CddaSongLoader : public QObject {
|
||||||
~CddaSongLoader();
|
~CddaSongLoader();
|
||||||
|
|
||||||
// Load songs.
|
// Load songs.
|
||||||
// Signals declared below will be emitted anytime new information will be
|
// Signals declared below will be emitted anytime new information becomes
|
||||||
// available.
|
// available.
|
||||||
void LoadSongs();
|
void LoadSongs();
|
||||||
bool IsActive() const;
|
bool IsActive() const;
|
||||||
|
|
||||||
|
// The list of currently cached tracks. This gets updated when
|
||||||
|
// LoadSongs() is called.
|
||||||
|
SongList cached_tracks() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
// Emitted whenever information about tracks were updated.
|
||||||
|
// Guarantees consistency with previous updates, i.e., consumers can rely
|
||||||
|
// entirely on the updated list and do not have to merge metadata. May be
|
||||||
|
// emitted multiple times during reading the disc as long as the Finished
|
||||||
|
// signal was not emitted.
|
||||||
|
void SongsUpdated(const SongList& songs);
|
||||||
|
// Emitted when no further updates will follow; guaranteed to be emitted
|
||||||
|
// at some point.
|
||||||
|
void Finished();
|
||||||
|
|
||||||
|
// The following signals are mostly for internal processing; other classes
|
||||||
|
// can get all relevant updates by just connecting to SongsUpdated. However,
|
||||||
|
// the more specialised remain available.
|
||||||
|
|
||||||
|
// Emitted when the number of tracks has been initially loaded from the disc.
|
||||||
|
// This is a specialised signal; subscribe to SongsUpdated for general updates
|
||||||
|
// information about tracks is updated.
|
||||||
void SongsLoaded(const SongList& songs);
|
void SongsLoaded(const SongList& songs);
|
||||||
|
// Emitted when track durations have been loaded from the disc.
|
||||||
|
// This is a specialised signal; subscribe to SongsUpdated for general updates
|
||||||
|
// information about tracks is updated.
|
||||||
void SongsDurationLoaded(const SongList& songs);
|
void SongsDurationLoaded(const SongList& songs);
|
||||||
|
// Emitted when metadata has been loaded from the disc.
|
||||||
|
// This is a specialised signal; subscribe to SongsUpdated for general updates
|
||||||
|
// information about tracks is updated.
|
||||||
void SongsMetadataLoaded(const SongList& songs);
|
void SongsMetadataLoaded(const SongList& songs);
|
||||||
|
// Emitted when the MusicBrainz disc id has been determined.
|
||||||
|
// This is a specialised signal; subscribe to SongsUpdated for general updates
|
||||||
|
// information about tracks is updated.
|
||||||
void MusicBrainzDiscIdLoaded(const QString& musicbrainz_discid);
|
void MusicBrainzDiscIdLoaded(const QString& musicbrainz_discid);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void LoadAudioCDTags(const QString& musicbrainz_discid) const;
|
void LoadAudioCDTags(const QString& musicbrainz_discid) const;
|
||||||
void AudioCDTagsLoaded(const QString& artist, const QString& album,
|
void ProcessMusicBrainzResponse(const QString& artist, const QString& album,
|
||||||
const MusicBrainzClient::ResultList& results);
|
const MusicBrainzClient::ResultList& results);
|
||||||
|
void SetDiscTracks(const SongList& songs, bool has_titles);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QUrl GetUrlFromTrack(int track_number) const;
|
QUrl GetUrlFromTrack(int track_number) const;
|
||||||
void LoadSongsFromCdda();
|
void LoadSongsFromCdda();
|
||||||
|
// Parse gstreamer taglist for a song
|
||||||
|
// Returns true if any tags were read, updates the Song object in songs
|
||||||
|
// accordingly, and returns the zero-based track index via the track_no
|
||||||
|
// argument.
|
||||||
|
bool ParseSongTags(SongList& songs, GstTagList* tags, gint* track_no);
|
||||||
|
|
||||||
|
struct Disc {
|
||||||
|
SongList tracks;
|
||||||
|
bool has_titles; // indicates that titles have been read and are not
|
||||||
|
// defaulted
|
||||||
|
};
|
||||||
|
|
||||||
QUrl url_;
|
QUrl url_;
|
||||||
GstElement* cdda_;
|
GstElement* cdda_;
|
||||||
QFuture<void> loading_future_;
|
QFuture<void> loading_future_;
|
||||||
std::atomic<bool> may_load_;
|
std::atomic<bool> may_load_;
|
||||||
|
Disc disc_;
|
||||||
|
mutable QMutex disc_mutex_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // CDDASONGLOADER_H
|
#endif // CDDASONGLOADER_H
|
||||||
|
|
|
@ -681,7 +681,7 @@ void DeviceManager::Forget(QModelIndex idx) {
|
||||||
info->LoadIcon(info->BestBackend()->lister_->DeviceIcons(id),
|
info->LoadIcon(info->BestBackend()->lister_->DeviceIcons(id),
|
||||||
info->friendly_name_);
|
info->friendly_name_);
|
||||||
|
|
||||||
dataChanged(idx, idx);
|
emit dataChanged(idx, idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -132,7 +132,7 @@ void Udisks2Lister::UnmountDevice(const QString& id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
device_data_.remove(id);
|
device_data_.remove(id);
|
||||||
DeviceRemoved(id);
|
emit DeviceRemoved(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,7 +251,7 @@ void Udisks2Lister::RemoveDevice(const QDBusObjectPath& device_path) {
|
||||||
|
|
||||||
qLog(Debug) << "UDisks2 device removed: " << device_path.path();
|
qLog(Debug) << "UDisks2 device removed: " << device_path.path();
|
||||||
device_data_.remove(id);
|
device_data_.remove(id);
|
||||||
DeviceRemoved(id);
|
emit DeviceRemoved(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QDBusObjectPath> Udisks2Lister::GetMountedPartitionsFromDBusArgument(
|
QList<QDBusObjectPath> Udisks2Lister::GetMountedPartitionsFromDBusArgument(
|
||||||
|
@ -297,7 +297,7 @@ void Udisks2Lister::HandleFinishedMountJob(
|
||||||
<< " | Partition = " << partition_data.dbus_path;
|
<< " | Partition = " << partition_data.dbus_path;
|
||||||
QWriteLocker locker(&device_data_lock_);
|
QWriteLocker locker(&device_data_lock_);
|
||||||
device_data_[partition_data.unique_id()] = partition_data;
|
device_data_[partition_data.unique_id()] = partition_data;
|
||||||
DeviceAdded(partition_data.unique_id());
|
emit DeviceAdded(partition_data.unique_id());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Udisks2Lister::HandleFinishedUnmountJob(
|
void Udisks2Lister::HandleFinishedUnmountJob(
|
||||||
|
@ -320,7 +320,7 @@ void Udisks2Lister::HandleFinishedUnmountJob(
|
||||||
qLog(Debug) << "Partition " << partition_data.dbus_path
|
qLog(Debug) << "Partition " << partition_data.dbus_path
|
||||||
<< " has no more mount points, removing it from device list";
|
<< " has no more mount points, removing it from device list";
|
||||||
device_data_.remove(id);
|
device_data_.remove(id);
|
||||||
DeviceRemoved(id);
|
emit DeviceRemoved(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,10 +39,6 @@
|
||||||
#include "devices/cddadevice.h"
|
#include "devices/cddadevice.h"
|
||||||
#endif
|
#endif
|
||||||
#include "internet/core/internetmodel.h"
|
#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::kGstStateTimeoutNanosecs = 10000000;
|
||||||
const int GstEnginePipeline::kFaderFudgeMsec = 2000;
|
const int GstEnginePipeline::kFaderFudgeMsec = 2000;
|
||||||
|
@ -171,58 +167,14 @@ QByteArray GstEnginePipeline::GstUriFromUrl(const QUrl& url) {
|
||||||
|
|
||||||
GstElement* GstEnginePipeline::CreateDecodeBinFromUrl(const QUrl& url) {
|
GstElement* GstEnginePipeline::CreateDecodeBinFromUrl(const QUrl& url) {
|
||||||
GstElement* new_bin = nullptr;
|
GstElement* new_bin = nullptr;
|
||||||
#ifdef HAVE_SPOTIFY
|
QByteArray uri = GstUriFromUrl(url);
|
||||||
if (url.scheme() == "spotify") {
|
new_bin = engine_->CreateElement("uridecodebin");
|
||||||
new_bin = gst_bin_new("spotify_bin");
|
if (!new_bin) return nullptr;
|
||||||
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);
|
||||||
// Create elements
|
CHECKED_GCONNECT(G_OBJECT(new_bin), "pad-added", &NewPadCallback, this);
|
||||||
GstElement* src = engine_->CreateElement("tcpserversrc", new_bin);
|
CHECKED_GCONNECT(G_OBJECT(new_bin), "notify::source", &SourceSetupCallback,
|
||||||
if (!src) {
|
this);
|
||||||
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
|
|
||||||
|
|
||||||
return new_bin;
|
return new_bin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,7 +392,7 @@ bool GstEnginePipeline::InitAudioBin() {
|
||||||
gst_element_link(queue_, audioconvert_);
|
gst_element_link(queue_, audioconvert_);
|
||||||
|
|
||||||
GstCaps* caps16 = gst_caps_new_simple("audio/x-raw", "format", G_TYPE_STRING,
|
GstCaps* caps16 = gst_caps_new_simple("audio/x-raw", "format", G_TYPE_STRING,
|
||||||
"S16LE", NULL);
|
"S16LE", nullptr);
|
||||||
gst_element_link_filtered(probe_converter, probe_sink, caps16);
|
gst_element_link_filtered(probe_converter, probe_sink, caps16);
|
||||||
gst_caps_unref(caps16);
|
gst_caps_unref(caps16);
|
||||||
|
|
||||||
|
@ -499,7 +451,7 @@ bool GstEnginePipeline::InitAudioBin() {
|
||||||
gst_object_unref(pad);
|
gst_object_unref(pad);
|
||||||
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
|
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
|
||||||
gst_bus_set_sync_handler(bus, BusCallbackSync, this, nullptr);
|
gst_bus_set_sync_handler(bus, BusCallbackSync, this, nullptr);
|
||||||
bus_cb_id_ = gst_bus_add_watch(bus, BusCallback, this);
|
gst_bus_add_watch(bus, BusCallback, this);
|
||||||
gst_object_unref(bus);
|
gst_object_unref(bus);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -567,10 +519,11 @@ bool GstEnginePipeline::InitFromReq(const MediaPlaybackRequest& req,
|
||||||
GstEnginePipeline::~GstEnginePipeline() {
|
GstEnginePipeline::~GstEnginePipeline() {
|
||||||
if (pipeline_) {
|
if (pipeline_) {
|
||||||
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
|
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
|
||||||
|
gst_bus_remove_watch(bus);
|
||||||
|
|
||||||
gst_bus_set_sync_handler(bus, nullptr, nullptr, nullptr);
|
gst_bus_set_sync_handler(bus, nullptr, nullptr, nullptr);
|
||||||
gst_object_unref(bus);
|
gst_object_unref(bus);
|
||||||
|
|
||||||
g_source_remove(bus_cb_id_);
|
|
||||||
gst_element_set_state(pipeline_, GST_STATE_NULL);
|
gst_element_set_state(pipeline_, GST_STATE_NULL);
|
||||||
|
|
||||||
if (tee_) {
|
if (tee_) {
|
||||||
|
@ -1198,26 +1151,6 @@ GstState GstEnginePipeline::state() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(GstState state) {
|
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>(
|
return ConcurrentRun::Run<GstStateChangeReturn, GstElement*, GstState>(
|
||||||
&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
|
&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,8 +294,6 @@ class GstEnginePipeline : public GstPipelineBase {
|
||||||
GstPad* tee_probe_pad_;
|
GstPad* tee_probe_pad_;
|
||||||
GstPad* tee_audio_pad_;
|
GstPad* tee_audio_pad_;
|
||||||
|
|
||||||
uint bus_cb_id_;
|
|
||||||
|
|
||||||
QThreadPool set_state_threadpool_;
|
QThreadPool set_state_threadpool_;
|
||||||
|
|
||||||
GstSegment last_decodebin_segment_;
|
GstSegment last_decodebin_segment_;
|
||||||
|
|
|
@ -52,6 +52,4 @@ void DigitallyImportedSearchProvider::RecreateItems() {
|
||||||
SetItems(items);
|
SetItems(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DigitallyImportedSearchProvider::ShowConfig() {
|
void DigitallyImportedSearchProvider::ShowConfig() { service_->ShowConfig(); }
|
||||||
service_->ShowSettingsDialog();
|
|
||||||
}
|
|
||||||
|
|
|
@ -448,37 +448,39 @@ bool GlobalSearchView::SearchKeyEvent(QKeyEvent* event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GlobalSearchView::ResultsContextMenuEvent(QContextMenuEvent* event) {
|
bool GlobalSearchView::ResultsContextMenuEvent(QContextMenuEvent* event) {
|
||||||
context_menu_ = new QMenu(this);
|
if (!context_menu_) {
|
||||||
context_actions_ << context_menu_->addAction(
|
context_menu_ = new QMenu(this);
|
||||||
IconLoader::Load("media-playback-start", IconLoader::Base),
|
|
||||||
tr("Append to current playlist"), this, SLOT(AddSelectedToPlaylist()));
|
|
||||||
context_actions_ << context_menu_->addAction(
|
|
||||||
IconLoader::Load("media-playback-start", IconLoader::Base),
|
|
||||||
tr("Replace current playlist"), this, SLOT(LoadSelected()));
|
|
||||||
context_actions_ << context_menu_->addAction(
|
|
||||||
IconLoader::Load("document-new", IconLoader::Base),
|
|
||||||
tr("Open in new playlist"), this, SLOT(OpenSelectedInNewPlaylist()));
|
|
||||||
|
|
||||||
context_menu_->addSeparator();
|
|
||||||
context_actions_ << context_menu_->addAction(
|
|
||||||
IconLoader::Load("go-next", IconLoader::Base), tr("Queue track"), this,
|
|
||||||
SLOT(AddSelectedToPlaylistEnqueue()));
|
|
||||||
|
|
||||||
context_menu_->addSeparator();
|
|
||||||
|
|
||||||
if (ui_->results->selectionModel() &&
|
|
||||||
ui_->results->selectionModel()->selectedRows().length() == 1) {
|
|
||||||
context_actions_ << context_menu_->addAction(
|
context_actions_ << context_menu_->addAction(
|
||||||
IconLoader::Load("system-search", IconLoader::Base),
|
IconLoader::Load("media-playback-start", IconLoader::Base),
|
||||||
tr("Search for this"), this, SLOT(SearchForThis()));
|
tr("Append to current playlist"), this, SLOT(AddSelectedToPlaylist()));
|
||||||
}
|
context_actions_ << context_menu_->addAction(
|
||||||
|
IconLoader::Load("media-playback-start", IconLoader::Base),
|
||||||
|
tr("Replace current playlist"), this, SLOT(LoadSelected()));
|
||||||
|
context_actions_ << context_menu_->addAction(
|
||||||
|
IconLoader::Load("document-new", IconLoader::Base),
|
||||||
|
tr("Open in new playlist"), this, SLOT(OpenSelectedInNewPlaylist()));
|
||||||
|
|
||||||
context_menu_->addSeparator();
|
context_menu_->addSeparator();
|
||||||
context_menu_->addMenu(tr("Group by"))
|
context_actions_ << context_menu_->addAction(
|
||||||
->addActions(group_by_actions_->actions());
|
IconLoader::Load("go-next", IconLoader::Base), tr("Queue track"), this,
|
||||||
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
|
SLOT(AddSelectedToPlaylistEnqueue()));
|
||||||
tr("Configure global search..."), this,
|
|
||||||
SLOT(OpenSettingsDialog()));
|
context_menu_->addSeparator();
|
||||||
|
|
||||||
|
if (ui_->results->selectionModel() &&
|
||||||
|
ui_->results->selectionModel()->selectedRows().length() == 1) {
|
||||||
|
context_actions_ << context_menu_->addAction(
|
||||||
|
IconLoader::Load("system-search", IconLoader::Base),
|
||||||
|
tr("Search for this"), this, SLOT(SearchForThis()));
|
||||||
|
}
|
||||||
|
|
||||||
|
context_menu_->addSeparator();
|
||||||
|
context_menu_->addMenu(tr("Group by"))
|
||||||
|
->addActions(group_by_actions_->actions());
|
||||||
|
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
|
||||||
|
tr("Configure global search..."), this,
|
||||||
|
SLOT(OpenSettingsDialog()));
|
||||||
|
}
|
||||||
|
|
||||||
const bool enable_context_actions =
|
const bool enable_context_actions =
|
||||||
ui_->results->selectionModel() &&
|
ui_->results->selectionModel() &&
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -34,7 +34,7 @@ bool CloudFileSearchProvider::IsLoggedIn() {
|
||||||
return service_->has_credentials();
|
return service_->has_credentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CloudFileSearchProvider::ShowConfig() { service_->ShowSettingsDialog(); }
|
void CloudFileSearchProvider::ShowConfig() { service_->ShowConfig(); }
|
||||||
|
|
||||||
InternetService* CloudFileSearchProvider::internet_service() {
|
InternetService* CloudFileSearchProvider::internet_service() {
|
||||||
return service_;
|
return service_;
|
||||||
|
|
|
@ -83,8 +83,8 @@ QStandardItem* CloudFileService::CreateRootItem() {
|
||||||
void CloudFileService::LazyPopulate(QStandardItem* item) {
|
void CloudFileService::LazyPopulate(QStandardItem* item) {
|
||||||
switch (item->data(InternetModel::Role_Type).toInt()) {
|
switch (item->data(InternetModel::Role_Type).toInt()) {
|
||||||
case InternetModel::Type_Service:
|
case InternetModel::Type_Service:
|
||||||
if (!has_credentials()) {
|
if (ConfigRequired()) {
|
||||||
ShowSettingsDialog();
|
ShowConfig();
|
||||||
} else {
|
} else {
|
||||||
Connect();
|
Connect();
|
||||||
}
|
}
|
||||||
|
@ -103,8 +103,7 @@ void CloudFileService::PopulateContextMenu() {
|
||||||
tr("Cover Manager"), this, SLOT(ShowCoverManager()));
|
tr("Cover Manager"), this, SLOT(ShowCoverManager()));
|
||||||
context_menu_->addSeparator();
|
context_menu_->addSeparator();
|
||||||
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
|
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
|
||||||
tr("Configure..."), this,
|
tr("Configure..."), this, SLOT(ShowConfig()));
|
||||||
SLOT(ShowSettingsDialog()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CloudFileService::ShowCoverManager() {
|
void CloudFileService::ShowCoverManager() {
|
||||||
|
@ -122,7 +121,7 @@ void CloudFileService::AddToPlaylist(QMimeData* mime) {
|
||||||
QModelIndex());
|
QModelIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CloudFileService::ShowSettingsDialog() {
|
void CloudFileService::ShowConfig() {
|
||||||
app_->OpenSettingsDialogAtPage(settings_page_);
|
app_->OpenSettingsDialogAtPage(settings_page_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,11 +49,12 @@ class CloudFileService : public InternetService {
|
||||||
virtual bool has_credentials() const = 0;
|
virtual bool has_credentials() const = 0;
|
||||||
bool is_indexing() const { return indexing_task_id_ != -1; }
|
bool is_indexing() const { return indexing_task_id_ != -1; }
|
||||||
|
|
||||||
|
bool ConfigRequired() override { return !has_credentials(); }
|
||||||
signals:
|
signals:
|
||||||
void AllIndexingTasksFinished();
|
void AllIndexingTasksFinished();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void ShowSettingsDialog();
|
void ShowConfig() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void Connect() = 0;
|
virtual void Connect() = 0;
|
||||||
|
|
|
@ -60,9 +60,6 @@
|
||||||
#ifdef HAVE_SEAFILE
|
#ifdef HAVE_SEAFILE
|
||||||
#include "internet/seafile/seafileservice.h"
|
#include "internet/seafile/seafileservice.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAVE_SPOTIFY
|
|
||||||
#include "internet/spotify/spotifyservice.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using smart_playlists::Generator;
|
using smart_playlists::Generator;
|
||||||
using smart_playlists::GeneratorMimeData;
|
using smart_playlists::GeneratorMimeData;
|
||||||
|
@ -98,9 +95,6 @@ InternetModel::InternetModel(Application* app, QObject* parent)
|
||||||
AddService(new SomaFMService(app, this));
|
AddService(new SomaFMService(app, this));
|
||||||
AddService(new IntergalacticFMService(app, this));
|
AddService(new IntergalacticFMService(app, this));
|
||||||
AddService(new RadioBrowserService(app, this));
|
AddService(new RadioBrowserService(app, this));
|
||||||
#ifdef HAVE_SPOTIFY
|
|
||||||
AddService(new SpotifyService(app, this));
|
|
||||||
#endif
|
|
||||||
AddService(new SubsonicService(app, this));
|
AddService(new SubsonicService(app, this));
|
||||||
#ifdef HAVE_BOX
|
#ifdef HAVE_BOX
|
||||||
AddService(new BoxService(app, this));
|
AddService(new BoxService(app, this));
|
||||||
|
|
|
@ -88,6 +88,8 @@ class InternetService : public QObject {
|
||||||
|
|
||||||
virtual QString Icon() { return QString(); }
|
virtual QString Icon() { return QString(); }
|
||||||
|
|
||||||
|
virtual bool ConfigRequired() { return false; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void StreamError(const QString& message);
|
void StreamError(const QString& message);
|
||||||
void StreamMetadataFound(const QUrl& original_url, const Song& song);
|
void StreamMetadataFound(const QUrl& original_url, const Song& song);
|
||||||
|
|
|
@ -126,8 +126,8 @@ void DigitallyImportedServiceBase::RefreshStreamsFinished(QNetworkReply* reply,
|
||||||
void DigitallyImportedServiceBase::PopulateStreams() {
|
void DigitallyImportedServiceBase::PopulateStreams() {
|
||||||
if (root_->hasChildren()) root_->removeRows(0, root_->rowCount());
|
if (root_->hasChildren()) root_->removeRows(0, root_->rowCount());
|
||||||
|
|
||||||
if (!is_premium_account()) {
|
if (ConfigRequired()) {
|
||||||
ShowSettingsDialog();
|
ShowConfig();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,8 +180,7 @@ void DigitallyImportedServiceBase::ShowContextMenu(const QPoint& global_pos) {
|
||||||
SLOT(ForceRefreshStreams()));
|
SLOT(ForceRefreshStreams()));
|
||||||
context_menu_->addSeparator();
|
context_menu_->addSeparator();
|
||||||
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
|
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
|
||||||
tr("Configure..."), this,
|
tr("Configure..."), this, SLOT(ShowConfig()));
|
||||||
SLOT(ShowSettingsDialog()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context_menu_->popup(global_pos);
|
context_menu_->popup(global_pos);
|
||||||
|
@ -204,7 +203,7 @@ void DigitallyImportedServiceBase::LoadPlaylistFinished(QNetworkReply* reply) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DigitallyImportedServiceBase::ShowSettingsDialog() {
|
void DigitallyImportedServiceBase::ShowConfig() {
|
||||||
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_DigitallyImported);
|
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_DigitallyImported);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,7 +259,7 @@ RockRadioService::RockRadioService(Application* app, InternetModel* model,
|
||||||
: DigitallyImportedServiceBase(
|
: DigitallyImportedServiceBase(
|
||||||
"RockRadio", "ROCKRADIO.com", QUrl("http://www.rockradio.com"),
|
"RockRadio", "ROCKRADIO.com", QUrl("http://www.rockradio.com"),
|
||||||
IconLoader::Load("rockradio", IconLoader::Provider), "rockradio", app,
|
IconLoader::Load("rockradio", IconLoader::Provider), "rockradio", app,
|
||||||
model, false, parent) {}
|
model, true, parent) {}
|
||||||
|
|
||||||
ClassicalRadioService::ClassicalRadioService(Application* app,
|
ClassicalRadioService::ClassicalRadioService(Application* app,
|
||||||
InternetModel* model,
|
InternetModel* model,
|
||||||
|
@ -269,4 +268,4 @@ ClassicalRadioService::ClassicalRadioService(Application* app,
|
||||||
"ClassicalRadio", "ClassicalRadio.com",
|
"ClassicalRadio", "ClassicalRadio.com",
|
||||||
QUrl("http://www.classicalradio.com"),
|
QUrl("http://www.classicalradio.com"),
|
||||||
IconLoader::Load("digitallyimported", IconLoader::Provider),
|
IconLoader::Load("digitallyimported", IconLoader::Provider),
|
||||||
"classicalradio", app, model, false, parent) {}
|
"classicalradio", app, model, true, parent) {}
|
||||||
|
|
|
@ -41,16 +41,16 @@ class DigitallyImportedServiceBase : public InternetService {
|
||||||
const QString& api_service_name,
|
const QString& api_service_name,
|
||||||
Application* app, InternetModel* model,
|
Application* app, InternetModel* model,
|
||||||
bool has_premium, QObject* parent = nullptr);
|
bool has_premium, QObject* parent = nullptr);
|
||||||
~DigitallyImportedServiceBase();
|
~DigitallyImportedServiceBase() override;
|
||||||
|
|
||||||
static const char* kSettingsGroup;
|
static const char* kSettingsGroup;
|
||||||
static const int kStreamsCacheDurationSecs;
|
static const int kStreamsCacheDurationSecs;
|
||||||
|
|
||||||
QStandardItem* CreateRootItem();
|
QStandardItem* CreateRootItem() override;
|
||||||
void LazyPopulate(QStandardItem* parent);
|
void LazyPopulate(QStandardItem* parent) override;
|
||||||
void ShowContextMenu(const QPoint& global_pos);
|
void ShowContextMenu(const QPoint& global_pos) override;
|
||||||
|
|
||||||
void ReloadSettings();
|
void ReloadSettings() override;
|
||||||
|
|
||||||
bool is_premium_account() const;
|
bool is_premium_account() const;
|
||||||
|
|
||||||
|
@ -64,8 +64,10 @@ class DigitallyImportedServiceBase : public InternetService {
|
||||||
void SongFromChannel(const DigitallyImportedClient::Channel& channel,
|
void SongFromChannel(const DigitallyImportedClient::Channel& channel,
|
||||||
Song* song) const;
|
Song* song) const;
|
||||||
|
|
||||||
|
bool ConfigRequired() override { return !is_premium_account(); }
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void ShowSettingsDialog();
|
void ShowConfig() override;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void StreamsChanged();
|
void StreamsChanged();
|
||||||
|
|
|
@ -57,7 +57,7 @@ UrlHandler::LoadResult DigitallyImportedUrlHandler::StartLoading(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!service_->is_premium_account()) {
|
if (!service_->is_premium_account()) {
|
||||||
service_->StreamError(tr("A premium account is required"));
|
emit service_->StreamError(tr("A premium account is required"));
|
||||||
return LoadResult(url, LoadResult::Error);
|
return LoadResult(url, LoadResult::Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ void DigitallyImportedUrlHandler::LoadPlaylistFinished(QIODevice* device) {
|
||||||
|
|
||||||
// Failed to get playlist?
|
// Failed to get playlist?
|
||||||
if (songs.count() == 0) {
|
if (songs.count() == 0) {
|
||||||
service_->StreamError(tr("Error loading di.fm playlist"));
|
emit service_->StreamError(tr("Error loading di.fm playlist"));
|
||||||
emit AsyncLoadComplete(LoadResult(last_original_url_, LoadResult::Error));
|
emit AsyncLoadComplete(LoadResult(last_original_url_, LoadResult::Error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,7 @@ void DropboxService::Connect() {
|
||||||
if (has_credentials()) {
|
if (has_credentials()) {
|
||||||
RequestFileList();
|
RequestFileList();
|
||||||
} else {
|
} else {
|
||||||
ShowSettingsDialog();
|
ShowConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -226,8 +226,7 @@ void GoogleDriveService::PopulateContextMenu() {
|
||||||
context_menu_->addAction(IconLoader::Load("download", IconLoader::Base),
|
context_menu_->addAction(IconLoader::Load("download", IconLoader::Base),
|
||||||
tr("Cover Manager"), this, SLOT(ShowCoverManager()));
|
tr("Cover Manager"), this, SLOT(ShowCoverManager()));
|
||||||
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
|
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
|
||||||
tr("Configure..."), this,
|
tr("Configure..."), this, SLOT(ShowConfig()));
|
||||||
SLOT(ShowSettingsDialog()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GoogleDriveService::UpdateContextMenu() {
|
void GoogleDriveService::UpdateContextMenu() {
|
||||||
|
|
|
@ -40,7 +40,7 @@ class IcecastService : public InternetService {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
IcecastService(Application* app, InternetModel* parent);
|
IcecastService(Application* app, InternetModel* parent);
|
||||||
~IcecastService();
|
~IcecastService() override;
|
||||||
|
|
||||||
static const char* kServiceName;
|
static const char* kServiceName;
|
||||||
static const char* kDirectoryUrl;
|
static const char* kDirectoryUrl;
|
||||||
|
@ -51,12 +51,12 @@ class IcecastService : public InternetService {
|
||||||
Type_Genre,
|
Type_Genre,
|
||||||
};
|
};
|
||||||
|
|
||||||
QStandardItem* CreateRootItem();
|
QStandardItem* CreateRootItem() override;
|
||||||
void LazyPopulate(QStandardItem* item);
|
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:
|
private slots:
|
||||||
void LoadDirectory();
|
void LoadDirectory();
|
||||||
|
|
|
@ -39,7 +39,7 @@ class IntergalacticFMServiceBase : public InternetService {
|
||||||
const QString& name, const QUrl& channel_list_url,
|
const QString& name, const QUrl& channel_list_url,
|
||||||
const QUrl& homepage_url,
|
const QUrl& homepage_url,
|
||||||
const QUrl& donate_page_url, const QIcon& icon);
|
const QUrl& donate_page_url, const QIcon& icon);
|
||||||
~IntergalacticFMServiceBase();
|
~IntergalacticFMServiceBase() override;
|
||||||
|
|
||||||
enum ItemType {
|
enum ItemType {
|
||||||
Type_Stream = 2000,
|
Type_Stream = 2000,
|
||||||
|
@ -59,14 +59,14 @@ class IntergalacticFMServiceBase : public InternetService {
|
||||||
const QString& url_scheme() const { return url_scheme_; }
|
const QString& url_scheme() const { return url_scheme_; }
|
||||||
const QIcon& icon() const { return icon_; }
|
const QIcon& icon() const { return icon_; }
|
||||||
|
|
||||||
QStandardItem* CreateRootItem();
|
QStandardItem* CreateRootItem() override;
|
||||||
void LazyPopulate(QStandardItem* item);
|
void LazyPopulate(QStandardItem* item) override;
|
||||||
void ShowContextMenu(const QPoint& global_pos);
|
void ShowContextMenu(const QPoint& global_pos) override;
|
||||||
|
|
||||||
PlaylistItem::Options playlistitem_options() const;
|
PlaylistItem::Options playlistitem_options() const override;
|
||||||
QNetworkAccessManager* network() const { return network_; }
|
QNetworkAccessManager* network() const { return network_; }
|
||||||
|
|
||||||
void ReloadSettings();
|
void ReloadSettings() override;
|
||||||
|
|
||||||
bool IsStreamListStale() const { return streams_.IsStale(); }
|
bool IsStreamListStale() const { return streams_.IsStale(); }
|
||||||
StreamList Streams();
|
StreamList Streams();
|
||||||
|
|
|
@ -34,7 +34,7 @@ class SavedRadio : public InternetService {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SavedRadio(Application* app, InternetModel* parent);
|
SavedRadio(Application* app, InternetModel* parent);
|
||||||
~SavedRadio();
|
~SavedRadio() override;
|
||||||
|
|
||||||
enum ItemType {
|
enum ItemType {
|
||||||
Type_Stream = 2000,
|
Type_Stream = 2000,
|
||||||
|
@ -57,10 +57,10 @@ class SavedRadio : public InternetService {
|
||||||
static const char* kServiceName;
|
static const char* kServiceName;
|
||||||
static const char* kSettingsGroup;
|
static const char* kSettingsGroup;
|
||||||
|
|
||||||
QStandardItem* CreateRootItem();
|
QStandardItem* CreateRootItem() override;
|
||||||
void LazyPopulate(QStandardItem* item);
|
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(),
|
void Add(const QUrl& url, const QString& name = QString(),
|
||||||
const QUrl& url_logo = QUrl());
|
const QUrl& url_logo = QUrl());
|
||||||
|
|
|
@ -43,14 +43,14 @@ class JamendoService : public InternetService {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
JamendoService(Application* app, InternetModel* parent);
|
JamendoService(Application* app, InternetModel* parent);
|
||||||
~JamendoService();
|
~JamendoService() override;
|
||||||
|
|
||||||
QStandardItem* CreateRootItem();
|
QStandardItem* CreateRootItem() override;
|
||||||
void LazyPopulate(QStandardItem* item);
|
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(); }
|
LibraryBackend* library_backend() const { return library_backend_.get(); }
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ class MagnatuneService : public InternetService {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
MagnatuneService(Application* app, InternetModel* parent);
|
MagnatuneService(Application* app, InternetModel* parent);
|
||||||
~MagnatuneService();
|
~MagnatuneService() override;
|
||||||
|
|
||||||
// Values are saved in QSettings and are indices into the combo box in
|
// Values are saved in QSettings and are indices into the combo box in
|
||||||
// MagnatuneConfig
|
// MagnatuneConfig
|
||||||
|
@ -71,14 +71,14 @@ class MagnatuneService : public InternetService {
|
||||||
|
|
||||||
static QString ReadElementText(QXmlStreamReader& reader);
|
static QString ReadElementText(QXmlStreamReader& reader);
|
||||||
|
|
||||||
QStandardItem* CreateRootItem();
|
QStandardItem* CreateRootItem() override;
|
||||||
void LazyPopulate(QStandardItem* item);
|
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
|
// Magnatune specific stuff
|
||||||
MembershipType membership_type() const { return membership_; }
|
MembershipType membership_type() const { return membership_; }
|
||||||
|
@ -99,7 +99,7 @@ class MagnatuneService : public InternetService {
|
||||||
|
|
||||||
void Download();
|
void Download();
|
||||||
void Homepage();
|
void Homepage();
|
||||||
void ShowConfig();
|
void ShowConfig() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void EnsureMenuCreated();
|
void EnsureMenuCreated();
|
||||||
|
|
|
@ -395,6 +395,20 @@ QStandardItem* PodcastService::CreatePodcastItem(const Podcast& podcast) {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PodcastService::RemovePodcastItem(QStandardItem* item) {
|
||||||
|
// Remove any episode ID -> item mappings for the episodes in this podcast.
|
||||||
|
for (int i = 0; i < item->rowCount(); ++i) {
|
||||||
|
QStandardItem* episode_item = item->child(i);
|
||||||
|
const int episode_id =
|
||||||
|
episode_item->data(Role_Episode).value<PodcastEpisode>().database_id();
|
||||||
|
|
||||||
|
episodes_by_database_id_.remove(episode_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove this podcast's row
|
||||||
|
model_->removeRow(item->row());
|
||||||
|
}
|
||||||
|
|
||||||
QStandardItem* PodcastService::CreatePodcastEpisodeItem(
|
QStandardItem* PodcastService::CreatePodcastEpisodeItem(
|
||||||
const PodcastEpisode& episode) {
|
const PodcastEpisode& episode) {
|
||||||
QStandardItem* item = new QStandardItem;
|
QStandardItem* item = new QStandardItem;
|
||||||
|
@ -621,18 +635,7 @@ void PodcastService::SubscriptionAdded(const Podcast& podcast) {
|
||||||
void PodcastService::SubscriptionRemoved(const Podcast& podcast) {
|
void PodcastService::SubscriptionRemoved(const Podcast& podcast) {
|
||||||
QStandardItem* item = podcasts_by_database_id_.take(podcast.database_id());
|
QStandardItem* item = podcasts_by_database_id_.take(podcast.database_id());
|
||||||
if (item) {
|
if (item) {
|
||||||
// Remove any episode ID -> item mappings for the episodes in this podcast.
|
RemovePodcastItem(item);
|
||||||
for (int i = 0; i < item->rowCount(); ++i) {
|
|
||||||
QStandardItem* episode_item = item->child(i);
|
|
||||||
const int episode_id = episode_item->data(Role_Episode)
|
|
||||||
.value<PodcastEpisode>()
|
|
||||||
.database_id();
|
|
||||||
|
|
||||||
episodes_by_database_id_.remove(episode_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove this episode's row
|
|
||||||
model_->removeRow(item->row());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -859,6 +862,6 @@ void PodcastService::ReloadPodcast(const Podcast& podcast) {
|
||||||
}
|
}
|
||||||
QStandardItem* item = podcasts_by_database_id_[podcast.database_id()];
|
QStandardItem* item = podcasts_by_database_id_[podcast.database_id()];
|
||||||
|
|
||||||
model_->invisibleRootItem()->removeRow(item->row());
|
RemovePodcastItem(item);
|
||||||
model_->invisibleRootItem()->appendRow(CreatePodcastItem(podcast));
|
model_->invisibleRootItem()->appendRow(CreatePodcastItem(podcast));
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ class PodcastService : public InternetService {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PodcastService(Application* app, InternetModel* parent);
|
PodcastService(Application* app, InternetModel* parent);
|
||||||
~PodcastService();
|
~PodcastService() override;
|
||||||
|
|
||||||
static const char* kServiceName;
|
static const char* kServiceName;
|
||||||
static const char* kSettingsGroup;
|
static const char* kSettingsGroup;
|
||||||
|
@ -57,12 +57,12 @@ class PodcastService : public InternetService {
|
||||||
|
|
||||||
enum Role { Role_Podcast = InternetModel::RoleCount, Role_Episode };
|
enum Role { Role_Podcast = InternetModel::RoleCount, Role_Episode };
|
||||||
|
|
||||||
QStandardItem* CreateRootItem();
|
QStandardItem* CreateRootItem() override;
|
||||||
void LazyPopulate(QStandardItem* parent);
|
void LazyPopulate(QStandardItem* parent) override;
|
||||||
bool has_initial_load_settings() const { return true; }
|
bool has_initial_load_settings() const override { return true; }
|
||||||
void ShowContextMenu(const QPoint& global_pos);
|
void ShowContextMenu(const QPoint& global_pos) override;
|
||||||
void ReloadSettings();
|
void ReloadSettings() override;
|
||||||
void InitialLoadSettings();
|
void InitialLoadSettings() override;
|
||||||
// Called by SongLoader when the user adds a Podcast URL directly. Adds a
|
// 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
|
// 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.
|
// contains an OPML file then this displays it in the Add Podcast dialog.
|
||||||
|
@ -81,7 +81,7 @@ class PodcastService : public InternetService {
|
||||||
void DeleteDownloadedData();
|
void DeleteDownloadedData();
|
||||||
void SetNew();
|
void SetNew();
|
||||||
void SetListened();
|
void SetListened();
|
||||||
void ShowConfig();
|
void ShowConfig() override;
|
||||||
|
|
||||||
void SubscriptionAdded(const Podcast& podcast);
|
void SubscriptionAdded(const Podcast& podcast);
|
||||||
void SubscriptionRemoved(const Podcast& podcast);
|
void SubscriptionRemoved(const Podcast& podcast);
|
||||||
|
@ -119,6 +119,7 @@ class PodcastService : public InternetService {
|
||||||
|
|
||||||
QStandardItem* CreatePodcastItem(const Podcast& podcast);
|
QStandardItem* CreatePodcastItem(const Podcast& podcast);
|
||||||
QStandardItem* CreatePodcastEpisodeItem(const PodcastEpisode& episode);
|
QStandardItem* CreatePodcastEpisodeItem(const PodcastEpisode& episode);
|
||||||
|
void RemovePodcastItem(QStandardItem* item);
|
||||||
|
|
||||||
QModelIndex MapToMergedModel(const QModelIndex& index) const;
|
QModelIndex MapToMergedModel(const QModelIndex& index) const;
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ class RadioBrowserService : public InternetService {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
RadioBrowserService(Application* app, InternetModel* parent);
|
RadioBrowserService(Application* app, InternetModel* parent);
|
||||||
~RadioBrowserService(){};
|
~RadioBrowserService() override{};
|
||||||
|
|
||||||
enum ItemType {
|
enum ItemType {
|
||||||
Type_Stream = 2000,
|
Type_Stream = 2000,
|
||||||
|
|
|
@ -218,7 +218,7 @@ void SeafileService::Connect() {
|
||||||
if (has_credentials()) {
|
if (has_credentials()) {
|
||||||
UpdateLibraries();
|
UpdateLibraries();
|
||||||
} else {
|
} else {
|
||||||
ShowSettingsDialog();
|
ShowConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -219,8 +219,7 @@ void SkydriveService::PopulateContextMenu() {
|
||||||
tr("Cover Manager"), this, SLOT(ShowCoverManager()));
|
tr("Cover Manager"), this, SLOT(ShowCoverManager()));
|
||||||
context_menu_->addSeparator();
|
context_menu_->addSeparator();
|
||||||
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
|
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
|
||||||
tr("Configure..."), this,
|
tr("Configure..."), this, SLOT(ShowConfig()));
|
||||||
SLOT(ShowSettingsDialog()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SkydriveService::UpdateContextMenu() {
|
void SkydriveService::UpdateContextMenu() {
|
||||||
|
|
|
@ -39,7 +39,7 @@ class SomaFMServiceBase : public InternetService {
|
||||||
const QString& name, const QUrl& channel_list_url,
|
const QString& name, const QUrl& channel_list_url,
|
||||||
const QUrl& homepage_url, const QUrl& donate_page_url,
|
const QUrl& homepage_url, const QUrl& donate_page_url,
|
||||||
const QIcon& icon);
|
const QIcon& icon);
|
||||||
~SomaFMServiceBase();
|
~SomaFMServiceBase() override;
|
||||||
|
|
||||||
enum ItemType {
|
enum ItemType {
|
||||||
Type_Stream = 2000,
|
Type_Stream = 2000,
|
||||||
|
@ -59,14 +59,14 @@ class SomaFMServiceBase : public InternetService {
|
||||||
const QString& url_scheme() const { return url_scheme_; }
|
const QString& url_scheme() const { return url_scheme_; }
|
||||||
const QIcon& icon() const { return icon_; }
|
const QIcon& icon() const { return icon_; }
|
||||||
|
|
||||||
QStandardItem* CreateRootItem();
|
QStandardItem* CreateRootItem() override;
|
||||||
void LazyPopulate(QStandardItem* item);
|
void LazyPopulate(QStandardItem* item) override;
|
||||||
void ShowContextMenu(const QPoint& global_pos);
|
void ShowContextMenu(const QPoint& global_pos) override;
|
||||||
|
|
||||||
PlaylistItem::Options playlistitem_options() const;
|
PlaylistItem::Options playlistitem_options() const override;
|
||||||
QNetworkAccessManager* network() const { return network_; }
|
QNetworkAccessManager* network() const { return network_; }
|
||||||
|
|
||||||
void ReloadSettings();
|
void ReloadSettings() override;
|
||||||
|
|
||||||
bool IsStreamListStale() const { return streams_.IsStale(); }
|
bool IsStreamListStale() const { return streams_.IsStale(); }
|
||||||
StreamList Streams();
|
StreamList Streams();
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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_
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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_
|
|
|
@ -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_);
|
|
||||||
}
|
|
|
@ -1,193 +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; }
|
|
||||||
|
|
||||||
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_
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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_
|
|
|
@ -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>
|
|
|
@ -168,7 +168,7 @@ void SubsonicService::LazyPopulate(QStandardItem* item) {
|
||||||
switch (item->data(InternetModel::Role_Type).toInt()) {
|
switch (item->data(InternetModel::Role_Type).toInt()) {
|
||||||
case InternetModel::Type_Service:
|
case InternetModel::Type_Service:
|
||||||
library_model_->Init();
|
library_model_->Init();
|
||||||
if (login_state() != LoginState_Loggedin) {
|
if (ConfigRequired()) {
|
||||||
ShowConfig();
|
ShowConfig();
|
||||||
} else if (total_song_count_ == 0 && !load_database_task_id_) {
|
} else if (total_song_count_ == 0 && !load_database_task_id_) {
|
||||||
ReloadDatabase();
|
ReloadDatabase();
|
||||||
|
@ -530,6 +530,11 @@ void SubsonicLibraryScanner::OnGetAlbumFinished(QNetworkReply* reply) {
|
||||||
|
|
||||||
// Read song information
|
// Read song information
|
||||||
while (reader.readNextStartElement()) {
|
while (reader.readNextStartElement()) {
|
||||||
|
// skip multi-artist and multi-genre tags
|
||||||
|
if ((reader.name() == "artists") || (reader.name() == "genres")) {
|
||||||
|
reader.skipCurrentElement();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (reader.name() != "song") {
|
if (reader.name() != "song") {
|
||||||
ParsingError("song tag expected. Aborting scan.");
|
ParsingError("song tag expected. Aborting scan.");
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -39,12 +39,10 @@ class SubsonicLibraryScanner;
|
||||||
|
|
||||||
class SubsonicService : public InternetService {
|
class SubsonicService : public InternetService {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_ENUMS(LoginState)
|
|
||||||
Q_ENUMS(ApiError)
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SubsonicService(Application* app, InternetModel* parent);
|
SubsonicService(Application* app, InternetModel* parent);
|
||||||
~SubsonicService();
|
~SubsonicService() override;
|
||||||
|
|
||||||
enum LoginState {
|
enum LoginState {
|
||||||
LoginState_Loggedin,
|
LoginState_Loggedin,
|
||||||
|
@ -63,6 +61,7 @@ class SubsonicService : public InternetService {
|
||||||
LoginState_RedirectLimitExceeded,
|
LoginState_RedirectLimitExceeded,
|
||||||
LoginState_RedirectNoUrl,
|
LoginState_RedirectNoUrl,
|
||||||
};
|
};
|
||||||
|
Q_ENUM(LoginState)
|
||||||
|
|
||||||
enum ApiError {
|
enum ApiError {
|
||||||
ApiError_Generic = 0,
|
ApiError_Generic = 0,
|
||||||
|
@ -74,6 +73,7 @@ class SubsonicService : public InternetService {
|
||||||
ApiError_Unlicensed = 60,
|
ApiError_Unlicensed = 60,
|
||||||
ApiError_NotFound = 70,
|
ApiError_NotFound = 70,
|
||||||
};
|
};
|
||||||
|
Q_ENUM(ApiError)
|
||||||
|
|
||||||
enum Type {
|
enum Type {
|
||||||
Type_Artist = InternetModel::TypeCount,
|
Type_Artist = InternetModel::TypeCount,
|
||||||
|
@ -90,11 +90,11 @@ class SubsonicService : public InternetService {
|
||||||
bool IsConfigured() const;
|
bool IsConfigured() const;
|
||||||
bool IsAmpache() const;
|
bool IsAmpache() const;
|
||||||
|
|
||||||
QStandardItem* CreateRootItem();
|
QStandardItem* CreateRootItem() override;
|
||||||
void LazyPopulate(QStandardItem* item);
|
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;
|
||||||
|
|
||||||
void Login();
|
void Login();
|
||||||
void Login(const QString& server, const QString& username,
|
void Login(const QString& server, const QString& username,
|
||||||
|
@ -103,6 +103,10 @@ class SubsonicService : public InternetService {
|
||||||
|
|
||||||
LoginState login_state() const { return login_state_; }
|
LoginState login_state() const { return login_state_; }
|
||||||
|
|
||||||
|
bool ConfigRequired() override {
|
||||||
|
return login_state() != LoginState_Loggedin;
|
||||||
|
}
|
||||||
|
|
||||||
// Subsonic API methods
|
// Subsonic API methods
|
||||||
void Ping();
|
void Ping();
|
||||||
|
|
||||||
|
@ -171,7 +175,7 @@ class SubsonicService : public InternetService {
|
||||||
void OnLoginStateChanged(SubsonicService::LoginState newstate);
|
void OnLoginStateChanged(SubsonicService::LoginState newstate);
|
||||||
void OnPingFinished(QNetworkReply* reply);
|
void OnPingFinished(QNetworkReply* reply);
|
||||||
|
|
||||||
void ShowConfig();
|
void ShowConfig() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SubsonicLibraryScanner : public QObject {
|
class SubsonicLibraryScanner : public QObject {
|
||||||
|
@ -180,7 +184,7 @@ class SubsonicLibraryScanner : public QObject {
|
||||||
public:
|
public:
|
||||||
explicit SubsonicLibraryScanner(SubsonicService* service,
|
explicit SubsonicLibraryScanner(SubsonicService* service,
|
||||||
QObject* parent = nullptr);
|
QObject* parent = nullptr);
|
||||||
~SubsonicLibraryScanner();
|
~SubsonicLibraryScanner() override;
|
||||||
|
|
||||||
void Scan();
|
void Scan();
|
||||||
const SongList& GetSongs() const { return songs_; }
|
const SongList& GetSongs() const { return songs_; }
|
||||||
|
|
|
@ -896,7 +896,7 @@ LibraryBackend::AlbumList LibraryBackend::GetAlbums(const QString& artist,
|
||||||
QString last_artist;
|
QString last_artist;
|
||||||
QString last_album_artist;
|
QString last_album_artist;
|
||||||
while (query.Next()) {
|
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;
|
Album info;
|
||||||
info.artist = compilation ? QString() : query.Value(1).toString();
|
info.artist = compilation ? QString() : query.Value(1).toString();
|
||||||
|
|
|
@ -46,7 +46,6 @@ class QSettings;
|
||||||
|
|
||||||
class LibraryModel : public SimpleTreeModel<LibraryItem> {
|
class LibraryModel : public SimpleTreeModel<LibraryItem> {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_ENUMS(GroupBy)
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
LibraryModel(std::shared_ptr<LibraryBackend> backend, Application* app,
|
LibraryModel(std::shared_ptr<LibraryBackend> backend, Application* app,
|
||||||
|
@ -91,6 +90,7 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
|
||||||
GroupBy_OriginalYearAlbum = 13,
|
GroupBy_OriginalYearAlbum = 13,
|
||||||
GroupBy_OriginalYear = 14,
|
GroupBy_OriginalYear = 14,
|
||||||
};
|
};
|
||||||
|
Q_ENUM(GroupBy)
|
||||||
|
|
||||||
struct Grouping {
|
struct Grouping {
|
||||||
Grouping(GroupBy f = GroupBy_None, GroupBy s = GroupBy_None,
|
Grouping(GroupBy f = GroupBy_None, GroupBy s = GroupBy_None,
|
||||||
|
|
|
@ -283,16 +283,16 @@ bool LibraryView::RestoreLevelFocus(const QModelIndex& parent) {
|
||||||
model()->data(current, LibraryModel::Role_Key).toString();
|
model()->data(current, LibraryModel::Role_Key).toString();
|
||||||
if (!last_selected_container_.isEmpty() &&
|
if (!last_selected_container_.isEmpty() &&
|
||||||
last_selected_container_ == text) {
|
last_selected_container_ == text) {
|
||||||
emit expand(current);
|
expand(current);
|
||||||
setCurrentIndex(current);
|
setCurrentIndex(current);
|
||||||
return true;
|
return true;
|
||||||
} else if (last_selected_path_.contains(text)) {
|
} else if (last_selected_path_.contains(text)) {
|
||||||
emit expand(current);
|
expand(current);
|
||||||
// If a selected container or song were not found, we've got into a
|
// If a selected container or song were not found, we've got into a
|
||||||
// wrong subtree
|
// wrong subtree
|
||||||
// (happens with "unknown" all the time)
|
// (happens with "unknown" all the time)
|
||||||
if (!RestoreLevelFocus(current)) {
|
if (!RestoreLevelFocus(current)) {
|
||||||
emit collapse(current);
|
collapse(current);
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -396,7 +396,7 @@ int main(int argc, char* argv[]) {
|
||||||
// Set the name of the app desktop file as per the freedesktop specifications
|
// Set the name of the app desktop file as per the freedesktop specifications
|
||||||
// This is needed on Wayland for the main window to show the correct icon
|
// This is needed on Wayland for the main window to show the correct icon
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
|
||||||
QGuiApplication::setDesktopFileName("clementine");
|
QGuiApplication::setDesktopFileName("org.clementine_player.Clementine");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Resources
|
// Resources
|
||||||
|
|
|
@ -92,12 +92,10 @@ void MoodbarBuilder::Normalize(QList<Rgb>* vals, double Rgb::*member) {
|
||||||
}
|
}
|
||||||
|
|
||||||
double avg = 0;
|
double avg = 0;
|
||||||
int t = 0;
|
|
||||||
for (const Rgb& rgb : *vals) {
|
for (const Rgb& rgb : *vals) {
|
||||||
const double value = rgb.*member;
|
const double value = rgb.*member;
|
||||||
if (value != mini && value != maxi) {
|
if (value != mini && value != maxi) {
|
||||||
avg += value / vals->count();
|
avg += value / vals->count();
|
||||||
t++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ QString Chromaprinter::CreateFingerprint() {
|
||||||
// Chromaprint expects mono 16-bit ints at a sample rate of 11025Hz.
|
// Chromaprint expects mono 16-bit ints at a sample rate of 11025Hz.
|
||||||
GstCaps* caps = gst_caps_new_simple(
|
GstCaps* caps = gst_caps_new_simple(
|
||||||
"audio/x-raw", "format", G_TYPE_STRING, "S16LE", "channels", G_TYPE_INT,
|
"audio/x-raw", "format", G_TYPE_STRING, "S16LE", "channels", G_TYPE_INT,
|
||||||
kDecodeChannels, "rate", G_TYPE_INT, kDecodeRate, NULL);
|
kDecodeChannels, "rate", G_TYPE_INT, kDecodeRate, nullptr);
|
||||||
gst_element_link_filtered(resample, sink, caps);
|
gst_element_link_filtered(resample, sink, caps);
|
||||||
gst_caps_unref(caps);
|
gst_caps_unref(caps);
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue