Compare commits
246 Commits
1.4.0rc1-6
...
master
Author | SHA1 | Date |
---|---|---|
Clementine Buildbot | 1506c27696 | |
Clementine Buildbot | d99cbb269b | |
Clementine Buildbot | 3cca9bb98a | |
Clementine Buildbot | 650bd81508 | |
Clementine Buildbot | 7607ddcb96 | |
Clementine Buildbot | e249911937 | |
Clementine Buildbot | 4ae57a4b5d | |
Clementine Buildbot | 2f3464403b | |
Clementine Buildbot | f76dbffa6b | |
Clementine Buildbot | fbb266adc2 | |
Clementine Buildbot | 9638ac70b3 | |
Clementine Buildbot | c93b4e1149 | |
Clementine Buildbot | d014a315c9 | |
Isaiah W | df4181940d | |
Isaiah W | ebe3c45476 | |
Clementine Buildbot | 634910238d | |
Clementine Buildbot | 62ed69fa3d | |
Clementine Buildbot | dd0a94e8a6 | |
Clementine Buildbot | 1566148c50 | |
Clementine Buildbot | 98a520552b | |
Clementine Buildbot | 5968648aa1 | |
Clementine Buildbot | f3ddd7eee4 | |
Clementine Buildbot | 19b44fb831 | |
Robert-André Mauchin | 994d16effa | |
xoza | 4768cb9efb | |
Clementine Buildbot | 7b678f26e0 | |
Marcus Müller | 3f572a4139 | |
Marcus Müller | f3837f95db | |
Marcus Müller | 6820a0a58d | |
Marcus Müller | cfcddf7c0f | |
Marcus Müller | 98e24f626b | |
Marcus Müller | 8e47ab59e5 | |
Marcus Müller | 63208b4e1f | |
Marcus Müller | 20773dee29 | |
John Maguire | c2a5b9b07e | |
Quentin Snow | de7455eebd | |
Marcus Müller | 2a14ec9d4d | |
Clementine Buildbot | 86e81cea05 | |
Clementine Buildbot | 10570316dd | |
Clementine Buildbot | ad8fd81ba9 | |
Clementine Buildbot | 6ff5768634 | |
Clementine Buildbot | 08bfb88912 | |
Clementine Buildbot | d3108b32e8 | |
Clementine Buildbot | 0701bef103 | |
Clementine Buildbot | bf4ac0cb46 | |
John Maguire | baf05335f9 | |
John Maguire | d21e9697d0 | |
John Maguire | ab057f8275 | |
John Maguire | 58325e45a7 | |
John Maguire | 1d0cbc0ebb | |
Clementine Buildbot | c83a0ac25f | |
John Maguire | 351a5e2547 | |
John Maguire | 8773e8fe0a | |
Clementine Buildbot | 3471134d52 | |
Clementine Buildbot | 26192c3469 | |
Clementine Buildbot | 982d8fbb63 | |
Clementine Buildbot | 20cf7f793b | |
Clementine Buildbot | ccf4f75c3d | |
Alexey Sokolov | 65319d4952 | |
John Maguire | 9ef681b0e9 | |
Clementine Buildbot | dfeb1182f9 | |
Clementine Buildbot | 384a8850d9 | |
adem | 0fab612784 | |
Clementine Buildbot | 336770bb95 | |
Andrei Stepanov | 101f450aaa | |
Clementine Buildbot | 6a440fe397 | |
John Maguire | 3a506e0917 | |
Clementine Buildbot | c716ddb722 | |
Clementine Buildbot | 519b33ed81 | |
Clementine Buildbot | f2011e7e26 | |
Clementine Buildbot | 42853b7b52 | |
Clementine Buildbot | 1c69e343b9 | |
Clementine Buildbot | 770080b80b | |
Clementine Buildbot | 3e9e251e90 | |
Jason Freidman | 72c2336d94 | |
Clementine Buildbot | 495803ab99 | |
Clementine Buildbot | 7da98fbbcc | |
Clementine Buildbot | 2055fd51fa | |
Clementine Buildbot | 39124eda38 | |
Clementine Buildbot | b567760ae1 | |
Clementine Buildbot | 2d3a604b85 | |
Clementine Buildbot | ce4a22bd5f | |
John Maguire | e6a7539480 | |
John Maguire | a551c40c4e | |
John Maguire | 99029ed643 | |
John Maguire | cf8047b4ce | |
Andrew Reading | f59c9f4b2b | |
John Maguire | 71eac9bb3b | |
John Maguire | 3fd467591a | |
John Maguire | a0ae9210dd | |
John Maguire | c1fa38120d | |
John Maguire | 13352c5802 | |
John Maguire | 5e5b888d41 | |
John Maguire | 662ac60eb1 | |
John Maguire | 9be5b9805d | |
John Maguire | 9de903d42d | |
John Maguire | 454678256e | |
John Maguire | d3c847b38c | |
John Maguire | 398893117e | |
Lorenz Bausch | bbda59a5f3 | |
John Maguire | bebd0b5d3c | |
Clementine Buildbot | 250024e117 | |
Clementine Buildbot | 9168299c0f | |
Clementine Buildbot | 24d4b6e7f2 | |
Clementine Buildbot | 644405ec7a | |
Clementine Buildbot | 2fb964fc29 | |
Clementine Buildbot | cf31624836 | |
Clementine Buildbot | d05616e37c | |
Clementine Buildbot | 0b5faa7550 | |
Clementine Buildbot | c0b42ace6d | |
Clementine Buildbot | 810f0b0acb | |
Clementine Buildbot | c2b8a35642 | |
Clementine Buildbot | 2b340da79f | |
Clementine Buildbot | 6698723991 | |
Clementine Buildbot | 7175ee4d37 | |
Clementine Buildbot | 20c6ae6c14 | |
Clementine Buildbot | 59d1c94b90 | |
Clementine Buildbot | 9d143334e2 | |
Clementine Buildbot | 4797edbc8a | |
Clementine Buildbot | 01f72b575d | |
Clementine Buildbot | dcbb3f8a58 | |
Clementine Buildbot | 3acf26015b | |
Clementine Buildbot | 333203c972 | |
Clementine Buildbot | 63b806dbb7 | |
Clementine Buildbot | a8d529ca14 | |
Clementine Buildbot | 111379dfd0 | |
Clementine Buildbot | 4821bd50c2 | |
Clementine Buildbot | c3a0bd69fd | |
Clementine Buildbot | 5487d0632c | |
Clementine Buildbot | 98b68afc28 | |
Clementine Buildbot | 15b819fea3 | |
Clementine Buildbot | e2f6ec8e12 | |
Clementine Buildbot | efa0530ed9 | |
Lukas Prediger | a504c1d391 | |
Lukas Prediger | 794c1b8c92 | |
Lukas Prediger | f35e1b543d | |
Clementine Buildbot | 497552aab2 | |
Clementine Buildbot | 9487f67f64 | |
Clementine Buildbot | 19a86ba2e4 | |
Lukas Prediger | 1aaf74788c | |
Clementine Buildbot | 7ce9928779 | |
Clementine Buildbot | af890f0736 | |
John Maguire | 09ccf93b98 | |
Clementine Buildbot | f237795850 | |
Lukasz Kryger | d79f837ddb | |
John Maguire | e69ceb25df | |
John Maguire | ab37de5e8f | |
Lukas Prediger | dd1393ea3a | |
Lukas Prediger | 6b6547095a | |
Clementine Buildbot | 24a766d0e5 | |
Lukas Prediger | cefe81d0c1 | |
Lukas Prediger | 0895297297 | |
Lukas Prediger | 3a40be6706 | |
Lukas Prediger | bb618efc5d | |
Clementine Buildbot | 245f64a882 | |
Clementine Buildbot | 0be314337d | |
Clementine Buildbot | 63eb7aa743 | |
Clementine Buildbot | 9dd008da2c | |
Clementine Buildbot | b1e750c52c | |
Clementine Buildbot | 41539d0c02 | |
Clementine Buildbot | 44dbc95554 | |
Clementine Buildbot | 1d8139e462 | |
Clementine Buildbot | 2d0518a5a8 | |
Clementine Buildbot | d5986a4820 | |
John Maguire | 590ab22f8d | |
Clementine Buildbot | b747423b5a | |
Clementine Buildbot | 15e45c9ec6 | |
John Maguire | d033b38c4b | |
Clementine Buildbot | 2469763b9b | |
John Maguire | e7b1c06341 | |
Clementine Buildbot | a25887be5c | |
John Maguire | 568ff1f9da | |
Clementine Buildbot | 174fc515ee | |
Lukas Prediger | 7b8b477d07 | |
Clementine Buildbot | b9dbcb78db | |
Clementine Buildbot | c29b1e10d2 | |
Clementine Buildbot | f8c167c9c6 | |
Clementine Buildbot | e5023535d2 | |
Alfred | 1b3b621957 | |
Clementine Buildbot | 2dc8df7e23 | |
Clementine Buildbot | 4eebf5747d | |
Clementine Buildbot | c24927a03b | |
Clementine Buildbot | 424dbd44e8 | |
Clementine Buildbot | 68bc9d9ebb | |
Clementine Buildbot | 294620fe66 | |
Clementine Buildbot | 21f038c156 | |
Lukas Prediger | 5705d4fd85 | |
Clementine Buildbot | 86b958015b | |
Clementine Buildbot | f8f849e49c | |
Clementine Buildbot | 69fd49b977 | |
Clementine Buildbot | 62922147e6 | |
Clementine Buildbot | 2e133f7ce4 | |
Clementine Buildbot | 0820035b84 | |
Clementine Buildbot | 679a0ee544 | |
Clementine Buildbot | 8715815452 | |
Clementine Buildbot | b762aeb1ba | |
Lukas Prediger | fd585e8aa4 | |
Lukas Prediger | 2936578fa4 | |
Lukas Prediger | 62b5a0e77b | |
Lukas Prediger | 50404a967b | |
Lukas Prediger | 6b03b8f5d1 | |
Lukas Prediger | 90ec6f6a24 | |
Clementine Buildbot | b020171da7 | |
Clementine Buildbot | c969bf9783 | |
Jim Broadus | 082f941bb9 | |
Tom Kranz | cd72cf3390 | |
Jim Broadus | ba29b0e3ba | |
Daniel Perelman | ab6a480131 | |
Daniel Perelman | f548884f57 | |
Clementine Buildbot | 1535e78aa0 | |
Clementine Buildbot | 2cca75d930 | |
Lukas Prediger | e556a59aea | |
Lukas Prediger | e187a68e9f | |
Clementine Buildbot | c58335c6c9 | |
Clementine Buildbot | 769d8bbe6d | |
Clementine Buildbot | 3b7d5880f9 | |
Clementine Buildbot | 7eb62b6266 | |
Ismael Luceno | 628ff65828 | |
Lukas Prediger | c8c110efaf | |
Lukas Prediger | a72e252ec6 | |
Lukas Prediger | b0704331d7 | |
Lukas Prediger | 5c8ca3754f | |
Lukas Prediger | 83d961f808 | |
Lukas Prediger | a6fef97cac | |
Clementine Buildbot | 922afe506f | |
Ismael Luceno | 8682d4de48 | |
Clementine Buildbot | 03e13c69e7 | |
Clementine Buildbot | 327d5fdac3 | |
Clementine Buildbot | b55e54388f | |
Clementine Buildbot | cddc08e148 | |
Clementine Buildbot | bf424ce986 | |
Clementine Buildbot | e2d6759d55 | |
Jim Broadus | 102317e5c8 | |
Clementine Buildbot | ac3a0d33f7 | |
Robin Lee | 224c475b50 | |
Clementine Buildbot | dbe15e5e9f | |
Clementine Buildbot | c0c9037677 | |
kentsangkm | 6982b47819 | |
Clementine Buildbot | 98dd3e48a6 | |
Clementine Buildbot | 1e39ce29a4 | |
Jonas Kvinge | daa2f25e3c | |
Jonas Kvinge | f379ad84d4 | |
Jonas Kvinge | 1d1d3b157f | |
Jonas Kvinge | 320a1b81c9 | |
Jonas Kvinge | 0c1b6a2a44 | |
Jonas Kvinge | 598e660aeb |
|
@ -10,7 +10,7 @@ on:
|
|||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
@ -25,10 +25,15 @@ jobs:
|
|||
push_translations:
|
||||
name: Push translation sources to Transifex
|
||||
if: github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: ubuntu:bionic
|
||||
steps:
|
||||
- uses: supplypike/setup-bin@v3
|
||||
with:
|
||||
uri: https://github.com/transifex/cli/releases/download/v1.6.7/tx-linux-amd64.tar.gz
|
||||
name: tx
|
||||
version: 1.6.7
|
||||
- name: Install dependencies
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
@ -58,20 +63,20 @@ jobs:
|
|||
libtag1-dev
|
||||
pkg-config
|
||||
protobuf-compiler
|
||||
python-pip
|
||||
python3-pip
|
||||
qtbase5-dev
|
||||
qttools5-dev-tools
|
||||
qttools5-dev
|
||||
libsparsehash-dev
|
||||
ssh
|
||||
- name: Install tx
|
||||
run: pip install transifex-client==0.13.9
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: tx init
|
||||
env:
|
||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||
run: tx init --no-interactive --force
|
||||
run: tx init
|
||||
- name: cmake
|
||||
working-directory: bin
|
||||
run: cmake ..
|
||||
|
@ -81,7 +86,7 @@ jobs:
|
|||
- name: tx config
|
||||
env:
|
||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||
run: tx config mapping --execute -r clementine.clementineplayer -f src/translations/translations.pot -s en -t PO --expression 'src/translations/<lang>.po'
|
||||
run: tx add --organization davidsansome --project clementine --resource clementineplayer --file-filter 'src/translations/<lang>.po' --type PO src/translations/en.po
|
||||
- name: tx push
|
||||
env:
|
||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||
|
@ -89,20 +94,20 @@ jobs:
|
|||
|
||||
create_release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.ref == 'refs/heads/master'
|
||||
needs:
|
||||
- build_bionic_64
|
||||
- build_bullseye_64
|
||||
- build_buster_64
|
||||
- build_fedora_33
|
||||
- build_fedora_34
|
||||
- build_fedora_36
|
||||
- build_fedora_37
|
||||
- build_fedora_38
|
||||
- build_focal_64
|
||||
- build_groovy_64
|
||||
- build_hirsute_64
|
||||
- build_mac
|
||||
- build_jammy_64
|
||||
# - build_mac
|
||||
- build_mingw
|
||||
- build_source
|
||||
- build_stretch_64
|
||||
steps:
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- uses: actions/download-artifact@v2
|
||||
|
@ -124,7 +129,7 @@ jobs:
|
|||
|
||||
build_source:
|
||||
name: Build source tarball
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: ubuntu:focal
|
||||
steps:
|
||||
|
@ -169,6 +174,8 @@ jobs:
|
|||
qttools5-dev
|
||||
ssh
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: cmake
|
||||
working-directory: bin
|
||||
run: cmake ..
|
||||
|
@ -180,11 +187,11 @@ jobs:
|
|||
name: release_source
|
||||
path: bin/clementine-*.tar.xz
|
||||
|
||||
build_fedora_34:
|
||||
name: Build Fedora 34 RPM
|
||||
runs-on: ubuntu-18.04
|
||||
build_fedora_36:
|
||||
name: Build Fedora 36 RPM
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: fedora:34
|
||||
image: fedora:36
|
||||
env:
|
||||
RPM_BUILD_NCPUS: "2"
|
||||
steps:
|
||||
|
@ -224,6 +231,8 @@ jobs:
|
|||
qt5-qtbase-devel
|
||||
qt5-qtx11extras-devel
|
||||
qt5-rpm-macros
|
||||
qtsingleapplication-qt5-devel
|
||||
qtsinglecoreapplication-qt5-devel
|
||||
rpmdevtools
|
||||
sha2-devel
|
||||
sparsehash-devel
|
||||
|
@ -231,9 +240,11 @@ jobs:
|
|||
taglib-devel
|
||||
tar
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: cmake
|
||||
working-directory: bin
|
||||
run: cmake ..
|
||||
run: cmake -DUSE_SYSTEM_QTSINGLEAPPLICATION=On ..
|
||||
- name: Build source tarball
|
||||
working-directory: bin
|
||||
run: ../dist/maketarball.sh
|
||||
|
@ -247,14 +258,14 @@ jobs:
|
|||
run: rpmbuild -ba ../dist/clementine.spec
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release_fedora_34
|
||||
name: release_fedora_36
|
||||
path: ~/rpmbuild/RPMS/*/clementine-*.rpm
|
||||
|
||||
build_fedora_33:
|
||||
name: Build Fedora 33 RPM
|
||||
runs-on: ubuntu-18.04
|
||||
build_fedora_37:
|
||||
name: Build Fedora 37 RPM
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: fedora:33
|
||||
image: fedora:37
|
||||
env:
|
||||
RPM_BUILD_NCPUS: "2"
|
||||
steps:
|
||||
|
@ -294,6 +305,8 @@ jobs:
|
|||
qt5-qtbase-devel
|
||||
qt5-qtx11extras-devel
|
||||
qt5-rpm-macros
|
||||
qtsingleapplication-qt5-devel
|
||||
qtsinglecoreapplication-qt5-devel
|
||||
rpmdevtools
|
||||
sha2-devel
|
||||
sparsehash-devel
|
||||
|
@ -301,9 +314,11 @@ jobs:
|
|||
taglib-devel
|
||||
tar
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: cmake
|
||||
working-directory: bin
|
||||
run: cmake ..
|
||||
run: cmake -DUSE_SYSTEM_QTSINGLEAPPLICATION=On ..
|
||||
- name: Build source tarball
|
||||
working-directory: bin
|
||||
run: ../dist/maketarball.sh
|
||||
|
@ -317,12 +332,87 @@ jobs:
|
|||
run: rpmbuild -ba ../dist/clementine.spec
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release_fedora_33
|
||||
name: release_fedora_37
|
||||
path: ~/rpmbuild/RPMS/*/clementine-*.rpm
|
||||
|
||||
build_fedora_38:
|
||||
name: Build Fedora 38 RPM
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: fedora:38
|
||||
env:
|
||||
RPM_BUILD_NCPUS: "2"
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: >
|
||||
dnf install --assumeyes
|
||||
@development-tools
|
||||
alsa-lib-devel
|
||||
boost-devel
|
||||
cmake
|
||||
cryptopp-devel
|
||||
dbus-devel
|
||||
desktop-file-utils
|
||||
fftw-devel
|
||||
gcc-c++
|
||||
gettext
|
||||
git
|
||||
glew-devel
|
||||
gstreamer1-devel
|
||||
gstreamer1-plugins-base-devel
|
||||
hicolor-icon-theme
|
||||
libappstream-glib
|
||||
libcdio-devel
|
||||
libchromaprint-devel
|
||||
libgpod-devel
|
||||
liblastfm-qt5-devel
|
||||
libmtp-devel
|
||||
libnotify-devel
|
||||
openssh
|
||||
pkgconfig
|
||||
protobuf-compiler
|
||||
protobuf-devel
|
||||
pulseaudio-libs-devel
|
||||
qca-qt5-devel
|
||||
qca-qt5-ossl
|
||||
qt5-linguist
|
||||
qt5-qtbase-devel
|
||||
qt5-qtx11extras-devel
|
||||
qt5-rpm-macros
|
||||
qtsingleapplication-qt5-devel
|
||||
qtsinglecoreapplication-qt5-devel
|
||||
rpmdevtools
|
||||
sha2-devel
|
||||
sparsehash-devel
|
||||
sqlite-devel
|
||||
taglib-devel
|
||||
tar
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: cmake
|
||||
working-directory: bin
|
||||
run: cmake -DUSE_SYSTEM_QTSINGLEAPPLICATION=On ..
|
||||
- name: Build source tarball
|
||||
working-directory: bin
|
||||
run: ../dist/maketarball.sh
|
||||
- name: Create rpmbuild directory
|
||||
run: mkdir -p ~/rpmbuild/SOURCES
|
||||
- name: Move source tarball
|
||||
working-directory: bin
|
||||
run: mv clementine-*.tar.xz ~/rpmbuild/SOURCES
|
||||
- name: Build RPM
|
||||
working-directory: bin
|
||||
run: rpmbuild -ba ../dist/clementine.spec
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release_fedora_38
|
||||
path: ~/rpmbuild/RPMS/*/clementine-*.rpm
|
||||
|
||||
|
||||
build_mingw:
|
||||
name: Build Windows Installer
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: eu.gcr.io/clementine-data/mingw-w64:latest
|
||||
env:
|
||||
|
@ -331,6 +421,8 @@ jobs:
|
|||
- name: Fix liblastfm includes
|
||||
run: ln -s /target/include/lastfm /target/include/lastfm5
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: cmake
|
||||
working-directory: bin
|
||||
run: >
|
||||
|
@ -511,67 +603,9 @@ jobs:
|
|||
name: release_mingw
|
||||
path: dist/windows/ClementineSetup*.exe
|
||||
|
||||
build_stretch_64:
|
||||
name: Build Debian Stretch 64-bit deb
|
||||
runs-on: ubuntu-18.04
|
||||
container:
|
||||
image: debian:stretch
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: >
|
||||
apt-get update && apt-get install -y
|
||||
build-essential
|
||||
cmake
|
||||
gettext
|
||||
git
|
||||
libasound2-dev
|
||||
libboost-dev
|
||||
libcdio-dev
|
||||
libchromaprint-dev
|
||||
libcrypto++-dev
|
||||
libdbus-1-dev
|
||||
libfftw3-dev
|
||||
libglew1.5-dev
|
||||
libglib2.0-dev
|
||||
libgpod-dev
|
||||
libgstreamer-plugins-base1.0-dev
|
||||
libgstreamer1.0-dev
|
||||
liblastfm5-dev
|
||||
libmtp-dev
|
||||
libmygpo-qt-dev
|
||||
libprotobuf-dev
|
||||
libpulse-dev
|
||||
libqt5x11extras5-dev
|
||||
libsparsehash-dev
|
||||
libsqlite3-dev
|
||||
libtag1-dev
|
||||
pkg-config
|
||||
protobuf-compiler
|
||||
qtbase5-dev
|
||||
qttools5-dev-tools
|
||||
qttools5-dev
|
||||
ssh
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: cmake
|
||||
working-directory: bin
|
||||
run: >
|
||||
cmake ..
|
||||
-DWITH_DEBIAN=ON
|
||||
-DDEB_ARCH=amd64
|
||||
-DDEB_DIST=stretch
|
||||
-DFORCE_GIT_VERSION=
|
||||
-DENABLE_SPOTIFY_BLOB=OFF
|
||||
- name: make
|
||||
working-directory: bin
|
||||
run : make -j2 deb
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release_stretch_64
|
||||
path: bin/clementine_*.deb
|
||||
|
||||
build_bionic_64:
|
||||
name: Build Ubuntu Bionic 64-bit deb
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: ubuntu:bionic
|
||||
steps:
|
||||
|
@ -610,6 +644,8 @@ jobs:
|
|||
libsparsehash-dev
|
||||
ssh
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: cmake
|
||||
working-directory: bin
|
||||
run: >
|
||||
|
@ -617,7 +653,6 @@ jobs:
|
|||
-DWITH_DEBIAN=ON
|
||||
-DDEB_ARCH=amd64
|
||||
-DDEB_DIST=bionic
|
||||
-DFORCE_GIT_VERSION=
|
||||
-DENABLE_SPOTIFY_BLOB=OFF
|
||||
- name: make
|
||||
working-directory: bin
|
||||
|
@ -627,9 +662,69 @@ jobs:
|
|||
name: release_bionic_64
|
||||
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:
|
||||
name: Build Debian Buster 64-bit deb
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: debian:buster
|
||||
steps:
|
||||
|
@ -668,6 +763,8 @@ jobs:
|
|||
qttools5-dev
|
||||
ssh
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: cmake
|
||||
working-directory: bin
|
||||
run: >
|
||||
|
@ -675,7 +772,6 @@ jobs:
|
|||
-DWITH_DEBIAN=ON
|
||||
-DDEB_ARCH=amd64
|
||||
-DDEB_DIST=buster
|
||||
-DFORCE_GIT_VERSION=
|
||||
-DENABLE_SPOTIFY_BLOB=OFF
|
||||
- name: make
|
||||
working-directory: bin
|
||||
|
@ -687,7 +783,7 @@ jobs:
|
|||
|
||||
build_focal_64:
|
||||
name: Build Ubuntu Focal 64-bit deb
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: ubuntu:focal
|
||||
steps:
|
||||
|
@ -732,6 +828,8 @@ jobs:
|
|||
qttools5-dev
|
||||
ssh
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: cmake
|
||||
working-directory: bin
|
||||
run: >
|
||||
|
@ -739,7 +837,6 @@ jobs:
|
|||
-DWITH_DEBIAN=ON
|
||||
-DDEB_ARCH=amd64
|
||||
-DDEB_DIST=focal
|
||||
-DFORCE_GIT_VERSION=
|
||||
-DENABLE_SPOTIFY_BLOB=OFF
|
||||
- name: make
|
||||
working-directory: bin
|
||||
|
@ -749,11 +846,11 @@ jobs:
|
|||
name: release_focal_64
|
||||
path: bin/clementine_*.deb
|
||||
|
||||
build_groovy_64:
|
||||
name: Build Ubuntu Groovy 64-bit deb
|
||||
runs-on: ubuntu-18.04
|
||||
build_jammy_64:
|
||||
name: Build Ubuntu Jammy 64-bit deb
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: ubuntu:groovy
|
||||
image: ubuntu:jammy
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
env:
|
||||
|
@ -776,7 +873,7 @@ jobs:
|
|||
libcrypto++-dev
|
||||
libdbus-1-dev
|
||||
libfftw3-dev
|
||||
libglew1.5-dev
|
||||
libglew-dev
|
||||
libgpod-dev
|
||||
libgstreamer-plugins-base1.0-dev
|
||||
libgstreamer1.0-dev
|
||||
|
@ -799,95 +896,32 @@ jobs:
|
|||
qttools5-dev
|
||||
ssh
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: cmake
|
||||
working-directory: bin
|
||||
run: >
|
||||
cmake ..
|
||||
-DWITH_DEBIAN=ON
|
||||
-DDEB_ARCH=amd64
|
||||
-DDEB_DIST=groovy
|
||||
-DFORCE_GIT_VERSION=
|
||||
-DDEB_DIST=jammy
|
||||
-DENABLE_SPOTIFY_BLOB=OFF
|
||||
- name: make
|
||||
working-directory: bin
|
||||
run : make -j2 deb
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release_groovy_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
|
||||
name: release_jammy_64
|
||||
path: bin/clementine_*.deb
|
||||
|
||||
build_mac:
|
||||
if: false
|
||||
name: Build Mac DMG
|
||||
runs-on: macos-10.15
|
||||
steps:
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: Install dependencies
|
||||
run: brew bundle
|
||||
|
||||
|
|
|
@ -6,36 +6,40 @@ on:
|
|||
jobs:
|
||||
pull_translations:
|
||||
name: Pull translations from Transifex
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: ubuntu:bionic
|
||||
image: ubuntu:jammy
|
||||
steps:
|
||||
- uses: supplypike/setup-bin@v3
|
||||
with:
|
||||
uri: https://github.com/transifex/cli/releases/download/v1.6.7/tx-linux-amd64.tar.gz
|
||||
name: tx
|
||||
version: 1.6.7
|
||||
- name: Install dependencies
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: >
|
||||
apt-get update && apt-get install -y
|
||||
git
|
||||
python-pip
|
||||
ssh
|
||||
- name: Install tx
|
||||
run: pip install transifex-client==0.13.9
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1.2.0
|
||||
- name: git hackery
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
- name: Switch to master
|
||||
run: git checkout master
|
||||
- name: tx init
|
||||
env:
|
||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||
run: tx init --no-interactive --force
|
||||
run: tx init
|
||||
- name: tx config
|
||||
env:
|
||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||
run: tx config mapping --execute -r clementine.clementineplayer -s en -t PO --expression 'src/translations/<lang>.po'
|
||||
run: tx add --organization davidsansome --project clementine --resource clementineplayer --file-filter 'src/translations/<lang>.po' --type PO src/translations/en.po
|
||||
- name: tx pull
|
||||
env:
|
||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||
run: tx pull --all -f --no-interactive
|
||||
run: tx pull -f -a
|
||||
- name: Setup git SSH
|
||||
uses: webfactory/ssh-agent@v0.4.1
|
||||
with:
|
||||
|
|
|
@ -44,3 +44,4 @@ dist/windows/Python27.zip
|
|||
src/translations/translations.pot
|
||||
*.DS_Store
|
||||
.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}
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
cmake_minimum_required(VERSION 3.0.0)
|
||||
cmake_policy(SET CMP0053 OLD)
|
||||
|
||||
include(CheckCXXCompilerFlag)
|
||||
|
@ -60,7 +60,6 @@ find_library(PROTOBUF_STATIC_LIBRARY libprotobuf.a libprotobuf)
|
|||
|
||||
pkg_check_modules(CDIO libcdio)
|
||||
pkg_check_modules(CHROMAPRINT REQUIRED libchromaprint)
|
||||
pkg_search_module(CRYPTOPP cryptopp libcrypto++)
|
||||
pkg_check_modules(GIO gio-2.0)
|
||||
pkg_check_modules(GLIB REQUIRED glib-2.0)
|
||||
pkg_check_modules(GOBJECT REQUIRED gobject-2.0)
|
||||
|
@ -75,12 +74,9 @@ pkg_check_modules(LIBMTP libmtp>=1.0)
|
|||
pkg_check_modules(LIBMYGPO_QT5 libmygpo-qt5>=1.0.9)
|
||||
pkg_check_modules(LIBPULSE libpulse)
|
||||
pkg_check_modules(LIBXML libxml-2.0)
|
||||
pkg_check_modules(LIBSPOTIFY libspotify>=12.1.45)
|
||||
pkg_check_modules(TAGLIB taglib)
|
||||
|
||||
if (WIN32)
|
||||
find_package(ZLIB REQUIRED)
|
||||
endif (WIN32)
|
||||
find_package(ZLIB REQUIRED)
|
||||
|
||||
find_library(LASTFM5_LIBRARIES lastfm5)
|
||||
find_path(LASTFM5_INCLUDE_DIRS lastfm5/ws.h)
|
||||
|
@ -167,12 +163,6 @@ endif()
|
|||
|
||||
if (APPLE)
|
||||
find_library(SPARKLE Sparkle)
|
||||
find_library(LIBSPOTIFY libspotify)
|
||||
|
||||
if(LIBSPOTIFY_FOUND)
|
||||
set(LIBSPOTIFY_INCLUDE_DIRS ${LIBSPOTIFY})
|
||||
set(LIBSPOTIFY_LIBRARIES ${LIBSPOTIFY})
|
||||
endif(LIBSPOTIFY_FOUND)
|
||||
|
||||
add_subdirectory(3rdparty/SPMediaKeyTap)
|
||||
set(SPMEDIAKEYTAP_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/SPMediaKeyTap)
|
||||
|
@ -296,19 +286,6 @@ optional_component(UDISKS2 ON "Devices: UDisks2 backend"
|
|||
DEPENDS "D-Bus support" Qt5DBus_FOUND
|
||||
)
|
||||
|
||||
optional_component(SPOTIFY_BLOB ON "Spotify support: non-GPL binary helper"
|
||||
DEPENDS "protobuf" PROTOBUF_FOUND PROTOBUF_PROTOC_EXECUTABLE
|
||||
DEPENDS "libspotify" LIBSPOTIFY_FOUND
|
||||
)
|
||||
|
||||
if (CRYPTOPP_FOUND OR HAVE_SPOTIFY_BLOB)
|
||||
set(CRYPTOPP_OR_HAVE_SPOTIFY_BLOB ON)
|
||||
endif()
|
||||
|
||||
optional_component(SPOTIFY ON "Spotify support"
|
||||
DEPENDS "cryptopp or spotify blob" CRYPTOPP_OR_HAVE_SPOTIFY_BLOB
|
||||
)
|
||||
|
||||
optional_component(MOODBAR ON "Moodbar support"
|
||||
DEPENDS "fftw3" FFTW3_FOUND
|
||||
)
|
||||
|
@ -340,13 +317,6 @@ if (APPLE AND USE_BUNDLE AND NOT USE_BUNDLE_DIR)
|
|||
set(USE_BUNDLE_DIR "../PlugIns")
|
||||
endif()
|
||||
|
||||
if(CRYPTOPP_FOUND)
|
||||
set(HAVE_CRYPTOPP ON)
|
||||
if(HAVE_SPOTIFY)
|
||||
set(HAVE_SPOTIFY_DOWNLOADER ON)
|
||||
endif(HAVE_SPOTIFY)
|
||||
endif(CRYPTOPP_FOUND)
|
||||
|
||||
# Remove GLU and GL from the link line - they're not really required
|
||||
# and don't exist on my mingw toolchain
|
||||
list(REMOVE_ITEM QT_LIBRARIES "-lGLU -lGL")
|
||||
|
@ -377,11 +347,18 @@ include_directories("3rdparty/qsqlite")
|
|||
|
||||
# When/if upstream accepts our patches then these options can be used to link
|
||||
# to system installed qtsingleapplication instead.
|
||||
option(USE_SYSTEM_QTSINGLEAPPLICATION "Don't set this option unless your system QtSingleApplication library has been compiled with the Clementine patches in 3rdparty" OFF)
|
||||
option(USE_SYSTEM_QTSINGLEAPPLICATION "Use the system-provided QtSingleApplication library (needs to have clementine patches, but these seem to be in Qt5)" OFF)
|
||||
if(USE_SYSTEM_QTSINGLEAPPLICATION)
|
||||
find_path(QTSINGLEAPPLICATION_INCLUDE_DIRS qtsingleapplication.h PATH_SUFFIXES qt5/QtSolutions)
|
||||
find_library(QTSINGLEAPPLICATION_LIBRARIES Qt5Solutions_SingleApplication-2.6)
|
||||
find_library(QTSINGLECOREAPPLICATION_LIBRARIES Qt5Solutions_SingleCoreApplication-2.6)
|
||||
find_path(QTSINGLEAPPLICATION_INCLUDE_DIRS qtsingleapplication.h PATH_SUFFIXES qt5/QtSolutions REQUIRED)
|
||||
find_library(QTSINGLEAPPLICATION_LIBRARIES Qt5Solutions_SingleApplication-2.6 REQUIRED)
|
||||
add_library(qtsingleapplication INTERFACE)
|
||||
target_link_libraries(qtsingleapplication INTERFACE QTSINGLEAPPLICATION_LIBRARIES)
|
||||
target_include_directories(qtsingleapplication INTERFACE QTSINGLEAPPLICATION_INCLUDE_DIRS)
|
||||
find_path(QTSINGLECOREAPPLICATION_INCLUDE_DIRS qtsinglecoreapplication.h PATH_SUFFIXES qt5/QtSolutions REQUIRED)
|
||||
find_library(QTSINGLECOREAPPLICATION_LIBRARIES Qt5Solutions_SingleCoreApplication-2.6 REQUIRED)
|
||||
add_library(qtsinglecoreapplication INTERFACE)
|
||||
target_link_libraries(qtsinglecoreapplication INTERFACE QTSINGLECOREAPPLICATION_LIBRARIES)
|
||||
target_include_directories(qtsinglecoreapplication INTERFACE QTSINGLECOREAPPLICATION_INCLUDE_DIRS)
|
||||
else(USE_SYSTEM_QTSINGLEAPPLICATION)
|
||||
add_subdirectory(3rdparty/qtsingleapplication)
|
||||
set(QTSINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/qtsingleapplication)
|
||||
|
@ -454,9 +431,6 @@ add_subdirectory(ext/libclementine-common)
|
|||
add_subdirectory(ext/libclementine-tagreader)
|
||||
add_subdirectory(ext/clementine-tagreader)
|
||||
add_subdirectory(ext/libclementine-remote)
|
||||
if(HAVE_SPOTIFY)
|
||||
add_subdirectory(ext/libclementine-spotifyblob)
|
||||
endif(HAVE_SPOTIFY)
|
||||
|
||||
option(WITH_DEBIAN OFF)
|
||||
if(WITH_DEBIAN)
|
||||
|
@ -467,10 +441,6 @@ if(HAVE_BREAKPAD)
|
|||
add_subdirectory(3rdparty/google-breakpad)
|
||||
endif(HAVE_BREAKPAD)
|
||||
|
||||
if(HAVE_SPOTIFY_BLOB)
|
||||
add_subdirectory(ext/clementine-spotifyblob)
|
||||
endif(HAVE_SPOTIFY_BLOB)
|
||||
|
||||
if(HAVE_MOODBAR)
|
||||
add_subdirectory(gst/moodbar)
|
||||
endif()
|
||||
|
|
|
@ -41,8 +41,8 @@ Compile and install:
|
|||
|
||||
cd bin
|
||||
cmake ..
|
||||
make -j8
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
|
||||
See the Wiki for more instructions and a list of dependencies:
|
||||
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)
|
||||
else(FORCE_GIT_REVISION)
|
||||
find_program(GIT_EXECUTABLE git)
|
||||
message(STATUS "Found git: ${GIT_EXECUTABLE}")
|
||||
|
||||
if(NOT GIT_EXECUTABLE-NOTFOUND)
|
||||
execute_process(COMMAND ${GIT_EXECUTABLE} describe
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
RESULT_VARIABLE GIT_INFO_RESULT
|
||||
OUTPUT_VARIABLE GIT_REV
|
||||
ERROR_QUIET
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
if(NOT ${GIT_INFO_RESULT} EQUAL 0)
|
||||
message(SEND_ERROR "git describe failed with code ${GIT_INFO_RESULT}: ${GIT_REV}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(${GIT_INFO_RESULT} EQUAL 0)
|
||||
string(REGEX REPLACE "^(.+)-([0-9]+)-(g[a-f0-9]+)$" "\\1;\\2;\\3"
|
||||
GIT_PARTS ${GIT_REV})
|
||||
string(REGEX REPLACE "^(.+)-([0-9]+)-(g[a-f0-9]+)$" "\\1;\\2;\\3"
|
||||
GIT_PARTS ${GIT_REV})
|
||||
|
||||
if(NOT GIT_PARTS)
|
||||
message(FATAL_ERROR "Failed to parse git revision string '${GIT_REV}'")
|
||||
endif(NOT GIT_PARTS)
|
||||
if(NOT GIT_PARTS)
|
||||
message(FATAL_ERROR "Failed to parse git revision string '${GIT_REV}'")
|
||||
endif(NOT GIT_PARTS)
|
||||
|
||||
list(LENGTH GIT_PARTS GIT_PARTS_LENGTH)
|
||||
if(GIT_PARTS_LENGTH EQUAL 3)
|
||||
list(GET GIT_PARTS 0 GIT_TAGNAME)
|
||||
list(GET GIT_PARTS 1 GIT_COMMITCOUNT)
|
||||
list(GET GIT_PARTS 2 GIT_SHA1)
|
||||
set(HAS_GET_REVISION ON)
|
||||
endif(GIT_PARTS_LENGTH EQUAL 3)
|
||||
endif(${GIT_INFO_RESULT} EQUAL 0)
|
||||
list(LENGTH GIT_PARTS GIT_PARTS_LENGTH)
|
||||
if(GIT_PARTS_LENGTH EQUAL 3)
|
||||
list(GET GIT_PARTS 0 GIT_TAGNAME)
|
||||
list(GET GIT_PARTS 1 GIT_COMMITCOUNT)
|
||||
list(GET GIT_PARTS 2 GIT_SHA1)
|
||||
set(HAS_GIT_REVISION ON)
|
||||
endif(GIT_PARTS_LENGTH EQUAL 3)
|
||||
|
||||
if(INCLUDE_GIT_REVISION AND HAS_GET_REVISION)
|
||||
if(INCLUDE_GIT_REVISION AND HAS_GIT_REVISION)
|
||||
set(CLEMENTINE_VERSION_DISPLAY "${GIT_REV}")
|
||||
set(CLEMENTINE_VERSION_DEB "${GIT_REV}")
|
||||
set(CLEMENTINE_VERSION_RPM_V "${GIT_TAGNAME}")
|
||||
set(CLEMENTINE_VERSION_RPM_R "2.${GIT_COMMITCOUNT}.${GIT_SHA1}")
|
||||
set(CLEMENTINE_VERSION_SPARKLE "${GIT_REV}")
|
||||
set(CLEMENTINE_VERSION_PLIST "4096.${GIT_TAGNAME}.2.${GIT_COMMITCOUNT}")
|
||||
endif(INCLUDE_GIT_REVISION AND HAS_GET_REVISION)
|
||||
endif(INCLUDE_GIT_REVISION AND HAS_GIT_REVISION)
|
||||
|
||||
if(0)
|
||||
message(STATUS "Display: ${CLEMENTINE_VERSION_DISPLAY}")
|
||||
|
|
|
@ -88,17 +88,6 @@
|
|||
<item begin="</a" end=">"/>
|
||||
</exclude>
|
||||
</provider>
|
||||
<provider name="hindilyrics.net (Bollywood songs)" title="{title} ({album})" charset="utf-8" url="http://www.hindilyrics.net/lyrics/of-{Title}.html">
|
||||
<urlFormat replace=" _@;\/"'()[]" 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}">
|
||||
<urlFormat replace="_@,;&\/"" with="_"/>
|
||||
<urlFormat replace=" " with="+"/>
|
||||
|
@ -114,13 +103,6 @@
|
|||
</extract>
|
||||
<invalidIndicator value="ERROR"/>
|
||||
</provider>
|
||||
<provider name="loudson.gs" title="" charset="utf-8" url="http://www.loudson.gs/{a}/{artist}/{album}/{title}">
|
||||
<urlFormat replace=" _@,;&\/"" 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">
|
||||
<urlFormat replace=" _@,;&\/"" with="-"/>
|
||||
<urlFormat replace="'." with=""/>
|
||||
|
@ -194,14 +176,6 @@
|
|||
</extract>
|
||||
<invalidIndicator value="Page not Found"/>
|
||||
</provider>
|
||||
<provider name="lyricstime.com" title="{artist} - {title} Lyrics" charset="iso-8859-1" url="http://www.lyricstime.com/{artist}-{title}-lyrics.html">
|
||||
<urlFormat replace=" _@,;&\/"'" 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}">
|
||||
<urlFormat replace=" _@,;&\/"" with="_"/>
|
||||
<urlFormat replace="." with=""/>
|
||||
|
@ -210,20 +184,6 @@
|
|||
<item tag="<p>"/>
|
||||
</extract>
|
||||
</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}/">
|
||||
<urlFormat replace=" _@,;&\/"" with="-"/>
|
||||
<urlFormat replace="'." with=""/>
|
||||
|
@ -251,13 +211,6 @@
|
|||
</exclude>
|
||||
<invalidIndicator value="We couldn't find that page."/>
|
||||
</provider>
|
||||
<provider name="seeklyrics.com" title="{artist} - {title} Lyrics" charset="iso-8859-1" url="http://www.seeklyrics.com/lyrics/{Artist}/{Title}.html">
|
||||
<urlFormat replace=" _@,;&\/'"" 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/">
|
||||
<urlFormat replace=" ._@,;&\/"" with="-"/>
|
||||
<urlFormat replace="'" with="_"/>
|
||||
|
|
|
@ -48,7 +48,7 @@ binary-arch: install
|
|||
dh_installchangelogs
|
||||
dh_installmenu
|
||||
dh_installdocs
|
||||
dh_gconf
|
||||
dh_installgsettings
|
||||
dh_link
|
||||
dh_strip
|
||||
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
|
||||
|
||||
set(APP_ID "org.clementine_player.Clementine")
|
||||
if(EXISTS /etc/lsb-release)
|
||||
file(READ "/etc/lsb-release" 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)
|
||||
install(FILES clementine_64.png
|
||||
DESTINATION share/icons/hicolor/64x64/apps/
|
||||
RENAME clementine.png
|
||||
RENAME ${APP_ID}.png
|
||||
)
|
||||
|
||||
install(FILES clementine_128.png
|
||||
DESTINATION share/icons/hicolor/128x128/apps/
|
||||
RENAME clementine.png
|
||||
RENAME ${APP_ID}.png
|
||||
)
|
||||
|
||||
install(FILES ../data/icon.svg
|
||||
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
|
||||
)
|
||||
|
||||
|
@ -64,7 +64,7 @@ if (NOT APPLE)
|
|||
DESTINATION share/kservices5
|
||||
)
|
||||
|
||||
install(FILES clementine.appdata.xml
|
||||
install(FILES ${APP_ID}.appdata.xml
|
||||
DESTINATION share/metainfo
|
||||
)
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ BuildRequires: liblastfm-qt5-devel
|
|||
BuildRequires: desktop-file-utils
|
||||
BuildRequires: hicolor-icon-theme
|
||||
BuildRequires: libappstream-glib
|
||||
BuildRequires: qtsingleapplication-qt5-devel
|
||||
BuildRequires: pkgconfig
|
||||
BuildRequires: pkgconfig(glib-2.0)
|
||||
BuildRequires: pkgconfig(gio-2.0)
|
||||
|
@ -97,7 +98,7 @@ Features include:
|
|||
|
||||
%build
|
||||
cd bin
|
||||
%{cmake} .. -DUSE_INSTALL_PREFIX=OFF -DBUNDLE_PROJECTM_PRESETS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
%{cmake} .. -DUSE_INSTALL_PREFIX=OFF -DBUNDLE_PROJECTM_PRESETS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DFORCE_GIT_REVISION=@GIT_REV@
|
||||
%{cmake_build}
|
||||
|
||||
%install
|
||||
|
@ -112,16 +113,16 @@ rm -f $RPM_BUILD_ROOT/usr/share/icons/ubuntu-mono-{dark,light}/apps/24/clementin
|
|||
%{_bindir}/clementine
|
||||
%{_bindir}/clementine-tagreader
|
||||
%dir %{_datadir}/metainfo/
|
||||
%{_datadir}/metainfo/clementine.appdata.xml
|
||||
%{_datadir}/applications/clementine.desktop
|
||||
%{_datadir}/metainfo/org.clementine_player.Clementine.appdata.xml
|
||||
%{_datadir}/applications/org.clementine_player.Clementine.desktop
|
||||
%{_datadir}/clementine/projectm-presets
|
||||
%{_datadir}/kservices5/clementine-itms.protocol
|
||||
%{_datadir}/kservices5/clementine-itpc.protocol
|
||||
%{_datadir}/kservices5/clementine-feed.protocol
|
||||
%{_datadir}/kservices5/clementine-zune.protocol
|
||||
%{_datadir}/icons/hicolor/64x64/apps/clementine.png
|
||||
%{_datadir}/icons/hicolor/128x128/apps/clementine.png
|
||||
%{_datadir}/icons/hicolor/scalable/apps/clementine.svg
|
||||
%{_datadir}/icons/hicolor/64x64/apps/org.clementine_player.Clementine.png
|
||||
%{_datadir}/icons/hicolor/128x128/apps/org.clementine_player.Clementine.png
|
||||
%{_datadir}/icons/hicolor/scalable/apps/org.clementine_player.Clementine.svg
|
||||
|
||||
%changelog
|
||||
* @RPM_DATE@ David Sansome <me@davidsansome.com> - @CLEMENTINE_VERSION_RPM_V@
|
||||
|
|
|
@ -66,7 +66,6 @@ GSTREAMER_PLUGINS = [
|
|||
'libgstisomp4.dylib',
|
||||
'libgstlame.dylib',
|
||||
'libgstlibav.dylib',
|
||||
'libgstmms.dylib',
|
||||
# TODO: Bring back Musepack support.
|
||||
'libgstogg.dylib',
|
||||
'libgstopus.dylib',
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<!-- Copyright 2014 David Sansome <me@davidsansome.com> -->
|
||||
<application>
|
||||
<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>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<name>Clementine Music Player</name>
|
|
@ -15,6 +15,7 @@ GenericName[hi]=क्लेमेंटैन् संगीत वादक
|
|||
GenericName[is]=Clementine tónlistarspilarinn
|
||||
GenericName[pl]=Odtwarzacz muzyki Clementine
|
||||
GenericName[pt]=Reprodutor de músicas Clementine
|
||||
GenericName[ru]=Музыкальный проигрыватель Clementine
|
||||
GenericName[sl]=Predvajalnik glasbe Clementine
|
||||
GenericName[sr]=Клементина музички плејер
|
||||
GenericName[sr@ijekavian]=Клементина музички плејер
|
||||
|
@ -26,6 +27,7 @@ Comment[es]=Reproducción de música y flujos de Last.fm
|
|||
Comment[is]=Spilar tónlist og streymi frá last.fm
|
||||
Comment[pl]=Odtwarzanie muzyki i strumieni last.fm
|
||||
Comment[pt]=Reprodução de músicas e emissões last.fm
|
||||
Comment[ru]=Прослушивание музыки и потоков last.fm
|
||||
Comment[sl]=Predvaja glasbo in pretoke last.fm
|
||||
Comment[sr]=Репродукује музику и last.fm токове
|
||||
Comment[sr@ijekavian]=Репродукује музику и last.fm токове
|
||||
|
@ -33,111 +35,63 @@ Comment[sr@ijekavianlatin]=Reprodukuje muziku i last.fm tokove
|
|||
Comment[sr@latin]=Reprodukuje muziku i last.fm tokove
|
||||
Exec=clementine %U
|
||||
TryExec=clementine
|
||||
Icon=clementine
|
||||
Icon=org.clementine_player.Clementine
|
||||
Terminal=false
|
||||
Categories=AudioVideo;Player;Qt;Audio;
|
||||
StartupNotify=false
|
||||
MimeType=application/ogg;application/x-ogg;application/x-ogm-audio;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/ogg;audio/vnd.rn-realaudio;audio/vorbis;audio/x-flac;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-oggflac;audio/x-pn-realaudio;audio/x-scpls;audio/x-speex;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-wav;video/x-ms-asf;x-content/audio-player;x-scheme-handler/zune;x-scheme-handler/itpc;x-scheme-handler/itms;x-scheme-handler/feed;
|
||||
Actions=Play;Pause;Stop;StopAfterCurrent;Previous;Next;
|
||||
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;
|
||||
|
||||
[Desktop Action Play]
|
||||
Name=Play
|
||||
Exec=clementine --play
|
||||
Name[af]=Speel
|
||||
Name[be]=Прайграць
|
||||
Name[bg]=Възпроизвеждане
|
||||
Name[br]=Lenn
|
||||
Name[ca]=Reprodueix
|
||||
Name[cs]=Přehrát
|
||||
Name[da]=Afspil
|
||||
Name[de]=Wiedergabe
|
||||
Name[el]=Αναπαραγωγή
|
||||
Name[es]=Reproducir
|
||||
Name[et]=Mängi
|
||||
Name[eu]=Erreproduzitu
|
||||
Name[fa]=پخش
|
||||
Name[fi]=Toista
|
||||
Name[fr]=Lecture
|
||||
Name[ga]=Seinn
|
||||
Name[gl]=Reproducir
|
||||
Name[he]=נגינה
|
||||
Name[hi]=गाना बजाएं
|
||||
Name[hr]=Pokreni reprodukciju
|
||||
Name[hu]=Lejátszás
|
||||
Name[is]=Spila
|
||||
Name[it]=Riproduci
|
||||
Name[ja]=再生
|
||||
Name[kk]=Ойнату
|
||||
Name[lt]=Groti
|
||||
Name[lv]=Atskaņot
|
||||
Name[ms]=Mainkan
|
||||
Name[nb]=Spill
|
||||
Name[nl]=Afspelen
|
||||
Name[oc]=Lectura
|
||||
Name[pl]=Odtwarzaj
|
||||
Name[pt]=Reproduzir
|
||||
Name[pt_BR]=Reproduzir
|
||||
Name[ro]=Redă
|
||||
Name[ru]=Воспроизвести
|
||||
Name[sk]=Hrať
|
||||
Name[sl]=Predvajaj
|
||||
Name[sr]=Пусти
|
||||
Name[sr@ijekavian]=Пусти
|
||||
Name[sr@ijekavianlatin]=Pusti
|
||||
Name[sr@latin]=Pusti
|
||||
Name[sv]=Spela upp
|
||||
Name[tr]=Çal
|
||||
Name[uk]=Відтворити
|
||||
Name[vi]=Phát
|
||||
Name[zh_CN]=播放
|
||||
Name[zh_TW]=播放
|
||||
|
||||
[Desktop Action Pause]
|
||||
Name=Pause
|
||||
Exec=clementine --pause
|
||||
Name[be]=Прыпыніць
|
||||
Name[bg]=Пауза
|
||||
Name[br]=Ehan
|
||||
Name[ca]=Pausa
|
||||
Name[cs]=Pozastavit
|
||||
Name[el]=Παύση
|
||||
Name[es]=Pausar
|
||||
Name[et]=Paus
|
||||
Name[eu]=Pausarazi
|
||||
Name[fa]=ایست
|
||||
Name[fi]=Keskeytä
|
||||
Name[ga]=Cuir ar sos
|
||||
Name[gl]=Pausa
|
||||
Name[he]=השהייה
|
||||
Name[hi]=गाना रोकें
|
||||
Name[hr]=Pauza
|
||||
Name[hu]=Szünet
|
||||
Name[is]=Setja í bið
|
||||
Name[it]=Pausa
|
||||
Name[ja]=一時停止
|
||||
Name[kk]=Аялдату
|
||||
Name[ko]=일시중지
|
||||
Name[lt]=Pristabdyti
|
||||
Name[lv]=Pauze
|
||||
Name[nl]=Pauze
|
||||
Name[oc]=Pausa
|
||||
Name[pl]=Pauza
|
||||
Name[pt]=Pausa
|
||||
Name[pt_BR]=Pausar
|
||||
Name[ro]=Pauză
|
||||
Name[ru]=Приостановить
|
||||
Name[sk]=Pozastaviť
|
||||
Name[sl]=Začasno ustavi
|
||||
Name[sr]=Паузирај
|
||||
Name[sr@ijekavian]=Паузирај
|
||||
Name[sr@ijekavianlatin]=Pauziraj
|
||||
Name[sr@latin]=Pauziraj
|
||||
Name[sv]=Gör paus
|
||||
Name[tr]=Duraklat
|
||||
Name[uk]=Призупинити
|
||||
Name[vi]=Tạm dừng
|
||||
Name[zh_CN]=暂停
|
||||
Name[zh_TW]=暫停
|
||||
[Desktop Action Play-Pause]
|
||||
Name=Play/Pause
|
||||
Exec=clementine --play-pause
|
||||
Name[be]=Прайграць/Прыпыніць
|
||||
Name[bg]=Възпроизвеждане/Пауза
|
||||
Name[br]=Lenn/Ehan
|
||||
Name[ca]=Reprodueix/Pausa
|
||||
Name[cs]=Přehrát/Pozastavit
|
||||
Name[da]=Afspil/Pause
|
||||
Name[de]=Wiedergabe/Pause
|
||||
Name[el]=Αναπαραγωγή/Παύση
|
||||
Name[es]=Reproducir/Pausar
|
||||
Name[et]=Mängi/Paus
|
||||
Name[eu]=Erreproduzitu/Pausarazi
|
||||
Name[fa]=پخش/مکث
|
||||
Name[fi]=Toista/Keskeytä
|
||||
Name[fr]=Lecture/Pause
|
||||
Name[ga]=Seinn/Cuir ar sos
|
||||
Name[gl]=Reproducir/Pausa
|
||||
Name[he]=נגן/השהה
|
||||
Name[hi]=गाना बजाएं/गाना रोकें
|
||||
Name[hr]=Pokreni reprodukciju/Pauza
|
||||
Name[hu]=Lejátszás/Szünet
|
||||
Name[is]=Spila/Setja í bið
|
||||
Name[it]=Riproduci/Pausa
|
||||
Name[ja]=再生/一時停止
|
||||
Name[kk]=Ойнату/Аялдату
|
||||
Name[ko]=재생/일시 중지
|
||||
Name[lt]=Groti/Pristabdyti
|
||||
Name[lv]=Atskaņot/Pauze
|
||||
Name[nb]=Spill/Pause
|
||||
Name[nl]=Afspelen/Pauze
|
||||
Name[oc]=Lectura/Pausa
|
||||
Name[pl]=Odtwarzaj/Pauza
|
||||
Name[pt]=Reproduzir/Pausa
|
||||
Name[pt_BR]=Reproduzir/Pausar
|
||||
Name[ro]=Redă/Pauză
|
||||
Name[ru]=Играть/пауза
|
||||
Name[sk]=Hrať/Pozastaviť
|
||||
Name[sl]=Predvajaj/Začasno ustavi
|
||||
Name[sr]=Пусти/Паузирај
|
||||
Name[sr@ijekavian]=Пусти/Паузирај
|
||||
Name[sr@ijekavianlatin]=Pusti/Pauziraj
|
||||
Name[sr@latin]=Pusti/Pauziraj
|
||||
Name[sv]=Spela upp/Gör paus
|
||||
Name[tr]=Çal/Duraklat
|
||||
Name[uk]=Відтворити/Призупинити
|
||||
Name[vi]=Phát/Tạm dừng
|
||||
Name[zh_CN]=播放/暂停
|
||||
Name[zh_TW]=播放/暫停
|
||||
|
||||
[Desktop Action Stop]
|
||||
Name=Stop
|
||||
|
@ -322,7 +276,7 @@ Name[pl]=Dalej
|
|||
Name[pt]=Seguinte
|
||||
Name[pt_BR]=Próximo
|
||||
Name[ro]=Următoarea
|
||||
Name[ru]=Дальше
|
||||
Name[ru]=Следующий
|
||||
Name[sk]=Ďalšia
|
||||
Name[sl]=Naslednji
|
||||
Name[sr]=Следећа
|
|
@ -7,7 +7,7 @@ import polib
|
|||
import re
|
||||
|
||||
PO_GLOB = 'src/translations/*.po'
|
||||
DESKTOP_PATH = 'dist/clementine.desktop'
|
||||
DESKTOP_PATH = 'dist/org.clementine_player.Clementine.desktop'
|
||||
|
||||
class ConfigParser(object):
|
||||
"""
|
||||
|
|
|
@ -302,7 +302,6 @@ Section "Clementine" Clementine
|
|||
|
||||
File "clementine.exe"
|
||||
File "clementine-tagreader.exe"
|
||||
File "clementine-spotifyblob.exe"
|
||||
File "clementine.ico"
|
||||
File "glew32.dll"
|
||||
File "libcdio-19.dll"
|
||||
|
@ -355,7 +354,6 @@ Section "Clementine" Clementine
|
|||
File "libpsl-5.dll"
|
||||
File "libsoup-2.4-1.dll"
|
||||
File "libspeex-1.dll"
|
||||
File "libspotify.dll"
|
||||
File "libssl-1_1.dll"
|
||||
File "libsqlite3-0.dll"
|
||||
File "libstdc++-6.dll"
|
||||
|
|
|
@ -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}
|
||||
${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,
|
||||
const char* slot, const Args&... args) {
|
||||
QFutureWatcher<T>* watcher = new QFutureWatcher<T>;
|
||||
watcher->setFuture(future);
|
||||
QObject::connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()));
|
||||
watcher->setFuture(future);
|
||||
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,
|
||||
const Args&... args) {
|
||||
QFutureWatcher<T>* watcher = new QFutureWatcher<T>;
|
||||
watcher->setFuture(future);
|
||||
QObject::connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()));
|
||||
watcher->setFuture(future);
|
||||
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 <QtGlobal>
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <execinfo.h>
|
||||
#endif
|
||||
#include "conf_backtrace.h"
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
|
@ -325,7 +323,7 @@ QString DemangleSymbol(const QString& symbol) {
|
|||
}
|
||||
|
||||
void DumpStackTrace() {
|
||||
#ifdef Q_OS_UNIX
|
||||
#ifdef Backtrace_FOUND
|
||||
void* callstack[128];
|
||||
int callstack_size =
|
||||
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
|
||||
make -j $(getconf _NPROCESSORS_ONLN)
|
||||
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|TryExec=.*|TryExec=/snap/bin/clementine|' $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/org.clementine_player.Clementine.desktop
|
||||
|
||||
build-packages:
|
||||
- cmake
|
||||
|
@ -170,7 +170,7 @@ parts:
|
|||
apps:
|
||||
clementine:
|
||||
command: desktop-launch $SNAP/usr/bin/clementine
|
||||
desktop: usr/share/applications/clementine.desktop
|
||||
desktop: usr/share/applications/org.clementine_player.Clementine.desktop
|
||||
environment:
|
||||
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
|
||||
|
|
|
@ -47,10 +47,6 @@ include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-tagreader)
|
|||
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-tagreader)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-remote)
|
||||
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-remote)
|
||||
if(HAVE_SPOTIFY)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-spotifyblob)
|
||||
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-spotifyblob)
|
||||
endif(HAVE_SPOTIFY)
|
||||
|
||||
include(../cmake/ParseArguments.cmake)
|
||||
|
||||
|
@ -385,6 +381,7 @@ set(SOURCES
|
|||
widgets/errordialog.cpp
|
||||
widgets/fancytabwidget.cpp
|
||||
widgets/favoritewidget.cpp
|
||||
widgets/filenameformatwidget.cpp
|
||||
widgets/fileview.cpp
|
||||
widgets/fileviewlist.cpp
|
||||
widgets/forcescrollperpixel.cpp
|
||||
|
@ -683,6 +680,7 @@ set(HEADERS
|
|||
widgets/errordialog.h
|
||||
widgets/fancytabwidget.h
|
||||
widgets/favoritewidget.h
|
||||
widgets/filenameformatwidget.h
|
||||
widgets/fileview.h
|
||||
widgets/fileviewlist.h
|
||||
widgets/freespacebar.h
|
||||
|
@ -807,6 +805,7 @@ set(UI
|
|||
|
||||
widgets/equalizerslider.ui
|
||||
widgets/errordialog.ui
|
||||
widgets/filenameformatwidget.ui
|
||||
widgets/fileview.ui
|
||||
widgets/loginstatewidget.ui
|
||||
widgets/osdpretty.ui
|
||||
|
@ -882,33 +881,6 @@ optional_source(HAVE_LIBLASTFM
|
|||
internet/lastfm/lastfmsettingspage.ui
|
||||
)
|
||||
|
||||
# Spotify support
|
||||
optional_source(HAVE_SPOTIFY
|
||||
SOURCES
|
||||
internet/spotify/spotifyserver.cpp
|
||||
internet/spotify/spotifyservice.cpp
|
||||
internet/spotify/spotifysettingspage.cpp
|
||||
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
|
||||
optional_source(APPLE
|
||||
INCLUDE_DIRECTORIES
|
||||
|
@ -1296,7 +1268,6 @@ target_link_libraries(clementine_lib
|
|||
${SQLITE_LIBRARIES}
|
||||
|
||||
Qocoa
|
||||
z
|
||||
)
|
||||
|
||||
link_directories(
|
||||
|
@ -1346,17 +1317,6 @@ if(HAVE_BREAKPAD)
|
|||
endif (LINUX)
|
||||
endif(HAVE_BREAKPAD)
|
||||
|
||||
if(HAVE_SPOTIFY)
|
||||
target_link_libraries(clementine_lib clementine-spotifyblob-messages)
|
||||
endif(HAVE_SPOTIFY)
|
||||
|
||||
if(HAVE_SPOTIFY_DOWNLOADER)
|
||||
target_link_libraries(clementine_lib
|
||||
${CRYPTOPP_LIBRARIES}
|
||||
)
|
||||
link_directories(${CRYPTOPP_LIBRARY_DIRS})
|
||||
endif(HAVE_SPOTIFY_DOWNLOADER)
|
||||
|
||||
if(HAVE_LIBPULSE)
|
||||
target_link_libraries(clementine_lib ${LIBPULSE_LIBRARIES})
|
||||
endif()
|
||||
|
@ -1390,7 +1350,6 @@ target_link_libraries(clementine_lib qsqlite)
|
|||
if (WIN32)
|
||||
target_link_libraries(clementine_lib
|
||||
protobuf
|
||||
${ZLIB_LIBRARIES}
|
||||
tinysvcmdns
|
||||
dsound
|
||||
)
|
||||
|
@ -1444,9 +1403,6 @@ target_link_libraries(clementine
|
|||
)
|
||||
|
||||
# macdeploy.py relies on the blob being built first.
|
||||
if(HAVE_SPOTIFY_BLOB)
|
||||
add_dependencies(clementine clementine-spotifyblob)
|
||||
endif(HAVE_SPOTIFY_BLOB)
|
||||
add_dependencies(clementine clementine-tagreader)
|
||||
|
||||
set_target_properties(clementine PROPERTIES
|
||||
|
|
|
@ -65,13 +65,15 @@ Analyzer::Base::Base(QWidget* parent, uint scopeSize)
|
|||
timeout_(40), // msec
|
||||
fht_(new FHT(scopeSize)),
|
||||
engine_(nullptr),
|
||||
lastScope_(512),
|
||||
lastScope_(),
|
||||
new_frame_(false),
|
||||
is_playing_(false),
|
||||
barkband_table_(),
|
||||
prev_color_index_(0),
|
||||
bands_(0),
|
||||
psychedelic_enabled_(false) {}
|
||||
psychedelic_enabled_(false) {
|
||||
lastScope_.resize(fht_->size());
|
||||
}
|
||||
|
||||
void Analyzer::Base::hideEvent(QHideEvent*) { timer_.stop(); }
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2022, Andrew Reading <andrew@areading.me>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -34,32 +35,32 @@
|
|||
|
||||
const uint BlockAnalyzer::kHeight = 2;
|
||||
const uint BlockAnalyzer::kWidth = 4;
|
||||
const uint BlockAnalyzer::kMinRows = 3; // arbituary
|
||||
const uint BlockAnalyzer::kMinColumns = 32; // arbituary
|
||||
const uint BlockAnalyzer::kMinRows = 3; // arbitrary
|
||||
const uint BlockAnalyzer::kMaxRows = 256; // arbitrary
|
||||
const uint BlockAnalyzer::kMinColumns = 32; // arbitrary
|
||||
const uint BlockAnalyzer::kMaxColumns = 256; // must be 2**n
|
||||
const uint BlockAnalyzer::kFadeSize = 90;
|
||||
const uint BlockAnalyzer::kFadeInitial = 32;
|
||||
|
||||
const char* BlockAnalyzer::kName =
|
||||
QT_TRANSLATE_NOOP("AnalyzerContainer", "Block analyzer");
|
||||
|
||||
BlockAnalyzer::BlockAnalyzer(QWidget* parent)
|
||||
: Analyzer::Base(parent, 9),
|
||||
scope_(kMinColumns),
|
||||
columns_(0),
|
||||
rows_(0),
|
||||
y_(0),
|
||||
barPixmap_(1, 1),
|
||||
topBarPixmap_(kWidth, kHeight),
|
||||
scope_(kMinColumns),
|
||||
store_(1 << 8, 0),
|
||||
fade_bars_(kFadeSize),
|
||||
fade_pos_(1 << 8, 50),
|
||||
fade_intensity_(1 << 8, 32) {
|
||||
canvas_(),
|
||||
rthresh_(kMaxRows + 1, 0.f),
|
||||
bg_grad_(kMaxRows + 1, 0),
|
||||
fade_bars_(kFadeSize, 0),
|
||||
bandinfo_(kMaxColumns) {
|
||||
// Right and bottom edges are 1px padding.
|
||||
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1);
|
||||
// -1 is padding, no drawing takes place there
|
||||
setMaximumWidth(kMaxColumns * (kWidth + 1) - 1);
|
||||
|
||||
// mxcl says null pixmaps cause crashes, so let's play it safe
|
||||
for (uint i = 0; i < kFadeSize; ++i) fade_bars_[i] = QPixmap(1, 1);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent, true);
|
||||
}
|
||||
|
||||
BlockAnalyzer::~BlockAnalyzer() {}
|
||||
|
@ -67,56 +68,53 @@ BlockAnalyzer::~BlockAnalyzer() {}
|
|||
void BlockAnalyzer::resizeEvent(QResizeEvent* e) {
|
||||
QWidget::resizeEvent(e);
|
||||
|
||||
background_ = QPixmap(size());
|
||||
canvas_ = QPixmap(size());
|
||||
|
||||
const uint oldRows = rows_;
|
||||
uint newRows, newCols;
|
||||
|
||||
// all is explained in analyze()..
|
||||
// +1 to counter -1 in maxSizes, trust me we need this!
|
||||
columns_ = qMin(
|
||||
static_cast<uint>(static_cast<double>(width() + 1) / (kWidth + 1)) + 1,
|
||||
kMaxColumns);
|
||||
rows_ = static_cast<uint>(static_cast<double>(height() + 1) / (kHeight + 1));
|
||||
newCols = 1 + (width() + 1) / (kWidth + 1);
|
||||
newRows = 0 + (height() + 1) / (kHeight + 1);
|
||||
newCols = qMin(kMaxColumns, qMax(kMinColumns, newCols));
|
||||
newRows = qMin(kMaxRows, qMax(kMinRows, newRows));
|
||||
|
||||
// this is the y-offset for drawing from the top of the widget
|
||||
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
|
||||
if (newCols != columns_) {
|
||||
columns_ = newCols;
|
||||
scope_.resize(columns_);
|
||||
|
||||
scope_.resize(columns_);
|
||||
updateBandSize(columns_);
|
||||
bandinfo_.fill(FHTBand());
|
||||
}
|
||||
|
||||
if (rows_ != oldRows) {
|
||||
barPixmap_ = QPixmap(kWidth, rows_ * (kHeight + 1));
|
||||
if (rows_ != newRows) {
|
||||
rows_ = newRows;
|
||||
|
||||
for (uint i = 0; i < kFadeSize; ++i)
|
||||
fade_bars_[i] = QPixmap(kWidth, rows_ * (kHeight + 1));
|
||||
// this is the y-offset for drawing from the top of the widget
|
||||
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
|
||||
|
||||
yscale_.resize(rows_ + 1);
|
||||
|
||||
const uint PRE = 1,
|
||||
PRO = 1; // PRE and PRO allow us to restrict the range somewhat
|
||||
const float PRE = 1.f,
|
||||
PRO =
|
||||
1.f, // PRE and PRO allow us to restrict the range somewhat
|
||||
SCL = log10f(PRE + PRO + (1.f * rows_));
|
||||
|
||||
for (uint z = 0; z < rows_; ++z)
|
||||
yscale_[z] = 1 - (log10(PRE + z) / log10(PRE + rows_ + PRO));
|
||||
rthresh_[z] = 1.f - log10f(PRE + (1.f * z)) / SCL;
|
||||
|
||||
yscale_[rows_] = 0;
|
||||
rthresh_[rows_] = 0.f;
|
||||
|
||||
determineStep();
|
||||
paletteChange(palette());
|
||||
}
|
||||
|
||||
updateBandSize(columns_);
|
||||
drawBackground();
|
||||
canvas_ = QImage(columns_ * (kWidth + 1), rows_ * (kHeight + 1),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
canvas_.fill(pad_color_);
|
||||
}
|
||||
|
||||
void BlockAnalyzer::determineStep() {
|
||||
// falltime is dependent on rowcount due to our digital resolution (ie we have
|
||||
// boxes/blocks of pixels)
|
||||
// I calculated the value 30 based on some trial and error
|
||||
|
||||
// falltime is dependent on rowcount
|
||||
// the fall time of 30 is too slow on framerates above 50fps
|
||||
const double fallTime = timeout() < 20 ? 20 * rows_ : 30 * rows_;
|
||||
|
||||
step_ = static_cast<double>(rows_ * timeout()) / fallTime;
|
||||
const float rFallTime = 1.f / (timeout() < 20 ? 20.f : 30.f);
|
||||
step_ = timeout() * rFallTime;
|
||||
}
|
||||
|
||||
void BlockAnalyzer::framerateChanged() { // virtual
|
||||
|
@ -124,10 +122,10 @@ void BlockAnalyzer::framerateChanged() { // virtual
|
|||
}
|
||||
|
||||
void BlockAnalyzer::transform(Analyzer::Scope& s) {
|
||||
for (uint x = 0; x < s.size(); ++x) s[x] *= 2;
|
||||
for (uint x = 0; x < s.size(); ++x) s[x] *= 2.f;
|
||||
|
||||
fht_->spectrum(s.data());
|
||||
fht_->scale(s.data(), 1.0 / 20);
|
||||
fht_->scale(s.data(), 1.f / 20.f);
|
||||
|
||||
// the second half is pretty dull, so only show it if the user has a large
|
||||
// analyzer
|
||||
|
@ -138,77 +136,225 @@ void BlockAnalyzer::transform(Analyzer::Scope& s) {
|
|||
|
||||
void BlockAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
|
||||
bool new_frame) {
|
||||
// y = 2 3 2 1 0 2
|
||||
// . . . . # .
|
||||
// . . . # # .
|
||||
// # . # # # #
|
||||
// # # # # # #
|
||||
//
|
||||
// visual aid for how this analyzer works.
|
||||
// y represents the number of blanks
|
||||
// y starts from the top and increases in units of blocks
|
||||
float yf;
|
||||
uint x, y;
|
||||
|
||||
// yscale_ looks similar to: { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
|
||||
// if it contains 6 elements there are 5 rows in the analyzer
|
||||
if (p.paintEngine() == 0) return;
|
||||
if (canvas_.isNull()) return;
|
||||
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
|
||||
if (!new_frame) {
|
||||
p.drawPixmap(0, 0, canvas_);
|
||||
p.drawImage(0, 0, canvas_, 0, 0, width(), height(), Qt::NoFormatConversion);
|
||||
return;
|
||||
}
|
||||
|
||||
QPainter canvas_painter(&canvas_);
|
||||
|
||||
Analyzer::interpolate(s, scope_);
|
||||
|
||||
// update the graphics with the new colour
|
||||
if (psychedelic_enabled_) {
|
||||
paletteChange(QPalette());
|
||||
}
|
||||
// Update the color palettes.
|
||||
if (psychedelic_enabled_) paletteChange(QPalette());
|
||||
|
||||
// Paint the background
|
||||
canvas_painter.drawPixmap(0, 0, background_);
|
||||
// Visual Aid
|
||||
//
|
||||
// This analyzer maintains a list of intensity thresholds for each row of
|
||||
// the analyzer. For each frequency band (represented column-wise, one per
|
||||
// band), the spectral power calculation obtained from the analyzer scope
|
||||
// output is compared against these thresholds to determine the row indices
|
||||
// at which the regions become active. While inactive regions are dark,
|
||||
// active regions and all those below the corresponding transition region
|
||||
// are "lit up".
|
||||
//
|
||||
// So, where
|
||||
// . indicates block is inactive/dark
|
||||
// # indicates block is active /lit,
|
||||
// what is drawn is (for example)
|
||||
//
|
||||
// COLUMNS/Bands
|
||||
// . . . . # . R
|
||||
// . . . # # . O
|
||||
// # . # # # # W
|
||||
// # # # # # # S
|
||||
//
|
||||
// y = 2 3 2 1 0 2
|
||||
//
|
||||
// Here y is the row index for which the intensity threshold is met, with
|
||||
// 0 indicating the topmost row. The nRows+1 intensity values are stored
|
||||
// in rthresh_[], sorted in decreasing order (the top, y=0 region would
|
||||
// be the most spectrally intense); the additional, final value is always
|
||||
// zero and exists mostly as a sort of loop optimization.
|
||||
//
|
||||
// For the above illustration, rthresh_[] might have values similar to
|
||||
// { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
|
||||
//
|
||||
// Now, consider two "frames" that occur sequentially after each other. Where
|
||||
// . indicates block is inactive/dark
|
||||
// o indicates block is inactive/dark and fading out (was active)
|
||||
// # indicates block is active /lit,
|
||||
// [ ] indicates block is the bar topper
|
||||
//
|
||||
// frame 1 ====> frame 2
|
||||
// COLUMNS/Bands COLUMNS/Bands
|
||||
// . . . . [#] . R . . . . o .
|
||||
// . . . [#] # . O . . [#] o [#] .
|
||||
// [#] . [#] # # [#] W o . # o # [#]
|
||||
// # [#] # # # # S [#] o # [#] # #
|
||||
//
|
||||
// 2 3 2 1 0 2 = B_y = 3 4 1 1 1 2
|
||||
//
|
||||
// After a previously active region becomes inactive, for a period of time
|
||||
// it is drawn in a color that darkens over time. These are based upon the
|
||||
// the current color scheme and get stored within fade_bars_[].
|
||||
// Additionally, a rowwise gradient is applied to active bands to help keep
|
||||
// the spectrum display visually interesting, with colors darkening as
|
||||
// intensities decrease -- that is, as rthresh_[] values decrease. The
|
||||
// inactive-active transition area is drawn with the brightest color and
|
||||
// acts as a "bar topper"; this topper should visually rise and fall over
|
||||
// time.
|
||||
//
|
||||
// As in the transition example above, bands (columns) are drawn vertically
|
||||
// from top to bottom, progressing from left to right. Supposing Y_r is the
|
||||
// row coordinate, B_y is the band coordinate, and
|
||||
// 0 <= Y_r,B_y < nRows <= kMaxRows,
|
||||
// the drawing procedure for each band can be described as follows:
|
||||
// a. Y_r < B_y
|
||||
// First the '.' regions that have not been recently active are
|
||||
// darkened (background). This is determined via the band's
|
||||
// fade_intensity and fade_row values.
|
||||
// b. Y_r < B_y
|
||||
// Recently active areas are drawn using a special darkening-fade
|
||||
// color, until either some number of frames have elapsed or they
|
||||
// became active since the countdown began.
|
||||
// c. Y_r = B_y
|
||||
// The transition region is drawn as a bar topper.
|
||||
// d. Y_r > B_y < nRows
|
||||
// Each subsequent region below the transition region should be active.
|
||||
// Draw these using a gradient that darkens as Y_r -> nRows.
|
||||
// The logic for these can be found in the colorFromRowAndBand() function.
|
||||
//
|
||||
|
||||
for (uint y, x = 0; x < scope_.size(); ++x) {
|
||||
// determine y
|
||||
for (y = 0; scope_[x] < yscale_[y]; ++y) continue;
|
||||
// Update band information.
|
||||
for (x = 0; x < scope_.size(); ++x) {
|
||||
const float& bandthr = scope_[x];
|
||||
FHTBand& band = bandinfo_[x];
|
||||
|
||||
// this is opposite to what you'd think, higher than y
|
||||
// means the bar is lower than y (physically)
|
||||
if (static_cast<float>(y) > store_[x])
|
||||
y = static_cast<int>(store_[x] += step_);
|
||||
else
|
||||
store_[x] = y;
|
||||
|
||||
// if y is lower than fade_pos_, then the bar has exceeded the kHeight of
|
||||
// the fadeout
|
||||
// if the fadeout is quite faded now, then display the new one
|
||||
if (y <= fade_pos_[x] /*|| fade_intensity_[x] < kFadeSize / 3*/) {
|
||||
fade_pos_[x] = y;
|
||||
fade_intensity_[x] = kFadeSize;
|
||||
// Calculate activity transition row values.
|
||||
// Note: rows_ < rthresh_.size()
|
||||
for (y = 0; y < rows_; ++y) {
|
||||
if (bandthr >= rthresh_[y]) break;
|
||||
}
|
||||
|
||||
if (fade_intensity_[x] > 0) {
|
||||
const uint offset = --fade_intensity_[x];
|
||||
const uint y = y_ + (fade_pos_[x] * (kHeight + 1));
|
||||
canvas_painter.drawPixmap(x * (kWidth + 1), y, fade_bars_[offset], 0, 0,
|
||||
kWidth, height() - y);
|
||||
// y <= band height :: band matches or exceeds power from last frame.
|
||||
// y > band height :: band lost power since last frame.
|
||||
if ((yf = 1.f * y) <= band.height) {
|
||||
band.height = yf;
|
||||
band.row = y;
|
||||
} else {
|
||||
// This band has lost power since the last-recorded maximal threshold
|
||||
// value. Gradually decrease this until it meets the current value.
|
||||
band.height += step_;
|
||||
band.row = y = static_cast<uint>(band.height);
|
||||
}
|
||||
|
||||
if (fade_intensity_[x] == 0) fade_pos_[x] = rows_;
|
||||
// y <= band fade_row :: the current threshold exceeds the previously-
|
||||
// marked position in which to begin fade-out. Use the current position
|
||||
// as a new marker and start/restart fade_intensity, the fade-out period
|
||||
// counter.
|
||||
if (y <= band.fade_row) {
|
||||
band.fade_row = y;
|
||||
band.fade_intensity = kFadeSize;
|
||||
}
|
||||
|
||||
// REMEMBER: y is a number from 0 to rows_, 0 means all blocks are glowing,
|
||||
// rows_ means none are
|
||||
canvas_painter.drawPixmap(x * (kWidth + 1), y * (kHeight + 1) + y_, *bar(),
|
||||
0, y * (kHeight + 1), bar()->width(),
|
||||
bar()->height());
|
||||
// Check the fade-out period counter. If expired (i.e., <= 0), the
|
||||
// fade-out effect is complete. Otherwise, continue downcounting and
|
||||
// select the next color for the fade-out sequence.
|
||||
if (band.fade_intensity <= 0) {
|
||||
// fade_intensity <= 0: Done with fade out effect (time expired).
|
||||
band.fade_row = rows_;
|
||||
band.fade_coloridx = 0;
|
||||
} else {
|
||||
// fade_intensity > 0: Continue effect; continue color change.
|
||||
band.fade_coloridx = --band.fade_intensity;
|
||||
}
|
||||
}
|
||||
|
||||
for (uint x = 0; x < store_.size(); ++x)
|
||||
canvas_painter.drawPixmap(x * (kWidth + 1),
|
||||
static_cast<int>(store_[x]) * (kHeight + 1) + y_,
|
||||
topBarPixmap_);
|
||||
// A block will be drawn and colored according to each band (column) of
|
||||
// the FHT spectrum data. This block is a kWidth x kHeight region, along
|
||||
// with 1-px of padding on its right and bottom.
|
||||
//
|
||||
// Conditional (FHTBand) Block State / Color
|
||||
// ===================== ===================
|
||||
// 0 < y < fade_row & fade-out : Inactive / BG color
|
||||
// fade_row < y < row & fade-out : Fade-out / darkening
|
||||
// 0 < y < row & no fade-out : Inactive / BG color
|
||||
// row == y : Threshold / FG color
|
||||
// row < y < rows_ : Active / Vert. gradient
|
||||
// {1-px padding region} : Padding / Pad color
|
||||
//
|
||||
|
||||
p.drawPixmap(0, 0, canvas_);
|
||||
//
|
||||
// Paint the canvas in one go in order to mimize cache thrashing.
|
||||
//
|
||||
QRgb* line; // Current scanline.
|
||||
uint px_w, px_h; // Current width and height in pixels (just to avoid cast).
|
||||
uint to_x; // [0, width()) Current and ending x pixel coordinate.
|
||||
uint to_y; // [0, height()) Current and ending y pixel coordinate.
|
||||
uint blk_r; // [0, rows_) Current block's row.
|
||||
uint blk_c; // [0, columns_) Current block's column.
|
||||
|
||||
quint32 padcolor = pad_color_.rgba();
|
||||
quint32 blkcolor;
|
||||
|
||||
px_w = static_cast<uint>(width());
|
||||
px_h = static_cast<uint>(height());
|
||||
|
||||
// Draw empty top padding, if needed (when y_ > 0. weird window size?).
|
||||
for (y = 0; y < y_; ++y) {
|
||||
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y));
|
||||
for (x = 0; x < px_w; line[x++] = padcolor)
|
||||
;
|
||||
}
|
||||
|
||||
// Draw the texture in one shot, iterating in a row-major fashion.
|
||||
for (blk_r = 0; blk_r < rows_; ++blk_r) {
|
||||
to_y = qMin(y + kHeight, px_h);
|
||||
|
||||
// This block may take several 1-px high scanlines. Each column needs
|
||||
// to be filled accordingly for each of these rows.
|
||||
for (; y < to_y; ++y) {
|
||||
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y));
|
||||
|
||||
for (x = 0, blk_c = 0; blk_c < columns_; ++blk_c) {
|
||||
to_x = qMin(x + kWidth, px_w);
|
||||
|
||||
// Draw [x, to_x], then padding on the right.
|
||||
blkcolor = colorFromRowAndBand(blk_r, bandinfo_[blk_c]);
|
||||
|
||||
for (; x < to_x; line[x++] = blkcolor)
|
||||
;
|
||||
if (x < px_w) line[x++] = padcolor;
|
||||
}
|
||||
|
||||
// If extra space remains in line, fill to the right edge.
|
||||
for (; x < px_w; line[x++] = padcolor)
|
||||
;
|
||||
}
|
||||
|
||||
// Draw a full line of padding below the just-drawn region (if in bounds).
|
||||
if (y < px_h) {
|
||||
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y++));
|
||||
for (x = 0; x < px_w; line[x++] = padcolor)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
// If not at bottom boundary yet, pad remaining lines.
|
||||
while (y < px_h) {
|
||||
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y++));
|
||||
for (x = 0; x < px_w; line[x++] = padcolor)
|
||||
;
|
||||
}
|
||||
|
||||
p.drawImage(0, 0, canvas_, 0, 0, width(), height(), Qt::NoFormatConversion);
|
||||
}
|
||||
|
||||
static inline void adjustToLimits(int& b, int& f, uint& amount) {
|
||||
|
@ -248,7 +394,8 @@ void BlockAnalyzer::psychedelicModeChanged(bool enabled) {
|
|||
* It won't modify the hue of fg unless absolutely necessary
|
||||
* @return the adjusted form of fg
|
||||
*/
|
||||
QColor ensureContrast(const QColor& bg, const QColor& fg, uint _amount = 150) {
|
||||
static QColor ensureContrast(const QColor& bg, const QColor& fg,
|
||||
uint _amount = 150) {
|
||||
class OutputOnExit {
|
||||
public:
|
||||
explicit OutputOnExit(const QColor& color) : c(color) {}
|
||||
|
@ -335,85 +482,58 @@ QColor ensureContrast(const QColor& bg, const QColor& fg, uint _amount = 150) {
|
|||
}
|
||||
|
||||
void BlockAnalyzer::paletteChange(const QPalette&) {
|
||||
const QColor bg = palette().color(QPalette::Background);
|
||||
QColor fg;
|
||||
QColor bg, bgdark, fg;
|
||||
|
||||
if (psychedelic_enabled_) {
|
||||
bg = palette().color(QPalette::Background);
|
||||
bgdark = bg.darker(112);
|
||||
|
||||
if (psychedelic_enabled_)
|
||||
fg = getPsychedelicColor(scope_, 10, 75);
|
||||
} else {
|
||||
else
|
||||
fg = ensureContrast(bg, palette().color(QPalette::Highlight));
|
||||
|
||||
fg_color_ = fg;
|
||||
bg_color_ = bgdark;
|
||||
pad_color_ = bg;
|
||||
|
||||
// Calculate background gradient colors.
|
||||
{
|
||||
const float dr = 15.f * (bg.red() - fg.red()) / (16.f * rows_);
|
||||
const float dg = 15.f * (bg.green() - fg.green()) / (16.f * rows_);
|
||||
const float db = 15.f * (bg.blue() - fg.blue()) / (16.f * rows_);
|
||||
|
||||
for (uint y = 0; y < rows_; ++y) {
|
||||
bg_grad_[y] = qRgba(fg.red() + static_cast<int>(dr * y),
|
||||
fg.green() + static_cast<int>(dg * y),
|
||||
fg.blue() + static_cast<int>(db * y), 255);
|
||||
}
|
||||
|
||||
bg_grad_[rows_] = bg.rgba();
|
||||
}
|
||||
|
||||
topBarPixmap_.fill(fg);
|
||||
|
||||
const double dr =
|
||||
15 * static_cast<double>(bg.red() - fg.red()) / (rows_ * 16);
|
||||
const double dg =
|
||||
15 * static_cast<double>(bg.green() - fg.green()) / (rows_ * 16);
|
||||
const double db =
|
||||
15 * static_cast<double>(bg.blue() - fg.blue()) / (rows_ * 16);
|
||||
const int r = fg.red(), g = fg.green(), b = fg.blue();
|
||||
|
||||
bar()->fill(bg);
|
||||
|
||||
QPainter p(bar());
|
||||
|
||||
for (int y = 0; static_cast<uint>(y) < rows_; ++y)
|
||||
// graduate the fg color
|
||||
p.fillRect(
|
||||
0, y * (kHeight + 1), kWidth, kHeight,
|
||||
QColor(r + static_cast<int>(dr * y), g + static_cast<int>(dg * y),
|
||||
b + static_cast<int>(db * y)));
|
||||
|
||||
// make a complimentary fadebar colour
|
||||
// TODO(John Maguire): dark is not always correct, dumbo!
|
||||
{
|
||||
const QColor bg = palette().color(QPalette::Background).darker(112);
|
||||
|
||||
// make a complimentary fadebar colour
|
||||
// TODO(John Maguire): dark is not always correct, dumbo!
|
||||
int h, s, v;
|
||||
palette().color(QPalette::Background).darker(150).getHsv(&h, &s, &v);
|
||||
const QColor fg(QColor::fromHsv(h + 120, s, v));
|
||||
|
||||
const double dr = fg.red() - bg.red();
|
||||
const double dg = fg.green() - bg.green();
|
||||
const double db = fg.blue() - bg.blue();
|
||||
const int r = bg.red(), g = bg.green(), b = bg.blue();
|
||||
bg.darker(150).getHsv(&h, &s, &v);
|
||||
fg = QColor::fromHsv(h + 120, s, v);
|
||||
|
||||
const float r = 1.f * bgdark.red();
|
||||
const float g = 1.f * bgdark.green();
|
||||
const float b = 1.f * bgdark.blue();
|
||||
const float dr = 1.f * fg.red() - r;
|
||||
const float dg = 1.f * fg.green() - g;
|
||||
const float db = 1.f * fg.blue() - b;
|
||||
|
||||
const float fFscl = 1. * kFadeSize;
|
||||
const float frlogFscl = 1.f / log10f(fFscl);
|
||||
|
||||
// Precalculate all fade-bar pixmaps
|
||||
for (uint y = 0; y < kFadeSize; ++y) {
|
||||
fade_bars_[y].fill(palette().color(QPalette::Background));
|
||||
QPainter f(&fade_bars_[y]);
|
||||
for (int z = 0; static_cast<uint>(z) < rows_; ++z) {
|
||||
const double Y = 1.0 - (log10(kFadeSize - y) / log10(kFadeSize));
|
||||
f.fillRect(
|
||||
0, z * (kHeight + 1), kWidth, kHeight,
|
||||
QColor(r + static_cast<int>(dr * Y), g + static_cast<int>(dg * Y),
|
||||
b + static_cast<int>(db * Y)));
|
||||
}
|
||||
const float lrY = 1.f - (frlogFscl * log10f(fFscl - y));
|
||||
fade_bars_[y] =
|
||||
qRgba(static_cast<int>(r + lrY * dr), static_cast<int>(g + lrY * dg),
|
||||
static_cast<int>(b + lrY * db), 255);
|
||||
}
|
||||
}
|
||||
|
||||
drawBackground();
|
||||
}
|
||||
|
||||
void BlockAnalyzer::drawBackground() {
|
||||
if (background_.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QColor bg = palette().color(QPalette::Background);
|
||||
const QColor bgdark = bg.darker(112);
|
||||
|
||||
background_.fill(bg);
|
||||
|
||||
QPainter p(&background_);
|
||||
|
||||
if (p.paintEngine() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int x = 0; (uint)x < columns_; ++x)
|
||||
for (int y = 0; (uint)y < rows_; ++y)
|
||||
p.fillRect(x * (kWidth + 1), y * (kHeight + 1) + y_, kWidth, kHeight,
|
||||
bgdark);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
|
||||
Copyright 2014, Krzysztof A. Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2022, Andrew Reading <andrew@areading.me>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -43,9 +44,11 @@ class BlockAnalyzer : public Analyzer::Base {
|
|||
static const uint kHeight;
|
||||
static const uint kWidth;
|
||||
static const uint kMinRows;
|
||||
static const uint kMaxRows;
|
||||
static const uint kMinColumns;
|
||||
static const uint kMaxColumns;
|
||||
static const uint kFadeSize;
|
||||
static const uint kFadeInitial;
|
||||
|
||||
static const char* kName;
|
||||
|
||||
|
@ -57,27 +60,58 @@ class BlockAnalyzer : public Analyzer::Base {
|
|||
virtual void framerateChanged();
|
||||
virtual void psychedelicModeChanged(bool);
|
||||
|
||||
void drawBackground();
|
||||
void determineStep();
|
||||
|
||||
private:
|
||||
QPixmap* bar() { return &barPixmap_; }
|
||||
struct FHTBand {
|
||||
FHTBand()
|
||||
: height(0.f),
|
||||
row(0),
|
||||
fade_row(0),
|
||||
fade_coloridx(kMaxRows),
|
||||
fade_intensity(kFadeInitial) {}
|
||||
|
||||
uint columns_, rows_; // number of rows and columns of blocks
|
||||
uint y_; // y-offset from top of widget
|
||||
QPixmap barPixmap_;
|
||||
QPixmap topBarPixmap_;
|
||||
QPixmap background_;
|
||||
QPixmap canvas_;
|
||||
Analyzer::Scope scope_; // so we don't create a vector every frame
|
||||
QVector<float> store_; // current bar kHeights
|
||||
QVector<float> yscale_;
|
||||
// Top of the spectral activity bar.
|
||||
float height; // Foreground-Background transition row.
|
||||
uint row; // Integer floor of the height value.
|
||||
|
||||
QVector<QPixmap> fade_bars_;
|
||||
QVector<uint> fade_pos_;
|
||||
QVector<int> fade_intensity_;
|
||||
// Vertical color fade effect (a sort of hysteresis).
|
||||
uint fade_row; // Row in which to begin showing BG gradient.
|
||||
uint fade_coloridx; // Current fade_bars_[] offset.
|
||||
int fade_intensity; // Current intensity frame counter value.
|
||||
};
|
||||
|
||||
float step_; // rows to fall per frame
|
||||
inline quint32 colorFromRowAndBand(uint cur_r, const FHTBand& band);
|
||||
|
||||
Analyzer::Scope scope_;
|
||||
|
||||
uint columns_; // Number of columns of blocks.
|
||||
uint rows_; // Number of rows of blocks.
|
||||
uint y_; // y-offset from top of widget.
|
||||
float step_; // Rows to fall per frame (during inactivity).
|
||||
|
||||
QColor fg_color_; // Foreground/Active block color.
|
||||
QColor bg_color_; // Background/Inactive block color.
|
||||
QColor pad_color_; // Color of 'lines' dividing the blocks.
|
||||
QImage canvas_; // Drawable canvas of widget.
|
||||
|
||||
QVector<float> rthresh_; // [rows_+1] Rowwise intensity thresholds.
|
||||
QVector<quint32> bg_grad_; // [rows_+1] Vertical background gradient.
|
||||
QVector<quint32> fade_bars_; // [kFadeSize] Block colors per fade level.
|
||||
QVector<FHTBand> bandinfo_; // [columns_] FHT band info.
|
||||
};
|
||||
|
||||
inline quint32 BlockAnalyzer::colorFromRowAndBand(uint r, const FHTBand& band) {
|
||||
// Calculate the block color given band info and the current row.
|
||||
// Note: 0 <= r <= rows_.
|
||||
if (r == band.row)
|
||||
return fg_color_.rgba();
|
||||
else if (r > band.row)
|
||||
return bg_grad_[r];
|
||||
else if ((band.fade_intensity > 0) && (r >= band.fade_row))
|
||||
return fade_bars_[band.fade_coloridx];
|
||||
else
|
||||
return bg_color_.rgba();
|
||||
}
|
||||
|
||||
#endif // ANALYZERS_BLOCKANALYZER_H_
|
||||
|
|
|
@ -48,7 +48,7 @@ void DeleteFiles::Start(const SongList& songs) {
|
|||
songs_ = songs;
|
||||
|
||||
task_id_ = task_manager_->StartTask(tr("Deleting files"));
|
||||
task_manager_->SetTaskBlocksLibraryScans(true);
|
||||
task_manager_->SetTaskBlocksLibraryScans(task_id_);
|
||||
|
||||
thread_ = new QThread;
|
||||
connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles()));
|
||||
|
|
|
@ -119,7 +119,7 @@ void GlobalShortcuts::AddRatingShortcut(const QString& id, const QString& name,
|
|||
const QKeySequence& default_key) {
|
||||
Shortcut shortcut = AddShortcut(id, name, default_key);
|
||||
connect(shortcut.action, &QAction::triggered,
|
||||
[this, rating]() { RateCurrentSong(rating); });
|
||||
[this, rating]() { emit RateCurrentSong(rating); });
|
||||
}
|
||||
|
||||
GlobalShortcuts::Shortcut GlobalShortcuts::AddShortcut(
|
||||
|
|
|
@ -70,12 +70,12 @@ void Organise::Start() {
|
|||
if (thread_) return;
|
||||
|
||||
task_id_ = task_manager_->StartTask(tr("Organising files"));
|
||||
task_manager_->SetTaskBlocksLibraryScans(true);
|
||||
task_manager_->SetTaskBlocksLibraryScans(task_id_);
|
||||
|
||||
thread_ = new QThread;
|
||||
connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles()));
|
||||
connect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)),
|
||||
SLOT(FileTranscoded(QString, QString, bool)));
|
||||
connect(transcoder_, SIGNAL(JobComplete(QUrl, QString, bool)),
|
||||
SLOT(FileTranscoded(QUrl, QString, bool)));
|
||||
|
||||
moveToThread(thread_);
|
||||
thread_->start();
|
||||
|
@ -177,7 +177,7 @@ void Organise::ProcessSomeFiles() {
|
|||
// Start the transcoding - this will happen in the background and
|
||||
// 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.
|
||||
transcoder_->AddJob(task.song_info_.song_.url().toLocalFile(), preset,
|
||||
transcoder_->AddJob(task.song_info_.song_.url(), preset,
|
||||
task.transcoded_filename_);
|
||||
transcoder_->Start();
|
||||
continue;
|
||||
|
@ -262,11 +262,12 @@ void Organise::UpdateProgress() {
|
|||
const int total = task_count_ * 100;
|
||||
|
||||
// Update transcoding progress
|
||||
QMap<QString, float> transcode_progress = transcoder_->GetProgress();
|
||||
for (const QString& filename : transcode_progress.keys()) {
|
||||
QMap<QUrl, float> transcode_progress = transcoder_->GetProgress();
|
||||
for (const QUrl& fileurl : transcode_progress.keys()) {
|
||||
QString filename = fileurl.toLocalFile();
|
||||
if (!tasks_transcoding_.contains(filename)) continue;
|
||||
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
|
||||
|
@ -287,14 +288,17 @@ void Organise::UpdateProgress() {
|
|||
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) {
|
||||
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();
|
||||
|
||||
Task task = tasks_transcoding_.take(input);
|
||||
Task task = tasks_transcoding_.take(input_file_path);
|
||||
if (!success) {
|
||||
files_with_errors_ << input;
|
||||
files_with_errors_ << input_file_path;
|
||||
} else {
|
||||
tasks_pending_ << task;
|
||||
}
|
||||
|
|
|
@ -67,8 +67,7 @@ class Organise : public QObject {
|
|||
|
||||
private slots:
|
||||
void ProcessSomeFiles();
|
||||
void FileTranscoded(const QString& input, const QString& output,
|
||||
bool success);
|
||||
void FileTranscoded(const QUrl& input, const QString& output, bool success);
|
||||
|
||||
private:
|
||||
void SetSongProgress(float progress, bool transcoded = false);
|
||||
|
|
|
@ -24,12 +24,14 @@
|
|||
|
||||
#include <QApplication>
|
||||
#include <QFileInfo>
|
||||
#include <QHash>
|
||||
#include <QPalette>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/arraysize.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/utilities.h"
|
||||
#include "transcoder/transcoder.h"
|
||||
|
||||
const char* OrganiseFormat::kTagPattern = "\\%([a-zA-Z]*)";
|
||||
const char* OrganiseFormat::kBlockPattern = "\\{([^{}]+)\\}";
|
||||
|
@ -96,7 +98,8 @@ bool OrganiseFormat::IsValid() const {
|
|||
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);
|
||||
|
||||
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("/");
|
||||
}
|
||||
|
||||
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,
|
||||
bool* any_empty) const {
|
||||
QRegExp tag_regexp(kTagPattern);
|
||||
|
|
|
@ -20,15 +20,19 @@
|
|||
#ifndef CORE_ORGANISEFORMAT_H_
|
||||
#define CORE_ORGANISEFORMAT_H_
|
||||
|
||||
#include <QStringList>
|
||||
#include <QSyntaxHighlighter>
|
||||
#include <QTextEdit>
|
||||
#include <QValidator>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
struct TranscoderPreset;
|
||||
|
||||
class OrganiseFormat {
|
||||
public:
|
||||
explicit OrganiseFormat(const QString& format = QString());
|
||||
OrganiseFormat(const OrganiseFormat& format) = default;
|
||||
|
||||
static const char* kTagPattern;
|
||||
static const char* kBlockPattern;
|
||||
|
@ -54,7 +58,11 @@ class OrganiseFormat {
|
|||
void reset_tag_overrides() { tag_overrides_.clear(); }
|
||||
|
||||
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 {
|
||||
public:
|
||||
|
|
|
@ -156,10 +156,13 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString& filename) {
|
|||
SongLoader::Result SongLoader::LoadAudioCD() {
|
||||
#ifdef HAVE_AUDIOCD
|
||||
CddaSongLoader* cdda_song_loader = new CddaSongLoader;
|
||||
connect(cdda_song_loader, SIGNAL(SongsDurationLoaded(SongList)), this,
|
||||
SLOT(AudioCDTracksLoadedSlot(SongList)));
|
||||
connect(cdda_song_loader, SIGNAL(SongsMetadataLoaded(SongList)), this,
|
||||
SLOT(AudioCDTracksTagsLoaded(SongList)));
|
||||
connect(cdda_song_loader, &CddaSongLoader::SongsUpdated, this,
|
||||
&SongLoader::AudioCDTracksLoadedSlot);
|
||||
connect(cdda_song_loader, &CddaSongLoader::Finished,
|
||||
[this, cdda_song_loader]() {
|
||||
cdda_song_loader->deleteLater();
|
||||
emit LoadAudioCDFinished(true);
|
||||
});
|
||||
cdda_song_loader->LoadSongs();
|
||||
return Success;
|
||||
#else // HAVE_AUDIOCD
|
||||
|
@ -172,13 +175,6 @@ void SongLoader::AudioCDTracksLoadedSlot(const SongList& songs) {
|
|||
songs_ = songs;
|
||||
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
|
||||
|
||||
SongLoader::Result SongLoader::LoadLocal(const QString& filename) {
|
||||
|
@ -421,6 +417,7 @@ SongLoader::Result SongLoader::LoadRemote() {
|
|||
CHECKED_GCONNECT(typefind, "have-type", &TypeFound, this);
|
||||
gst_bus_set_sync_handler(bus, BusCallbackSync, this, NULL);
|
||||
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
|
||||
GstPad* pad = gst_element_get_static_pad(fakesink, "sink");
|
||||
|
|
|
@ -88,7 +88,6 @@ class SongLoader : public QObject {
|
|||
void StopTypefind();
|
||||
#ifdef HAVE_AUDIOCD
|
||||
void AudioCDTracksLoadedSlot(const SongList& songs);
|
||||
void AudioCDTracksTagsLoaded(const SongList& songs);
|
||||
#endif // HAVE_AUDIOCD
|
||||
|
||||
private:
|
||||
|
|
|
@ -34,9 +34,6 @@
|
|||
#include "core/tagreaderclient.h"
|
||||
#include "core/utilities.h"
|
||||
#include "internet/core/internetmodel.h"
|
||||
#ifdef HAVE_SPOTIFY
|
||||
#include "internet/spotify/spotifyservice.h"
|
||||
#endif
|
||||
|
||||
AlbumCoverLoader::AlbumCoverLoader(QObject* parent)
|
||||
: QObject(parent),
|
||||
|
@ -180,31 +177,7 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(
|
|||
|
||||
remote_tasks_.insert(reply, task);
|
||||
return TryLoadResult(true, false, QImage());
|
||||
}
|
||||
#ifdef HAVE_SPOTIFY
|
||||
else if (filename.toLower().startsWith("spotify://image/")) {
|
||||
// HACK: we should add generic image URL handlers
|
||||
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
|
||||
|
||||
if (!connected_spotify_) {
|
||||
connect(spotify, SIGNAL(ImageLoaded(QString, QImage)),
|
||||
SLOT(SpotifyImageLoaded(QString, QImage)));
|
||||
connected_spotify_ = true;
|
||||
}
|
||||
|
||||
QString id = QUrl(filename).path();
|
||||
if (id.startsWith('/')) {
|
||||
id.remove(0, 1);
|
||||
}
|
||||
remote_spotify_tasks_.insert(id, task);
|
||||
|
||||
// Need to schedule this in the spotify service's thread
|
||||
QMetaObject::invokeMethod(spotify, "LoadImage", Qt::QueuedConnection,
|
||||
Q_ARG(QString, id));
|
||||
return TryLoadResult(true, false, QImage());
|
||||
}
|
||||
#endif
|
||||
else if (filename.isEmpty()) {
|
||||
} else if (filename.isEmpty()) {
|
||||
// Avoid "QFSFileEngine::open: No file name specified" messages if we know
|
||||
// that the filename is empty
|
||||
return TryLoadResult(false, false, task.options.default_output_image_);
|
||||
|
@ -216,18 +189,6 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(
|
|||
image.isNull() ? task.options.default_output_image_ : image);
|
||||
}
|
||||
|
||||
#ifdef HAVE_SPOTIFY
|
||||
void AlbumCoverLoader::SpotifyImageLoaded(const QString& id,
|
||||
const QImage& image) {
|
||||
if (!remote_spotify_tasks_.contains(id)) return;
|
||||
|
||||
Task task = remote_spotify_tasks_.take(id);
|
||||
QImage scaled = ScaleAndPad(task.options, image);
|
||||
emit ImageLoaded(task.id, scaled);
|
||||
emit ImageLoaded(task.id, scaled, image);
|
||||
}
|
||||
#endif
|
||||
|
||||
void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply* reply) {
|
||||
reply->deleteLater();
|
||||
|
||||
|
|
|
@ -67,9 +67,6 @@ class AlbumCoverLoader : public QObject {
|
|||
protected slots:
|
||||
void ProcessTasks();
|
||||
void RemoteFetchFinished(QNetworkReply* reply);
|
||||
#ifdef HAVE_SPOTIFY
|
||||
void SpotifyImageLoaded(const QString& url, const QImage& image);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
enum State {
|
||||
|
|
|
@ -32,12 +32,10 @@ CddaDevice::CddaDevice(const QUrl& url, DeviceLister* lister,
|
|||
cdio_(nullptr),
|
||||
disc_changed_timer_(),
|
||||
cdda_song_loader_(url) {
|
||||
connect(&cdda_song_loader_, SIGNAL(SongsLoaded(SongList)), this,
|
||||
SLOT(SongsLoaded(SongList)));
|
||||
connect(&cdda_song_loader_, SIGNAL(SongsDurationLoaded(SongList)), this,
|
||||
SLOT(SongsLoaded(SongList)));
|
||||
connect(&cdda_song_loader_, SIGNAL(SongsMetadataLoaded(SongList)), this,
|
||||
connect(&cdda_song_loader_, SIGNAL(SongsUpdated(SongList)), this,
|
||||
SLOT(SongsLoaded(SongList)));
|
||||
connect(&cdda_song_loader_, SIGNAL(Finished()), this,
|
||||
SLOT(SongsLoadingFinished()));
|
||||
connect(this, SIGNAL(SongsDiscovered(SongList)), model_,
|
||||
SLOT(SongsDiscovered(SongList)));
|
||||
connect(&disc_changed_timer_, SIGNAL(timeout()), SLOT(CheckDiscChanged()));
|
||||
|
@ -62,8 +60,6 @@ bool CddaDevice::Init() {
|
|||
|
||||
CddaSongLoader* CddaDevice::loader() { return &cdda_song_loader_; }
|
||||
|
||||
CdIo_t* CddaDevice::raw_cdio() { return cdio_; }
|
||||
|
||||
bool CddaDevice::IsValid() const { return (cdio_ != nullptr); }
|
||||
|
||||
void CddaDevice::WatchForDiscChanges(bool watch) {
|
||||
|
@ -73,14 +69,29 @@ void CddaDevice::WatchForDiscChanges(bool watch) {
|
|||
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) {
|
||||
model_->Reset();
|
||||
emit SongsDiscovered(songs);
|
||||
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() {
|
||||
if (!cdio_) return; // do nothing if not initialized
|
||||
|
||||
|
@ -96,3 +107,5 @@ void CddaDevice::CheckDiscChanged() {
|
|||
LoadSongs();
|
||||
}
|
||||
}
|
||||
|
||||
SongList CddaDevice::songs() const { return cdda_song_loader_.cached_tracks(); }
|
||||
|
|
|
@ -49,11 +49,10 @@ class CddaDevice : public ConnectedDevice {
|
|||
return false;
|
||||
}
|
||||
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.
|
||||
bool IsValid() const;
|
||||
void WatchForDiscChanges(bool watch);
|
||||
SongList songs() const;
|
||||
|
||||
static QStringList url_schemes() { return QStringList() << "cdda"; }
|
||||
|
||||
|
@ -74,6 +73,7 @@ class CddaDevice : public ConnectedDevice {
|
|||
|
||||
private slots:
|
||||
void SongsLoaded(const SongList& songs);
|
||||
void SongsLoadingFinished();
|
||||
void CheckDiscChanged();
|
||||
|
||||
private:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* This file is part of Clementine.
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -27,9 +28,21 @@
|
|||
#include "core/timeconstants.h"
|
||||
|
||||
CddaSongLoader::CddaSongLoader(const QUrl& url, QObject* parent)
|
||||
: QObject(parent), url_(url), cdda_(nullptr), may_load_(true) {
|
||||
connect(this, SIGNAL(MusicBrainzDiscIdLoaded(const QString&)),
|
||||
SLOT(LoadAudioCDTags(const QString&)));
|
||||
: QObject(parent), url_(url), cdda_(nullptr), may_load_(true), disc_() {
|
||||
connect(this, &CddaSongLoader::MusicBrainzDiscIdLoaded, this,
|
||||
&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() {
|
||||
|
@ -55,21 +68,99 @@ bool CddaSongLoader::IsActive() const { return loading_future_.isRunning(); }
|
|||
void CddaSongLoader::LoadSongs() {
|
||||
// only dispatch a new thread for loading tracks if not already running.
|
||||
if (!IsActive()) {
|
||||
QMutexLocker lock(&disc_mutex_);
|
||||
disc_ = Disc();
|
||||
loading_future_ =
|
||||
QtConcurrent::run(this, &CddaSongLoader::LoadSongsFromCdda);
|
||||
}
|
||||
}
|
||||
|
||||
void CddaSongLoader::LoadSongsFromCdda() {
|
||||
if (!may_load_) return;
|
||||
bool CddaSongLoader::ParseSongTags(SongList& songs, GstTagList* tags,
|
||||
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
|
||||
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) {
|
||||
qLog(Error) << error->code << QString::fromLocal8Bit(error->message);
|
||||
}
|
||||
if (cdda_ == nullptr) {
|
||||
emit SongsLoaded(initial_song_list);
|
||||
emit Finished();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -79,7 +170,7 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
|||
}
|
||||
if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda_),
|
||||
"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
|
||||
|
@ -89,21 +180,24 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
|||
GST_STATE_CHANGE_FAILURE) {
|
||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
|
||||
emit SongsLoaded(initial_song_list);
|
||||
emit Finished();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get number of tracks
|
||||
GstFormat fmt = gst_format_get_by_nick("track");
|
||||
GstFormat out_fmt = fmt;
|
||||
GstFormat track_fmt = gst_format_get_by_nick("track");
|
||||
gint64 num_tracks = 0;
|
||||
if (!gst_element_query_duration(cdda_, out_fmt, &num_tracks) ||
|
||||
out_fmt != fmt) {
|
||||
qLog(Error) << "Error while querying cdda GstElement";
|
||||
if (!gst_element_query_duration(cdda_, track_fmt, &num_tracks)) {
|
||||
qLog(Error) << "Error while querying cdda GstElement for track count";
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
|
||||
emit SongsLoaded(initial_song_list);
|
||||
emit Finished();
|
||||
return;
|
||||
}
|
||||
|
||||
SongList songs;
|
||||
for (int track_number = 1; track_number <= num_tracks; track_number++) {
|
||||
// Init song
|
||||
Song song;
|
||||
|
@ -113,15 +207,17 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
|||
song.set_url(GetUrlFromTrack(track_number));
|
||||
song.set_title(QString("Track %1").arg(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();
|
||||
|
||||
GstElement* pipeline = gst_pipeline_new("pipeline");
|
||||
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_set_state(pipeline, GST_STATE_READY);
|
||||
gst_element_set_state(pipeline, GST_STATE_PAUSED);
|
||||
|
@ -131,6 +227,7 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
|||
GstMessageType msg_filter =
|
||||
static_cast<GstMessageType>(GST_MESSAGE_TOC | GST_MESSAGE_TAG);
|
||||
QString musicbrainz_discid;
|
||||
bool loaded_cd_tags = false;
|
||||
while (may_load_ && msg_filter &&
|
||||
(msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline),
|
||||
10 * GST_SECOND, msg_filter))) {
|
||||
|
@ -140,7 +237,7 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
|||
gst_message_parse_toc(msg, &toc, nullptr);
|
||||
if (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;
|
||||
for (GList* node = entries; node != nullptr; node = node->next) {
|
||||
GstTocEntry* entry = static_cast<GstTocEntry*>(node->data);
|
||||
|
@ -148,35 +245,84 @@ void CddaSongLoader::LoadSongsFromCdda() {
|
|||
gint64 start, stop;
|
||||
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop))
|
||||
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>(
|
||||
static_cast<int>(msg_filter) ^ GST_MESSAGE_TOC);
|
||||
}
|
||||
gst_toc_unref(toc);
|
||||
}
|
||||
} 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;
|
||||
gst_message_parse_tag(msg, &tags);
|
||||
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)) {
|
||||
QString musicbrainz_discid = QString::fromUtf8(string_mb);
|
||||
musicbrainz_discid = QString::fromUtf8(string_mb);
|
||||
g_free(string_mb);
|
||||
|
||||
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) ^
|
||||
GST_MESSAGE_TAG);
|
||||
}
|
||||
gst_tag_list_free(tags);
|
||||
}
|
||||
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);
|
||||
// This will also cause cdda_ to be unref'd.
|
||||
gst_object_unref(pipeline);
|
||||
|
@ -187,36 +333,63 @@ void CddaSongLoader::LoadAudioCDTags(const QString& musicbrainz_discid) const {
|
|||
connect(musicbrainz_client,
|
||||
SIGNAL(Finished(const QString&, const QString&,
|
||||
MusicBrainzClient::ResultList)),
|
||||
SLOT(AudioCDTagsLoaded(const QString&, const QString&,
|
||||
MusicBrainzClient::ResultList)));
|
||||
SLOT(ProcessMusicBrainzResponse(const QString&, const QString&,
|
||||
MusicBrainzClient::ResultList)));
|
||||
|
||||
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
|
||||
}
|
||||
|
||||
void CddaSongLoader::AudioCDTagsLoaded(
|
||||
void CddaSongLoader::ProcessMusicBrainzResponse(
|
||||
const QString& artist, const QString& album,
|
||||
const MusicBrainzClient::ResultList& results) {
|
||||
MusicBrainzClient* musicbrainz_client =
|
||||
qobject_cast<MusicBrainzClient*>(sender());
|
||||
musicbrainz_client->deleteLater();
|
||||
SongList songs;
|
||||
if (results.empty()) return;
|
||||
int track_number = 1;
|
||||
for (const MusicBrainzClient::Result& ret : results) {
|
||||
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;
|
||||
if (results.empty()) {
|
||||
// no real update; signal that no further updates will follow now
|
||||
emit Finished();
|
||||
return;
|
||||
}
|
||||
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.
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -42,30 +43,74 @@ class CddaSongLoader : public QObject {
|
|||
~CddaSongLoader();
|
||||
|
||||
// Load songs.
|
||||
// Signals declared below will be emitted anytime new information will be
|
||||
// Signals declared below will be emitted anytime new information becomes
|
||||
// available.
|
||||
void LoadSongs();
|
||||
bool IsActive() const;
|
||||
|
||||
// The list of currently cached tracks. This gets updated when
|
||||
// LoadSongs() is called.
|
||||
SongList cached_tracks() const;
|
||||
|
||||
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);
|
||||
// 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);
|
||||
// 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);
|
||||
// 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);
|
||||
|
||||
private slots:
|
||||
void LoadAudioCDTags(const QString& musicbrainz_discid) const;
|
||||
void AudioCDTagsLoaded(const QString& artist, const QString& album,
|
||||
const MusicBrainzClient::ResultList& results);
|
||||
void ProcessMusicBrainzResponse(const QString& artist, const QString& album,
|
||||
const MusicBrainzClient::ResultList& results);
|
||||
void SetDiscTracks(const SongList& songs, bool has_titles);
|
||||
|
||||
private:
|
||||
QUrl GetUrlFromTrack(int track_number) const;
|
||||
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_;
|
||||
GstElement* cdda_;
|
||||
QFuture<void> loading_future_;
|
||||
std::atomic<bool> may_load_;
|
||||
Disc disc_;
|
||||
mutable QMutex disc_mutex_;
|
||||
};
|
||||
|
||||
#endif // CDDASONGLOADER_H
|
||||
|
|
|
@ -681,7 +681,7 @@ void DeviceManager::Forget(QModelIndex idx) {
|
|||
info->LoadIcon(info->BestBackend()->lister_->DeviceIcons(id),
|
||||
info->friendly_name_);
|
||||
|
||||
dataChanged(idx, idx);
|
||||
emit dataChanged(idx, idx);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ void Udisks2Lister::UnmountDevice(const QString& 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();
|
||||
device_data_.remove(id);
|
||||
DeviceRemoved(id);
|
||||
emit DeviceRemoved(id);
|
||||
}
|
||||
|
||||
QList<QDBusObjectPath> Udisks2Lister::GetMountedPartitionsFromDBusArgument(
|
||||
|
@ -297,7 +297,7 @@ void Udisks2Lister::HandleFinishedMountJob(
|
|||
<< " | Partition = " << partition_data.dbus_path;
|
||||
QWriteLocker locker(&device_data_lock_);
|
||||
device_data_[partition_data.unique_id()] = partition_data;
|
||||
DeviceAdded(partition_data.unique_id());
|
||||
emit DeviceAdded(partition_data.unique_id());
|
||||
}
|
||||
|
||||
void Udisks2Lister::HandleFinishedUnmountJob(
|
||||
|
@ -320,7 +320,7 @@ void Udisks2Lister::HandleFinishedUnmountJob(
|
|||
qLog(Debug) << "Partition " << partition_data.dbus_path
|
||||
<< " has no more mount points, removing it from device list";
|
||||
device_data_.remove(id);
|
||||
DeviceRemoved(id);
|
||||
emit DeviceRemoved(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,10 +39,6 @@
|
|||
#include "devices/cddadevice.h"
|
||||
#endif
|
||||
#include "internet/core/internetmodel.h"
|
||||
#ifdef HAVE_SPOTIFY
|
||||
#include "internet/spotify/spotifyserver.h"
|
||||
#include "internet/spotify/spotifyservice.h"
|
||||
#endif
|
||||
|
||||
const int GstEnginePipeline::kGstStateTimeoutNanosecs = 10000000;
|
||||
const int GstEnginePipeline::kFaderFudgeMsec = 2000;
|
||||
|
@ -171,58 +167,14 @@ QByteArray GstEnginePipeline::GstUriFromUrl(const QUrl& url) {
|
|||
|
||||
GstElement* GstEnginePipeline::CreateDecodeBinFromUrl(const QUrl& url) {
|
||||
GstElement* new_bin = nullptr;
|
||||
#ifdef HAVE_SPOTIFY
|
||||
if (url.scheme() == "spotify") {
|
||||
new_bin = gst_bin_new("spotify_bin");
|
||||
if (!new_bin) return nullptr;
|
||||
|
||||
// Create elements
|
||||
GstElement* src = engine_->CreateElement("tcpserversrc", new_bin);
|
||||
if (!src) {
|
||||
gst_object_unref(GST_OBJECT(new_bin));
|
||||
return nullptr;
|
||||
}
|
||||
GstElement* gdp = engine_->CreateElement("gdpdepay", new_bin);
|
||||
if (!gdp) {
|
||||
gst_object_unref(GST_OBJECT(new_bin));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Pick a port number
|
||||
const int port = Utilities::PickUnusedPort();
|
||||
g_object_set(G_OBJECT(src), "host", "127.0.0.1", nullptr);
|
||||
g_object_set(G_OBJECT(src), "port", port, nullptr);
|
||||
|
||||
// Link the elements
|
||||
gst_element_link(src, gdp);
|
||||
|
||||
// Add a ghost pad
|
||||
GstPad* pad = gst_element_get_static_pad(gdp, "src");
|
||||
gst_element_add_pad(GST_ELEMENT(new_bin), gst_ghost_pad_new("src", pad));
|
||||
gst_object_unref(GST_OBJECT(pad));
|
||||
|
||||
// Tell spotify to start sending data to us.
|
||||
SpotifyServer* spotify_server =
|
||||
InternetModel::Service<SpotifyService>()->server();
|
||||
// Need to schedule this in the spotify server's thread
|
||||
QMetaObject::invokeMethod(
|
||||
spotify_server, "StartPlayback", Qt::QueuedConnection,
|
||||
Q_ARG(QString, url.toString()), Q_ARG(quint16, port));
|
||||
} else {
|
||||
#endif
|
||||
QByteArray uri = GstUriFromUrl(url);
|
||||
new_bin = engine_->CreateElement("uridecodebin");
|
||||
if (!new_bin) return nullptr;
|
||||
g_object_set(G_OBJECT(new_bin), "uri", uri.constData(), nullptr);
|
||||
CHECKED_GCONNECT(G_OBJECT(new_bin), "drained", &SourceDrainedCallback,
|
||||
this);
|
||||
CHECKED_GCONNECT(G_OBJECT(new_bin), "pad-added", &NewPadCallback, this);
|
||||
CHECKED_GCONNECT(G_OBJECT(new_bin), "notify::source", &SourceSetupCallback,
|
||||
this);
|
||||
#ifdef HAVE_SPOTIFY
|
||||
}
|
||||
#endif
|
||||
|
||||
QByteArray uri = GstUriFromUrl(url);
|
||||
new_bin = engine_->CreateElement("uridecodebin");
|
||||
if (!new_bin) return nullptr;
|
||||
g_object_set(G_OBJECT(new_bin), "uri", uri.constData(), nullptr);
|
||||
CHECKED_GCONNECT(G_OBJECT(new_bin), "drained", &SourceDrainedCallback, this);
|
||||
CHECKED_GCONNECT(G_OBJECT(new_bin), "pad-added", &NewPadCallback, this);
|
||||
CHECKED_GCONNECT(G_OBJECT(new_bin), "notify::source", &SourceSetupCallback,
|
||||
this);
|
||||
return new_bin;
|
||||
}
|
||||
|
||||
|
@ -440,7 +392,7 @@ bool GstEnginePipeline::InitAudioBin() {
|
|||
gst_element_link(queue_, audioconvert_);
|
||||
|
||||
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_caps_unref(caps16);
|
||||
|
||||
|
@ -499,7 +451,7 @@ bool GstEnginePipeline::InitAudioBin() {
|
|||
gst_object_unref(pad);
|
||||
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
|
||||
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);
|
||||
|
||||
return true;
|
||||
|
@ -567,10 +519,11 @@ bool GstEnginePipeline::InitFromReq(const MediaPlaybackRequest& req,
|
|||
GstEnginePipeline::~GstEnginePipeline() {
|
||||
if (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_object_unref(bus);
|
||||
|
||||
g_source_remove(bus_cb_id_);
|
||||
gst_element_set_state(pipeline_, GST_STATE_NULL);
|
||||
|
||||
if (tee_) {
|
||||
|
@ -1198,26 +1151,6 @@ GstState GstEnginePipeline::state() const {
|
|||
}
|
||||
|
||||
QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(GstState state) {
|
||||
#ifdef HAVE_SPOTIFY
|
||||
if (current_.url_.scheme() == "spotify" && !buffering_) {
|
||||
const GstState current_state = this->state();
|
||||
|
||||
if (state == GST_STATE_PAUSED && current_state == GST_STATE_PLAYING) {
|
||||
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
|
||||
|
||||
// Need to schedule this in the spotify service's thread
|
||||
QMetaObject::invokeMethod(spotify, "SetPaused", Qt::QueuedConnection,
|
||||
Q_ARG(bool, true));
|
||||
} else if (state == GST_STATE_PLAYING &&
|
||||
current_state == GST_STATE_PAUSED) {
|
||||
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
|
||||
|
||||
// Need to schedule this in the spotify service's thread
|
||||
QMetaObject::invokeMethod(spotify, "SetPaused", Qt::QueuedConnection,
|
||||
Q_ARG(bool, false));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return ConcurrentRun::Run<GstStateChangeReturn, GstElement*, GstState>(
|
||||
&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
|
||||
}
|
||||
|
|
|
@ -294,8 +294,6 @@ class GstEnginePipeline : public GstPipelineBase {
|
|||
GstPad* tee_probe_pad_;
|
||||
GstPad* tee_audio_pad_;
|
||||
|
||||
uint bus_cb_id_;
|
||||
|
||||
QThreadPool set_state_threadpool_;
|
||||
|
||||
GstSegment last_decodebin_segment_;
|
||||
|
|
|
@ -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
|
|
@ -60,9 +60,6 @@
|
|||
#ifdef HAVE_SEAFILE
|
||||
#include "internet/seafile/seafileservice.h"
|
||||
#endif
|
||||
#ifdef HAVE_SPOTIFY
|
||||
#include "internet/spotify/spotifyservice.h"
|
||||
#endif
|
||||
|
||||
using smart_playlists::Generator;
|
||||
using smart_playlists::GeneratorMimeData;
|
||||
|
@ -98,9 +95,6 @@ InternetModel::InternetModel(Application* app, QObject* parent)
|
|||
AddService(new SomaFMService(app, this));
|
||||
AddService(new IntergalacticFMService(app, this));
|
||||
AddService(new RadioBrowserService(app, this));
|
||||
#ifdef HAVE_SPOTIFY
|
||||
AddService(new SpotifyService(app, this));
|
||||
#endif
|
||||
AddService(new SubsonicService(app, this));
|
||||
#ifdef HAVE_BOX
|
||||
AddService(new BoxService(app, this));
|
||||
|
|
|
@ -259,7 +259,7 @@ RockRadioService::RockRadioService(Application* app, InternetModel* model,
|
|||
: DigitallyImportedServiceBase(
|
||||
"RockRadio", "ROCKRADIO.com", QUrl("http://www.rockradio.com"),
|
||||
IconLoader::Load("rockradio", IconLoader::Provider), "rockradio", app,
|
||||
model, false, parent) {}
|
||||
model, true, parent) {}
|
||||
|
||||
ClassicalRadioService::ClassicalRadioService(Application* app,
|
||||
InternetModel* model,
|
||||
|
@ -268,4 +268,4 @@ ClassicalRadioService::ClassicalRadioService(Application* app,
|
|||
"ClassicalRadio", "ClassicalRadio.com",
|
||||
QUrl("http://www.classicalradio.com"),
|
||||
IconLoader::Load("digitallyimported", IconLoader::Provider),
|
||||
"classicalradio", app, model, false, parent) {}
|
||||
"classicalradio", app, model, true, parent) {}
|
||||
|
|
|
@ -41,16 +41,16 @@ class DigitallyImportedServiceBase : public InternetService {
|
|||
const QString& api_service_name,
|
||||
Application* app, InternetModel* model,
|
||||
bool has_premium, QObject* parent = nullptr);
|
||||
~DigitallyImportedServiceBase();
|
||||
~DigitallyImportedServiceBase() override;
|
||||
|
||||
static const char* kSettingsGroup;
|
||||
static const int kStreamsCacheDurationSecs;
|
||||
|
||||
QStandardItem* CreateRootItem();
|
||||
void LazyPopulate(QStandardItem* parent);
|
||||
void ShowContextMenu(const QPoint& global_pos);
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* parent) override;
|
||||
void ShowContextMenu(const QPoint& global_pos) override;
|
||||
|
||||
void ReloadSettings();
|
||||
void ReloadSettings() override;
|
||||
|
||||
bool is_premium_account() const;
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ UrlHandler::LoadResult DigitallyImportedUrlHandler::StartLoading(
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ void DigitallyImportedUrlHandler::LoadPlaylistFinished(QIODevice* device) {
|
|||
|
||||
// Failed to get playlist?
|
||||
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));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ class IcecastService : public InternetService {
|
|||
|
||||
public:
|
||||
IcecastService(Application* app, InternetModel* parent);
|
||||
~IcecastService();
|
||||
~IcecastService() override;
|
||||
|
||||
static const char* kServiceName;
|
||||
static const char* kDirectoryUrl;
|
||||
|
@ -51,12 +51,12 @@ class IcecastService : public InternetService {
|
|||
Type_Genre,
|
||||
};
|
||||
|
||||
QStandardItem* CreateRootItem();
|
||||
void LazyPopulate(QStandardItem* item);
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* item) override;
|
||||
|
||||
void ShowContextMenu(const QPoint& global_pos);
|
||||
void ShowContextMenu(const QPoint& global_pos) override;
|
||||
|
||||
QWidget* HeaderWidget() const;
|
||||
QWidget* HeaderWidget() const override;
|
||||
|
||||
private slots:
|
||||
void LoadDirectory();
|
||||
|
|
|
@ -39,7 +39,7 @@ class IntergalacticFMServiceBase : public InternetService {
|
|||
const QString& name, const QUrl& channel_list_url,
|
||||
const QUrl& homepage_url,
|
||||
const QUrl& donate_page_url, const QIcon& icon);
|
||||
~IntergalacticFMServiceBase();
|
||||
~IntergalacticFMServiceBase() override;
|
||||
|
||||
enum ItemType {
|
||||
Type_Stream = 2000,
|
||||
|
@ -59,14 +59,14 @@ class IntergalacticFMServiceBase : public InternetService {
|
|||
const QString& url_scheme() const { return url_scheme_; }
|
||||
const QIcon& icon() const { return icon_; }
|
||||
|
||||
QStandardItem* CreateRootItem();
|
||||
void LazyPopulate(QStandardItem* item);
|
||||
void ShowContextMenu(const QPoint& global_pos);
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* item) override;
|
||||
void ShowContextMenu(const QPoint& global_pos) override;
|
||||
|
||||
PlaylistItem::Options playlistitem_options() const;
|
||||
PlaylistItem::Options playlistitem_options() const override;
|
||||
QNetworkAccessManager* network() const { return network_; }
|
||||
|
||||
void ReloadSettings();
|
||||
void ReloadSettings() override;
|
||||
|
||||
bool IsStreamListStale() const { return streams_.IsStale(); }
|
||||
StreamList Streams();
|
||||
|
|
|
@ -34,7 +34,7 @@ class SavedRadio : public InternetService {
|
|||
|
||||
public:
|
||||
SavedRadio(Application* app, InternetModel* parent);
|
||||
~SavedRadio();
|
||||
~SavedRadio() override;
|
||||
|
||||
enum ItemType {
|
||||
Type_Stream = 2000,
|
||||
|
@ -57,10 +57,10 @@ class SavedRadio : public InternetService {
|
|||
static const char* kServiceName;
|
||||
static const char* kSettingsGroup;
|
||||
|
||||
QStandardItem* CreateRootItem();
|
||||
void LazyPopulate(QStandardItem* item);
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* item) override;
|
||||
|
||||
void ShowContextMenu(const QPoint& global_pos);
|
||||
void ShowContextMenu(const QPoint& global_pos) override;
|
||||
|
||||
void Add(const QUrl& url, const QString& name = QString(),
|
||||
const QUrl& url_logo = QUrl());
|
||||
|
|
|
@ -43,14 +43,14 @@ class JamendoService : public InternetService {
|
|||
|
||||
public:
|
||||
JamendoService(Application* app, InternetModel* parent);
|
||||
~JamendoService();
|
||||
~JamendoService() override;
|
||||
|
||||
QStandardItem* CreateRootItem();
|
||||
void LazyPopulate(QStandardItem* item);
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* item) override;
|
||||
|
||||
void ShowContextMenu(const QPoint& global_pos);
|
||||
void ShowContextMenu(const QPoint& global_pos) override;
|
||||
|
||||
QWidget* HeaderWidget() const;
|
||||
QWidget* HeaderWidget() const override;
|
||||
|
||||
LibraryBackend* library_backend() const { return library_backend_.get(); }
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ class MagnatuneService : public InternetService {
|
|||
|
||||
public:
|
||||
MagnatuneService(Application* app, InternetModel* parent);
|
||||
~MagnatuneService();
|
||||
~MagnatuneService() override;
|
||||
|
||||
// Values are saved in QSettings and are indices into the combo box in
|
||||
// MagnatuneConfig
|
||||
|
@ -71,14 +71,14 @@ class MagnatuneService : public InternetService {
|
|||
|
||||
static QString ReadElementText(QXmlStreamReader& reader);
|
||||
|
||||
QStandardItem* CreateRootItem();
|
||||
void LazyPopulate(QStandardItem* item);
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* item) override;
|
||||
|
||||
void ShowContextMenu(const QPoint& global_pos);
|
||||
void ShowContextMenu(const QPoint& global_pos) override;
|
||||
|
||||
QWidget* HeaderWidget() const;
|
||||
QWidget* HeaderWidget() const override;
|
||||
|
||||
void ReloadSettings();
|
||||
void ReloadSettings() override;
|
||||
|
||||
// Magnatune specific stuff
|
||||
MembershipType membership_type() const { return membership_; }
|
||||
|
@ -99,7 +99,7 @@ class MagnatuneService : public InternetService {
|
|||
|
||||
void Download();
|
||||
void Homepage();
|
||||
void ShowConfig();
|
||||
void ShowConfig() override;
|
||||
|
||||
private:
|
||||
void EnsureMenuCreated();
|
||||
|
|
|
@ -395,6 +395,20 @@ QStandardItem* PodcastService::CreatePodcastItem(const Podcast& podcast) {
|
|||
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(
|
||||
const PodcastEpisode& episode) {
|
||||
QStandardItem* item = new QStandardItem;
|
||||
|
@ -621,18 +635,7 @@ void PodcastService::SubscriptionAdded(const Podcast& podcast) {
|
|||
void PodcastService::SubscriptionRemoved(const Podcast& podcast) {
|
||||
QStandardItem* item = podcasts_by_database_id_.take(podcast.database_id());
|
||||
if (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 episode's row
|
||||
model_->removeRow(item->row());
|
||||
RemovePodcastItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -859,6 +862,6 @@ void PodcastService::ReloadPodcast(const Podcast& podcast) {
|
|||
}
|
||||
QStandardItem* item = podcasts_by_database_id_[podcast.database_id()];
|
||||
|
||||
model_->invisibleRootItem()->removeRow(item->row());
|
||||
RemovePodcastItem(item);
|
||||
model_->invisibleRootItem()->appendRow(CreatePodcastItem(podcast));
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ class PodcastService : public InternetService {
|
|||
|
||||
public:
|
||||
PodcastService(Application* app, InternetModel* parent);
|
||||
~PodcastService();
|
||||
~PodcastService() override;
|
||||
|
||||
static const char* kServiceName;
|
||||
static const char* kSettingsGroup;
|
||||
|
@ -57,12 +57,12 @@ class PodcastService : public InternetService {
|
|||
|
||||
enum Role { Role_Podcast = InternetModel::RoleCount, Role_Episode };
|
||||
|
||||
QStandardItem* CreateRootItem();
|
||||
void LazyPopulate(QStandardItem* parent);
|
||||
bool has_initial_load_settings() const { return true; }
|
||||
void ShowContextMenu(const QPoint& global_pos);
|
||||
void ReloadSettings();
|
||||
void InitialLoadSettings();
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* parent) override;
|
||||
bool has_initial_load_settings() const override { return true; }
|
||||
void ShowContextMenu(const QPoint& global_pos) override;
|
||||
void ReloadSettings() override;
|
||||
void InitialLoadSettings() override;
|
||||
// Called by SongLoader when the user adds a Podcast URL directly. Adds a
|
||||
// subscription to the podcast and displays it in the UI. If the QVariant
|
||||
// contains an OPML file then this displays it in the Add Podcast dialog.
|
||||
|
@ -81,7 +81,7 @@ class PodcastService : public InternetService {
|
|||
void DeleteDownloadedData();
|
||||
void SetNew();
|
||||
void SetListened();
|
||||
void ShowConfig();
|
||||
void ShowConfig() override;
|
||||
|
||||
void SubscriptionAdded(const Podcast& podcast);
|
||||
void SubscriptionRemoved(const Podcast& podcast);
|
||||
|
@ -119,6 +119,7 @@ class PodcastService : public InternetService {
|
|||
|
||||
QStandardItem* CreatePodcastItem(const Podcast& podcast);
|
||||
QStandardItem* CreatePodcastEpisodeItem(const PodcastEpisode& episode);
|
||||
void RemovePodcastItem(QStandardItem* item);
|
||||
|
||||
QModelIndex MapToMergedModel(const QModelIndex& index) const;
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class RadioBrowserService : public InternetService {
|
|||
|
||||
public:
|
||||
RadioBrowserService(Application* app, InternetModel* parent);
|
||||
~RadioBrowserService(){};
|
||||
~RadioBrowserService() override{};
|
||||
|
||||
enum ItemType {
|
||||
Type_Stream = 2000,
|
||||
|
|
|
@ -39,7 +39,7 @@ class SomaFMServiceBase : public InternetService {
|
|||
const QString& name, const QUrl& channel_list_url,
|
||||
const QUrl& homepage_url, const QUrl& donate_page_url,
|
||||
const QIcon& icon);
|
||||
~SomaFMServiceBase();
|
||||
~SomaFMServiceBase() override;
|
||||
|
||||
enum ItemType {
|
||||
Type_Stream = 2000,
|
||||
|
@ -59,14 +59,14 @@ class SomaFMServiceBase : public InternetService {
|
|||
const QString& url_scheme() const { return url_scheme_; }
|
||||
const QIcon& icon() const { return icon_; }
|
||||
|
||||
QStandardItem* CreateRootItem();
|
||||
void LazyPopulate(QStandardItem* item);
|
||||
void ShowContextMenu(const QPoint& global_pos);
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* item) override;
|
||||
void ShowContextMenu(const QPoint& global_pos) override;
|
||||
|
||||
PlaylistItem::Options playlistitem_options() const;
|
||||
PlaylistItem::Options playlistitem_options() const override;
|
||||
QNetworkAccessManager* network() const { return network_; }
|
||||
|
||||
void ReloadSettings();
|
||||
void ReloadSettings() override;
|
||||
|
||||
bool IsStreamListStale() const { return streams_.IsStale(); }
|
||||
StreamList Streams();
|
||||
|
|
|
@ -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,195 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, Tyler Rhodes <tyler.s.rhodes@gmail.com>
|
||||
Copyright 2011-2012, 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
|
||||
Copyright 2011-2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2011-2012, 2014, David Sansome <me@davidsansome.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef INTERNET_SPOTIFY_SPOTIFYSERVICE_H_
|
||||
#define INTERNET_SPOTIFY_SPOTIFYSERVICE_H_
|
||||
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
|
||||
#include "internet/core/internetmodel.h"
|
||||
#include "internet/core/internetservice.h"
|
||||
#include "spotifymessages.pb.h"
|
||||
|
||||
class Playlist;
|
||||
class SearchBoxWidget;
|
||||
class SpotifyServer;
|
||||
|
||||
class QMenu;
|
||||
|
||||
class SpotifyService : public InternetService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SpotifyService(Application* app, InternetModel* parent);
|
||||
~SpotifyService();
|
||||
|
||||
enum Type {
|
||||
Type_SearchResults = InternetModel::TypeCount,
|
||||
Type_StarredPlaylist,
|
||||
Type_InboxPlaylist,
|
||||
Type_Toplist,
|
||||
};
|
||||
|
||||
enum Role {
|
||||
Role_UserPlaylistIndex = InternetModel::RoleCount,
|
||||
};
|
||||
|
||||
// Values are persisted - don't change.
|
||||
enum LoginState {
|
||||
LoginState_LoggedIn = 1,
|
||||
LoginState_Banned = 2,
|
||||
LoginState_BadCredentials = 3,
|
||||
LoginState_NoPremium = 4,
|
||||
LoginState_OtherError = 5,
|
||||
LoginState_ReloginFailed = 6
|
||||
};
|
||||
|
||||
static const char* kServiceName;
|
||||
static const char* kSettingsGroup;
|
||||
static const char* kBlobDownloadUrl;
|
||||
static const int kSearchDelayMsec;
|
||||
|
||||
void ReloadSettings() override;
|
||||
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* parent) override;
|
||||
void ShowContextMenu(const QPoint& global_pos) override;
|
||||
void DropMimeData(const QMimeData* data, const QModelIndex& index) override;
|
||||
QList<QAction*> playlistitem_actions(const Song& song) override;
|
||||
PlaylistItem::Options playlistitem_options() const override;
|
||||
QWidget* HeaderWidget() const override;
|
||||
|
||||
void Logout();
|
||||
void Login(const QString& username, const QString& password);
|
||||
Q_INVOKABLE void LoadImage(const QString& id);
|
||||
Q_INVOKABLE void SetPaused(bool paused);
|
||||
|
||||
SpotifyServer* server() const;
|
||||
|
||||
bool IsBlobInstalled() const;
|
||||
void InstallBlob();
|
||||
|
||||
// Persisted in the settings and updated on each Login().
|
||||
LoginState login_state() const { return login_state_; }
|
||||
bool IsLoggedIn() const { return login_state_ == LoginState_LoggedIn; }
|
||||
|
||||
bool ConfigRequired() override { return !IsLoggedIn(); }
|
||||
|
||||
static void SongFromProtobuf(const cpb::spotify::Track& track, Song* song);
|
||||
|
||||
signals:
|
||||
void BlobStateChanged();
|
||||
void LoginFinished(bool success);
|
||||
void ImageLoaded(const QString& id, const QImage& image);
|
||||
|
||||
public slots:
|
||||
void Search(const QString& text, bool now = false);
|
||||
void ShowConfig() override;
|
||||
void RemoveCurrentFromPlaylist();
|
||||
|
||||
private:
|
||||
void StartBlobProcess();
|
||||
void FillPlaylist(
|
||||
QStandardItem* item,
|
||||
const google::protobuf::RepeatedPtrField<cpb::spotify::Track>& tracks);
|
||||
void FillPlaylist(QStandardItem* item,
|
||||
const cpb::spotify::LoadPlaylistResponse& response);
|
||||
void AddSongsToUserPlaylist(int playlist_index,
|
||||
const QList<QUrl>& songs_urls);
|
||||
void AddSongsToStarred(const QList<QUrl>& songs_urls);
|
||||
void EnsureMenuCreated();
|
||||
// Create a new "show config" action. The caller is responsible for deleting
|
||||
// the pointer (or adding it to menu or anything else that will take ownership
|
||||
// of it)
|
||||
QAction* GetNewShowConfigAction();
|
||||
void InitSearch();
|
||||
void ClearSearchResults();
|
||||
QStandardItem* PlaylistBySpotifyIndex(int index) const;
|
||||
bool DoPlaylistsDiffer(const cpb::spotify::Playlists& response) const;
|
||||
|
||||
private slots:
|
||||
void EnsureServerCreated(const QString& username = QString(),
|
||||
const QString& password = QString());
|
||||
void BlobProcessError(QProcess::ProcessError error);
|
||||
void LoginCompleted(bool success, const QString& error,
|
||||
cpb::spotify::LoginResponse_Error error_code);
|
||||
void AddCurrentSongToUserPlaylist(QAction* action);
|
||||
void AddCurrentSongToStarredPlaylist();
|
||||
void RemoveSongsFromUserPlaylist(int playlist_index,
|
||||
const QList<int>& songs_indices_to_remove);
|
||||
void RemoveSongsFromStarred(const QList<int>& songs_indices_to_remove);
|
||||
void PlaylistsUpdated(const cpb::spotify::Playlists& response);
|
||||
void InboxLoaded(const cpb::spotify::LoadPlaylistResponse& response);
|
||||
void StarredLoaded(const cpb::spotify::LoadPlaylistResponse& response);
|
||||
void UserPlaylistLoaded(const cpb::spotify::LoadPlaylistResponse& response);
|
||||
void SearchResults(const cpb::spotify::SearchResponse& response);
|
||||
void SyncPlaylistProgress(const cpb::spotify::SyncPlaylistProgress& progress);
|
||||
void ToplistLoaded(const cpb::spotify::BrowseToplistResponse& response);
|
||||
void GetCurrentSongUrlToShare() const;
|
||||
void GetCurrentPlaylistUrlToShare() const;
|
||||
|
||||
void DoSearch();
|
||||
|
||||
void SyncPlaylist();
|
||||
void BlobDownloadFinished();
|
||||
|
||||
private:
|
||||
SpotifyServer* server_;
|
||||
|
||||
QString system_blob_path_;
|
||||
QString local_blob_version_;
|
||||
QString local_blob_path_;
|
||||
QProcess* blob_process_;
|
||||
|
||||
QStandardItem* root_;
|
||||
QStandardItem* search_;
|
||||
QStandardItem* starred_;
|
||||
QStandardItem* inbox_;
|
||||
QStandardItem* toplist_;
|
||||
QList<QStandardItem*> playlists_;
|
||||
|
||||
int login_task_id_;
|
||||
QString pending_search_;
|
||||
|
||||
QMenu* playlist_context_menu_;
|
||||
QMenu* song_context_menu_;
|
||||
QAction* playlist_sync_action_;
|
||||
QAction* get_url_to_share_playlist_;
|
||||
QList<QAction*> playlistitem_actions_;
|
||||
QAction* remove_from_playlist_;
|
||||
QUrl current_song_url_;
|
||||
QUrl current_playlist_url_;
|
||||
|
||||
SearchBoxWidget* search_box_;
|
||||
|
||||
QTimer* search_delay_;
|
||||
|
||||
int inbox_sync_id_;
|
||||
int starred_sync_id_;
|
||||
QMap<int, int> playlist_sync_ids_;
|
||||
|
||||
LoginState login_state_;
|
||||
cpb::spotify::Bitrate bitrate_;
|
||||
bool volume_normalisation_;
|
||||
};
|
||||
|
||||
#endif // INTERNET_SPOTIFY_SPOTIFYSERVICE_H_
|
|
@ -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>
|
|
@ -530,6 +530,11 @@ void SubsonicLibraryScanner::OnGetAlbumFinished(QNetworkReply* reply) {
|
|||
|
||||
// Read song information
|
||||
while (reader.readNextStartElement()) {
|
||||
// skip multi-artist and multi-genre tags
|
||||
if ((reader.name() == "artists") || (reader.name() == "genres")) {
|
||||
reader.skipCurrentElement();
|
||||
continue;
|
||||
}
|
||||
if (reader.name() != "song") {
|
||||
ParsingError("song tag expected. Aborting scan.");
|
||||
return;
|
||||
|
|
|
@ -39,12 +39,10 @@ class SubsonicLibraryScanner;
|
|||
|
||||
class SubsonicService : public InternetService {
|
||||
Q_OBJECT
|
||||
Q_ENUMS(LoginState)
|
||||
Q_ENUMS(ApiError)
|
||||
|
||||
public:
|
||||
SubsonicService(Application* app, InternetModel* parent);
|
||||
~SubsonicService();
|
||||
~SubsonicService() override;
|
||||
|
||||
enum LoginState {
|
||||
LoginState_Loggedin,
|
||||
|
@ -63,6 +61,7 @@ class SubsonicService : public InternetService {
|
|||
LoginState_RedirectLimitExceeded,
|
||||
LoginState_RedirectNoUrl,
|
||||
};
|
||||
Q_ENUM(LoginState)
|
||||
|
||||
enum ApiError {
|
||||
ApiError_Generic = 0,
|
||||
|
@ -74,6 +73,7 @@ class SubsonicService : public InternetService {
|
|||
ApiError_Unlicensed = 60,
|
||||
ApiError_NotFound = 70,
|
||||
};
|
||||
Q_ENUM(ApiError)
|
||||
|
||||
enum Type {
|
||||
Type_Artist = InternetModel::TypeCount,
|
||||
|
@ -90,11 +90,11 @@ class SubsonicService : public InternetService {
|
|||
bool IsConfigured() const;
|
||||
bool IsAmpache() const;
|
||||
|
||||
QStandardItem* CreateRootItem();
|
||||
void LazyPopulate(QStandardItem* item);
|
||||
void ShowContextMenu(const QPoint& global_pos);
|
||||
QWidget* HeaderWidget() const;
|
||||
void ReloadSettings();
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* item) override;
|
||||
void ShowContextMenu(const QPoint& global_pos) override;
|
||||
QWidget* HeaderWidget() const override;
|
||||
void ReloadSettings() override;
|
||||
|
||||
void Login();
|
||||
void Login(const QString& server, const QString& username,
|
||||
|
@ -175,7 +175,7 @@ class SubsonicService : public InternetService {
|
|||
void OnLoginStateChanged(SubsonicService::LoginState newstate);
|
||||
void OnPingFinished(QNetworkReply* reply);
|
||||
|
||||
void ShowConfig();
|
||||
void ShowConfig() override;
|
||||
};
|
||||
|
||||
class SubsonicLibraryScanner : public QObject {
|
||||
|
@ -184,7 +184,7 @@ class SubsonicLibraryScanner : public QObject {
|
|||
public:
|
||||
explicit SubsonicLibraryScanner(SubsonicService* service,
|
||||
QObject* parent = nullptr);
|
||||
~SubsonicLibraryScanner();
|
||||
~SubsonicLibraryScanner() override;
|
||||
|
||||
void Scan();
|
||||
const SongList& GetSongs() const { return songs_; }
|
||||
|
|
|
@ -896,7 +896,7 @@ LibraryBackend::AlbumList LibraryBackend::GetAlbums(const QString& artist,
|
|||
QString last_artist;
|
||||
QString last_album_artist;
|
||||
while (query.Next()) {
|
||||
bool compilation = query.Value(3).toBool() | query.Value(4).toBool();
|
||||
bool compilation = query.Value(3).toBool() || query.Value(4).toBool();
|
||||
|
||||
Album info;
|
||||
info.artist = compilation ? QString() : query.Value(1).toString();
|
||||
|
|
|
@ -46,7 +46,6 @@ class QSettings;
|
|||
|
||||
class LibraryModel : public SimpleTreeModel<LibraryItem> {
|
||||
Q_OBJECT
|
||||
Q_ENUMS(GroupBy)
|
||||
|
||||
public:
|
||||
LibraryModel(std::shared_ptr<LibraryBackend> backend, Application* app,
|
||||
|
@ -91,6 +90,7 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
|
|||
GroupBy_OriginalYearAlbum = 13,
|
||||
GroupBy_OriginalYear = 14,
|
||||
};
|
||||
Q_ENUM(GroupBy)
|
||||
|
||||
struct Grouping {
|
||||
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();
|
||||
if (!last_selected_container_.isEmpty() &&
|
||||
last_selected_container_ == text) {
|
||||
emit expand(current);
|
||||
expand(current);
|
||||
setCurrentIndex(current);
|
||||
return true;
|
||||
} 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
|
||||
// wrong subtree
|
||||
// (happens with "unknown" all the time)
|
||||
if (!RestoreLevelFocus(current)) {
|
||||
emit collapse(current);
|
||||
collapse(current);
|
||||
} else {
|
||||
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
|
||||
// This is needed on Wayland for the main window to show the correct icon
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
|
||||
QGuiApplication::setDesktopFileName("clementine");
|
||||
QGuiApplication::setDesktopFileName("org.clementine_player.Clementine");
|
||||
#endif
|
||||
|
||||
// Resources
|
||||
|
|
|
@ -92,12 +92,10 @@ void MoodbarBuilder::Normalize(QList<Rgb>* vals, double Rgb::*member) {
|
|||
}
|
||||
|
||||
double avg = 0;
|
||||
int t = 0;
|
||||
for (const Rgb& rgb : *vals) {
|
||||
const double value = rgb.*member;
|
||||
if (value != mini && value != maxi) {
|
||||
avg += value / vals->count();
|
||||
t++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ QString Chromaprinter::CreateFingerprint() {
|
|||
// Chromaprint expects mono 16-bit ints at a sample rate of 11025Hz.
|
||||
GstCaps* caps = gst_caps_new_simple(
|
||||
"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_caps_unref(caps);
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ void MusicBrainzClient::DiscIdRequestFinished(const QString& discid,
|
|||
<< "Error:"
|
||||
<< reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()
|
||||
<< "http status code received";
|
||||
qLog(Error) << reply->readAll();
|
||||
if (reply->isOpen()) qLog(Error) << reply->readAll();
|
||||
emit Finished(artist, album, ret);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -50,9 +50,9 @@ void TagFetcher::StartFetch(const SongList& songs) {
|
|||
|
||||
QFuture<QString> future = QtConcurrent::mapped(songs_, GetFingerprint);
|
||||
fingerprint_watcher_ = new QFutureWatcher<QString>(this);
|
||||
fingerprint_watcher_->setFuture(future);
|
||||
connect(fingerprint_watcher_, SIGNAL(resultReadyAt(int)),
|
||||
SLOT(FingerprintFound(int)));
|
||||
fingerprint_watcher_->setFuture(future);
|
||||
|
||||
for (const Song& song : songs) {
|
||||
emit Progress(song, tr("Fingerprinting song"));
|
||||
|
|
|
@ -98,7 +98,6 @@ void OutgoingDataCreator::CheckEnabledProviders() {
|
|||
<< "lyricstime.com"
|
||||
<< "lyricsreg.com"
|
||||
<< "lyricsmania.com"
|
||||
<< "metrolyrics.com"
|
||||
<< "azlyrics.com"
|
||||
<< "songlyrics.com"
|
||||
<< "elyrics.net"
|
||||
|
@ -106,7 +105,6 @@ void OutgoingDataCreator::CheckEnabledProviders() {
|
|||
<< "lyrics.com"
|
||||
<< "lyricsbay.com"
|
||||
<< "directlyrics.com"
|
||||
<< "loudson.gs"
|
||||
<< "teksty.org"
|
||||
<< "tekstowo.pl (Polish translations)"
|
||||
<< "vagalume.uol.com.br"
|
||||
|
|
|
@ -53,16 +53,16 @@ SongSender::SongSender(Application* app, RemoteClient* client)
|
|||
}
|
||||
qLog(Debug) << "Transcoder preset" << transcoder_preset_.codec_mimetype_;
|
||||
|
||||
connect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)),
|
||||
SLOT(TranscodeJobComplete(QString, QString, bool)));
|
||||
connect(transcoder_, SIGNAL(JobComplete(QUrl, QString, bool)),
|
||||
SLOT(TranscodeJobComplete(QUrl, QString, bool)));
|
||||
connect(transcoder_, SIGNAL(AllJobsComplete()), SLOT(StartTransfer()));
|
||||
|
||||
total_transcode_ = 0;
|
||||
}
|
||||
|
||||
SongSender::~SongSender() {
|
||||
disconnect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)), this,
|
||||
SLOT(TranscodeJobComplete(QString, QString, bool)));
|
||||
disconnect(transcoder_, SIGNAL(JobComplete(QUrl, QString, bool)), this,
|
||||
SLOT(TranscodeJobComplete(QUrl, QString, bool)));
|
||||
disconnect(transcoder_, SIGNAL(AllJobsComplete()), this,
|
||||
SLOT(StartTransfer()));
|
||||
transcoder_->Cancel();
|
||||
|
@ -110,11 +110,11 @@ void SongSender::TranscodeLosslessFiles() {
|
|||
if (!item.song_.IsFileLossless()) continue;
|
||||
|
||||
// Add the file to the transcoder
|
||||
QString local_file = item.song_.url().toLocalFile();
|
||||
QUrl local_file = item.song_.url();
|
||||
|
||||
transcoder_->AddTemporaryJob(local_file, transcoder_preset_);
|
||||
|
||||
qLog(Debug) << "transcoding" << local_file;
|
||||
qLog(Debug) << "transcoding" << local_file.toLocalFile();
|
||||
total_transcode_++;
|
||||
}
|
||||
|
||||
|
@ -126,13 +126,14 @@ void SongSender::TranscodeLosslessFiles() {
|
|||
}
|
||||
}
|
||||
|
||||
void SongSender::TranscodeJobComplete(const QString& input,
|
||||
const QString& output, bool success) {
|
||||
qLog(Debug) << input << "transcoded to" << output << success;
|
||||
void SongSender::TranscodeJobComplete(const QUrl& input, const QString& output,
|
||||
bool success) {
|
||||
Q_ASSERT(input.isLocalFile()); // songsender only handles local files
|
||||
qLog(Debug) << input.toLocalFile() << "transcoded to" << output << success;
|
||||
|
||||
// If it wasn't successful send original file
|
||||
if (success) {
|
||||
transcoder_map_.insert(input, output);
|
||||
transcoder_map_.insert(input.toLocalFile(), output);
|
||||
}
|
||||
|
||||
SendTranscoderStatus();
|
||||
|
|
|
@ -34,7 +34,7 @@ class SongSender : public QObject {
|
|||
void ResponseSongOffer(bool accepted);
|
||||
|
||||
private slots:
|
||||
void TranscodeJobComplete(const QString& input, const QString& output,
|
||||
void TranscodeJobComplete(const QUrl& input, const QString& output,
|
||||
bool success);
|
||||
void StartTransfer();
|
||||
|
||||
|
|
|
@ -1729,13 +1729,11 @@ PlaylistItemList Playlist::RemoveItemsWithoutUndo(int row, int count) {
|
|||
endRemoveRows();
|
||||
|
||||
QList<int>::iterator it = virtual_items_.begin();
|
||||
int i = 0;
|
||||
while (it != virtual_items_.end()) {
|
||||
if (*it >= items_.count())
|
||||
it = virtual_items_.erase(it);
|
||||
else
|
||||
++it;
|
||||
++i;
|
||||
}
|
||||
|
||||
// Reset current_virtual_index_
|
||||
|
|
|
@ -242,7 +242,7 @@ void Queue::Clear() {
|
|||
}
|
||||
|
||||
void Queue::Move(const QList<int>& proxy_rows, int pos) {
|
||||
layoutAboutToBeChanged();
|
||||
emit layoutAboutToBeChanged();
|
||||
QList<QPersistentModelIndex> moved_items;
|
||||
|
||||
// Take the items out of the list first, keeping track of whether the
|
||||
|
@ -279,7 +279,7 @@ void Queue::Move(const QList<int>& proxy_rows, int pos) {
|
|||
}
|
||||
}
|
||||
|
||||
layoutChanged();
|
||||
emit layoutChanged();
|
||||
}
|
||||
|
||||
void Queue::MoveUp(int row) { Move(QList<int>() << row, row - 1); }
|
||||
|
@ -400,7 +400,7 @@ void Queue::Remove(QList<int>& proxy_rows) {
|
|||
std::stable_sort(proxy_rows.begin(), proxy_rows.end());
|
||||
|
||||
// reflects immediately changes in the playlist
|
||||
layoutAboutToBeChanged();
|
||||
emit layoutAboutToBeChanged();
|
||||
|
||||
int removed_rows = 0;
|
||||
for (int row : proxy_rows) {
|
||||
|
@ -412,5 +412,5 @@ void Queue::Remove(QList<int>& proxy_rows) {
|
|||
removed_rows++;
|
||||
}
|
||||
|
||||
layoutChanged();
|
||||
emit layoutChanged();
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ SongList XSPFParser::Load(QIODevice* device, const QString& playlist_path,
|
|||
}
|
||||
|
||||
Song XSPFParser::ParseTrack(QXmlStreamReader* reader, const QDir& dir) const {
|
||||
QString title, artist, album, location;
|
||||
QString art, title, artist, album, location;
|
||||
qint64 nanosec = -1;
|
||||
int track_num = -1;
|
||||
|
||||
|
@ -68,6 +68,8 @@ Song XSPFParser::ParseTrack(QXmlStreamReader* reader, const QDir& dir) const {
|
|||
artist = reader->readElementText();
|
||||
} else if (name == "album") {
|
||||
album = reader->readElementText();
|
||||
} else if (name == "image") {
|
||||
art = reader->readElementText();
|
||||
} else if (name == "duration") { // in milliseconds.
|
||||
const QString duration = reader->readElementText();
|
||||
bool ok = false;
|
||||
|
@ -82,8 +84,6 @@ Song XSPFParser::ParseTrack(QXmlStreamReader* reader, const QDir& dir) const {
|
|||
if (!ok || track_num < 1) {
|
||||
track_num = -1;
|
||||
}
|
||||
} else if (name == "image") {
|
||||
// TODO: Fetch album covers.
|
||||
} else if (name == "info") {
|
||||
// TODO: Do something with extra info?
|
||||
}
|
||||
|
@ -106,6 +106,7 @@ return_song:
|
|||
song.set_title(title);
|
||||
song.set_artist(artist);
|
||||
song.set_album(album);
|
||||
song.set_art_manual(art);
|
||||
song.set_length_nanosec(nanosec);
|
||||
song.set_track(track_num);
|
||||
return song;
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
#include "config.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/organiseformat.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "devices/cddadevice.h"
|
||||
#include "devices/cddasongloader.h"
|
||||
|
@ -49,10 +50,12 @@ const int kCheckboxColumn = 0;
|
|||
const int kTrackNumberColumn = 1;
|
||||
const int kTrackTitleColumn = 2;
|
||||
const int kTrackDurationColumn = 3;
|
||||
const int kTrackFilenamePreviewColumn = 4;
|
||||
} // namespace
|
||||
|
||||
const char* RipCDDialog::kSettingsGroup = "Transcoder";
|
||||
const int RipCDDialog::kMaxDestinationItems = 10;
|
||||
const int RipCDDialog::kTranscodingProgressIntervalMs = 500;
|
||||
|
||||
RipCDDialog::RipCDDialog(DeviceManager* device_manager, QWidget* parent)
|
||||
: QDialog(parent),
|
||||
|
@ -62,7 +65,7 @@ RipCDDialog::RipCDDialog(DeviceManager* device_manager, QWidget* parent)
|
|||
device_manager->FindDevicesByUrlSchemes(CddaDevice::url_schemes())),
|
||||
working_(false),
|
||||
cdda_device_(),
|
||||
loader_(nullptr) {
|
||||
transcoding_progress_timer_(this) {
|
||||
Q_ASSERT(device_manager);
|
||||
// Init
|
||||
ui_->setupUi(this);
|
||||
|
@ -73,7 +76,11 @@ RipCDDialog::RipCDDialog(DeviceManager* device_manager, QWidget* parent)
|
|||
ui_->tableWidget->horizontalHeader()->setSectionResizeMode(
|
||||
kTrackNumberColumn, QHeaderView::ResizeToContents);
|
||||
ui_->tableWidget->horizontalHeader()->setSectionResizeMode(
|
||||
kTrackTitleColumn, QHeaderView::Stretch);
|
||||
kTrackDurationColumn, QHeaderView::ResizeToContents);
|
||||
ui_->tableWidget->horizontalHeader()->setSectionResizeMode(
|
||||
kTrackTitleColumn, QHeaderView::ResizeToContents);
|
||||
ui_->tableWidget->horizontalHeader()->setSectionResizeMode(
|
||||
kTrackFilenamePreviewColumn, QHeaderView::Stretch);
|
||||
|
||||
// Add a rip button
|
||||
rip_button_ = ui_->button_box->addButton(tr("Start ripping"),
|
||||
|
@ -85,10 +92,9 @@ RipCDDialog::RipCDDialog(DeviceManager* device_manager, QWidget* parent)
|
|||
cancel_button_->hide();
|
||||
ui_->progress_group->hide();
|
||||
|
||||
rip_button_->setEnabled(false); // will be enabled by DeviceSelected if a
|
||||
// valid device is selected
|
||||
|
||||
InitializeDevices();
|
||||
rip_button_->setEnabled(
|
||||
false); // will be enabled by signal handlers if a valid device is
|
||||
// selected by user and a list of tracks is loaded
|
||||
|
||||
connect(ui_->select_all_button, SIGNAL(clicked()), SLOT(SelectAll()));
|
||||
connect(ui_->select_none_button, SIGNAL(clicked()), SLOT(SelectNone()));
|
||||
|
@ -100,6 +106,11 @@ RipCDDialog::RipCDDialog(DeviceManager* device_manager, QWidget* parent)
|
|||
connect(ui_->options, SIGNAL(clicked()), SLOT(Options()));
|
||||
connect(ui_->select, SIGNAL(clicked()), SLOT(AddDestination()));
|
||||
|
||||
connect(ui_->naming_group, SIGNAL(FormatStringChanged()),
|
||||
SLOT(FormatStringUpdated()));
|
||||
connect(ui_->naming_group, SIGNAL(OptionChanged()),
|
||||
SLOT(FormatStringUpdated()));
|
||||
|
||||
setWindowTitle(tr("Rip CD"));
|
||||
AddDestinationDirectory(QDir::homePath());
|
||||
|
||||
|
@ -117,14 +128,32 @@ RipCDDialog::RipCDDialog(DeviceManager* device_manager, QWidget* parent)
|
|||
s.beginGroup(kSettingsGroup);
|
||||
last_add_dir_ = s.value("last_add_dir", QDir::homePath()).toString();
|
||||
|
||||
QString last_output_format = s.value("last_output_format", "ogg").toString();
|
||||
QString last_output_format =
|
||||
s.value("last_output_format", "audio/x-vorbis").toString();
|
||||
qLog(Debug) << "last_output_format loaded: " << last_output_format;
|
||||
for (int i = 0; i < ui_->format->count(); ++i) {
|
||||
if (last_output_format ==
|
||||
ui_->format->itemData(i).value<TranscoderPreset>().extension_) {
|
||||
ui_->format->itemData(i).value<TranscoderPreset>().codec_mimetype_) {
|
||||
ui_->format->setCurrentIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
connect(ui_->format, SIGNAL(currentIndexChanged(int)),
|
||||
SLOT(UpdateFileNamePreviews()));
|
||||
|
||||
connect(ui_->artistLineEdit, SIGNAL(textEdited(const QString&)),
|
||||
SLOT(UpdateMetadataFromGUI()));
|
||||
connect(ui_->albumLineEdit, SIGNAL(textEdited(const QString&)),
|
||||
SLOT(UpdateMetadataFromGUI()));
|
||||
connect(ui_->genreLineEdit, SIGNAL(textEdited(const QString&)),
|
||||
SLOT(UpdateMetadataFromGUI()));
|
||||
connect(ui_->yearLineEdit, SIGNAL(textEdited(const QString&)),
|
||||
SLOT(YearEditChanged(const QString&)));
|
||||
connect(ui_->discLineEdit, SIGNAL(textEdited(const QString&)),
|
||||
SLOT(DiscEditChanged(const QString&)));
|
||||
|
||||
InitializeDevices();
|
||||
}
|
||||
|
||||
RipCDDialog::~RipCDDialog() {}
|
||||
|
@ -176,17 +205,28 @@ void RipCDDialog::InitializeDevices() {
|
|||
void RipCDDialog::ClickedRipButton() {
|
||||
Q_ASSERT(cdda_device_);
|
||||
|
||||
OrganiseFormat format = ui_->naming_group->format();
|
||||
Q_ASSERT(format.IsValid());
|
||||
|
||||
QFileInfo path(
|
||||
ui_->destination->itemData(ui_->destination->currentIndex()).toString());
|
||||
|
||||
// create and connect Ripper instance for this task
|
||||
Ripper* ripper = new Ripper(cdda_device_->raw_cdio(), this);
|
||||
Ripper* ripper = new Ripper(cdda_device_->song_count(), this);
|
||||
|
||||
connect(cancel_button_, SIGNAL(clicked()), ripper, SLOT(Cancel()));
|
||||
|
||||
connect(ripper, &Ripper::Finished, this,
|
||||
[this, ripper]() { this->Finished(ripper); });
|
||||
connect(ripper, &Ripper::Cancelled, this,
|
||||
[this, ripper]() { this->Cancelled(ripper); });
|
||||
connect(ripper, SIGNAL(ProgressInterval(int, int)),
|
||||
SLOT(SetupProgressBarLimits(int, int)));
|
||||
connect(ripper, SIGNAL(Progress(int)), SLOT(UpdateProgressBar(int)));
|
||||
connect(ripper, &Ripper::Finished, this, [this, ripper]() {
|
||||
this->Finished(ripper, /*progress_to_display = */ 1.0f);
|
||||
});
|
||||
connect(ripper, &Ripper::Cancelled, this, [this, ripper]() {
|
||||
this->Finished(ripper, /*progress_to_display = */ 0.0f);
|
||||
});
|
||||
|
||||
ui_->progress_bar->setRange(0, 100);
|
||||
transcoding_progress_timer_connection_ =
|
||||
connect(&transcoding_progress_timer_, &QTimer::timeout, this,
|
||||
[this, ripper]() { this->TranscodingProgressTimeout(ripper); });
|
||||
|
||||
// Add tracks and album information to the ripper.
|
||||
ripper->ClearTracks();
|
||||
|
@ -196,11 +236,13 @@ void RipCDDialog::ClickedRipButton() {
|
|||
if (!checkboxes_.value(i - 1)->isChecked()) {
|
||||
continue;
|
||||
}
|
||||
QString transcoded_filename = GetOutputFileName(
|
||||
ParseFileFormatString(ui_->format_filename->text(), i));
|
||||
QString title = track_names_.value(i - 1)->text();
|
||||
ripper->AddTrack(i, title, transcoded_filename, preset);
|
||||
Song& song = songs_[i - 1];
|
||||
QString transcoded_filename = format.GetFilenameForSong(
|
||||
song, preset, /*prefix_path=*/path.filePath());
|
||||
ripper->AddTrack(i, song.title(), transcoded_filename, preset,
|
||||
ui_->naming_group->overwrite_existing());
|
||||
}
|
||||
|
||||
ripper->SetAlbumInformation(
|
||||
ui_->albumLineEdit->text(), ui_->artistLineEdit->text(),
|
||||
ui_->genreLineEdit->text(), ui_->yearLineEdit->text().toInt(),
|
||||
|
@ -208,6 +250,15 @@ void RipCDDialog::ClickedRipButton() {
|
|||
|
||||
SetWorking(true);
|
||||
ripper->Start();
|
||||
transcoding_progress_timer_.start(kTranscodingProgressIntervalMs);
|
||||
|
||||
// store settings
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("last_output_format", preset.codec_mimetype_);
|
||||
qLog(Debug) << "last_output_format stored: " << preset.codec_mimetype_;
|
||||
|
||||
ui_->naming_group->StoreSettings();
|
||||
}
|
||||
|
||||
void RipCDDialog::Options() {
|
||||
|
@ -270,11 +321,11 @@ void RipCDDialog::InvertSelection() {
|
|||
}
|
||||
|
||||
void RipCDDialog::DeviceSelected(int device_index) {
|
||||
// disconnecting from previous loader and device, if any
|
||||
if (loader_) disconnect(loader_, nullptr, this, nullptr);
|
||||
// disconnecting from previous device, if any
|
||||
if (cdda_device_) disconnect(cdda_device_.get(), nullptr, this, nullptr);
|
||||
|
||||
ResetDialog();
|
||||
EnableIfPossible();
|
||||
if (device_index < 0)
|
||||
return; // Invalid selection, probably no devices around
|
||||
|
||||
|
@ -292,49 +343,43 @@ void RipCDDialog::DeviceSelected(int device_index) {
|
|||
return;
|
||||
}
|
||||
|
||||
SongList songs = cdda_device_->songs();
|
||||
SongsLoaded(songs);
|
||||
|
||||
connect(cdda_device_.get(), SIGNAL(DiscChanged()), SLOT(DiscChanged()));
|
||||
|
||||
// get SongLoader from device and connect signals
|
||||
loader_ = cdda_device_->loader();
|
||||
Q_ASSERT(loader_);
|
||||
|
||||
connect(loader_, SIGNAL(SongsDurationLoaded(SongList)),
|
||||
SLOT(BuildTrackListTable(SongList)));
|
||||
connect(loader_, SIGNAL(SongsMetadataLoaded(SongList)),
|
||||
SLOT(UpdateTrackListTable(SongList)));
|
||||
connect(loader_, SIGNAL(SongsMetadataLoaded(SongList)),
|
||||
SLOT(AddAlbumMetadataFromMusicBrainz(SongList)));
|
||||
|
||||
// load songs from new SongLoader
|
||||
loader_->LoadSongs();
|
||||
rip_button_->setEnabled(true);
|
||||
connect(cdda_device_.get(), SIGNAL(SongsDiscovered(SongList)),
|
||||
SLOT(SongsLoaded(SongList)));
|
||||
}
|
||||
|
||||
void RipCDDialog::Finished(Ripper* ripper) {
|
||||
void RipCDDialog::Finished(Ripper* ripper, float progress_to_display) {
|
||||
SetWorking(false);
|
||||
ripper->deleteLater();
|
||||
}
|
||||
transcoding_progress_timer_.stop();
|
||||
disconnect(transcoding_progress_timer_connection_);
|
||||
|
||||
void RipCDDialog::Cancelled(Ripper* ripper) {
|
||||
ui_->progress_bar->setValue(0);
|
||||
Finished(ripper);
|
||||
}
|
||||
|
||||
void RipCDDialog::SetupProgressBarLimits(int min, int max) {
|
||||
ui_->progress_bar->setRange(min, max);
|
||||
}
|
||||
|
||||
void RipCDDialog::UpdateProgressBar(int progress) {
|
||||
int progress = qBound(0, static_cast<int>(progress_to_display * 100.0f), 100);
|
||||
ui_->progress_bar->setValue(progress);
|
||||
}
|
||||
|
||||
void RipCDDialog::BuildTrackListTable(const SongList& songs) {
|
||||
checkboxes_.clear();
|
||||
track_names_.clear();
|
||||
void RipCDDialog::SongsLoaded(const SongList& songs) {
|
||||
if (songs_.isEmpty() || songs_.length() == songs.length()) {
|
||||
songs_ = songs;
|
||||
UpdateTrackListTable();
|
||||
UpdateMetadataEdits();
|
||||
} else {
|
||||
qLog(Error) << "Number of tracks in metadata does not match number of "
|
||||
"songs on disc!";
|
||||
}
|
||||
EnableIfPossible();
|
||||
}
|
||||
|
||||
ui_->tableWidget->setRowCount(songs.length());
|
||||
void RipCDDialog::UpdateTrackListTable() {
|
||||
checkboxes_.clear();
|
||||
|
||||
ui_->tableWidget->clear();
|
||||
ui_->tableWidget->setRowCount(songs_.length());
|
||||
int current_row = 0;
|
||||
for (const Song& song : songs) {
|
||||
for (const Song& song : songs_) {
|
||||
QCheckBox* checkbox = new QCheckBox(ui_->tableWidget);
|
||||
checkbox->setCheckState(Qt::Checked);
|
||||
checkboxes_.append(checkbox);
|
||||
|
@ -343,31 +388,49 @@ void RipCDDialog::BuildTrackListTable(const SongList& songs) {
|
|||
new QLabel(QString::number(song.track())));
|
||||
QLineEdit* line_edit_track_title =
|
||||
new QLineEdit(song.title(), ui_->tableWidget);
|
||||
track_names_.append(line_edit_track_title);
|
||||
connect(line_edit_track_title, &QLineEdit::textChanged,
|
||||
[this, current_row](const QString& text) {
|
||||
songs_[current_row].set_title(text);
|
||||
UpdateFileNamePreviews();
|
||||
});
|
||||
ui_->tableWidget->setCellWidget(current_row, kTrackTitleColumn,
|
||||
line_edit_track_title);
|
||||
ui_->tableWidget->setCellWidget(current_row, kTrackDurationColumn,
|
||||
new QLabel(song.PrettyLength()));
|
||||
current_row++;
|
||||
}
|
||||
UpdateFileNamePreviews();
|
||||
}
|
||||
|
||||
void RipCDDialog::UpdateTrackListTable(const SongList& songs) {
|
||||
if (track_names_.length() == songs.length()) {
|
||||
BuildTrackListTable(songs);
|
||||
} else {
|
||||
qLog(Error) << "Number of tracks in metadata does not match number of "
|
||||
"songs on disc!";
|
||||
void RipCDDialog::UpdateFileNamePreviews() {
|
||||
OrganiseFormat format = ui_->naming_group->format();
|
||||
TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex())
|
||||
.value<TranscoderPreset>();
|
||||
|
||||
int current_row = 0;
|
||||
for (const Song& song : songs_) {
|
||||
if (format.IsValid())
|
||||
ui_->tableWidget->setCellWidget(
|
||||
current_row, kTrackFilenamePreviewColumn,
|
||||
new QLabel(format.GetFilenameForSong(song, preset)));
|
||||
else
|
||||
ui_->tableWidget->setCellWidget(current_row, kTrackFilenamePreviewColumn,
|
||||
new QLabel(tr("Invalid format")));
|
||||
current_row++;
|
||||
}
|
||||
}
|
||||
|
||||
void RipCDDialog::AddAlbumMetadataFromMusicBrainz(const SongList& songs) {
|
||||
Q_ASSERT(songs.length() > 0);
|
||||
void RipCDDialog::UpdateMetadataEdits() {
|
||||
if (songs_.length() <= 0) return;
|
||||
|
||||
const Song& song = songs.first();
|
||||
const Song& song = songs_.first();
|
||||
ui_->albumLineEdit->setText(song.album());
|
||||
ui_->artistLineEdit->setText(song.artist());
|
||||
ui_->yearLineEdit->setText(QString::number(song.year()));
|
||||
if (!song.artist().isEmpty())
|
||||
ui_->artistLineEdit->setText(song.artist());
|
||||
else
|
||||
ui_->artistLineEdit->setText(song.albumartist());
|
||||
ui_->yearLineEdit->setText(song.PrettyYear());
|
||||
ui_->genreLineEdit->setText(song.genre());
|
||||
}
|
||||
|
||||
void RipCDDialog::DiscChanged() { ResetDialog(); }
|
||||
|
@ -382,31 +445,8 @@ void RipCDDialog::SetWorking(bool working) {
|
|||
ui_->progress_group->setVisible(true);
|
||||
}
|
||||
|
||||
QString RipCDDialog::GetOutputFileName(const QString& basename) const {
|
||||
QFileInfo path(
|
||||
ui_->destination->itemData(ui_->destination->currentIndex()).toString());
|
||||
QString extension = ui_->format->itemData(ui_->format->currentIndex())
|
||||
.value<TranscoderPreset>()
|
||||
.extension_;
|
||||
return path.filePath() + '/' + basename + '.' + extension;
|
||||
}
|
||||
|
||||
QString RipCDDialog::ParseFileFormatString(const QString& file_format,
|
||||
int track_no) const {
|
||||
QString to_return = file_format;
|
||||
to_return.replace(QString("%artist"), ui_->artistLineEdit->text());
|
||||
to_return.replace(QString("%album"), ui_->albumLineEdit->text());
|
||||
to_return.replace(QString("%disc"), ui_->discLineEdit->text());
|
||||
to_return.replace(QString("%genre"), ui_->genreLineEdit->text());
|
||||
to_return.replace(QString("%year"), ui_->yearLineEdit->text());
|
||||
to_return.replace(QString("%title"),
|
||||
track_names_.value(track_no - 1)->text());
|
||||
to_return.replace(QString("%track"), QString::number(track_no));
|
||||
|
||||
return to_return;
|
||||
}
|
||||
|
||||
void RipCDDialog::ResetDialog() {
|
||||
songs_.clear();
|
||||
ui_->tableWidget->setRowCount(0);
|
||||
ui_->albumLineEdit->clear();
|
||||
ui_->artistLineEdit->clear();
|
||||
|
@ -414,3 +454,87 @@ void RipCDDialog::ResetDialog() {
|
|||
ui_->yearLineEdit->clear();
|
||||
ui_->discLineEdit->clear();
|
||||
}
|
||||
|
||||
void RipCDDialog::FormatStringUpdated() {
|
||||
UpdateFileNamePreviews();
|
||||
EnableIfPossible();
|
||||
}
|
||||
|
||||
void RipCDDialog::EnableIfPossible() {
|
||||
bool disc_ok;
|
||||
ui_->discLineEdit->text().toInt(&disc_ok);
|
||||
disc_ok |= ui_->discLineEdit->text().isEmpty();
|
||||
|
||||
bool year_ok;
|
||||
ui_->yearLineEdit->text().toInt(&year_ok);
|
||||
year_ok |= ui_->yearLineEdit->text().isEmpty();
|
||||
|
||||
rip_button_->setEnabled(!songs_.isEmpty() &&
|
||||
ui_->naming_group->format().IsValid() && disc_ok &&
|
||||
year_ok);
|
||||
}
|
||||
|
||||
void RipCDDialog::DiscEditChanged(const QString& disc_string) {
|
||||
bool disc_ok = false;
|
||||
disc_string.toInt(&disc_ok);
|
||||
|
||||
bool is_valid = disc_string.isEmpty() || disc_ok;
|
||||
|
||||
QString style;
|
||||
if (!is_valid) {
|
||||
style = "color: red;";
|
||||
} else {
|
||||
UpdateMetadataFromGUI();
|
||||
}
|
||||
ui_->discLineEdit->setStyleSheet(style);
|
||||
EnableIfPossible();
|
||||
}
|
||||
|
||||
void RipCDDialog::YearEditChanged(const QString& year_string) {
|
||||
bool year_ok = false;
|
||||
year_string.toInt(&year_ok);
|
||||
|
||||
bool is_valid = year_string.isEmpty() || year_ok;
|
||||
|
||||
QString style;
|
||||
if (!is_valid) {
|
||||
style = "color: red;";
|
||||
} else {
|
||||
UpdateMetadataFromGUI();
|
||||
}
|
||||
ui_->yearLineEdit->setStyleSheet(style);
|
||||
EnableIfPossible();
|
||||
}
|
||||
|
||||
void RipCDDialog::UpdateMetadataFromGUI() {
|
||||
QString artist = ui_->artistLineEdit->text();
|
||||
QString album = ui_->albumLineEdit->text();
|
||||
QString genre = ui_->genreLineEdit->text();
|
||||
bool disc_ok = false;
|
||||
int disc = ui_->discLineEdit->text().toInt(&disc_ok);
|
||||
bool year_ok = false;
|
||||
int year = ui_->yearLineEdit->text().toInt(&year_ok);
|
||||
|
||||
for (Song& song : songs_) {
|
||||
song.set_artist(artist);
|
||||
song.set_album(album);
|
||||
song.set_genre(genre);
|
||||
if (disc_ok)
|
||||
song.set_disc(disc);
|
||||
else
|
||||
song.set_disc(-1);
|
||||
if (year_ok)
|
||||
song.set_year(year);
|
||||
else
|
||||
song.set_year(-1);
|
||||
}
|
||||
UpdateFileNamePreviews();
|
||||
}
|
||||
|
||||
void RipCDDialog::TranscodingProgressTimeout(Ripper* ripper) {
|
||||
if (working_) {
|
||||
int progress =
|
||||
qBound(0, static_cast<int>(ripper->GetProgress() * 100.0f), 100);
|
||||
ui_->progress_bar->setValue(progress);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#define SRC_RIPPER_RIPCDDIALOG_H_
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTimer>
|
||||
#include <memory>
|
||||
|
||||
#include "core/song.h"
|
||||
|
@ -29,7 +30,6 @@ class QCloseEvent;
|
|||
class QLineEdit;
|
||||
class QShowEvent;
|
||||
|
||||
class CddaSongLoader;
|
||||
class Ripper;
|
||||
class Ui_RipCDDialog;
|
||||
class CddaDevice;
|
||||
|
@ -56,35 +56,30 @@ class RipCDDialog : public QDialog {
|
|||
void SelectNone();
|
||||
void InvertSelection();
|
||||
void DeviceSelected(int device_index);
|
||||
void Finished(Ripper* ripper);
|
||||
void Cancelled(Ripper* ripper);
|
||||
void SetupProgressBarLimits(int min, int max);
|
||||
void UpdateProgressBar(int progress);
|
||||
// Initializes track list table based on preliminary song list with durations
|
||||
// but without metadata.
|
||||
void BuildTrackListTable(const SongList& songs);
|
||||
// Update track list based on metadata.
|
||||
void UpdateTrackListTable(const SongList& songs);
|
||||
// Update album information with metadata.
|
||||
void AddAlbumMetadataFromMusicBrainz(const SongList& songs);
|
||||
void Finished(Ripper* ripper, float progress_to_display);
|
||||
void SongsLoaded(const SongList& songs);
|
||||
void DiscChanged();
|
||||
void FormatStringUpdated();
|
||||
void UpdateFileNamePreviews();
|
||||
void DiscEditChanged(const QString& disc_string);
|
||||
void YearEditChanged(const QString& year_string);
|
||||
void UpdateMetadataEdits();
|
||||
void UpdateMetadataFromGUI();
|
||||
void TranscodingProgressTimeout(Ripper* ripper);
|
||||
|
||||
private:
|
||||
static const char* kSettingsGroup;
|
||||
static const int kMaxDestinationItems;
|
||||
static const int kTranscodingProgressIntervalMs;
|
||||
|
||||
// Constructs a filename from the given base name with a path taken
|
||||
// from the ui dialog and an extension that corresponds to the audio
|
||||
// format chosen in the ui.
|
||||
void AddDestinationDirectory(QString dir);
|
||||
QString GetOutputFileName(const QString& basename) const;
|
||||
QString ParseFileFormatString(const QString& file_format, int track_no) const;
|
||||
void SetWorking(bool working);
|
||||
void ResetDialog();
|
||||
void InitializeDevices();
|
||||
void EnableIfPossible();
|
||||
void UpdateTrackListTable();
|
||||
|
||||
QList<QCheckBox*> checkboxes_;
|
||||
QList<QLineEdit*> track_names_;
|
||||
QString last_add_dir_;
|
||||
QPushButton* cancel_button_;
|
||||
QPushButton* close_button_;
|
||||
|
@ -94,6 +89,8 @@ class RipCDDialog : public QDialog {
|
|||
QList<DeviceInfo*> cdda_devices_;
|
||||
bool working_;
|
||||
std::shared_ptr<CddaDevice> cdda_device_;
|
||||
CddaSongLoader* loader_;
|
||||
SongList songs_;
|
||||
QTimer transcoding_progress_timer_;
|
||||
QMetaObject::Connection transcoding_progress_timer_connection_;
|
||||
};
|
||||
#endif // SRC_RIPPER_RIPCDDIALOG_H_
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue