Merge remote-tracking branch 'upstream/master' into qt5
This commit is contained in:
commit
e6e189967d
@ -7,7 +7,6 @@ include(cmake/C++11Compat.cmake)
|
||||
include(cmake/Summary.cmake)
|
||||
include(cmake/Version.cmake)
|
||||
include(cmake/Deb.cmake)
|
||||
include(cmake/Rpm.cmake)
|
||||
include(cmake/SpotifyVersion.cmake)
|
||||
include(cmake/OptionalSource.cmake)
|
||||
include(cmake/Format.cmake)
|
||||
|
295
Changelog
295
Changelog
@ -1,160 +1,183 @@
|
||||
Next release:
|
||||
Version 1.3:
|
||||
Major features:
|
||||
* Vk.com support
|
||||
* Seafile support (server >= 4.4.1)
|
||||
* Amazon Cloud Drive support
|
||||
* Add Ampache compatibility (through Subsonic service)
|
||||
* Add new analyzer "Rainbow Dash"
|
||||
* Answer to the ultimate question of life, the universe and everything
|
||||
* Add "Psychedelic Colour" mode to all analyzers
|
||||
|
||||
Other features:
|
||||
* Add left click to fullsize cover on playing widget
|
||||
* Add m4b support for non-drm files
|
||||
* Ignore english articles for library sorting
|
||||
* Previous track in dynamic random mix
|
||||
* Improve the organize dialog
|
||||
* Add playlist save preference
|
||||
* Add a preference to disable the pause notification
|
||||
* Add a preference tab to hide some internet services
|
||||
* Add an option to disable inline song metadata editing
|
||||
* Use a save dialog option instead of quick change menu
|
||||
* Add ability to fetch lyrics from lololyrics.com
|
||||
* Add support for monitors in portrait mode
|
||||
* Add now playing widget mode
|
||||
* Add icons to extra
|
||||
* Add a source icon for CD tracks
|
||||
* Allow user to remove directories
|
||||
* Add ability to remove unavailable items from playlist
|
||||
* Add an import button to the transcode UI, allowing the user to pull in
|
||||
all files in a folder hierarchy to be transcoded
|
||||
* Make it impossible to collapse either side of the MainWindow splitter
|
||||
* Add menu items for updating and doing a full rescan of Google Drive
|
||||
* Increase Soundcloud cover image size
|
||||
* Ability to pause Spotify tracks
|
||||
* Add left click to fullsize cover on playing widget.
|
||||
* Add m4b support for non-drm files.
|
||||
* Ignore English articles for library sorting.
|
||||
* Improve the organize dialog.
|
||||
* Add an option to warn before closing a playlist tab.
|
||||
* Add an option to disable the pause notification.
|
||||
* Add options to hide some internet services.
|
||||
* Add an option to disable inline song metadata editing.
|
||||
* Add "details below" and "no details" now playing widget options.
|
||||
* Add "no song details" now playing widget option.
|
||||
* Add icons to the extras menu.
|
||||
* Add a source icon for CD tracks.
|
||||
* Allow user to remove directories in the Files tab.
|
||||
* Add ability to remove unavailable items from playlist.
|
||||
* Add a button to the transcode dialog to add all files in a directory.
|
||||
* Make it impossible to collapse either side of the MainWindow splitter.
|
||||
* Add menu items for updating and doing a full rescan of Google Drive.
|
||||
* Increase Soundcloud cover image size.
|
||||
* Add the ability to pause Spotify tracks.
|
||||
* Add the ability to add or remove a Spotify track to a Spotify playlist
|
||||
through context menu
|
||||
* Add Spotify tracks to Spotify playlists by drag and drop
|
||||
* Add ability to get a link to share Spotify playlists and songs
|
||||
* Add ability to automatically set podcast as listened after sucesfully sending
|
||||
it to a device
|
||||
* Add ability to order podcasts by age
|
||||
* Allow user to download multiple podcasts at the same time
|
||||
* Add ability to cancel podcast downloads in progress
|
||||
* Allow user to hide listened podcast episodes
|
||||
* Huge improvement of the speed at startup
|
||||
* Improve performance of mass rating changes
|
||||
* Improve ripping performance
|
||||
through context menu.
|
||||
* Add Spotify tracks to Spotify playlists by drag and drop.
|
||||
* Add ability to get a link to share Spotify playlists and songs.
|
||||
* Improve handling of Spotify Top Tracks and compilations.
|
||||
* Add playlist actions to Spotify songs.
|
||||
* Add ability to automatically set podcast as listened after successfully
|
||||
sending it to a device.
|
||||
* Add ability to order podcasts by age.
|
||||
* Allow user to download multiple podcasts at the same time.
|
||||
* Add ability to cancel podcast downloads in progress.
|
||||
* Allow user to hide listened podcast episodes.
|
||||
* Huge improvement of the speed at startup.
|
||||
* Improve performance of mass rating changes.
|
||||
* Improve ripping performance.
|
||||
* Persistent cache for pixmaps. Huge improvement of the performance when
|
||||
scrolling the library for example
|
||||
* Add AppData file for Clementine (for GNOME and KDE Software Centers)
|
||||
* Add iPod-like behaviour to previous button
|
||||
* Add "no song details" now playing widget option
|
||||
* Ability to add tracks to Spotify starred playlist by drag and drop
|
||||
* Add HipHop and Kuduro equalizers
|
||||
* Add AZLyrics lyric provider
|
||||
* Remember current playlist between restarts
|
||||
* (OSX) Use Alt+Tab to switch between playlist tabs
|
||||
* IDv3 tag lyrics support
|
||||
* Improve handling of Spotify Top Tracks and compilations
|
||||
* Scroll to last played track when switching playlists
|
||||
* Add stop after each song repeat mode
|
||||
* Sort discs numerically when using Group by disc
|
||||
* Add ability for sort by group and performer in the library view
|
||||
* Parse the year of a disc from musicbrainz
|
||||
* Add track intro mode
|
||||
* Add ability to add a search term with tab and space in the smart
|
||||
playlist window
|
||||
* Add love/ban (lastfm) global shortcuts
|
||||
* Add support for "original year" tags
|
||||
* Send album artist to Last.fm with liblastfm >= 1.0.0
|
||||
* Add sample rate selection
|
||||
scrolling the library for example.
|
||||
* Add AppData file for Clementine (for GNOME and KDE Software Centers).
|
||||
* Add iPod-like behaviour to previous button.
|
||||
* Add HipHop and Kuduro equalizers.
|
||||
* Remember current playlist between restarts.
|
||||
* IDv3 tag lyrics support.
|
||||
* Scroll to last played track when switching playlists.
|
||||
* Add stop after each song repeat mode.
|
||||
* Sort discs numerically when using Group by disc.
|
||||
* Add ability for sort by group and performer in the library view.
|
||||
* Parse the year of a disc from musicbrainz.
|
||||
* Add track intro mode.
|
||||
* Add ability to add a search term with tab and space in the smart playlist
|
||||
window.
|
||||
* Add love/ban (Last.fm) global shortcuts.
|
||||
* Add support for "original year" tags.
|
||||
* Send album artist to Last.fm with liblastfm >= 1.0.0.
|
||||
* Add sample rate selection.
|
||||
* Add option to change the time step when seeking using the keyboard.
|
||||
* Playlist sort by album considers disc and track numbers.
|
||||
* Add options for double clicking song in the playlist.
|
||||
* Volume slider handles glow effect using system theme.
|
||||
* Library view sort line themable.
|
||||
* Show track durations in the CD ripper dialog.
|
||||
* Add ability to read REM DISC tag from Cue sheet.
|
||||
* Add ability to lock/unlock rating edit status.
|
||||
* Add the support of trackNum elements in XSPF.
|
||||
* Add support for inhibiting the screensaver on windows.
|
||||
* Add "Smart Playlists" for Subsonic.
|
||||
* Add lyrics from AZLyrics.
|
||||
* Add lyrics from bollywoodlyrics.com.
|
||||
* Add lyrics from hindilyrics.net.
|
||||
* Add lyrics from lololyrics.com.
|
||||
* Add lyrics from Musixmatch.
|
||||
* Add lyrics from Tekstowo.pl.
|
||||
* (Mac OS X) Use Alt+Tab to switch between playlist tabs.
|
||||
|
||||
Bugfixes:
|
||||
* Fix crash when click on a SoundCloud entry in internet tab
|
||||
* Fix crash when marking podcast as listened
|
||||
* Fix crash after pressing OK in the device properties window
|
||||
* Fix stop after track which doesn't remove now playing
|
||||
* Fix play bleeding into next track after auto stop
|
||||
* Fix analyzer framerate when mouseover play scrubber
|
||||
* Fix issues with buffers sent to analyzer
|
||||
* Fix block analyzer framerate
|
||||
* Fix dbz possibility with small buffers at end of track
|
||||
* Fix dbz possibility in moodbar
|
||||
* Fix oversized album cover art
|
||||
* Fix Grooveshark SSL errors
|
||||
* Clean cover art from /tmp
|
||||
* Fix crash when click on a SoundCloud entry in internet tab.
|
||||
* Fix crash when marking podcast as listened.
|
||||
* Fix crash after pressing OK in the device properties window.
|
||||
* Fix stop after track which doesn't remove now playing.
|
||||
* Fix play bleeding into next track after auto stop.
|
||||
* Fix analyzer framerate when mouseover play scrubber.
|
||||
* Fix issues with buffers sent to analyzer.
|
||||
* Fix block analyzer framerate.
|
||||
* Fix divide-by-zero possibility with small buffers at end of track.
|
||||
* Fix divide-by-zero possibility in moodbar.
|
||||
* Fix oversized album cover art.
|
||||
* Clean cover art from /tmp.
|
||||
* Fix the rendering of the little numbers in the boxes on queued items in
|
||||
the playlist
|
||||
* Fix parsing of MusicBrainz data for discid
|
||||
* Fix random artifacting on nyanalyzer on startup
|
||||
* Fix podcasts length issues (which caused issues with seeking for example)
|
||||
* Fix too small equalizer window size
|
||||
* Fix labels which don't inherit system text colors in the edit tag dialog
|
||||
* Fix the mess of the queue manager after playlist re-sort
|
||||
* Fix for queue ordering issue in the playlist view when using c-d to
|
||||
dequeue a track
|
||||
* Fix detection of parent-relative paths in playlist saving
|
||||
* Fix path seperators issue when reading playlists
|
||||
* Fix m3u parser issue when an artist's name has a hyphen
|
||||
* Fix bug with percents when fetch the Jamendo catalogue
|
||||
* Fix a little dropout when transition to next track
|
||||
* Fix broken RockRadio.com for premium users
|
||||
* Fix Subsonic login with + characters in the password
|
||||
* Fix accents issue in when save playlist in xspf format
|
||||
the playlist.
|
||||
* Fix parsing of MusicBrainz data for discid.
|
||||
* Fix random artifacting on nyanalyzer on startup.
|
||||
* Fix podcasts length issues (which caused issues with seeking for example).
|
||||
* Fix too small equalizer window size.
|
||||
* Fix labels which don't inherit system text colors in the edit tag dialog.
|
||||
* Fix the mess of the queue manager after playlist re-sort.
|
||||
* Fix for queue ordering issue in the playlist view when using Ctrl+D to
|
||||
dequeue a track.
|
||||
* Fix detection of parent-relative paths in playlist saving.
|
||||
* Fix path seperators issue when reading playlists.
|
||||
* Fix m3u parser issue when an artist's name has a hyphen.
|
||||
* Fix bug with percents when fetch the Jamendo catalogue.
|
||||
* Fix a little dropout when transition to next track.
|
||||
* Fix broken RockRadio.com for premium users.
|
||||
* Fix Subsonic login with + characters in the password.
|
||||
* Fix accents issue in when save playlist in xspf format.
|
||||
* Fix issues with some songs length thanks to Taglib. People with Taglib
|
||||
installed on their system will have to wait a new release of Taglib
|
||||
* Fix moodbars not generating correctly
|
||||
* Fix socket leak in moodbar
|
||||
* Fix memory leak in tagreader
|
||||
* Fix crash when trying to fingerprint but missing a plugin
|
||||
* Fix infinite scan with Subsonic when the library is empty
|
||||
* Fix shortcut/media keys issues on Mac
|
||||
* Fix compilation issues on Yosemite
|
||||
* Fix performer tag for mpeg
|
||||
* Fix parsing issues with "innovative" datetime formats
|
||||
* Fix laggy interface on Mac
|
||||
* Fix crash in GrooveShark
|
||||
* Fix playback breaks in Spotify
|
||||
* Fix memory leaks
|
||||
* Fix crash when stopping song that is fading after pausing
|
||||
* Fix crash when trying to download a track but there is no current one playing
|
||||
* (OSX) Fix Soundcloud API Search which simply doesn't work
|
||||
* Fix default spinner gif image which shows white pixels around the image
|
||||
* Fix setting album artist tag for FLAC files if it already exists
|
||||
* Fix crash when Clementine lists the albums on Ampache
|
||||
* Fix Last.fm scrobbling after seek
|
||||
* Fix metadata not processed properly for some streams (Akamai)
|
||||
* Fix save state when the song was paused
|
||||
installed on their system will have to wait a new release of Taglib.
|
||||
* Fix moodbars not generating correctly.
|
||||
* Fix socket leak in moodbar.
|
||||
* Fix memory leak in tagreader.
|
||||
* Fix crash when trying to fingerprint but missing a plugin.
|
||||
* Fix infinite scan with Subsonic when the library is empty.
|
||||
* Fix shortcut/media keys issues on Mac.
|
||||
* Fix compilation issues on Yosemite.
|
||||
* Fix performer tag for mpeg.
|
||||
* Fix parsing issues with "innovative" datetime formats.
|
||||
* Fix laggy interface on Mac.
|
||||
* Fix playback breaks in Spotify.
|
||||
* Fix memory leaks.
|
||||
* Fix crash when stopping song that is fading after pausing.
|
||||
* Fix crash when trying to download a track but there is no current one
|
||||
playing.
|
||||
* Fix default spinner gif image which shows white pixels around the image.
|
||||
* Fix setting album artist tag for FLAC files if it already exists.
|
||||
* Fix crash when Clementine lists the albums on Ampache.
|
||||
* Fix Last.fm scrobbling after seek.
|
||||
* Fix metadata not processed properly for some streams (Akamai).
|
||||
* Fix save state when the song was paused.
|
||||
* Fix some issues in Boom and Turbine analyzers.
|
||||
* Fix song continuously rewinding when seeking using keyboard arrow keys.
|
||||
* Fix OSD re-posistioning which doesn't work on multiple monitors.
|
||||
* Fix Sonogram state while paused.
|
||||
* Fix crash when changing 'group by' while album covers are still loading.
|
||||
* Fix loss of valid data from an mp3 file when using the metadata editor.
|
||||
* Fix track slider twitching.
|
||||
* Fix Di.fm stations stuck when try to play them without internet.
|
||||
* Make mood files hidden in NTFS.
|
||||
* Fix time labels blinking when playing streams without known duration.
|
||||
* Fix tag fetcher which applies incorrect tags for songs without any results.
|
||||
* Fix Clementine getting stuck when transitioning from a local track to a
|
||||
Spotify track with crossfade disabled.
|
||||
* Fix previous track when playing a dynamic random mix.
|
||||
* Fix fullscreen album covers for monitors in portrait mode.
|
||||
* Don't scale down star icons by 1 pixel.
|
||||
* (Mac OS X) Fix Soundcloud API Search which simply doesn't work
|
||||
* (Windows) Fix context menu for the "now playing widget"
|
||||
* Fix some issues in Boom and Turbine analyzers
|
||||
* Fix song continuously rewinding when seeking using keyboard arrow keys
|
||||
* Fix OSD re-posistioning which doesn't work on multiple monitors
|
||||
* Fix Sonogram state while paused
|
||||
|
||||
Removed features:
|
||||
* Remove Ubuntu One support
|
||||
* Remove Discogs support
|
||||
* Remove GrooveShark support
|
||||
* Remove Ubuntu One support.
|
||||
* Remove Discogs support.
|
||||
* Remove GrooveShark support.
|
||||
* Remove Radio GFM support.
|
||||
|
||||
Build system changes:
|
||||
* Update to gstreamer 1.0
|
||||
* Don't compile vreen with link-time optimizations
|
||||
* Use the system's sha2 library if it's available
|
||||
* (Windows) Add libgmp-10.dll which is required by libgiognutls.dll
|
||||
* (Fedora) Don't depend on libplist or usbmuxd
|
||||
* Remove libindicate-qt
|
||||
* (Debian/Ubuntu) Remove internal copy of chromaprint and add it as
|
||||
dependency
|
||||
* (Debian/Ubuntu) Add libmygpo-qt-dev (=> 1.0.7)
|
||||
* Remove internal copy of libechonest and add it as dependency
|
||||
* Update to gstreamer 1.0.
|
||||
* Don't compile vreen with link-time optimizations.
|
||||
* Use the system's sha2 library if it's available.
|
||||
* Remove libindicate-qt.
|
||||
* Remove internal copy of libechonest and add it as dependency.
|
||||
* Use libcrypto++ instead of QCA.
|
||||
* Update TagLib to 1.10.0.
|
||||
* (Windows) Uninstall a previous install of Clementine when installing a new
|
||||
version
|
||||
* (Windows) Remember the last installation path
|
||||
* (GNU/Linux) Follow freedesktop.org specifications for icons
|
||||
* (GNU/Linux) Add a 128x128 version of the Clementine icon
|
||||
* (OSX) Use a GTlsDatabase for gstreamer SSL
|
||||
* Use libcrypto++ instead of QCA
|
||||
version.
|
||||
* (Windows) Remember the last installation path.
|
||||
* (Windows) Add libgmp-10.dll which is required by libgiognutls.dll.
|
||||
* (Mac OS X) Use a GTlsDatabase for gstreamer SSL.
|
||||
* (Linux) Follow freedesktop.org specifications for icons.
|
||||
* (Linux) Add a 128x128 version of the Clementine icon.
|
||||
* (Debian/Ubuntu) Remove internal copy of chromaprint and add it as
|
||||
dependency.
|
||||
* (Debian/Ubuntu) Add libmygpo-qt-dev (=> 1.0.7).
|
||||
* (Fedora) Don't depend on libplist or usbmuxd.
|
||||
|
||||
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
set(RPMBUILD_DIR ~/rpmbuild CACHE STRING "Rpmbuild directory, for the rpm target")
|
||||
set(MOCK_COMMAND mock CACHE STRING "Command to use for running mock")
|
||||
set(MOCK_CHROOT fedora-13-x86_64 CACHE STRING "Chroot to use when building an rpm with mock")
|
||||
set(RPM_DISTRO fc13 CACHE STRING "Suffix of the rpm file")
|
||||
set(RPM_ARCH x86_64 CACHE STRING "Architecture of the rpm file")
|
||||
|
||||
add_custom_target(rpm
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/maketarball.sh
|
||||
COMMAND ${CMAKE_COMMAND} -E copy clementine-${CLEMENTINE_VERSION_SPARKLE}.tar.xz ${RPMBUILD_DIR}/SOURCES/
|
||||
COMMAND rpmbuild -bs ${CMAKE_SOURCE_DIR}/dist/clementine.spec
|
||||
COMMAND ${MOCK_COMMAND}
|
||||
--verbose
|
||||
--root=${MOCK_CHROOT}
|
||||
--resultdir=${CMAKE_BINARY_DIR}/mock_result/
|
||||
${RPMBUILD_DIR}/SRPMS/clementine-${CLEMENTINE_VERSION_RPM_V}-${CLEMENTINE_VERSION_RPM_R}.${RPM_DISTRO}.src.rpm
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_BINARY_DIR}/mock_result/clementine-${CLEMENTINE_VERSION_RPM_V}-${CLEMENTINE_VERSION_RPM_R}.${RPM_DISTRO}.${RPM_ARCH}.rpm
|
||||
${CMAKE_BINARY_DIR}/clementine-${CLEMENTINE_VERSION_RPM_V}-${CLEMENTINE_VERSION_RPM_R}.${RPM_DISTRO}.${RPM_ARCH}.rpm
|
||||
)
|
@ -1,3 +1,3 @@
|
||||
# Increment this whenever the user needs to download a new blob
|
||||
# Remember to upload and sign the new version of the blob.
|
||||
set(SPOTIFY_BLOB_VERSION 15)
|
||||
set(SPOTIFY_BLOB_VERSION 16)
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
3
debian/copyright
vendored
3
debian/copyright
vendored
@ -45,8 +45,7 @@ Files: src/core/fht.*
|
||||
Copyright: 2004, Melchior FRANZ <mfranz@kde.org>
|
||||
License: GPL-2+
|
||||
|
||||
Files: ext/libclementine-common/core/arraysize.h
|
||||
ext/libclementine-common/core/scoped_nsautorelease_pool.*
|
||||
Files: ext/libclementine-common/core/scoped_nsautorelease_pool.*
|
||||
src/core/scoped_nsobject.h
|
||||
src/core/scoped_cftyperef.h
|
||||
Copyright: 2011, The Chromium Authors
|
||||
|
4
dist/clementine.spec.in
vendored
4
dist/clementine.spec.in
vendored
@ -1,6 +1,6 @@
|
||||
Name: clementine
|
||||
Version: @CLEMENTINE_VERSION_RPM_V@
|
||||
Release: @CLEMENTINE_VERSION_RPM_R@.@RPM_DISTRO@
|
||||
Release: @CLEMENTINE_VERSION_RPM_R@%{?dist}
|
||||
Summary: A music player and library organiser
|
||||
|
||||
Group: Applications/Multimedia
|
||||
@ -61,7 +61,7 @@ Features include:
|
||||
|
||||
%build
|
||||
cd bin
|
||||
%{cmake} .. -DUSE_INSTALL_PREFIX=OFF -DBUNDLE_PROJECTM_PRESETS=ON
|
||||
%{cmake} .. -DUSE_INSTALL_PREFIX=OFF -DBUNDLE_PROJECTM_PRESETS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
make %{?_smp_mflags}
|
||||
|
||||
%install
|
||||
|
@ -1,34 +1,5 @@
|
||||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// From Chromium src/base/macros.h
|
||||
|
||||
#include <stddef.h> // For size_t.
|
||||
|
||||
// The arraysize(arr) macro returns the # of elements in an array arr.
|
||||
// The expression is a compile-time constant, and therefore can be
|
||||
// used in defining new arrays, for example. If you use arraysize on
|
||||
// a pointer by mistake, you will get a compile-time error.
|
||||
//
|
||||
// One caveat is that arraysize() doesn't accept any array of an
|
||||
// anonymous type or a type defined inside a function. In these rare
|
||||
// cases, you have to use the unsafe ARRAYSIZE_UNSAFE() macro below. This is
|
||||
// due to a limitation in C++'s template system. The limitation might
|
||||
// eventually be removed, but it hasn't happened yet.
|
||||
|
||||
// This template function declaration is used in defining arraysize.
|
||||
// Note that the function doesn't need an implementation, as we only
|
||||
// use its type.
|
||||
template <typename T, size_t N>
|
||||
char (&ArraySizeHelper(T (&array)[N]))[N];
|
||||
|
||||
// That gcc wants both of these prototypes seems mysterious. VC, for
|
||||
// its part, can't decide which to use (another mystery). Matching of
|
||||
// template overloads: the final frontier.
|
||||
#ifndef _MSC_VER
|
||||
template <typename T, size_t N>
|
||||
char (&ArraySizeHelper(const T (&array)[N]))[N];
|
||||
#endif
|
||||
|
||||
#define arraysize(array) (sizeof(ArraySizeHelper(array)))
|
||||
template <typename T>
|
||||
constexpr size_t arraysize(const T&) {
|
||||
static_assert(std::is_array<T>::value, "Argument must be array");
|
||||
return std::extent<T>::value;
|
||||
}
|
||||
|
65
ext/libclementine-common/core/lazy.h
Normal file
65
ext/libclementine-common/core/lazy.h
Normal file
@ -0,0 +1,65 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2016, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef LAZY_H
|
||||
#define LAZY_H
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
// Helper for lazy initialisation of objects.
|
||||
// Usage:
|
||||
// Lazy<Foo> my_lazy_object([]() { return new Foo; });
|
||||
template <typename T>
|
||||
class Lazy {
|
||||
public:
|
||||
explicit Lazy(std::function<T*()> init) : init_(init) {}
|
||||
|
||||
// Convenience constructor that will lazily default construct the object.
|
||||
Lazy() : init_([]() { return new T; }) {}
|
||||
|
||||
T* get() const {
|
||||
CheckInitialised();
|
||||
return ptr_.get();
|
||||
}
|
||||
|
||||
typename std::add_lvalue_reference<T>::type operator*() const {
|
||||
CheckInitialised();
|
||||
return *ptr_;
|
||||
}
|
||||
|
||||
T* operator->() const { return get(); }
|
||||
|
||||
// Returns true if the object is not yet initialised.
|
||||
explicit operator bool() const { return ptr_; }
|
||||
|
||||
// Deletes the underlying object and will re-run the initialisation function
|
||||
// if the object is requested again.
|
||||
void reset() { ptr_.reset(nullptr); }
|
||||
|
||||
private:
|
||||
void CheckInitialised() const {
|
||||
if (!ptr_) {
|
||||
ptr_.reset(init_());
|
||||
}
|
||||
}
|
||||
|
||||
const std::function<T*()> init_;
|
||||
mutable std::unique_ptr<T> ptr_;
|
||||
};
|
||||
|
||||
#endif // LAZY_H
|
@ -25,6 +25,8 @@
|
||||
#include <execinfo.h>
|
||||
#endif
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QStringList>
|
||||
@ -204,7 +206,8 @@ QDebug CreateLogger(Level level, const QString& class_name, int line) {
|
||||
}
|
||||
|
||||
QDebug ret(type);
|
||||
ret.nospace() << kMessageHandlerMagic << QDateTime::currentDateTime()
|
||||
ret.nospace() << kMessageHandlerMagic
|
||||
<< QDateTime::currentDateTime()
|
||||
.toString("hh:mm:ss.zzz")
|
||||
.toLatin1()
|
||||
.constData() << level_name
|
||||
@ -259,7 +262,8 @@ void DumpStackTrace() {
|
||||
backtrace_symbols(reinterpret_cast<void**>(&callstack), callstack_size);
|
||||
// Start from 1 to skip ourself.
|
||||
for (int i = 1; i < callstack_size; ++i) {
|
||||
qLog(Debug) << DemangleSymbol(QString::fromLatin1(symbols[i]));
|
||||
std::cerr << DemangleSymbol(QString::fromLatin1(symbols[i])).toStdString()
|
||||
<< std::endl;
|
||||
}
|
||||
free(symbols);
|
||||
#else
|
||||
|
@ -205,6 +205,7 @@ set(SOURCES
|
||||
library/libraryview.cpp
|
||||
library/libraryviewcontainer.cpp
|
||||
library/librarywatcher.cpp
|
||||
library/savedgroupingmanager.cpp
|
||||
library/sqlrow.cpp
|
||||
|
||||
musicbrainz/acoustidclient.cpp
|
||||
@ -506,6 +507,7 @@ set(HEADERS
|
||||
library/libraryview.h
|
||||
library/libraryviewcontainer.h
|
||||
library/librarywatcher.h
|
||||
library/savedgroupingmanager.h
|
||||
|
||||
musicbrainz/acoustidclient.h
|
||||
musicbrainz/musicbrainzclient.h
|
||||
@ -693,6 +695,7 @@ set(UI
|
||||
library/libraryfilterwidget.ui
|
||||
library/librarysettingspage.ui
|
||||
library/libraryviewcontainer.ui
|
||||
library/savedgroupingmanager.ui
|
||||
|
||||
playlist/dynamicplaylistcontrols.ui
|
||||
playlist/playlistcontainer.ui
|
||||
|
@ -21,31 +21,39 @@
|
||||
*/
|
||||
|
||||
#include "application.h"
|
||||
#include "appearance.h"
|
||||
|
||||
#include "config.h"
|
||||
#include "database.h"
|
||||
#include "player.h"
|
||||
#include "tagreaderclient.h"
|
||||
#include "taskmanager.h"
|
||||
#include "core/appearance.h"
|
||||
#include "core/database.h"
|
||||
#include "core/lazy.h"
|
||||
#include "core/player.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "covers/albumcoverloader.h"
|
||||
#include "covers/amazoncoverprovider.h"
|
||||
#include "covers/coverproviders.h"
|
||||
#include "covers/currentartloader.h"
|
||||
#include "covers/musicbrainzcoverprovider.h"
|
||||
#include "devices/devicemanager.h"
|
||||
#include "internet/core/internetmodel.h"
|
||||
#include "globalsearch/globalsearch.h"
|
||||
#include "library/library.h"
|
||||
#include "library/librarybackend.h"
|
||||
#include "networkremote/networkremote.h"
|
||||
#include "networkremote/networkremotehelper.h"
|
||||
#include "playlist/playlistbackend.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
#include "internet/core/internetmodel.h"
|
||||
#include "internet/core/scrobbler.h"
|
||||
#include "internet/podcasts/gpoddersync.h"
|
||||
#include "internet/podcasts/podcastbackend.h"
|
||||
#include "internet/podcasts/podcastdeleter.h"
|
||||
#include "internet/podcasts/podcastdownloader.h"
|
||||
#include "internet/podcasts/podcastupdater.h"
|
||||
#include "library/librarybackend.h"
|
||||
#include "library/library.h"
|
||||
#include "moodbar/moodbarcontroller.h"
|
||||
#include "moodbar/moodbarloader.h"
|
||||
#include "networkremote/networkremote.h"
|
||||
#include "networkremote/networkremotehelper.h"
|
||||
#include "playlist/playlistbackend.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
#include "covers/lastfmcoverprovider.h"
|
||||
#include "internet/lastfm/lastfmservice.h"
|
||||
#endif // HAVE_LIBLASTFM
|
||||
|
||||
@ -56,101 +64,137 @@
|
||||
|
||||
bool Application::kIsPortable = false;
|
||||
|
||||
Application::Application(QObject* parent)
|
||||
: QObject(parent),
|
||||
tag_reader_client_(nullptr),
|
||||
database_(nullptr),
|
||||
album_cover_loader_(nullptr),
|
||||
playlist_backend_(nullptr),
|
||||
podcast_backend_(nullptr),
|
||||
appearance_(nullptr),
|
||||
cover_providers_(nullptr),
|
||||
task_manager_(nullptr),
|
||||
player_(nullptr),
|
||||
playlist_manager_(nullptr),
|
||||
current_art_loader_(nullptr),
|
||||
global_search_(nullptr),
|
||||
internet_model_(nullptr),
|
||||
library_(nullptr),
|
||||
device_manager_(nullptr),
|
||||
podcast_updater_(nullptr),
|
||||
podcast_deleter_(nullptr),
|
||||
podcast_downloader_(nullptr),
|
||||
gpodder_sync_(nullptr),
|
||||
moodbar_loader_(nullptr),
|
||||
moodbar_controller_(nullptr),
|
||||
network_remote_(nullptr),
|
||||
network_remote_helper_(nullptr),
|
||||
scrobbler_(nullptr) {
|
||||
tag_reader_client_ = new TagReaderClient(this);
|
||||
MoveToNewThread(tag_reader_client_);
|
||||
tag_reader_client_->Start();
|
||||
|
||||
database_ = new Database(this, this);
|
||||
MoveToNewThread(database_);
|
||||
|
||||
album_cover_loader_ = new AlbumCoverLoader(this);
|
||||
MoveToNewThread(album_cover_loader_);
|
||||
|
||||
playlist_backend_ = new PlaylistBackend(this, this);
|
||||
MoveToThread(playlist_backend_, database_->thread());
|
||||
|
||||
podcast_backend_ = new PodcastBackend(this, this);
|
||||
MoveToThread(podcast_backend_, database_->thread());
|
||||
|
||||
appearance_ = new Appearance(this);
|
||||
cover_providers_ = new CoverProviders(this);
|
||||
task_manager_ = new TaskManager(this);
|
||||
player_ = new Player(this, this);
|
||||
playlist_manager_ = new PlaylistManager(this, this);
|
||||
current_art_loader_ = new CurrentArtLoader(this, this);
|
||||
global_search_ = new GlobalSearch(this, this);
|
||||
internet_model_ = new InternetModel(this, this);
|
||||
library_ = new Library(this, this);
|
||||
device_manager_ = new DeviceManager(this, this);
|
||||
podcast_updater_ = new PodcastUpdater(this, this);
|
||||
|
||||
podcast_deleter_ = new PodcastDeleter(this, this);
|
||||
MoveToNewThread(podcast_deleter_);
|
||||
|
||||
podcast_downloader_ = new PodcastDownloader(this, this);
|
||||
gpodder_sync_ = new GPodderSync(this, this);
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
moodbar_loader_ = new MoodbarLoader(this, this);
|
||||
moodbar_controller_ = new MoodbarController(this, this);
|
||||
class ApplicationImpl {
|
||||
public:
|
||||
ApplicationImpl(Application* app)
|
||||
: tag_reader_client_([=]() {
|
||||
TagReaderClient* client = new TagReaderClient(app);
|
||||
app->MoveToNewThread(client);
|
||||
client->Start();
|
||||
return client;
|
||||
}),
|
||||
database_([=]() {
|
||||
Database* db = new Database(app, app);
|
||||
app->MoveToNewThread(db);
|
||||
DoInAMinuteOrSo(db, SLOT(DoBackup()));
|
||||
return db;
|
||||
}),
|
||||
album_cover_loader_([=]() {
|
||||
AlbumCoverLoader* loader = new AlbumCoverLoader(app);
|
||||
app->MoveToNewThread(loader);
|
||||
return loader;
|
||||
}),
|
||||
playlist_backend_([=]() {
|
||||
PlaylistBackend* backend = new PlaylistBackend(app, app);
|
||||
app->MoveToThread(backend, database_->thread());
|
||||
return backend;
|
||||
}),
|
||||
podcast_backend_([=]() {
|
||||
PodcastBackend* backend = new PodcastBackend(app, app);
|
||||
app->MoveToThread(backend, database_->thread());
|
||||
return backend;
|
||||
}),
|
||||
appearance_([=]() { return new Appearance(app); }),
|
||||
cover_providers_([=]() {
|
||||
CoverProviders* cover_providers = new CoverProviders(app);
|
||||
// Initialize the repository of cover providers.
|
||||
cover_providers->AddProvider(new AmazonCoverProvider);
|
||||
cover_providers->AddProvider(new MusicbrainzCoverProvider);
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
cover_providers->AddProvider(new LastFmCoverProvider(app));
|
||||
#endif
|
||||
return cover_providers;
|
||||
}),
|
||||
task_manager_([=]() { return new TaskManager(app); }),
|
||||
player_([=]() { return new Player(app, app); }),
|
||||
playlist_manager_([=]() { return new PlaylistManager(app); }),
|
||||
current_art_loader_([=]() { return new CurrentArtLoader(app, app); }),
|
||||
global_search_([=]() { return new GlobalSearch(app, app); }),
|
||||
internet_model_([=]() { return new InternetModel(app, app); }),
|
||||
library_([=]() { return new Library(app, app); }),
|
||||
device_manager_([=]() { return new DeviceManager(app, app); }),
|
||||
podcast_updater_([=]() { return new PodcastUpdater(app, app); }),
|
||||
podcast_deleter_([=]() {
|
||||
PodcastDeleter* deleter = new PodcastDeleter(app, app);
|
||||
app->MoveToNewThread(deleter);
|
||||
return deleter;
|
||||
}),
|
||||
podcast_downloader_([=]() { return new PodcastDownloader(app, app); }),
|
||||
gpodder_sync_([=]() { return new GPodderSync(app, app); }),
|
||||
moodbar_loader_([=]() {
|
||||
#ifdef HAVE_MOODBAR
|
||||
return new MoodbarLoader(app, app);
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}),
|
||||
moodbar_controller_([=]() {
|
||||
#ifdef HAVE_MOODBAR
|
||||
return new MoodbarController(app, app);
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}),
|
||||
network_remote_([=]() {
|
||||
NetworkRemote* remote = new NetworkRemote(app);
|
||||
app->MoveToNewThread(remote);
|
||||
return remote;
|
||||
}),
|
||||
network_remote_helper_([=]() { return new NetworkRemoteHelper(app); }),
|
||||
scrobbler_([=]() {
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
return new LastFMService(app, app);
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}) {
|
||||
}
|
||||
|
||||
// Network Remote
|
||||
network_remote_ = new NetworkRemote(this);
|
||||
MoveToNewThread(network_remote_);
|
||||
Lazy<TagReaderClient> tag_reader_client_;
|
||||
Lazy<Database> database_;
|
||||
Lazy<AlbumCoverLoader> album_cover_loader_;
|
||||
Lazy<PlaylistBackend> playlist_backend_;
|
||||
Lazy<PodcastBackend> podcast_backend_;
|
||||
Lazy<Appearance> appearance_;
|
||||
Lazy<CoverProviders> cover_providers_;
|
||||
Lazy<TaskManager> task_manager_;
|
||||
Lazy<Player> player_;
|
||||
Lazy<PlaylistManager> playlist_manager_;
|
||||
Lazy<CurrentArtLoader> current_art_loader_;
|
||||
Lazy<GlobalSearch> global_search_;
|
||||
Lazy<InternetModel> internet_model_;
|
||||
Lazy<Library> library_;
|
||||
Lazy<DeviceManager> device_manager_;
|
||||
Lazy<PodcastUpdater> podcast_updater_;
|
||||
Lazy<PodcastDeleter> podcast_deleter_;
|
||||
Lazy<PodcastDownloader> podcast_downloader_;
|
||||
Lazy<GPodderSync> gpodder_sync_;
|
||||
Lazy<MoodbarLoader> moodbar_loader_;
|
||||
Lazy<MoodbarController> moodbar_controller_;
|
||||
Lazy<NetworkRemote> network_remote_;
|
||||
Lazy<NetworkRemoteHelper> network_remote_helper_;
|
||||
Lazy<Scrobbler> scrobbler_;
|
||||
};
|
||||
|
||||
// This must be before libraray_->Init();
|
||||
Application::Application(QObject* parent)
|
||||
: QObject(parent), p_(new ApplicationImpl(this)) {
|
||||
// This must be before library_->Init();
|
||||
// In the constructor the helper waits for the signal
|
||||
// PlaylistManagerInitialized
|
||||
// to start the remote. Without the playlist manager clementine can
|
||||
// crash when a client connects before the manager is initialized!
|
||||
network_remote_helper_ = new NetworkRemoteHelper(this);
|
||||
network_remote_helper();
|
||||
library()->Init();
|
||||
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
scrobbler_ = new LastFMService(this, this);
|
||||
#endif // HAVE_LIBLASTFM
|
||||
|
||||
library_->Init();
|
||||
|
||||
DoInAMinuteOrSo(database_, SLOT(DoBackup()));
|
||||
// TODO(John Maguire): Make this not a weird singleton.
|
||||
tag_reader_client();
|
||||
}
|
||||
|
||||
Application::~Application() {
|
||||
// It's important that the device manager is deleted before the database.
|
||||
// Deleting the database deletes all objects that have been created in its
|
||||
// thread, including some device library backends.
|
||||
delete device_manager_;
|
||||
device_manager_ = nullptr;
|
||||
|
||||
for (QObject* object : objects_in_threads_) {
|
||||
object->deleteLater();
|
||||
}
|
||||
p_->device_manager_.reset();
|
||||
|
||||
for (QThread* thread : threads_) {
|
||||
thread->quit();
|
||||
@ -173,7 +217,6 @@ void Application::MoveToNewThread(QObject* object) {
|
||||
void Application::MoveToThread(QObject* object, QThread* thread) {
|
||||
object->setParent(nullptr);
|
||||
object->moveToThread(thread);
|
||||
objects_in_threads_ << object;
|
||||
}
|
||||
|
||||
void Application::AddError(const QString& message) { emit ErrorAdded(message); }
|
||||
@ -186,14 +229,100 @@ QString Application::language_without_region() const {
|
||||
return language_name_;
|
||||
}
|
||||
|
||||
void Application::ReloadSettings() { emit SettingsChanged(); }
|
||||
|
||||
void Application::OpenSettingsDialogAtPage(SettingsDialog::Page page) {
|
||||
emit SettingsDialogRequested(page);
|
||||
}
|
||||
|
||||
AlbumCoverLoader* Application::album_cover_loader() const {
|
||||
return p_->album_cover_loader_.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(); }
|
||||
|
||||
DeviceManager* Application::device_manager() const {
|
||||
return p_->device_manager_.get();
|
||||
}
|
||||
|
||||
GlobalSearch* Application::global_search() const {
|
||||
return p_->global_search_.get();
|
||||
}
|
||||
|
||||
GPodderSync* Application::gpodder_sync() const {
|
||||
return p_->gpodder_sync_.get();
|
||||
}
|
||||
|
||||
InternetModel* Application::internet_model() const {
|
||||
return p_->internet_model_.get();
|
||||
}
|
||||
|
||||
Library* Application::library() const { return p_->library_.get(); }
|
||||
|
||||
LibraryBackend* Application::library_backend() const {
|
||||
return library()->backend();
|
||||
}
|
||||
|
||||
LibraryModel* Application::library_model() const { return library()->model(); }
|
||||
|
||||
void Application::ReloadSettings() { emit SettingsChanged(); }
|
||||
|
||||
void Application::OpenSettingsDialogAtPage(SettingsDialog::Page page) {
|
||||
emit SettingsDialogRequested(page);
|
||||
MoodbarController* Application::moodbar_controller() const {
|
||||
return p_->moodbar_controller_.get();
|
||||
}
|
||||
|
||||
MoodbarLoader* Application::moodbar_loader() const {
|
||||
return p_->moodbar_loader_.get();
|
||||
}
|
||||
|
||||
NetworkRemoteHelper* Application::network_remote_helper() const {
|
||||
return p_->network_remote_helper_.get();
|
||||
}
|
||||
|
||||
NetworkRemote* Application::network_remote() const {
|
||||
return p_->network_remote_.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();
|
||||
}
|
||||
|
||||
PodcastBackend* Application::podcast_backend() const {
|
||||
return p_->podcast_backend_.get();
|
||||
}
|
||||
|
||||
PodcastDeleter* Application::podcast_deleter() const {
|
||||
return p_->podcast_deleter_.get();
|
||||
}
|
||||
|
||||
PodcastDownloader* Application::podcast_downloader() const {
|
||||
return p_->podcast_downloader_.get();
|
||||
}
|
||||
|
||||
PodcastUpdater* Application::podcast_updater() const {
|
||||
return p_->podcast_updater_.get();
|
||||
}
|
||||
|
||||
Scrobbler* Application::scrobbler() const { return p_->scrobbler_.get(); }
|
||||
|
||||
TagReaderClient* Application::tag_reader_client() const {
|
||||
return p_->tag_reader_client_.get();
|
||||
}
|
||||
|
||||
TaskManager* Application::task_manager() const {
|
||||
return p_->task_manager_.get();
|
||||
}
|
||||
|
@ -22,12 +22,15 @@
|
||||
#ifndef CORE_APPLICATION_H_
|
||||
#define CORE_APPLICATION_H_
|
||||
|
||||
#include "ui/settingsdialog.h"
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "ui/settingsdialog.h"
|
||||
|
||||
class AlbumCoverLoader;
|
||||
class Appearance;
|
||||
class ApplicationImpl;
|
||||
class CoverProviders;
|
||||
class CurrentArtLoader;
|
||||
class Database;
|
||||
@ -44,10 +47,10 @@ class NetworkRemote;
|
||||
class NetworkRemoteHelper;
|
||||
class Player;
|
||||
class PlaylistBackend;
|
||||
class PodcastDeleter;
|
||||
class PodcastDownloader;
|
||||
class PlaylistManager;
|
||||
class PodcastBackend;
|
||||
class PodcastDeleter;
|
||||
class PodcastDownloader;
|
||||
class PodcastUpdater;
|
||||
class Scrobbler;
|
||||
class TagReaderClient;
|
||||
@ -68,35 +71,32 @@ class Application : public QObject {
|
||||
QString language_without_region() const;
|
||||
void set_language_name(const QString& name) { language_name_ = name; }
|
||||
|
||||
TagReaderClient* tag_reader_client() const { return tag_reader_client_; }
|
||||
Database* database() const { return database_; }
|
||||
AlbumCoverLoader* album_cover_loader() const { return album_cover_loader_; }
|
||||
PlaylistBackend* playlist_backend() const { return playlist_backend_; }
|
||||
PodcastBackend* podcast_backend() const { return podcast_backend_; }
|
||||
Appearance* appearance() const { return appearance_; }
|
||||
CoverProviders* cover_providers() const { return cover_providers_; }
|
||||
TaskManager* task_manager() const { return task_manager_; }
|
||||
Player* player() const { return player_; }
|
||||
PlaylistManager* playlist_manager() const { return playlist_manager_; }
|
||||
CurrentArtLoader* current_art_loader() const { return current_art_loader_; }
|
||||
GlobalSearch* global_search() const { return global_search_; }
|
||||
InternetModel* internet_model() const { return internet_model_; }
|
||||
Library* library() const { return library_; }
|
||||
DeviceManager* device_manager() const { return device_manager_; }
|
||||
PodcastUpdater* podcast_updater() const { return podcast_updater_; }
|
||||
PodcastDeleter* podcast_deleter() const { return podcast_deleter_; }
|
||||
PodcastDownloader* podcast_downloader() const { return podcast_downloader_; }
|
||||
GPodderSync* gpodder_sync() const { return gpodder_sync_; }
|
||||
MoodbarLoader* moodbar_loader() const { return moodbar_loader_; }
|
||||
MoodbarController* moodbar_controller() const { return moodbar_controller_; }
|
||||
NetworkRemote* network_remote() const { return network_remote_; }
|
||||
NetworkRemoteHelper* network_remote_helper() const {
|
||||
return network_remote_helper_;
|
||||
}
|
||||
Scrobbler* scrobbler() const { return scrobbler_; }
|
||||
|
||||
AlbumCoverLoader* album_cover_loader() const;
|
||||
Appearance* appearance() const;
|
||||
CoverProviders* cover_providers() const;
|
||||
CurrentArtLoader* current_art_loader() const;
|
||||
Database* database() const;
|
||||
DeviceManager* device_manager() const;
|
||||
GlobalSearch* global_search() const;
|
||||
GPodderSync* gpodder_sync() const;
|
||||
InternetModel* internet_model() const;
|
||||
Library* library() const;
|
||||
LibraryBackend* library_backend() const;
|
||||
LibraryModel* library_model() const;
|
||||
MoodbarController* moodbar_controller() const;
|
||||
MoodbarLoader* moodbar_loader() const;
|
||||
NetworkRemoteHelper* network_remote_helper() const;
|
||||
NetworkRemote* network_remote() const;
|
||||
Player* player() const;
|
||||
PlaylistBackend* playlist_backend() const;
|
||||
PlaylistManager* playlist_manager() const;
|
||||
PodcastBackend* podcast_backend() const;
|
||||
PodcastDeleter* podcast_deleter() const;
|
||||
PodcastDownloader* podcast_downloader() const;
|
||||
PodcastUpdater* podcast_updater() const;
|
||||
Scrobbler* scrobbler() const;
|
||||
TagReaderClient* tag_reader_client() const;
|
||||
TaskManager* task_manager() const;
|
||||
|
||||
void MoveToNewThread(QObject* object);
|
||||
void MoveToThread(QObject* object, QThread* thread);
|
||||
@ -113,33 +113,7 @@ class Application : public QObject {
|
||||
|
||||
private:
|
||||
QString language_name_;
|
||||
|
||||
TagReaderClient* tag_reader_client_;
|
||||
Database* database_;
|
||||
AlbumCoverLoader* album_cover_loader_;
|
||||
PlaylistBackend* playlist_backend_;
|
||||
PodcastBackend* podcast_backend_;
|
||||
Appearance* appearance_;
|
||||
CoverProviders* cover_providers_;
|
||||
TaskManager* task_manager_;
|
||||
Player* player_;
|
||||
PlaylistManager* playlist_manager_;
|
||||
CurrentArtLoader* current_art_loader_;
|
||||
GlobalSearch* global_search_;
|
||||
InternetModel* internet_model_;
|
||||
Library* library_;
|
||||
DeviceManager* device_manager_;
|
||||
PodcastUpdater* podcast_updater_;
|
||||
PodcastDeleter* podcast_deleter_;
|
||||
PodcastDownloader* podcast_downloader_;
|
||||
GPodderSync* gpodder_sync_;
|
||||
MoodbarLoader* moodbar_loader_;
|
||||
MoodbarController* moodbar_controller_;
|
||||
NetworkRemote* network_remote_;
|
||||
NetworkRemoteHelper* network_remote_helper_;
|
||||
Scrobbler* scrobbler_;
|
||||
|
||||
QList<QObject*> objects_in_threads_;
|
||||
std::unique_ptr<ApplicationImpl> p_;
|
||||
QList<QThread*> threads_;
|
||||
};
|
||||
|
||||
|
@ -512,7 +512,6 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
|
||||
d->genre_ = QStringFromStdString(pb.genre());
|
||||
d->comment_ = QStringFromStdString(pb.comment());
|
||||
d->compilation_ = pb.compilation();
|
||||
d->playcount_ = pb.playcount();
|
||||
d->skipcount_ = pb.skipcount();
|
||||
d->lastplayed_ = pb.lastplayed();
|
||||
d->score_ = pb.score();
|
||||
@ -536,6 +535,10 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
|
||||
d->rating_ = pb.rating();
|
||||
}
|
||||
|
||||
if (pb.has_playcount()) {
|
||||
d->playcount_ = pb.playcount();
|
||||
}
|
||||
|
||||
InitArtManual();
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,7 @@
|
||||
#include "core/application.h"
|
||||
#include "covers/albumcoverloader.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
CurrentArtLoader::CurrentArtLoader(Application* app, QObject* parent)
|
||||
: QObject(parent),
|
||||
@ -34,7 +35,9 @@ CurrentArtLoader::CurrentArtLoader(Application* app, QObject* parent)
|
||||
id_(0) {
|
||||
options_.scale_output_image_ = false;
|
||||
options_.pad_output_image_ = false;
|
||||
options_.default_output_image_ = QImage(":nocover.png");
|
||||
QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
|
||||
options_.default_output_image_ = nocover.pixmap(nocover.availableSizes()
|
||||
.last()).toImage();
|
||||
|
||||
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)),
|
||||
SLOT(TempArtLoaded(quint64, QImage)));
|
||||
|
@ -44,6 +44,8 @@ GlobalSearch::GlobalSearch(Application* app, QObject* parent)
|
||||
|
||||
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)),
|
||||
SLOT(AlbumArtLoaded(quint64, QImage)));
|
||||
connect(this, SIGNAL(SearchAsyncSig(int,QString)),
|
||||
this, SLOT(DoSearchAsync(int,QString)));
|
||||
|
||||
ConnectProvider(url_provider_);
|
||||
}
|
||||
@ -82,6 +84,13 @@ void GlobalSearch::AddProvider(SearchProvider* provider) {
|
||||
|
||||
int GlobalSearch::SearchAsync(const QString& query) {
|
||||
const int id = next_id_++;
|
||||
|
||||
emit SearchAsyncSig(id, query);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
void GlobalSearch::DoSearchAsync(int id, const QString& query) {
|
||||
pending_search_providers_[id] = 0;
|
||||
|
||||
int timer_id = -1;
|
||||
@ -106,8 +115,6 @@ int GlobalSearch::SearchAsync(const QString& query) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
void GlobalSearch::CancelSearch(int id) {
|
||||
|
@ -62,9 +62,11 @@ class GlobalSearch : public QObject {
|
||||
bool is_provider_usable(SearchProvider* provider) const;
|
||||
|
||||
public slots:
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
signals:
|
||||
void SearchAsyncSig(int id, const QString& query);
|
||||
void ResultsAvailable(int id, const SearchProvider::ResultList& results);
|
||||
void ProviderSearchFinished(int id, const SearchProvider* provider);
|
||||
void SearchFinished(int id);
|
||||
@ -79,6 +81,7 @@ signals:
|
||||
void timerEvent(QTimerEvent* e);
|
||||
|
||||
private slots:
|
||||
void DoSearchAsync(int id, const QString& query);
|
||||
void ResultsAvailableSlot(int id, SearchProvider::ResultList results);
|
||||
void SearchFinishedSlot(int id);
|
||||
|
||||
|
@ -33,8 +33,10 @@ GlobalSearchModel::GlobalSearchModel(GlobalSearch* engine, QObject* parent)
|
||||
group_by_[1] = LibraryModel::GroupBy_Album;
|
||||
group_by_[2] = LibraryModel::GroupBy_None;
|
||||
|
||||
no_cover_icon_ = QPixmap(":nocover.png").scaled(
|
||||
LibraryModel::kPrettyCoverSize, LibraryModel::kPrettyCoverSize,
|
||||
QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
|
||||
no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(
|
||||
LibraryModel::kPrettyCoverSize,
|
||||
LibraryModel::kPrettyCoverSize,
|
||||
Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
const char* Geolocator::kUrl = "http://data.clementine-player.org/geolocate";
|
||||
const char* Geolocator::kUrl = "https://data.clementine-player.org/geolocate";
|
||||
|
||||
using std::numeric_limits;
|
||||
|
||||
|
@ -42,10 +42,32 @@ InternetService::InternetService(const QString& name, Application* app,
|
||||
app_(app),
|
||||
model_(model),
|
||||
name_(name),
|
||||
append_to_playlist_(nullptr),
|
||||
replace_playlist_(nullptr),
|
||||
open_in_new_playlist_(nullptr),
|
||||
separator_(nullptr) {}
|
||||
append_to_playlist_([&]() {
|
||||
QAction* action = new QAction(
|
||||
IconLoader::Load("media-playback-start", IconLoader::Base),
|
||||
tr("Append to current playlist"), nullptr);
|
||||
connect(action, SIGNAL(triggered()), this, SLOT(AppendToPlaylist()));
|
||||
return action;
|
||||
}),
|
||||
replace_playlist_([&]() {
|
||||
QAction* action = new QAction(
|
||||
IconLoader::Load("media-playback-start", IconLoader::Base),
|
||||
tr("Replace current playlist"), nullptr);
|
||||
connect(action, SIGNAL(triggered()), this, SLOT(ReplacePlaylist()));
|
||||
return action;
|
||||
}),
|
||||
open_in_new_playlist_([&]() {
|
||||
QAction* action =
|
||||
new QAction(IconLoader::Load("document-new", IconLoader::Base),
|
||||
tr("Open in new playlist"), nullptr);
|
||||
connect(action, SIGNAL(triggered()), this, SLOT(OpenInNewPlaylist()));
|
||||
return action;
|
||||
}),
|
||||
separator_([]() {
|
||||
QAction* action = new QAction(nullptr);
|
||||
action->setSeparator(true);
|
||||
return action;
|
||||
}) {}
|
||||
|
||||
void InternetService::ShowUrlBox(const QString& title, const QString& url) {
|
||||
QMessageBox url_box;
|
||||
@ -64,50 +86,21 @@ void InternetService::ShowUrlBox(const QString& title, const QString& url) {
|
||||
}
|
||||
|
||||
QList<QAction*> InternetService::GetPlaylistActions() {
|
||||
if (!separator_) {
|
||||
separator_ = new QAction(this);
|
||||
separator_->setSeparator(true);
|
||||
}
|
||||
|
||||
return QList<QAction*>() << GetAppendToPlaylistAction()
|
||||
<< GetReplacePlaylistAction()
|
||||
<< GetOpenInNewPlaylistAction() << separator_;
|
||||
<< GetOpenInNewPlaylistAction() << separator_.get();
|
||||
}
|
||||
|
||||
QAction* InternetService::GetAppendToPlaylistAction() {
|
||||
if (!append_to_playlist_) {
|
||||
append_to_playlist_ = new QAction(IconLoader::Load("media-playback-start",
|
||||
IconLoader::Base),
|
||||
tr("Append to current playlist"), this);
|
||||
connect(append_to_playlist_, SIGNAL(triggered()), this,
|
||||
SLOT(AppendToPlaylist()));
|
||||
}
|
||||
|
||||
return append_to_playlist_;
|
||||
return append_to_playlist_.get();
|
||||
}
|
||||
|
||||
QAction* InternetService::GetReplacePlaylistAction() {
|
||||
if (!replace_playlist_) {
|
||||
replace_playlist_ = new QAction(IconLoader::Load("media-playback-start",
|
||||
IconLoader::Base),
|
||||
tr("Replace current playlist"), this);
|
||||
connect(replace_playlist_, SIGNAL(triggered()), this,
|
||||
SLOT(ReplacePlaylist()));
|
||||
}
|
||||
|
||||
return replace_playlist_;
|
||||
return replace_playlist_.get();
|
||||
}
|
||||
|
||||
QAction* InternetService::GetOpenInNewPlaylistAction() {
|
||||
if (!open_in_new_playlist_) {
|
||||
open_in_new_playlist_ = new QAction(IconLoader::Load("document-new",
|
||||
IconLoader::Base),
|
||||
tr("Open in new playlist"), this);
|
||||
connect(open_in_new_playlist_, SIGNAL(triggered()), this,
|
||||
SLOT(OpenInNewPlaylist()));
|
||||
}
|
||||
|
||||
return open_in_new_playlist_;
|
||||
return open_in_new_playlist_.get();
|
||||
}
|
||||
|
||||
void InternetService::AddItemToPlaylist(const QModelIndex& index,
|
||||
|
@ -23,10 +23,12 @@
|
||||
#ifndef INTERNET_CORE_INTERNETSERVICE_H_
|
||||
#define INTERNET_CORE_INTERNETSERVICE_H_
|
||||
|
||||
#include <QAction>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/lazy.h"
|
||||
#include "core/song.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
#include "smartplaylists/generator.h"
|
||||
@ -135,10 +137,10 @@ class InternetService : public QObject {
|
||||
InternetModel* model_;
|
||||
QString name_;
|
||||
|
||||
QAction* append_to_playlist_;
|
||||
QAction* replace_playlist_;
|
||||
QAction* open_in_new_playlist_;
|
||||
QAction* separator_;
|
||||
Lazy<QAction> append_to_playlist_;
|
||||
Lazy<QAction> replace_playlist_;
|
||||
Lazy<QAction> open_in_new_playlist_;
|
||||
Lazy<QAction> separator_;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(InternetService*);
|
||||
|
@ -32,7 +32,6 @@ InternetView::InternetView(QWidget* parent) : AutoExpandingTreeView(parent) {
|
||||
SetExpandOnReset(false);
|
||||
setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
setAnimated(true);
|
||||
}
|
||||
|
||||
void InternetView::contextMenuEvent(QContextMenuEvent* e) {
|
||||
@ -47,11 +46,6 @@ void InternetView::contextMenuEvent(QContextMenuEvent* e) {
|
||||
e->globalPos());
|
||||
}
|
||||
|
||||
void InternetView::currentChanged(const QModelIndex& current,
|
||||
const QModelIndex&) {
|
||||
emit CurrentIndexChanged(current);
|
||||
}
|
||||
|
||||
void InternetView::setModel(QAbstractItemModel* model) {
|
||||
AutoExpandingTreeView::setModel(model);
|
||||
|
||||
|
@ -33,11 +33,7 @@ class InternetView : public AutoExpandingTreeView {
|
||||
void contextMenuEvent(QContextMenuEvent* e);
|
||||
|
||||
// QTreeView
|
||||
void currentChanged(const QModelIndex& current, const QModelIndex& previous);
|
||||
void setModel(QAbstractItemModel* model);
|
||||
|
||||
signals:
|
||||
void CurrentIndexChanged(const QModelIndex& index);
|
||||
};
|
||||
|
||||
#endif // INTERNET_CORE_INTERNETVIEW_H_
|
||||
|
@ -53,7 +53,6 @@ DigitallyImportedServiceBase::DigitallyImportedServiceBase(
|
||||
api_service_name_(api_service_name),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
url_handler_(new DigitallyImportedUrlHandler(app, this)),
|
||||
basic_audio_type_(1),
|
||||
premium_audio_type_(2),
|
||||
has_premium_(has_premium),
|
||||
root_(nullptr),
|
||||
@ -66,10 +65,6 @@ DigitallyImportedServiceBase::DigitallyImportedServiceBase(
|
||||
model->app()->global_search()->AddProvider(
|
||||
new DigitallyImportedSearchProvider(this, app_, this));
|
||||
|
||||
basic_playlists_ << "http://%1/public3/%2.pls"
|
||||
<< "http://%1/public1/%2.pls"
|
||||
<< "http://%1/public5/%2.asx";
|
||||
|
||||
premium_playlists_ << "http://%1/premium_high/%2.pls?hash=%3"
|
||||
<< "http://%1/premium_medium/%2.pls?hash=%3"
|
||||
<< "http://%1/premium/%2.pls?hash=%3"
|
||||
@ -130,14 +125,18 @@ void DigitallyImportedServiceBase::RefreshStreamsFinished(QNetworkReply* reply,
|
||||
void DigitallyImportedServiceBase::PopulateStreams() {
|
||||
if (root_->hasChildren()) root_->removeRows(0, root_->rowCount());
|
||||
|
||||
if (!is_premium_account()) {
|
||||
ShowSettingsDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add each stream to the model
|
||||
for (const DigitallyImportedClient::Channel& channel : saved_channels_) {
|
||||
Song song;
|
||||
SongFromChannel(channel, &song);
|
||||
|
||||
QStandardItem* item =
|
||||
new QStandardItem(IconLoader::Load("icon_radio",
|
||||
IconLoader::Lastfm), song.title());
|
||||
QStandardItem* item = new QStandardItem(
|
||||
IconLoader::Load("icon_radio", IconLoader::Lastfm), song.title());
|
||||
item->setData(channel.description_, Qt::ToolTipRole);
|
||||
item->setData(InternetModel::PlayBehaviour_SingleItem,
|
||||
InternetModel::Role_PlayBehaviour);
|
||||
@ -162,7 +161,6 @@ void DigitallyImportedServiceBase::ReloadSettings() {
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
basic_audio_type_ = s.value("basic_audio_type", 1).toInt();
|
||||
premium_audio_type_ = s.value("premium_audio_type", 2).toInt();
|
||||
username_ = s.value("username").toString();
|
||||
listen_hash_ = s.value("listen_hash").toString();
|
||||
@ -224,12 +222,8 @@ void DigitallyImportedServiceBase::LoadStation(const QString& key) {
|
||||
// Replace "www." with "listen." in the hostname.
|
||||
const QString host = "listen." + homepage_url_.host().remove("www.");
|
||||
|
||||
if (is_premium_account()) {
|
||||
playlist_url = QUrl(
|
||||
premium_playlists_[premium_audio_type_].arg(host, key, listen_hash_));
|
||||
} else {
|
||||
playlist_url = QUrl(basic_playlists_[basic_audio_type_].arg(host, key));
|
||||
}
|
||||
|
||||
qLog(Debug) << "Getting playlist URL" << playlist_url;
|
||||
|
||||
@ -241,32 +235,28 @@ void DigitallyImportedServiceBase::LoadStation(const QString& key) {
|
||||
DigitallyImportedService::DigitallyImportedService(Application* app,
|
||||
InternetModel* model,
|
||||
QObject* parent)
|
||||
: DigitallyImportedServiceBase("DigitallyImported", "Digitally Imported",
|
||||
QUrl("http://www.di.fm"),
|
||||
IconLoader::Load("digitallyimported",
|
||||
IconLoader::Provider),
|
||||
"di", app, model, true, parent) {}
|
||||
: DigitallyImportedServiceBase(
|
||||
"DigitallyImported", "Digitally Imported", QUrl("http://www.di.fm"),
|
||||
IconLoader::Load("digitallyimported", IconLoader::Provider), "di",
|
||||
app, model, true, parent) {}
|
||||
|
||||
RadioTunesService::RadioTunesService(Application* app, InternetModel* model,
|
||||
QObject* parent)
|
||||
: DigitallyImportedServiceBase("RadioTunes", "RadioTunes.com",
|
||||
QUrl("http://www.radiotunes.com/"),
|
||||
IconLoader::Load("radiotunes",
|
||||
IconLoader::Provider),
|
||||
"radiotunes", app, model, true, parent) {}
|
||||
: DigitallyImportedServiceBase(
|
||||
"RadioTunes", "RadioTunes.com", QUrl("http://www.radiotunes.com/"),
|
||||
IconLoader::Load("radiotunes", IconLoader::Provider), "radiotunes",
|
||||
app, model, true, parent) {}
|
||||
|
||||
JazzRadioService::JazzRadioService(Application* app, InternetModel* model,
|
||||
QObject* parent)
|
||||
: DigitallyImportedServiceBase("JazzRadio", "JAZZRADIO.com",
|
||||
QUrl("http://www.jazzradio.com"),
|
||||
IconLoader::Load("jazzradio",
|
||||
IconLoader::Provider),
|
||||
"jazzradio", app, model, true, parent) {}
|
||||
: DigitallyImportedServiceBase(
|
||||
"JazzRadio", "JAZZRADIO.com", QUrl("http://www.jazzradio.com"),
|
||||
IconLoader::Load("jazzradio", IconLoader::Provider), "jazzradio", app,
|
||||
model, true, parent) {}
|
||||
|
||||
RockRadioService::RockRadioService(Application* app, InternetModel* model,
|
||||
QObject* parent)
|
||||
: DigitallyImportedServiceBase("RockRadio", "ROCKRADIO.com",
|
||||
QUrl("http://www.rockradio.com"),
|
||||
IconLoader::Load("rockradio",
|
||||
IconLoader::Provider),
|
||||
"rockradio", app, model, false, parent) {}
|
||||
: DigitallyImportedServiceBase(
|
||||
"RockRadio", "ROCKRADIO.com", QUrl("http://www.rockradio.com"),
|
||||
IconLoader::Load("rockradio", IconLoader::Provider), "rockradio", app,
|
||||
model, false, parent) {}
|
||||
|
@ -89,13 +89,11 @@ signals:
|
||||
QString service_description_;
|
||||
QString api_service_name_;
|
||||
|
||||
QStringList basic_playlists_;
|
||||
QStringList premium_playlists_;
|
||||
|
||||
QNetworkAccessManager* network_;
|
||||
DigitallyImportedUrlHandler* url_handler_;
|
||||
|
||||
int basic_audio_type_;
|
||||
int premium_audio_type_;
|
||||
QString username_;
|
||||
QString listen_hash_;
|
||||
|
@ -45,9 +45,7 @@ DigitallyImportedSettingsPage::DigitallyImportedSettingsPage(
|
||||
ui_->login_state->AddCredentialField(ui_->password);
|
||||
ui_->login_state->AddCredentialGroup(ui_->credential_group);
|
||||
|
||||
ui_->login_state->SetAccountTypeText(
|
||||
tr("You can listen for free without an account, but Premium members can "
|
||||
"listen to higher quality streams without advertisements."));
|
||||
ui_->login_state->SetAccountTypeText(tr("A premium account is required"));
|
||||
ui_->login_state->SetAccountTypeVisible(true);
|
||||
}
|
||||
|
||||
|
@ -56,6 +56,12 @@ UrlHandler::LoadResult DigitallyImportedUrlHandler::StartLoading(
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (!service_->is_premium_account()) {
|
||||
service_->StreamError(tr("A premium account is required"));
|
||||
ret.type_ = LoadResult::NoMoreTracks;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Start loading the station
|
||||
const QString key = url.host();
|
||||
qLog(Info) << "Loading station" << key;
|
||||
|
@ -67,21 +67,24 @@ const char* LastFMService::kSettingsGroup = "Last.fm";
|
||||
const char* LastFMService::kAudioscrobblerClientId = "tng";
|
||||
const char* LastFMService::kApiKey = "75d20fb472be99275392aefa2760ea09";
|
||||
const char* LastFMService::kSecret = "d3072b60ae626be12be69448f5c46e70";
|
||||
const char* LastFMService::kAuthLoginUrl =
|
||||
"https://www.last.fm/api/auth/?api_key=%1&token=%2";
|
||||
|
||||
LastFMService::LastFMService(Application* app, QObject* parent)
|
||||
: Scrobbler(parent),
|
||||
scrobbler_(nullptr),
|
||||
already_scrobbled_(false),
|
||||
scrobbling_enabled_(false),
|
||||
connection_problems_(false),
|
||||
app_(app) {
|
||||
#ifdef HAVE_LIBLASTFM1
|
||||
lastfm::ws::setScheme(lastfm::ws::Https);
|
||||
#endif
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
// we emit the signal the first time to be sure the buttons are in the right
|
||||
// state
|
||||
emit ScrobblingEnabledChanged(scrobbling_enabled_);
|
||||
|
||||
app_->cover_providers()->AddProvider(new LastFmCoverProvider(this));
|
||||
}
|
||||
|
||||
LastFMService::~LastFMService() {}
|
||||
@ -120,13 +123,32 @@ bool LastFMService::IsSubscriber() const {
|
||||
return settings.value("Subscriber", false).toBool();
|
||||
}
|
||||
|
||||
void LastFMService::Authenticate(const QString& username,
|
||||
const QString& password) {
|
||||
void LastFMService::GetToken() {
|
||||
QMap<QString, QString> params;
|
||||
params["method"] = "auth.getMobileSession";
|
||||
params["username"] = username;
|
||||
params["authToken"] =
|
||||
lastfm::md5((username + lastfm::md5(password.toUtf8())).toUtf8());
|
||||
params["method"] = "auth.getToken";
|
||||
QNetworkReply* reply = lastfm::ws::post(params);
|
||||
NewClosure(reply, SIGNAL(finished()), this,
|
||||
SLOT(GetTokenReplyFinished(QNetworkReply*)), reply);
|
||||
}
|
||||
|
||||
void LastFMService::GetTokenReplyFinished(QNetworkReply* reply) {
|
||||
reply->deleteLater();
|
||||
|
||||
// Parse the reply
|
||||
lastfm::XmlQuery lfm(lastfm::compat::EmptyXmlQuery());
|
||||
if (lastfm::compat::ParseQuery(reply->readAll(), &lfm)) {
|
||||
QString token = lfm["token"].text();
|
||||
|
||||
emit TokenReceived(true, token);
|
||||
} else {
|
||||
emit TokenReceived(false, lfm["error"].text().trimmed());
|
||||
}
|
||||
}
|
||||
|
||||
void LastFMService::Authenticate(const QString& token) {
|
||||
QMap<QString, QString> params;
|
||||
params["method"] = "auth.getSession";
|
||||
params["token"] = token;
|
||||
|
||||
QNetworkReply* reply = lastfm::ws::post(params);
|
||||
NewClosure(reply, SIGNAL(finished()), this,
|
||||
@ -134,17 +156,6 @@ void LastFMService::Authenticate(const QString& username,
|
||||
// If we need more detailed error reporting, handle error(NetworkError) signal
|
||||
}
|
||||
|
||||
void LastFMService::SignOut() {
|
||||
lastfm::ws::Username.clear();
|
||||
lastfm::ws::SessionKey.clear();
|
||||
|
||||
QSettings settings;
|
||||
settings.beginGroup(kSettingsGroup);
|
||||
|
||||
settings.setValue("Username", QString());
|
||||
settings.setValue("Session", QString());
|
||||
}
|
||||
|
||||
void LastFMService::AuthenticateReplyFinished(QNetworkReply* reply) {
|
||||
reply->deleteLater();
|
||||
|
||||
@ -168,12 +179,22 @@ void LastFMService::AuthenticateReplyFinished(QNetworkReply* reply) {
|
||||
}
|
||||
|
||||
// Invalidate the scrobbler - it will get recreated later
|
||||
delete scrobbler_;
|
||||
scrobbler_ = nullptr;
|
||||
scrobbler_.reset(nullptr);
|
||||
|
||||
emit AuthenticationComplete(true, QString());
|
||||
}
|
||||
|
||||
void LastFMService::SignOut() {
|
||||
lastfm::ws::Username.clear();
|
||||
lastfm::ws::SessionKey.clear();
|
||||
|
||||
QSettings settings;
|
||||
settings.beginGroup(kSettingsGroup);
|
||||
|
||||
settings.setValue("Username", QString());
|
||||
settings.setValue("Session", QString());
|
||||
}
|
||||
|
||||
void LastFMService::UpdateSubscriberStatus() {
|
||||
QMap<QString, QString> params;
|
||||
params["method"] = "user.getInfo";
|
||||
@ -261,16 +282,16 @@ bool LastFMService::InitScrobbler() {
|
||||
if (!IsAuthenticated() || !IsScrobblingEnabled()) return false;
|
||||
|
||||
if (!scrobbler_)
|
||||
scrobbler_ = new lastfm::Audioscrobbler(kAudioscrobblerClientId);
|
||||
scrobbler_.reset(new lastfm::Audioscrobbler(kAudioscrobblerClientId));
|
||||
|
||||
// reemit the signal since the sender is private
|
||||
#ifdef HAVE_LIBLASTFM1
|
||||
connect(scrobbler_, SIGNAL(scrobblesSubmitted(QList<lastfm::Track>)),
|
||||
connect(scrobbler_.get(), SIGNAL(scrobblesSubmitted(QList<lastfm::Track>)),
|
||||
SIGNAL(ScrobbleSubmitted()));
|
||||
connect(scrobbler_, SIGNAL(nowPlayingError(int, QString)),
|
||||
connect(scrobbler_.get(), SIGNAL(nowPlayingError(int, QString)),
|
||||
SIGNAL(ScrobbleError(int)));
|
||||
#else
|
||||
connect(scrobbler_, SIGNAL(status(int)), SLOT(ScrobblerStatus(int)));
|
||||
connect(scrobbler_.get(), SIGNAL(status(int)), SLOT(ScrobblerStatus(int)));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ class LastFMService : public Scrobbler {
|
||||
static const char* kAudioscrobblerClientId;
|
||||
static const char* kApiKey;
|
||||
static const char* kSecret;
|
||||
static const char* kAuthLoginUrl;
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
@ -68,7 +69,8 @@ class LastFMService : public Scrobbler {
|
||||
bool PreferAlbumArtist() const { return prefer_albumartist_; }
|
||||
bool HasConnectionProblems() const { return connection_problems_; }
|
||||
|
||||
void Authenticate(const QString& username, const QString& password);
|
||||
void GetToken();
|
||||
void Authenticate(const QString& token);
|
||||
void SignOut();
|
||||
void UpdateSubscriberStatus();
|
||||
|
||||
@ -81,6 +83,7 @@ class LastFMService : public Scrobbler {
|
||||
void ToggleScrobbling();
|
||||
|
||||
signals:
|
||||
void TokenReceived(bool success, const QString& token);
|
||||
void AuthenticationComplete(bool success, const QString& error_message);
|
||||
void ScrobblingEnabledChanged(bool value);
|
||||
void ButtonVisibilityChanged(bool value);
|
||||
@ -94,6 +97,7 @@ class LastFMService : public Scrobbler {
|
||||
void SavedItemsChanged();
|
||||
|
||||
private slots:
|
||||
void GetTokenReplyFinished(QNetworkReply* reply);
|
||||
void AuthenticateReplyFinished(QNetworkReply* reply);
|
||||
void UpdateSubscriberStatusFinished(QNetworkReply* reply);
|
||||
|
||||
@ -107,7 +111,7 @@ class LastFMService : public Scrobbler {
|
||||
static QUrl FixupUrl(const QUrl& url);
|
||||
|
||||
private:
|
||||
lastfm::Audioscrobbler* scrobbler_;
|
||||
std::unique_ptr<lastfm::Audioscrobbler> scrobbler_;
|
||||
lastfm::Track last_track_;
|
||||
lastfm::Track next_metadata_;
|
||||
bool already_scrobbled_;
|
||||
|
@ -24,6 +24,7 @@
|
||||
|
||||
#include <lastfm5/ws.h>
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QMessageBox>
|
||||
#include <QSettings>
|
||||
|
||||
@ -42,17 +43,16 @@ LastFMSettingsPage::LastFMSettingsPage(SettingsDialog* dialog)
|
||||
// Icons
|
||||
setWindowIcon(IconLoader::Load("lastfm", IconLoader::Provider));
|
||||
|
||||
connect(service_, SIGNAL(TokenReceived(bool,QString)),
|
||||
SLOT(TokenReceived(bool,QString)));
|
||||
connect(service_, SIGNAL(AuthenticationComplete(bool, QString)),
|
||||
SLOT(AuthenticationComplete(bool, QString)));
|
||||
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout()));
|
||||
connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(Login()));
|
||||
connect(ui_->login, SIGNAL(clicked()), SLOT(Login()));
|
||||
|
||||
ui_->login_state->AddCredentialField(ui_->username);
|
||||
ui_->login_state->AddCredentialField(ui_->password);
|
||||
ui_->login_state->AddCredentialGroup(ui_->groupBox);
|
||||
ui_->login_state->AddCredentialGroup(ui_->login_container);
|
||||
|
||||
ui_->username->setMinimumWidth(QFontMetrics(QFont()).width("WWWWWWWWWWWW"));
|
||||
resize(sizeHint());
|
||||
}
|
||||
|
||||
@ -62,7 +62,22 @@ void LastFMSettingsPage::Login() {
|
||||
waiting_for_auth_ = true;
|
||||
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
|
||||
service_->Authenticate(ui_->username->text(), ui_->password->text());
|
||||
service_->GetToken();
|
||||
}
|
||||
|
||||
void LastFMSettingsPage::TokenReceived(bool success, const QString &token) {
|
||||
if (!success) {
|
||||
QMessageBox::warning(this, tr("Last.fm authentication failed"), token);
|
||||
return;
|
||||
}
|
||||
|
||||
QString url = QString(LastFMService::kAuthLoginUrl).arg(LastFMService::kApiKey, token);
|
||||
QDesktopServices::openUrl(QUrl(url));
|
||||
|
||||
QMessageBox::information(this, tr("Last.fm authentication"),
|
||||
tr("Click Ok once you authenticated Clementine in your last.fm account."));
|
||||
|
||||
service_->Authenticate(token);
|
||||
}
|
||||
|
||||
void LastFMSettingsPage::AuthenticationComplete(bool success,
|
||||
@ -72,8 +87,6 @@ void LastFMSettingsPage::AuthenticationComplete(bool success,
|
||||
waiting_for_auth_ = false;
|
||||
|
||||
if (success) {
|
||||
// Clear password just to be sure
|
||||
ui_->password->clear();
|
||||
// Save settings
|
||||
Save();
|
||||
} else {
|
||||
@ -109,8 +122,6 @@ void LastFMSettingsPage::Save() {
|
||||
}
|
||||
|
||||
void LastFMSettingsPage::Logout() {
|
||||
ui_->username->clear();
|
||||
ui_->password->clear();
|
||||
RefreshControls(false);
|
||||
|
||||
service_->SignOut();
|
||||
|
@ -38,6 +38,7 @@ class LastFMSettingsPage : public SettingsPage {
|
||||
|
||||
private slots:
|
||||
void Login();
|
||||
void TokenReceived(bool success, const QString& token);
|
||||
void AuthenticationComplete(bool success, const QString& error_message);
|
||||
void Logout();
|
||||
|
||||
|
@ -18,26 +18,19 @@
|
||||
<widget class="LoginStateWidget" name="login_state" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Account details</string>
|
||||
<widget class="QWidget" name="login_container" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="leftMargin">
|
||||
<number>28</number>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Last.fm username</string>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="username"/>
|
||||
</item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="login">
|
||||
<property name="text">
|
||||
@ -45,19 +38,28 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Last.fm password</string>
|
||||
<string>Clicking the Login button will open a web browser. You should return to Clementine after you have logged in.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="password">
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -112,6 +114,9 @@
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::MinimumExpanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
@ -131,9 +136,6 @@
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>username</tabstop>
|
||||
<tabstop>password</tabstop>
|
||||
<tabstop>login</tabstop>
|
||||
<tabstop>scrobble</tabstop>
|
||||
<tabstop>love_ban_</tabstop>
|
||||
<tabstop>scrobble_button</tabstop>
|
||||
|
@ -291,7 +291,12 @@ void PodcastParser::ParseOutline(QXmlStreamReader* reader,
|
||||
// Parse the feed and add it to this container
|
||||
Podcast podcast;
|
||||
podcast.set_description(attributes.value("description").toString());
|
||||
podcast.set_title(attributes.value("text").toString());
|
||||
|
||||
QString title = attributes.value("title").toString();
|
||||
if (title.isEmpty()) {
|
||||
title = attributes.value("text").toString();
|
||||
}
|
||||
podcast.set_title(title);
|
||||
podcast.set_image_url_large(QUrl::fromEncoded(
|
||||
attributes.value("imageHref").toString().toLatin1()));
|
||||
podcast.set_url(QUrl::fromEncoded(
|
||||
|
@ -354,6 +354,10 @@ void SoundCloudService::EnsureMenuCreated() {
|
||||
context_menu_->addAction(IconLoader::Load("download", IconLoader::Base),
|
||||
tr("Open %1 in browser").arg("soundcloud.com"),
|
||||
this, SLOT(Homepage()));
|
||||
context_menu_->addSeparator();
|
||||
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
|
||||
tr("Configure SoundCloud..."),
|
||||
this, SLOT(ShowConfig()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,8 +82,8 @@ void SpotifyBlobDownloader::Start() {
|
||||
const QStringList filenames =
|
||||
QStringList() << "blob"
|
||||
<< "blob" + QString(kSignatureSuffix)
|
||||
<< "libspotify.so.12.1.45"
|
||||
<< "libspotify.so.12.1.45" + QString(kSignatureSuffix);
|
||||
<< "libspotify.so.12.1.51"
|
||||
<< "libspotify.so.12.1.51" + QString(kSignatureSuffix);
|
||||
|
||||
for (const QString& filename : filenames) {
|
||||
const QUrl url(SpotifyService::kBlobDownloadUrl + version_ + "/" +
|
||||
|
@ -63,7 +63,7 @@ Q_DECLARE_METATYPE(QStandardItem*);
|
||||
const char* SpotifyService::kServiceName = "Spotify";
|
||||
const char* SpotifyService::kSettingsGroup = "Spotify";
|
||||
const char* SpotifyService::kBlobDownloadUrl =
|
||||
"http://spotify.clementine-player.org/";
|
||||
"https://spotify.clementine-player.org/";
|
||||
const int SpotifyService::kSearchDelayMsec = 400;
|
||||
|
||||
SpotifyService::SpotifyService(Application* app, InternetModel* parent)
|
||||
@ -413,7 +413,8 @@ void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) {
|
||||
search_->setData(InternetModel::PlayBehaviour_MultipleItems,
|
||||
InternetModel::Role_PlayBehaviour);
|
||||
|
||||
starred_ = new QStandardItem(QIcon(":/star-on.png"), tr("Starred"));
|
||||
starred_ = new QStandardItem(IconLoader::Load("star-on", IconLoader::Other),
|
||||
tr("Starred"));
|
||||
starred_->setData(Type_StarredPlaylist, InternetModel::Role_Type);
|
||||
starred_->setData(true, InternetModel::Role_CanLazyLoad);
|
||||
starred_->setData(InternetModel::PlayBehaviour_MultipleItems,
|
||||
@ -610,7 +611,8 @@ QList<QAction*> SpotifyService::playlistitem_actions(const Song& song) {
|
||||
}
|
||||
|
||||
QAction* add_to_starred =
|
||||
new QAction(QIcon(":/star-on.png"), tr("Add to Spotify starred"), this);
|
||||
new QAction(IconLoader::Load("star-on", IconLoader::Other),
|
||||
tr("Add to Spotify starred"), this);
|
||||
connect(add_to_starred, SIGNAL(triggered()),
|
||||
SLOT(AddCurrentSongToStarredPlaylist()));
|
||||
playlistitem_actions_.append(add_to_starred);
|
||||
|
@ -579,19 +579,21 @@ void VkService::ChangeConnectionState(Vreen::Client::State state) {
|
||||
switch (state) {
|
||||
case Vreen::Client::StateOnline:
|
||||
emit LoginSuccess(true);
|
||||
UpdateRoot();
|
||||
break;
|
||||
|
||||
case Vreen::Client::StateInvalid:
|
||||
case Vreen::Client::StateOffline:
|
||||
emit LoginSuccess(false);
|
||||
UpdateRoot();
|
||||
break;
|
||||
case Vreen::Client::StateConnecting:
|
||||
break;
|
||||
return;
|
||||
default:
|
||||
qLog(Error) << "Wrong connection state " << state;
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root_item_->data(InternetModel::Role_CanLazyLoad).toBool()) {
|
||||
UpdateRoot();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@
|
||||
#include "ui/settingsdialog.h"
|
||||
|
||||
#include <QActionGroup>
|
||||
#include <QInputDialog>
|
||||
#include <QKeyEvent>
|
||||
#include <QMenu>
|
||||
#include <QRegExp>
|
||||
@ -100,11 +101,17 @@ LibraryFilterWidget::LibraryFilterWidget(QWidget* parent)
|
||||
connect(group_by_group_, SIGNAL(triggered(QAction*)),
|
||||
SLOT(GroupByClicked(QAction*)));
|
||||
|
||||
connect(ui_->save_grouping, SIGNAL(triggered()), this, SLOT(SaveGroupBy()));
|
||||
connect(ui_->manage_groupings, SIGNAL(triggered()), this,
|
||||
SLOT(ShowGroupingManager()));
|
||||
|
||||
// Library config menu
|
||||
library_menu_ = new QMenu(tr("Display options"), this);
|
||||
library_menu_->setIcon(ui_->options->icon());
|
||||
library_menu_->addMenu(filter_age_menu_);
|
||||
library_menu_->addMenu(group_by_menu_);
|
||||
library_menu_->addAction(ui_->save_grouping);
|
||||
library_menu_->addAction(ui_->manage_groupings);
|
||||
library_menu_->addSeparator();
|
||||
ui_->options->setMenu(library_menu_);
|
||||
|
||||
@ -114,6 +121,22 @@ LibraryFilterWidget::LibraryFilterWidget(QWidget* parent)
|
||||
|
||||
LibraryFilterWidget::~LibraryFilterWidget() { delete ui_; }
|
||||
|
||||
void LibraryFilterWidget::UpdateGroupByActions() {
|
||||
if (group_by_group_) {
|
||||
disconnect(group_by_group_, 0, 0, 0);
|
||||
delete group_by_group_;
|
||||
}
|
||||
|
||||
group_by_group_ = CreateGroupByActions(this);
|
||||
group_by_menu_->clear();
|
||||
group_by_menu_->addActions(group_by_group_->actions());
|
||||
connect(group_by_group_, SIGNAL(triggered(QAction*)),
|
||||
SLOT(GroupByClicked(QAction*)));
|
||||
if (model_) {
|
||||
CheckCurrentGrouping(model_->GetGroupBy());
|
||||
}
|
||||
}
|
||||
|
||||
QActionGroup* LibraryFilterWidget::CreateGroupByActions(QObject* parent) {
|
||||
QActionGroup* ret = new QActionGroup(parent);
|
||||
ret->addAction(CreateGroupByAction(
|
||||
@ -139,6 +162,27 @@ QActionGroup* LibraryFilterWidget::CreateGroupByActions(QObject* parent) {
|
||||
LibraryModel::Grouping(LibraryModel::GroupBy_Genre,
|
||||
LibraryModel::GroupBy_Artist,
|
||||
LibraryModel::GroupBy_Album)));
|
||||
|
||||
QAction* sep1 = new QAction(parent);
|
||||
sep1->setSeparator(true);
|
||||
ret->addAction(sep1);
|
||||
|
||||
// read saved groupings
|
||||
QSettings s;
|
||||
s.beginGroup(LibraryModel::kSavedGroupingsSettingsGroup);
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
LibraryModel::Grouping g;
|
||||
ds >> g;
|
||||
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
|
||||
}
|
||||
|
||||
QAction* sep2 = new QAction(parent);
|
||||
sep2->setSeparator(true);
|
||||
ret->addAction(sep2);
|
||||
|
||||
ret->addAction(CreateGroupByAction(tr("Advanced grouping..."), parent,
|
||||
LibraryModel::Grouping()));
|
||||
|
||||
@ -158,6 +202,24 @@ QAction* LibraryFilterWidget::CreateGroupByAction(
|
||||
return ret;
|
||||
}
|
||||
|
||||
void LibraryFilterWidget::SaveGroupBy() {
|
||||
QString text =
|
||||
QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||
if (!text.isEmpty() && model_) {
|
||||
model_->SaveGrouping(text);
|
||||
UpdateGroupByActions();
|
||||
}
|
||||
}
|
||||
|
||||
void LibraryFilterWidget::ShowGroupingManager() {
|
||||
if (!groupings_manager_) {
|
||||
groupings_manager_.reset(new SavedGroupingManager);
|
||||
}
|
||||
groupings_manager_->SetFilter(this);
|
||||
groupings_manager_->UpdateModel();
|
||||
groupings_manager_->show();
|
||||
}
|
||||
|
||||
void LibraryFilterWidget::FocusOnFilter(QKeyEvent* event) {
|
||||
ui_->filter->setFocus();
|
||||
QApplication::sendEvent(ui_->filter, event);
|
||||
@ -220,6 +282,11 @@ void LibraryFilterWidget::GroupingChanged(const LibraryModel::Grouping& g) {
|
||||
}
|
||||
|
||||
// Now make sure the correct action is checked
|
||||
CheckCurrentGrouping(g);
|
||||
}
|
||||
|
||||
void LibraryFilterWidget::CheckCurrentGrouping(
|
||||
const LibraryModel::Grouping& g) {
|
||||
for (QAction* action : group_by_group_->actions()) {
|
||||
if (action->property("group_by").isNull()) continue;
|
||||
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include <QWidget>
|
||||
|
||||
#include "librarymodel.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
|
||||
class GroupByDialog;
|
||||
class SettingsDialog;
|
||||
@ -51,6 +52,7 @@ class LibraryFilterWidget : public QWidget {
|
||||
|
||||
static QActionGroup* CreateGroupByActions(QObject* parent);
|
||||
|
||||
void UpdateGroupByActions();
|
||||
void SetFilterHint(const QString& hint);
|
||||
void SetApplyFilterToLibrary(bool filter_applies_to_model) {
|
||||
filter_applies_to_model_ = filter_applies_to_model;
|
||||
@ -84,6 +86,8 @@ signals:
|
||||
private slots:
|
||||
void GroupingChanged(const LibraryModel::Grouping& g);
|
||||
void GroupByClicked(QAction* action);
|
||||
void SaveGroupBy();
|
||||
void ShowGroupingManager();
|
||||
|
||||
void FilterTextChanged(const QString& text);
|
||||
void FilterDelayTimeout();
|
||||
@ -91,12 +95,14 @@ signals:
|
||||
private:
|
||||
static QAction* CreateGroupByAction(const QString& text, QObject* parent,
|
||||
const LibraryModel::Grouping& grouping);
|
||||
void CheckCurrentGrouping(const LibraryModel::Grouping& g);
|
||||
|
||||
private:
|
||||
Ui_LibraryFilterWidget* ui_;
|
||||
LibraryModel* model_;
|
||||
|
||||
std::unique_ptr<GroupByDialog> group_by_dialog_;
|
||||
std::unique_ptr<SavedGroupingManager> groupings_manager_;
|
||||
SettingsDialog* settings_dialog_;
|
||||
|
||||
QMenu* filter_age_menu_;
|
||||
|
@ -98,6 +98,16 @@
|
||||
<string>Added this month</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="save_grouping">
|
||||
<property name="text">
|
||||
<string>Save current grouping</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="manage_groupings">
|
||||
<property name="text">
|
||||
<string>Manage saved groupings</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
@ -59,6 +59,7 @@ const char* LibraryModel::kSmartPlaylistsMimeType =
|
||||
"application/x-clementine-smart-playlist-generator";
|
||||
const char* LibraryModel::kSmartPlaylistsSettingsGroup =
|
||||
"SerialisedSmartPlaylists";
|
||||
const char* LibraryModel::kSavedGroupingsSettingsGroup = "SavedGroupings";
|
||||
const int LibraryModel::kSmartPlaylistsVersion = 4;
|
||||
const int LibraryModel::kPrettyCoverSize = 32;
|
||||
const qint64 LibraryModel::kIconCacheSize = 100000000; //~100MB
|
||||
@ -106,9 +107,11 @@ LibraryModel::LibraryModel(LibraryBackend* backend, Application* app,
|
||||
Utilities::GetConfigPath(Utilities::Path_CacheRoot) + "/pixmapcache");
|
||||
icon_cache_->setMaximumCacheSize(LibraryModel::kIconCacheSize);
|
||||
|
||||
no_cover_icon_ = QPixmap(":nocover.png")
|
||||
.scaled(kPrettyCoverSize, kPrettyCoverSize,
|
||||
Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
|
||||
no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(
|
||||
kPrettyCoverSize, kPrettyCoverSize,
|
||||
Qt::KeepAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
|
||||
connect(backend_, SIGNAL(SongsDiscovered(SongList)),
|
||||
SLOT(SongsDiscovered(SongList)));
|
||||
@ -141,6 +144,18 @@ void LibraryModel::set_show_dividers(bool show_dividers) {
|
||||
}
|
||||
}
|
||||
|
||||
void LibraryModel::SaveGrouping(QString name) {
|
||||
qLog(Debug) << "Model, save to: " << name;
|
||||
|
||||
QByteArray buffer;
|
||||
QDataStream ds(&buffer, QIODevice::WriteOnly);
|
||||
ds << group_by_;
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSavedGroupingsSettingsGroup);
|
||||
s.setValue(name, buffer);
|
||||
}
|
||||
|
||||
void LibraryModel::Init(bool async) {
|
||||
if (async) {
|
||||
// Show a loading indicator in the model.
|
||||
@ -1491,3 +1506,19 @@ void LibraryModel::TotalSongCountUpdatedSlot(int count) {
|
||||
total_song_count_ = count;
|
||||
emit TotalSongCountUpdated(count);
|
||||
}
|
||||
|
||||
QDataStream& operator<<(QDataStream& s, const LibraryModel::Grouping& g) {
|
||||
s << quint32(g.first) << quint32(g.second) << quint32(g.third);
|
||||
return s;
|
||||
}
|
||||
|
||||
QDataStream& operator>>(QDataStream& s, LibraryModel::Grouping& g) {
|
||||
quint32 buf;
|
||||
s >> buf;
|
||||
g.first = LibraryModel::GroupBy(buf);
|
||||
s >> buf;
|
||||
g.second = LibraryModel::GroupBy(buf);
|
||||
s >> buf;
|
||||
g.third = LibraryModel::GroupBy(buf);
|
||||
return s;
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
|
||||
static const char* kSmartPlaylistsMimeType;
|
||||
static const char* kSmartPlaylistsSettingsGroup;
|
||||
static const char* kSmartPlaylistsArray;
|
||||
static const char* kSavedGroupingsSettingsGroup;
|
||||
static const int kSmartPlaylistsVersion;
|
||||
static const int kPrettyCoverSize;
|
||||
static const qint64 kIconCacheSize;
|
||||
@ -161,6 +162,9 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
|
||||
// Whether or not to show letters heading in the library view
|
||||
void set_show_dividers(bool show_dividers);
|
||||
|
||||
// Save the current grouping
|
||||
void SaveGrouping(QString name);
|
||||
|
||||
// Utility functions for manipulating text
|
||||
static QString TextOrUnknown(const QString& text);
|
||||
static QString PrettyYearAlbum(int year, const QString& album);
|
||||
@ -179,6 +183,7 @@ signals:
|
||||
void SetFilterQueryMode(QueryOptions::QueryMode query_mode);
|
||||
|
||||
void SetGroupBy(const LibraryModel::Grouping& g);
|
||||
const LibraryModel::Grouping& GetGroupBy() const { return group_by_; }
|
||||
void Init(bool async = true);
|
||||
void Reset();
|
||||
void ResetAsync();
|
||||
@ -300,4 +305,7 @@ signals:
|
||||
|
||||
Q_DECLARE_METATYPE(LibraryModel::Grouping);
|
||||
|
||||
QDataStream& operator<<(QDataStream& s, const LibraryModel::Grouping& g);
|
||||
QDataStream& operator>>(QDataStream& s, LibraryModel::Grouping& g);
|
||||
|
||||
#endif // LIBRARYMODEL_H
|
||||
|
@ -173,9 +173,10 @@ LibraryView::LibraryView(QWidget* parent)
|
||||
app_(nullptr),
|
||||
filter_(nullptr),
|
||||
total_song_count_(-1),
|
||||
nomusic_(":nomusic.png"),
|
||||
context_menu_(nullptr),
|
||||
is_in_keyboard_search_(false) {
|
||||
QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
|
||||
nomusic_ = nocover.pixmap(nocover.availableSizes().last());
|
||||
setItemDelegate(new LibraryItemDelegate(this));
|
||||
setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
setHeaderHidden(true);
|
||||
@ -185,7 +186,6 @@ LibraryView::LibraryView(QWidget* parent)
|
||||
setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
|
||||
setStyleSheet("QTreeView::item{padding-top:1px;}");
|
||||
setAnimated(true);
|
||||
}
|
||||
|
||||
LibraryView::~LibraryView() {}
|
||||
|
@ -491,8 +491,9 @@ SongList LibraryWatcher::ScanNewFile(const QString& file, const QString& path,
|
||||
// Ignore FILEs pointing to other media files. Also, watch out for incorrect
|
||||
// media files. Playlist parser for CUEs considers every entry in sheet
|
||||
// valid and we don't want invalid media getting into library!
|
||||
QString file_nfd = file.normalized(QString::NormalizationForm_D);
|
||||
for (const Song& cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
|
||||
if (cue_song.url().toLocalFile() == file) {
|
||||
if (cue_song.url().toLocalFile().normalized(QString::NormalizationForm_D) == file_nfd) {
|
||||
if (TagReaderClient::Instance()->IsMediaFileBlocking(file)) {
|
||||
song_list << cue_song;
|
||||
}
|
||||
|
152
src/library/savedgroupingmanager.cpp
Normal file
152
src/library/savedgroupingmanager.cpp
Normal file
@ -0,0 +1,152 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "libraryfilterwidget.h"
|
||||
#include "librarymodel.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
#include "ui_savedgroupingmanager.h"
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
#include <QKeySequence>
|
||||
#include <QList>
|
||||
#include <QSettings>
|
||||
#include <QStandardItem>
|
||||
|
||||
SavedGroupingManager::SavedGroupingManager(QWidget* parent)
|
||||
: QDialog(parent),
|
||||
ui_(new Ui_SavedGroupingManager),
|
||||
model_(new QStandardItemModel(0, 4, this)) {
|
||||
ui_->setupUi(this);
|
||||
|
||||
model_->setHorizontalHeaderItem(0, new QStandardItem(tr("Name")));
|
||||
model_->setHorizontalHeaderItem(1, new QStandardItem(tr("First level")));
|
||||
model_->setHorizontalHeaderItem(2, new QStandardItem(tr("Second Level")));
|
||||
model_->setHorizontalHeaderItem(3, new QStandardItem(tr("Third Level")));
|
||||
ui_->list->setModel(model_);
|
||||
ui_->remove->setIcon(IconLoader::Load("edit-delete", IconLoader::Base));
|
||||
ui_->remove->setEnabled(false);
|
||||
|
||||
ui_->remove->setShortcut(QKeySequence::Delete);
|
||||
connect(ui_->list->selectionModel(),
|
||||
SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
|
||||
SLOT(UpdateButtonState()));
|
||||
|
||||
connect(ui_->remove, SIGNAL(clicked()), SLOT(Remove()));
|
||||
}
|
||||
|
||||
SavedGroupingManager::~SavedGroupingManager() {
|
||||
delete ui_;
|
||||
delete model_;
|
||||
}
|
||||
|
||||
QString SavedGroupingManager::GroupByToString(const LibraryModel::GroupBy& g) {
|
||||
switch (g) {
|
||||
case LibraryModel::GroupBy_None: {
|
||||
return tr("None");
|
||||
}
|
||||
case LibraryModel::GroupBy_Artist: {
|
||||
return tr("Artist");
|
||||
}
|
||||
case LibraryModel::GroupBy_Album: {
|
||||
return tr("Album");
|
||||
}
|
||||
case LibraryModel::GroupBy_YearAlbum: {
|
||||
return tr("Year - Album");
|
||||
}
|
||||
case LibraryModel::GroupBy_Year: {
|
||||
return tr("Year");
|
||||
}
|
||||
case LibraryModel::GroupBy_Composer: {
|
||||
return tr("Composer");
|
||||
}
|
||||
case LibraryModel::GroupBy_Genre: {
|
||||
return tr("Genre");
|
||||
}
|
||||
case LibraryModel::GroupBy_AlbumArtist: {
|
||||
return tr("Album artist");
|
||||
}
|
||||
case LibraryModel::GroupBy_FileType: {
|
||||
return tr("File type");
|
||||
}
|
||||
case LibraryModel::GroupBy_Performer: {
|
||||
return tr("Performer");
|
||||
}
|
||||
case LibraryModel::GroupBy_Grouping: {
|
||||
return tr("Grouping");
|
||||
}
|
||||
case LibraryModel::GroupBy_Bitrate: {
|
||||
return tr("Bitrate");
|
||||
}
|
||||
case LibraryModel::GroupBy_Disc: {
|
||||
return tr("Disc");
|
||||
}
|
||||
case LibraryModel::GroupBy_OriginalYearAlbum: {
|
||||
return tr("Original year - Album");
|
||||
}
|
||||
case LibraryModel::GroupBy_OriginalYear: {
|
||||
return tr("Original year");
|
||||
}
|
||||
default: { return tr("Unknown"); }
|
||||
}
|
||||
}
|
||||
|
||||
void SavedGroupingManager::UpdateModel() {
|
||||
model_->setRowCount(0); // don't use clear, it deletes headers
|
||||
QSettings s;
|
||||
s.beginGroup(LibraryModel::kSavedGroupingsSettingsGroup);
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
LibraryModel::Grouping g;
|
||||
ds >> g;
|
||||
|
||||
QList<QStandardItem*> list;
|
||||
list << new QStandardItem(saved.at(i))
|
||||
<< new QStandardItem(GroupByToString(g.first))
|
||||
<< new QStandardItem(GroupByToString(g.second))
|
||||
<< new QStandardItem(GroupByToString(g.third));
|
||||
|
||||
model_->appendRow(list);
|
||||
}
|
||||
}
|
||||
|
||||
void SavedGroupingManager::Remove() {
|
||||
if (ui_->list->selectionModel()->hasSelection()) {
|
||||
QSettings s;
|
||||
s.beginGroup(LibraryModel::kSavedGroupingsSettingsGroup);
|
||||
for (const QModelIndex& index :
|
||||
ui_->list->selectionModel()->selectedRows()) {
|
||||
if (index.isValid()) {
|
||||
qLog(Debug) << "Remove saved grouping: "
|
||||
<< model_->item(index.row(), 0)->text();
|
||||
s.remove(model_->item(index.row(), 0)->text());
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateModel();
|
||||
filter_->UpdateGroupByActions();
|
||||
}
|
||||
|
||||
void SavedGroupingManager::UpdateButtonState() {
|
||||
if (ui_->list->selectionModel()->hasSelection()) {
|
||||
const QModelIndex current = ui_->list->selectionModel()->currentIndex();
|
||||
ui_->remove->setEnabled(current.isValid());
|
||||
} else {
|
||||
ui_->remove->setEnabled(false);
|
||||
}
|
||||
}
|
51
src/library/savedgroupingmanager.h
Normal file
51
src/library/savedgroupingmanager.h
Normal file
@ -0,0 +1,51 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2015, Nick Lanham <nick@afternight.org>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SAVEDGROUPINGMANAGER_H
|
||||
#define SAVEDGROUPINGMANAGER_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
#include "librarymodel.h"
|
||||
|
||||
class Ui_SavedGroupingManager;
|
||||
class LibraryFilterWidget;
|
||||
|
||||
class SavedGroupingManager : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SavedGroupingManager(QWidget* parent = nullptr);
|
||||
~SavedGroupingManager();
|
||||
|
||||
void UpdateModel();
|
||||
void SetFilter(LibraryFilterWidget* filter) { filter_ = filter; }
|
||||
|
||||
static QString GroupByToString(const LibraryModel::GroupBy& g);
|
||||
|
||||
private slots:
|
||||
void UpdateButtonState();
|
||||
void Remove();
|
||||
|
||||
private:
|
||||
Ui_SavedGroupingManager* ui_;
|
||||
QStandardItemModel* model_;
|
||||
LibraryFilterWidget* filter_;
|
||||
};
|
||||
|
||||
#endif // SAVEDGROUPINGMANAGER_H
|
144
src/library/savedgroupingmanager.ui
Normal file
144
src/library/savedgroupingmanager.ui
Normal file
@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SavedGroupingManager</class>
|
||||
<widget class="QDialog" name="SavedGroupingManager">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>582</width>
|
||||
<height>363</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Saved Grouping Manager</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/icon.png</normaloff>:/icon.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QTreeView" name="list">
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="remove">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Up</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>SavedGroupingManager</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>SavedGroupingManager</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -56,9 +56,6 @@
|
||||
#include "core/song.h"
|
||||
#include "core/ubuntuunityhack.h"
|
||||
#include "core/utilities.h"
|
||||
#include "covers/amazoncoverprovider.h"
|
||||
#include "covers/coverproviders.h"
|
||||
#include "covers/musicbrainzcoverprovider.h"
|
||||
#include "engines/enginebase.h"
|
||||
#include "smartplaylists/generator.h"
|
||||
#include "ui/iconloader.h"
|
||||
@ -435,11 +432,6 @@ int main(int argc, char* argv[]) {
|
||||
QNetworkProxyFactory::setApplicationProxyFactory(
|
||||
NetworkProxyFactory::Instance());
|
||||
|
||||
// Initialize the repository of cover providers. Last.fm registers itself
|
||||
// when its service is created.
|
||||
app.cover_providers()->AddProvider(new AmazonCoverProvider);
|
||||
app.cover_providers()->AddProvider(new MusicbrainzCoverProvider);
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
// In 11.04 Ubuntu decided that the system tray should be reserved for certain
|
||||
// whitelisted applications. Clementine will override this setting and insert
|
||||
|
@ -247,13 +247,6 @@ void MoodbarProxyStyle::EnsureMoodbarRendered(const QStyleOptionSlider* opt) {
|
||||
}
|
||||
}
|
||||
|
||||
int MoodbarProxyStyle::GetExtraSpace(const QStyleOptionComplex* opt) const {
|
||||
int space_available = slider_->style()->pixelMetric(
|
||||
QStyle::PM_SliderSpaceAvailable, opt, slider_);
|
||||
int w = slider_->width();
|
||||
return w - space_available;
|
||||
}
|
||||
|
||||
QRect MoodbarProxyStyle::subControlRect(ComplexControl cc,
|
||||
const QStyleOptionComplex* opt,
|
||||
SubControl sc,
|
||||
@ -270,29 +263,23 @@ QRect MoodbarProxyStyle::subControlRect(ComplexControl cc,
|
||||
case MoodbarOn:
|
||||
case FadingToOn:
|
||||
switch (sc) {
|
||||
case SC_SliderGroove: {
|
||||
int margin_leftright = GetExtraSpace(opt) / 2;
|
||||
return opt->rect.adjusted(margin_leftright, kMarginSize,
|
||||
-margin_leftright, -kMarginSize);
|
||||
}
|
||||
case SC_SliderGroove:
|
||||
return opt->rect.adjusted(kMarginSize, kMarginSize, -kMarginSize,
|
||||
-kMarginSize);
|
||||
|
||||
case SC_SliderHandle: {
|
||||
const QStyleOptionSlider* slider_opt =
|
||||
qstyleoption_cast<const QStyleOptionSlider*>(opt);
|
||||
|
||||
int space_available = slider_->style()->pixelMetric(
|
||||
QStyle::PM_SliderSpaceAvailable, opt, slider_);
|
||||
int w = slider_->width();
|
||||
int margin = (w - space_available) / 2;
|
||||
int x = 0;
|
||||
|
||||
if (slider_opt->maximum != slider_opt->minimum) {
|
||||
/* slider_opt->{maximum,minimum} can have the value 0 (their default
|
||||
values), so this check avoids a division by 0. */
|
||||
if (slider_opt->maximum > slider_opt->minimum) {
|
||||
x = (slider_opt->sliderValue - slider_opt->minimum) *
|
||||
(space_available - kArrowWidth) /
|
||||
(opt->rect.width() - kArrowWidth) /
|
||||
(slider_opt->maximum - slider_opt->minimum);
|
||||
}
|
||||
|
||||
x += margin;
|
||||
|
||||
return QRect(QPoint(opt->rect.left() + x, opt->rect.top()),
|
||||
QSize(kArrowWidth, kArrowHeight));
|
||||
}
|
||||
@ -330,14 +317,9 @@ QPixmap MoodbarProxyStyle::MoodbarPixmap(const ColorVector& colors,
|
||||
const QSize& size,
|
||||
const QPalette& palette,
|
||||
const QStyleOptionSlider* opt) {
|
||||
int margin_leftright = GetExtraSpace(opt);
|
||||
const QRect rect(QPoint(0, 0), size);
|
||||
QRect rect(QPoint(0, 0), size);
|
||||
QRect border_rect(rect);
|
||||
// I would expect we need to adjust by margin_lr/2, so the extra space is
|
||||
// distributed on both side, but if we do so, the margin is too small, and I'm
|
||||
// not sure why...
|
||||
border_rect.adjust(margin_leftright, kMarginSize, -margin_leftright,
|
||||
-kMarginSize);
|
||||
border_rect.adjust(kMarginSize, kMarginSize, -kMarginSize, -kMarginSize);
|
||||
|
||||
QRect inner_rect(border_rect);
|
||||
inner_rect.adjust(kBorderSize, kBorderSize, -kBorderSize, -kBorderSize);
|
||||
@ -356,14 +338,8 @@ QPixmap MoodbarProxyStyle::MoodbarPixmap(const ColorVector& colors,
|
||||
// Draw the outer bit
|
||||
p.setPen(QPen(palette.brush(QPalette::Active, QPalette::Background),
|
||||
kMarginSize, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
|
||||
// First: a rectangle around the slider
|
||||
|
||||
p.drawRect(rect.adjusted(1, 1, -2, -2));
|
||||
// Then, thicker border on left and right, because of the margins.
|
||||
p.setPen(QPen(palette.brush(QPalette::Active, QPalette::Background),
|
||||
margin_leftright * 2 - kBorderSize, Qt::SolidLine, Qt::FlatCap,
|
||||
Qt::MiterJoin));
|
||||
p.drawLine(rect.topLeft(), rect.bottomLeft());
|
||||
p.drawLine(rect.topRight(), rect.bottomRight());
|
||||
|
||||
p.end();
|
||||
|
||||
|
@ -79,10 +79,6 @@ class MoodbarProxyStyle : public QProxyStyle {
|
||||
void ChangeStyle(QAction* action);
|
||||
|
||||
private:
|
||||
// The slider "groove" is smaller than the actual slider: this convenient
|
||||
// function returns the difference between groove width and slider width.
|
||||
int GetExtraSpace(const QStyleOptionComplex* opt) const;
|
||||
|
||||
Application* app_;
|
||||
QSlider* slider_;
|
||||
|
||||
|
@ -17,15 +17,18 @@
|
||||
|
||||
#include "networkremote.h"
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "covers/currentartloader.h"
|
||||
#include "networkremote/zeroconf.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QSettings>
|
||||
#include <QHostInfo>
|
||||
#include <QNetworkProxy>
|
||||
#include <QTcpServer>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "covers/currentartloader.h"
|
||||
#include "networkremote/incomingdataparser.h"
|
||||
#include "networkremote/outgoingdatacreator.h"
|
||||
#include "networkremote/zeroconf.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
|
||||
const char* NetworkRemote::kSettingsGroup = "NetworkRemote";
|
||||
const quint16 NetworkRemote::kDefaultServerPort = 5500;
|
||||
|
@ -3,14 +3,17 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
|
||||
#include "core/player.h"
|
||||
#include "core/application.h"
|
||||
#include "incomingdataparser.h"
|
||||
#include "outgoingdatacreator.h"
|
||||
#include "remoteclient.h"
|
||||
class Application;
|
||||
class IncomingDataParser;
|
||||
class OutgoingDataCreator;
|
||||
class QHostAddress;
|
||||
class QImage;
|
||||
class QTcpServer;
|
||||
class QTcpSocket;
|
||||
class RemoteClient;
|
||||
|
||||
class NetworkRemote : public QObject {
|
||||
Q_OBJECT
|
||||
|
@ -15,10 +15,12 @@
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "networkremote/networkremotehelper.h"
|
||||
|
||||
#include "networkremote.h"
|
||||
#include "networkremotehelper.h"
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "networkremote/networkremote.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
|
||||
NetworkRemoteHelper* NetworkRemoteHelper::sInstance = nullptr;
|
||||
|
||||
|
@ -17,14 +17,15 @@
|
||||
|
||||
#include "songsender.h"
|
||||
|
||||
#include "networkremote.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/utilities.h"
|
||||
#include "library/librarybackend.h"
|
||||
#include "networkremote/networkremote.h"
|
||||
#include "networkremote/outgoingdatacreator.h"
|
||||
#include "networkremote/remoteclient.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
|
||||
const quint32 SongSender::kFileChunkSize = 100000; // in Bytes
|
||||
@ -32,14 +33,16 @@ const quint32 SongSender::kFileChunkSize = 100000; // in Bytes
|
||||
SongSender::SongSender(Application* app, RemoteClient* client)
|
||||
: app_(app),
|
||||
client_(client),
|
||||
transcoder_(new Transcoder(this)) {
|
||||
transcoder_(
|
||||
new Transcoder(this, NetworkRemote::kTranscoderSettingPostfix)) {
|
||||
QSettings s;
|
||||
s.beginGroup(NetworkRemote::kSettingsGroup);
|
||||
|
||||
transcode_lossless_files_ = s.value("convert_lossless", false).toBool();
|
||||
|
||||
// Load preset
|
||||
QString last_output_format = s.value("last_output_format", "audio/x-vorbis").toString();
|
||||
QString last_output_format =
|
||||
s.value("last_output_format", "audio/x-vorbis").toString();
|
||||
QList<TranscoderPreset> presets = transcoder_->GetAllPresets();
|
||||
for (int i = 0; i < presets.count(); ++i) {
|
||||
if (last_output_format == presets.at(i).codec_mimetype_) {
|
||||
@ -59,7 +62,8 @@ SongSender::SongSender(Application* app, RemoteClient* client)
|
||||
SongSender::~SongSender() {
|
||||
disconnect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)), this,
|
||||
SLOT(TranscodeJobComplete(QString, QString, bool)));
|
||||
disconnect(transcoder_, SIGNAL(AllJobsComplete()), this, SLOT(StartTransfer()));
|
||||
disconnect(transcoder_, SIGNAL(AllJobsComplete()), this,
|
||||
SLOT(StartTransfer()));
|
||||
transcoder_->Cancel();
|
||||
}
|
||||
|
||||
@ -102,8 +106,7 @@ void SongSender::SendSongs(const pb::remote::RequestDownloadSongs& request) {
|
||||
void SongSender::TranscodeLosslessFiles() {
|
||||
for (DownloadItem item : download_queue_) {
|
||||
// Check only lossless files
|
||||
if (!item.song_.IsFileLossless())
|
||||
continue;
|
||||
if (!item.song_.IsFileLossless()) continue;
|
||||
|
||||
// Add the file to the transcoder
|
||||
QString local_file = item.song_.url().toLocalFile();
|
||||
@ -122,7 +125,8 @@ void SongSender::TranscodeLosslessFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
void SongSender::TranscodeJobComplete(const QString& input, const QString& output, bool success) {
|
||||
void SongSender::TranscodeJobComplete(const QString& input,
|
||||
const QString& output, bool success) {
|
||||
qLog(Debug) << input << "transcoded to" << output << success;
|
||||
|
||||
// If it wasn't successful send original file
|
||||
@ -204,7 +208,8 @@ void SongSender::OfferNextSong() {
|
||||
chunk->set_file_number(item.song_no_);
|
||||
chunk->set_size(file.size());
|
||||
|
||||
OutgoingDataCreator::CreateSong(item.song_, QImage(), -1, chunk->mutable_song_metadata());
|
||||
OutgoingDataCreator::CreateSong(item.song_, QImage(), -1,
|
||||
chunk->mutable_song_metadata());
|
||||
}
|
||||
|
||||
client_->SendData(&msg);
|
||||
@ -215,8 +220,7 @@ void SongSender::ResponseSongOffer(bool accepted) {
|
||||
|
||||
// Get the item and send the single song
|
||||
DownloadItem item = download_queue_.dequeue();
|
||||
if (accepted)
|
||||
SendSingleSong(item);
|
||||
if (accepted) SendSingleSong(item);
|
||||
|
||||
// And offer the next song
|
||||
OfferNextSong();
|
||||
@ -273,7 +277,8 @@ void SongSender::SendSingleSong(DownloadItem download_item) {
|
||||
int i = app_->playlist_manager()->active()->current_row();
|
||||
pb::remote::SongMetadata* song_metadata =
|
||||
msg.mutable_response_song_file_chunk()->mutable_song_metadata();
|
||||
OutgoingDataCreator::CreateSong(download_item.song_, null_image, i,song_metadata);
|
||||
OutgoingDataCreator::CreateSong(download_item.song_, null_image, i,
|
||||
song_metadata);
|
||||
|
||||
// if the file was transcoded, we have to change the filename and filesize
|
||||
if (is_transcoded) {
|
||||
|
@ -414,9 +414,15 @@ bool Playlist::setData(const QModelIndex& index, const QVariant& value,
|
||||
void Playlist::SongSaveComplete(TagReaderReply* reply,
|
||||
const QPersistentModelIndex& index) {
|
||||
if (reply->is_successful() && index.isValid()) {
|
||||
if (reply->message().save_file_response().success()) {
|
||||
QFuture<void> future = item_at(index.row())->BackgroundReload();
|
||||
NewClosure(future, this, SLOT(ItemReloadComplete(QPersistentModelIndex)),
|
||||
index);
|
||||
} else {
|
||||
emit Error(tr("An error occurred writing metadata to '%1'").arg(
|
||||
QString::fromStdString(
|
||||
reply->request_message().save_file_request().filename())));
|
||||
}
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
@ -691,7 +697,7 @@ void Playlist::set_current_row(int i, bool is_stopping) {
|
||||
void Playlist::InsertDynamicItems(int count) {
|
||||
GeneratorInserter* inserter =
|
||||
new GeneratorInserter(task_manager_, library_, this);
|
||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(LoadTracksError(QString)));
|
||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
connect(inserter, SIGNAL(PlayRequested(QModelIndex)),
|
||||
SIGNAL(PlayRequested(QModelIndex)));
|
||||
|
||||
@ -819,7 +825,7 @@ bool Playlist::dropMimeData(const QMimeData* data, Qt::DropAction action,
|
||||
} else if (data->hasFormat(kCddaMimeType)) {
|
||||
SongLoaderInserter* inserter = new SongLoaderInserter(
|
||||
task_manager_, library_, backend_->app()->player());
|
||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(LoadTracksError(QString)));
|
||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
inserter->LoadAudioCD(this, row, play_now, enqueue_now);
|
||||
} else if (data->hasUrls()) {
|
||||
// URL list dragged from the file list or some other app
|
||||
@ -833,7 +839,7 @@ void Playlist::InsertUrls(const QList<QUrl>& urls, int pos, bool play_now,
|
||||
bool enqueue) {
|
||||
SongLoaderInserter* inserter = new SongLoaderInserter(
|
||||
task_manager_, library_, backend_->app()->player());
|
||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(LoadTracksError(QString)));
|
||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
|
||||
inserter->Load(this, pos, play_now, enqueue, urls);
|
||||
}
|
||||
@ -847,7 +853,7 @@ void Playlist::InsertSmartPlaylist(GeneratorPtr generator, int pos,
|
||||
|
||||
GeneratorInserter* inserter =
|
||||
new GeneratorInserter(task_manager_, library_, this);
|
||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(LoadTracksError(QString)));
|
||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
|
||||
inserter->Load(this, pos, play_now, enqueue, generator);
|
||||
|
||||
|
@ -348,7 +348,7 @@ signals:
|
||||
void PlaylistChanged();
|
||||
void DynamicModeChanged(bool dynamic);
|
||||
|
||||
void LoadTracksError(const QString& message);
|
||||
void Error(const QString& message);
|
||||
|
||||
// Signals that the queue has changed, meaning that the remaining queued
|
||||
// items should update their position.
|
||||
|
@ -68,6 +68,9 @@ PlaylistContainer::PlaylistContainer(QWidget* parent)
|
||||
no_matches_color);
|
||||
no_matches_label_->setPalette(no_matches_palette);
|
||||
|
||||
// Remove QFrame border
|
||||
ui_->toolbar->setStyleSheet("QFrame { border: 0px; }");
|
||||
|
||||
// Make it bold
|
||||
QFont no_matches_font = no_matches_label_->font();
|
||||
no_matches_font.setBold(true);
|
||||
@ -224,11 +227,11 @@ void PlaylistContainer::SetViewModel(Playlist* playlist) {
|
||||
}
|
||||
|
||||
void PlaylistContainer::ActivePlaying() {
|
||||
UpdateActiveIcon(QIcon(":tiny-start.png"));
|
||||
UpdateActiveIcon(IconLoader::Load("tiny-start", IconLoader::Other));
|
||||
}
|
||||
|
||||
void PlaylistContainer::ActivePaused() {
|
||||
UpdateActiveIcon(QIcon(":tiny-pause.png"));
|
||||
UpdateActiveIcon(IconLoader::Load("tiny-pause", IconLoader::Other));
|
||||
}
|
||||
|
||||
void PlaylistContainer::ActiveStopped() { UpdateActiveIcon(QIcon()); }
|
||||
|
@ -104,13 +104,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSearchField" name="filter" native="true"/>
|
||||
</item>
|
||||
|
@ -117,7 +117,7 @@ Playlist* PlaylistManager::AddPlaylist(int id, const QString& name,
|
||||
connect(ret, SIGNAL(PlaylistChanged()), SLOT(UpdateSummaryText()));
|
||||
connect(ret, SIGNAL(EditingFinished(QModelIndex)),
|
||||
SIGNAL(EditingFinished(QModelIndex)));
|
||||
connect(ret, SIGNAL(LoadTracksError(QString)), SIGNAL(Error(QString)));
|
||||
connect(ret, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
connect(ret, SIGNAL(PlayRequested(QModelIndex)),
|
||||
SIGNAL(PlayRequested(QModelIndex)));
|
||||
connect(playlist_container_->view(),
|
||||
|
@ -26,6 +26,7 @@
|
||||
#include "core/player.h"
|
||||
#include "covers/currentartloader.h"
|
||||
#include "ui/qt_blurimage.h"
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
#include <QCommonStyle>
|
||||
#include <QClipboard>
|
||||
@ -128,8 +129,6 @@ PlaylistView::PlaylistView(QWidget* parent)
|
||||
inhibit_autoscroll_(false),
|
||||
currently_autoscrolling_(false),
|
||||
row_height_(-1),
|
||||
currenttrack_play_(":currenttrack_play.png"),
|
||||
currenttrack_pause_(":currenttrack_pause.png"),
|
||||
cached_current_row_row_(-1),
|
||||
drop_indicator_row_(-1),
|
||||
drag_over_(false),
|
||||
@ -139,6 +138,17 @@ PlaylistView::PlaylistView(QWidget* parent)
|
||||
setStyle(style_);
|
||||
setMouseTracking(true);
|
||||
|
||||
QIcon currenttrack_play = IconLoader::Load("currenttrack_play",
|
||||
IconLoader::Other);
|
||||
currenttrack_play_ = currenttrack_play.pixmap(currenttrack_play
|
||||
.availableSizes()
|
||||
.last());
|
||||
QIcon currenttrack_pause = IconLoader::Load("currenttrack_pause",
|
||||
IconLoader::Other);
|
||||
currenttrack_pause_ = currenttrack_pause.pixmap(currenttrack_pause
|
||||
.availableSizes()
|
||||
.last());
|
||||
|
||||
connect(header_, SIGNAL(sectionResized(int, int, int)), SLOT(SaveGeometry()));
|
||||
connect(header_, SIGNAL(sectionMoved(int, int, int)), SLOT(SaveGeometry()));
|
||||
connect(header_, SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)),
|
||||
@ -497,6 +507,9 @@ void PlaylistView::drawRow(QPainter* painter,
|
||||
is_paused ? currenttrack_pause_ : currenttrack_play_);
|
||||
|
||||
// Set the font
|
||||
opt.palette.setColor(QPalette::Inactive, QPalette::HighlightedText,
|
||||
QApplication::palette().color(
|
||||
QPalette::Active, QPalette::HighlightedText));
|
||||
opt.palette.setColor(QPalette::Text, QApplication::palette().color(
|
||||
QPalette::HighlightedText));
|
||||
opt.palette.setColor(QPalette::Highlight, Qt::transparent);
|
||||
|
@ -17,46 +17,132 @@
|
||||
|
||||
#include "echonestimages.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include <Artist.h>
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
|
||||
struct EchoNestImages::Request {
|
||||
Request(int id) : id_(id), artist_(new Echonest::Artist) {}
|
||||
namespace {
|
||||
static const char* kSpotifyBucket = "spotify";
|
||||
static const char* kSpotifyArtistUrl = "https://api.spotify.com/v1/artists/%1";
|
||||
}
|
||||
|
||||
int id_;
|
||||
std::unique_ptr<Echonest::Artist> artist_;
|
||||
};
|
||||
EchoNestImages::EchoNestImages() : network_(new NetworkAccessManager) {}
|
||||
|
||||
EchoNestImages::~EchoNestImages() {}
|
||||
|
||||
void EchoNestImages::FetchInfo(int id, const Song& metadata) {
|
||||
std::shared_ptr<Request> request(new Request(id));
|
||||
request->artist_->setName(metadata.artist());
|
||||
Echonest::Artist artist;
|
||||
artist.setName(metadata.artist());
|
||||
|
||||
QNetworkReply* reply = request->artist_->fetchImages();
|
||||
connect(reply, SIGNAL(finished()), SLOT(RequestFinished()));
|
||||
requests_[reply] = request;
|
||||
// Search for images directly on echonest.
|
||||
// This is currently a bit limited as most results are for last.fm urls that
|
||||
// no longer work.
|
||||
QNetworkReply* reply = artist.fetchImages();
|
||||
RegisterReply(reply, id);
|
||||
NewClosure(reply, SIGNAL(finished()), this,
|
||||
SLOT(RequestFinished(QNetworkReply*, int, Echonest::Artist)),
|
||||
reply, id, artist);
|
||||
|
||||
// Also look up the artist id for the spotify API so we can directly request
|
||||
// images from there too.
|
||||
Echonest::Artist::SearchParams params;
|
||||
params.push_back(
|
||||
qMakePair(Echonest::Artist::Name, QVariant(metadata.artist())));
|
||||
QNetworkReply* rosetta_reply = Echonest::Artist::search(
|
||||
params,
|
||||
Echonest::ArtistInformation(Echonest::ArtistInformation::NoInformation,
|
||||
QStringList() << kSpotifyBucket));
|
||||
RegisterReply(rosetta_reply, id);
|
||||
NewClosure(rosetta_reply, SIGNAL(finished()), this,
|
||||
SLOT(IdsFound(QNetworkReply*, int)), rosetta_reply, id);
|
||||
}
|
||||
|
||||
void EchoNestImages::RequestFinished() {
|
||||
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
|
||||
if (!reply || !requests_.contains(reply)) return;
|
||||
void EchoNestImages::RequestFinished(QNetworkReply* reply, int id,
|
||||
Echonest::Artist artist) {
|
||||
reply->deleteLater();
|
||||
|
||||
RequestPtr request = requests_.take(reply);
|
||||
|
||||
try {
|
||||
request->artist_->parseProfile(reply);
|
||||
}
|
||||
catch (Echonest::ParseError e) {
|
||||
artist.parseProfile(reply);
|
||||
} catch (Echonest::ParseError e) {
|
||||
qLog(Warning) << "Error parsing echonest reply:" << e.errorType()
|
||||
<< e.what();
|
||||
}
|
||||
|
||||
for (const Echonest::ArtistImage& image : request->artist_->images()) {
|
||||
emit ImageReady(request->id_, image.url());
|
||||
for (const Echonest::ArtistImage& image : artist.images()) {
|
||||
// Echonest still sends these broken URLs for last.fm.
|
||||
if (image.url().authority() != "userserve-ak.last.fm") {
|
||||
emit ImageReady(id, image.url());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit Finished(request->id_);
|
||||
void EchoNestImages::IdsFound(QNetworkReply* reply, int request_id) {
|
||||
reply->deleteLater();
|
||||
try {
|
||||
Echonest::Artists artists = Echonest::Artist::parseSearch(reply);
|
||||
if (artists.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const Echonest::ForeignIds& foreign_ids = artists.first().foreignIds();
|
||||
for (const Echonest::ForeignId& id : foreign_ids) {
|
||||
if (id.catalog.contains("spotify")) {
|
||||
DoSpotifyImageRequest(id.foreign_id, request_id);
|
||||
}
|
||||
}
|
||||
} catch (Echonest::ParseError e) {
|
||||
qLog(Warning) << "Error parsing echonest reply:" << e.errorType()
|
||||
<< e.what();
|
||||
}
|
||||
}
|
||||
|
||||
void EchoNestImages::DoSpotifyImageRequest(const QString& id, int request_id) {
|
||||
QString artist_id = id.split(":").last();
|
||||
QUrl url(QString(kSpotifyArtistUrl).arg(artist_id));
|
||||
QNetworkReply* reply = network_->get(QNetworkRequest(url));
|
||||
RegisterReply(reply, request_id);
|
||||
NewClosure(reply, SIGNAL(finished()), [this, reply, request_id]() {
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonObject result = QJsonDocument::fromJson(reply->readAll()).object();
|
||||
QJsonArray images = result["images"].toArray();
|
||||
QList<QPair<QUrl, QSize>> image_urls;
|
||||
for (const QJsonValue& image : images) {
|
||||
QJsonObject image_result = image.toObject();
|
||||
image_urls.append(qMakePair(image_result["url"].toVariant().toUrl(),
|
||||
QSize(image_result["width"].toInt(),
|
||||
image_result["height"].toInt())));
|
||||
}
|
||||
// All the images are the same just different sizes; just pick the largest.
|
||||
std::sort(image_urls.begin(), image_urls.end(),
|
||||
[](const QPair<QUrl, QSize>& a,
|
||||
const QPair<QUrl, QSize>& b) {
|
||||
// Sorted by area ascending.
|
||||
return (a.second.height() * a.second.width()) <
|
||||
(b.second.height() * b.second.width());
|
||||
});
|
||||
if (!image_urls.isEmpty()) {
|
||||
emit ImageReady(request_id, image_urls.last().first);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keeps track of replies and emits Finished() when all replies associated with
|
||||
// a request are finished with.
|
||||
void EchoNestImages::RegisterReply(QNetworkReply* reply, int id) {
|
||||
replies_.insert(id, reply);
|
||||
NewClosure(reply, SIGNAL(destroyed()), [this, reply, id]() {
|
||||
replies_.remove(id, reply);
|
||||
if (!replies_.contains(id)) {
|
||||
emit Finished(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -20,24 +20,33 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "songinfoprovider.h"
|
||||
#include <QMultiMap>
|
||||
|
||||
#include <echonest/Artist.h>
|
||||
|
||||
#include "songinfo/songinfoprovider.h"
|
||||
|
||||
class NetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
class EchoNestImages : public SongInfoProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
EchoNestImages();
|
||||
virtual ~EchoNestImages();
|
||||
void FetchInfo(int id, const Song& metadata);
|
||||
|
||||
private slots:
|
||||
void RequestFinished();
|
||||
void RequestFinished(QNetworkReply*, int id, Echonest::Artist artist);
|
||||
void IdsFound(QNetworkReply* reply, int id);
|
||||
|
||||
private:
|
||||
struct Request;
|
||||
typedef std::shared_ptr<Request> RequestPtr;
|
||||
void DoSpotifyImageRequest(const QString& id, int request_id);
|
||||
|
||||
QMap<QNetworkReply*, RequestPtr> requests_;
|
||||
void RegisterReply(QNetworkReply* reply, int id);
|
||||
QMultiMap<int, QNetworkReply*> replies_;
|
||||
std::unique_ptr<NetworkAccessManager> network_;
|
||||
};
|
||||
|
||||
#endif // ECHONESTIMAGES_H
|
||||
|
@ -25,13 +25,14 @@
|
||||
#include <QJsonArray>
|
||||
|
||||
#include <Artist.h>
|
||||
#include <TypeInformation.h>
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "songkickconcertwidget.h"
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
const char* SongkickConcerts::kSongkickArtistBucket = "id:songkick";
|
||||
const char* SongkickConcerts::kSongkickArtistBucket = "songkick";
|
||||
const char* SongkickConcerts::kSongkickArtistCalendarUrl =
|
||||
"https://api.songkick.com/api/3.0/artists/%1/calendar.json?"
|
||||
"per_page=5&"
|
||||
@ -50,10 +51,11 @@ void SongkickConcerts::FetchInfo(int id, const Song& metadata) {
|
||||
Echonest::Artist::SearchParams params;
|
||||
params.push_back(
|
||||
qMakePair(Echonest::Artist::Name, QVariant(metadata.artist())));
|
||||
params.push_back(
|
||||
qMakePair(Echonest::Artist::IdSpace, QVariant(kSongkickArtistBucket)));
|
||||
qLog(Debug) << "Params:" << params;
|
||||
QNetworkReply* reply = Echonest::Artist::search(params);
|
||||
QNetworkReply* reply = Echonest::Artist::search(
|
||||
params,
|
||||
Echonest::ArtistInformation(Echonest::ArtistInformation::NoInformation,
|
||||
QStringList() << kSongkickArtistBucket));
|
||||
qLog(Debug) << reply->request().url();
|
||||
NewClosure(reply, SIGNAL(finished()), this,
|
||||
SLOT(ArtistSearchFinished(QNetworkReply*, int)), reply, id);
|
||||
@ -93,8 +95,7 @@ void SongkickConcerts::ArtistSearchFinished(QNetworkReply* reply, int id) {
|
||||
}
|
||||
|
||||
FetchSongkickCalendar(split[2], id);
|
||||
}
|
||||
catch (Echonest::ParseError& e) {
|
||||
} catch (Echonest::ParseError& e) {
|
||||
qLog(Error) << "Error parsing echonest reply:" << e.errorType() << e.what();
|
||||
emit Finished(id);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
<string>Transcode Music</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/icon.png</normaloff>:/icon.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
@ -64,7 +64,17 @@
|
||||
<item>
|
||||
<widget class="QPushButton" name="add">
|
||||
<property name="text">
|
||||
<string>Add...</string>
|
||||
<string>Add file...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="import">
|
||||
<property name="toolTip">
|
||||
<string>Add all tracks from a directory and all its subdirectories</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add directory...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -88,16 +98,6 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="import">
|
||||
<property name="toolTip">
|
||||
<string>Add all tracks from a directory and all its subdirectories</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Import...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
@ -216,10 +216,16 @@
|
||||
<tabstops>
|
||||
<tabstop>files</tabstop>
|
||||
<tabstop>add</tabstop>
|
||||
<tabstop>import</tabstop>
|
||||
<tabstop>remove</tabstop>
|
||||
<tabstop>format</tabstop>
|
||||
<tabstop>button_box</tabstop>
|
||||
<tabstop>options</tabstop>
|
||||
<tabstop>destination</tabstop>
|
||||
<tabstop>select</tabstop>
|
||||
<tabstop>details</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@ -82,10 +82,10 @@ GstElement* Transcoder::CreateElementForMimeType(const QString& element_type,
|
||||
GstElement* bin) {
|
||||
if (mime_type.isEmpty()) return nullptr;
|
||||
|
||||
// HACK: Force ffmux_mp4 because it doesn't set any useful src caps
|
||||
// HACK: Force mp4mux because it doesn't set any useful src caps
|
||||
if (mime_type == "audio/mp4") {
|
||||
LogLine(QString("Using '%1' (rank %2)").arg("ffmux_mp4").arg(-1));
|
||||
return CreateElement("ffmux_mp4", bin);
|
||||
LogLine(QString("Using '%1' (rank %2)").arg("mp4mux").arg(-1));
|
||||
return CreateElement("mp4mux", bin);
|
||||
}
|
||||
|
||||
// Keep track of all the suitable elements we find and figure out which
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user