Compare commits
230 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 |
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -66,7 +66,6 @@ GSTREAMER_PLUGINS = [
|
|||
'libgstisomp4.dylib',
|
||||
'libgstlame.dylib',
|
||||
'libgstlibav.dylib',
|
||||
'libgstmms.dylib',
|
||||
# TODO: Bring back Musepack support.
|
||||
'libgstogg.dylib',
|
||||
'libgstopus.dylib',
|
||||
|
|
|
@ -15,6 +15,7 @@ GenericName[hi]=क्लेमेंटैन् संगीत वादक
|
|||
GenericName[is]=Clementine tónlistarspilarinn
|
||||
GenericName[pl]=Odtwarzacz muzyki Clementine
|
||||
GenericName[pt]=Reprodutor de músicas Clementine
|
||||
GenericName[ru]=Музыкальный проигрыватель Clementine
|
||||
GenericName[sl]=Predvajalnik glasbe Clementine
|
||||
GenericName[sr]=Клементина музички плејер
|
||||
GenericName[sr@ijekavian]=Клементина музички плејер
|
||||
|
@ -26,6 +27,7 @@ Comment[es]=Reproducción de música y flujos de Last.fm
|
|||
Comment[is]=Spilar tónlist og streymi frá last.fm
|
||||
Comment[pl]=Odtwarzanie muzyki i strumieni last.fm
|
||||
Comment[pt]=Reprodução de músicas e emissões last.fm
|
||||
Comment[ru]=Прослушивание музыки и потоков last.fm
|
||||
Comment[sl]=Predvaja glasbo in pretoke last.fm
|
||||
Comment[sr]=Репродукује музику и last.fm токове
|
||||
Comment[sr@ijekavian]=Репродукује музику и last.fm токове
|
||||
|
@ -38,106 +40,58 @@ Terminal=false
|
|||
Categories=AudioVideo;Player;Qt;Audio;
|
||||
StartupNotify=false
|
||||
MimeType=application/ogg;application/x-ogg;application/x-ogm-audio;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/ogg;audio/vnd.rn-realaudio;audio/vorbis;audio/x-flac;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-oggflac;audio/x-pn-realaudio;audio/x-scpls;audio/x-speex;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-wav;video/x-ms-asf;x-content/audio-player;x-scheme-handler/zune;x-scheme-handler/itpc;x-scheme-handler/itms;x-scheme-handler/feed;
|
||||
Actions=Play;Pause;Stop;StopAfterCurrent;Previous;Next;
|
||||
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;
|
||||
|
||||
[Desktop Action Play]
|
||||
Name=Play
|
||||
Exec=clementine --play
|
||||
Name[af]=Speel
|
||||
Name[be]=Прайграць
|
||||
Name[bg]=Възпроизвеждане
|
||||
Name[br]=Lenn
|
||||
Name[ca]=Reprodueix
|
||||
Name[cs]=Přehrát
|
||||
Name[da]=Afspil
|
||||
Name[de]=Wiedergabe
|
||||
Name[el]=Αναπαραγωγή
|
||||
Name[es]=Reproducir
|
||||
Name[et]=Mängi
|
||||
Name[eu]=Erreproduzitu
|
||||
Name[fa]=پخش
|
||||
Name[fi]=Toista
|
||||
Name[fr]=Lecture
|
||||
Name[ga]=Seinn
|
||||
Name[gl]=Reproducir
|
||||
Name[he]=נגינה
|
||||
Name[hi]=गाना बजाएं
|
||||
Name[hr]=Pokreni reprodukciju
|
||||
Name[hu]=Lejátszás
|
||||
Name[is]=Spila
|
||||
Name[it]=Riproduci
|
||||
Name[ja]=再生
|
||||
Name[kk]=Ойнату
|
||||
Name[lt]=Groti
|
||||
Name[lv]=Atskaņot
|
||||
Name[ms]=Mainkan
|
||||
Name[nb]=Spill
|
||||
Name[nl]=Afspelen
|
||||
Name[oc]=Lectura
|
||||
Name[pl]=Odtwarzaj
|
||||
Name[pt]=Reproduzir
|
||||
Name[pt_BR]=Reproduzir
|
||||
Name[ro]=Redă
|
||||
Name[ru]=Воспроизвести
|
||||
Name[sk]=Hrať
|
||||
Name[sl]=Predvajaj
|
||||
Name[sr]=Пусти
|
||||
Name[sr@ijekavian]=Пусти
|
||||
Name[sr@ijekavianlatin]=Pusti
|
||||
Name[sr@latin]=Pusti
|
||||
Name[sv]=Spela upp
|
||||
Name[tr]=Çal
|
||||
Name[uk]=Відтворити
|
||||
Name[vi]=Phát
|
||||
Name[zh_CN]=播放
|
||||
Name[zh_TW]=播放
|
||||
|
||||
[Desktop Action Pause]
|
||||
Name=Pause
|
||||
Exec=clementine --pause
|
||||
Name[be]=Прыпыніць
|
||||
Name[bg]=Пауза
|
||||
Name[br]=Ehan
|
||||
Name[ca]=Pausa
|
||||
Name[cs]=Pozastavit
|
||||
Name[el]=Παύση
|
||||
Name[es]=Pausar
|
||||
Name[et]=Paus
|
||||
Name[eu]=Pausarazi
|
||||
Name[fa]=ایست
|
||||
Name[fi]=Keskeytä
|
||||
Name[ga]=Cuir ar sos
|
||||
Name[gl]=Pausa
|
||||
Name[he]=השהייה
|
||||
Name[hi]=गाना रोकें
|
||||
Name[hr]=Pauza
|
||||
Name[hu]=Szünet
|
||||
Name[is]=Setja í bið
|
||||
Name[it]=Pausa
|
||||
Name[ja]=一時停止
|
||||
Name[kk]=Аялдату
|
||||
Name[ko]=일시중지
|
||||
Name[lt]=Pristabdyti
|
||||
Name[lv]=Pauze
|
||||
Name[nl]=Pauze
|
||||
Name[oc]=Pausa
|
||||
Name[pl]=Pauza
|
||||
Name[pt]=Pausa
|
||||
Name[pt_BR]=Pausar
|
||||
Name[ro]=Pauză
|
||||
Name[ru]=Приостановить
|
||||
Name[sk]=Pozastaviť
|
||||
Name[sl]=Začasno ustavi
|
||||
Name[sr]=Паузирај
|
||||
Name[sr@ijekavian]=Паузирај
|
||||
Name[sr@ijekavianlatin]=Pauziraj
|
||||
Name[sr@latin]=Pauziraj
|
||||
Name[sv]=Gör paus
|
||||
Name[tr]=Duraklat
|
||||
Name[uk]=Призупинити
|
||||
Name[vi]=Tạm dừng
|
||||
Name[zh_CN]=暂停
|
||||
Name[zh_TW]=暫停
|
||||
[Desktop Action Play-Pause]
|
||||
Name=Play/Pause
|
||||
Exec=clementine --play-pause
|
||||
Name[be]=Прайграць/Прыпыніць
|
||||
Name[bg]=Възпроизвеждане/Пауза
|
||||
Name[br]=Lenn/Ehan
|
||||
Name[ca]=Reprodueix/Pausa
|
||||
Name[cs]=Přehrát/Pozastavit
|
||||
Name[da]=Afspil/Pause
|
||||
Name[de]=Wiedergabe/Pause
|
||||
Name[el]=Αναπαραγωγή/Παύση
|
||||
Name[es]=Reproducir/Pausar
|
||||
Name[et]=Mängi/Paus
|
||||
Name[eu]=Erreproduzitu/Pausarazi
|
||||
Name[fa]=پخش/مکث
|
||||
Name[fi]=Toista/Keskeytä
|
||||
Name[fr]=Lecture/Pause
|
||||
Name[ga]=Seinn/Cuir ar sos
|
||||
Name[gl]=Reproducir/Pausa
|
||||
Name[he]=נגן/השהה
|
||||
Name[hi]=गाना बजाएं/गाना रोकें
|
||||
Name[hr]=Pokreni reprodukciju/Pauza
|
||||
Name[hu]=Lejátszás/Szünet
|
||||
Name[is]=Spila/Setja í bið
|
||||
Name[it]=Riproduci/Pausa
|
||||
Name[ja]=再生/一時停止
|
||||
Name[kk]=Ойнату/Аялдату
|
||||
Name[ko]=재생/일시 중지
|
||||
Name[lt]=Groti/Pristabdyti
|
||||
Name[lv]=Atskaņot/Pauze
|
||||
Name[nb]=Spill/Pause
|
||||
Name[nl]=Afspelen/Pauze
|
||||
Name[oc]=Lectura/Pausa
|
||||
Name[pl]=Odtwarzaj/Pauza
|
||||
Name[pt]=Reproduzir/Pausa
|
||||
Name[pt_BR]=Reproduzir/Pausar
|
||||
Name[ro]=Redă/Pauză
|
||||
Name[ru]=Играть/пауза
|
||||
Name[sk]=Hrať/Pozastaviť
|
||||
Name[sl]=Predvajaj/Začasno ustavi
|
||||
Name[sr]=Пусти/Паузирај
|
||||
Name[sr@ijekavian]=Пусти/Паузирај
|
||||
Name[sr@ijekavianlatin]=Pusti/Pauziraj
|
||||
Name[sr@latin]=Pusti/Pauziraj
|
||||
Name[sv]=Spela upp/Gör paus
|
||||
Name[tr]=Çal/Duraklat
|
||||
Name[uk]=Відтворити/Призупинити
|
||||
Name[vi]=Phát/Tạm dừng
|
||||
Name[zh_CN]=播放/暂停
|
||||
Name[zh_TW]=播放/暫停
|
||||
|
||||
[Desktop Action Stop]
|
||||
Name=Stop
|
||||
|
@ -322,7 +276,7 @@ Name[pl]=Dalej
|
|||
Name[pt]=Seguinte
|
||||
Name[pt_BR]=Próximo
|
||||
Name[ro]=Următoarea
|
||||
Name[ru]=Дальше
|
||||
Name[ru]=Следующий
|
||||
Name[sk]=Ďalšia
|
||||
Name[sl]=Naslednji
|
||||
Name[sr]=Следећа
|
||||
|
|
|
@ -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(${LIBSPOTIFY_INCLUDE_DIRS})
|
||||
include_directories(${PROTOBUF_INCLUDE_DIRS})
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR})
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
|
||||
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-spotifyblob)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-spotifyblob)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-common)
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Woverloaded-virtual -Wall -Wno-sign-compare -Wno-deprecated-declarations -Wno-unused-local-typedefs -Wno-unused-private-field -Wno-unknown-warning-option")
|
||||
|
||||
link_directories(${LIBSPOTIFY_LIBRARY_DIRS})
|
||||
|
||||
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
|
||||
|
||||
set(SOURCES
|
||||
main.cpp
|
||||
mediapipeline.cpp
|
||||
spotifyclient.cpp
|
||||
spotify_utilities.cpp
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
spotifyclient.h
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
list(APPEND SOURCES spotify_utilities.mm)
|
||||
endif(APPLE)
|
||||
|
||||
qt5_wrap_cpp(MOC ${HEADERS})
|
||||
|
||||
if(WIN32 AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT ENABLE_WIN32_CONSOLE)
|
||||
set(win32_build_flag WIN32)
|
||||
endif(WIN32 AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT ENABLE_WIN32_CONSOLE)
|
||||
|
||||
add_executable(clementine-spotifyblob
|
||||
${win32_build_flag}
|
||||
${SOURCES}
|
||||
${MOC}
|
||||
)
|
||||
|
||||
target_link_libraries(clementine-spotifyblob
|
||||
${LIBSPOTIFY_LIBRARIES} ${LIBSPOTIFY_LDFLAGS}
|
||||
${QT_QTCORE_LIBRARY}
|
||||
${QT_QTNETWORK_LIBRARY}
|
||||
${GSTREAMER_BASE_LIBRARIES}
|
||||
${GSTREAMER_APP_LIBRARIES}
|
||||
${PROTOBUF_STATIC_LIBRARY}
|
||||
clementine-spotifyblob-messages
|
||||
libclementine-common
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(clementine-spotifyblob
|
||||
"-framework Foundation"
|
||||
)
|
||||
endif(APPLE)
|
||||
|
||||
if(NOT APPLE)
|
||||
# macdeploy.py takes care of this on mac
|
||||
install(TARGETS clementine-spotifyblob
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
endif(NOT APPLE)
|
||||
|
||||
if(LINUX)
|
||||
# Versioned name of the blob
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
set(SPOTIFY_BLOB_ARCH 32)
|
||||
else(CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
set(SPOTIFY_BLOB_ARCH 64)
|
||||
endif(CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
|
||||
install(
|
||||
FILES ${CMAKE_BINARY_DIR}/clementine-spotifyblob
|
||||
DESTINATION ${CMAKE_BINARY_DIR}/spotify/version${SPOTIFY_BLOB_VERSION}-${SPOTIFY_BLOB_ARCH}bit/
|
||||
RENAME blob
|
||||
)
|
||||
endif(LINUX)
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,37 +881,6 @@ optional_source(HAVE_LIBLASTFM
|
|||
internet/lastfm/lastfmsettingspage.ui
|
||||
)
|
||||
|
||||
# Spotify support
|
||||
optional_source(HAVE_SPOTIFY
|
||||
SOURCES
|
||||
internet/spotify/spotifyserver.cpp
|
||||
internet/spotify/spotifyservice.cpp
|
||||
internet/spotify/spotifysettingspage.cpp
|
||||
internet/spotifywebapi/spotifywebapiservice.cpp
|
||||
globalsearch/spotifysearchprovider.cpp
|
||||
globalsearch/spotifywebapisearchprovider.cpp
|
||||
HEADERS
|
||||
globalsearch/spotifysearchprovider.h
|
||||
globalsearch/spotifywebapisearchprovider.h
|
||||
internet/spotify/spotifyserver.h
|
||||
internet/spotify/spotifyservice.h
|
||||
internet/spotify/spotifysettingspage.h
|
||||
internet/spotifywebapi/spotifywebapiservice.h
|
||||
UI
|
||||
internet/spotify/spotifysettingspage.ui
|
||||
)
|
||||
|
||||
if(HAVE_SPOTIFY)
|
||||
optional_source(HAVE_SPOTIFY_DOWNLOADER
|
||||
SOURCES
|
||||
internet/spotify/spotifyblobdownloader.cpp
|
||||
HEADERS
|
||||
internet/spotify/spotifyblobdownloader.h
|
||||
INCLUDE_DIRECTORIES
|
||||
${CRYPTOPP_INCLUDE_DIRS}
|
||||
)
|
||||
endif(HAVE_SPOTIFY)
|
||||
|
||||
# Platform specific - OS X
|
||||
optional_source(APPLE
|
||||
INCLUDE_DIRECTORIES
|
||||
|
@ -1300,7 +1268,6 @@ target_link_libraries(clementine_lib
|
|||
${SQLITE_LIBRARIES}
|
||||
|
||||
Qocoa
|
||||
z
|
||||
)
|
||||
|
||||
link_directories(
|
||||
|
@ -1350,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()
|
||||
|
@ -1394,7 +1350,6 @@ target_link_libraries(clementine_lib qsqlite)
|
|||
if (WIN32)
|
||||
target_link_libraries(clementine_lib
|
||||
protobuf
|
||||
${ZLIB_LIBRARIES}
|
||||
tinysvcmdns
|
||||
dsound
|
||||
)
|
||||
|
@ -1448,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_
|
||||
|
|
|
@ -74,8 +74,8 @@ void Organise::Start() {
|
|||
|
||||
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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -1,97 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spotifywebapisearchprovider.h"
|
||||
|
||||
#include <qurl.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "internet/spotifywebapi/spotifywebapiservice.h"
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
namespace {
|
||||
static constexpr int kNoRunningSearch = -1;
|
||||
}
|
||||
|
||||
SpotifyWebApiSearchProvider::SpotifyWebApiSearchProvider(
|
||||
Application* app, SpotifyWebApiService* parent)
|
||||
: SearchProvider(app, parent),
|
||||
parent_{parent},
|
||||
last_search_id_{kNoRunningSearch} {
|
||||
Init("Spotify (Experimential)", "spotify_web_api",
|
||||
IconLoader::Load("spotify", IconLoader::Provider),
|
||||
WantsDelayedQueries | WantsSerialisedArtQueries | ArtIsProbablyRemote |
|
||||
CanGiveSuggestions);
|
||||
|
||||
connect(parent, &SpotifyWebApiService::SearchFinished, this,
|
||||
&SpotifyWebApiSearchProvider::SearchFinishedSlot);
|
||||
}
|
||||
|
||||
void SpotifyWebApiSearchProvider::SearchAsync(int id, const QString& query) {
|
||||
if (last_search_id_ != kNoRunningSearch) {
|
||||
// Cancel last pending search
|
||||
emit SearchFinished(last_search_id_);
|
||||
|
||||
// Set the pending query
|
||||
last_search_id_ = id;
|
||||
last_query_ = query;
|
||||
|
||||
// And wait for the current search to be completed
|
||||
return;
|
||||
}
|
||||
|
||||
last_search_id_ = id;
|
||||
last_query_ = query;
|
||||
|
||||
parent_->Search(last_search_id_, last_query_);
|
||||
}
|
||||
|
||||
void SpotifyWebApiSearchProvider::SearchFinishedSlot(
|
||||
int searchId, const QList<Song>& apiResult) {
|
||||
ResultList ret;
|
||||
for (auto&& item : apiResult) {
|
||||
Result result{this};
|
||||
result.group_automatically_ = true;
|
||||
result.metadata_ = item;
|
||||
ret += result;
|
||||
}
|
||||
emit ResultsAvailable(searchId, ret);
|
||||
emit SearchFinished(searchId);
|
||||
|
||||
// Search again if we have a pending query
|
||||
if (searchId != last_search_id_) {
|
||||
parent_->Search(last_search_id_, last_query_);
|
||||
} else {
|
||||
last_search_id_ = kNoRunningSearch;
|
||||
}
|
||||
}
|
||||
|
||||
void SpotifyWebApiSearchProvider::LoadArtAsync(int id, const Result& result) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
void SpotifyWebApiSearchProvider::ShowConfig() {}
|
||||
|
||||
InternetService* SpotifyWebApiSearchProvider::internet_service() {
|
||||
return parent_;
|
||||
}
|
||||
|
||||
QStringList SpotifyWebApiSearchProvider::GetSuggestions(int count) {
|
||||
// TODO
|
||||
return QStringList{};
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SPOTIFYWEBAPISEARCHPROVIDER_H
|
||||
#define SPOTIFYWEBAPISEARCHPROVIDER_H
|
||||
|
||||
#include "core/song.h"
|
||||
#include "searchprovider.h"
|
||||
|
||||
class SpotifyWebApiService;
|
||||
|
||||
class SpotifyWebApiSearchProvider : public SearchProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SpotifyWebApiSearchProvider(Application* app, SpotifyWebApiService* parent);
|
||||
|
||||
void SearchAsync(int id, const QString& query) override;
|
||||
void LoadArtAsync(int id, const Result& result) override;
|
||||
QStringList GetSuggestions(int count) override;
|
||||
|
||||
void ShowConfig() override;
|
||||
InternetService* internet_service() override;
|
||||
|
||||
private slots:
|
||||
void SearchFinishedSlot(int id, const QList<Song>&);
|
||||
|
||||
private:
|
||||
SpotifyWebApiService* parent_;
|
||||
int last_search_id_;
|
||||
QString last_query_;
|
||||
};
|
||||
|
||||
#endif // SPOTIFYWEBAPISEARCHPROVIDER_H
|
|
@ -60,10 +60,6 @@
|
|||
#ifdef HAVE_SEAFILE
|
||||
#include "internet/seafile/seafileservice.h"
|
||||
#endif
|
||||
#ifdef HAVE_SPOTIFY
|
||||
#include "internet/spotify/spotifyservice.h"
|
||||
#include "internet/spotifywebapi/spotifywebapiservice.h"
|
||||
#endif
|
||||
|
||||
using smart_playlists::Generator;
|
||||
using smart_playlists::GeneratorMimeData;
|
||||
|
@ -99,10 +95,6 @@ InternetModel::InternetModel(Application* app, QObject* parent)
|
|||
AddService(new SomaFMService(app, this));
|
||||
AddService(new IntergalacticFMService(app, this));
|
||||
AddService(new RadioBrowserService(app, this));
|
||||
#ifdef HAVE_SPOTIFY
|
||||
AddService(new SpotifyService(app, this));
|
||||
AddService(new SpotifyWebApiService(app, this));
|
||||
#endif
|
||||
AddService(new SubsonicService(app, this));
|
||||
#ifdef HAVE_BOX
|
||||
AddService(new BoxService(app, this));
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -1,198 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spotifywebapiservice.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QtDebug>
|
||||
#include <utility>
|
||||
|
||||
#include "3rdparty/qtiocompressor/qtiocompressor.h"
|
||||
#include "core/application.h"
|
||||
#include "core/network.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "globalsearch/globalsearch.h"
|
||||
#include "globalsearch/spotifywebapisearchprovider.h"
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
namespace {
|
||||
static constexpr const char* kServiceName = "SpotifyWebApi";
|
||||
static constexpr const char* kGetAccessTokenUrl =
|
||||
"https://open.spotify.com/"
|
||||
"get_access_token?reason=transport&productType=web_player";
|
||||
static constexpr const char* kSearchUrl =
|
||||
"https://api.spotify.com/v1/search?q=%1&type=track&limit=50";
|
||||
|
||||
template <typename... Args>
|
||||
inline QJsonValue Get(QJsonValue obj, Args&&... args) {
|
||||
std::array<const char*, sizeof...(Args)> names = {
|
||||
std::forward<Args>(args)...};
|
||||
for (auto&& name : names) {
|
||||
Q_ASSERT(obj.isObject());
|
||||
obj = obj.toObject()[name];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
inline QJsonValue Get(const QJsonDocument& obj, Args&&... args) {
|
||||
return Get(obj.object(), std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
QString concat(const QJsonArray& array, const char* name) {
|
||||
QStringList ret;
|
||||
for (auto&& item : array) {
|
||||
ret << Get(item, name).toString();
|
||||
}
|
||||
return ret.join(", ");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SpotifyWebApiService::SpotifyWebApiService(Application* app,
|
||||
InternetModel* parent)
|
||||
: InternetService(kServiceName, app, parent, parent),
|
||||
network_(new NetworkAccessManager{this}),
|
||||
token_expiration_ms_{0} {
|
||||
app_->global_search()->AddProvider(
|
||||
new SpotifyWebApiSearchProvider(app_, this));
|
||||
}
|
||||
|
||||
SpotifyWebApiService::~SpotifyWebApiService() {}
|
||||
|
||||
QStandardItem* SpotifyWebApiService::CreateRootItem() {
|
||||
root_ = new QStandardItem(IconLoader::Load("spotify", IconLoader::Provider),
|
||||
kServiceName);
|
||||
return root_;
|
||||
}
|
||||
|
||||
void SpotifyWebApiService::LazyPopulate(QStandardItem* item) {}
|
||||
|
||||
void SpotifyWebApiService::Search(int searchId, QString queryStr) {
|
||||
if (QDateTime::currentDateTime().toMSecsSinceEpoch() >=
|
||||
token_expiration_ms_) {
|
||||
QNetworkRequest request{QUrl{kGetAccessTokenUrl}};
|
||||
request.setRawHeader("Accept-Encoding", "gzip");
|
||||
|
||||
QNetworkReply* reply = network_->get(request);
|
||||
connect(reply, &QNetworkReply::finished, [=]() {
|
||||
reply->deleteLater();
|
||||
|
||||
OnTokenReady(ParseJsonReplyWithGzip(reply), searchId, queryStr);
|
||||
});
|
||||
} else {
|
||||
OnReadyToSearch(searchId, queryStr);
|
||||
}
|
||||
}
|
||||
|
||||
void SpotifyWebApiService::OnTokenReady(const QJsonDocument& json, int searchId,
|
||||
QString queryStr) {
|
||||
if (!json.isEmpty()) {
|
||||
token_ = Get(json, "accessToken").toString();
|
||||
token_expiration_ms_ = static_cast<qint64>(
|
||||
Get(json, "accessTokenExpirationTimestampMs").toDouble());
|
||||
|
||||
qLog(Debug) << "Spotify API Token:" << token_;
|
||||
|
||||
OnReadyToSearch(searchId, queryStr);
|
||||
}
|
||||
}
|
||||
|
||||
void SpotifyWebApiService::OnReadyToSearch(int searchId, QString queryStr) {
|
||||
qLog(Debug) << "Spotify API Searching: " << queryStr;
|
||||
|
||||
QNetworkRequest request{
|
||||
QUrl{QString(kSearchUrl).arg(queryStr.toHtmlEscaped())}};
|
||||
|
||||
request.setRawHeader("Accept-Encoding", "gzip");
|
||||
request.setRawHeader("Authorization", ("Bearer " + token_).toUtf8());
|
||||
|
||||
QNetworkReply* reply = network_->get(request);
|
||||
connect(reply, &QNetworkReply::finished, [=] {
|
||||
reply->deleteLater();
|
||||
|
||||
BuildResultList(ParseJsonReplyWithGzip(reply), searchId);
|
||||
});
|
||||
}
|
||||
|
||||
void SpotifyWebApiService::BuildResultList(const QJsonDocument& json,
|
||||
int searchId) {
|
||||
QList<Song> result;
|
||||
|
||||
for (auto&& item : Get(json, "tracks", "items").toArray()) {
|
||||
Song song;
|
||||
|
||||
song.set_albumartist(
|
||||
concat(Get(item, "album", "artists").toArray(), "name"));
|
||||
song.set_album(Get(item, "album", "name").toString());
|
||||
song.set_artist(concat(Get(item, "artists").toArray(), "name"));
|
||||
song.set_disc(Get(item, "disc_number").toInt());
|
||||
song.set_length_nanosec(Get(item, "duration_ms").toInt() * kNsecPerMsec);
|
||||
song.set_title(Get(item, "name").toString());
|
||||
song.set_track(Get(item, "track_number").toInt());
|
||||
song.set_url(QUrl{Get(item, "uri").toString()});
|
||||
song.set_filetype(Song::Type_Stream);
|
||||
song.set_valid(true);
|
||||
song.set_directory_id(0);
|
||||
song.set_mtime(0);
|
||||
song.set_ctime(0);
|
||||
song.set_filesize(0);
|
||||
|
||||
result += song;
|
||||
}
|
||||
|
||||
emit SearchFinished(searchId, result);
|
||||
}
|
||||
|
||||
QJsonDocument SpotifyWebApiService::ParseJsonReplyWithGzip(
|
||||
QNetworkReply* reply) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
app_->AddError(tr("%1 request failed:\n%2")
|
||||
.arg(kServiceName)
|
||||
.arg(reply->errorString()));
|
||||
return QJsonDocument();
|
||||
}
|
||||
|
||||
QByteArray output;
|
||||
if (reply->hasRawHeader("content-encoding") &&
|
||||
reply->rawHeader("content-encoding") == "gzip") {
|
||||
QtIOCompressor gzip(reply);
|
||||
gzip.setStreamFormat(QtIOCompressor::GzipFormat);
|
||||
if (!gzip.open(QIODevice::ReadOnly)) {
|
||||
app_->AddError(tr("%1 failed to decode as gzip stream:\n%2")
|
||||
.arg(kServiceName)
|
||||
.arg(gzip.errorString()));
|
||||
return QJsonDocument();
|
||||
}
|
||||
output = gzip.readAll();
|
||||
} else {
|
||||
output = reply->readAll();
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument document = QJsonDocument::fromJson(output, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
app_->AddError(tr("Failed to parse %1 response:\n%2")
|
||||
.arg(kServiceName)
|
||||
.arg(error.errorString()));
|
||||
return QJsonDocument();
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SPOTIFYWEBAPISERVICE_H
|
||||
#define SPOTIFYWEBAPISERVICE_H
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "internet/core/internetmodel.h"
|
||||
#include "internet/core/internetservice.h"
|
||||
|
||||
class NetworkAccessManager;
|
||||
|
||||
class SpotifyWebApiService : public InternetService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SpotifyWebApiService(Application* app, InternetModel* parent);
|
||||
~SpotifyWebApiService();
|
||||
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* parent) override;
|
||||
|
||||
public:
|
||||
void Search(int searchId, QString queryStr);
|
||||
|
||||
private:
|
||||
void OnTokenReady(const QJsonDocument&, int searchId, QString queryStr);
|
||||
void OnReadyToSearch(int searchId, QString queryStr);
|
||||
void BuildResultList(const QJsonDocument&, int searchId);
|
||||
|
||||
signals:
|
||||
void SearchFinished(int searchId, const QList<Song>&);
|
||||
|
||||
private:
|
||||
QJsonDocument ParseJsonReplyWithGzip(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
QStandardItem* root_;
|
||||
NetworkAccessManager* network_;
|
||||
|
||||
QString token_;
|
||||
qint64 token_expiration_ms_;
|
||||
};
|
||||
|
||||
#endif // SPOTIFYWEBAPISERVICE_H
|
|
@ -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;
|
||||
|
|
|
@ -42,7 +42,7 @@ class SubsonicService : public InternetService {
|
|||
|
||||
public:
|
||||
SubsonicService(Application* app, InternetModel* parent);
|
||||
~SubsonicService();
|
||||
~SubsonicService() override;
|
||||
|
||||
enum LoginState {
|
||||
LoginState_Loggedin,
|
||||
|
@ -90,11 +90,11 @@ class SubsonicService : public InternetService {
|
|||
bool IsConfigured() const;
|
||||
bool IsAmpache() const;
|
||||
|
||||
QStandardItem* CreateRootItem();
|
||||
void LazyPopulate(QStandardItem* item);
|
||||
void ShowContextMenu(const QPoint& global_pos);
|
||||
QWidget* HeaderWidget() const;
|
||||
void ReloadSettings();
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* item) override;
|
||||
void ShowContextMenu(const QPoint& global_pos) override;
|
||||
QWidget* HeaderWidget() const override;
|
||||
void ReloadSettings() override;
|
||||
|
||||
void Login();
|
||||
void Login(const QString& server, const QString& username,
|
||||
|
@ -175,7 +175,7 @@ class SubsonicService : public InternetService {
|
|||
void OnLoginStateChanged(SubsonicService::LoginState newstate);
|
||||
void OnPingFinished(QNetworkReply* reply);
|
||||
|
||||
void ShowConfig();
|
||||
void ShowConfig() override;
|
||||
};
|
||||
|
||||
class SubsonicLibraryScanner : public QObject {
|
||||
|
@ -184,7 +184,7 @@ class SubsonicLibraryScanner : public QObject {
|
|||
public:
|
||||
explicit SubsonicLibraryScanner(SubsonicService* service,
|
||||
QObject* parent = nullptr);
|
||||
~SubsonicLibraryScanner();
|
||||
~SubsonicLibraryScanner() override;
|
||||
|
||||
void Scan();
|
||||
const SongList& GetSongs() const { return songs_; }
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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_
|
||||
|
|
|
@ -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_
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>522</width>
|
||||
<height>575</height>
|
||||
<width>600</width>
|
||||
<height>800</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -119,13 +119,16 @@
|
|||
</sizepolicy>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
<number>5</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderVisible">
|
||||
<bool>true</bool>
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||
<number>10</number>
|
||||
<number>4</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderDefaultSectionSize">
|
||||
<number>20</number>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
|
@ -153,6 +156,11 @@
|
|||
<string>Duration</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Filename Preview</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -224,51 +232,30 @@
|
|||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="FileNameFormatWidget" name="naming_group" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="output_group">
|
||||
<property name="title">
|
||||
<string>Output options</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="2" column="2">
|
||||
<widget class="QPushButton" name="select">
|
||||
<property name="text">
|
||||
<string>Select...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>File Format</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QLineEdit" name="format_filename">
|
||||
<property name="text">
|
||||
<string notr="true">%track - %artist - %title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Destination</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="format">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Audio format</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="destination">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
|
@ -281,17 +268,27 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="options">
|
||||
<property name="text">
|
||||
<string>Options...</string>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="format">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="select">
|
||||
<property name="text">
|
||||
<string>Audio format</string>
|
||||
<string>Select...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QPushButton" name="options">
|
||||
<property name="text">
|
||||
<string>Options...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -322,6 +319,14 @@
|
|||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>FileNameFormatWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/filenameformatwidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>tableWidget</tabstop>
|
||||
<tabstop>select_all_button</tabstop>
|
||||
|
@ -332,7 +337,6 @@
|
|||
<tabstop>genreLineEdit</tabstop>
|
||||
<tabstop>yearLineEdit</tabstop>
|
||||
<tabstop>discLineEdit</tabstop>
|
||||
<tabstop>format_filename</tabstop>
|
||||
<tabstop>format</tabstop>
|
||||
<tabstop>options</tabstop>
|
||||
<tabstop>destination</tabstop>
|
||||
|
|
|
@ -19,12 +19,14 @@
|
|||
|
||||
#include <QFile>
|
||||
#include <QMutexLocker>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/utilities.h"
|
||||
#include "devices/cddadevice.h"
|
||||
#include "transcoder/transcoder.h"
|
||||
|
||||
// winspool.h defines this :(
|
||||
|
@ -32,23 +34,20 @@
|
|||
#undef AddJob
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
const char kWavHeaderRiffMarker[] = "RIFF";
|
||||
const char kWavFileTypeFormatChunk[] = "WAVEfmt ";
|
||||
const char kWavDataString[] = "data";
|
||||
} // namespace
|
||||
|
||||
Ripper::Ripper(CdIo_t* cdio, QObject* parent)
|
||||
Ripper::Ripper(int track_count, QObject* parent)
|
||||
: QObject(parent),
|
||||
cdio_(cdio),
|
||||
track_count_(track_count),
|
||||
transcoder_(new Transcoder(this)),
|
||||
cancel_requested_(false),
|
||||
finished_success_(0),
|
||||
finished_failed_(0),
|
||||
files_tagged_(0) {
|
||||
Q_ASSERT(cdio_); // TODO: error handling
|
||||
connect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)),
|
||||
SLOT(TranscodingJobComplete(QString, QString, bool)));
|
||||
Q_ASSERT(track_count >= 0);
|
||||
|
||||
transcoder_->set_max_threads(1); // we want transcoder to read only one song
|
||||
// at once from disc to prevent seeking
|
||||
connect(transcoder_, SIGNAL(JobComplete(QUrl, QString, bool)),
|
||||
SLOT(TranscodingJobComplete(QUrl, QString, bool)));
|
||||
connect(transcoder_, SIGNAL(AllJobsComplete()),
|
||||
SLOT(AllTranscodingJobsComplete()));
|
||||
connect(transcoder_, SIGNAL(LogLine(QString)), SLOT(LogLine(QString)));
|
||||
|
@ -58,12 +57,13 @@ Ripper::~Ripper() {}
|
|||
|
||||
void Ripper::AddTrack(int track_number, const QString& title,
|
||||
const QString& transcoded_filename,
|
||||
const TranscoderPreset& preset) {
|
||||
const TranscoderPreset& preset, bool overwrite_existing) {
|
||||
if (track_number < 1 || track_number > TracksOnDisc()) {
|
||||
qLog(Warning) << "Invalid track number:" << track_number << "Ignoring";
|
||||
return;
|
||||
}
|
||||
TrackInformation track(track_number, title, transcoded_filename, preset);
|
||||
TrackInformation track(track_number, title, transcoded_filename, preset,
|
||||
overwrite_existing);
|
||||
tracks_.append(track);
|
||||
}
|
||||
|
||||
|
@ -78,12 +78,7 @@ void Ripper::SetAlbumInformation(const QString& album, const QString& artist,
|
|||
album_.type = type;
|
||||
}
|
||||
|
||||
int Ripper::TracksOnDisc() const {
|
||||
int number_of_tracks = cdio_get_num_tracks(cdio_);
|
||||
// Return zero tracks if there is an error, e.g. no medium found.
|
||||
if (number_of_tracks == CDIO_INVALID_TRACK) number_of_tracks = 0;
|
||||
return number_of_tracks;
|
||||
}
|
||||
int Ripper::TracksOnDisc() const { return track_count_; }
|
||||
|
||||
int Ripper::AddedTracks() const { return tracks_.length(); }
|
||||
|
||||
|
@ -94,7 +89,6 @@ void Ripper::Start() {
|
|||
QMutexLocker l(&mutex_);
|
||||
cancel_requested_ = false;
|
||||
}
|
||||
SetupProgressInterval();
|
||||
|
||||
qLog(Debug) << "Ripping" << AddedTracks() << "tracks.";
|
||||
QtConcurrent::run(this, &Ripper::Rip);
|
||||
|
@ -106,160 +100,65 @@ void Ripper::Cancel() {
|
|||
cancel_requested_ = true;
|
||||
}
|
||||
transcoder_->Cancel();
|
||||
RemoveTemporaryDirectory();
|
||||
emit Cancelled();
|
||||
emit(Cancelled());
|
||||
}
|
||||
|
||||
void Ripper::TranscodingJobComplete(const QString& input, const QString& output,
|
||||
void Ripper::TranscodingJobComplete(const QUrl& input, const QString& output,
|
||||
bool success) {
|
||||
if (success)
|
||||
finished_success_++;
|
||||
else
|
||||
finished_failed_++;
|
||||
UpdateProgress();
|
||||
|
||||
// The the transcoder does not overwrite files. Instead, it changes
|
||||
// The transcoder does not necessarily overwrite files. If not, it changes
|
||||
// the name of the output file. We need to update the transcoded
|
||||
// filename for the corresponding track so that we tag the correct
|
||||
// file later on.
|
||||
for (QList<TrackInformation>::iterator it = tracks_.begin();
|
||||
it != tracks_.end(); ++it) {
|
||||
if (it->temporary_filename == input) {
|
||||
QUrl track_url =
|
||||
CddaDevice::TrackStrToUrl(QString("cdda://%1").arg(it->track_number));
|
||||
if (track_url == input) {
|
||||
it->transcoded_filename = output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Ripper::AllTranscodingJobsComplete() {
|
||||
RemoveTemporaryDirectory();
|
||||
TagFiles();
|
||||
}
|
||||
void Ripper::AllTranscodingJobsComplete() { TagFiles(); }
|
||||
|
||||
void Ripper::LogLine(const QString& message) { qLog(Debug) << message; }
|
||||
|
||||
/*
|
||||
* WAV Header documentation
|
||||
* as taken from:
|
||||
* http://www.topherlee.com/software/pcm-tut-wavformat.html
|
||||
* Pos Value Description
|
||||
* 0-3 | "RIFF" | Marks the file as a riff file.
|
||||
* | Characters are each 1 byte long.
|
||||
* 4-7 | File size (integer) | Size of the overall file - 8 bytes,
|
||||
* | in bytes (32-bit integer).
|
||||
* 8-11 | "WAVE" | File Type Header. For our purposes,
|
||||
* | it always equals "WAVE".
|
||||
* 13-16 | "fmt " | Format chunk marker. Includes trailing null.
|
||||
* 17-20 | 16 | Length of format data as listed above
|
||||
* 21-22 | 1 | Type of format (1 is PCM) - 2 byte integer
|
||||
* 23-24 | 2 | Number of Channels - 2 byte integer
|
||||
* 25-28 | 44100 | Sample Rate - 32 byte integer. Common values
|
||||
* | are 44100 (CD), 48000 (DAT).
|
||||
* | Sample Rate = Number of Samples per second, or Hertz.
|
||||
* 29-32 | 176400 | (Sample Rate * BitsPerSample * Channels) / 8.
|
||||
* 33-34 | 4 | (BitsPerSample * Channels) / 8.1 - 8 bit mono2 - 8 bit stereo/16
|
||||
* bit mono4 - 16 bit stereo
|
||||
* 35-36 | 16 | Bits per sample
|
||||
* 37-40 | "data" | "data" chunk header.
|
||||
* | Marks the beginning of the data section.
|
||||
* 41-44 | File size (data) | Size of the data section.
|
||||
*/
|
||||
void Ripper::WriteWAVHeader(QFile* stream, int32_t i_bytecount) {
|
||||
QDataStream data_stream(stream);
|
||||
data_stream.setByteOrder(QDataStream::LittleEndian);
|
||||
// sizeof() - 1 to avoid including "\0" in the file too
|
||||
data_stream.writeRawData(kWavHeaderRiffMarker,
|
||||
sizeof(kWavHeaderRiffMarker) - 1); /* 0-3 */
|
||||
data_stream << qint32(i_bytecount + 44 - 8); /* 4-7 */
|
||||
data_stream.writeRawData(kWavFileTypeFormatChunk,
|
||||
sizeof(kWavFileTypeFormatChunk) - 1); /* 8-15 */
|
||||
data_stream << (qint32)16; /* 16-19 */
|
||||
data_stream << (qint16)1; /* 20-21 */
|
||||
data_stream << (qint16)2; /* 22-23 */
|
||||
data_stream << (qint32)44100; /* 24-27 */
|
||||
data_stream << (qint32)(44100 * 2 * 2); /* 28-31 */
|
||||
data_stream << (qint16)4; /* 32-33 */
|
||||
data_stream << (qint16)16; /* 34-35 */
|
||||
data_stream.writeRawData(kWavDataString,
|
||||
sizeof(kWavDataString) - 1); /* 36-39 */
|
||||
data_stream << (qint32)i_bytecount; /* 40-43 */
|
||||
}
|
||||
|
||||
void Ripper::Rip() {
|
||||
if (tracks_.isEmpty()) {
|
||||
emit Finished();
|
||||
return;
|
||||
}
|
||||
|
||||
temporary_directory_ = Utilities::MakeTempDir() + "/";
|
||||
finished_success_ = 0;
|
||||
finished_failed_ = 0;
|
||||
|
||||
// Set up progress bar
|
||||
UpdateProgress();
|
||||
|
||||
for (QList<TrackInformation>::iterator it = tracks_.begin();
|
||||
it != tracks_.end(); ++it) {
|
||||
QString filename =
|
||||
QString("%1%2.wav").arg(temporary_directory_).arg(it->track_number);
|
||||
QFile destination_file(filename);
|
||||
destination_file.open(QIODevice::WriteOnly);
|
||||
|
||||
lsn_t i_first_lsn = cdio_get_track_lsn(cdio_, it->track_number);
|
||||
lsn_t i_last_lsn = cdio_get_track_last_lsn(cdio_, it->track_number);
|
||||
WriteWAVHeader(&destination_file,
|
||||
(i_last_lsn - i_first_lsn + 1) * CDIO_CD_FRAMESIZE_RAW);
|
||||
|
||||
QByteArray buffered_input_bytes(CDIO_CD_FRAMESIZE_RAW, '\0');
|
||||
for (lsn_t i_cursor = i_first_lsn; i_cursor <= i_last_lsn; i_cursor++) {
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
if (cancel_requested_) {
|
||||
qLog(Debug) << "CD ripping canceled.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cdio_read_audio_sector(cdio_, buffered_input_bytes.data(),
|
||||
i_cursor) == DRIVER_OP_SUCCESS) {
|
||||
destination_file.write(buffered_input_bytes.data(),
|
||||
buffered_input_bytes.size());
|
||||
} else {
|
||||
qLog(Error) << "CD read error";
|
||||
break;
|
||||
}
|
||||
}
|
||||
finished_success_++;
|
||||
UpdateProgress();
|
||||
|
||||
it->temporary_filename = filename;
|
||||
transcoder_->AddJob(it->temporary_filename, it->preset,
|
||||
it->transcoded_filename);
|
||||
QUrl track_url =
|
||||
CddaDevice::TrackStrToUrl(QString("cdda://%1").arg(it->track_number));
|
||||
transcoder_->AddJob(track_url, it->preset, it->transcoded_filename);
|
||||
}
|
||||
transcoder_->Start();
|
||||
emit RippingComplete();
|
||||
}
|
||||
|
||||
// The progress interval is [0, 200*AddedTracks()], where the first
|
||||
// half corresponds to the CD ripping and the second half corresponds
|
||||
// to the transcoding.
|
||||
void Ripper::SetupProgressInterval() {
|
||||
int max = AddedTracks() * 2 * 100;
|
||||
emit ProgressInterval(0, max);
|
||||
}
|
||||
float Ripper::GetProgress() const {
|
||||
int added_tracks = AddedTracks();
|
||||
if (added_tracks == 0) return 1.0f;
|
||||
|
||||
void Ripper::UpdateProgress() {
|
||||
int progress = (finished_success_ + finished_failed_) * 100;
|
||||
QMap<QString, float> current_jobs = transcoder_->GetProgress();
|
||||
for (float value : current_jobs.values()) {
|
||||
progress += qBound(0, static_cast<int>(value * 100), 99);
|
||||
}
|
||||
emit Progress(progress);
|
||||
qLog(Debug) << "Progress:" << progress;
|
||||
}
|
||||
float progress = finished_success_ + finished_failed_;
|
||||
QList<float> current_job_progress_ = transcoder_->GetProgress().values();
|
||||
progress += std::accumulate(current_job_progress_.begin(),
|
||||
current_job_progress_.end(), 0.0f);
|
||||
progress /= added_tracks;
|
||||
|
||||
void Ripper::RemoveTemporaryDirectory() {
|
||||
if (!temporary_directory_.isEmpty())
|
||||
Utilities::RemoveRecursive(temporary_directory_);
|
||||
temporary_directory_.clear();
|
||||
qLog(Debug) << "Progress: " << progress;
|
||||
return progress;
|
||||
}
|
||||
|
||||
void Ripper::TagFiles() {
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
|
@ -40,7 +41,7 @@ class Ripper : public QObject {
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Ripper(CdIo_t* cdio, QObject* parent = nullptr);
|
||||
explicit Ripper(int track_count, QObject* parent = nullptr);
|
||||
~Ripper();
|
||||
|
||||
// Adds a track to the rip list if the track number corresponds to a
|
||||
|
@ -48,7 +49,7 @@ class Ripper : public QObject {
|
|||
// chosen TranscoderPreset.
|
||||
void AddTrack(int track_number, const QString& title,
|
||||
const QString& transcoded_filename,
|
||||
const TranscoderPreset& preset);
|
||||
const TranscoderPreset& preset, bool overwrite_existing);
|
||||
// Sets album metadata. This information is used when tagging the
|
||||
// final files.
|
||||
void SetAlbumInformation(const QString& album, const QString& artist,
|
||||
|
@ -60,12 +61,17 @@ class Ripper : public QObject {
|
|||
int AddedTracks() const;
|
||||
// Clears the rip list.
|
||||
void ClearTracks();
|
||||
// Returns the current progress of the ripping process for all tracks as a
|
||||
// floating point number between 0 and 1.
|
||||
float GetProgress() const;
|
||||
|
||||
signals:
|
||||
// Emitted when the full process, i.e., ripping, transcoding and tagging, is
|
||||
// completed or has failed.
|
||||
void Finished();
|
||||
void Cancelled();
|
||||
void ProgressInterval(int min, int max);
|
||||
void Progress(int progress);
|
||||
// Emitted when ripping and transcoding files is completed, but files still
|
||||
// need to be tagged.
|
||||
void RippingComplete();
|
||||
|
||||
public slots:
|
||||
|
@ -73,7 +79,7 @@ class Ripper : public QObject {
|
|||
void Cancel();
|
||||
|
||||
private slots:
|
||||
void TranscodingJobComplete(const QString& input, const QString& output,
|
||||
void TranscodingJobComplete(const QUrl& input, const QString& output,
|
||||
bool success);
|
||||
void AllTranscodingJobsComplete();
|
||||
void LogLine(const QString& message);
|
||||
|
@ -83,17 +89,18 @@ class Ripper : public QObject {
|
|||
struct TrackInformation {
|
||||
TrackInformation(int track_number, const QString& title,
|
||||
const QString& transcoded_filename,
|
||||
const TranscoderPreset& preset)
|
||||
const TranscoderPreset& preset, bool overwrite_existing)
|
||||
: track_number(track_number),
|
||||
title(title),
|
||||
transcoded_filename(transcoded_filename),
|
||||
preset(preset) {}
|
||||
preset(preset),
|
||||
overwrite_existing(overwrite_existing) {}
|
||||
|
||||
int track_number;
|
||||
QString title;
|
||||
QString transcoded_filename;
|
||||
TranscoderPreset preset;
|
||||
QString temporary_filename;
|
||||
bool overwrite_existing;
|
||||
};
|
||||
|
||||
struct AlbumInformation {
|
||||
|
@ -107,16 +114,13 @@ class Ripper : public QObject {
|
|||
Song::FileType type;
|
||||
};
|
||||
|
||||
void WriteWAVHeader(QFile* stream, int32_t i_bytecount);
|
||||
void Rip();
|
||||
void SetupProgressInterval();
|
||||
void UpdateProgress();
|
||||
void RemoveTemporaryDirectory();
|
||||
void TagFiles();
|
||||
|
||||
CdIo_t* cdio_;
|
||||
int track_count_;
|
||||
Transcoder* transcoder_;
|
||||
QString temporary_directory_;
|
||||
bool cancel_requested_;
|
||||
QMutex mutex_;
|
||||
int finished_success_;
|
||||
|
|
|
@ -90,10 +90,8 @@ void SongInfoView::ReloadSettings() {
|
|||
|
||||
QVariantList default_order;
|
||||
default_order << "lyrics.wikia.com"
|
||||
<< "lyricstime.com"
|
||||
<< "lyricsreg.com"
|
||||
<< "lyricsmania.com"
|
||||
<< "metrolyrics.com"
|
||||
<< "azlyrics.com"
|
||||
<< "songlyrics.com"
|
||||
<< "elyrics.net"
|
||||
|
@ -101,7 +99,6 @@ void SongInfoView::ReloadSettings() {
|
|||
<< "lyrics.com"
|
||||
<< "lyricsbay.com"
|
||||
<< "directlyrics.com"
|
||||
<< "loudson.gs"
|
||||
<< "teksty.org"
|
||||
<< "tekstowo.pl (Polish translations)"
|
||||
<< "vagalume.uol.com.br"
|
||||
|
|
|
@ -98,6 +98,9 @@ TranscodeDialog::TranscodeDialog(QWidget* parent)
|
|||
}
|
||||
}
|
||||
|
||||
ui_->remove_original->setChecked(
|
||||
s.value("overwrite_existing", false).toBool());
|
||||
|
||||
// Add a start button
|
||||
start_button_ = ui_->button_box->addButton(tr("Start transcoding"),
|
||||
QDialogButtonBox::ActionRole);
|
||||
|
@ -121,8 +124,8 @@ TranscodeDialog::TranscodeDialog(QWidget* parent)
|
|||
connect(ui_->options, SIGNAL(clicked()), SLOT(Options()));
|
||||
connect(ui_->select, SIGNAL(clicked()), SLOT(AddDestination()));
|
||||
|
||||
connect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)),
|
||||
SLOT(JobComplete(QString, QString, bool)));
|
||||
connect(transcoder_, SIGNAL(JobComplete(QUrl, QString, bool)),
|
||||
SLOT(JobComplete(QUrl, QString, bool)));
|
||||
connect(transcoder_, SIGNAL(LogLine(QString)), SLOT(LogLine(QString)));
|
||||
connect(transcoder_, SIGNAL(AllJobsComplete()), SLOT(AllJobsComplete()));
|
||||
}
|
||||
|
@ -162,7 +165,8 @@ void TranscodeDialog::Start() {
|
|||
QFileInfo input_fileinfo(
|
||||
file_model->index(i, 0).data(Qt::UserRole).toString());
|
||||
QString output_filename = GetOutputFileName(input_fileinfo, preset);
|
||||
transcoder_->AddJob(input_fileinfo.filePath(), preset, output_filename);
|
||||
transcoder_->AddJob(QUrl::fromLocalFile(input_fileinfo.filePath()), preset,
|
||||
output_filename);
|
||||
}
|
||||
|
||||
// Set up the progressbar
|
||||
|
@ -182,6 +186,7 @@ void TranscodeDialog::Start() {
|
|||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("last_output_format", preset.codec_mimetype_);
|
||||
s.setValue("overwrite_existing", ui_->remove_original->isChecked());
|
||||
}
|
||||
|
||||
void TranscodeDialog::Cancel() {
|
||||
|
@ -195,7 +200,7 @@ void TranscodeDialog::PipelineDumpAction(bool checked) {
|
|||
}
|
||||
}
|
||||
|
||||
void TranscodeDialog::JobComplete(const QString& input, const QString& output,
|
||||
void TranscodeDialog::JobComplete(const QUrl& input, const QString& output,
|
||||
bool success) {
|
||||
if (success)
|
||||
finished_success_++;
|
||||
|
@ -205,12 +210,29 @@ void TranscodeDialog::JobComplete(const QString& input, const QString& output,
|
|||
|
||||
UpdateStatusText();
|
||||
UpdateProgress();
|
||||
|
||||
bool overwrite_existing = ui_->remove_original->isChecked();
|
||||
|
||||
if (success && overwrite_existing && input.isLocalFile()) {
|
||||
QFileInfo input_fileinfo(input.toLocalFile());
|
||||
QFileInfo output_fileinfo(output);
|
||||
|
||||
bool same_extension = input_fileinfo.suffix() == output_fileinfo.suffix();
|
||||
bool same_path =
|
||||
input_fileinfo.absolutePath() == output_fileinfo.absolutePath();
|
||||
|
||||
QFile(input_fileinfo.absoluteFilePath()).remove();
|
||||
if (same_path && same_extension) {
|
||||
QFile(output_fileinfo.absoluteFilePath())
|
||||
.rename(input_fileinfo.fileName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TranscodeDialog::UpdateProgress() {
|
||||
int progress = (finished_success_ + finished_failed_) * 100;
|
||||
|
||||
QMap<QString, float> current_jobs = transcoder_->GetProgress();
|
||||
QMap<QUrl, float> current_jobs = transcoder_->GetProgress();
|
||||
for (float value : current_jobs.values()) {
|
||||
progress += qBound(0, int(value * 100), 99);
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ class TranscodeDialog : public QDialog {
|
|||
void Remove();
|
||||
void Start();
|
||||
void Cancel();
|
||||
void JobComplete(const QString& input, const QString& output, bool success);
|
||||
void JobComplete(const QUrl& input, const QString& output, bool success);
|
||||
void LogLine(const QString& message);
|
||||
void AllJobsComplete();
|
||||
void Options();
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>499</width>
|
||||
<height>448</height>
|
||||
<height>482</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -165,6 +165,16 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="remove_original">
|
||||
<property name="toolTip">
|
||||
<string>If enabled the original files will be removed. If transcoded files have the same file extension and the destination is the same directory as the original files, the original files will be replaced.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove or replace original files </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
|
@ -32,6 +32,11 @@
|
|||
|
||||
using std::shared_ptr;
|
||||
|
||||
static QString UrlToLocalFileIfPossible(QUrl url) {
|
||||
if (url.isLocalFile()) return url.toLocalFile();
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
int Transcoder::JobFinishedEvent::sEventType = -1;
|
||||
|
||||
TranscoderPreset::TranscoderPreset(Song::FileType type, const QString& name,
|
||||
|
@ -210,8 +215,7 @@ void Transcoder::JobState::PostFinished(bool success) {
|
|||
}
|
||||
|
||||
QString Transcoder::JobState::GetDisplayName() {
|
||||
return QFileInfo(job_.input).fileName() + " => " +
|
||||
QFileInfo(job_.output).fileName();
|
||||
return job_.input.fileName() + " => " + QFileInfo(job_.output).fileName();
|
||||
}
|
||||
|
||||
Transcoder::Transcoder(QObject* parent, const QString& settings_postfix)
|
||||
|
@ -318,8 +322,8 @@ Song::FileType Transcoder::PickBestFormat(QList<Song::FileType> supported) {
|
|||
return supported[0];
|
||||
}
|
||||
|
||||
void Transcoder::AddJob(const QString& input, const TranscoderPreset& preset,
|
||||
const QString& output) {
|
||||
void Transcoder::AddJob(const QUrl& input, const TranscoderPreset& preset,
|
||||
const QString& output, bool overwrite_existing) {
|
||||
Job job;
|
||||
job.input = input;
|
||||
job.preset = preset;
|
||||
|
@ -329,10 +333,11 @@ void Transcoder::AddJob(const QString& input, const TranscoderPreset& preset,
|
|||
if (!output.isEmpty())
|
||||
job.output = output;
|
||||
else
|
||||
job.output = input.section('.', 0, -2) + '.' + preset.extension_;
|
||||
job.output = UrlToLocalFileIfPossible(input).section('.', 0, -2) + '.' +
|
||||
preset.extension_;
|
||||
|
||||
// Never overwrite existing files
|
||||
if (QFile::exists(job.output)) {
|
||||
// Don't overwrite existing files if overwrite_existing is not set
|
||||
if (!overwrite_existing && QFile::exists(job.output)) {
|
||||
for (int i = 0;; ++i) {
|
||||
QString new_filename = QString("%1.%2.%3")
|
||||
.arg(job.output.section('.', 0, -2))
|
||||
|
@ -348,14 +353,9 @@ void Transcoder::AddJob(const QString& input, const TranscoderPreset& preset,
|
|||
queued_jobs_ << job;
|
||||
}
|
||||
|
||||
void Transcoder::AddTemporaryJob(const QString& input,
|
||||
void Transcoder::AddTemporaryJob(const QUrl& input,
|
||||
const TranscoderPreset& preset) {
|
||||
Job job;
|
||||
job.input = input;
|
||||
job.output = Utilities::GetTemporaryFileName();
|
||||
job.preset = preset;
|
||||
|
||||
queued_jobs_ << job;
|
||||
AddJob(input, preset, Utilities::GetTemporaryFileName());
|
||||
}
|
||||
|
||||
void Transcoder::Start() {
|
||||
|
@ -432,15 +432,20 @@ void Transcoder::JobState::ReportError(GstMessage* msg) {
|
|||
g_error_free(error);
|
||||
free(debugs);
|
||||
|
||||
// clean up output file if it was already created
|
||||
if (QFile::exists(job_.output)) {
|
||||
QFile::remove(job_.output);
|
||||
}
|
||||
|
||||
emit parent_->LogLine(
|
||||
tr("Error processing %1: %2")
|
||||
.arg(QDir::toNativeSeparators(job_.input), message));
|
||||
.arg(UrlToLocalFileIfPossible(job_.input), message));
|
||||
}
|
||||
|
||||
bool Transcoder::StartJob(const Job& job) {
|
||||
shared_ptr<JobState> state(new JobState(job, this));
|
||||
|
||||
emit LogLine(tr("Starting %1").arg(QDir::toNativeSeparators(job.input)));
|
||||
emit LogLine(tr("Starting %1").arg(UrlToLocalFileIfPossible(job.input)));
|
||||
|
||||
// Create the pipeline.
|
||||
// This should be a scoped_ptr, but scoped_ptr doesn't support custom
|
||||
|
@ -448,8 +453,7 @@ bool Transcoder::StartJob(const Job& job) {
|
|||
if (!state->Init()) return false;
|
||||
|
||||
// Create all the elements
|
||||
GstElement* src = CreateElement("filesrc", state->Pipeline());
|
||||
GstElement* decode = CreateElement("decodebin", state->Pipeline());
|
||||
GstElement* decode = CreateElement("uridecodebin", state->Pipeline());
|
||||
GstElement* convert = CreateElement("audioconvert", state->Pipeline());
|
||||
GstElement* resample = CreateElement("audioresample", state->Pipeline());
|
||||
GstElement* codec = CreateElementForMimeType(
|
||||
|
@ -458,7 +462,7 @@ bool Transcoder::StartJob(const Job& job) {
|
|||
"Codec/Muxer", job.preset.muxer_mimetype_, state->Pipeline());
|
||||
GstElement* sink = CreateElement("filesink", state->Pipeline());
|
||||
|
||||
if (!src || !decode || !convert || !sink) return false;
|
||||
if (!decode || !convert || !sink) return false;
|
||||
|
||||
if (!codec && !job.preset.codec_mimetype_.isEmpty()) {
|
||||
emit LogLine(
|
||||
|
@ -476,7 +480,6 @@ bool Transcoder::StartJob(const Job& job) {
|
|||
}
|
||||
|
||||
// Join them together
|
||||
gst_element_link(src, decode);
|
||||
if (codec && muxer)
|
||||
gst_element_link_many(convert, resample, codec, muxer, sink, nullptr);
|
||||
else if (codec)
|
||||
|
@ -485,9 +488,14 @@ bool Transcoder::StartJob(const Job& job) {
|
|||
gst_element_link_many(convert, resample, muxer, sink, nullptr);
|
||||
|
||||
// Set properties
|
||||
g_object_set(src, "location", job.input.toUtf8().constData(), nullptr);
|
||||
g_object_set(decode, "uri", job.input.toString().toUtf8().constData(),
|
||||
nullptr);
|
||||
g_object_set(sink, "location", job.output.toUtf8().constData(), nullptr);
|
||||
|
||||
// Create target directory, if it does not exist
|
||||
QFileInfo output_file_path(job.output);
|
||||
output_file_path.dir().mkpath(".");
|
||||
|
||||
// Set callbacks
|
||||
state->convert_element_ = convert;
|
||||
|
||||
|
@ -524,7 +532,7 @@ bool Transcoder::event(QEvent* e) {
|
|||
return true;
|
||||
}
|
||||
|
||||
QString input = (*it)->job_.input;
|
||||
QUrl input = (*it)->job_.input;
|
||||
QString output = (*it)->job_.output;
|
||||
|
||||
// Remove event handlers from the gstreamer pipeline so they don't get
|
||||
|
@ -587,8 +595,8 @@ void Transcoder::DumpGraph(int id) {
|
|||
}
|
||||
}
|
||||
|
||||
QMap<QString, float> Transcoder::GetProgress() const {
|
||||
QMap<QString, float> ret;
|
||||
QMap<QUrl, float> Transcoder::GetProgress() const {
|
||||
QMap<QUrl, float> ret;
|
||||
|
||||
for (const auto& state : current_jobs_) {
|
||||
if (!state->Pipeline()) continue;
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#include <QMetaType>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <memory>
|
||||
|
||||
#include "core/song.h"
|
||||
|
@ -70,11 +71,12 @@ class Transcoder : public QObject {
|
|||
int max_threads() const { return max_threads_; }
|
||||
void set_max_threads(int count) { max_threads_ = count; }
|
||||
|
||||
void AddJob(const QString& input, const TranscoderPreset& preset,
|
||||
const QString& output = QString());
|
||||
void AddTemporaryJob(const QString& input, const TranscoderPreset& preset);
|
||||
void AddJob(const QUrl& input, const TranscoderPreset& preset,
|
||||
const QString& output = QString(),
|
||||
bool overwrite_existing = false);
|
||||
void AddTemporaryJob(const QUrl& input, const TranscoderPreset& preset);
|
||||
|
||||
QMap<QString, float> GetProgress() const;
|
||||
QMap<QUrl, float> GetProgress() const;
|
||||
int QueuedJobsCount() const { return queued_jobs_.count(); }
|
||||
|
||||
GstPipelineModel* model() { return model_; }
|
||||
|
@ -85,7 +87,7 @@ class Transcoder : public QObject {
|
|||
|
||||
static QString GetEncoderFactoryForMimeType(const QString& mime_type);
|
||||
signals:
|
||||
void JobComplete(const QString& input, const QString& output, bool success);
|
||||
void JobComplete(const QUrl& input, const QString& output, bool success);
|
||||
void LogLine(const QString& message);
|
||||
void AllJobsComplete();
|
||||
|
||||
|
@ -95,7 +97,7 @@ class Transcoder : public QObject {
|
|||
private:
|
||||
// The description of a file to transcode - lives in the main thread.
|
||||
struct Job {
|
||||
QString input;
|
||||
QUrl input;
|
||||
QString output;
|
||||
TranscoderPreset preset;
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue