diff --git a/CMakeLists.txt b/CMakeLists.txt index 57466aaad..118f2f22b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,5 @@ # Strawberry Music Player # Copyright 2013, Jonas Kvinge -# This file was part of Clementine. -# Copyright 2010, David Sansome # # Strawberry is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -111,14 +109,13 @@ pkg_check_modules(PHONON phonon4qt5) pkg_check_modules(SQLITE REQUIRED sqlite3>=3.7) pkg_check_modules(LIBPULSE libpulse) pkg_check_modules(CHROMAPRINT libchromaprint) -#if(CHROMAPRINT_FOUND) -# set(HAVE_CHROMAPRINT ON) -#endif() pkg_check_modules(LIBGPOD libgpod-1.0>=0.7.92) pkg_check_modules(LIBMTP libmtp>=1.0) pkg_check_modules(IMOBILEDEVICE libimobiledevice-1.0) pkg_check_modules(USBMUXD libusbmuxd) pkg_check_modules(PLIST libplist) +pkg_check_modules(LIBDEEZER libdeezer) +pkg_check_modules(LIBDZMEDIA libdzmedia) if(WIN32) find_package(ZLIB REQUIRED) @@ -287,6 +284,18 @@ optional_component(PHONON OFF "Engine: Phonon backend" DEPENDS "phonon4qt5" PHONON_FOUND ) +if (WIN32) + optional_component(DEEZER ON "Engine: Deezer backend" + DEPENDS "libdeezer" LIBDEEZER_FOUND + ) +else () + optional_component(DEEZER ON "Engine: Deezer backend" + DEPENDS "Linux" LINUX + DEPENDS "libdeezer" LIBDEEZER_FOUND + DEPENDS "libpulse" LIBPULSE_FOUND + ) +endif() + optional_component(LIBPULSE ON "Pulse audio integration" DEPENDS "libpulse" LIBPULSE_FOUND ) @@ -336,6 +345,10 @@ optional_component(SPARKLE ON "Sparkle integration" DEPENDS "Sparkle" SPARKLE ) +optional_component(DZMEDIA ON "DZMedia" + DEPENDS "libdzmedia" LIBDZMEDIA_FOUND +) + #if(IMOBILEDEVICE_FOUND AND PLIST_FOUND) #add_subdirectory(ext/gstafc) #endif(IMOBILEDEVICE_FOUND AND PLIST_FOUND) @@ -374,6 +387,6 @@ add_custom_target(uninstall # Show a summary of what we have enabled summary_show() -if(NOT HAVE_GSTREAMER AND NOT HAVE_XINE AND NOT HAVE_VLC AND NOT HAVE_PHONON) - message(FATAL_ERROR "You need to enable either GStreamer, Xine, VLC or Phonon to compile!") +if(NOT HAVE_GSTREAMER AND NOT HAVE_XINE AND NOT HAVE_VLC AND NOT HAVE_PHONON AND NOT HAVE_DEEZER) + message(FATAL_ERROR "You need to enable either GStreamer, Xine, VLC, Phonon or Deezer to compile!") endif() diff --git a/data/data.qrc b/data/data.qrc index d0a30ed07..f614b0a33 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -7,6 +7,7 @@ schema/device-schema.sql style/strawberry.css misc/playing_tooltip.txt + misc/oauthsuccess.html pictures/strawberry.png pictures/strawbs-transparent.png pictures/noalbumart.png @@ -27,442 +28,7 @@ pictures/osd_background.png pictures/osd_shadow_corner.png pictures/osd_shadow_edge.png - icons/128x128/albums.png - icons/128x128/alsa.png - icons/128x128/application-exit.png - icons/128x128/applications-internet.png - icons/128x128/bluetooth.png - icons/128x128/cdcase.png - icons/128x128/cd.png - icons/128x128/configure.png - icons/128x128/device-ipod-nano.png - icons/128x128/device-ipod.png - icons/128x128/device-phone.png - icons/128x128/device.png - icons/128x128/device-usb-drive.png - icons/128x128/device-usb-flash.png - icons/128x128/dialog-error.png - icons/128x128/dialog-information.png - icons/128x128/dialog-ok-apply.png - icons/128x128/dialog-password.png - icons/128x128/dialog-warning.png - icons/128x128/document-download.png - icons/128x128/document-new.png - icons/128x128/document-open-folder.png - icons/128x128/document-open.png - icons/128x128/document-save.png - icons/128x128/document-search.png - icons/128x128/download.png - icons/128x128/edit-clear-list.png - icons/128x128/edit-clear-locationbar-ltr.png - icons/128x128/edit-copy.png - icons/128x128/edit-delete.png - icons/128x128/edit-find.png - icons/128x128/edit-redo.png - icons/128x128/edit-rename.png - icons/128x128/edit-undo.png - icons/128x128/electrocompaniet.png - icons/128x128/equalizer.png - icons/128x128/folder-new.png - icons/128x128/folder.png - icons/128x128/folder-sound.png - icons/128x128/footsteps.png - icons/128x128/go-down.png - icons/128x128/go-home.png - icons/128x128/go-jump.png - icons/128x128/go-next.png - icons/128x128/go-previous.png - icons/128x128/go-up.png - icons/128x128/gstreamer.png - icons/128x128/headset.png - icons/128x128/help-hint.png - icons/128x128/intel.png - icons/128x128/jack.png - icons/128x128/keyboard.png - icons/128x128/list-add.png - icons/128x128/list-remove.png - icons/128x128/mcintosh-player.png - icons/128x128/mcintosh-text.png - icons/128x128/media-eject.png - icons/128x128/media-forward.png - icons/128x128/media-pause.png - icons/128x128/media-play.png - icons/128x128/media-rewind.png - icons/128x128/media-stop.png - icons/128x128/nvidia.png - icons/128x128/play2.png - icons/128x128/realtek.png - icons/128x128/search.png - icons/128x128/soundcard.png - icons/128x128/speaker.png - icons/128x128/star-grey.png - icons/128x128/star.png - icons/128x128/strawberry.png - icons/128x128/strawberry.svg - icons/128x128/tools-wizard.png - icons/128x128/view-choose.png - icons/128x128/view-fullscreen.png - icons/128x128/view-media-lyrics.png - icons/128x128/view-media-playlist.png - icons/128x128/view-media-visualization.png - icons/128x128/view-refresh.png - icons/128x128/vinyl.png - icons/128x128/vlc.png - icons/128x128/xine.png - icons/128x128/zoom-in.png - icons/128x128/zoom-out.png - icons/128x128/tidal.png - icons/64x64/albums.png - icons/64x64/alsa.png - icons/64x64/application-exit.png - icons/64x64/applications-internet.png - icons/64x64/bluetooth.png - icons/64x64/cdcase.png - icons/64x64/cd.png - icons/64x64/configure.png - icons/64x64/device-ipod-nano.png - icons/64x64/device-ipod.png - icons/64x64/device-phone.png - icons/64x64/device.png - icons/64x64/device-usb-drive.png - icons/64x64/device-usb-flash.png - icons/64x64/dialog-error.png - icons/64x64/dialog-information.png - icons/64x64/dialog-ok-apply.png - icons/64x64/dialog-password.png - icons/64x64/dialog-warning.png - icons/64x64/document-download.png - icons/64x64/document-new.png - icons/64x64/document-open-folder.png - icons/64x64/document-open.png - icons/64x64/document-save.png - icons/64x64/document-search.png - icons/64x64/download.png - icons/64x64/edit-clear-list.png - icons/64x64/edit-clear-locationbar-ltr.png - icons/64x64/edit-copy.png - icons/64x64/edit-delete.png - icons/64x64/edit-find.png - icons/64x64/edit-redo.png - icons/64x64/edit-rename.png - icons/64x64/edit-undo.png - icons/64x64/electrocompaniet.png - icons/64x64/equalizer.png - icons/64x64/folder-new.png - icons/64x64/folder.png - icons/64x64/folder-sound.png - icons/64x64/footsteps.png - icons/64x64/go-down.png - icons/64x64/go-home.png - icons/64x64/go-jump.png - icons/64x64/go-next.png - icons/64x64/go-previous.png - icons/64x64/go-up.png - icons/64x64/gstreamer.png - icons/64x64/headset.png - icons/64x64/help-hint.png - icons/64x64/intel.png - icons/64x64/jack.png - icons/64x64/keyboard.png - icons/64x64/list-add.png - icons/64x64/list-remove.png - icons/64x64/mcintosh-player.png - icons/64x64/mcintosh-text.png - icons/64x64/media-eject.png - icons/64x64/media-forward.png - icons/64x64/media-pause.png - icons/64x64/media-play.png - icons/64x64/media-rewind.png - icons/64x64/media-stop.png - icons/64x64/nvidia.png - icons/64x64/play2.png - icons/64x64/pulseaudio.png - icons/64x64/realtek.png - icons/64x64/search.png - icons/64x64/soundcard.png - icons/64x64/speaker.png - icons/64x64/star-grey.png - icons/64x64/star.png - icons/64x64/strawberry.png - icons/64x64/tools-wizard.png - icons/64x64/view-choose.png - icons/64x64/view-fullscreen.png - icons/64x64/view-media-lyrics.png - icons/64x64/view-media-playlist.png - icons/64x64/view-media-visualization.png - icons/64x64/view-refresh.png - icons/64x64/vinyl.png - icons/64x64/vlc.png - icons/64x64/xine.png - icons/64x64/zoom-in.png - icons/64x64/zoom-out.png - icons/64x64/tidal.png - icons/48x48/albums.png - icons/48x48/alsa.png - icons/48x48/application-exit.png - icons/48x48/applications-internet.png - icons/48x48/bluetooth.png - icons/48x48/cdcase.png - icons/48x48/cd.png - icons/48x48/configure.png - icons/48x48/device-ipod-nano.png - icons/48x48/device-ipod.png - icons/48x48/device-phone.png - icons/48x48/device.png - icons/48x48/device-usb-drive.png - icons/48x48/device-usb-flash.png - icons/48x48/dialog-error.png - icons/48x48/dialog-information.png - icons/48x48/dialog-ok-apply.png - icons/48x48/dialog-password.png - icons/48x48/dialog-warning.png - icons/48x48/document-download.png - icons/48x48/document-new.png - icons/48x48/document-open-folder.png - icons/48x48/document-open.png - icons/48x48/document-save.png - icons/48x48/document-search.png - icons/48x48/download.png - icons/48x48/edit-clear-list.png - icons/48x48/edit-clear-locationbar-ltr.png - icons/48x48/edit-copy.png - icons/48x48/edit-delete.png - icons/48x48/edit-find.png - icons/48x48/edit-redo.png - icons/48x48/edit-rename.png - icons/48x48/edit-undo.png - icons/48x48/electrocompaniet.png - icons/48x48/equalizer.png - icons/48x48/folder-new.png - icons/48x48/folder.png - icons/48x48/folder-sound.png - icons/48x48/footsteps.png - icons/48x48/go-down.png - icons/48x48/go-home.png - icons/48x48/go-jump.png - icons/48x48/go-next.png - icons/48x48/go-previous.png - icons/48x48/go-up.png - icons/48x48/gstreamer.png - icons/48x48/headset.png - icons/48x48/help-hint.png - icons/48x48/intel.png - icons/48x48/jack.png - icons/48x48/keyboard.png - icons/48x48/list-add.png - icons/48x48/list-remove.png - icons/48x48/mcintosh-player.png - icons/48x48/mcintosh.png - icons/48x48/mcintosh-text.png - icons/48x48/media-eject.png - icons/48x48/media-forward.png - icons/48x48/media-pause.png - icons/48x48/media-playlist-repeat.png - icons/48x48/media-playlist-shuffle.png - icons/48x48/media-play.png - icons/48x48/media-rewind.png - icons/48x48/media-stop.png - icons/48x48/nvidia.png - icons/48x48/play2.png - icons/48x48/pulseaudio.png - icons/48x48/realtek.png - icons/48x48/search.png - icons/48x48/soundcard.png - icons/48x48/speaker.png - icons/48x48/star-grey.png - icons/48x48/star.png - icons/48x48/strawberry.png - icons/48x48/tools-wizard.png - icons/48x48/view-choose.png - icons/48x48/view-fullscreen.png - icons/48x48/view-media-lyrics.png - icons/48x48/view-media-playlist.png - icons/48x48/view-media-visualization.png - icons/48x48/view-refresh.png - icons/48x48/vinyl.png - icons/48x48/vlc.png - icons/48x48/xine.png - icons/48x48/zoom-in.png - icons/48x48/zoom-out.png - icons/48x48/tidal.png - icons/32x32/albums.png - icons/32x32/alsa.png - icons/32x32/application-exit.png - icons/32x32/applications-internet.png - icons/32x32/bluetooth.png - icons/32x32/cdcase.png - icons/32x32/cd.png - icons/32x32/configure.png - icons/32x32/device-ipod-nano.png - icons/32x32/device-ipod.png - icons/32x32/device-phone.png - icons/32x32/device.png - icons/32x32/device-usb-drive.png - icons/32x32/device-usb-flash.png - icons/32x32/dialog-error.png - icons/32x32/dialog-information.png - icons/32x32/dialog-ok-apply.png - icons/32x32/dialog-password.png - icons/32x32/dialog-warning.png - icons/32x32/document-download.png - icons/32x32/document-new.png - icons/32x32/document-open-folder.png - icons/32x32/document-open.png - icons/32x32/document-save.png - icons/32x32/document-search.png - icons/32x32/download.png - icons/32x32/edit-clear-list.png - icons/32x32/edit-clear-locationbar-ltr.png - icons/32x32/edit-copy.png - icons/32x32/edit-delete.png - icons/32x32/edit-find.png - icons/32x32/edit-redo.png - icons/32x32/edit-rename.png - icons/32x32/edit-undo.png - icons/32x32/electrocompaniet.png - icons/32x32/equalizer.png - icons/32x32/folder-new.png - icons/32x32/folder.png - icons/32x32/folder-sound.png - icons/32x32/footsteps.png - icons/32x32/go-down.png - icons/32x32/go-home.png - icons/32x32/go-jump.png - icons/32x32/go-next.png - icons/32x32/go-previous.png - icons/32x32/go-up.png - icons/32x32/gstreamer.png - icons/32x32/headset.png - icons/32x32/help-hint.png - icons/32x32/intel.png - icons/32x32/jack.png - icons/32x32/keyboard.png - icons/32x32/list-add.png - icons/32x32/list-remove.png - icons/32x32/mcintosh-player.png - icons/32x32/mcintosh.png - icons/32x32/mcintosh-text.png - icons/32x32/media-eject.png - icons/32x32/media-forward.png - icons/32x32/media-pause.png - icons/32x32/media-playlist-repeat.png - icons/32x32/media-playlist-shuffle.png - icons/32x32/media-play.png - icons/32x32/media-rewind.png - icons/32x32/media-stop.png - icons/32x32/nvidia.png - icons/32x32/play2.png - icons/32x32/pulseaudio.png - icons/32x32/realtek.png - icons/32x32/search.png - icons/32x32/soundcard.png - icons/32x32/speaker.png - icons/32x32/star-grey.png - icons/32x32/star.png - icons/32x32/strawberry.png - icons/32x32/strawberry.svg - icons/32x32/tools-wizard.png - icons/32x32/view-choose.png - icons/32x32/view-fullscreen.png - icons/32x32/view-media-lyrics.png - icons/32x32/view-media-playlist.png - icons/32x32/view-media-visualization.png - icons/32x32/view-refresh.png - icons/32x32/vinyl.png - icons/32x32/vlc.png - icons/32x32/xine.png - icons/32x32/zoom-in.png - icons/32x32/zoom-out.png - icons/32x32/tidal.png - icons/22x22/albums.png - icons/22x22/alsa.png - icons/22x22/application-exit.png - icons/22x22/applications-internet.png - icons/22x22/bluetooth.png - icons/22x22/cdcase.png - icons/22x22/cd.png - icons/22x22/configure.png - icons/22x22/device-ipod-nano.png - icons/22x22/device-ipod.png - icons/22x22/device-phone.png - icons/22x22/device.png - icons/22x22/device-usb-drive.png - icons/22x22/device-usb-flash.png - icons/22x22/dialog-error.png - icons/22x22/dialog-information.png - icons/22x22/dialog-ok-apply.png - icons/22x22/dialog-password.png - icons/22x22/dialog-warning.png - icons/22x22/document-download.png - icons/22x22/document-new.png - icons/22x22/document-open-folder.png - icons/22x22/document-open.png - icons/22x22/document-save.png - icons/22x22/document-search.png - icons/22x22/download.png - icons/22x22/edit-clear-list.png - icons/22x22/edit-clear-locationbar-ltr.png - icons/22x22/edit-copy.png - icons/22x22/edit-delete.png - icons/22x22/edit-find.png - icons/22x22/edit-redo.png - icons/22x22/edit-rename.png - icons/22x22/edit-undo.png - icons/22x22/electrocompaniet.png - icons/22x22/equalizer.png - icons/22x22/folder-new.png - icons/22x22/folder.png - icons/22x22/folder-sound.png - icons/22x22/footsteps.png - icons/22x22/go-down.png - icons/22x22/go-home.png - icons/22x22/go-jump.png - icons/22x22/go-next.png - icons/22x22/go-previous.png - icons/22x22/go-up.png - icons/22x22/gstreamer.png - icons/22x22/headset.png - icons/22x22/help-hint.png - icons/22x22/intel.png - icons/22x22/jack.png - icons/22x22/keyboard.png - icons/22x22/list-add.png - icons/22x22/list-remove.png - icons/22x22/mcintosh-player.png - icons/22x22/mcintosh.png - icons/22x22/mcintosh-text.png - icons/22x22/media-eject.png - icons/22x22/media-forward.png - icons/22x22/media-pause.png - icons/22x22/media-playlist-repeat.png - icons/22x22/media-playlist-shuffle.png - icons/22x22/media-play.png - icons/22x22/media-rewind.png - icons/22x22/media-stop.png - icons/22x22/nvidia.png - icons/22x22/play2.png - icons/22x22/pulseaudio.png - icons/22x22/realtek.png - icons/22x22/search.png - icons/22x22/soundcard.png - icons/22x22/speaker.png - icons/22x22/star-grey.png - icons/22x22/star.png - icons/22x22/strawberry.png - icons/22x22/strawberry.svg - icons/22x22/tools-wizard.png - icons/22x22/view-choose.png - icons/22x22/view-fullscreen.png - icons/22x22/view-media-lyrics.png - icons/22x22/view-media-playlist.png - icons/22x22/view-media-visualization.png - icons/22x22/view-refresh.png - icons/22x22/vinyl.png - icons/22x22/vlc.png - icons/22x22/xine.png - icons/22x22/zoom-in.png - icons/22x22/zoom-out.png - icons/22x22/tidal.png + pictures/deezer.png fonts/HumongousofEternitySt.ttf diff --git a/data/icons.qrc b/data/icons.qrc index 63ea26386..cc9f52f85 100644 --- a/data/icons.qrc +++ b/data/icons.qrc @@ -1,4 +1,444 @@ - - + + icons/128x128/albums.png + icons/128x128/alsa.png + icons/128x128/application-exit.png + icons/128x128/applications-internet.png + icons/128x128/bluetooth.png + icons/128x128/cdcase.png + icons/128x128/cd.png + icons/128x128/configure.png + icons/128x128/device-ipod-nano.png + icons/128x128/device-ipod.png + icons/128x128/device-phone.png + icons/128x128/device.png + icons/128x128/device-usb-drive.png + icons/128x128/device-usb-flash.png + icons/128x128/dialog-error.png + icons/128x128/dialog-information.png + icons/128x128/dialog-ok-apply.png + icons/128x128/dialog-password.png + icons/128x128/dialog-warning.png + icons/128x128/document-download.png + icons/128x128/document-new.png + icons/128x128/document-open-folder.png + icons/128x128/document-open.png + icons/128x128/document-save.png + icons/128x128/document-search.png + icons/128x128/download.png + icons/128x128/edit-clear-list.png + icons/128x128/edit-clear-locationbar-ltr.png + icons/128x128/edit-copy.png + icons/128x128/edit-delete.png + icons/128x128/edit-find.png + icons/128x128/edit-redo.png + icons/128x128/edit-rename.png + icons/128x128/edit-undo.png + icons/128x128/electrocompaniet.png + icons/128x128/equalizer.png + icons/128x128/folder-new.png + icons/128x128/folder.png + icons/128x128/folder-sound.png + icons/128x128/footsteps.png + icons/128x128/go-down.png + icons/128x128/go-home.png + icons/128x128/go-jump.png + icons/128x128/go-next.png + icons/128x128/go-previous.png + icons/128x128/go-up.png + icons/128x128/gstreamer.png + icons/128x128/headset.png + icons/128x128/help-hint.png + icons/128x128/intel.png + icons/128x128/jack.png + icons/128x128/keyboard.png + icons/128x128/list-add.png + icons/128x128/list-remove.png + icons/128x128/mcintosh-player.png + icons/128x128/mcintosh-text.png + icons/128x128/media-eject.png + icons/128x128/media-forward.png + icons/128x128/media-pause.png + icons/128x128/media-play.png + icons/128x128/media-rewind.png + icons/128x128/media-stop.png + icons/128x128/nvidia.png + icons/128x128/play2.png + icons/128x128/realtek.png + icons/128x128/search.png + icons/128x128/soundcard.png + icons/128x128/speaker.png + icons/128x128/star-grey.png + icons/128x128/star.png + icons/128x128/strawberry.png + icons/128x128/strawberry.svg + icons/128x128/tools-wizard.png + icons/128x128/view-choose.png + icons/128x128/view-fullscreen.png + icons/128x128/view-media-lyrics.png + icons/128x128/view-media-playlist.png + icons/128x128/view-media-visualization.png + icons/128x128/view-refresh.png + icons/128x128/vinyl.png + icons/128x128/vlc.png + icons/128x128/xine.png + icons/128x128/zoom-in.png + icons/128x128/zoom-out.png + icons/128x128/tidal.png + icons/128x128/deezer.png + icons/64x64/albums.png + icons/64x64/alsa.png + icons/64x64/application-exit.png + icons/64x64/applications-internet.png + icons/64x64/bluetooth.png + icons/64x64/cdcase.png + icons/64x64/cd.png + icons/64x64/configure.png + icons/64x64/device-ipod-nano.png + icons/64x64/device-ipod.png + icons/64x64/device-phone.png + icons/64x64/device.png + icons/64x64/device-usb-drive.png + icons/64x64/device-usb-flash.png + icons/64x64/dialog-error.png + icons/64x64/dialog-information.png + icons/64x64/dialog-ok-apply.png + icons/64x64/dialog-password.png + icons/64x64/dialog-warning.png + icons/64x64/document-download.png + icons/64x64/document-new.png + icons/64x64/document-open-folder.png + icons/64x64/document-open.png + icons/64x64/document-save.png + icons/64x64/document-search.png + icons/64x64/download.png + icons/64x64/edit-clear-list.png + icons/64x64/edit-clear-locationbar-ltr.png + icons/64x64/edit-copy.png + icons/64x64/edit-delete.png + icons/64x64/edit-find.png + icons/64x64/edit-redo.png + icons/64x64/edit-rename.png + icons/64x64/edit-undo.png + icons/64x64/electrocompaniet.png + icons/64x64/equalizer.png + icons/64x64/folder-new.png + icons/64x64/folder.png + icons/64x64/folder-sound.png + icons/64x64/footsteps.png + icons/64x64/go-down.png + icons/64x64/go-home.png + icons/64x64/go-jump.png + icons/64x64/go-next.png + icons/64x64/go-previous.png + icons/64x64/go-up.png + icons/64x64/gstreamer.png + icons/64x64/headset.png + icons/64x64/help-hint.png + icons/64x64/intel.png + icons/64x64/jack.png + icons/64x64/keyboard.png + icons/64x64/list-add.png + icons/64x64/list-remove.png + icons/64x64/mcintosh-player.png + icons/64x64/mcintosh-text.png + icons/64x64/media-eject.png + icons/64x64/media-forward.png + icons/64x64/media-pause.png + icons/64x64/media-play.png + icons/64x64/media-rewind.png + icons/64x64/media-stop.png + icons/64x64/nvidia.png + icons/64x64/play2.png + icons/64x64/pulseaudio.png + icons/64x64/realtek.png + icons/64x64/search.png + icons/64x64/soundcard.png + icons/64x64/speaker.png + icons/64x64/star-grey.png + icons/64x64/star.png + icons/64x64/strawberry.png + icons/64x64/tools-wizard.png + icons/64x64/view-choose.png + icons/64x64/view-fullscreen.png + icons/64x64/view-media-lyrics.png + icons/64x64/view-media-playlist.png + icons/64x64/view-media-visualization.png + icons/64x64/view-refresh.png + icons/64x64/vinyl.png + icons/64x64/vlc.png + icons/64x64/xine.png + icons/64x64/zoom-in.png + icons/64x64/zoom-out.png + icons/64x64/tidal.png + icons/64x64/deezer.png + icons/48x48/albums.png + icons/48x48/alsa.png + icons/48x48/application-exit.png + icons/48x48/applications-internet.png + icons/48x48/bluetooth.png + icons/48x48/cdcase.png + icons/48x48/cd.png + icons/48x48/configure.png + icons/48x48/device-ipod-nano.png + icons/48x48/device-ipod.png + icons/48x48/device-phone.png + icons/48x48/device.png + icons/48x48/device-usb-drive.png + icons/48x48/device-usb-flash.png + icons/48x48/dialog-error.png + icons/48x48/dialog-information.png + icons/48x48/dialog-ok-apply.png + icons/48x48/dialog-password.png + icons/48x48/dialog-warning.png + icons/48x48/document-download.png + icons/48x48/document-new.png + icons/48x48/document-open-folder.png + icons/48x48/document-open.png + icons/48x48/document-save.png + icons/48x48/document-search.png + icons/48x48/download.png + icons/48x48/edit-clear-list.png + icons/48x48/edit-clear-locationbar-ltr.png + icons/48x48/edit-copy.png + icons/48x48/edit-delete.png + icons/48x48/edit-find.png + icons/48x48/edit-redo.png + icons/48x48/edit-rename.png + icons/48x48/edit-undo.png + icons/48x48/electrocompaniet.png + icons/48x48/equalizer.png + icons/48x48/folder-new.png + icons/48x48/folder.png + icons/48x48/folder-sound.png + icons/48x48/footsteps.png + icons/48x48/go-down.png + icons/48x48/go-home.png + icons/48x48/go-jump.png + icons/48x48/go-next.png + icons/48x48/go-previous.png + icons/48x48/go-up.png + icons/48x48/gstreamer.png + icons/48x48/headset.png + icons/48x48/help-hint.png + icons/48x48/intel.png + icons/48x48/jack.png + icons/48x48/keyboard.png + icons/48x48/list-add.png + icons/48x48/list-remove.png + icons/48x48/mcintosh-player.png + icons/48x48/mcintosh.png + icons/48x48/mcintosh-text.png + icons/48x48/media-eject.png + icons/48x48/media-forward.png + icons/48x48/media-pause.png + icons/48x48/media-playlist-repeat.png + icons/48x48/media-playlist-shuffle.png + icons/48x48/media-play.png + icons/48x48/media-rewind.png + icons/48x48/media-stop.png + icons/48x48/nvidia.png + icons/48x48/play2.png + icons/48x48/pulseaudio.png + icons/48x48/realtek.png + icons/48x48/search.png + icons/48x48/soundcard.png + icons/48x48/speaker.png + icons/48x48/star-grey.png + icons/48x48/star.png + icons/48x48/strawberry.png + icons/48x48/tools-wizard.png + icons/48x48/view-choose.png + icons/48x48/view-fullscreen.png + icons/48x48/view-media-lyrics.png + icons/48x48/view-media-playlist.png + icons/48x48/view-media-visualization.png + icons/48x48/view-refresh.png + icons/48x48/vinyl.png + icons/48x48/vlc.png + icons/48x48/xine.png + icons/48x48/zoom-in.png + icons/48x48/zoom-out.png + icons/48x48/tidal.png + icons/32x32/albums.png + icons/32x32/alsa.png + icons/32x32/application-exit.png + icons/32x32/applications-internet.png + icons/32x32/bluetooth.png + icons/32x32/cdcase.png + icons/32x32/cd.png + icons/32x32/configure.png + icons/32x32/device-ipod-nano.png + icons/32x32/device-ipod.png + icons/32x32/device-phone.png + icons/32x32/device.png + icons/32x32/device-usb-drive.png + icons/32x32/device-usb-flash.png + icons/32x32/dialog-error.png + icons/32x32/dialog-information.png + icons/32x32/dialog-ok-apply.png + icons/32x32/dialog-password.png + icons/32x32/dialog-warning.png + icons/32x32/document-download.png + icons/32x32/document-new.png + icons/32x32/document-open-folder.png + icons/32x32/document-open.png + icons/32x32/document-save.png + icons/32x32/document-search.png + icons/32x32/download.png + icons/32x32/edit-clear-list.png + icons/32x32/edit-clear-locationbar-ltr.png + icons/32x32/edit-copy.png + icons/32x32/edit-delete.png + icons/32x32/edit-find.png + icons/32x32/edit-redo.png + icons/32x32/edit-rename.png + icons/32x32/edit-undo.png + icons/32x32/electrocompaniet.png + icons/32x32/equalizer.png + icons/32x32/folder-new.png + icons/32x32/folder.png + icons/32x32/folder-sound.png + icons/32x32/footsteps.png + icons/32x32/go-down.png + icons/32x32/go-home.png + icons/32x32/go-jump.png + icons/32x32/go-next.png + icons/32x32/go-previous.png + icons/32x32/go-up.png + icons/32x32/gstreamer.png + icons/32x32/headset.png + icons/32x32/help-hint.png + icons/32x32/intel.png + icons/32x32/jack.png + icons/32x32/keyboard.png + icons/32x32/list-add.png + icons/32x32/list-remove.png + icons/32x32/mcintosh-player.png + icons/32x32/mcintosh.png + icons/32x32/mcintosh-text.png + icons/32x32/media-eject.png + icons/32x32/media-forward.png + icons/32x32/media-pause.png + icons/32x32/media-playlist-repeat.png + icons/32x32/media-playlist-shuffle.png + icons/32x32/media-play.png + icons/32x32/media-rewind.png + icons/32x32/media-stop.png + icons/32x32/nvidia.png + icons/32x32/play2.png + icons/32x32/pulseaudio.png + icons/32x32/realtek.png + icons/32x32/search.png + icons/32x32/soundcard.png + icons/32x32/speaker.png + icons/32x32/star-grey.png + icons/32x32/star.png + icons/32x32/strawberry.png + icons/32x32/strawberry.svg + icons/32x32/tools-wizard.png + icons/32x32/view-choose.png + icons/32x32/view-fullscreen.png + icons/32x32/view-media-lyrics.png + icons/32x32/view-media-playlist.png + icons/32x32/view-media-visualization.png + icons/32x32/view-refresh.png + icons/32x32/vinyl.png + icons/32x32/vlc.png + icons/32x32/xine.png + icons/32x32/zoom-in.png + icons/32x32/zoom-out.png + icons/32x32/tidal.png + icons/32x32/deezer.png + icons/22x22/albums.png + icons/22x22/alsa.png + icons/22x22/application-exit.png + icons/22x22/applications-internet.png + icons/22x22/bluetooth.png + icons/22x22/cdcase.png + icons/22x22/cd.png + icons/22x22/configure.png + icons/22x22/device-ipod-nano.png + icons/22x22/device-ipod.png + icons/22x22/device-phone.png + icons/22x22/device.png + icons/22x22/device-usb-drive.png + icons/22x22/device-usb-flash.png + icons/22x22/dialog-error.png + icons/22x22/dialog-information.png + icons/22x22/dialog-ok-apply.png + icons/22x22/dialog-password.png + icons/22x22/dialog-warning.png + icons/22x22/document-download.png + icons/22x22/document-new.png + icons/22x22/document-open-folder.png + icons/22x22/document-open.png + icons/22x22/document-save.png + icons/22x22/document-search.png + icons/22x22/download.png + icons/22x22/edit-clear-list.png + icons/22x22/edit-clear-locationbar-ltr.png + icons/22x22/edit-copy.png + icons/22x22/edit-delete.png + icons/22x22/edit-find.png + icons/22x22/edit-redo.png + icons/22x22/edit-rename.png + icons/22x22/edit-undo.png + icons/22x22/electrocompaniet.png + icons/22x22/equalizer.png + icons/22x22/folder-new.png + icons/22x22/folder.png + icons/22x22/folder-sound.png + icons/22x22/footsteps.png + icons/22x22/go-down.png + icons/22x22/go-home.png + icons/22x22/go-jump.png + icons/22x22/go-next.png + icons/22x22/go-previous.png + icons/22x22/go-up.png + icons/22x22/gstreamer.png + icons/22x22/headset.png + icons/22x22/help-hint.png + icons/22x22/intel.png + icons/22x22/jack.png + icons/22x22/keyboard.png + icons/22x22/list-add.png + icons/22x22/list-remove.png + icons/22x22/mcintosh-player.png + icons/22x22/mcintosh.png + icons/22x22/mcintosh-text.png + icons/22x22/media-eject.png + icons/22x22/media-forward.png + icons/22x22/media-pause.png + icons/22x22/media-playlist-repeat.png + icons/22x22/media-playlist-shuffle.png + icons/22x22/media-play.png + icons/22x22/media-rewind.png + icons/22x22/media-stop.png + icons/22x22/nvidia.png + icons/22x22/play2.png + icons/22x22/pulseaudio.png + icons/22x22/realtek.png + icons/22x22/search.png + icons/22x22/soundcard.png + icons/22x22/speaker.png + icons/22x22/star-grey.png + icons/22x22/star.png + icons/22x22/strawberry.png + icons/22x22/strawberry.svg + icons/22x22/tools-wizard.png + icons/22x22/view-choose.png + icons/22x22/view-fullscreen.png + icons/22x22/view-media-lyrics.png + icons/22x22/view-media-playlist.png + icons/22x22/view-media-visualization.png + icons/22x22/view-refresh.png + icons/22x22/vinyl.png + icons/22x22/vlc.png + icons/22x22/xine.png + icons/22x22/zoom-in.png + icons/22x22/zoom-out.png + icons/22x22/tidal.png + icons/22x22/deezer.png + diff --git a/data/icons/128x128/deezer.png b/data/icons/128x128/deezer.png new file mode 100644 index 000000000..ca22afc36 Binary files /dev/null and b/data/icons/128x128/deezer.png differ diff --git a/data/icons/22x22/deezer.png b/data/icons/22x22/deezer.png new file mode 100644 index 000000000..2fd13abc3 Binary files /dev/null and b/data/icons/22x22/deezer.png differ diff --git a/data/icons/32x32/deezer.png b/data/icons/32x32/deezer.png new file mode 100644 index 000000000..9fb0924cf Binary files /dev/null and b/data/icons/32x32/deezer.png differ diff --git a/data/icons/48x48/deezer.png b/data/icons/48x48/deezer.png new file mode 100644 index 000000000..ffda072f3 Binary files /dev/null and b/data/icons/48x48/deezer.png differ diff --git a/data/icons/64x64/deezer.png b/data/icons/64x64/deezer.png new file mode 100644 index 000000000..06b9d92bf Binary files /dev/null and b/data/icons/64x64/deezer.png differ diff --git a/data/icons/full/deezer.png b/data/icons/full/deezer.png new file mode 100644 index 000000000..83cb80e08 Binary files /dev/null and b/data/icons/full/deezer.png differ diff --git a/data/misc/oauthsuccess.html b/data/misc/oauthsuccess.html new file mode 100644 index 000000000..752655469 --- /dev/null +++ b/data/misc/oauthsuccess.html @@ -0,0 +1,41 @@ + + + + + tr("Return to Strawberry") + + + + + +
+

tr("Success!")

+ +

tr("Please close your browser and return to Strawberry.")

+
+ + diff --git a/data/pictures/deezer.png b/data/pictures/deezer.png new file mode 100644 index 000000000..ebf4a057c Binary files /dev/null and b/data/pictures/deezer.png differ diff --git a/data/pictures/deezer_black_big.jpg b/data/pictures/deezer_black_big.jpg new file mode 100644 index 000000000..60da04225 Binary files /dev/null and b/data/pictures/deezer_black_big.jpg differ diff --git a/data/pictures/deezer_white_big.jpg b/data/pictures/deezer_white_big.jpg new file mode 100644 index 000000000..b9f5130fa Binary files /dev/null and b/data/pictures/deezer_white_big.jpg differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3a8c0efc3..70eb252d3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,5 @@ # Strawberry Music Player # Copyright 2013, Jonas Kvinge -# This file was part of Clementine. -# Copyright 2010, David Sansome # # Strawberry is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -59,6 +57,14 @@ if(HAVE_PHONON) include_directories(${PHONON_INCLUDE_DIRS}) endif() +if(HAVE_LIBDEEZER) + include_directories(${DEEZER_INCLUDE_DIRS}) +endif() + +if(HAVE_LIBDZMEDIA) + include_directories(${DZMEDIA_INCLUDE_DIRS}) +endif() + link_directories(${TAGLIB_LIBRARY_DIRS}) include_directories(${TAGLIB_INCLUDE_DIRS}) @@ -212,6 +218,7 @@ set(SOURCES settings/appearancesettingspage.cpp settings/notificationssettingspage.cpp settings/tidalsettingspage.cpp + settings/deezersettingspage.cpp dialogs/about.cpp dialogs/console.cpp @@ -257,6 +264,7 @@ set(SOURCES internet/internetmodel.cpp internet/internetservice.cpp internet/internetplaylistitem.cpp + internet/localredirectserver.cpp tidal/tidalservice.cpp tidal/tidalsearch.cpp @@ -266,6 +274,14 @@ set(SOURCES tidal/tidalsearchitemdelegate.cpp tidal/tidalurlhandler.cpp + deezer/deezerservice.cpp + deezer/deezersearch.cpp + deezer/deezersearchview.cpp + deezer/deezersearchmodel.cpp + deezer/deezersearchsortmodel.cpp + deezer/deezersearchitemdelegate.cpp + deezer/deezerurlhandler.cpp + ) set(HEADERS @@ -379,6 +395,7 @@ set(HEADERS settings/appearancesettingspage.h settings/notificationssettingspage.h settings/tidalsettingspage.h + settings/deezersettingspage.h dialogs/about.h dialogs/errordialog.h @@ -422,6 +439,7 @@ set(HEADERS internet/internetservice.h internet/internetmimedata.h internet/internetsongmimedata.h + internet/localredirectserver.h tidal/tidalservice.h tidal/tidalsearch.h @@ -429,6 +447,12 @@ set(HEADERS tidal/tidalsearchmodel.h tidal/tidalurlhandler.h + deezer/deezerservice.h + deezer/deezersearch.h + deezer/deezersearchview.h + deezer/deezersearchmodel.h + deezer/deezerurlhandler.h + ) set(UI @@ -465,6 +489,7 @@ set(UI settings/appearancesettingspage.ui settings/notificationssettingspage.ui settings/tidalsettingspage.ui + settings/deezersettingspage.ui equalizer/equalizer.ui equalizer/equalizerslider.ui @@ -483,10 +508,11 @@ set(UI globalshortcuts/globalshortcutgrabber.ui tidal/tidalsearchview.ui + deezer/deezersearchview.ui ) -set(RESOURCES ../data/data.qrc) +set(RESOURCES ../data/data.qrc ../data/icons.qrc) set(OTHER_SOURCES) option(USE_INSTALL_PREFIX "Look for data in CMAKE_INSTALL_PREFIX" ON) @@ -524,6 +550,12 @@ optional_source(HAVE_PHONON HEADERS engine/phononengine.h ) +# Deezer +optional_source(HAVE_DEEZER + SOURCES engine/deezerengine.cpp + HEADERS engine/deezerengine.h +) + # Lastfm optional_source(HAVE_LIBLASTFM SOURCES @@ -906,6 +938,14 @@ if(HAVE_PHONON) target_link_libraries(strawberry_lib ${PHONON_LIBRARIES}) endif() +if(HAVE_DEEZER) + target_link_libraries(strawberry_lib ${LIBDEEZER_LIBRARIES}) +endif() + +if(HAVE_DZMEDIA) + target_link_libraries(strawberry_lib ${LIBDZMEDIA_LIBRARIES}) +endif() + if(HAVE_LIBLASTFM) target_link_libraries(strawberry_lib ${LASTFM5_LIBRARIES}) endif(HAVE_LIBLASTFM) diff --git a/src/collection/groupbydialog.ui b/src/collection/groupbydialog.ui index b81a999d8..6614c7e6c 100644 --- a/src/collection/groupbydialog.ui +++ b/src/collection/groupbydialog.ui @@ -14,7 +14,7 @@ Collection advanced grouping - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -358,6 +358,7 @@ + diff --git a/src/collection/savedgroupingmanager.ui b/src/collection/savedgroupingmanager.ui index 6f8e51053..1ecd124b8 100644 --- a/src/collection/savedgroupingmanager.ui +++ b/src/collection/savedgroupingmanager.ui @@ -106,6 +106,7 @@ + diff --git a/src/config.h.in b/src/config.h.in index 864304cec..aa93c8a1c 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -1,18 +1,21 @@ -/* This file is part of Strawberry. - - Strawberry 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. - - Strawberry 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 Strawberry. If not, see . -*/ +/* + * Strawberry Music Player + * Copyright 2013, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ #ifndef CONFIG_H_IN #define CONFIG_H_IN @@ -38,6 +41,7 @@ #cmakedefine HAVE_SPARKLE #cmakedefine HAVE_CHROMAPRINT #cmakedefine HAVE_TAGLIB_DSFFILE +#cmakedefine HAVE_DZMEDIA #cmakedefine IMOBILEDEVICE_USES_UDIDS #cmakedefine USE_INSTALL_PREFIX #cmakedefine USE_SYSTEM_SHA2 @@ -46,6 +50,7 @@ #cmakedefine HAVE_VLC #cmakedefine HAVE_XINE #cmakedefine HAVE_PHONON +#cmakedefine HAVE_DEEZER #endif // CONFIG_H_IN diff --git a/src/context/contextviewcontainer.ui b/src/context/contextviewcontainer.ui index e56398882..2a28a61b8 100644 --- a/src/context/contextviewcontainer.ui +++ b/src/context/contextviewcontainer.ui @@ -562,6 +562,7 @@ + diff --git a/src/core/application.cpp b/src/core/application.cpp index 6cd954faa..42bc8cd41 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -60,6 +60,7 @@ #include "internet/internetmodel.h" #include "tidal/tidalsearch.h" +#include "deezer/deezersearch.h" bool Application::kIsPortable = false; @@ -108,15 +109,16 @@ class ApplicationImpl { return loader; }), current_art_loader_([=]() { return new CurrentArtLoader(app, app); }), - internet_model_([=]() { return new InternetModel(app, app); }), - tidal_search_([=]() { return new TidalSearch(app, app); }), lyrics_providers_([=]() { LyricsProviders *lyrics_providers = new LyricsProviders(app); lyrics_providers->AddProvider(new AuddLyricsProvider(app)); - lyrics_providers->AddProvider(new APISeedsLyricsProvider(app)); - return lyrics_providers; - }) - { } + lyrics_providers->AddProvider(new APISeedsLyricsProvider(app)); + return lyrics_providers; + }), + internet_model_([=]() { return new InternetModel(app, app); }), + tidal_search_([=]() { return new TidalSearch(app, app); }), + deezer_search_([=]() { return new DeezerSearch(app, app); }) + {} Lazy tag_reader_client_; Lazy database_; @@ -133,9 +135,10 @@ class ApplicationImpl { Lazy cover_providers_; Lazy album_cover_loader_; Lazy current_art_loader_; + Lazy lyrics_providers_; Lazy internet_model_; Lazy tidal_search_; - Lazy lyrics_providers_; + Lazy deezer_search_; }; @@ -181,73 +184,27 @@ void Application::MoveToThread(QObject *object, QThread *thread) { } void Application::AddError(const QString& message) { emit ErrorAdded(message); } - void Application::ReloadSettings() { emit SettingsChanged(); } +void Application::OpenSettingsDialogAtPage(SettingsDialog::Page page) { emit SettingsDialogRequested(page); } -void Application::OpenSettingsDialogAtPage(SettingsDialog::Page page) { - emit SettingsDialogRequested(page); -} - -AlbumCoverLoader *Application::album_cover_loader() const { - return p_->album_cover_loader_.get(); -} - +TagReaderClient *Application::tag_reader_client() const { return p_->tag_reader_client_.get(); } Appearance *Application::appearance() const { return p_->appearance_.get(); } - -CoverProviders *Application::cover_providers() const { - return p_->cover_providers_.get(); -} - -CurrentArtLoader *Application::current_art_loader() const { - return p_->current_art_loader_.get(); -} - Database *Application::database() const { return p_->database_.get(); } - -#ifndef Q_OS_WIN -DeviceManager *Application::device_manager() const { - return p_->device_manager_.get(); -} -#endif - -SCollection *Application::collection() const { return p_->collection_.get(); } - -CollectionBackend *Application::collection_backend() const { - return collection()->backend(); -} - -CollectionModel *Application::collection_model() const { return collection()->model(); } - +TaskManager *Application::task_manager() const { return p_->task_manager_.get(); } Player *Application::player() const { return p_->player_.get(); } - -PlaylistBackend *Application::playlist_backend() const { - return p_->playlist_backend_.get(); -} - -PlaylistManager *Application::playlist_manager() const { - return p_->playlist_manager_.get(); -} - -TagReaderClient *Application::tag_reader_client() const { - return p_->tag_reader_client_.get(); -} - -TaskManager *Application::task_manager() const { - return p_->task_manager_.get(); -} - -EngineDevice *Application::enginedevice() const { - return p_->enginedevice_.get(); -} - -InternetModel* Application::internet_model() const { - return p_->internet_model_.get(); -} - -TidalSearch* Application::tidal_search() const { - return p_->tidal_search_.get(); -} - -LyricsProviders *Application::lyrics_providers() const { - return p_->lyrics_providers_.get(); -} +EngineDevice *Application::enginedevice() const { return p_->enginedevice_.get(); } +#ifndef Q_OS_WIN +DeviceManager *Application::device_manager() const { return p_->device_manager_.get(); } +#endif +SCollection *Application::collection() const { return p_->collection_.get(); } +CollectionBackend *Application::collection_backend() const { return collection()->backend(); } +CollectionModel *Application::collection_model() const { return collection()->model(); } +AlbumCoverLoader *Application::album_cover_loader() const { return p_->album_cover_loader_.get(); } +CoverProviders *Application::cover_providers() const { return p_->cover_providers_.get(); } +CurrentArtLoader *Application::current_art_loader() const { return p_->current_art_loader_.get(); } +LyricsProviders *Application::lyrics_providers() const { return p_->lyrics_providers_.get(); } +PlaylistBackend *Application::playlist_backend() const { return p_->playlist_backend_.get(); } +PlaylistManager *Application::playlist_manager() const { return p_->playlist_manager_.get(); } +InternetModel *Application::internet_model() const { return p_->internet_model_.get(); } +TidalSearch *Application::tidal_search() const { return p_->tidal_search_.get(); } +DeezerSearch *Application::deezer_search() const { return p_->deezer_search_.get(); } diff --git a/src/core/application.h b/src/core/application.h index 3c20903d9..11ad8acb4 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -51,9 +51,10 @@ class DeviceManager; class CoverProviders; class AlbumCoverLoader; class CurrentArtLoader; +class LyricsProviders; class InternetModel; class TidalSearch; -class LyricsProviders; +class DeezerSearch; class Application : public QObject { Q_OBJECT @@ -75,6 +76,8 @@ class Application : public QObject { #endif SCollection *collection() const; + CollectionBackend *collection_backend() const; + CollectionModel *collection_model() const; PlaylistBackend *playlist_backend() const; PlaylistManager *playlist_manager() const; @@ -83,13 +86,11 @@ class Application : public QObject { AlbumCoverLoader *album_cover_loader() const; CurrentArtLoader *current_art_loader() const; - CollectionBackend *collection_backend() const; - CollectionModel *collection_model() const; + LyricsProviders *lyrics_providers() const; InternetModel *internet_model() const; TidalSearch *tidal_search() const; - - LyricsProviders *lyrics_providers() const; + DeezerSearch *deezer_search() const; void MoveToNewThread(QObject *object); void MoveToThread(QObject *object, QThread *thread); diff --git a/src/core/main.cpp b/src/core/main.cpp index 17ccb6c6a..931dc5be7 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -200,6 +200,7 @@ int main(int argc, char* argv[]) { // Resources Q_INIT_RESOURCE(data); + Q_INIT_RESOURCE(icons); Application app; diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index b06ca6aa0..844bbc3cb 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -134,7 +134,11 @@ #include "settings/playlistsettingspage.h" #include "settings/settingsdialog.h" +#include "internet/internetmodel.h" +#include "internet/internetservice.h" + #include "tidal/tidalsearchview.h" +#include "deezer/deezersearchview.h" #if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT) # include "musicbrainz/tagfetcher.h" @@ -200,6 +204,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co return manager; }), tidal_search_view_(new TidalSearchView(app_, this)), + deezer_search_view_(new DeezerSearchView(app_, this)), playlist_menu_(new QMenu(this)), playlist_add_to_another_(nullptr), playlistitem_actions_separator_(nullptr), @@ -243,7 +248,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co ui_->volume->setValue(volume); VolumeChanged(volume); - // Initialise the tidal search widget + // Initialise the search widget StyleHelper::setBaseColor(palette().color(QPalette::Highlight).darker()); // Add tabs to the fancy tab widget @@ -255,6 +260,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co ui_->tabs->addTab(device_view_, IconLoader::Load("device"), tr("Devices")); #endif ui_->tabs->addTab(tidal_search_view_, IconLoader::Load("tidal"), tr("Tidal", "Tidal")); + ui_->tabs->addTab(deezer_search_view_, IconLoader::Load("deezer"), tr("Deezer", "Deezer")); //ui_->tabs->AddSpacer(); // Add the playing widget to the fancy tab widget @@ -515,6 +521,8 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co // Tidal connect(tidal_search_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); + // Deezer + connect(deezer_search_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); // Playlist menu playlist_play_pause_ = playlist_menu_->addAction(tr("Play"), this, SLOT(PlaylistPlay())); @@ -710,12 +718,6 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co ReloadSettings(); - // Tidal search shortcut - QAction *tidal_search_action = new QAction(this); - tidal_search_action->setShortcuts(QList() << QKeySequence("Ctrl+F") << QKeySequence("Ctrl+L")); - addAction(tidal_search_action); - connect(tidal_search_action, SIGNAL(triggered()), SLOT(FocusTidalSearchField())); - // Reload pretty OSD to avoid issues with fonts osd_->ReloadPrettyOSDSettings(); @@ -809,6 +811,7 @@ void MainWindow::ReloadAllSettings() { collection_view_->ReloadSettings(); ui_->playlist->view()->ReloadSettings(); tidal_search_view_->ReloadSettings(); + deezer_search_view_->ReloadSettings(); } @@ -2311,39 +2314,6 @@ void MainWindow::keyPressEvent(QKeyEvent *event) { } } -void MainWindow::FocusTidalSearchField() { - ui_->tabs->setCurrentWidget(tidal_search_view_); - tidal_search_view_->FocusSearchField(); -} - -void MainWindow::DoTidalSearch(const QString& query) { - FocusTidalSearchField(); - tidal_search_view_->StartSearch(query); -} - -void MainWindow::SearchForArtist() { - - PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(playlist_menu_index_.row())); - Song song = item->Metadata(); - if (!song.albumartist().isEmpty()) { - DoTidalSearch(song.albumartist().simplified()); - } - else if (!song.artist().isEmpty()) { - DoTidalSearch(song.artist().simplified()); - } - -} - -void MainWindow::SearchForAlbum() { - - PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(playlist_menu_index_.row())); - Song song = item->Metadata(); - if (!song.album().isEmpty()) { - DoTidalSearch(song.album().simplified()); - } - -} - void MainWindow::LoadCoverFromFile() { album_cover_choice_controller_->LoadCoverFromFile(&song_); } @@ -2402,4 +2372,3 @@ void MainWindow::GetCoverAutomatically() { if (search) album_cover_choice_controller_->SearchCoverAutomatically(song_); } - diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index a7c29f419..52846708f 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -89,6 +89,7 @@ class TranscodeDialog; class Ui_MainWindow; class Windows7ThumbBar; class TidalSearchView; +class DeezerSearchView; class MainWindow : public QMainWindow, public PlatformInterface { Q_OBJECT @@ -273,11 +274,6 @@ signals: void ShowConsole(); - void FocusTidalSearchField(); - void DoTidalSearch(const QString& query); - void SearchForArtist(); - void SearchForAlbum(); - void LoadCoverFromFile(); void SaveCoverToFile(); void LoadCoverFromURL(); @@ -342,6 +338,7 @@ signals: #endif TidalSearchView *tidal_search_view_; + DeezerSearchView *deezer_search_view_; QAction *collection_show_all_; QAction *collection_show_duplicates_; diff --git a/src/core/mainwindow.ui b/src/core/mainwindow.ui index 545f5d2b6..9e332b48c 100644 --- a/src/core/mainwindow.ui +++ b/src/core/mainwindow.ui @@ -14,7 +14,7 @@ Strawberry Music Player - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -553,7 +553,7 @@ - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -782,6 +782,7 @@ + diff --git a/src/core/metatypes.cpp b/src/core/metatypes.cpp index 455c8fdea..592b704c4 100644 --- a/src/core/metatypes.cpp +++ b/src/core/metatypes.cpp @@ -61,6 +61,7 @@ #endif #include "tidal/tidalsearch.h" +#include "deezer/deezersearch.h" void RegisterMetaTypes() { @@ -118,4 +119,7 @@ void RegisterMetaTypes() { qRegisterMetaType("TidalSearch::ResultList"); qRegisterMetaType("TidalSearch::Result"); + qRegisterMetaType("DeezerSearch::ResultList"); + qRegisterMetaType("DeezerSearch::Result"); + } diff --git a/src/core/player.cpp b/src/core/player.cpp index 6dca439e7..df1547e34 100644 --- a/src/core/player.cpp +++ b/src/core/player.cpp @@ -59,6 +59,9 @@ #ifdef HAVE_VLC # include "engine/vlcengine.h" #endif +#ifdef HAVE_DEEZER +# include "engine/deezerengine.h" +#endif #include "collection/collectionbackend.h" #include "playlist/playlist.h" @@ -70,6 +73,8 @@ #include "settings/backendsettingspage.h" #include "settings/behavioursettingspage.h" #include "settings/playlistsettingspage.h" +#include "internet/internetmodel.h" +#include "internet/internetservice.h" using std::shared_ptr; @@ -103,7 +108,7 @@ Player::~Player() { void Player::CreateEngine(Engine::EngineType enginetype) { - Engine::EngineType use_enginetype = Engine::None; + Engine::EngineType use_enginetype(Engine::None); for (int i = 0 ; use_enginetype == Engine::None ; i++) { switch(enginetype) { @@ -131,6 +136,15 @@ void Player::CreateEngine(Engine::EngineType enginetype) { use_enginetype=Engine::Phonon; engine_.reset(new PhononEngine(app_->task_manager())); break; +#endif +#ifdef HAVE_DEEZER + case Engine::Deezer:{ + use_enginetype=Engine::Deezer; + DeezerEngine *deezerengine = new DeezerEngine(app_->task_manager()); + connect(this, SIGNAL(Authenticated()), deezerengine, SLOT(LoadAccessToken())); + engine_.reset(deezerengine); + break; + } #endif default: if (i > 0) { qFatal("No engine available!"); } @@ -144,7 +158,7 @@ void Player::CreateEngine(Engine::EngineType enginetype) { s.beginGroup(BackendSettingsPage::kSettingsGroup); s.setValue("engine", EngineName(use_enginetype)); s.setValue("output", engine_->DefaultOutput()); - s.setValue("device", QVariant("")); + s.setValue("device", QVariant()); s.endGroup(); } @@ -499,7 +513,7 @@ void Player::PlayAt(int index, Engine::TrackChangeFlags change, bool reshuffle) current_item_ = app_->playlist_manager()->active()->current_item(); const QUrl url = current_item_->Url(); - if (url_handlers_.contains(url.scheme())) { + if (url_handlers_.contains(url.scheme()) && !(engine_->type() == Engine::Deezer && url.scheme() == "dzmedia")) { // It's already loading if (url == loading_async_) return; @@ -761,3 +775,7 @@ void Player::UrlHandlerDestroyed(QObject *object) { } } + +void Player::HandleAuthentication() { + emit Authenticated(); +} diff --git a/src/core/player.h b/src/core/player.h index 4bce66a7a..ceb72981a 100644 --- a/src/core/player.h +++ b/src/core/player.h @@ -114,6 +114,9 @@ class PlayerInterface : public QObject { // The toggle parameter is true when user requests to toggle visibility for Pretty OSD void ForceShowOSD(Song, bool toogle); + + void Authenticated(); + }; class Player : public PlayerInterface { @@ -176,6 +179,8 @@ class Player : public PlayerInterface { void Play(); void ShowOSD(); void TogglePrettyOSD(); + + void HandleAuthentication(); private slots: void EngineStateChanged(Engine::State); diff --git a/src/core/song.cpp b/src/core/song.cpp index 36ae8e855..8958decfd 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -298,7 +298,7 @@ uint Song::mtime() const { return d->mtime_; } uint Song::ctime() const { return d->ctime_; } int Song::filesize() const { return d->filesize_; } Song::FileType Song::filetype() const { return d->filetype_; } -bool Song::is_stream() const { return d->source_ == Source_Stream || d->source_ == Source_Tidal; } +bool Song::is_stream() const { return d->source_ == Source_Stream || d->source_ == Source_Tidal || d->source_ == Source_Deezer; } bool Song::is_cdda() const { return d->source_ == Source_CDDA; } bool Song::is_collection_song() const { return !is_cdda() && !is_stream() && id() != -1; @@ -384,6 +384,7 @@ QString Song::TextForSource(Source source) { case Song::Source_Device: return QObject::tr("Device"); case Song::Source_Stream: return QObject::tr("Stream"); case Song::Source_Tidal: return QObject::tr("Tidal"); + case Song::Source_Deezer: return QObject::tr("Deezer"); default: return QObject::tr("Unknown"); } @@ -398,6 +399,7 @@ QIcon Song::IconForSource(Source source) { case Song::Source_Device: return IconLoader::Load("device"); case Song::Source_Stream: return IconLoader::Load("applications-internet"); case Song::Source_Tidal: return IconLoader::Load("tidal"); + case Song::Source_Deezer: return IconLoader::Load("deezer"); default: return IconLoader::Load("edit-delete"); } diff --git a/src/core/song.h b/src/core/song.h index 8c91f1690..8f6708c32 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -98,6 +98,7 @@ class Song { Source_Device = 4, Source_Stream = 5, Source_Tidal = 6, + Source_Deezer = 7, }; enum FileType { diff --git a/src/covermanager/albumcoverexport.ui b/src/covermanager/albumcoverexport.ui index e8ed915f3..14cd2ccf4 100644 --- a/src/covermanager/albumcoverexport.ui +++ b/src/covermanager/albumcoverexport.ui @@ -20,7 +20,7 @@ Export covers - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -202,6 +202,7 @@ + diff --git a/src/covermanager/albumcovermanager.ui b/src/covermanager/albumcovermanager.ui index 66958348c..b5d25c030 100644 --- a/src/covermanager/albumcovermanager.ui +++ b/src/covermanager/albumcovermanager.ui @@ -14,7 +14,7 @@ Cover Manager - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -297,6 +297,7 @@ + diff --git a/src/covermanager/coverfromurldialog.ui b/src/covermanager/coverfromurldialog.ui index f9a908327..971d499d7 100644 --- a/src/covermanager/coverfromurldialog.ui +++ b/src/covermanager/coverfromurldialog.ui @@ -14,7 +14,7 @@ Load cover from URL - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -79,6 +79,7 @@ + diff --git a/src/deezer/deezersearch.cpp b/src/deezer/deezersearch.cpp new file mode 100644 index 000000000..41f1fa374 --- /dev/null +++ b/src/deezer/deezersearch.cpp @@ -0,0 +1,329 @@ +/* + * Strawberry Music Player + * This code was part of Clementine (GlobalSearch) + * Copyright 2010, David Sansome + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/application.h" +#include "core/logging.h" +#include "core/closure.h" +#include "core/iconloader.h" +#include "covermanager/albumcoverloader.h" +#include "internet/internetsongmimedata.h" +#include "playlist/songmimedata.h" +#include "deezersearch.h" +#include "deezerservice.h" +#include "settings/deezersettingspage.h" + +const int DeezerSearch::kDelayedSearchTimeoutMs = 200; +const int DeezerSearch::kMaxResultsPerEmission = 2000; +const int DeezerSearch::kArtHeight = 32; + +DeezerSearch::DeezerSearch(Application *app, QObject *parent) + : QObject(parent), + app_(app), + service_(app->internet_model()->Service()), + name_("Deezer"), + id_("deezer"), + icon_(IconLoader::Load("deezer")), + searches_next_id_(1), + art_searches_next_id_(1) { + + cover_loader_options_.desired_height_ = kArtHeight; + cover_loader_options_.pad_output_image_ = true; + cover_loader_options_.scale_output_image_ = true; + + connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(AlbumArtLoaded(quint64, QImage))); + connect(this, SIGNAL(SearchAsyncSig(int, QString, DeezerSettingsPage::SearchBy)), this, SLOT(DoSearchAsync(int, QString, DeezerSettingsPage::SearchBy))); + connect(this, SIGNAL(ResultsAvailable(int, DeezerSearch::ResultList)), SLOT(ResultsAvailableSlot(int, DeezerSearch::ResultList))); + connect(this, SIGNAL(ArtLoaded(int, QImage)), SLOT(ArtLoadedSlot(int, QImage))); + connect(service_, SIGNAL(UpdateStatus(QString)), SLOT(UpdateStatusSlot(QString))); + connect(service_, SIGNAL(ProgressSetMaximum(int)), SLOT(ProgressSetMaximumSlot(int))); + connect(service_, SIGNAL(UpdateProgress(int)), SLOT(UpdateProgressSlot(int))); + connect(service_, SIGNAL(SearchResults(int, SongList)), SLOT(SearchDone(int, SongList))); + connect(service_, SIGNAL(SearchError(int, QString)), SLOT(HandleError(int, QString))); + + icon_as_image_ = QImage(icon_.pixmap(48, 48).toImage()); + +} + +DeezerSearch::~DeezerSearch() {} + +QStringList DeezerSearch::TokenizeQuery(const QString &query) { + + QStringList tokens(query.split(QRegExp("\\s+"))); + + for (QStringList::iterator it = tokens.begin(); it != tokens.end(); ++it) { + (*it).remove('('); + (*it).remove(')'); + (*it).remove('"'); + + const int colon = (*it).indexOf(":"); + if (colon != -1) { + (*it).remove(0, colon + 1); + } + } + + return tokens; + +} + +bool DeezerSearch::Matches(const QStringList &tokens, const QString &string) { + + for (const QString &token : tokens) { + if (!string.contains(token, Qt::CaseInsensitive)) { + return false; + } + } + + return true; + +} + +int DeezerSearch::SearchAsync(const QString &query, DeezerSettingsPage::SearchBy searchby) { + + const int id = searches_next_id_++; + + emit SearchAsyncSig(id, query, searchby); + + return id; + +} + +void DeezerSearch::SearchAsync(int id, const QString &query, DeezerSettingsPage::SearchBy searchby) { + + const int service_id = service_->Search(query, searchby); + pending_searches_[service_id] = PendingState(id, TokenizeQuery(query)); + +} + +void DeezerSearch::DoSearchAsync(int id, const QString &query, DeezerSettingsPage::SearchBy searchby) { + + int timer_id = startTimer(kDelayedSearchTimeoutMs); + delayed_searches_[timer_id].id_ = id; + delayed_searches_[timer_id].query_ = query; + delayed_searches_[timer_id].searchby_ = searchby; + +} + +void DeezerSearch::SearchDone(int service_id, const SongList &songs) { + + // Map back to the original id. + const PendingState state = pending_searches_.take(service_id); + const int search_id = state.orig_id_; + + ResultList ret; + for (const Song &song : songs) { + Result result; + result.metadata_ = song; + ret << result; + } + + emit ResultsAvailable(search_id, ret); + MaybeSearchFinished(search_id); + +} + +void DeezerSearch::HandleError(const int id, const QString error) { + + emit SearchError(id, error); + +} + +void DeezerSearch::MaybeSearchFinished(int id) { + + if (pending_searches_.keys(PendingState(id, QStringList())).isEmpty()) { + emit SearchFinished(id); + } + +} + +void DeezerSearch::CancelSearch(int id) { + QMap::iterator it; + for (it = delayed_searches_.begin(); it != delayed_searches_.end(); ++it) { + if (it.value().id_ == id) { + killTimer(it.key()); + delayed_searches_.erase(it); + return; + } + } + service_->CancelSearch(); +} + +void DeezerSearch::timerEvent(QTimerEvent *e) { + QMap::iterator it = delayed_searches_.find(e->timerId()); + if (it != delayed_searches_.end()) { + SearchAsync(it.value().id_, it.value().query_, it.value().searchby_); + delayed_searches_.erase(it); + return; + } + + QObject::timerEvent(e); +} + +void DeezerSearch::ResultsAvailableSlot(int id, DeezerSearch::ResultList results) { + + if (results.isEmpty()) return; + + // Limit the number of results that are used from each emission. + if (results.count() > kMaxResultsPerEmission) { + DeezerSearch::ResultList::iterator begin = results.begin(); + std::advance(begin, kMaxResultsPerEmission); + results.erase(begin, results.end()); + } + + // Load cached pixmaps into the results + for (DeezerSearch::ResultList::iterator it = results.begin(); it != results.end(); ++it) { + it->pixmap_cache_key_ = PixmapCacheKey(*it); + } + + emit AddResults(id, results); + +} + +QString DeezerSearch::PixmapCacheKey(const DeezerSearch::Result &result) const { + return "deezer:" % result.metadata_.url().toString(); +} + +bool DeezerSearch::FindCachedPixmap(const DeezerSearch::Result &result, QPixmap *pixmap) const { + return pixmap_cache_.find(result.pixmap_cache_key_, pixmap); +} + +int DeezerSearch::LoadArtAsync(const DeezerSearch::Result &result) { + + const int id = art_searches_next_id_++; + + pending_art_searches_[id] = result.pixmap_cache_key_; + + quint64 loader_id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, result.metadata_); + cover_loader_tasks_[loader_id] = id; + + return id; + +} + +void DeezerSearch::ArtLoadedSlot(int id, const QImage &image) { + HandleLoadedArt(id, image); +} + +void DeezerSearch::AlbumArtLoaded(quint64 id, const QImage &image) { + + if (!cover_loader_tasks_.contains(id)) return; + int orig_id = cover_loader_tasks_.take(id); + + HandleLoadedArt(orig_id, image); + +} + +void DeezerSearch::HandleLoadedArt(int id, const QImage &image) { + + const QString key = pending_art_searches_.take(id); + + QPixmap pixmap = QPixmap::fromImage(image); + pixmap_cache_.insert(key, pixmap); + + emit ArtLoaded(id, pixmap); + +} + +QImage DeezerSearch::ScaleAndPad(const QImage &image) { + + if (image.isNull()) return QImage(); + + const QSize target_size = QSize(kArtHeight, kArtHeight); + + if (image.size() == target_size) return image; + + // Scale the image down + QImage copy; + copy = image.scaled(target_size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + + // Pad the image to kHeight x kHeight + if (copy.size() == target_size) return copy; + + QImage padded_image(kArtHeight, kArtHeight, QImage::Format_ARGB32); + padded_image.fill(0); + + QPainter p(&padded_image); + p.drawImage((kArtHeight - copy.width()) / 2, (kArtHeight - copy.height()) / 2, copy); + p.end(); + + return padded_image; + +} + +MimeData *DeezerSearch::LoadTracks(const ResultList &results) { + + if (results.isEmpty()) { + return nullptr; + } + + ResultList results_copy; + for (const Result &result : results) { + results_copy << result; + } + + SongList songs; + for (const Result &result : results) { + songs << result.metadata_; + } + + InternetSongMimeData *internet_song_mime_data = new InternetSongMimeData(service_); + internet_song_mime_data->songs = songs; + MimeData *mime_data = internet_song_mime_data; + + QList urls; + for (const Result &result : results) { + urls << result.metadata_.url(); + } + mime_data->setUrls(urls); + + return mime_data; + +} + +void DeezerSearch::UpdateStatusSlot(QString text) { + emit UpdateStatus(text); +} + +void DeezerSearch::ProgressSetMaximumSlot(int max) { + emit ProgressSetMaximum(max); +} + +void DeezerSearch::UpdateProgressSlot(int progress) { + emit UpdateProgress(progress); +} diff --git a/src/deezer/deezersearch.h b/src/deezer/deezersearch.h new file mode 100644 index 000000000..b0f4b0f46 --- /dev/null +++ b/src/deezer/deezersearch.h @@ -0,0 +1,164 @@ +/* + * Strawberry Music Player + * This code was part of Clementine (GlobalSearch) + * Copyright 2010, David Sansome + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#ifndef DEEZERSEARCH_H +#define DEEZERSEARCH_H + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "core/song.h" +#include "covermanager/albumcoverloaderoptions.h" +#include "settings/deezersettingspage.h" + +class Application; +class MimeData; +class AlbumCoverLoader; +class InternetService; +class DeezerService; + +class DeezerSearch : public QObject { + Q_OBJECT + + public: + DeezerSearch(Application *app, QObject *parent = nullptr); + ~DeezerSearch(); + + struct Result { + Song metadata_; + QString pixmap_cache_key_; + }; + typedef QList ResultList; + + static const int kDelayedSearchTimeoutMs; + static const int kMaxResultsPerEmission; + + Application *application() const { return app_; } + DeezerService *service() const { return service_; } + + int SearchAsync(const QString &query, DeezerSettingsPage::SearchBy searchby); + int LoadArtAsync(const DeezerSearch::Result &result); + + void CancelSearch(int id); + void CancelArt(int id); + + // Loads tracks for results that were previously emitted by ResultsAvailable. + // The implementation creates a SongMimeData with one Song for each Result. + MimeData *LoadTracks(const ResultList &results); + + signals: + void SearchAsyncSig(int id, const QString &query, DeezerSettingsPage::SearchBy searchby); + void ResultsAvailable(int id, const DeezerSearch::ResultList &results); + void AddResults(int id, const DeezerSearch::ResultList &results); + void SearchError(const int id, const QString error); + void SearchFinished(int id); + void UpdateStatus(QString text); + void ProgressSetMaximum(int progress); + void UpdateProgress(int max); + + void ArtLoaded(int id, const QPixmap &pixmap); + void ArtLoaded(int id, const QImage &image); + + protected: + + struct PendingState { + PendingState() : orig_id_(-1) {} + PendingState(int orig_id, QStringList tokens) + : orig_id_(orig_id), tokens_(tokens) {} + int orig_id_; + QStringList tokens_; + + bool operator<(const PendingState &b) const { + return orig_id_ < b.orig_id_; + } + + bool operator==(const PendingState &b) const { + return orig_id_ == b.orig_id_; + } + }; + + void timerEvent(QTimerEvent *e); + + // These functions treat queries in the same way as LibraryQuery. + // They're useful for figuring out whether you got a result because it matched in the song title or the artist/album name. + static QStringList TokenizeQuery(const QString &query); + static bool Matches(const QStringList &tokens, const QString &string); + + private slots: + void DoSearchAsync(int id, const QString &query, DeezerSettingsPage::SearchBy searchby); + void SearchDone(int id, const SongList &songs); + void HandleError(const int id, const QString error); + void ResultsAvailableSlot(int id, DeezerSearch::ResultList results); + + void ArtLoadedSlot(int id, const QImage &image); + void AlbumArtLoaded(quint64 id, const QImage &image); + + void UpdateStatusSlot(QString text); + void ProgressSetMaximumSlot(int progress); + void UpdateProgressSlot(int max); + + private: + void SearchAsync(int id, const QString &query, DeezerSettingsPage::SearchBy searchby); + void HandleLoadedArt(int id, const QImage &image); + bool FindCachedPixmap(const DeezerSearch::Result &result, QPixmap *pixmap) const; + QString PixmapCacheKey(const DeezerSearch::Result &result) const; + void MaybeSearchFinished(int id); + void ShowConfig() {} + static QImage ScaleAndPad(const QImage &image); + + private: + struct DelayedSearch { + int id_; + QString query_; + DeezerSettingsPage::SearchBy searchby_; + }; + + static const int kArtHeight; + + Application *app_; + DeezerService *service_; + Song::Source source_; + QString name_; + QString id_; + QIcon icon_; + QImage icon_as_image_; + int searches_next_id_; + int art_searches_next_id_; + + QMap delayed_searches_; + QMap pending_art_searches_; + QPixmapCache pixmap_cache_; + AlbumCoverLoaderOptions cover_loader_options_; + QMap cover_loader_tasks_; + + QMap pending_searches_; + +}; + +Q_DECLARE_METATYPE(DeezerSearch::Result) +Q_DECLARE_METATYPE(DeezerSearch::ResultList) + +#endif // DEEZERSEARCH_H diff --git a/src/deezer/deezersearchitemdelegate.cpp b/src/deezer/deezersearchitemdelegate.cpp new file mode 100644 index 000000000..e1639d7f7 --- /dev/null +++ b/src/deezer/deezersearchitemdelegate.cpp @@ -0,0 +1,35 @@ +/* + * Strawberry Music Player + * This code was part of Clementine (GlobalSearch) + * Copyright 2012, David Sansome + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#include +#include + +#include "deezersearchitemdelegate.h" +#include "deezersearchview.h" + +DeezerSearchItemDelegate::DeezerSearchItemDelegate(DeezerSearchView* view) + : CollectionItemDelegate(view), view_(view) {} + +void DeezerSearchItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + // Tell the view we painted this item so it can lazy load some art. + const_cast(view_)->LazyLoadArt(index); + + CollectionItemDelegate::paint(painter, option, index); +} diff --git a/src/deezer/deezersearchitemdelegate.h b/src/deezer/deezersearchitemdelegate.h new file mode 100644 index 000000000..d00a3906b --- /dev/null +++ b/src/deezer/deezersearchitemdelegate.h @@ -0,0 +1,41 @@ +/* + * Strawberry Music Player + * This code was part of Clementine (GlobalSearch) + * Copyright 2012, David Sansome + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#ifndef DEEZERSEARCHITEMDELEGATE_H +#define DEEZERSEARCHITEMDELEGATE_H + +#include +#include + +#include "collection/collectionview.h" + +class DeezerSearchView; + +class DeezerSearchItemDelegate : public CollectionItemDelegate { + public: + DeezerSearchItemDelegate(DeezerSearchView *view); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + + private: + DeezerSearchView* view_; +}; + +#endif // DEEZERSEARCHITEMDELEGATE_H diff --git a/src/deezer/deezersearchmodel.cpp b/src/deezer/deezersearchmodel.cpp new file mode 100644 index 000000000..21f5c4375 --- /dev/null +++ b/src/deezer/deezersearchmodel.cpp @@ -0,0 +1,319 @@ +/* + * Strawberry Music Player + * This code was part of Clementine (GlobalSearch) + * Copyright 2012, David Sansome + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/mimedata.h" +#include "core/iconloader.h" +#include "core/logging.h" +#include "deezersearch.h" +#include "deezersearchmodel.h" + +DeezerSearchModel::DeezerSearchModel(DeezerSearch *engine, QObject *parent) + : QStandardItemModel(parent), + engine_(engine), + proxy_(nullptr), + use_pretty_covers_(true), + artist_icon_(IconLoader::Load("folder-sound")) { + + group_by_[0] = CollectionModel::GroupBy_Artist; + group_by_[1] = CollectionModel::GroupBy_Album; + group_by_[2] = CollectionModel::GroupBy_None; + + QIcon nocover = IconLoader::Load("cdcase"); + no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(CollectionModel::kPrettyCoverSize, CollectionModel::kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + + //no_cover_icon_ = QPixmap(":/pictures/noalbumart.png").scaled(CollectionModel::kPrettyCoverSize, CollectionModel::kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + album_icon_ = no_cover_icon_; + +} + +void DeezerSearchModel::AddResults(const DeezerSearch::ResultList &results) { + + int sort_index = 0; + + for (const DeezerSearch::Result &result : results) { + QStandardItem *parent = invisibleRootItem(); + + // Find (or create) the container nodes for this result if we can. + ContainerKey key; + key.provider_index_ = sort_index; + parent = BuildContainers(result.metadata_, parent, &key); + + // Create the item + QStandardItem *item = new QStandardItem; + item->setText(result.metadata_.TitleWithCompilationArtist()); + item->setData(QVariant::fromValue(result), Role_Result); + item->setData(sort_index, Role_ProviderIndex); + + parent->appendRow(item); + + } + +} + +QStandardItem *DeezerSearchModel::BuildContainers(const Song &s, QStandardItem *parent, ContainerKey *key, int level) { + + if (level >= 3) { + return parent; + } + + bool has_artist_icon = false; + bool has_album_icon = false; + QString display_text; + QString sort_text; + int unique_tag = -1; + int year = 0; + + switch (group_by_[level]) { + case CollectionModel::GroupBy_Artist: + if (s.is_compilation()) { + display_text = tr("Various artists"); + sort_text = "aaaaaa"; + } + else { + display_text = CollectionModel::TextOrUnknown(s.artist()); + sort_text = CollectionModel::SortTextForArtist(s.artist()); + } + has_artist_icon = true; + break; + + case CollectionModel::GroupBy_YearAlbum: + year = qMax(0, s.year()); + display_text = CollectionModel::PrettyYearAlbum(year, s.album()); + sort_text = CollectionModel::SortTextForNumber(year) + s.album(); + unique_tag = s.album_id(); + has_album_icon = true; + break; + + case CollectionModel::GroupBy_OriginalYearAlbum: + year = qMax(0, s.effective_originalyear()); + display_text = CollectionModel::PrettyYearAlbum(year, s.album()); + sort_text = CollectionModel::SortTextForNumber(year) + s.album(); + unique_tag = s.album_id(); + has_album_icon = true; + break; + + case CollectionModel::GroupBy_Year: + year = qMax(0, s.year()); + display_text = QString::number(year); + sort_text = CollectionModel::SortTextForNumber(year) + " "; + break; + + case CollectionModel::GroupBy_OriginalYear: + year = qMax(0, s.effective_originalyear()); + display_text = QString::number(year); + sort_text = CollectionModel::SortTextForNumber(year) + " "; + break; + + case CollectionModel::GroupBy_Composer: + display_text = s.composer(); + case CollectionModel::GroupBy_Performer: + display_text = s.performer(); + case CollectionModel::GroupBy_Disc: + display_text = s.disc(); + case CollectionModel::GroupBy_Grouping: + display_text = s.grouping(); + case CollectionModel::GroupBy_Genre: + if (display_text.isNull()) display_text = s.genre(); + case CollectionModel::GroupBy_Album: + unique_tag = s.album_id(); + if (display_text.isNull()) { + display_text = s.album(); + } + // fallthrough + case CollectionModel::GroupBy_AlbumArtist: + if (display_text.isNull()) display_text = s.effective_albumartist(); + display_text = CollectionModel::TextOrUnknown(display_text); + sort_text = CollectionModel::SortTextForArtist(display_text); + has_album_icon = true; + break; + + case CollectionModel::GroupBy_FileType: + display_text = s.TextForFiletype(); + sort_text = display_text; + break; + + case CollectionModel::GroupBy_Bitrate: + display_text = QString(s.bitrate(), 1); + sort_text = display_text; + break; + + case CollectionModel::GroupBy_Samplerate: + display_text = QString(s.samplerate(), 1); + sort_text = display_text; + break; + + case CollectionModel::GroupBy_Bitdepth: + display_text = QString(s.bitdepth(), 1); + sort_text = display_text; + break; + + case CollectionModel::GroupBy_None: + return parent; + } + + // Find a container for this level + key->group_[level] = display_text + QString::number(unique_tag); + QStandardItem *container = containers_[*key]; + if (!container) { + container = new QStandardItem(display_text); + container->setData(key->provider_index_, Role_ProviderIndex); + container->setData(sort_text, CollectionModel::Role_SortText); + container->setData(group_by_[level], CollectionModel::Role_ContainerType); + + if (has_artist_icon) { + container->setIcon(artist_icon_); + } + else if (has_album_icon) { + if (use_pretty_covers_) { + container->setData(no_cover_icon_, Qt::DecorationRole); + } + else { + container->setIcon(album_icon_); + } + } + + parent->appendRow(container); + containers_[*key] = container; + } + + // Create the container for the next level. + return BuildContainers(s, container, key, level + 1); + +} + +void DeezerSearchModel::Clear() { + containers_.clear(); + clear(); +} + +DeezerSearch::ResultList DeezerSearchModel::GetChildResults(const QModelIndexList &indexes) const { + + QList items; + for (const QModelIndex &index : indexes) { + items << itemFromIndex(index); + } + return GetChildResults(items); + +} + +DeezerSearch::ResultList DeezerSearchModel::GetChildResults(const QList &items) const { + + DeezerSearch::ResultList results; + QSet visited; + + for (QStandardItem *item : items) { + GetChildResults(item, &results, &visited); + } + + return results; + +} + +void DeezerSearchModel::GetChildResults(const QStandardItem *item, DeezerSearch::ResultList *results, QSet *visited) const { + + if (visited->contains(item)) { + return; + } + visited->insert(item); + + // Does this item have children? + if (item->rowCount()) { + const QModelIndex parent_proxy_index = proxy_->mapFromSource(item->index()); + + // Yes - visit all the children, but do so through the proxy so we get them + // in the right order. + for (int i = 0; i < item->rowCount(); ++i) { + const QModelIndex proxy_index = parent_proxy_index.child(i, 0); + const QModelIndex index = proxy_->mapToSource(proxy_index); + GetChildResults(itemFromIndex(index), results, visited); + } + } + else { + // No - maybe it's a song, add its result if valid + QVariant result = item->data(Role_Result); + if (result.isValid()) { + results->append(result.value()); + } + else { + // Maybe it's a provider then? + bool is_provider; + const int sort_index = item->data(Role_ProviderIndex).toInt(&is_provider); + if (is_provider) { + // Go through all the items (through the proxy to keep them ordered) and add the ones belonging to this provider to our list + for (int i = 0; i < proxy_->rowCount(invisibleRootItem()->index()); ++i) { + QModelIndex child_index = proxy_->index(i, 0, invisibleRootItem()->index()); + const QStandardItem *child_item = itemFromIndex(proxy_->mapToSource(child_index)); + if (child_item->data(Role_ProviderIndex).toInt() == sort_index) { + GetChildResults(child_item, results, visited); + } + } + } + } + } + +} + +QMimeData *DeezerSearchModel::mimeData(const QModelIndexList &indexes) const { + return engine_->LoadTracks(GetChildResults(indexes)); +} + +namespace { +void GatherResults(const QStandardItem *parent, DeezerSearch::ResultList *results) { + + QVariant result_variant = parent->data(DeezerSearchModel::Role_Result); + if (result_variant.isValid()) { + DeezerSearch::Result result = result_variant.value(); + (*results).append(result); + } + + for (int i = 0; i < parent->rowCount(); ++i) { + GatherResults(parent->child(i), results); + } +} +} + +void DeezerSearchModel::SetGroupBy(const CollectionModel::Grouping &grouping, bool regroup_now) { + + const CollectionModel::Grouping old_group_by = group_by_; + group_by_ = grouping; + + if (regroup_now && group_by_ != old_group_by) { + // Walk the tree gathering the results we have already + DeezerSearch::ResultList results; + GatherResults(invisibleRootItem(), &results); + + // Reset the model and re-add all the results using the new grouping. + Clear(); + AddResults(results); + } + +} diff --git a/src/deezer/deezersearchmodel.h b/src/deezer/deezersearchmodel.h new file mode 100644 index 000000000..6b5ec1924 --- /dev/null +++ b/src/deezer/deezersearchmodel.h @@ -0,0 +1,109 @@ +/* + * Strawberry Music Player + * This code was part of Clementine (GlobalSearch) + * Copyright 2012, David Sansome + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#ifndef DEEZERSEARCHMODEL_H +#define DEEZERSEARCHMODEL_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "collection/collectionmodel.h" +#include "deezersearch.h" + +class DeezerSearchModel : public QStandardItemModel { + Q_OBJECT + + public: + DeezerSearchModel(DeezerSearch *engine, QObject *parent = nullptr); + + enum Role { + Role_Result = CollectionModel::LastRole, + Role_LazyLoadingArt, + Role_ProviderIndex, + LastRole + }; + + struct ContainerKey { + int provider_index_; + QString group_[3]; + }; + + void set_proxy(QSortFilterProxyModel *proxy) { proxy_ = proxy; } + void set_use_pretty_covers(bool pretty) { use_pretty_covers_ = pretty; } + void SetGroupBy(const CollectionModel::Grouping &grouping, bool regroup_now); + + void Clear(); + + DeezerSearch::ResultList GetChildResults(const QModelIndexList &indexes) const; + DeezerSearch::ResultList GetChildResults(const QList &items) const; + + QMimeData *mimeData(const QModelIndexList &indexes) const; + + public slots: + void AddResults(const DeezerSearch::ResultList &results); + + private: + QStandardItem *BuildContainers(const Song &metadata, QStandardItem *parent, ContainerKey *key, int level = 0); + void GetChildResults(const QStandardItem *item, DeezerSearch::ResultList *results, QSet *visited) const; + + private: + DeezerSearch *engine_; + QSortFilterProxyModel *proxy_; + bool use_pretty_covers_; + QIcon artist_icon_; + QPixmap no_cover_icon_; + QIcon album_icon_; + CollectionModel::Grouping group_by_; + QMap containers_; + +}; + +inline uint qHash(const DeezerSearchModel::ContainerKey &key) { + return qHash(key.provider_index_) ^ qHash(key.group_[0]) ^ qHash(key.group_[1]) ^ qHash(key.group_[2]); +} + +inline bool operator<(const DeezerSearchModel::ContainerKey &left, const DeezerSearchModel::ContainerKey &right) { +#define CMP(field) \ + if (left.field < right.field) return true; \ + if (left.field > right.field) return false + + CMP(provider_index_); + CMP(group_[0]); + CMP(group_[1]); + CMP(group_[2]); + return false; + +#undef CMP +} + +#endif // DEEZERSEARCHMODEL_H diff --git a/src/deezer/deezersearchsortmodel.cpp b/src/deezer/deezersearchsortmodel.cpp new file mode 100644 index 000000000..8a400db01 --- /dev/null +++ b/src/deezer/deezersearchsortmodel.cpp @@ -0,0 +1,79 @@ +/* + * Strawberry Music Player + * This code was part of Clementine (GlobalSearch) + * Copyright 2010, David Sansome + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include + +#include "core/logging.h" +#include "deezersearchmodel.h" +#include "deezersearchsortmodel.h" + +DeezerSearchSortModel::DeezerSearchSortModel(QObject *parent) + : QSortFilterProxyModel(parent) {} + +bool DeezerSearchSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { + // Compare the provider sort index first. + const int index_left = left.data(DeezerSearchModel::Role_ProviderIndex).toInt(); + const int index_right = right.data(DeezerSearchModel::Role_ProviderIndex).toInt(); + if (index_left < index_right) return true; + if (index_left > index_right) return false; + + // Dividers always go first + if (left.data(CollectionModel::Role_IsDivider).toBool()) return true; + if (right.data(CollectionModel::Role_IsDivider).toBool()) return false; + + // Containers go before songs if they're at the same level + const bool left_is_container = left.data(CollectionModel::Role_ContainerType).isValid(); + const bool right_is_container = right.data(CollectionModel::Role_ContainerType).isValid(); + if (left_is_container && !right_is_container) return true; + if (right_is_container && !left_is_container) return false; + + // Containers get sorted on their sort text. + if (left_is_container) { + return QString::localeAwareCompare(left.data(CollectionModel::Role_SortText).toString(), right.data(CollectionModel::Role_SortText).toString()) < 0; + } + + // Otherwise we're comparing songs. Sort by disc, track, then title. + const DeezerSearch::Result r1 = left.data(DeezerSearchModel::Role_Result).value(); + const DeezerSearch::Result r2 = right.data(DeezerSearchModel::Role_Result).value(); + +#define CompareInt(field) \ + if (r1.metadata_.field() < r2.metadata_.field()) return true; \ + if (r1.metadata_.field() > r2.metadata_.field()) return false + + int ret = 0; + +#define CompareString(field) \ + ret = QString::localeAwareCompare(r1.metadata_.field(), r2.metadata_.field()); \ + if (ret < 0) return true; \ + if (ret > 0) return false + + CompareInt(disc); + CompareInt(track); + CompareString(title); + + return false; + +#undef CompareInt +#undef CompareString +} diff --git a/src/deezer/deezersearchsortmodel.h b/src/deezer/deezersearchsortmodel.h new file mode 100644 index 000000000..437b7b9cb --- /dev/null +++ b/src/deezer/deezersearchsortmodel.h @@ -0,0 +1,35 @@ +/* + * Strawberry Music Player + * This code was part of Clementine (GlobalSearch) + * Copyright 2010, David Sansome + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#ifndef DEEZERSEARCHSORTMODEL_H +#define DEEZERSEARCHSORTMODEL_H + +#include +#include + +class DeezerSearchSortModel : public QSortFilterProxyModel { + public: + DeezerSearchSortModel(QObject *parent = nullptr); + + protected: + bool lessThan(const QModelIndex &left, const QModelIndex &right) const; +}; + +#endif // DEEZERSEARCHSORTMODEL_H diff --git a/src/deezer/deezersearchview.cpp b/src/deezer/deezersearchview.cpp new file mode 100644 index 000000000..e30276035 --- /dev/null +++ b/src/deezer/deezersearchview.cpp @@ -0,0 +1,574 @@ +/* + * Strawberry Music Player + * This code was part of Clementine (GlobalSearch) + * Copyright 2012, David Sansome + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/application.h" +#include "core/logging.h" +#include "core/mimedata.h" +#include "core/timeconstants.h" +#include "core/iconloader.h" +#include "internet/internetsongmimedata.h" +#include "collection/collectionfilterwidget.h" +#include "collection/collectionmodel.h" +#include "collection/groupbydialog.h" +#include "playlist/songmimedata.h" +#include "deezersearch.h" +#include "deezersearchitemdelegate.h" +#include "deezersearchmodel.h" +#include "deezersearchsortmodel.h" +#include "deezersearchview.h" +#include "ui_deezersearchview.h" +#include "settings/deezersettingspage.h" + +using std::placeholders::_1; +using std::placeholders::_2; +using std::swap; + +const int DeezerSearchView::kSwapModelsTimeoutMsec = 250; + +DeezerSearchView::DeezerSearchView(Application *app, QWidget *parent) + : QWidget(parent), + app_(app), + engine_(app_->deezer_search()), + ui_(new Ui_DeezerSearchView), + context_menu_(nullptr), + last_search_id_(0), + front_model_(new DeezerSearchModel(engine_, this)), + back_model_(new DeezerSearchModel(engine_, this)), + current_model_(front_model_), + front_proxy_(new DeezerSearchSortModel(this)), + back_proxy_(new DeezerSearchSortModel(this)), + current_proxy_(front_proxy_), + swap_models_timer_(new QTimer(this)), + search_icon_(IconLoader::Load("search")), + warning_icon_(IconLoader::Load("dialog-warning")), + error_(false) { + + ui_->setupUi(this); + ui_->progressbar->hide(); + ui_->progressbar->reset(); + + front_model_->set_proxy(front_proxy_); + back_model_->set_proxy(back_proxy_); + + ui_->search->installEventFilter(this); + ui_->results_stack->installEventFilter(this); + + ui_->settings->setIcon(IconLoader::Load("configure")); + + // Must be a queued connection to ensure the DeezerSearch handles it first. + connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()), Qt::QueuedConnection); + + connect(ui_->search, SIGNAL(textChanged(QString)), SLOT(TextEdited(QString))); + connect(ui_->results, SIGNAL(AddToPlaylistSignal(QMimeData*)), SIGNAL(AddToPlaylist(QMimeData*))); + connect(ui_->results, SIGNAL(FocusOnFilterSignal(QKeyEvent*)), SLOT(FocusOnFilter(QKeyEvent*))); + + // Set the appearance of the results list + ui_->results->setItemDelegate(new DeezerSearchItemDelegate(this)); + ui_->results->setAttribute(Qt::WA_MacShowFocusRect, false); + ui_->results->setStyleSheet("QTreeView::item{padding-top:1px;}"); + + // Show the help page initially + ui_->results_stack->setCurrentWidget(ui_->help_page); + ui_->help_frame->setBackgroundRole(QPalette::Base); + + // Set the colour of the help text to the disabled window text colour + QPalette help_palette = ui_->label_helptext->palette(); + const QColor help_color = help_palette.color(QPalette::Disabled, QPalette::WindowText); + help_palette.setColor(QPalette::Normal, QPalette::WindowText, help_color); + help_palette.setColor(QPalette::Inactive, QPalette::WindowText, help_color); + ui_->label_helptext->setPalette(help_palette); + + // Make it bold + QFont help_font = ui_->label_helptext->font(); + help_font.setBold(true); + ui_->label_helptext->setFont(help_font); + + // Set up the sorting proxy model + front_proxy_->setSourceModel(front_model_); + front_proxy_->setDynamicSortFilter(true); + front_proxy_->sort(0); + + back_proxy_->setSourceModel(back_model_); + back_proxy_->setDynamicSortFilter(true); + back_proxy_->sort(0); + + swap_models_timer_->setSingleShot(true); + swap_models_timer_->setInterval(kSwapModelsTimeoutMsec); + connect(swap_models_timer_, SIGNAL(timeout()), SLOT(SwapModels())); + + // Add actions to the settings menu + group_by_actions_ = CollectionFilterWidget::CreateGroupByActions(this); + QMenu *settings_menu = new QMenu(this); + settings_menu->addActions(group_by_actions_->actions()); + settings_menu->addSeparator(); + settings_menu->addAction(IconLoader::Load("configure"), tr("Configure Deezer..."), this, SLOT(OpenSettingsDialog())); + ui_->settings->setMenu(settings_menu); + + connect(ui_->radiobutton_searchbyalbums, SIGNAL(clicked(bool)), SLOT(SearchByAlbumsClicked(bool))); + connect(ui_->radiobutton_searchbysongs, SIGNAL(clicked(bool)), SLOT(SearchBySongsClicked(bool))); + + connect(group_by_actions_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*))); + + // These have to be queued connections because they may get emitted before our call to Search() (or whatever) returns and we add the ID to the map. + + connect(engine_, SIGNAL(UpdateStatus(QString)), SLOT(UpdateStatus(QString))); + connect(engine_, SIGNAL(ProgressSetMaximum(int)), SLOT(ProgressSetMaximum(int)), Qt::QueuedConnection); + connect(engine_, SIGNAL(UpdateProgress(int)), SLOT(UpdateProgress(int)), Qt::QueuedConnection); + + connect(engine_, SIGNAL(AddResults(int, DeezerSearch::ResultList)), SLOT(AddResults(int, DeezerSearch::ResultList)), Qt::QueuedConnection); + connect(engine_, SIGNAL(SearchError(int, QString)), SLOT(SearchError(int, QString)), Qt::QueuedConnection); + connect(engine_, SIGNAL(ArtLoaded(int, QPixmap)), SLOT(ArtLoaded(int, QPixmap)), Qt::QueuedConnection); + + ReloadSettings(); + +} + +DeezerSearchView::~DeezerSearchView() { delete ui_; } + +void DeezerSearchView::ReloadSettings() { + + QSettings s; + + // Collection settings + + s.beginGroup(DeezerSettingsPage::kSettingsGroup); + const bool pretty = s.value("pretty_covers", true).toBool(); + front_model_->set_use_pretty_covers(pretty); + back_model_->set_use_pretty_covers(pretty); + s.endGroup(); + + // Deezer search settings + + s.beginGroup(DeezerSettingsPage::kSettingsGroup); + searchby_ = DeezerSettingsPage::SearchBy(s.value("searchby", int(DeezerSettingsPage::SearchBy_Songs)).toInt()); + switch (searchby_) { + case DeezerSettingsPage::SearchBy_Songs: + ui_->radiobutton_searchbysongs->setChecked(true); + break; + case DeezerSettingsPage::SearchBy_Albums: + ui_->radiobutton_searchbyalbums->setChecked(true); + break; + } + + SetGroupBy(CollectionModel::Grouping( + CollectionModel::GroupBy(s.value("group_by1", int(CollectionModel::GroupBy_Artist)).toInt()), + CollectionModel::GroupBy(s.value("group_by2", int(CollectionModel::GroupBy_Album)).toInt()), + CollectionModel::GroupBy(s.value("group_by3", int(CollectionModel::GroupBy_None)).toInt()))); + s.endGroup(); + +} + +void DeezerSearchView::StartSearch(const QString &query) { + + ui_->search->setText(query); + TextEdited(query); + + // Swap models immediately + swap_models_timer_->stop(); + SwapModels(); + +} + +void DeezerSearchView::TextEdited(const QString &text) { + + const QString trimmed(text.trimmed()); + + error_ = false; + + // Add results to the back model, switch models after some delay. + back_model_->Clear(); + current_model_ = back_model_; + current_proxy_ = back_proxy_; + swap_models_timer_->start(); + + // Cancel the last search (if any) and start the new one. + engine_->CancelSearch(last_search_id_); + // If text query is empty, don't start a new search + if (trimmed.isEmpty()) { + last_search_id_ = -1; + ui_->label_helptext->setText("Enter search terms above to find music"); + ui_->label_status->clear(); + ui_->progressbar->hide(); + ui_->progressbar->reset(); + } + else { + ui_->progressbar->reset(); + last_search_id_ = engine_->SearchAsync(trimmed, searchby_); + } + +} + +void DeezerSearchView::AddResults(int id, const DeezerSearch::ResultList &results) { + if (id != last_search_id_) return; + if (results.isEmpty()) return; + ui_->label_status->clear(); + ui_->progressbar->reset(); + ui_->progressbar->hide(); + current_model_->AddResults(results); +} + +void DeezerSearchView::SearchError(const int id, const QString error) { + error_ = true; + ui_->label_helptext->setText(error); + ui_->label_status->clear(); + ui_->progressbar->reset(); + ui_->progressbar->hide(); + ui_->results_stack->setCurrentWidget(ui_->help_page); +} + +void DeezerSearchView::SwapModels() { + + art_requests_.clear(); + + std::swap(front_model_, back_model_); + std::swap(front_proxy_, back_proxy_); + + ui_->results->setModel(front_proxy_); + + if (ui_->search->text().trimmed().isEmpty() || error_) { + ui_->results_stack->setCurrentWidget(ui_->help_page); + } + else { + ui_->results_stack->setCurrentWidget(ui_->results_page); + } + +} + +void DeezerSearchView::LazyLoadArt(const QModelIndex &proxy_index) { + + if (!proxy_index.isValid() || proxy_index.model() != front_proxy_) { + return; + } + + // Already loading art for this item? + if (proxy_index.data(DeezerSearchModel::Role_LazyLoadingArt).isValid()) { + return; + } + + // Should we even load art at all? + if (!app_->collection_model()->use_pretty_covers()) { + return; + } + + // Is this an album? + const CollectionModel::GroupBy container_type = CollectionModel::GroupBy(proxy_index.data(CollectionModel::Role_ContainerType).toInt()); + if (container_type != CollectionModel::GroupBy_Album && + container_type != CollectionModel::GroupBy_AlbumArtist && + container_type != CollectionModel::GroupBy_YearAlbum && + container_type != CollectionModel::GroupBy_OriginalYearAlbum) { + return; + } + + // Mark the item as loading art + const QModelIndex source_index = front_proxy_->mapToSource(proxy_index); + QStandardItem *item = front_model_->itemFromIndex(source_index); + item->setData(true, DeezerSearchModel::Role_LazyLoadingArt); + + // Walk down the item's children until we find a track + while (item->rowCount()) { + item = item->child(0); + } + + // Get the track's Result + const DeezerSearch::Result result = item->data(DeezerSearchModel::Role_Result).value(); + + // Load the art. + int id = engine_->LoadArtAsync(result); + art_requests_[id] = source_index; + +} + +void DeezerSearchView::ArtLoaded(int id, const QPixmap &pixmap) { + + if (!art_requests_.contains(id)) return; + QModelIndex index = art_requests_.take(id); + + if (!pixmap.isNull()) { + front_model_->itemFromIndex(index)->setData(pixmap, Qt::DecorationRole); + } + +} + +MimeData *DeezerSearchView::SelectedMimeData() { + + if (!ui_->results->selectionModel()) return nullptr; + + // Get all selected model indexes + QModelIndexList indexes = ui_->results->selectionModel()->selectedRows(); + if (indexes.isEmpty()) { + // There's nothing selected - take the first thing in the model that isn't a divider. + for (int i = 0; i < front_proxy_->rowCount(); ++i) { + QModelIndex index = front_proxy_->index(i, 0); + if (!index.data(CollectionModel::Role_IsDivider).toBool()) { + indexes << index; + ui_->results->setCurrentIndex(index); + break; + } + } + } + + // Still got nothing? Give up. + if (indexes.isEmpty()) { + return nullptr; + } + + // Get items for these indexes + QList items; + for (const QModelIndex &index : indexes) { + items << (front_model_->itemFromIndex(front_proxy_->mapToSource(index))); + } + + // Get a MimeData for these items + return engine_->LoadTracks(front_model_->GetChildResults(items)); + +} + +bool DeezerSearchView::eventFilter(QObject *object, QEvent *event) { + + if (object == ui_->search && event->type() == QEvent::KeyRelease) { + if (SearchKeyEvent(static_cast(event))) { + return true; + } + } + else if (object == ui_->results_stack && event->type() == QEvent::ContextMenu) { + if (ResultsContextMenuEvent(static_cast(event))) { + return true; + } + } + + return QWidget::eventFilter(object, event); + +} + +bool DeezerSearchView::SearchKeyEvent(QKeyEvent *event) { + + switch (event->key()) { + case Qt::Key_Up: + ui_->results->UpAndFocus(); + break; + + case Qt::Key_Down: + ui_->results->DownAndFocus(); + break; + + case Qt::Key_Escape: + ui_->search->clear(); + break; + + case Qt::Key_Return: + TextEdited(ui_->search->text()); + break; + + default: + return false; + } + + event->accept(); + return true; + +} + +bool DeezerSearchView::ResultsContextMenuEvent(QContextMenuEvent *event) { + + context_menu_ = new QMenu(this); + context_actions_ << context_menu_->addAction( IconLoader::Load("media-play"), tr("Append to current playlist"), this, SLOT(AddSelectedToPlaylist())); + context_actions_ << context_menu_->addAction( IconLoader::Load("media-play"), tr("Replace current playlist"), this, SLOT(LoadSelected())); + context_actions_ << context_menu_->addAction( IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenSelectedInNewPlaylist())); + + context_menu_->addSeparator(); + context_actions_ << context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddSelectedToPlaylistEnqueue())); + + context_menu_->addSeparator(); + + if (ui_->results->selectionModel() && ui_->results->selectionModel()->selectedRows().length() == 1) { + context_actions_ << context_menu_->addAction(IconLoader::Load("search"), tr("Search for this"), this, SLOT(SearchForThis())); + } + + context_menu_->addSeparator(); + context_menu_->addMenu(tr("Group by"))->addActions(group_by_actions_->actions()); + context_menu_->addAction(IconLoader::Load("configure"), tr("Configure Deezer..."), this, SLOT(OpenSettingsDialog())); + + const bool enable_context_actions = ui_->results->selectionModel() && ui_->results->selectionModel()->hasSelection(); + + for (QAction *action : context_actions_) { + action->setEnabled(enable_context_actions); + } + + context_menu_->popup(event->globalPos()); + + return true; + +} + +void DeezerSearchView::AddSelectedToPlaylist() { + emit AddToPlaylist(SelectedMimeData()); +} + +void DeezerSearchView::LoadSelected() { + MimeData *data = SelectedMimeData(); + if (!data) return; + + data->clear_first_ = true; + emit AddToPlaylist(data); +} + +void DeezerSearchView::AddSelectedToPlaylistEnqueue() { + MimeData *data = SelectedMimeData(); + if (!data) return; + + data->enqueue_now_ = true; + emit AddToPlaylist(data); +} + +void DeezerSearchView::OpenSelectedInNewPlaylist() { + MimeData *data = SelectedMimeData(); + if (!data) return; + + data->open_in_new_playlist_ = true; + emit AddToPlaylist(data); +} + +void DeezerSearchView::SearchForThis() { + StartSearch(ui_->results->selectionModel()->selectedRows().first().data().toString()); +} + +void DeezerSearchView::showEvent(QShowEvent *e) { + QWidget::showEvent(e); + FocusSearchField(); +} + +void DeezerSearchView::FocusSearchField() { + ui_->search->setFocus(); + ui_->search->selectAll(); +} + +void DeezerSearchView::hideEvent(QHideEvent *e) { + QWidget::hideEvent(e); +} + +void DeezerSearchView::FocusOnFilter(QKeyEvent *event) { + ui_->search->setFocus(); + QApplication::sendEvent(ui_->search, event); +} + +void DeezerSearchView::OpenSettingsDialog() { + app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Deezer); +} + +void DeezerSearchView::GroupByClicked(QAction *action) { + + if (action->property("group_by").isNull()) { + if (!group_by_dialog_) { + group_by_dialog_.reset(new GroupByDialog); + connect(group_by_dialog_.data(), SIGNAL(Accepted(CollectionModel::Grouping)), SLOT(SetGroupBy(CollectionModel::Grouping))); + } + + group_by_dialog_->show(); + return; + } + + SetGroupBy(action->property("group_by").value()); + +} + +void DeezerSearchView::SetGroupBy(const CollectionModel::Grouping &g) { + + // Clear requests: changing "group by" on the models will cause all the items to be removed/added again, + // so all the QModelIndex here will become invalid. New requests will be created for those + // songs when they will be displayed again anyway (when DeezerSearchItemDelegate::paint will call LazyLoadArt) + art_requests_.clear(); + // Update the models + front_model_->SetGroupBy(g, true); + back_model_->SetGroupBy(g, false); + + // Save the setting + QSettings s; + s.beginGroup(DeezerSettingsPage::kSettingsGroup); + s.setValue("group_by1", int(g.first)); + s.setValue("group_by2", int(g.second)); + s.setValue("group_by3", int(g.third)); + s.endGroup(); + + // Make sure the correct action is checked. + for (QAction *action : group_by_actions_->actions()) { + if (action->property("group_by").isNull()) continue; + + if (g == action->property("group_by").value()) { + action->setChecked(true); + return; + } + } + + // Check the advanced action + group_by_actions_->actions().last()->setChecked(true); + +} + +void DeezerSearchView::SearchBySongsClicked(bool checked) { + SetSearchBy(DeezerSettingsPage::SearchBy_Songs); +} + +void DeezerSearchView::SearchByAlbumsClicked(bool checked) { + SetSearchBy(DeezerSettingsPage::SearchBy_Albums); +} + +void DeezerSearchView::SetSearchBy(DeezerSettingsPage::SearchBy searchby) { + searchby_ = searchby; + QSettings s; + s.beginGroup(DeezerSettingsPage::kSettingsGroup); + s.setValue("searchby", int(searchby)); + s.endGroup(); + TextEdited(ui_->search->text()); +} + +void DeezerSearchView::UpdateStatus(QString text) { + ui_->progressbar->show(); + ui_->label_status->setText(text); +} + +void DeezerSearchView::ProgressSetMaximum(int max) { + ui_->progressbar->setMaximum(max); +} + +void DeezerSearchView::UpdateProgress(int progress) { + ui_->progressbar->setValue(progress); +} diff --git a/src/deezer/deezersearchview.h b/src/deezer/deezersearchview.h new file mode 100644 index 000000000..24b3d35f3 --- /dev/null +++ b/src/deezer/deezersearchview.h @@ -0,0 +1,142 @@ +/* + * Strawberry Music Player + * This code was part of Clementine (GlobalSearch) + * Copyright 2012, David Sansome + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#ifndef DEEZERSEARCHVIEW_H +#define DEEZERSEARCHVIEW_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "collection/collectionmodel.h" +#include "settings/settingsdialog.h" +#include "playlist/playlistmanager.h" +#include "deezersearch.h" +#include "settings/deezersettingspage.h" + +class Application; +class GroupByDialog; +class DeezerSearchModel; +class Ui_DeezerSearchView; + +class DeezerSearchView : public QWidget { + Q_OBJECT + + public: + DeezerSearchView(Application *app, QWidget *parent = nullptr); + ~DeezerSearchView(); + + static const int kSwapModelsTimeoutMsec; + + void LazyLoadArt(const QModelIndex &index); + + void showEvent(QShowEvent *e); + void hideEvent(QHideEvent *e); + bool eventFilter(QObject *object, QEvent *event); + + public slots: + void ReloadSettings(); + void StartSearch(const QString &query); + void FocusSearchField(); + void OpenSettingsDialog(); + +signals: + void AddToPlaylist(QMimeData *data); + + private slots: + void SwapModels(); + void TextEdited(const QString &text); + void UpdateStatus(QString text); + void ProgressSetMaximum(int progress); + void UpdateProgress(int max); + void AddResults(int id, const DeezerSearch::ResultList &results); + void SearchError(const int id, const QString error); + void ArtLoaded(int id, const QPixmap &pixmap); + + void FocusOnFilter(QKeyEvent *event); + + void AddSelectedToPlaylist(); + void LoadSelected(); + void OpenSelectedInNewPlaylist(); + void AddSelectedToPlaylistEnqueue(); + + void SearchForThis(); + + void SearchBySongsClicked(bool); + void SearchByAlbumsClicked(bool); + void GroupByClicked(QAction *action); + void SetSearchBy(DeezerSettingsPage::SearchBy searchby); + void SetGroupBy(const CollectionModel::Grouping &g); + + private: + MimeData *SelectedMimeData(); + + bool SearchKeyEvent(QKeyEvent *event); + bool ResultsContextMenuEvent(QContextMenuEvent *event); + + Application *app_; + DeezerSearch *engine_; + Ui_DeezerSearchView *ui_; + QScopedPointer group_by_dialog_; + + QMenu *context_menu_; + QList context_actions_; + QActionGroup *group_by_actions_; + + int last_search_id_; + + // Like graphics APIs have a front buffer and a back buffer, there's a front model and a back model + // The front model is the one that's shown in the UI and the back model is the one that lies in wait. + // current_model_ will point to either the front or the back model. + DeezerSearchModel *front_model_; + DeezerSearchModel *back_model_; + DeezerSearchModel *current_model_; + + QSortFilterProxyModel *front_proxy_; + QSortFilterProxyModel *back_proxy_; + QSortFilterProxyModel *current_proxy_; + + QMap art_requests_; + + QTimer *swap_models_timer_; + + QIcon search_icon_; + QIcon warning_icon_; + + DeezerSettingsPage::SearchBy searchby_; + bool error_; + +}; + +#endif // DEEZERSEARCHVIEW_H diff --git a/src/deezer/deezersearchview.ui b/src/deezer/deezersearchview.ui new file mode 100644 index 000000000..357891442 --- /dev/null +++ b/src/deezer/deezersearchview.ui @@ -0,0 +1,283 @@ + + + DeezerSearchView + + + + 0 + 0 + 400 + 660 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + + + + + + + Search for anything + + + + + + + + 20 + 0 + + + + QToolButton::InstantPopup + + + true + + + + + + + + + QLayout::SetFixedSize + + + + + true + + + Search by + + + 10 + + + + + + + a&lbums + + + + + + + son&gs + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + true + + + + + + + 0 + + + + + + + + + + + + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::DragOnly + + + QAbstractItemView::ExtendedSelection + + + true + + + false + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + 0 + 398 + 502 + + + + + + + + 32 + + + 16 + + + 32 + + + 64 + + + + + Enter search terms above to find music + + + Qt::AlignCenter + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + + + + QSearchField + QWidget +
3rdparty/qocoa/qsearchfield.h
+
+ + AutoExpandingTreeView + QTreeView +
widgets/autoexpandingtreeview.h
+
+
+ + +
diff --git a/src/deezer/deezerservice.cpp b/src/deezer/deezerservice.cpp new file mode 100644 index 000000000..558592c3f --- /dev/null +++ b/src/deezer/deezerservice.cpp @@ -0,0 +1,823 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#include "config.h" + +#ifdef HAVE_DZMEDIA +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/application.h" +#include "core/player.h" +#include "core/closure.h" +#include "core/logging.h" +#include "core/mergedproxymodel.h" +#include "core/network.h" +#include "core/song.h" +#include "core/iconloader.h" +#include "core/taskmanager.h" +#include "core/timeconstants.h" +#include "core/utilities.h" +#include "internet/internetmodel.h" +#include "internet/localredirectserver.h" +#include "deezerservice.h" +#include "deezersearch.h" +#include "deezerurlhandler.h" +#include "settings/deezersettingspage.h" + +const Song::Source DeezerService::kSource = Song::Source_Deezer; +const char *DeezerService::kApiUrl = "https://api.deezer.com"; +const char *DeezerService::kOAuthUrl = "https://connect.deezer.com/oauth/auth.php"; +const char *DeezerService::kOAuthAccessTokenUrl = "https://connect.deezer.com/oauth/access_token.php"; +const char *DeezerService::kOAuthRedirectUrl = "https://oauth.strawbs.net"; +const int DeezerService::kAppID = 303684; +const char *DeezerService::kSecretKey = "06911976010b9ddd7256769adf2b2e56"; + +typedef QPair Param; + +DeezerService::DeezerService(Application *app, InternetModel *parent) + : InternetService(Song::Source_Deezer, "Deezer", "dzmedia", app, parent, parent), + network_(new NetworkAccessManager(this)), + url_handler_(new DeezerUrlHandler(app, this)), +#ifdef HAVE_DZMEDIA + dzmedia_(new DZMedia(this)), +#endif + timer_searchdelay_(new QTimer(this)), + searchdelay_(1500), + albumssearchlimit_(1), + songssearchlimit_(1), + fetchalbums_(false), + preview_(false), + pending_search_id_(0), + next_pending_search_id_(1), + search_id_(0), + albums_requested_(0), + albums_received_(0) + { + + timer_searchdelay_->setSingleShot(true); + connect(timer_searchdelay_, SIGNAL(timeout()), SLOT(StartSearch())); + + connect(this, SIGNAL(Authenticated()), app->player(), SLOT(HandleAuthentication())); + + app->player()->RegisterUrlHandler(url_handler_); + + ReloadSettings(); + LoadAccessToken(); + +#ifdef HAVE_DZMEDIA + connect(dzmedia_, SIGNAL(StreamURLReceived(QUrl, QUrl, DZMedia::FileType)), this, SLOT(GetStreamURLFinished(QUrl, QUrl, DZMedia::FileType))); +#endif + +} + +DeezerService::~DeezerService() {} + +void DeezerService::ShowConfig() { + app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Deezer); +} + +void DeezerService::ReloadSettings() { + + QSettings s; + s.beginGroup(DeezerSettingsPage::kSettingsGroup); + quality_ = s.value("quality", "FLAC").toString(); + searchdelay_ = s.value("searchdelay", 1500).toInt(); + albumssearchlimit_ = s.value("albumssearchlimit", 100).toInt(); + songssearchlimit_ = s.value("songssearchlimit", 100).toInt(); + fetchalbums_ = s.value("fetchalbums", false).toBool(); + coversize_ = s.value("coversize", "cover_big").toString(); + preview_ = s.value("preview", false).toBool(); + s.endGroup(); + +} + +void DeezerService::LoadAccessToken() { + + QSettings s; + s.beginGroup(DeezerSettingsPage::kSettingsGroup); + if (s.contains("access_token") && s.contains("expiry_time")) { + access_token_ = s.value("access_token").toString(); + expiry_time_ = s.value("expiry_time").toDateTime(); + } + s.endGroup(); + +} + +void DeezerService::Logout() { +} + +void DeezerService::StartAuthorisation() { + + LocalRedirectServer *server = new LocalRedirectServer(this); + server->Listen(); + + QUrl url = QUrl(kOAuthUrl); + QUrlQuery url_query; + //url_query.addQueryItem("response_type", "token"); + url_query.addQueryItem("response_type", "code"); + url_query.addQueryItem("app_id", QString::number(kAppID)); + QUrl redirect_url; + QUrlQuery redirect_url_query; + + const QString port = QString::number(server->url().port()); + + redirect_url = QUrl(kOAuthRedirectUrl); + redirect_url_query.addQueryItem("port", port); + redirect_url.setQuery(redirect_url_query); + url_query.addQueryItem("redirect_uri", redirect_url.toString()); + url.setQuery(url_query); + + NewClosure(server, SIGNAL(Finished()), this, &DeezerService::RedirectArrived, server, redirect_url); + QDesktopServices::openUrl(url); + +} + +void DeezerService::RedirectArrived(LocalRedirectServer *server, QUrl url) { + + server->deleteLater(); + QUrl request_url = server->request_url(); + RequestAccessToken(QUrlQuery(request_url).queryItemValue("code").toUtf8()); + +} + +void DeezerService::RequestAccessToken(const QByteArray &code) { + + typedef QPair Arg; + typedef QList ArgList; + + typedef QPair EncodedArg; + typedef QList EncodedArgList; + + ArgList args = ArgList() << Arg("app_id", QString::number(kAppID)) + << Arg("secret", kSecretKey) + << Arg("code", code); + + QUrlQuery url_query; + for (const Arg &arg : args) { + EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second)); + url_query.addQueryItem(encoded_arg.first, encoded_arg.second); + } + + QUrl url(kOAuthAccessTokenUrl); + QNetworkRequest request = QNetworkRequest(url); + QNetworkReply *reply = network_->post(request, url_query.toString(QUrl::FullyEncoded).toUtf8()); + NewClosure(reply, SIGNAL(finished()), this, SLOT(FetchAccessTokenFinished(QNetworkReply*)), reply); + +} + +void DeezerService::FetchAccessTokenFinished(QNetworkReply *reply) { + + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + return; + } + + forever { + QByteArray line = reply->readLine(); + QString str(line); + QStringList args = str.split("&"); + for (QString arg : args) { + QStringList params = arg.split("="); + if (params.count() < 2) continue; + QString param1 = params.first(); + QString param2 = params[1]; + if (param1 == "access_token") access_token_ = param2; + else if (param1 == "expires") SetExpiryTime(param2.toInt()); + } + if (reply->atEnd()) break; + } + + QSettings s; + s.beginGroup(DeezerSettingsPage::kSettingsGroup); + s.setValue("access_token", access_token_); + s.setValue("expiry_time", expiry_time_); + s.endGroup(); + + emit Authenticated(); + emit LoginSuccess(); + +} + +void DeezerService::SetExpiryTime(int expires_in_seconds) { + + // Set the expiry time with two minutes' grace. + expiry_time_ = QDateTime::currentDateTime().addSecs(expires_in_seconds - 120); + qLog(Debug) << "Current oauth access token expires at:" << expiry_time_; + +} + +QNetworkReply *DeezerService::CreateRequest(const QString &ressource_name, const QList ¶ms) { + + typedef QPair Arg; + typedef QList ArgList; + + typedef QPair EncodedArg; + typedef QList EncodedArgList; + + ArgList args = ArgList() << Arg("access_token", access_token_) + << Arg("output", "json") + << params; + + QUrlQuery url_query; + for (const Arg& arg : args) { + EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second)); + url_query.addQueryItem(encoded_arg.first, encoded_arg.second); + } + + QUrl url(kApiUrl + QString("/") + ressource_name); + url.setQuery(url_query); + QNetworkRequest req(url); + QNetworkReply *reply = network_->get(req); + + //qLog(Debug) << "Deezer: Sending request" << url; + + return reply; + +} + +QByteArray DeezerService::GetReplyData(QNetworkReply *reply) { + + QByteArray data; + + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + data = reply->readAll(); + } + else { + if (reply->error() < 200) { + // This is a network error, there is nothing more to do. + QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + Error(failure_reason); + } + else { + // See if there is Json data containing "error" - then use that instead. + data = reply->readAll(); + QJsonParseError error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); + QString failure_reason; + if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QJsonObject json_obj = json_doc.object(); + if (json_obj.contains("error")) { + QJsonValue json_value_error = json_obj["error"]; + if (json_value_error.isObject()) { + QJsonObject json_error = json_value_error.toObject(); + int code = json_error["code"].toInt(); + if (code == 300) access_token_.clear(); + QString message = json_error["message"].toString(); + QString type = json_error["type"].toString(); + failure_reason = QString("%1 (%2)").arg(message).arg(code); + } + else { failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); } + } + else { + failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + } + else { + failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + if (reply->error() == QNetworkReply::ContentAccessDenied || reply->error() == QNetworkReply::ContentOperationNotPermittedError || reply->error() == QNetworkReply::AuthenticationRequiredError) { + // Session is probably expired + Logout(); + Error(failure_reason); + } + else if (reply->error() == QNetworkReply::ContentNotFoundError) { // Ignore this error + Error(failure_reason); + } + else { // Fail + Error(failure_reason); + } + } + return QByteArray(); + } + + return data; + +} + +QJsonObject DeezerService::ExtractJsonObj(QByteArray &data) { + + QJsonParseError error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); + + //qLog(Debug) << json_doc; + + if (error.error != QJsonParseError::NoError) { + Error("Reply from server missing Json data.", data); + return QJsonObject(); + } + + if (json_doc.isNull() || json_doc.isEmpty()) { + Error("Received empty Json document.", json_doc); + return QJsonObject(); + } + + if (!json_doc.isObject()) { + Error("Json document is not an object.", json_doc); + return QJsonObject(); + } + + QJsonObject json_obj = json_doc.object(); + if (json_obj.isEmpty()) { + Error("Received empty Json object.", json_doc); + return QJsonObject(); + } + + //qLog(Debug) << json_obj; + + return json_obj; + +} + +QJsonValue DeezerService::ExtractData(QByteArray &data) { + + QJsonObject json_obj = ExtractJsonObj(data); + if (json_obj.isEmpty()) return QJsonObject(); + + if (json_obj.contains("error")) { + QJsonValue json_value_error = json_obj["error"]; + if (!json_value_error.isObject()) { + Error("Error missing object", json_obj); + return QJsonValue(); + } + QJsonObject json_error = json_value_error.toObject(); + int code = json_error["code"].toInt(); + if (code == 300) access_token_.clear(); + QString message = json_error["message"].toString(); + QString type = json_error["type"].toString(); + Error(QString("%1 (%2)").arg(message).arg(code)); + return QJsonValue(); + } + + if (!json_obj.contains("data") && !json_obj.contains("DATA")) { + Error("Json reply is missing data.", json_obj); + return QJsonValue(); + } + + QJsonValue json_data; + if (json_obj.contains("data")) json_data = json_obj["data"]; + else json_data = json_obj["DATA"]; + + return json_data; + +} + +int DeezerService::Search(const QString &text, DeezerSettingsPage::SearchBy searchby) { + + pending_search_id_ = next_pending_search_id_; + pending_search_text_ = text; + pending_searchby_ = searchby; + + next_pending_search_id_++; + + if (text.isEmpty()) { + timer_searchdelay_->stop(); + return pending_search_id_; + } + timer_searchdelay_->setInterval(searchdelay_); + timer_searchdelay_->start(); + + return pending_search_id_; + +} + +void DeezerService::StartSearch() { + + if (access_token_.isEmpty()) { + emit SearchError(pending_search_id_, "Not authenticated."); + next_pending_search_id_ = 1; + ShowConfig(); + return; + } + ClearSearch(); + search_id_ = pending_search_id_; + search_text_ = pending_search_text_; + + SendSearch(); + +} + +void DeezerService::CancelSearch() { + ClearSearch(); +} + +void DeezerService::ClearSearch() { + search_id_ = 0; + search_text_.clear(); + search_error_.clear(); + albums_requested_ = 0; + albums_received_ = 0; + requests_album_.clear(); + requests_song_.clear(); + songs_.clear(); +} + +void DeezerService::SendSearch() { + + emit UpdateStatus("Searching..."); + + QList parameters; + parameters << Param("q", search_text_); + QString searchparam; + switch (pending_searchby_) { + case DeezerSettingsPage::SearchBy_Songs: + searchparam = "search/track"; + parameters << Param("limit", QString::number(songssearchlimit_)); + break; + case DeezerSettingsPage::SearchBy_Albums: + default: + searchparam = "search/album"; + parameters << Param("limit", QString::number(albumssearchlimit_)); + break; + } + + QNetworkReply *reply = CreateRequest(searchparam, parameters); + NewClosure(reply, SIGNAL(finished()), this, SLOT(SearchFinished(QNetworkReply*, int)), reply, search_id_); + +} + +void DeezerService::SearchFinished(QNetworkReply *reply, int id) { + + reply->deleteLater(); + + if (id != search_id_) return; + + QByteArray data = GetReplyData(reply); + if (data.isEmpty()) { + CheckFinish(); + return; + } + + QJsonValue json_value = ExtractData(data); + if (!json_value.isArray()) { + CheckFinish(); + return; + } + + QJsonArray json_data = json_value.toArray(); + if (json_data.isEmpty()) { + Error("No match."); + CheckFinish(); + return; + } + + //qLog(Debug) << json_data; + + for (const QJsonValue &value : json_data) { + //qLog(Debug) << value; + if (!value.isObject()) { + Error("Invalid Json reply, data is not an object.", value); + continue; + } + QJsonObject json_obj = value.toObject(); + //qLog(Debug) << json_obj; + + if (!json_obj.contains("id") || !json_obj.contains("type")) { + Error("Invalid Json reply, item is missing ID or type.", json_obj); + continue; + } + + //int id = json_obj["id"].toInt(); + QString type = json_obj["type"].toString(); + + if (!json_obj.contains("artist")) { + Error("Invalid Json reply, item missing artist.", json_obj); + continue; + } + QJsonValue json_value_artist = json_obj["artist"]; + if (!json_value_artist.isObject()) { + Error("Invalid Json reply, item artist is not a object.", json_value_artist); + continue; + } + QJsonObject json_artist = json_value_artist.toObject(); + + if (!json_artist.contains("name")) { + Error("Invalid Json reply, artist data missing name.", json_artist); + continue; + } + QString artist = json_artist["name"].toString(); + int album_id(0); + QString album; + QString cover; + + if (type == "album") { + album_id = json_obj["id"].toInt(); + album = json_obj["title"].toString(); + cover = json_obj[coversize_].toString(); + } + else if (type == "track") { + + if (!json_obj.contains("album")) { + Error("Invalid Json reply, missing album data.", json_obj); + continue; + } + QJsonValue json_value_album = json_obj["album"]; + if (!json_value_album.isObject()) { + Error("Invalid Json reply, album data is not an object.", json_value_album); + continue; + } + QJsonObject json_album = json_value_album.toObject(); + if (!json_album.contains("id") || !json_album.contains("title")) { + Error("Invalid Json reply, album data is missing ID or title.", json_album); + continue; + } + album_id = json_album["id"].toInt(); + album = json_album["title"].toString(); + cover = json_album[coversize_].toString(); + if (!fetchalbums_) { + Song song = ParseSong(album_id, album, cover, value); + songs_ << song; + continue; + } + } + + DeezerAlbumContext *album_ctx; + if (requests_album_.contains(album_id)) { + album_ctx = requests_album_.value(album_id); + album_ctx->search_id = search_id_; + continue; + } + album_ctx = CreateAlbum(album_id, artist, album, cover); + GetAlbum(album_ctx); + albums_requested_++; + if (albums_requested_ >= albumssearchlimit_) break; + + } + + if (albums_requested_ > 0) { + emit UpdateStatus(QString("Retriving %1 album%2...").arg(albums_requested_).arg(albums_requested_ == 1 ? "" : "s")); + emit ProgressSetMaximum(albums_requested_); + emit UpdateProgress(0); + } + + CheckFinish(); + +} + +DeezerAlbumContext *DeezerService::CreateAlbum(const int album_id, const QString &artist, const QString &album, const QString &cover) { + + DeezerAlbumContext *album_ctx = new DeezerAlbumContext; + album_ctx->id = album_id; + album_ctx->artist = artist; + album_ctx->album = album; + album_ctx->cover = cover; + album_ctx->cover_url.setUrl(cover); + requests_album_.insert(album_id, album_ctx); + + return album_ctx; + + } + +void DeezerService::GetAlbum(const DeezerAlbumContext *album_ctx) { + + QList parameters; + QNetworkReply *reply = CreateRequest(QString("album/%1/tracks").arg(album_ctx->id), parameters); + NewClosure(reply, SIGNAL(finished()), this, SLOT(GetAlbumFinished(QNetworkReply*, int, int)), reply, search_id_, album_ctx->id); + +} + +void DeezerService::GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id) { + + reply->deleteLater(); + + if (!requests_album_.contains(album_id)) { + qLog(Error) << "Deezer: Got reply for cancelled album request: " << album_id; + CheckFinish(); + return; + } + DeezerAlbumContext *album_ctx = requests_album_.value(album_id); + + if (search_id != search_id_) { + if (album_ctx->search_id == search_id) delete requests_album_.take(album_ctx->id); + return; + } + + albums_received_++; + emit UpdateProgress(albums_received_); + + QByteArray data = GetReplyData(reply); + if (data.isEmpty()) { + CheckFinish(); + return; + } + + QJsonValue json_value = ExtractData(data); + if (!json_value.isArray()) { + delete requests_album_.take(album_ctx->id); + CheckFinish(); + return; + } + + QJsonArray json_data = json_value.toArray(); + if (json_data.isEmpty()) { + delete requests_album_.take(album_ctx->id); + CheckFinish(); + return; + } + + bool compilation = false; + bool multidisc = false; + Song first_song; + SongList songs; + for (const QJsonValue &value : json_data) { + Song song = ParseSong(album_ctx->id, album_ctx->album, album_ctx->cover, value); + if (!song.is_valid()) continue; + if (song.disc() >= 2) multidisc = true; + if (song.is_compilation() || (first_song.is_valid() && song.artist() != first_song.artist())) compilation = true; + if (!first_song.is_valid()) first_song = song; + songs << song; + } + for (Song &song : songs) { + if (compilation) song.set_compilation_detected(true); + if (multidisc) { + QString album_full(QString("%1 - (Disc %2)").arg(song.album()).arg(song.disc())); + song.set_album(album_full); + } + songs_ << song; + } + + delete requests_album_.take(album_ctx->id); + CheckFinish(); + +} + +Song DeezerService::ParseSong(const int album_id, const QString &album, const QString &album_cover, const QJsonValue &value) { + + if (!value.isObject()) { + Error("Invalid Json reply, track is not an object.", value); + return Song(); + } + QJsonObject json_obj = value.toObject(); + + //qLog(Debug) << json_obj; + + if ( + !json_obj.contains("id") || + !json_obj.contains("title") || + !json_obj.contains("artist") || + !json_obj.contains("duration") || + !json_obj.contains("preview") + ) { + Error("Invalid Json reply, track is missing one or more values.", json_obj); + return Song(); + } + + int song_id = json_obj["id"].toInt(); + QString title = json_obj["title"].toString(); + QJsonValue json_value_artist = json_obj["artist"]; + QVariant q_duration = json_obj["duration"].toVariant(); + int track(0); + if (json_obj.contains("track_position")) track = json_obj["track_position"].toInt(); + int disc(0); + if (json_obj.contains("disk_number")) disc = json_obj["disk_number"].toInt(); + QString preview = json_obj["preview"].toString(); + + if (!json_value_artist.isObject()) { + Error("Invalid Json reply, track artist is not an object.", json_value_artist); + return Song(); + } + QJsonObject json_artist = json_value_artist.toObject(); + if (!json_artist.contains("name")) { + Error("Invalid Json reply, track artist is missing name.", json_artist); + return Song(); + } + QString artist = json_artist["name"].toString(); + + Song song; + song.set_source(Song::Source_Deezer); + song.set_id(song_id); + song.set_album_id(album_id); + song.set_artist(artist); + song.set_album(album); + song.set_title(title); + song.set_disc(disc); + song.set_track(track); + song.set_art_automatic(album_cover); + + QUrl url; + if (preview_) { + url.setUrl(preview); + quint64 duration = (30 * kNsecPerSec); + song.set_length_nanosec(duration); + } + else { + url.setScheme(url_handler_->scheme()); + url.setPath(QString("track/%1").arg(QString::number(song_id))); + if (q_duration.isValid()) { + quint64 duration = q_duration.toULongLong() * kNsecPerSec; + song.set_length_nanosec(duration); + } + } + song.set_url(url); + + song.set_valid(true); + + return song; + +} + +void DeezerService::GetStreamURL(const QUrl &original_url) { + +#ifdef HAVE_DZMEDIA + stream_request_url_ = original_url; + dzmedia_->GetStreamURL(original_url); +#else + stream_request_url_ = QUrl(); + emit StreamURLReceived(original_url, original_url, Song::FileType_Stream); +#endif + +} + +#ifdef HAVE_DZMEDIA +void DeezerService::GetStreamURLFinished(const QUrl original_url, const QUrl media_url, const DZMedia::FileType dzmedia_filetype) { + + Song::FileType filetype(Song::FileType_Unknown); + + switch (dzmedia_filetype) { + case DZMedia::FileType_FLAC: + filetype = Song::FileType_FLAC; + break; + case DZMedia::FileType_MPEG: + filetype = Song::FileType_MPEG; + break; + case DZMedia::FileType_Stream: + filetype = Song::FileType_Stream; + break; + default: + filetype = Song::FileType_Unknown; + break; + } + stream_request_url_ = QUrl(); + emit StreamURLReceived(original_url, media_url, filetype); + +} +#endif + +void DeezerService::CheckFinish() { + + if (search_id_ == 0) return; + + if (albums_requested_ <= albums_received_) { + if (songs_.isEmpty()) { + if (search_error_.isEmpty()) emit SearchError(search_id_, "Unknown error"); + else emit SearchError(search_id_, search_error_); + } + else emit SearchResults(search_id_, songs_); + ClearSearch(); + } + +} + +void DeezerService::Error(QString error, QVariant debug) { + qLog(Error) << "Deezer:" << error; + if (!debug.isValid()) qLog(Debug) << debug; + if (search_id_ != 0) { + if (!error.isEmpty()) { + search_error_ += error; + search_error_ += "
"; + } + CheckFinish(); + } + if (!stream_request_url_.isEmpty()) { + emit StreamURLReceived(stream_request_url_, stream_request_url_, Song::FileType_Stream); + stream_request_url_ = QUrl(); + } +} diff --git a/src/deezer/deezerservice.h b/src/deezer/deezerservice.h new file mode 100644 index 000000000..10e047db0 --- /dev/null +++ b/src/deezer/deezerservice.h @@ -0,0 +1,163 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#ifndef DEEZERSERVICE_H +#define DEEZERSERVICE_H + +#include "config.h" + +#ifdef HAVE_DZMEDIA +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/song.h" +#include "internet/internetmodel.h" +#include "internet/internetservice.h" +#include "settings/deezersettingspage.h" + +class NetworkAccessManager; +class LocalRedirectServer; +class DeezerUrlHandler; + +struct DeezerAlbumContext { + int id; + int search_id; + QString artist; + QString album; + QString cover; + QUrl cover_url; +}; +Q_DECLARE_METATYPE(DeezerAlbumContext); + +class DeezerService : public InternetService { + Q_OBJECT + + public: + DeezerService(Application *app, InternetModel *parent); + ~DeezerService(); + + static const Song::Source kSource; + static const int kAppID; + + void ReloadSettings(); + + void Logout(); + int Search(const QString &query, DeezerSettingsPage::SearchBy searchby); + void CancelSearch(); + + const bool app_id() { return kAppID; } + const bool authenticated() { return !access_token_.isEmpty(); } + + void GetStreamURL(const QUrl &url); + + signals: + void Login(); + void LoginSuccess(); + void LoginFailure(QString failure_reason); + void Authenticated(); + void SearchResults(int id, SongList songs); + void SearchError(int id, QString message); + void UpdateStatus(QString text); + void ProgressSetMaximum(int max); + void UpdateProgress(int max); + void StreamURLReceived(const QUrl original_url, const QUrl media_url, const Song::FileType filetype); + + public slots: + void ShowConfig(); + + private slots: + void StartAuthorisation(); + void FetchAccessTokenFinished(QNetworkReply *reply); + void StartSearch(); + void SearchFinished(QNetworkReply *reply, int search_id); + void GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id); +#ifdef HAVE_DZMEDIA + void GetStreamURLFinished(const QUrl original_url, const QUrl media_url, const DZMedia::FileType dzmedia_filetype); +#endif + + private: + void LoadAccessToken(); + void RedirectArrived(LocalRedirectServer *server, QUrl url); + void RequestAccessToken(const QByteArray &code); + void SetExpiryTime(int expires_in_seconds); + void ClearSearch(); + QNetworkReply *CreateRequest(const QString &ressource_name, const QList> ¶ms); + QByteArray GetReplyData(QNetworkReply *reply); + QJsonObject ExtractJsonObj(QByteArray &data); + QJsonValue ExtractData(QByteArray &data); + void SendSearch(); + DeezerAlbumContext *CreateAlbum(const int album_id, const QString &artist, const QString &album, const QString &cover); + void GetAlbum(const DeezerAlbumContext *album_ctx); + Song ParseSong(const int album_id, const QString &album, const QString &album_cover, const QJsonValue &value); + void CheckFinish(); + void Error(QString error, QVariant debug = QString()); + + static const char *kApiUrl; + static const char *kOAuthUrl; + static const char *kOAuthAccessTokenUrl; + static const char *kOAuthRedirectUrl; + static const char *kSecretKey; + + NetworkAccessManager *network_; + DeezerUrlHandler *url_handler_; +#ifdef HAVE_DZMEDIA + DZMedia *dzmedia_; +#endif + QTimer *timer_searchdelay_; + + QString quality_; + int searchdelay_; + int albumssearchlimit_; + int songssearchlimit_; + bool fetchalbums_; + QString coversize_; + bool preview_; + QString access_token_; + QDateTime expiry_time_; + + int pending_search_id_; + int next_pending_search_id_; + QString pending_search_text_; + DeezerSettingsPage::SearchBy pending_searchby_; + + int search_id_; + QString search_text_; + QHash requests_album_; + QHash requests_song_; + int albums_requested_; + int albums_received_; + SongList songs_; + QString search_error_; + QUrl stream_request_url_; + +}; + +#endif // DEEZERSERVICE_H diff --git a/src/deezer/deezerurlhandler.cpp b/src/deezer/deezerurlhandler.cpp new file mode 100644 index 000000000..5dd2d53e1 --- /dev/null +++ b/src/deezer/deezerurlhandler.cpp @@ -0,0 +1,65 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#include +#include +#include + +#include "core/application.h" +#include "core/taskmanager.h" +#include "core/iconloader.h" +#include "core/logging.h" +#include "core/song.h" +#include "deezer/deezerservice.h" +#include "deezerurlhandler.h" + +DeezerUrlHandler::DeezerUrlHandler( + Application *app, DeezerService *service) + : UrlHandler(service), app_(app), service_(service), task_id_(-1) { + + connect(service, SIGNAL(StreamURLReceived(QUrl, QUrl, Song::FileType)), this, SLOT(GetStreamURLFinished(QUrl, QUrl, Song::FileType))); + +} + +UrlHandler::LoadResult DeezerUrlHandler::StartLoading(const QUrl &url) { + + LoadResult ret(url); + if (task_id_ != -1) return ret; + last_original_url_ = url; + task_id_ = app_->task_manager()->StartTask(QString("Loading %1 stream...").arg(url.scheme())); + service_->GetStreamURL(url); + ret.type_ = LoadResult::WillLoadAsynchronously; + return ret; + +} + +void DeezerUrlHandler::GetStreamURLFinished(QUrl original_url, QUrl media_url, Song::FileType filetype) { + + if (task_id_ == -1) return; + CancelTask(); + emit AsyncLoadComplete(LoadResult(original_url, LoadResult::TrackAvailable, media_url, filetype)); + +} + +void DeezerUrlHandler::CancelTask() { + + app_->task_manager()->SetTaskFinished(task_id_); + task_id_ = -1; + +} diff --git a/src/deezer/deezerurlhandler.h b/src/deezer/deezerurlhandler.h new file mode 100644 index 000000000..e17f9dc92 --- /dev/null +++ b/src/deezer/deezerurlhandler.h @@ -0,0 +1,56 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#ifndef DEEZERURLHANDLER_H +#define DEEZERURLHANDLER_H + +#include +#include +#include + +#include "core/urlhandler.h" +#include "core/song.h" +#include "deezer/deezerservice.h" + +class Application; +class DeezerService; + +class DeezerUrlHandler : public UrlHandler { + Q_OBJECT + + public: + DeezerUrlHandler(Application *app, DeezerService *service); + + QString scheme() const { return service_->url_scheme(); } + LoadResult StartLoading(const QUrl &url); + + void CancelTask(); + + private slots: + void GetStreamURLFinished(QUrl original_url, QUrl media_url, Song::FileType filetype); + + private: + Application *app_; + DeezerService *service_; + int task_id_; + QUrl last_original_url_; + +}; + +#endif diff --git a/src/device/deviceproperties.ui b/src/device/deviceproperties.ui index 98d9ad5a1..72a128b58 100644 --- a/src/device/deviceproperties.ui +++ b/src/device/deviceproperties.ui @@ -14,7 +14,7 @@ Device Properties - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -453,6 +453,7 @@ + diff --git a/src/dialogs/about.ui b/src/dialogs/about.ui index 372176f49..158b678d4 100644 --- a/src/dialogs/about.ui +++ b/src/dialogs/about.ui @@ -12,7 +12,7 @@ Qt::StrongFocus - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -32,7 +32,7 @@ - :/icons/64x64/strawberry.png + :/icons/64x64/strawberry.png false @@ -145,6 +145,7 @@ + diff --git a/src/dialogs/edittagdialog.ui b/src/dialogs/edittagdialog.ui index 171a81338..b382f7729 100644 --- a/src/dialogs/edittagdialog.ui +++ b/src/dialogs/edittagdialog.ui @@ -14,7 +14,7 @@ Edit track information - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -861,6 +861,7 @@ + diff --git a/src/dialogs/organisedialog.ui b/src/dialogs/organisedialog.ui index 68045fc36..d090395ff 100644 --- a/src/dialogs/organisedialog.ui +++ b/src/dialogs/organisedialog.ui @@ -14,7 +14,7 @@ Organise Files - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -271,6 +271,7 @@ + diff --git a/src/dialogs/trackselectiondialog.ui b/src/dialogs/trackselectiondialog.ui index acb1adede..49504cc44 100644 --- a/src/dialogs/trackselectiondialog.ui +++ b/src/dialogs/trackselectiondialog.ui @@ -14,7 +14,7 @@ Tag fetcher - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -261,6 +261,7 @@ + diff --git a/src/engine/deezerengine.cpp b/src/engine/deezerengine.cpp new file mode 100644 index 000000000..db1941f0f --- /dev/null +++ b/src/engine/deezerengine.cpp @@ -0,0 +1,487 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "core/timeconstants.h" +#include "core/taskmanager.h" +#include "core/logging.h" +#include "engine_fwd.h" +#include "enginebase.h" +#include "enginetype.h" +#include "deezerengine.h" +#include "deezer/deezerservice.h" +#include "settings/deezersettingspage.h" + +DeezerEngine::DeezerEngine(TaskManager *task_manager) + : EngineBase(), + state_(Engine::Empty), + position_(0) { + + type_ = Engine::Deezer; + ReloadSettings(); + +} + +DeezerEngine::~DeezerEngine() { + + if (player_) { + dz_object_release((dz_object_handle) player_); + player_ = nullptr; + } + + if (connect_) { + dz_object_release((dz_object_handle) connect_); + connect_ = nullptr; + } + +} + +bool DeezerEngine::Init() { + + qLog(Debug) << "Deezer native SDK Version:" << dz_connect_get_build_id(); + + struct dz_connect_configuration config; + memset(&config, 0, sizeof(struct dz_connect_configuration)); + config.app_id = QString::number(DeezerService::kAppID).toUtf8(); + config.product_id = QCoreApplication::applicationName().toUtf8(); + config.product_build_id = QCoreApplication::applicationVersion().toUtf8().constData(); + config.user_profile_path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation).toUtf8().constData(); + config.connect_event_cb = ConnectEventCallback; + + connect_ = dz_connect_new(&config); + if (!connect_) { + qLog(Error) << "Deezer: Failed to create connect."; + return false; + } + + qLog(Debug) << "Device ID:" << dz_connect_get_device_id(connect_); + + dz_error_t dzerr(DZ_ERROR_NO_ERROR); + + dzerr = dz_connect_debug_log_disable(connect_); + if (dzerr != DZ_ERROR_NO_ERROR) { + qLog(Error) << "Deezer: Failed to disable debug log."; + return false; + } + + dzerr = dz_connect_activate(connect_, this); + if (dzerr != DZ_ERROR_NO_ERROR) { + qLog(Error) << "Deezer: Failed to activate connect."; + return false; + } + + dz_connect_cache_path_set(connect_, nullptr, nullptr, QStandardPaths::writableLocation(QStandardPaths::CacheLocation).toUtf8().constData()); + + player_ = dz_player_new(connect_); + if (!player_) { + qLog(Error) << "Deezer: Failed to create player."; + return false; + } + + dzerr = dz_player_activate(player_, this); + if (dzerr != DZ_ERROR_NO_ERROR) { + qLog(Error) << "Deezer: Failed to activate player."; + return false; + } + + dzerr = dz_player_set_event_cb(player_, PlayerEventCallback); + if (dzerr != DZ_ERROR_NO_ERROR) { + qLog(Error) << "Deezer: Failed to set event callback."; + return false; + } + + dzerr = dz_player_set_metadata_cb(player_, PlayerMetaDataCallback); + if (dzerr != DZ_ERROR_NO_ERROR) { + qLog(Error) << "Deezer: Failed to set metadata callback."; + return false; + } + + dzerr = dz_player_set_render_progress_cb(player_, PlayerProgressCallback, 1000); + if (dzerr != DZ_ERROR_NO_ERROR) { + qLog(Error) << "Deezer: Failed to set progress callback."; + return false; + } + + dzerr = dz_player_set_crossfading_duration(player_, nullptr, nullptr, 3000); + if (dzerr != DZ_ERROR_NO_ERROR) { + qLog(Error) << "Deezer: Failed to set crossfade duration."; + return false; + } + + dzerr = dz_connect_offline_mode(connect_, nullptr, nullptr, false); + if (dzerr != DZ_ERROR_NO_ERROR) { + qLog(Error) << "Deezer: Failed to set offline mode."; + return false; + } + + LoadAccessToken(); + + return true; + +} + +bool DeezerEngine::Initialised() const { + + if (connect_ && player_) return true; + return false; + +} + +void DeezerEngine::LoadAccessToken() { + + QSettings s; + s.beginGroup(DeezerSettingsPage::kSettingsGroup); + if (!s.contains("access_token") || !s.contains("expiry_time")) return; + access_token_ = s.value("access_token").toString(); + expiry_time_ = s.value("expiry_time").toDateTime(); + s.endGroup(); + + dz_error_t dzerr = dz_connect_set_access_token(connect_, nullptr, nullptr, access_token_.toUtf8().constData()); + if (dzerr != DZ_ERROR_NO_ERROR) { + qLog(Error) << "Deezer: Failed to set access token."; + } + +} + +bool DeezerEngine::Load(const QUrl &media_url, const QUrl &original_url, Engine::TrackChangeFlags change, bool force_stop_at_end, quint64 beginning_nanosec, qint64 end_nanosec) { + + if (!Initialised()) return false; + + Engine::Base::Load(media_url, original_url, change, force_stop_at_end, beginning_nanosec, end_nanosec); + dz_error_t dzerr = dz_player_load(player_, nullptr, nullptr, media_url.toString().toUtf8().constData()); + if (dzerr != DZ_ERROR_NO_ERROR) return false; + + return true; + +} + +bool DeezerEngine::Play(quint64 offset_nanosec) { + + if (!Initialised()) return false; + + dz_error_t dzerr(DZ_ERROR_NO_ERROR); + if (state() == Engine::Paused) dzerr = dz_player_resume(player_, nullptr, nullptr); + else dzerr = dz_player_play(player_, nullptr, nullptr, DZ_PLAYER_PLAY_CMD_START_TRACKLIST, DZ_INDEX_IN_QUEUELIST_CURRENT); + if (dzerr != DZ_ERROR_NO_ERROR) return false; + + Seek(offset_nanosec); + + return true; + +} + +void DeezerEngine::Stop(bool stop_after) { + + if (!Initialised()) return; + + dz_error_t dzerr = dz_player_stop(player_, nullptr, nullptr); + if (dzerr != DZ_ERROR_NO_ERROR) return; + + state_ = Engine::Empty; + emit TrackEnded(); + +} + +void DeezerEngine::Pause() { + + if (!Initialised()) return; + + dz_error_t dzerr = dz_player_pause(player_, nullptr, nullptr); + if (dzerr != DZ_ERROR_NO_ERROR) return; + +} + +void DeezerEngine::Unpause() { + + if (!Initialised()) return; + dz_error_t dzerr = dz_player_resume(player_, nullptr, nullptr); + if (dzerr != DZ_ERROR_NO_ERROR) return; + +} + +void DeezerEngine::Seek(quint64 offset_nanosec) { + + if (!Initialised()) return; + + int offset = (offset_nanosec / kNsecPerMsec); + + uint len = (length_nanosec() / kNsecPerMsec); + if (len == 0) return; + + float pos = float(offset) / len; + + dz_error_t dzerr = dz_player_seek(player_, nullptr, nullptr, pos); + if (dzerr != DZ_ERROR_NO_ERROR) return; + +} + +void DeezerEngine::SetVolumeSW(uint percent) { + + if (!Initialised()) return; + + dz_error_t dzerr = dz_player_set_output_volume(player_, nullptr, nullptr, percent); + if (dzerr != DZ_ERROR_NO_ERROR) qLog(Error) << "Deezer: Failed to set volume."; + +} + +qint64 DeezerEngine::position_nanosec() const { + + if (state() == Engine::Empty) return 0; + const qint64 result = (position_ * kNsecPerUsec); + return qint64(qMax(0ll, result)); + +} + +qint64 DeezerEngine::length_nanosec() const { + + if (state() == Engine::Empty) return 0; + + const qint64 result = (end_nanosec_ - beginning_nanosec_); + return result; + +} + +EngineBase::OutputDetailsList DeezerEngine::GetOutputsList() const { + OutputDetailsList ret; + OutputDetails output; + output.name = "default"; + output.description = "Default"; + output.iconname = "soundcard"; + ret << output; + return ret; +} + +bool DeezerEngine::ValidOutput(const QString &output) { + return(true); +} + +bool DeezerEngine::CustomDeviceSupport(const QString &output) { + return false; +} + +bool DeezerEngine::ALSADeviceSupport(const QString &output) { + return false; +} + +bool DeezerEngine::CanDecode(const QUrl &url) { + if (url.scheme() == "dzmedia") return true; + else return false; +} + +void DeezerEngine::ConnectEventCallback(dz_connect_handle handle, dz_connect_event_handle event, void *delegate) { + + dz_connect_event_t type = dz_connect_event_get_type(event); + //DeezerEngine *engine = reinterpret_cast(delegate); + + switch (type) { + case DZ_CONNECT_EVENT_USER_OFFLINE_AVAILABLE: + qLog(Debug) << "CONNECT_EVENT USER_OFFLINE_AVAILABLE"; + break; + + case DZ_CONNECT_EVENT_USER_ACCESS_TOKEN_OK: { + const char* szAccessToken; + szAccessToken = dz_connect_event_get_access_token(event); + qLog(Debug) << "CONNECT_EVENT USER_ACCESS_TOKEN_OK Access_token :" << szAccessToken; + } + break; + + case DZ_CONNECT_EVENT_USER_ACCESS_TOKEN_FAILED: + qLog(Debug) << "CONNECT_EVENT USER_ACCESS_TOKEN_FAILED"; + break; + + case DZ_CONNECT_EVENT_USER_LOGIN_OK: + qLog(Debug) << "Deezer CONNECT_EVENT USER_LOGIN_OK"; + break; + + case DZ_CONNECT_EVENT_USER_NEW_OPTIONS: + qLog(Debug) << "Deezer: CONNECT_EVENT USER_NEW_OPTIONS"; + break; + + case DZ_CONNECT_EVENT_USER_LOGIN_FAIL_NETWORK_ERROR: + qLog(Debug) << "Deezer: CONNECT_EVENT USER_LOGIN_FAIL_NETWORK_ERROR"; + break; + + case DZ_CONNECT_EVENT_USER_LOGIN_FAIL_BAD_CREDENTIALS: + qLog(Debug) << "Deezer: CONNECT_EVENT USER_LOGIN_FAIL_BAD_CREDENTIALS"; + break; + + case DZ_CONNECT_EVENT_USER_LOGIN_FAIL_USER_INFO: + qLog(Debug) << "Deezer: CONNECT_EVENT USER_LOGIN_FAIL_USER_INFO"; + break; + + case DZ_CONNECT_EVENT_USER_LOGIN_FAIL_OFFLINE_MODE: + qLog(Debug) << "Deezer: CONNECT_EVENT USER_LOGIN_FAIL_OFFLINE_MODE"; + break; + + case DZ_CONNECT_EVENT_ADVERTISEMENT_START: + qLog(Debug) << "Deezer: CONNECT_EVENTADVERTISEMENT_START"; + break; + + case DZ_CONNECT_EVENT_ADVERTISEMENT_STOP: + qLog(Debug) << "Deezer: CONNECT_EVENTADVERTISEMENT_STOP"; + break; + + case DZ_CONNECT_EVENT_UNKNOWN: + default: + qLog(Debug) << "Deezer: CONNECT_EVENTUNKNOWN or default (type =" << type; + break; + } + +} + + +void DeezerEngine::PlayerEventCallback(dz_player_handle handle, dz_player_event_handle event, void *supervisor) { + + DeezerEngine *engine = reinterpret_cast(supervisor); + dz_streaming_mode_t streaming_mode; + dz_index_in_queuelist idx; + dz_player_event_t type = dz_player_event_get_type(event); + + if (!dz_player_event_get_queuelist_context(event, &streaming_mode, &idx)) { + streaming_mode = DZ_STREAMING_MODE_ONDEMAND; + idx = DZ_INDEX_IN_QUEUELIST_INVALID; + } + + switch (type) { + + case DZ_PLAYER_EVENT_LIMITATION_FORCED_PAUSE: + qLog(Debug) << "Deezer: PLAYER_EVENT_LIMITATION_FORCED_PAUSE"; + break; + + case DZ_PLAYER_EVENT_QUEUELIST_LOADED: + qLog(Debug) << "Deezer: PLAYER_EVENT_QUEUELIST_LOADED"; + break; + + case DZ_PLAYER_EVENT_QUEUELIST_NO_RIGHT: + qLog(Debug) << "Deezer: PLAYER_EVENT_QUEUELIST_NO_RIGHT"; + break; + + case DZ_PLAYER_EVENT_QUEUELIST_NEED_NATURAL_NEXT: + qLog(Debug) << "Deezer: PLAYER_EVENT_QUEUELIST_NEED_NATURAL_NEXT"; + break; + + case DZ_PLAYER_EVENT_QUEUELIST_TRACK_NOT_AVAILABLE_OFFLINE: + qLog(Debug) << "Deezer: PLAYER_EVENT_QUEUELIST_TRACK_NOT_AVAILABLE_OFFLINE"; + engine->state_ = Engine::Error; + emit engine->StateChanged(engine->state_); + emit engine->Error("Track not available offline."); + break; + + case DZ_PLAYER_EVENT_QUEUELIST_TRACK_RIGHTS_AFTER_AUDIOADS: + qLog(Debug) << "Deezer: PLAYER_EVENT_QUEUELIST_TRACK_RIGHTS_AFTER_AUDIOADS"; + break; + + case DZ_PLAYER_EVENT_QUEUELIST_SKIP_NO_RIGHT: + qLog(Debug) << "Deezer: PLAYER_EVENT_QUEUELIST_SKIP_NO_RIGHT"; + break; + + case DZ_PLAYER_EVENT_QUEUELIST_TRACK_SELECTED: + break; + + case DZ_PLAYER_EVENT_MEDIASTREAM_DATA_READY: + qLog(Debug) << "Deezer: PLAYER_EVENT_MEDIASTREAM_DATA_READY"; + break; + + case DZ_PLAYER_EVENT_MEDIASTREAM_DATA_READY_AFTER_SEEK: + qLog(Debug) << "Deezer: PLAYER_EVENT_MEDIASTREAM_DATA_READY_AFTER_SEEK"; + break; + + case DZ_PLAYER_EVENT_RENDER_TRACK_START_FAILURE: + qLog(Debug) << "Deezer: PLAYER_EVENT_RENDER_TRACK_START_FAILURE"; + engine->state_ = Engine::Error; + emit engine->StateChanged(engine->state_); + emit engine->Error("Track start failure."); + break; + + case DZ_PLAYER_EVENT_RENDER_TRACK_START: + qLog(Debug) << "Deezer: PLAYER_EVENT_RENDER_TRACK_START"; + engine->state_ = Engine::Playing; + engine->position_ = 0; + emit engine->StateChanged(engine->state_); + break; + + case DZ_PLAYER_EVENT_RENDER_TRACK_END: + qLog(Debug) << "Deezer: PLAYER_EVENT_RENDER_TRACK_END"; + engine->state_ = Engine::Idle; + engine->position_ = 0; + emit engine->TrackEnded(); + break; + + case DZ_PLAYER_EVENT_RENDER_TRACK_PAUSED: + qLog(Debug) << "Deezer: PLAYER_EVENT_RENDER_TRACK_PAUSED"; + engine->state_ = Engine::Paused; + emit engine->StateChanged(engine->state_); + break; + + case DZ_PLAYER_EVENT_RENDER_TRACK_UNDERFLOW: + qLog(Debug) << "Deezer: PLAYER_EVENT_RENDER_TRACK_UNDERFLOW"; + break; + + case DZ_PLAYER_EVENT_RENDER_TRACK_RESUMED: + qLog(Debug) << "Deezer: PLAYER_EVENT_RENDER_TRACK_RESUMED"; + engine->state_ = Engine::Playing; + emit engine->StateChanged(engine->state_); + break; + + case DZ_PLAYER_EVENT_RENDER_TRACK_SEEKING: + qLog(Debug) << "Deezer: PLAYER_EVENT_RENDER_TRACK_SEEKING"; + break; + + case DZ_PLAYER_EVENT_RENDER_TRACK_REMOVED: + qLog(Debug) << "Deezer: PLAYER_EVENT_RENDER_TRACK_REMOVED"; + engine->state_ = Engine::Empty; + engine->position_ = 0; + emit engine->TrackEnded(); + break; + + case DZ_PLAYER_EVENT_UNKNOWN: + default: + qLog(Error) << "Deezer: Unknown player event" << type; + break; + } + //emit engine->StateChanged(engine->state_); + +} + +void DeezerEngine::PlayerProgressCallback(dz_player_handle handle, dz_useconds_t progress, void *userdata) { + DeezerEngine *engine = reinterpret_cast(userdata); + engine->position_ = progress; +} + +void DeezerEngine::PlayerMetaDataCallback(dz_player_handle handle, dz_track_metadata_handle metadata, void *userdata) { + //DeezerEngine *engine = reinterpret_cast(userdata); +} diff --git a/src/engine/deezerengine.h b/src/engine/deezerengine.h new file mode 100644 index 000000000..3d425ff0c --- /dev/null +++ b/src/engine/deezerengine.h @@ -0,0 +1,88 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#ifndef DEEZERENGINE_H +#define DEEZERENGINE_H + +#include "config.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "engine_fwd.h" +#include "enginebase.h" + +class TaskManager; + +class DeezerEngine : public Engine::Base { + Q_OBJECT + + public: + DeezerEngine(TaskManager *task_manager); + ~DeezerEngine(); + + bool Init(); + Engine::State state() const { return state_; } + bool Load(const QUrl &media_url, const QUrl &original_url, Engine::TrackChangeFlags change, bool force_stop_at_end, quint64 beginning_nanosec, qint64 end_nanosec); + bool Play(quint64 offset_nanosec); + void Stop(bool stop_after = false); + void Pause(); + void Unpause(); + void Seek(quint64 offset_nanosec); + protected: + void SetVolumeSW(uint percent); + public: + virtual qint64 position_nanosec() const; + virtual qint64 length_nanosec() const; + + OutputDetailsList GetOutputsList() const; + bool ValidOutput(const QString &output); + QString DefaultOutput() { return ""; } + bool CustomDeviceSupport(const QString &output); + bool ALSADeviceSupport(const QString &output); + + private: + Engine::State state_; + dz_connect_handle connect_; + dz_player_handle player_; + QString access_token_; + QDateTime expiry_time_; + qint64 position_; + + bool Initialised() const; + bool CanDecode(const QUrl &url); + + static void ConnectEventCallback(dz_connect_handle handle, dz_connect_event_handle event, void *delegate); + static void PlayerEventCallback(dz_player_handle handle, dz_player_event_handle event, void *supervisor); + static void PlayerMetaDataCallback(dz_player_handle handle, dz_track_metadata_handle metadata, void *userdata); + static void PlayerProgressCallback(dz_player_handle handle, dz_useconds_t progress, void *userdata); + + public slots: + void LoadAccessToken(); + +}; + +#endif diff --git a/src/engine/enginebase.h b/src/engine/enginebase.h index ef7aedc1f..be4d9db5e 100644 --- a/src/engine/enginebase.h +++ b/src/engine/enginebase.h @@ -147,8 +147,7 @@ signals: void MetaData(const Engine::SimpleMetaBundle&); // Signals that the engine's state has changed (a stream was stopped for example). - // Always use the state from event, because it's not guaranteed that immediate - // subsequent call to state() won't return a stale value. + // Always use the state from event, because it's not guaranteed that immediate subsequent call to state() won't return a stale value. void StateChanged(Engine::State); protected: diff --git a/src/engine/enginedevice.cpp b/src/engine/enginedevice.cpp index 37af9d31a..fd414fa35 100644 --- a/src/engine/enginedevice.cpp +++ b/src/engine/enginedevice.cpp @@ -1,7 +1,6 @@ /* * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2014, David Sansome + * Copyright 2014, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/engine/enginedevice.h b/src/engine/enginedevice.h index 3cc5312a1..795b265db 100644 --- a/src/engine/enginedevice.h +++ b/src/engine/enginedevice.h @@ -1,7 +1,6 @@ /* * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2014, David Sansome + * Copyright 2014, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/engine/enginetype.cpp b/src/engine/enginetype.cpp index 718a77ba6..4096639f5 100644 --- a/src/engine/enginetype.cpp +++ b/src/engine/enginetype.cpp @@ -28,19 +28,21 @@ namespace Engine { Engine::EngineType EngineTypeFromName(QString enginename) { QString lower = enginename.toLower(); - if (lower == "xine") return Engine::Xine; - else if (lower == "gstreamer") return Engine::GStreamer; - else if (lower == "phonon") return Engine::Phonon; - else if (lower == "vlc") return Engine::VLC; - else return Engine::None; + if (lower == "gstreamer") return Engine::GStreamer; + else if (lower == "xine") return Engine::Xine; + else if (lower == "vlc") return Engine::VLC; + else if (lower == "phonon") return Engine::Phonon; + else if (lower == "deezer") return Engine::Deezer; + else return Engine::None; } QString EngineName(Engine::EngineType enginetype) { switch (enginetype) { - case Engine::Xine: return QString("xine"); case Engine::GStreamer: return QString("gstreamer"); - case Engine::Phonon: return QString("phonon"); + case Engine::Xine: return QString("xine"); case Engine::VLC: return QString("vlc"); + case Engine::Phonon: return QString("phonon"); + case Engine::Deezer: return QString("deezer"); case Engine::None: default: return QString("None"); } @@ -48,10 +50,11 @@ QString EngineName(Engine::EngineType enginetype) { QString EngineDescription(Engine::EngineType enginetype) { switch (enginetype) { - case Engine::Xine: return QString("Xine"); case Engine::GStreamer: return QString("GStreamer"); - case Engine::Phonon: return QString("Phonon"); + case Engine::Xine: return QString("Xine"); case Engine::VLC: return QString("VLC"); + case Engine::Phonon: return QString("Phonon"); + case Engine::Deezer: return QString("Deezer"); case Engine::None: default: return QString("None"); diff --git a/src/engine/enginetype.h b/src/engine/enginetype.h index 71435eea5..4039a4ad9 100644 --- a/src/engine/enginetype.h +++ b/src/engine/enginetype.h @@ -32,7 +32,8 @@ enum EngineType { GStreamer, VLC, Xine, - Phonon + Phonon, + Deezer }; Engine::EngineType EngineTypeFromName(QString enginename); diff --git a/src/equalizer/equalizer.ui b/src/equalizer/equalizer.ui index d7e2a4fe9..f9d57d192 100644 --- a/src/equalizer/equalizer.ui +++ b/src/equalizer/equalizer.ui @@ -14,7 +14,7 @@ Equalizer - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -158,6 +158,7 @@ + diff --git a/src/globalshortcuts/globalshortcutgrabber.ui b/src/globalshortcuts/globalshortcutgrabber.ui index adc634134..7ee8195ae 100644 --- a/src/globalshortcuts/globalshortcutgrabber.ui +++ b/src/globalshortcuts/globalshortcutgrabber.ui @@ -14,7 +14,7 @@ Press a key - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -52,6 +52,7 @@
+ diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp index 7891f4bc1..e54b80ced 100644 --- a/src/internet/internetmodel.cpp +++ b/src/internet/internetmodel.cpp @@ -31,6 +31,7 @@ #include "internetmodel.h" #include "internetservice.h" #include "tidal/tidalservice.h" +#include "deezer/deezerservice.h" QMap* InternetModel::sServices = nullptr; @@ -41,6 +42,7 @@ InternetModel::InternetModel(Application *app, QObject *parent) if (!sServices) sServices = new QMap; Q_ASSERT(sServices->isEmpty()); AddService(new TidalService(app, this)); + AddService(new DeezerService(app, this)); } diff --git a/src/internet/internetmodel.h b/src/internet/internetmodel.h index 0bf045fbb..2cd832580 100644 --- a/src/internet/internetmodel.h +++ b/src/internet/internetmodel.h @@ -106,7 +106,6 @@ class InternetModel : public QStandardItemModel { // Needs to be static for InternetPlaylistItem::restore static InternetService *ServiceBySource(const Song::Source &source); - //static InternetService *ServiceByName(const QString &name); template static T *Service() { diff --git a/src/internet/localredirectserver.cpp b/src/internet/localredirectserver.cpp new file mode 100644 index 000000000..ae1cb8adf --- /dev/null +++ b/src/internet/localredirectserver.cpp @@ -0,0 +1,111 @@ +/* + * This file was part of Clementine. + * Copyright 2012, 2014, John Maguire + * Copyright 2014, Krzysztof Sobiecki + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#include "localredirectserver.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "core/closure.h" + +LocalRedirectServer::LocalRedirectServer(QObject* parent) + : QObject(parent), server_(new QTcpServer(this)) {} + +void LocalRedirectServer::Listen() { + + server_->listen(QHostAddress::LocalHost); + // We have to calculate this and store it now as the server port is cleared once we close the socket. + url_.setScheme("http"); + url_.setHost("localhost"); + url_.setPort(server_->serverPort()); + url_.setPath("/"); + connect(server_, SIGNAL(newConnection()), SLOT(NewConnection())); + +} + +void LocalRedirectServer::NewConnection() { + QTcpSocket* socket = server_->nextPendingConnection(); + server_->close(); + + QByteArray buffer; + NewClosure(socket, SIGNAL(readyRead()), this, SLOT(ReadyRead(QTcpSocket*, QByteArray)), socket, buffer); +} + +void LocalRedirectServer::ReadyRead(QTcpSocket* socket, QByteArray buffer) { + buffer.append(socket->readAll()); + if (socket->atEnd() || buffer.endsWith("\r\n\r\n")) { + WriteTemplate(socket); + socket->deleteLater(); + request_url_ = ParseUrlFromRequest(buffer); + emit Finished(); + } + else { + NewClosure(socket, SIGNAL(readyRead()), this, SLOT(ReadyReady(QTcpSocket*, QByteArray)), socket, buffer); + } +} + +void LocalRedirectServer::WriteTemplate(QTcpSocket* socket) const { + + QFile page_file(":/misc/oauthsuccess.html"); + page_file.open(QIODevice::ReadOnly); + QString page_data = QString::fromUtf8(page_file.readAll()); + + QRegExp tr_regexp("tr\\(\"([^\"]+)\"\\)"); + int offset = 0; + forever { + offset = tr_regexp.indexIn(page_data, offset); + if (offset == -1) { + break; + } + + page_data.replace(offset, tr_regexp.matchedLength(), tr(tr_regexp.cap(1).toUtf8())); + offset += tr_regexp.matchedLength(); + } + + QBuffer image_buffer; + image_buffer.open(QIODevice::ReadWrite); + QApplication::style() + ->standardIcon(QStyle::SP_DialogOkButton) + .pixmap(16) + .toImage() + .save(&image_buffer, "PNG"); + page_data.replace("@IMAGE_DATA@", image_buffer.data().toBase64()); + + socket->write("HTTP/1.0 200 OK\r\n"); + socket->write("Content-type: text/html;charset=UTF-8\r\n"); + socket->write("\r\n\r\n"); + socket->write(page_data.toUtf8()); + socket->flush(); + +} + +QUrl LocalRedirectServer::ParseUrlFromRequest(const QByteArray& request) const { + QList lines = request.split('\r'); + const QByteArray& request_line = lines[0]; + QByteArray path = request_line.split(' ')[1]; + QUrl base_url = url(); + QUrl request_url(base_url.toString() + path.mid(1), QUrl::StrictMode); + return request_url; +} diff --git a/src/internet/localredirectserver.h b/src/internet/localredirectserver.h new file mode 100644 index 000000000..e62086bac --- /dev/null +++ b/src/internet/localredirectserver.h @@ -0,0 +1,63 @@ +/* + * This file was part of Clementine. + * Copyright 2012, 2014, John Maguire + * Copyright 2014, Krzysztof Sobiecki + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#ifndef LOCALREDIRECTSERVER_H +#define LOCALREDIRECTSERVER_H + +#include +#include +#include + +class QTcpServer; +class QTcpSocket; + +class LocalRedirectServer : public QObject { + Q_OBJECT + + public: + explicit LocalRedirectServer(QObject* parent = nullptr); + + // Causes the server to listen for _one_ request. + void Listen(); + + // Returns the HTTP URL of this server. + const QUrl& url() const { return url_; } + + // Returns the URL requested by the OAuth redirect. + const QUrl& request_url() const { return request_url_; } + + signals: + void Finished(); + + private slots: + void NewConnection(); + void ReadyRead(QTcpSocket* socket, QByteArray buffer); + + private: + void WriteTemplate(QTcpSocket* socket) const; + QUrl ParseUrlFromRequest(const QByteArray& request) const; + + private: + QTcpServer* server_; + QUrl url_; + QUrl request_url_; +}; + +#endif diff --git a/src/playlist/playlistview.h b/src/playlist/playlistview.h index 100724ef5..cf8906577 100644 --- a/src/playlist/playlistview.h +++ b/src/playlist/playlistview.h @@ -75,7 +75,7 @@ class PlaylistHeader; // that uses Gtk to paint row backgrounds, ignoring any custom brush or palette the caller set in the QStyleOption. // That breaks our currently playing track animation, which relies on the background painted by Qt to be transparent. // This proxy style uses QCommonStyle to paint the affected elements. -// This class is used by tidal search view as well. +// This class is used by tidal and deezer search view as well. class PlaylistProxyStyle : public QProxyStyle { public: PlaylistProxyStyle(QStyle *base); diff --git a/src/playlist/queuemanager.ui b/src/playlist/queuemanager.ui index d6d88b7f3..807e31f2f 100644 --- a/src/playlist/queuemanager.ui +++ b/src/playlist/queuemanager.ui @@ -14,7 +14,7 @@ Queue Manager - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -157,6 +157,7 @@ + diff --git a/src/settings/backendsettingspage.cpp b/src/settings/backendsettingspage.cpp index 9f0918b75..4562e9b1e 100644 --- a/src/settings/backendsettingspage.cpp +++ b/src/settings/backendsettingspage.cpp @@ -96,6 +96,9 @@ void BackendSettingsPage::Load() { #ifdef HAVE_PHONON ui_->combobox_engine->addItem(IconLoader::Load("speaker"), EngineDescription(Engine::Phonon), Engine::Phonon); #endif +#ifdef HAVE_DEEZER + ui_->combobox_engine->addItem(IconLoader::Load("deezer"), EngineDescription(Engine::Deezer), Engine::Deezer); +#endif enginetype_current_ = enginetype; output_current_ = s_.value("output", "").toString(); diff --git a/src/settings/deezersettingspage.cpp b/src/settings/deezersettingspage.cpp new file mode 100644 index 000000000..c073174cb --- /dev/null +++ b/src/settings/deezersettingspage.cpp @@ -0,0 +1,142 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "deezersettingspage.h" +#include "ui_deezersettingspage.h" +#include "core/application.h" +#include "core/iconloader.h" +#include "internet/internetmodel.h" +#include "deezer/deezerservice.h" + +const char *DeezerSettingsPage::kSettingsGroup = "Deezer"; + +DeezerSettingsPage::DeezerSettingsPage(SettingsDialog *parent) + : SettingsPage(parent), + ui_(new Ui::DeezerSettingsPage), + service_(dialog()->app()->internet_model()->Service()) { + + ui_->setupUi(this); + setWindowIcon(IconLoader::Load("deezer")); + + connect(ui_->button_login, SIGNAL(clicked()), SLOT(LoginClicked())); + connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked())); + + connect(this, SIGNAL(Login()), service_, SLOT(StartAuthorisation())); + + connect(service_, SIGNAL(LoginFailure(QString)), SLOT(LoginFailure(QString))); + connect(service_, SIGNAL(LoginSuccess()), SLOT(LoginSuccess())); + + dialog()->installEventFilter(this); + + ui_->combobox_quality->addItem("AAC (64)", "AAC_64"); + ui_->combobox_quality->addItem("MP3 (64)", "MP3_64"); + ui_->combobox_quality->addItem("MP3 (128)", "MP3_128"); + ui_->combobox_quality->addItem("MP3 (256)", "MP3_256"); + ui_->combobox_quality->addItem("MP3 (320)", "MP3_320"); + ui_->combobox_quality->addItem("FLAC", "FLAC"); + + ui_->combobox_coversize->addItem("Small", "cover_small"); + ui_->combobox_coversize->addItem("Medium", "cover_medium"); + ui_->combobox_coversize->addItem("Big", "cover_big"); + ui_->combobox_coversize->addItem("XL", "cover_xl"); + +} + +DeezerSettingsPage::~DeezerSettingsPage() { delete ui_; } + +void DeezerSettingsPage::Load() { + + QSettings s; + + s.beginGroup(kSettingsGroup); + ui_->username->setText(s.value("username").toString()); + QByteArray password = s.value("password").toByteArray(); + if (password.isEmpty()) ui_->password->clear(); + else ui_->password->setText(QString::fromUtf8(QByteArray::fromBase64(password))); + dialog()->ComboBoxLoadFromSettings(s, ui_->combobox_quality, "quality", "FLAC"); + ui_->spinbox_searchdelay->setValue(s.value("searchdelay", 1500).toInt()); + ui_->spinbox_albumssearchlimit->setValue(s.value("albumssearchlimit", 100).toInt()); + ui_->spinbox_songssearchlimit->setValue(s.value("songssearchlimit", 100).toInt()); + ui_->checkbox_fetchalbums->setChecked(s.value("fetchalbums", false).toBool()); + dialog()->ComboBoxLoadFromSettings(s, ui_->combobox_coversize, "coversize", "cover_big"); + ui_->checkbox_preview->setChecked(s.value("preview", false).toBool()); + s.endGroup(); + + if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); + +} + +void DeezerSettingsPage::Save() { + + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue("username", ui_->username->text()); + s.setValue("password", QString::fromUtf8(ui_->password->text().toUtf8().toBase64())); + s.setValue("quality", ui_->combobox_quality->itemData(ui_->combobox_quality->currentIndex())); + s.setValue("searchdelay", ui_->spinbox_searchdelay->value()); + s.setValue("albumssearchlimit", ui_->spinbox_albumssearchlimit->value()); + s.setValue("songssearchlimit", ui_->spinbox_songssearchlimit->value()); + s.setValue("fetchalbums", ui_->checkbox_fetchalbums->isChecked()); + s.setValue("coversize", ui_->combobox_coversize->itemData(ui_->combobox_coversize->currentIndex())); + s.setValue("preview", ui_->checkbox_preview->isChecked()); + s.endGroup(); + + service_->ReloadSettings(); + +} + +void DeezerSettingsPage::LoginClicked() { + emit Login(); + ui_->button_login->setEnabled(false); +} + +bool DeezerSettingsPage::eventFilter(QObject *object, QEvent *event) { + + if (object == dialog() && event->type() == QEvent::Enter) { + ui_->button_login->setEnabled(true); + return false; + } + + return SettingsPage::eventFilter(object, event); +} + +void DeezerSettingsPage::LogoutClicked() { + service_->Logout(); + ui_->button_login->setEnabled(true); + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut); +} + +void DeezerSettingsPage::LoginSuccess() { + if (!this->isVisible()) return; + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); + ui_->button_login->setEnabled(false); +} + +void DeezerSettingsPage::LoginFailure(QString failure_reason) { + if (!this->isVisible()) return; + QMessageBox::warning(this, tr("Authentication failed"), failure_reason); +} diff --git a/src/settings/deezersettingspage.h b/src/settings/deezersettingspage.h new file mode 100644 index 000000000..ae1618575 --- /dev/null +++ b/src/settings/deezersettingspage.h @@ -0,0 +1,66 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry 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. + * + * Strawberry 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 Strawberry. If not, see . + * + */ + +#ifndef DEEZERSETTINGSPAGE_H +#define DEEZERSETTINGSPAGE_H + +#include +#include +#include + +#include "settings/settingspage.h" + +class DeezerService; +class Ui_DeezerSettingsPage; + +class DeezerSettingsPage : public SettingsPage { + Q_OBJECT + + public: + explicit DeezerSettingsPage(SettingsDialog* parent = nullptr); + ~DeezerSettingsPage(); + + enum SearchBy { + SearchBy_Songs = 1, + SearchBy_Albums = 2, + }; + + static const char *kSettingsGroup; + + void Load(); + void Save(); + + bool eventFilter(QObject *object, QEvent *event); + +signals: + void Login(); + void Login(const QString &username, const QString &password); + + private slots: + void LoginClicked(); + void LogoutClicked(); + void LoginSuccess(); + void LoginFailure(QString failure_reason); + + private: + Ui_DeezerSettingsPage* ui_; + DeezerService *service_; +}; + +#endif diff --git a/src/settings/deezersettingspage.ui b/src/settings/deezersettingspage.ui new file mode 100644 index 000000000..d2938d82d --- /dev/null +++ b/src/settings/deezersettingspage.ui @@ -0,0 +1,417 @@ + + + DeezerSettingsPage + + + + 0 + 0 + 715 + 547 + + + + Deezer + + + + + + + 0 + 0 + + + + Authentication + + + + + + + + Login + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + 70 + 0 + + + + Username + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 70 + 0 + + + + Password + + + + + + + QLineEdit::Password + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + Preferences + + + + + + + + + 150 + 0 + + + + Audio quality + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 150 + 0 + + + + Search delay + + + + + + + ms + + + 0 + + + 10000 + + + 50 + + + 1500 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 150 + 0 + + + + Albums search limit + + + + + + + 1 + + + 1000 + + + 50 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 150 + 0 + + + + Songs search limit + + + + + + + 1 + + + 1000 + + + 50 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Fetch entire albums when searching songs + + + + + + + + + + 150 + 0 + + + + Album cover size + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Use 30 seconds preview streams + + + + + + + + + + Qt::Vertical + + + + 20 + 30 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 200 + 62 + + + + + 200 + 62 + + + + :/pictures/deezer.png + + + + + + + + + + LoginStateWidget + QWidget +
widgets/loginstatewidget.h
+ 1 +
+
+ + + + + +
diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp index 09817676e..b5efa6248 100644 --- a/src/settings/settingsdialog.cpp +++ b/src/settings/settingsdialog.cpp @@ -63,6 +63,7 @@ #include "shortcutssettingspage.h" #include "transcodersettingspage.h" #include "tidalsettingspage.h" +#include "deezersettingspage.h" #include "ui_settingsdialog.h" @@ -114,7 +115,6 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent) ui_->list->setItemDelegate(new SettingsItemDelegate(this)); QTreeWidgetItem *general = AddCategory(tr("General")); - AddPage(Page_Behaviour, new BehaviourSettingsPage(this), general); AddPage(Page_Collection, new CollectionSettingsPage(this), general); AddPage(Page_Backend, new BackendSettingsPage(this), general); @@ -124,7 +124,10 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent) #ifdef HAVE_GSTREAMER AddPage(Page_Transcoding, new TranscoderSettingsPage(this), general); #endif - AddPage(Page_Tidal, new TidalSettingsPage(this), general); + + QTreeWidgetItem *internet = AddCategory(tr("Internet")); + AddPage(Page_Tidal, new TidalSettingsPage(this), internet); + AddPage(Page_Deezer, new DeezerSettingsPage(this), internet); // User interface QTreeWidgetItem *iface = AddCategory(tr("User interface")); diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h index 5f529e196..a152c0176 100644 --- a/src/settings/settingsdialog.h +++ b/src/settings/settingsdialog.h @@ -82,6 +82,7 @@ public: Page_Proxy, Page_Transcoding, Page_Tidal, + Page_Deezer, }; enum Role { diff --git a/src/settings/settingsdialog.ui b/src/settings/settingsdialog.ui index c1d8a8244..f2b3913f4 100644 --- a/src/settings/settingsdialog.ui +++ b/src/settings/settingsdialog.ui @@ -14,7 +14,7 @@ Settings - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -97,6 +97,7 @@ + diff --git a/src/settings/shortcutssettingspage.ui b/src/settings/shortcutssettingspage.ui index 5ac13826d..05599b69e 100644 --- a/src/settings/shortcutssettingspage.ui +++ b/src/settings/shortcutssettingspage.ui @@ -14,7 +14,7 @@ Shortcuts - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -212,6 +212,7 @@ + diff --git a/src/settings/tidalsettingspage.cpp b/src/settings/tidalsettingspage.cpp index 9a2b9596a..98429f8a2 100644 --- a/src/settings/tidalsettingspage.cpp +++ b/src/settings/tidalsettingspage.cpp @@ -76,7 +76,7 @@ void TidalSettingsPage::Load() { s.beginGroup(kSettingsGroup); ui_->username->setText(s.value("username").toString()); QByteArray password = s.value("password").toByteArray(); - if (password.isEmpty()) ui_->password->setText(""); + if (password.isEmpty()) ui_->password->clear(); else ui_->password->setText(QString::fromUtf8(QByteArray::fromBase64(password))); dialog()->ComboBoxLoadFromSettings(s, ui_->combobox_quality, "quality", "HIGH"); ui_->spinbox_searchdelay->setValue(s.value("searchdelay", 1500).toInt()); diff --git a/src/settings/tidalsettingspage.ui b/src/settings/tidalsettingspage.ui index 7013741e3..dc5c3f1f7 100644 --- a/src/settings/tidalsettingspage.ui +++ b/src/settings/tidalsettingspage.ui @@ -360,7 +360,7 @@ - :/icons/64x64/tidal.png + :/icons/64x64/tidal.png @@ -383,6 +383,7 @@ + diff --git a/src/transcoder/transcodelogdialog.ui b/src/transcoder/transcodelogdialog.ui index b9c3315cc..c416f2b06 100644 --- a/src/transcoder/transcodelogdialog.ui +++ b/src/transcoder/transcodelogdialog.ui @@ -14,7 +14,7 @@ Transcoder Log - + :/icons/64x64/strawberry.png:/icons/64x64/strawberry.png @@ -36,6 +36,7 @@ +