Compare commits

...

234 Commits

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

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

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

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

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

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

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

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

The specific details of the block analyzer and explanations for how it works
(and used to work) have been documented via fairly extensive comments
in blockanalyzer.cpp.
2022-09-01 19:36:03 +01:00
John Maguire 71eac9bb3b Remove support for FC34 2022-09-01 19:35:27 +01:00
John Maguire 3fd467591a Force GIT_REV in Fedora RPM builds 2022-09-01 19:00:51 +01:00
John Maguire a0ae9210dd Try using %{version} for RPM builds 2022-09-01 19:00:51 +01:00
John Maguire c1fa38120d Add git hackery for all builds 2022-09-01 19:00:51 +01:00
John Maguire 13352c5802 Git hackery to make git describe work 2022-09-01 19:00:51 +01:00
John Maguire 5e5b888d41 Make non-zero from `git describe` fatal 2022-09-01 19:00:51 +01:00
John Maguire 662ac60eb1 Add debug messages for git rev versioning 2022-09-01 19:00:51 +01:00
John Maguire 9be5b9805d Fix typo 2022-09-01 19:00:51 +01:00
John Maguire 9de903d42d Remove useless config 2022-09-01 19:00:51 +01:00
John Maguire 454678256e Try `dh_installgsettings` 2022-09-01 19:00:51 +01:00
John Maguire d3c847b38c Build for Ubuntu 22.04 Jammy Jellyfish
Deprecate support for Ubuntu 21.04 Hirsute Hippo
2022-09-01 19:00:51 +01:00
John Maguire 398893117e Remove obsolete Ubuntu builds 2022-09-01 19:00:51 +01:00
Lorenz Bausch bbda59a5f3 Build RPMs for Fedora 36 2022-05-18 14:23:11 +01:00
John Maguire bebd0b5d3c Remove mms plugin from mac build
Removed from upstream gstreamer
2022-05-18 14:21:58 +01:00
Clementine Buildbot 250024e117 Automatic merge of translations from Transifex 2022-04-11 02:56:58 +00:00
Clementine Buildbot 9168299c0f Automatic merge of translations from Transifex 2022-04-10 02:49:22 +00:00
Clementine Buildbot 24d4b6e7f2 Automatic merge of translations from Transifex 2022-04-09 02:44:35 +00:00
Clementine Buildbot 644405ec7a Automatic merge of translations from Transifex 2022-04-06 02:49:01 +00:00
Clementine Buildbot 2fb964fc29 Automatic merge of translations from Transifex 2022-04-05 02:47:59 +00:00
Clementine Buildbot cf31624836 Automatic merge of translations from Transifex 2022-03-30 03:10:46 +00:00
Clementine Buildbot d05616e37c Automatic merge of translations from Transifex 2022-03-29 02:51:19 +00:00
Clementine Buildbot 0b5faa7550 Automatic merge of translations from Transifex 2022-03-28 02:51:09 +00:00
Clementine Buildbot c0b42ace6d Automatic merge of translations from Transifex 2022-03-27 02:40:50 +00:00
Clementine Buildbot 810f0b0acb Automatic merge of translations from Transifex 2022-03-26 02:41:48 +00:00
Clementine Buildbot c2b8a35642 Automatic merge of translations from Transifex 2022-03-25 02:40:45 +00:00
Clementine Buildbot 2b340da79f Automatic merge of translations from Transifex 2022-03-24 02:44:40 +00:00
Clementine Buildbot 6698723991 Automatic merge of translations from Transifex 2022-03-23 02:48:42 +00:00
Clementine Buildbot 7175ee4d37 Automatic merge of translations from Transifex 2022-03-17 02:38:47 +00:00
Clementine Buildbot 20c6ae6c14 Automatic merge of translations from Transifex 2022-03-12 02:48:05 +00:00
Clementine Buildbot 59d1c94b90 Automatic merge of translations from Transifex 2022-03-09 02:54:15 +00:00
Clementine Buildbot 9d143334e2 Automatic merge of translations from Transifex 2022-03-04 02:57:35 +00:00
Clementine Buildbot 4797edbc8a Automatic merge of translations from Transifex 2022-03-03 02:56:13 +00:00
Clementine Buildbot 01f72b575d Automatic merge of translations from Transifex 2022-03-02 02:52:49 +00:00
Clementine Buildbot dcbb3f8a58 Automatic merge of translations from Transifex 2022-03-01 02:58:38 +00:00
Clementine Buildbot 3acf26015b Automatic merge of translations from Transifex 2022-02-21 02:43:40 +00:00
Clementine Buildbot 333203c972 Automatic merge of translations from Transifex 2022-02-18 02:48:18 +00:00
Clementine Buildbot 63b806dbb7 Automatic merge of translations from Transifex 2022-02-16 02:47:09 +00:00
Clementine Buildbot a8d529ca14 Automatic merge of translations from Transifex 2022-02-11 02:46:32 +00:00
Clementine Buildbot 111379dfd0 Automatic merge of translations from Transifex 2022-02-08 02:47:41 +00:00
Clementine Buildbot 4821bd50c2 Automatic merge of translations from Transifex 2022-02-06 02:50:42 +00:00
Clementine Buildbot c3a0bd69fd Automatic merge of translations from Transifex 2022-02-05 02:31:12 +00:00
Clementine Buildbot 5487d0632c Automatic merge of translations from Transifex 2022-02-04 02:32:02 +00:00
Clementine Buildbot 98b68afc28 Automatic merge of translations from Transifex 2022-02-01 02:47:06 +00:00
Clementine Buildbot 15b819fea3 Automatic merge of translations from Transifex 2022-01-30 02:45:07 +00:00
Clementine Buildbot e2f6ec8e12 Automatic merge of translations from Transifex 2022-01-27 02:45:40 +00:00
Clementine Buildbot efa0530ed9 Automatic merge of translations from Transifex 2022-01-26 02:49:13 +00:00
Lukas Prediger a504c1d391 RipCDDialog: References to pointers in function args. 2022-01-26 01:09:50 +00:00
Lukas Prediger 794c1b8c92 Ripper: Ensuring that GetProgress does never divide by zero
Also removed superfluous null check in RipCDDialog
2022-01-26 01:09:50 +00:00
Lukas Prediger f35e1b543d Regular progress bar updates for CD ripping.
Previously the progress bar of the CD ripping dialog would only update
after a track completed, now it gets updated continuously during the
ripping process.
2022-01-26 01:09:50 +00:00
Clementine Buildbot 497552aab2 Automatic merge of translations from Transifex 2022-01-23 02:44:36 +00:00
Clementine Buildbot 9487f67f64 Automatic merge of translations from Transifex 2022-01-22 02:46:46 +00:00
Clementine Buildbot 19a86ba2e4 Automatic merge of translations from Transifex 2022-01-21 02:46:43 +00:00
Lukas Prediger 1aaf74788c Added option to remove/replace originals in transcoder dialog (fixed).
This is a squashed and fixed version of previous commits
6b6547095a
dd1393ea3a
2022-01-20 11:43:07 +00:00
Clementine Buildbot 7ce9928779 Automatic merge of translations from Transifex 2022-01-19 02:45:01 +00:00
Clementine Buildbot af890f0736 Automatic merge of translations from Transifex 2022-01-18 02:52:40 +00:00
John Maguire 09ccf93b98 Remove builds for unsupported FC33 2022-01-17 12:01:15 +00:00
Clementine Buildbot f237795850 Automatic merge of translations from Transifex 2022-01-17 02:54:02 +00:00
Lukasz Kryger d79f837ddb Update wiki link in the "compiling" section 2022-01-13 23:20:53 +00:00
John Maguire e69ceb25df Revert "Added option to remove/replace originals in transcoder dialog."
This reverts commit 6b6547095a.
2022-01-13 22:16:39 +00:00
John Maguire ab37de5e8f Revert "Transcodedialog: moving things into more specific scope"
This reverts commit dd1393ea3a.
2022-01-13 22:16:39 +00:00
Lukas Prediger dd1393ea3a Transcodedialog: moving things into more specific scope 2022-01-13 20:38:19 +00:00
Lukas Prediger 6b6547095a Added option to remove/replace originals in transcoder dialog. 2022-01-13 20:38:19 +00:00
Clementine Buildbot 24a766d0e5 Automatic merge of translations from Transifex 2022-01-11 02:53:36 +00:00
Lukas Prediger cefe81d0c1 Removing Ripper dependence on cdio
and therefore no longer exposing cdio through CddaDevice
2022-01-10 16:39:58 +00:00
Lukas Prediger 0895297297 CD ripping now transcodes directly from disc. 2022-01-10 16:39:58 +00:00
Lukas Prediger 3a40be6706 Transcoder now deletes created files on error 2022-01-10 16:39:58 +00:00
Lukas Prediger bb618efc5d Transcoder now accepts URLs for sources. 2022-01-10 16:39:58 +00:00
Clementine Buildbot 245f64a882 Automatic merge of translations from Transifex 2022-01-08 02:54:48 +00:00
Clementine Buildbot 0be314337d Automatic merge of translations from Transifex 2022-01-06 02:55:06 +00:00
Clementine Buildbot 63eb7aa743 Automatic merge of translations from Transifex 2021-12-30 02:50:28 +00:00
Clementine Buildbot 9dd008da2c Automatic merge of translations from Transifex 2021-12-27 02:51:25 +00:00
Clementine Buildbot b1e750c52c Automatic merge of translations from Transifex 2021-12-13 02:49:24 +00:00
Clementine Buildbot 41539d0c02 Automatic merge of translations from Transifex 2021-12-10 02:48:17 +00:00
Clementine Buildbot 44dbc95554 Automatic merge of translations from Transifex 2021-12-09 02:49:21 +00:00
Clementine Buildbot 1d8139e462 Automatic merge of translations from Transifex 2021-12-08 02:49:59 +00:00
Clementine Buildbot 2d0518a5a8 Automatic merge of translations from Transifex 2021-12-07 02:51:20 +00:00
Clementine Buildbot d5986a4820 Automatic merge of translations from Transifex 2021-11-29 02:48:28 +00:00
John Maguire 590ab22f8d Remove unsupported Ubuntu groovy builds 2021-11-18 12:36:13 +00:00
Clementine Buildbot b747423b5a Automatic merge of translations from Transifex 2021-11-13 02:46:17 +00:00
Clementine Buildbot 15e45c9ec6 Automatic merge of translations from Transifex 2021-11-11 02:46:39 +00:00
John Maguire d033b38c4b Revert "Fix: GstEnginePipeline BusCallback erroneously returned false."
This reverts commit 7b8b477d07.
2021-11-05 16:54:05 +00:00
Clementine Buildbot 2469763b9b Automatic merge of translations from Transifex 2021-11-03 02:45:57 +00:00
John Maguire e7b1c06341 Build for Fedora Core 35 2021-11-02 15:51:02 +00:00
Clementine Buildbot a25887be5c Automatic merge of translations from Transifex 2021-11-02 02:47:55 +00:00
John Maguire 568ff1f9da Build for Ubuntu Impish 21.10 2021-10-31 13:04:06 +00:00
Clementine Buildbot 174fc515ee Automatic merge of translations from Transifex 2021-10-31 02:47:03 +00:00
Lukas Prediger 7b8b477d07 Fix: GstEnginePipeline BusCallback erroneously returned false. 2021-10-30 13:46:10 +01:00
Clementine Buildbot b9dbcb78db Automatic merge of translations from Transifex 2021-10-30 02:45:50 +00:00
Clementine Buildbot c29b1e10d2 Automatic merge of translations from Transifex 2021-10-29 02:45:29 +00:00
Clementine Buildbot f8c167c9c6 Automatic merge of translations from Transifex 2021-10-28 02:46:25 +00:00
Clementine Buildbot e5023535d2 Automatic merge of translations from Transifex 2021-10-26 02:44:59 +00:00
Alfred 1b3b621957 add tooltips to IconOnly tabs 2021-10-25 16:21:14 +02:00
Clementine Buildbot 2dc8df7e23 Automatic merge of translations from Transifex 2021-10-23 02:47:38 +00:00
Clementine Buildbot 4eebf5747d Automatic merge of translations from Transifex 2021-10-20 02:46:38 +00:00
Clementine Buildbot c24927a03b Automatic merge of translations from Transifex 2021-10-19 02:47:25 +00:00
Clementine Buildbot 424dbd44e8 Automatic merge of translations from Transifex 2021-10-18 02:47:57 +00:00
Clementine Buildbot 68bc9d9ebb Automatic merge of translations from Transifex 2021-10-17 02:45:03 +00:00
Clementine Buildbot 294620fe66 Automatic merge of translations from Transifex 2021-10-15 02:45:17 +00:00
Clementine Buildbot 21f038c156 Automatic merge of translations from Transifex 2021-10-14 02:45:24 +00:00
Lukas Prediger 5705d4fd85 Fix: Detaching bus callback in ~GstEnginePipeline 2021-10-09 12:07:56 +01:00
Clementine Buildbot 86b958015b Automatic merge of translations from Transifex 2021-10-09 02:44:42 +00:00
Clementine Buildbot f8f849e49c Automatic merge of translations from Transifex 2021-10-06 02:44:29 +00:00
Clementine Buildbot 69fd49b977 Automatic merge of translations from Transifex 2021-09-29 02:42:17 +00:00
Clementine Buildbot 62922147e6 Automatic merge of translations from Transifex 2021-09-28 02:44:15 +00:00
Clementine Buildbot 2e133f7ce4 Automatic merge of translations from Transifex 2021-09-27 02:44:15 +00:00
Clementine Buildbot 0820035b84 Automatic merge of translations from Transifex 2021-09-25 02:45:40 +00:00
Clementine Buildbot 679a0ee544 Automatic merge of translations from Transifex 2021-09-24 02:44:30 +00:00
Clementine Buildbot 8715815452 Automatic merge of translations from Transifex 2021-09-23 02:45:20 +00:00
Clementine Buildbot b762aeb1ba Automatic merge of translations from Transifex 2021-09-22 02:45:17 +00:00
Lukas Prediger fd585e8aa4 RipCDDialog: no longer forces Cdda* to emit signals
- CddaDevice: Removed LoadSongs() method (then renamed ForceLoadSongs to LoadSongs)
- CddaDevice: added songs() method to get currently song list
- CddaSongLoader: cached_tracks is now thread-safe
2021-09-21 10:53:43 +01:00
Lukas Prediger 2936578fa4 CddaDevice: Preventing double read on disc change. 2021-09-21 10:53:43 +01:00
Lukas Prediger 62b5a0e77b CddaDevice now does not re-read song list if disc is not changed 2021-09-21 10:53:43 +01:00
Lukas Prediger 50404a967b RipCDDialog now only depends on CddaDevice, no longer on CddaSongLoader 2021-09-21 10:53:43 +01:00
Lukas Prediger 6b03b8f5d1 CddaSongLoader now emits a Finished signal if no further updates will follow for the same disc read. 2021-09-21 10:53:43 +01:00
Lukas Prediger 90ec6f6a24 CddaSongLoader now reads CD-Text for metadata
currently this gets overwritten by musicbrainz response almost immediately, though
2021-09-21 10:53:43 +01:00
Clementine Buildbot b020171da7 Automatic merge of translations from Transifex 2021-09-21 02:44:04 +00:00
Clementine Buildbot c969bf9783 Automatic merge of translations from Transifex 2021-09-20 02:44:43 +00:00
Jim Broadus 082f941bb9 build: Fix translations
transifex-client indirectly requires Unidecode>=0.04.16. As of version
1.3.2, Unidecode no longer supports Python 2. Its setup shebang points
to /usr/bin/python, which it assumes is python3. To correct the issue,
install pip3 instead of pip, avoiding the installation of Python 2.
2021-09-19 15:28:40 +01:00
Tom Kranz cd72cf3390 Use XSPF image elements as manually set artwork 2021-09-18 23:52:37 +01:00
Jim Broadus ba29b0e3ba build: Add Debian Bullseye.
Reference: https://www.debian.org/releases/bullseye/
2021-09-12 00:31:48 +01:00
Daniel Perelman ab6a480131 Apply reformatting from lint. 2021-09-07 18:04:38 +01:00
Daniel Perelman f548884f57 Correct time computation for seekbar tooltip. 2021-09-07 18:04:38 +01:00
Clementine Buildbot 1535e78aa0 Automatic merge of translations from Transifex 2021-09-06 02:44:55 +00:00
Clementine Buildbot 2cca75d930 Automatic merge of translations from Transifex 2021-09-05 02:43:23 +00:00
Lukas Prediger e556a59aea RipCDDialog: Changing metadata edits now updates filename preview 2021-09-04 15:24:44 +01:00
Lukas Prediger e187a68e9f MusicBrainzClient fix: Don't try to read reply on connection timeout
that previously resulted in an IO error
2021-09-04 15:24:44 +01:00
Clementine Buildbot c58335c6c9 Automatic merge of translations from Transifex 2021-09-04 02:44:48 +00:00
Clementine Buildbot 769d8bbe6d Automatic merge of translations from Transifex 2021-09-03 02:43:09 +00:00
Clementine Buildbot 3b7d5880f9 Automatic merge of translations from Transifex 2021-09-02 02:42:38 +00:00
Clementine Buildbot 7eb62b6266 Automatic merge of translations from Transifex 2021-09-01 02:44:42 +00:00
Ismael Luceno 628ff65828 logging: Add proper backtrace support detection
The Q_OS_UNIX macro is inappropriate because many UNIX-like platforms may
lack backtrace support in the libc. E.g.: Darwin / Mac OS X, Musl libc,
OpenBSD, OpenIndiana.
2021-08-31 11:15:55 +01:00
Lukas Prediger c8c110efaf Fix: RipCDDialog now correctly loads and stores chosen transcoder preset 2021-08-31 10:19:32 +01:00
Lukas Prediger a72e252ec6 RipCDDialog: Added file name preview 2021-08-31 10:19:32 +01:00
Lukas Prediger b0704331d7 Integrate file name format options into RipCDDialog
for consistency with OrganiseDialog and reducing code duplication
2021-08-31 10:19:32 +01:00
Lukas Prediger 5c8ca3754f Added overwrite_existing argument to Transcoder::AddJob 2021-08-31 10:19:32 +01:00
Lukas Prediger 83d961f808 Transcoder: create output paths if they do not already exist 2021-08-31 10:19:32 +01:00
Lukas Prediger a6fef97cac Separating out filename formatting options into separate widget.
First step towards unifying filename formatting over different dialogs.
2021-08-31 10:19:32 +01:00
Clementine Buildbot 922afe506f Automatic merge of translations from Transifex 2021-08-27 02:44:07 +00:00
Ismael Luceno 8682d4de48 Fix sentinels in variadic function calls
Replace sentinel NULL with nullptr, guaranteed to be correctly expanded.

NULL may be defined as plain 0 in C++; which may lead to undefined upper
bits passed in variadic function arguments, causing crashes.

See: https://ewontfix.com/11/
2021-08-23 21:33:46 +01:00
Clementine Buildbot 03e13c69e7 Automatic merge of translations from Transifex 2021-08-17 02:42:46 +00:00
Clementine Buildbot 327d5fdac3 Automatic merge of translations from Transifex 2021-08-13 02:42:58 +00:00
Clementine Buildbot b55e54388f Automatic merge of translations from Transifex 2021-08-11 02:43:14 +00:00
Clementine Buildbot cddc08e148 Automatic merge of translations from Transifex 2021-08-09 02:42:51 +00:00
Clementine Buildbot bf424ce986 Automatic merge of translations from Transifex 2021-08-01 02:46:18 +00:00
Clementine Buildbot e2d6759d55 Automatic merge of translations from Transifex 2021-07-27 02:45:37 +00:00
Jim Broadus 102317e5c8 internet/podcasts: Fix crash when updating podcasts.
When a podcast is updated and the number of visible items is set in the
podcast settings, child items that disappear from the view, and are
deleted, are still referenced by the database id map.

Move the removal code from SubscriptionRemoved to a common method and
use that for this case.
2021-07-26 12:36:24 +01:00
Clementine Buildbot ac3a0d33f7 Automatic merge of translations from Transifex 2021-07-26 02:45:57 +00:00
Robin Lee 224c475b50 Rename multiple files to match the application ID
The desktop entry file, appdata file and installed icons are renamed to
match the application ID for the benefit of Flatpak packaging.
2021-07-20 11:21:05 +01:00
Clementine Buildbot dbe15e5e9f Automatic merge of translations from Transifex 2021-07-20 02:45:18 +00:00
Clementine Buildbot c0c9037677 Automatic merge of translations from Transifex 2021-07-19 02:44:08 +00:00
179 changed files with 34881 additions and 235071 deletions

View File

@ -10,7 +10,7 @@ on:
jobs:
lint:
name: Lint
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
@ -25,10 +25,15 @@ jobs:
push_translations:
name: Push translation sources to Transifex
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
container:
image: ubuntu:bionic
steps:
- uses: supplypike/setup-bin@v3
with:
uri: https://github.com/transifex/cli/releases/download/v1.6.7/tx-linux-amd64.tar.gz
name: tx
version: 1.6.7
- name: Install dependencies
env:
DEBIAN_FRONTEND: noninteractive
@ -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

View File

@ -6,36 +6,40 @@ on:
jobs:
pull_translations:
name: Pull translations from Transifex
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
container:
image: ubuntu:bionic
image: ubuntu:jammy
steps:
- uses: supplypike/setup-bin@v3
with:
uri: https://github.com/transifex/cli/releases/download/v1.6.7/tx-linux-amd64.tar.gz
name: tx
version: 1.6.7
- name: Install dependencies
env:
DEBIAN_FRONTEND: noninteractive
run: >
apt-get update && apt-get install -y
git
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:

View File

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

View File

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

View File

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

View File

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

View File

@ -41,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

View File

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

View File

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

2
debian/rules.in vendored
View File

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

12
dist/CMakeLists.txt vendored
View File

@ -22,7 +22,7 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/clementine.nsi.in
# windows/windres.rc is done by src/CMakeLists.txt
set(APP_ID "org.clementine_player.Clementine")
if(EXISTS /etc/lsb-release)
file(READ "/etc/lsb-release" LSB_RELEASE_CONTENTS)
string(REGEX MATCH "DISTRIB_ID=Ubuntu" IS_UBUNTU ${LSB_RELEASE_CONTENTS})
@ -40,20 +40,20 @@ option(INSTALL_UBUNTU_ICONS "Install the Ubuntu themed monochrome panel icons" $
if (NOT APPLE)
install(FILES clementine_64.png
DESTINATION share/icons/hicolor/64x64/apps/
RENAME clementine.png
RENAME ${APP_ID}.png
)
install(FILES clementine_128.png
DESTINATION share/icons/hicolor/128x128/apps/
RENAME clementine.png
RENAME ${APP_ID}.png
)
install(FILES ../data/icon.svg
DESTINATION share/icons/hicolor/scalable/apps/
RENAME clementine.svg
RENAME ${APP_ID}.svg
)
install(FILES clementine.desktop
install(FILES ${APP_ID}.desktop
DESTINATION share/applications
)
@ -64,7 +64,7 @@ if (NOT APPLE)
DESTINATION share/kservices5
)
install(FILES clementine.appdata.xml
install(FILES ${APP_ID}.appdata.xml
DESTINATION share/metainfo
)

View File

@ -18,6 +18,7 @@ BuildRequires: liblastfm-qt5-devel
BuildRequires: desktop-file-utils
BuildRequires: hicolor-icon-theme
BuildRequires: libappstream-glib
BuildRequires: qtsingleapplication-qt5-devel
BuildRequires: pkgconfig
BuildRequires: pkgconfig(glib-2.0)
BuildRequires: pkgconfig(gio-2.0)
@ -97,7 +98,7 @@ Features include:
%build
cd bin
%{cmake} .. -DUSE_INSTALL_PREFIX=OFF -DBUNDLE_PROJECTM_PRESETS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON
%{cmake} .. -DUSE_INSTALL_PREFIX=OFF -DBUNDLE_PROJECTM_PRESETS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DFORCE_GIT_REVISION=@GIT_REV@
%{cmake_build}
%install
@ -112,16 +113,16 @@ rm -f $RPM_BUILD_ROOT/usr/share/icons/ubuntu-mono-{dark,light}/apps/24/clementin
%{_bindir}/clementine
%{_bindir}/clementine-tagreader
%dir %{_datadir}/metainfo/
%{_datadir}/metainfo/clementine.appdata.xml
%{_datadir}/applications/clementine.desktop
%{_datadir}/metainfo/org.clementine_player.Clementine.appdata.xml
%{_datadir}/applications/org.clementine_player.Clementine.desktop
%{_datadir}/clementine/projectm-presets
%{_datadir}/kservices5/clementine-itms.protocol
%{_datadir}/kservices5/clementine-itpc.protocol
%{_datadir}/kservices5/clementine-feed.protocol
%{_datadir}/kservices5/clementine-zune.protocol
%{_datadir}/icons/hicolor/64x64/apps/clementine.png
%{_datadir}/icons/hicolor/128x128/apps/clementine.png
%{_datadir}/icons/hicolor/scalable/apps/clementine.svg
%{_datadir}/icons/hicolor/64x64/apps/org.clementine_player.Clementine.png
%{_datadir}/icons/hicolor/128x128/apps/org.clementine_player.Clementine.png
%{_datadir}/icons/hicolor/scalable/apps/org.clementine_player.Clementine.svg
%changelog
* @RPM_DATE@ David Sansome <me@davidsansome.com> - @CLEMENTINE_VERSION_RPM_V@

1
dist/macdeploy.py vendored
View File

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

View File

@ -2,7 +2,7 @@
<!-- Copyright 2014 David Sansome <me@davidsansome.com> -->
<application>
<id>org.clementine_player.Clementine</id>
<id type="desktop">clementine.desktop</id>
<id type="desktop">org.clementine_player.Clementine.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>Clementine Music Player</name>

View File

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

View File

@ -7,7 +7,7 @@ import polib
import re
PO_GLOB = 'src/translations/*.po'
DESKTOP_PATH = 'dist/clementine.desktop'
DESKTOP_PATH = 'dist/org.clementine_player.Clementine.desktop'
class ConfigParser(object):
"""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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)

View File

@ -0,0 +1,4 @@
#cmakedefine Backtrace_FOUND
#ifdef Backtrace_FOUND
#include <@Backtrace_HEADER@>
#endif

View File

@ -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));

View File

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

View File

@ -1,23 +0,0 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SPOTIFY_BLOBVERSION_H_IN
#define SPOTIFY_BLOBVERSION_H_IN
#define SPOTIFY_BLOB_VERSION ${SPOTIFY_BLOB_VERSION}
#endif // SPOTIFY_BLOBVERSION_H_IN

View File

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

View File

@ -72,8 +72,8 @@ parts:
cmake ../src -DCMAKE_INSTALL_PREFIX=/usr
make -j $(getconf _NPROCESSORS_ONLN)
make DESTDIR=$SNAPCRAFT_PART_INSTALL install
sed -i 's|Icon=clementine|Icon=/usr/share/icons/hicolor/scalable/apps/clementine\.svg|' $SNAPCRAFT_PART_INSTALL/usr/share/applications/clementine.desktop
sed -i 's|TryExec=.*|TryExec=/snap/bin/clementine|' $SNAPCRAFT_PART_INSTALL/usr/share/applications/clementine.desktop
sed -i 's|Icon=clementine|Icon=/usr/share/icons/hicolor/scalable/apps/clementine\.svg|' $SNAPCRAFT_PART_INSTALL/usr/share/applications/org.clementine_player.Clementine.desktop
sed -i 's|TryExec=.*|TryExec=/snap/bin/clementine|' $SNAPCRAFT_PART_INSTALL/usr/share/applications/org.clementine_player.Clementine.desktop
build-packages:
- cmake
@ -170,7 +170,7 @@ parts:
apps:
clementine:
command: desktop-launch $SNAP/usr/bin/clementine
desktop: usr/share/applications/clementine.desktop
desktop: usr/share/applications/org.clementine_player.Clementine.desktop
environment:
ALSA_CONFIG_PATH: /snap/$SNAPCRAFT_PROJECT_NAME/current/usr/share/alsa/alsa.conf
LD_LIBRARY_PATH: $LD_LIBRARY_PATH:$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/pulseaudio

View File

@ -47,10 +47,6 @@ include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-tagreader)
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-tagreader)
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-remote)
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-remote)
if(HAVE_SPOTIFY)
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-spotifyblob)
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-spotifyblob)
endif(HAVE_SPOTIFY)
include(../cmake/ParseArguments.cmake)
@ -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

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

@ -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);

View File

@ -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);

View File

@ -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:

View File

@ -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) {

View File

@ -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:

View File

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

View File

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

View File

@ -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(); }

View File

@ -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:

View File

@ -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;
}

View File

@ -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

View File

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

View File

@ -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_;

View File

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

View File

@ -1,67 +0,0 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SPOTIFYSEARCHPROVIDER_H
#define SPOTIFYSEARCHPROVIDER_H
#include "internet/spotify/spotifyservice.h"
#include "searchprovider.h"
#include "spotifymessages.pb.h"
class SpotifyServer;
class SpotifySearchProvider : public SearchProvider {
Q_OBJECT
public:
SpotifySearchProvider(Application* app, QObject* parent = nullptr);
void SearchAsync(int id, const QString& query) override;
void LoadArtAsync(int id, const Result& result) override;
QStringList GetSuggestions(int count) override;
// SearchProvider
bool IsLoggedIn() override;
void ShowConfig() override;
InternetService* internet_service() override { return service_; }
private slots:
void ServerDestroyed();
void SearchFinishedSlot(const cpb::spotify::SearchResponse& response);
void ArtLoadedSlot(const QString& id, const QImage& image);
void SuggestionsLoaded(const cpb::spotify::LoadPlaylistResponse& response);
void SuggestionsLoaded(const cpb::spotify::BrowseToplistResponse& response);
private:
SpotifyServer* server();
void LoadSuggestions();
void AddSuggestionFromTrack(const cpb::spotify::Track& track);
void AddSuggestionFromAlbum(const cpb::spotify::Album& album);
private:
SpotifyServer* server_;
SpotifyService* service_;
QMap<QString, PendingState> queries_;
QMap<QString, int> pending_art_;
QMap<QString, int> pending_tracks_;
QSet<QString> suggestions_;
};
#endif // SPOTIFYSEARCHPROVIDER_H

View File

@ -1,97 +0,0 @@
/* This file is part of Clementine.
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "spotifywebapisearchprovider.h"
#include <qurl.h>
#include <iostream>
#include "internet/spotifywebapi/spotifywebapiservice.h"
#include "ui/iconloader.h"
namespace {
static constexpr int kNoRunningSearch = -1;
}
SpotifyWebApiSearchProvider::SpotifyWebApiSearchProvider(
Application* app, SpotifyWebApiService* parent)
: SearchProvider(app, parent),
parent_{parent},
last_search_id_{kNoRunningSearch} {
Init("Spotify (Experimential)", "spotify_web_api",
IconLoader::Load("spotify", IconLoader::Provider),
WantsDelayedQueries | WantsSerialisedArtQueries | ArtIsProbablyRemote |
CanGiveSuggestions);
connect(parent, &SpotifyWebApiService::SearchFinished, this,
&SpotifyWebApiSearchProvider::SearchFinishedSlot);
}
void SpotifyWebApiSearchProvider::SearchAsync(int id, const QString& query) {
if (last_search_id_ != kNoRunningSearch) {
// Cancel last pending search
emit SearchFinished(last_search_id_);
// Set the pending query
last_search_id_ = id;
last_query_ = query;
// And wait for the current search to be completed
return;
}
last_search_id_ = id;
last_query_ = query;
parent_->Search(last_search_id_, last_query_);
}
void SpotifyWebApiSearchProvider::SearchFinishedSlot(
int searchId, const QList<Song>& apiResult) {
ResultList ret;
for (auto&& item : apiResult) {
Result result{this};
result.group_automatically_ = true;
result.metadata_ = item;
ret += result;
}
emit ResultsAvailable(searchId, ret);
emit SearchFinished(searchId);
// Search again if we have a pending query
if (searchId != last_search_id_) {
parent_->Search(last_search_id_, last_query_);
} else {
last_search_id_ = kNoRunningSearch;
}
}
void SpotifyWebApiSearchProvider::LoadArtAsync(int id, const Result& result) {
// TODO
}
void SpotifyWebApiSearchProvider::ShowConfig() {}
InternetService* SpotifyWebApiSearchProvider::internet_service() {
return parent_;
}
QStringList SpotifyWebApiSearchProvider::GetSuggestions(int count) {
// TODO
return QStringList{};
}

View File

@ -1,48 +0,0 @@
/* This file is part of Clementine.
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SPOTIFYWEBAPISEARCHPROVIDER_H
#define SPOTIFYWEBAPISEARCHPROVIDER_H
#include "core/song.h"
#include "searchprovider.h"
class SpotifyWebApiService;
class SpotifyWebApiSearchProvider : public SearchProvider {
Q_OBJECT
public:
SpotifyWebApiSearchProvider(Application* app, SpotifyWebApiService* parent);
void SearchAsync(int id, const QString& query) override;
void LoadArtAsync(int id, const Result& result) override;
QStringList GetSuggestions(int count) override;
void ShowConfig() override;
InternetService* internet_service() override;
private slots:
void SearchFinishedSlot(int id, const QList<Song>&);
private:
SpotifyWebApiService* parent_;
int last_search_id_;
QString last_query_;
};
#endif // SPOTIFYWEBAPISEARCHPROVIDER_H

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -395,6 +395,20 @@ QStandardItem* PodcastService::CreatePodcastItem(const Podcast& podcast) {
return item;
}
void PodcastService::RemovePodcastItem(QStandardItem* item) {
// Remove any episode ID -> item mappings for the episodes in this podcast.
for (int i = 0; i < item->rowCount(); ++i) {
QStandardItem* episode_item = item->child(i);
const int episode_id =
episode_item->data(Role_Episode).value<PodcastEpisode>().database_id();
episodes_by_database_id_.remove(episode_id);
}
// Remove this podcast's row
model_->removeRow(item->row());
}
QStandardItem* PodcastService::CreatePodcastEpisodeItem(
const PodcastEpisode& episode) {
QStandardItem* item = new QStandardItem;
@ -621,18 +635,7 @@ void PodcastService::SubscriptionAdded(const Podcast& podcast) {
void PodcastService::SubscriptionRemoved(const Podcast& podcast) {
QStandardItem* item = podcasts_by_database_id_.take(podcast.database_id());
if (item) {
// Remove any episode ID -> item mappings for the episodes in this podcast.
for (int i = 0; i < item->rowCount(); ++i) {
QStandardItem* episode_item = item->child(i);
const int episode_id = episode_item->data(Role_Episode)
.value<PodcastEpisode>()
.database_id();
episodes_by_database_id_.remove(episode_id);
}
// Remove this episode's row
model_->removeRow(item->row());
RemovePodcastItem(item);
}
}
@ -859,6 +862,6 @@ void PodcastService::ReloadPodcast(const Podcast& podcast) {
}
QStandardItem* item = podcasts_by_database_id_[podcast.database_id()];
model_->invisibleRootItem()->removeRow(item->row());
RemovePodcastItem(item);
model_->invisibleRootItem()->appendRow(CreatePodcastItem(podcast));
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,70 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_
#define INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_
#include <QMap>
#include <QObject>
class QNetworkAccessManager;
class QNetworkReply;
class QProgressDialog;
class SpotifyBlobDownloader : public QObject {
Q_OBJECT
public:
SpotifyBlobDownloader(const QString& version, const QString& path,
QObject* parent = nullptr);
~SpotifyBlobDownloader();
static const char* kSignatureSuffix;
static bool Prompt();
void Start();
signals:
void Finished();
private slots:
void ReplyFinished();
void ReplyProgress();
void Cancel();
private:
void ShowError(const QString& message);
void EmitFinished();
bool CheckSignature(const QMap<QString, QByteArray>& file_data,
const QStringList& signature_filenames);
static QByteArray ConvertPEMToDER(const QByteArray& pem);
private:
QString version_;
QString path_;
QNetworkAccessManager* network_;
QList<QNetworkReply*> replies_;
QProgressDialog* progress_;
};
#endif // INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_

View File

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

View File

@ -1,112 +0,0 @@
/* This file is part of Clementine.
Copyright 2011-2012, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2011-2012, 2014, David Sansome <me@davidsansome.com>
Copyright 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
Copyright 2014, pie.or.paj <pie.or.paj@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INTERNET_SPOTIFY_SPOTIFYSERVER_H_
#define INTERNET_SPOTIFY_SPOTIFYSERVER_H_
#include <QImage>
#include <QObject>
#include "core/messagehandler.h"
#include "spotifymessages.pb.h"
class QTcpServer;
class QTcpSocket;
class SpotifyServer : public AbstractMessageHandler<cpb::spotify::Message> {
Q_OBJECT
public:
explicit SpotifyServer(QObject* parent = nullptr);
void Init();
void Login(const QString& username, const QString& password,
cpb::spotify::Bitrate bitrate, bool volume_normalisation);
void LoadStarred();
void SyncStarred();
void LoadInbox();
void SyncInbox();
void LoadUserPlaylist(int index);
void SyncUserPlaylist(int index);
void AddSongsToStarred(const QList<QUrl>& songs_urls);
void AddSongsToUserPlaylist(int playlist_index,
const QList<QUrl>& songs_urls);
void RemoveSongsFromUserPlaylist(int playlist_index,
const QList<int>& songs_indices_to_remove);
void RemoveSongsFromStarred(const QList<int>& songs_indices_to_remove);
void Search(const QString& text, int limit, int limit_album = 0);
void LoadImage(const QString& id);
void AlbumBrowse(const QString& uri);
void SetPlaybackSettings(cpb::spotify::Bitrate bitrate,
bool volume_normalisation);
void LoadToplist();
void SetPaused(const bool paused);
int server_port() const;
public slots:
void StartPlayback(const QString& uri, quint16 port);
void Seek(qint64 offset_nsec);
signals:
void LoginCompleted(bool success, const QString& error,
cpb::spotify::LoginResponse_Error error_code);
void PlaylistsUpdated(const cpb::spotify::Playlists& playlists);
void StarredLoaded(const cpb::spotify::LoadPlaylistResponse& response);
void InboxLoaded(const cpb::spotify::LoadPlaylistResponse& response);
void UserPlaylistLoaded(const cpb::spotify::LoadPlaylistResponse& response);
void PlaybackError(const QString& message);
void SearchResults(const cpb::spotify::SearchResponse& response);
void ImageLoaded(const QString& id, const QImage& image);
void SyncPlaylistProgress(const cpb::spotify::SyncPlaylistProgress& progress);
void AlbumBrowseResults(const cpb::spotify::BrowseAlbumResponse& response);
void ToplistBrowseResults(
const cpb::spotify::BrowseToplistResponse& response);
protected:
void MessageArrived(const cpb::spotify::Message& message);
private slots:
void NewConnection();
private:
void LoadPlaylist(cpb::spotify::PlaylistType type, int index = -1);
void SyncPlaylist(cpb::spotify::PlaylistType type, int index, bool offline);
void AddSongsToPlaylist(const cpb::spotify::PlaylistType playlist_type,
const QList<QUrl>& songs_urls,
// Used iff type is user_playlist
int playlist_index = -1);
void RemoveSongsFromPlaylist(const cpb::spotify::PlaylistType playlist_type,
const QList<int>& songs_indices_to_remove,
// Used iff type is user_playlist
int playlist_index = -1);
void SendOrQueueMessage(const cpb::spotify::Message& message);
QTcpServer* server_;
bool logged_in_;
QList<cpb::spotify::Message> queued_login_messages_;
QList<cpb::spotify::Message> queued_messages_;
};
#endif // INTERNET_SPOTIFY_SPOTIFYSERVER_H_

View File

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

View File

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

View File

@ -1,176 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, Andrea Decorte <adecorte@gmail.com>
Copyright 2011-2013, David Sansome <me@davidsansome.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "spotifysettingspage.h"
#include <QMessageBox>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSettings>
#include <QtDebug>
#include "config.h"
#include "core/network.h"
#include "internet/core/internetmodel.h"
#include "spotifymessages.pb.h"
#include "spotifyservice.h"
#include "ui/iconloader.h"
#include "ui_spotifysettingspage.h"
SpotifySettingsPage::SpotifySettingsPage(SettingsDialog* dialog)
: SettingsPage(dialog),
ui_(new Ui_SpotifySettingsPage),
service_(InternetModel::Service<SpotifyService>()),
validated_(false) {
ui_->setupUi(this);
setWindowIcon(IconLoader::Load("spotify", IconLoader::Provider));
QFont bold_font(font());
bold_font.setBold(true);
ui_->blob_status->setFont(bold_font);
connect(ui_->download_blob, SIGNAL(clicked()), SLOT(DownloadBlob()));
connect(ui_->login, SIGNAL(clicked()), SLOT(Login()));
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout()));
connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(Login()));
connect(service_, SIGNAL(LoginFinished(bool)), SLOT(LoginFinished(bool)));
connect(service_, SIGNAL(BlobStateChanged()), SLOT(BlobStateChanged()));
ui_->login_state->AddCredentialField(ui_->username);
ui_->login_state->AddCredentialField(ui_->password);
ui_->login_state->AddCredentialGroup(ui_->account_group);
ui_->bitrate->addItem("96 " + tr("kbps"), cpb::spotify::Bitrate96k);
ui_->bitrate->addItem("160 " + tr("kbps"), cpb::spotify::Bitrate160k);
ui_->bitrate->addItem("320 " + tr("kbps"), cpb::spotify::Bitrate320k);
BlobStateChanged();
}
SpotifySettingsPage::~SpotifySettingsPage() { delete ui_; }
void SpotifySettingsPage::BlobStateChanged() {
const bool installed = service_->IsBlobInstalled();
ui_->account_group->setEnabled(installed);
ui_->blob_status->setText(installed ? tr("Installed") : tr("Not installed"));
#ifdef HAVE_SPOTIFY_DOWNLOADER
ui_->download_blob->setEnabled(!installed);
#else
ui_->download_blob->hide();
#endif
}
void SpotifySettingsPage::DownloadBlob() { service_->InstallBlob(); }
void SpotifySettingsPage::Login() {
if (!service_->IsBlobInstalled()) {
return;
}
if (ui_->username->text() == original_username_ &&
ui_->password->text() == original_password_ &&
service_->login_state() == SpotifyService::LoginState_LoggedIn) {
return;
}
ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
service_->Login(ui_->username->text(), ui_->password->text());
}
void SpotifySettingsPage::Load() {
QSettings s;
s.beginGroup(SpotifyService::kSettingsGroup);
original_username_ = s.value("username").toString();
ui_->username->setText(original_username_);
validated_ = false;
ui_->bitrate->setCurrentIndex(ui_->bitrate->findData(
s.value("bitrate", cpb::spotify::Bitrate320k).toInt()));
ui_->volume_normalisation->setChecked(
s.value("volume_normalisation", false).toBool());
UpdateLoginState();
}
void SpotifySettingsPage::Save() {
QSettings s;
s.beginGroup(SpotifyService::kSettingsGroup);
s.setValue("username", ui_->username->text());
s.setValue("password", ui_->password->text());
s.setValue("bitrate",
ui_->bitrate->itemData(ui_->bitrate->currentIndex()).toInt());
s.setValue("volume_normalisation", ui_->volume_normalisation->isChecked());
}
void SpotifySettingsPage::LoginFinished(bool success) {
validated_ = success;
Save();
UpdateLoginState();
}
void SpotifySettingsPage::UpdateLoginState() {
const bool logged_in =
service_->login_state() == SpotifyService::LoginState_LoggedIn;
ui_->login_state->SetLoggedIn(
logged_in ? LoginStateWidget::LoggedIn : LoginStateWidget::LoggedOut,
ui_->username->text());
ui_->login_state->SetAccountTypeVisible(!logged_in);
switch (service_->login_state()) {
case SpotifyService::LoginState_NoPremium:
ui_->login_state->SetAccountTypeText(
tr("You do not have a Spotify Premium account."));
break;
case SpotifyService::LoginState_Banned:
case SpotifyService::LoginState_BadCredentials:
ui_->login_state->SetAccountTypeText(
tr("Your username or password was incorrect."));
break;
case SpotifyService::LoginState_ReloginFailed:
ui_->login_state->SetAccountTypeText(
tr("You have been logged out of Spotify, please re-enter your "
"password."));
break;
default:
ui_->login_state->SetAccountTypeText(
tr("A Spotify Premium account is required."));
break;
}
}
void SpotifySettingsPage::Logout() {
service_->Logout();
UpdateLoginState();
ui_->username->clear();
}

View File

@ -1,60 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INTERNET_SPOTIFY_SPOTIFYSETTINGSPAGE_H_
#define INTERNET_SPOTIFY_SPOTIFYSETTINGSPAGE_H_
#include "ui/settingspage.h"
class NetworkAccessManager;
class Ui_SpotifySettingsPage;
class SpotifyService;
class SpotifySettingsPage : public SettingsPage {
Q_OBJECT
public:
explicit SpotifySettingsPage(SettingsDialog* dialog);
~SpotifySettingsPage();
void Load();
void Save();
public slots:
void BlobStateChanged();
void DownloadBlob();
private slots:
void Login();
void LoginFinished(bool success);
void Logout();
private:
void UpdateLoginState();
private:
Ui_SpotifySettingsPage* ui_;
SpotifyService* service_;
bool validated_;
QString original_username_;
QString original_password_;
};
#endif // INTERNET_SPOTIFY_SPOTIFYSETTINGSPAGE_H_

View File

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

View File

@ -1,198 +0,0 @@
/* This file is part of Clementine.
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "spotifywebapiservice.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QtDebug>
#include <utility>
#include "3rdparty/qtiocompressor/qtiocompressor.h"
#include "core/application.h"
#include "core/network.h"
#include "core/timeconstants.h"
#include "globalsearch/globalsearch.h"
#include "globalsearch/spotifywebapisearchprovider.h"
#include "ui/iconloader.h"
namespace {
static constexpr const char* kServiceName = "SpotifyWebApi";
static constexpr const char* kGetAccessTokenUrl =
"https://open.spotify.com/"
"get_access_token?reason=transport&productType=web_player";
static constexpr const char* kSearchUrl =
"https://api.spotify.com/v1/search?q=%1&type=track&limit=50";
template <typename... Args>
inline QJsonValue Get(QJsonValue obj, Args&&... args) {
std::array<const char*, sizeof...(Args)> names = {
std::forward<Args>(args)...};
for (auto&& name : names) {
Q_ASSERT(obj.isObject());
obj = obj.toObject()[name];
}
return obj;
}
template <typename... Args>
inline QJsonValue Get(const QJsonDocument& obj, Args&&... args) {
return Get(obj.object(), std::forward<Args>(args)...);
}
QString concat(const QJsonArray& array, const char* name) {
QStringList ret;
for (auto&& item : array) {
ret << Get(item, name).toString();
}
return ret.join(", ");
}
} // namespace
SpotifyWebApiService::SpotifyWebApiService(Application* app,
InternetModel* parent)
: InternetService(kServiceName, app, parent, parent),
network_(new NetworkAccessManager{this}),
token_expiration_ms_{0} {
app_->global_search()->AddProvider(
new SpotifyWebApiSearchProvider(app_, this));
}
SpotifyWebApiService::~SpotifyWebApiService() {}
QStandardItem* SpotifyWebApiService::CreateRootItem() {
root_ = new QStandardItem(IconLoader::Load("spotify", IconLoader::Provider),
kServiceName);
return root_;
}
void SpotifyWebApiService::LazyPopulate(QStandardItem* item) {}
void SpotifyWebApiService::Search(int searchId, QString queryStr) {
if (QDateTime::currentDateTime().toMSecsSinceEpoch() >=
token_expiration_ms_) {
QNetworkRequest request{QUrl{kGetAccessTokenUrl}};
request.setRawHeader("Accept-Encoding", "gzip");
QNetworkReply* reply = network_->get(request);
connect(reply, &QNetworkReply::finished, [=]() {
reply->deleteLater();
OnTokenReady(ParseJsonReplyWithGzip(reply), searchId, queryStr);
});
} else {
OnReadyToSearch(searchId, queryStr);
}
}
void SpotifyWebApiService::OnTokenReady(const QJsonDocument& json, int searchId,
QString queryStr) {
if (!json.isEmpty()) {
token_ = Get(json, "accessToken").toString();
token_expiration_ms_ = static_cast<qint64>(
Get(json, "accessTokenExpirationTimestampMs").toDouble());
qLog(Debug) << "Spotify API Token:" << token_;
OnReadyToSearch(searchId, queryStr);
}
}
void SpotifyWebApiService::OnReadyToSearch(int searchId, QString queryStr) {
qLog(Debug) << "Spotify API Searching: " << queryStr;
QNetworkRequest request{
QUrl{QString(kSearchUrl).arg(queryStr.toHtmlEscaped())}};
request.setRawHeader("Accept-Encoding", "gzip");
request.setRawHeader("Authorization", ("Bearer " + token_).toUtf8());
QNetworkReply* reply = network_->get(request);
connect(reply, &QNetworkReply::finished, [=] {
reply->deleteLater();
BuildResultList(ParseJsonReplyWithGzip(reply), searchId);
});
}
void SpotifyWebApiService::BuildResultList(const QJsonDocument& json,
int searchId) {
QList<Song> result;
for (auto&& item : Get(json, "tracks", "items").toArray()) {
Song song;
song.set_albumartist(
concat(Get(item, "album", "artists").toArray(), "name"));
song.set_album(Get(item, "album", "name").toString());
song.set_artist(concat(Get(item, "artists").toArray(), "name"));
song.set_disc(Get(item, "disc_number").toInt());
song.set_length_nanosec(Get(item, "duration_ms").toInt() * kNsecPerMsec);
song.set_title(Get(item, "name").toString());
song.set_track(Get(item, "track_number").toInt());
song.set_url(QUrl{Get(item, "uri").toString()});
song.set_filetype(Song::Type_Stream);
song.set_valid(true);
song.set_directory_id(0);
song.set_mtime(0);
song.set_ctime(0);
song.set_filesize(0);
result += song;
}
emit SearchFinished(searchId, result);
}
QJsonDocument SpotifyWebApiService::ParseJsonReplyWithGzip(
QNetworkReply* reply) {
if (reply->error() != QNetworkReply::NoError) {
app_->AddError(tr("%1 request failed:\n%2")
.arg(kServiceName)
.arg(reply->errorString()));
return QJsonDocument();
}
QByteArray output;
if (reply->hasRawHeader("content-encoding") &&
reply->rawHeader("content-encoding") == "gzip") {
QtIOCompressor gzip(reply);
gzip.setStreamFormat(QtIOCompressor::GzipFormat);
if (!gzip.open(QIODevice::ReadOnly)) {
app_->AddError(tr("%1 failed to decode as gzip stream:\n%2")
.arg(kServiceName)
.arg(gzip.errorString()));
return QJsonDocument();
}
output = gzip.readAll();
} else {
output = reply->readAll();
}
QJsonParseError error;
QJsonDocument document = QJsonDocument::fromJson(output, &error);
if (error.error != QJsonParseError::NoError) {
app_->AddError(tr("Failed to parse %1 response:\n%2")
.arg(kServiceName)
.arg(error.errorString()));
return QJsonDocument();
}
return document;
}

View File

@ -1,60 +0,0 @@
/* This file is part of Clementine.
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SPOTIFYWEBAPISERVICE_H
#define SPOTIFYWEBAPISERVICE_H
#include <chrono>
#include "internet/core/internetmodel.h"
#include "internet/core/internetservice.h"
class NetworkAccessManager;
class SpotifyWebApiService : public InternetService {
Q_OBJECT
public:
SpotifyWebApiService(Application* app, InternetModel* parent);
~SpotifyWebApiService();
QStandardItem* CreateRootItem() override;
void LazyPopulate(QStandardItem* parent) override;
public:
void Search(int searchId, QString queryStr);
private:
void OnTokenReady(const QJsonDocument&, int searchId, QString queryStr);
void OnReadyToSearch(int searchId, QString queryStr);
void BuildResultList(const QJsonDocument&, int searchId);
signals:
void SearchFinished(int searchId, const QList<Song>&);
private:
QJsonDocument ParseJsonReplyWithGzip(QNetworkReply* reply);
private:
QStandardItem* root_;
NetworkAccessManager* network_;
QString token_;
qint64 token_expiration_ms_;
};
#endif // SPOTIFYWEBAPISERVICE_H

View File

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

View File

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

View File

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

View File

@ -396,7 +396,7 @@ int main(int argc, char* argv[]) {
// Set the name of the app desktop file as per the freedesktop specifications
// This is needed on Wayland for the main window to show the correct icon
#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
QGuiApplication::setDesktopFileName("clementine");
QGuiApplication::setDesktopFileName("org.clementine_player.Clementine");
#endif
// Resources

View File

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

View File

@ -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);

View File

@ -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;
}

View File

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

View File

@ -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();

View File

@ -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();

View File

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

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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_

View File

@ -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>

View File

@ -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() {

View File

@ -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_;

View File

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

View File

@ -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);
}

View File

@ -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();

View File

@ -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>

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