Merge remote-tracking branch 'upstream/master' into qt5

This commit is contained in:
Chocobozzz 2016-02-29 18:03:02 +01:00
commit e6e189967d
160 changed files with 48383 additions and 48358 deletions

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View 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

View File

@ -25,6 +25,8 @@
#include <execinfo.h>
#endif
#include <iostream>
#include <QCoreApplication>
#include <QDateTime>
#include <QStringList>
@ -204,10 +206,11 @@ QDebug CreateLogger(Level level, const QString& class_name, int line) {
}
QDebug ret(type);
ret.nospace() << kMessageHandlerMagic << QDateTime::currentDateTime()
.toString("hh:mm:ss.zzz")
.toLatin1()
.constData() << level_name
ret.nospace() << kMessageHandlerMagic
<< QDateTime::currentDateTime()
.toString("hh:mm:ss.zzz")
.toLatin1()
.constData() << level_name
<< function_line.leftJustified(32).toLatin1().constData();
return ret.space();
@ -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
@ -288,7 +292,7 @@ QDebug CreateLoggerDebug(int line, const char *class_name) { return qCreateLogge
namespace {
template<typename T>
template <typename T>
QString print_duration(T duration, const std::string& unit) {
return QString("%1%2").arg(duration.count()).arg(unit.c_str());
}

View File

@ -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,7 +507,8 @@ set(HEADERS
library/libraryview.h
library/libraryviewcontainer.h
library/librarywatcher.h
library/savedgroupingmanager.h
musicbrainz/acoustidclient.h
musicbrainz/musicbrainzclient.h
musicbrainz/tagfetcher.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

View File

@ -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);
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
moodbar_loader_ = new MoodbarLoader(this, this);
moodbar_controller_ = new MoodbarController(this, this);
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();
}

View File

@ -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);
@ -106,40 +106,14 @@ class Application : public QObject {
void ReloadSettings();
void OpenSettingsDialogAtPage(SettingsDialog::Page page);
signals:
signals:
void ErrorAdded(const QString& message);
void SettingsChanged();
void SettingsDialogRequested(SettingsDialog::Page page);
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_;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
@ -83,7 +85,7 @@ class InternetService : public QObject {
virtual QString Icon() { return QString(); }
signals:
signals:
void StreamError(const QString& message);
void StreamMetadataFound(const QUrl& original_url, const Song& song);
@ -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*);

View File

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

View File

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

View File

@ -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();
@ -180,8 +178,8 @@ void DigitallyImportedServiceBase::ShowContextMenu(const QPoint& global_pos) {
tr("Refresh streams"), this,
SLOT(ForceRefreshStreams()));
context_menu_->addSeparator();
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
tr("Configure..."), this,
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
tr("Configure..."), this,
SLOT(ShowSettingsDialog()));
}
@ -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));
}
playlist_url = QUrl(
premium_playlists_[premium_audio_type_].arg(host, key, listen_hash_));
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) {}

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
@ -80,7 +82,8 @@ class LastFMService : public Scrobbler {
void ShowConfig();
void ToggleScrobbling();
signals:
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_;

View File

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

View File

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

View File

@ -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>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
<widget class="QWidget" name="login_container" native="true">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>28</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Last.fm username</string>
</property>
</widget>
</item>
<item row="1" column="1">
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="username"/>
</item>
<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>

View File

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

View File

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

View File

@ -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_ + "/" +

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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

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

View File

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

View File

@ -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) /
(slider_opt->maximum - slider_opt->minimum);
(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();

View File

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

View File

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

View File

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

View File

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

View File

@ -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,16 +33,18 @@ 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) {
for (int i = 0; i < presets.count(); ++i) {
if (last_output_format == presets.at(i).codec_mimetype_) {
transcoder_preset_ = presets.at(i);
break;
@ -58,8 +61,9 @@ 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()));
SLOT(TranscodeJobComplete(QString, QString, bool)));
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) {
@ -341,7 +346,7 @@ void SongSender::SendPlaylist(int playlist_id) {
}
}
void SongSender::SendUrls(const pb::remote::RequestDownloadSongs &request) {
void SongSender::SendUrls(const pb::remote::RequestDownloadSongs& request) {
SongList song_list;
// First gather all valid songs

View File

@ -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()) {
QFuture<void> future = item_at(index.row())->BackgroundReload();
NewClosure(future, this, SLOT(ItemReloadComplete(QPersistentModelIndex)),
index);
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);

View File

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

View File

@ -67,7 +67,10 @@ PlaylistContainer::PlaylistContainer(QWidget* parent)
no_matches_palette.setColor(QPalette::Inactive, QPalette::WindowText,
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()); }

View File

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

View File

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

View File

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

View File

@ -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) {}
int id_;
std::unique_ptr<Echonest::Artist> artist_;
};
void EchoNestImages::FetchInfo(int id, const Song& metadata) {
std::shared_ptr<Request> request(new Request(id));
request->artist_->setName(metadata.artist());
QNetworkReply* reply = request->artist_->fetchImages();
connect(reply, SIGNAL(finished()), SLOT(RequestFinished()));
requests_[reply] = request;
namespace {
static const char* kSpotifyBucket = "spotify";
static const char* kSpotifyArtistUrl = "https://api.spotify.com/v1/artists/%1";
}
void EchoNestImages::RequestFinished() {
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
if (!reply || !requests_.contains(reply)) return;
EchoNestImages::EchoNestImages() : network_(new NetworkAccessManager) {}
EchoNestImages::~EchoNestImages() {}
void EchoNestImages::FetchInfo(int id, const Song& metadata) {
Echonest::Artist artist;
artist.setName(metadata.artist());
// 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, 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);
}
});
}

View File

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

View File

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

View File

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

View File

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