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/Summary.cmake)
include(cmake/Version.cmake) include(cmake/Version.cmake)
include(cmake/Deb.cmake) include(cmake/Deb.cmake)
include(cmake/Rpm.cmake)
include(cmake/SpotifyVersion.cmake) include(cmake/SpotifyVersion.cmake)
include(cmake/OptionalSource.cmake) include(cmake/OptionalSource.cmake)
include(cmake/Format.cmake) include(cmake/Format.cmake)

295
Changelog
View File

@ -1,160 +1,183 @@
Next release: Version 1.3:
Major features: Major features:
* Vk.com support * Vk.com support
* Seafile support (server >= 4.4.1) * Seafile support (server >= 4.4.1)
* Amazon Cloud Drive support
* Add Ampache compatibility (through Subsonic service) * Add Ampache compatibility (through Subsonic service)
* Add new analyzer "Rainbow Dash" * Add new analyzer "Rainbow Dash"
* Answer to the ultimate question of life, the universe and everything * Answer to the ultimate question of life, the universe and everything
* Add "Psychedelic Colour" mode to all analyzers * Add "Psychedelic Colour" mode to all analyzers
Other features: Other features:
* Add left click to fullsize cover on playing widget * Add left click to fullsize cover on playing widget.
* Add m4b support for non-drm files * Add m4b support for non-drm files.
* Ignore english articles for library sorting * Ignore English articles for library sorting.
* Previous track in dynamic random mix * Improve the organize dialog.
* Improve the organize dialog * Add an option to warn before closing a playlist tab.
* Add playlist save preference * Add an option to disable the pause notification.
* Add a preference to disable the pause notification * Add options to hide some internet services.
* Add a preference tab to hide some internet services * Add an option to disable inline song metadata editing.
* Add an option to disable inline song metadata editing * Add "details below" and "no details" now playing widget options.
* Use a save dialog option instead of quick change menu * Add "no song details" now playing widget option.
* Add ability to fetch lyrics from lololyrics.com * Add icons to the extras menu.
* Add support for monitors in portrait mode * Add a source icon for CD tracks.
* Add now playing widget mode * Allow user to remove directories in the Files tab.
* Add icons to extra * Add ability to remove unavailable items from playlist.
* Add a source icon for CD tracks * Add a button to the transcode dialog to add all files in a directory.
* Allow user to remove directories * Make it impossible to collapse either side of the MainWindow splitter.
* Add ability to remove unavailable items from playlist * Add menu items for updating and doing a full rescan of Google Drive.
* Add an import button to the transcode UI, allowing the user to pull in * Increase Soundcloud cover image size.
all files in a folder hierarchy to be transcoded * Add the ability to pause Spotify tracks.
* 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 the ability to add or remove a Spotify track to a Spotify playlist * Add the ability to add or remove a Spotify track to a Spotify playlist
through context menu through context menu.
* Add Spotify tracks to Spotify playlists by drag and drop * 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 get a link to share Spotify playlists and songs.
* Add ability to automatically set podcast as listened after sucesfully sending * Improve handling of Spotify Top Tracks and compilations.
it to a device * Add playlist actions to Spotify songs.
* Add ability to order podcasts by age * Add ability to automatically set podcast as listened after successfully
* Allow user to download multiple podcasts at the same time sending it to a device.
* Add ability to cancel podcast downloads in progress * Add ability to order podcasts by age.
* Allow user to hide listened podcast episodes * Allow user to download multiple podcasts at the same time.
* Huge improvement of the speed at startup * Add ability to cancel podcast downloads in progress.
* Improve performance of mass rating changes * Allow user to hide listened podcast episodes.
* Improve ripping performance * 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 * Persistent cache for pixmaps. Huge improvement of the performance when
scrolling the library for example scrolling the library for example.
* Add AppData file for Clementine (for GNOME and KDE Software Centers) * Add AppData file for Clementine (for GNOME and KDE Software Centers).
* Add iPod-like behaviour to previous button * Add iPod-like behaviour to previous button.
* Add "no song details" now playing widget option * Add HipHop and Kuduro equalizers.
* Ability to add tracks to Spotify starred playlist by drag and drop * Remember current playlist between restarts.
* Add HipHop and Kuduro equalizers * IDv3 tag lyrics support.
* Add AZLyrics lyric provider * Scroll to last played track when switching playlists.
* Remember current playlist between restarts * Add stop after each song repeat mode.
* (OSX) Use Alt+Tab to switch between playlist tabs * Sort discs numerically when using Group by disc.
* IDv3 tag lyrics support * Add ability for sort by group and performer in the library view.
* Improve handling of Spotify Top Tracks and compilations * Parse the year of a disc from musicbrainz.
* Scroll to last played track when switching playlists * Add track intro mode.
* Add stop after each song repeat mode * Add ability to add a search term with tab and space in the smart playlist
* Sort discs numerically when using Group by disc window.
* Add ability for sort by group and performer in the library view * Add love/ban (Last.fm) global shortcuts.
* Parse the year of a disc from musicbrainz * Add support for "original year" tags.
* Add track intro mode * Send album artist to Last.fm with liblastfm >= 1.0.0.
* Add ability to add a search term with tab and space in the smart * Add sample rate selection.
playlist window * Add option to change the time step when seeking using the keyboard.
* Add love/ban (lastfm) global shortcuts * Playlist sort by album considers disc and track numbers.
* Add support for "original year" tags * Add options for double clicking song in the playlist.
* Send album artist to Last.fm with liblastfm >= 1.0.0 * Volume slider handles glow effect using system theme.
* Add sample rate selection * 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: Bugfixes:
* Fix crash when click on a SoundCloud entry in internet tab * Fix crash when click on a SoundCloud entry in internet tab.
* Fix crash when marking podcast as listened * Fix crash when marking podcast as listened.
* Fix crash after pressing OK in the device properties window * Fix crash after pressing OK in the device properties window.
* Fix stop after track which doesn't remove now playing * Fix stop after track which doesn't remove now playing.
* Fix play bleeding into next track after auto stop * Fix play bleeding into next track after auto stop.
* Fix analyzer framerate when mouseover play scrubber * Fix analyzer framerate when mouseover play scrubber.
* Fix issues with buffers sent to analyzer * Fix issues with buffers sent to analyzer.
* Fix block analyzer framerate * Fix block analyzer framerate.
* Fix dbz possibility with small buffers at end of track * Fix divide-by-zero possibility with small buffers at end of track.
* Fix dbz possibility in moodbar * Fix divide-by-zero possibility in moodbar.
* Fix oversized album cover art * Fix oversized album cover art.
* Fix Grooveshark SSL errors * Clean cover art from /tmp.
* Clean cover art from /tmp
* Fix the rendering of the little numbers in the boxes on queued items in * Fix the rendering of the little numbers in the boxes on queued items in
the playlist the playlist.
* Fix parsing of MusicBrainz data for discid * Fix parsing of MusicBrainz data for discid.
* Fix random artifacting on nyanalyzer on startup * Fix random artifacting on nyanalyzer on startup.
* Fix podcasts length issues (which caused issues with seeking for example) * Fix podcasts length issues (which caused issues with seeking for example).
* Fix too small equalizer window size * Fix too small equalizer window size.
* Fix labels which don't inherit system text colors in the edit tag dialog * 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 the mess of the queue manager after playlist re-sort.
* Fix for queue ordering issue in the playlist view when using c-d to * Fix for queue ordering issue in the playlist view when using Ctrl+D to
dequeue a track dequeue a track.
* Fix detection of parent-relative paths in playlist saving * Fix detection of parent-relative paths in playlist saving.
* Fix path seperators issue when reading playlists * Fix path seperators issue when reading playlists.
* Fix m3u parser issue when an artist's name has a hyphen * Fix m3u parser issue when an artist's name has a hyphen.
* Fix bug with percents when fetch the Jamendo catalogue * Fix bug with percents when fetch the Jamendo catalogue.
* Fix a little dropout when transition to next track * Fix a little dropout when transition to next track.
* Fix broken RockRadio.com for premium users * Fix broken RockRadio.com for premium users.
* Fix Subsonic login with + characters in the password * Fix Subsonic login with + characters in the password.
* Fix accents issue in when save playlist in xspf format * Fix accents issue in when save playlist in xspf format.
* Fix issues with some songs length thanks to Taglib. People with Taglib * 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 installed on their system will have to wait a new release of Taglib.
* Fix moodbars not generating correctly * Fix moodbars not generating correctly.
* Fix socket leak in moodbar * Fix socket leak in moodbar.
* Fix memory leak in tagreader * Fix memory leak in tagreader.
* Fix crash when trying to fingerprint but missing a plugin * Fix crash when trying to fingerprint but missing a plugin.
* Fix infinite scan with Subsonic when the library is empty * Fix infinite scan with Subsonic when the library is empty.
* Fix shortcut/media keys issues on Mac * Fix shortcut/media keys issues on Mac.
* Fix compilation issues on Yosemite * Fix compilation issues on Yosemite.
* Fix performer tag for mpeg * Fix performer tag for mpeg.
* Fix parsing issues with "innovative" datetime formats * Fix parsing issues with "innovative" datetime formats.
* Fix laggy interface on Mac * Fix laggy interface on Mac.
* Fix crash in GrooveShark * Fix playback breaks in Spotify.
* Fix playback breaks in Spotify * Fix memory leaks.
* Fix memory leaks * Fix crash when stopping song that is fading after pausing.
* Fix crash when stopping song that is fading after pausing * Fix crash when trying to download a track but there is no current one
* Fix crash when trying to download a track but there is no current one playing playing.
* (OSX) Fix Soundcloud API Search which simply doesn't work * Fix default spinner gif image which shows white pixels around the image.
* 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 setting album artist tag for FLAC files if it already exists * Fix crash when Clementine lists the albums on Ampache.
* Fix crash when Clementine lists the albums on Ampache * Fix Last.fm scrobbling after seek.
* Fix Last.fm scrobbling after seek * Fix metadata not processed properly for some streams (Akamai).
* Fix metadata not processed properly for some streams (Akamai) * Fix save state when the song was paused.
* 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" * (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: Removed features:
* Remove Ubuntu One support * Remove Ubuntu One support.
* Remove Discogs support * Remove Discogs support.
* Remove GrooveShark support * Remove GrooveShark support.
* Remove Radio GFM support.
Build system changes: Build system changes:
* Update to gstreamer 1.0 * Update to gstreamer 1.0.
* Don't compile vreen with link-time optimizations * Don't compile vreen with link-time optimizations.
* Use the system's sha2 library if it's available * Use the system's sha2 library if it's available.
* (Windows) Add libgmp-10.dll which is required by libgiognutls.dll * Remove libindicate-qt.
* (Fedora) Don't depend on libplist or usbmuxd * Remove internal copy of libechonest and add it as dependency.
* Remove libindicate-qt * Use libcrypto++ instead of QCA.
* (Debian/Ubuntu) Remove internal copy of chromaprint and add it as * Update TagLib to 1.10.0.
dependency
* (Debian/Ubuntu) Add libmygpo-qt-dev (=> 1.0.7)
* Remove internal copy of libechonest and add it as dependency
* (Windows) Uninstall a previous install of Clementine when installing a new * (Windows) Uninstall a previous install of Clementine when installing a new
version version.
* (Windows) Remember the last installation path * (Windows) Remember the last installation path.
* (GNU/Linux) Follow freedesktop.org specifications for icons * (Windows) Add libgmp-10.dll which is required by libgiognutls.dll.
* (GNU/Linux) Add a 128x128 version of the Clementine icon * (Mac OS X) Use a GTlsDatabase for gstreamer SSL.
* (OSX) Use a GTlsDatabase for gstreamer SSL * (Linux) Follow freedesktop.org specifications for icons.
* Use libcrypto++ instead of QCA * (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 # Increment this whenever the user needs to download a new blob
# Remember to upload and sign the new version of the 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> Copyright: 2004, Melchior FRANZ <mfranz@kde.org>
License: GPL-2+ License: GPL-2+
Files: ext/libclementine-common/core/arraysize.h Files: ext/libclementine-common/core/scoped_nsautorelease_pool.*
ext/libclementine-common/core/scoped_nsautorelease_pool.*
src/core/scoped_nsobject.h src/core/scoped_nsobject.h
src/core/scoped_cftyperef.h src/core/scoped_cftyperef.h
Copyright: 2011, The Chromium Authors Copyright: 2011, The Chromium Authors

View File

@ -1,6 +1,6 @@
Name: clementine Name: clementine
Version: @CLEMENTINE_VERSION_RPM_V@ 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 Summary: A music player and library organiser
Group: Applications/Multimedia Group: Applications/Multimedia
@ -61,7 +61,7 @@ Features include:
%build %build
cd bin 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} make %{?_smp_mflags}
%install %install

View File

@ -1,34 +1,5 @@
// Copyright 2014 The Chromium Authors. All rights reserved. template <typename T>
// Use of this source code is governed by a BSD-style license that can be constexpr size_t arraysize(const T&) {
// found in the LICENSE file. static_assert(std::is_array<T>::value, "Argument must be array");
return std::extent<T>::value;
// 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)))

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> #include <execinfo.h>
#endif #endif
#include <iostream>
#include <QCoreApplication> #include <QCoreApplication>
#include <QDateTime> #include <QDateTime>
#include <QStringList> #include <QStringList>
@ -204,10 +206,11 @@ QDebug CreateLogger(Level level, const QString& class_name, int line) {
} }
QDebug ret(type); QDebug ret(type);
ret.nospace() << kMessageHandlerMagic << QDateTime::currentDateTime() ret.nospace() << kMessageHandlerMagic
.toString("hh:mm:ss.zzz") << QDateTime::currentDateTime()
.toLatin1() .toString("hh:mm:ss.zzz")
.constData() << level_name .toLatin1()
.constData() << level_name
<< function_line.leftJustified(32).toLatin1().constData(); << function_line.leftJustified(32).toLatin1().constData();
return ret.space(); return ret.space();
@ -259,7 +262,8 @@ void DumpStackTrace() {
backtrace_symbols(reinterpret_cast<void**>(&callstack), callstack_size); backtrace_symbols(reinterpret_cast<void**>(&callstack), callstack_size);
// Start from 1 to skip ourself. // Start from 1 to skip ourself.
for (int i = 1; i < callstack_size; ++i) { 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); free(symbols);
#else #else
@ -288,7 +292,7 @@ QDebug CreateLoggerDebug(int line, const char *class_name) { return qCreateLogge
namespace { namespace {
template<typename T> template <typename T>
QString print_duration(T duration, const std::string& unit) { QString print_duration(T duration, const std::string& unit) {
return QString("%1%2").arg(duration.count()).arg(unit.c_str()); return QString("%1%2").arg(duration.count()).arg(unit.c_str());
} }

View File

@ -205,6 +205,7 @@ set(SOURCES
library/libraryview.cpp library/libraryview.cpp
library/libraryviewcontainer.cpp library/libraryviewcontainer.cpp
library/librarywatcher.cpp library/librarywatcher.cpp
library/savedgroupingmanager.cpp
library/sqlrow.cpp library/sqlrow.cpp
musicbrainz/acoustidclient.cpp musicbrainz/acoustidclient.cpp
@ -506,7 +507,8 @@ set(HEADERS
library/libraryview.h library/libraryview.h
library/libraryviewcontainer.h library/libraryviewcontainer.h
library/librarywatcher.h library/librarywatcher.h
library/savedgroupingmanager.h
musicbrainz/acoustidclient.h musicbrainz/acoustidclient.h
musicbrainz/musicbrainzclient.h musicbrainz/musicbrainzclient.h
musicbrainz/tagfetcher.h musicbrainz/tagfetcher.h
@ -693,6 +695,7 @@ set(UI
library/libraryfilterwidget.ui library/libraryfilterwidget.ui
library/librarysettingspage.ui library/librarysettingspage.ui
library/libraryviewcontainer.ui library/libraryviewcontainer.ui
library/savedgroupingmanager.ui
playlist/dynamicplaylistcontrols.ui playlist/dynamicplaylistcontrols.ui
playlist/playlistcontainer.ui playlist/playlistcontainer.ui

View File

@ -21,31 +21,39 @@
*/ */
#include "application.h" #include "application.h"
#include "appearance.h"
#include "config.h" #include "config.h"
#include "database.h" #include "core/appearance.h"
#include "player.h" #include "core/database.h"
#include "tagreaderclient.h" #include "core/lazy.h"
#include "taskmanager.h" #include "core/player.h"
#include "core/tagreaderclient.h"
#include "core/taskmanager.h"
#include "covers/albumcoverloader.h" #include "covers/albumcoverloader.h"
#include "covers/amazoncoverprovider.h"
#include "covers/coverproviders.h" #include "covers/coverproviders.h"
#include "covers/currentartloader.h" #include "covers/currentartloader.h"
#include "covers/musicbrainzcoverprovider.h"
#include "devices/devicemanager.h" #include "devices/devicemanager.h"
#include "internet/core/internetmodel.h"
#include "globalsearch/globalsearch.h" #include "globalsearch/globalsearch.h"
#include "library/library.h" #include "internet/core/internetmodel.h"
#include "library/librarybackend.h" #include "internet/core/scrobbler.h"
#include "networkremote/networkremote.h"
#include "networkremote/networkremotehelper.h"
#include "playlist/playlistbackend.h"
#include "playlist/playlistmanager.h"
#include "internet/podcasts/gpoddersync.h" #include "internet/podcasts/gpoddersync.h"
#include "internet/podcasts/podcastbackend.h" #include "internet/podcasts/podcastbackend.h"
#include "internet/podcasts/podcastdeleter.h" #include "internet/podcasts/podcastdeleter.h"
#include "internet/podcasts/podcastdownloader.h" #include "internet/podcasts/podcastdownloader.h"
#include "internet/podcasts/podcastupdater.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 #ifdef HAVE_LIBLASTFM
#include "covers/lastfmcoverprovider.h"
#include "internet/lastfm/lastfmservice.h" #include "internet/lastfm/lastfmservice.h"
#endif // HAVE_LIBLASTFM #endif // HAVE_LIBLASTFM
@ -56,101 +64,137 @@
bool Application::kIsPortable = false; bool Application::kIsPortable = false;
Application::Application(QObject* parent) class ApplicationImpl {
: QObject(parent), public:
tag_reader_client_(nullptr), ApplicationImpl(Application* app)
database_(nullptr), : tag_reader_client_([=]() {
album_cover_loader_(nullptr), TagReaderClient* client = new TagReaderClient(app);
playlist_backend_(nullptr), app->MoveToNewThread(client);
podcast_backend_(nullptr), client->Start();
appearance_(nullptr), return client;
cover_providers_(nullptr), }),
task_manager_(nullptr), database_([=]() {
player_(nullptr), Database* db = new Database(app, app);
playlist_manager_(nullptr), app->MoveToNewThread(db);
current_art_loader_(nullptr), DoInAMinuteOrSo(db, SLOT(DoBackup()));
global_search_(nullptr), return db;
internet_model_(nullptr), }),
library_(nullptr), album_cover_loader_([=]() {
device_manager_(nullptr), AlbumCoverLoader* loader = new AlbumCoverLoader(app);
podcast_updater_(nullptr), app->MoveToNewThread(loader);
podcast_deleter_(nullptr), return loader;
podcast_downloader_(nullptr), }),
gpodder_sync_(nullptr), playlist_backend_([=]() {
moodbar_loader_(nullptr), PlaylistBackend* backend = new PlaylistBackend(app, app);
moodbar_controller_(nullptr), app->MoveToThread(backend, database_->thread());
network_remote_(nullptr), return backend;
network_remote_helper_(nullptr), }),
scrobbler_(nullptr) { podcast_backend_([=]() {
tag_reader_client_ = new TagReaderClient(this); PodcastBackend* backend = new PodcastBackend(app, app);
MoveToNewThread(tag_reader_client_); app->MoveToThread(backend, database_->thread());
tag_reader_client_->Start(); return backend;
}),
database_ = new Database(this, this); appearance_([=]() { return new Appearance(app); }),
MoveToNewThread(database_); cover_providers_([=]() {
CoverProviders* cover_providers = new CoverProviders(app);
album_cover_loader_ = new AlbumCoverLoader(this); // Initialize the repository of cover providers.
MoveToNewThread(album_cover_loader_); cover_providers->AddProvider(new AmazonCoverProvider);
cover_providers->AddProvider(new MusicbrainzCoverProvider);
playlist_backend_ = new PlaylistBackend(this, this); #ifdef HAVE_LIBLASTFM
MoveToThread(playlist_backend_, database_->thread()); cover_providers->AddProvider(new LastFmCoverProvider(app));
#endif
podcast_backend_ = new PodcastBackend(this, this); return cover_providers;
MoveToThread(podcast_backend_, database_->thread()); }),
task_manager_([=]() { return new TaskManager(app); }),
appearance_ = new Appearance(this); player_([=]() { return new Player(app, app); }),
cover_providers_ = new CoverProviders(this); playlist_manager_([=]() { return new PlaylistManager(app); }),
task_manager_ = new TaskManager(this); current_art_loader_([=]() { return new CurrentArtLoader(app, app); }),
player_ = new Player(this, this); global_search_([=]() { return new GlobalSearch(app, app); }),
playlist_manager_ = new PlaylistManager(this, this); internet_model_([=]() { return new InternetModel(app, app); }),
current_art_loader_ = new CurrentArtLoader(this, this); library_([=]() { return new Library(app, app); }),
global_search_ = new GlobalSearch(this, this); device_manager_([=]() { return new DeviceManager(app, app); }),
internet_model_ = new InternetModel(this, this); podcast_updater_([=]() { return new PodcastUpdater(app, app); }),
library_ = new Library(this, this); podcast_deleter_([=]() {
device_manager_ = new DeviceManager(this, this); PodcastDeleter* deleter = new PodcastDeleter(app, app);
podcast_updater_ = new PodcastUpdater(this, this); app->MoveToNewThread(deleter);
return deleter;
podcast_deleter_ = new PodcastDeleter(this, this); }),
MoveToNewThread(podcast_deleter_); podcast_downloader_([=]() { return new PodcastDownloader(app, app); }),
gpodder_sync_([=]() { return new GPodderSync(app, app); }),
podcast_downloader_ = new PodcastDownloader(this, this); moodbar_loader_([=]() {
gpodder_sync_ = new GPodderSync(this, this);
#ifdef HAVE_MOODBAR #ifdef HAVE_MOODBAR
moodbar_loader_ = new MoodbarLoader(this, this); return new MoodbarLoader(app, app);
moodbar_controller_ = new MoodbarController(this, this); #else
return nullptr;
#endif #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 Lazy<TagReaderClient> tag_reader_client_;
network_remote_ = new NetworkRemote(this); Lazy<Database> database_;
MoveToNewThread(network_remote_); 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 // In the constructor the helper waits for the signal
// PlaylistManagerInitialized // PlaylistManagerInitialized
// to start the remote. Without the playlist manager clementine can // to start the remote. Without the playlist manager clementine can
// crash when a client connects before the manager is initialized! // crash when a client connects before the manager is initialized!
network_remote_helper_ = new NetworkRemoteHelper(this); network_remote_helper();
library()->Init();
#ifdef HAVE_LIBLASTFM // TODO(John Maguire): Make this not a weird singleton.
scrobbler_ = new LastFMService(this, this); tag_reader_client();
#endif // HAVE_LIBLASTFM
library_->Init();
DoInAMinuteOrSo(database_, SLOT(DoBackup()));
} }
Application::~Application() { Application::~Application() {
// It's important that the device manager is deleted before the database. // It's important that the device manager is deleted before the database.
// Deleting the database deletes all objects that have been created in its // Deleting the database deletes all objects that have been created in its
// thread, including some device library backends. // thread, including some device library backends.
delete device_manager_; p_->device_manager_.reset();
device_manager_ = nullptr;
for (QObject* object : objects_in_threads_) {
object->deleteLater();
}
for (QThread* thread : threads_) { for (QThread* thread : threads_) {
thread->quit(); thread->quit();
@ -173,7 +217,6 @@ void Application::MoveToNewThread(QObject* object) {
void Application::MoveToThread(QObject* object, QThread* thread) { void Application::MoveToThread(QObject* object, QThread* thread) {
object->setParent(nullptr); object->setParent(nullptr);
object->moveToThread(thread); object->moveToThread(thread);
objects_in_threads_ << object;
} }
void Application::AddError(const QString& message) { emit ErrorAdded(message); } void Application::AddError(const QString& message) { emit ErrorAdded(message); }
@ -186,14 +229,100 @@ QString Application::language_without_region() const {
return language_name_; 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 { LibraryBackend* Application::library_backend() const {
return library()->backend(); return library()->backend();
} }
LibraryModel* Application::library_model() const { return library()->model(); } LibraryModel* Application::library_model() const { return library()->model(); }
void Application::ReloadSettings() { emit SettingsChanged(); } MoodbarController* Application::moodbar_controller() const {
return p_->moodbar_controller_.get();
void Application::OpenSettingsDialogAtPage(SettingsDialog::Page page) { }
emit SettingsDialogRequested(page);
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_ #ifndef CORE_APPLICATION_H_
#define CORE_APPLICATION_H_ #define CORE_APPLICATION_H_
#include "ui/settingsdialog.h" #include <memory>
#include <QObject> #include <QObject>
#include "ui/settingsdialog.h"
class AlbumCoverLoader; class AlbumCoverLoader;
class Appearance; class Appearance;
class ApplicationImpl;
class CoverProviders; class CoverProviders;
class CurrentArtLoader; class CurrentArtLoader;
class Database; class Database;
@ -44,10 +47,10 @@ class NetworkRemote;
class NetworkRemoteHelper; class NetworkRemoteHelper;
class Player; class Player;
class PlaylistBackend; class PlaylistBackend;
class PodcastDeleter;
class PodcastDownloader;
class PlaylistManager; class PlaylistManager;
class PodcastBackend; class PodcastBackend;
class PodcastDeleter;
class PodcastDownloader;
class PodcastUpdater; class PodcastUpdater;
class Scrobbler; class Scrobbler;
class TagReaderClient; class TagReaderClient;
@ -68,35 +71,32 @@ class Application : public QObject {
QString language_without_region() const; QString language_without_region() const;
void set_language_name(const QString& name) { language_name_ = name; } void set_language_name(const QString& name) { language_name_ = name; }
TagReaderClient* tag_reader_client() const { return tag_reader_client_; } AlbumCoverLoader* album_cover_loader() const;
Database* database() const { return database_; } Appearance* appearance() const;
AlbumCoverLoader* album_cover_loader() const { return album_cover_loader_; } CoverProviders* cover_providers() const;
PlaylistBackend* playlist_backend() const { return playlist_backend_; } CurrentArtLoader* current_art_loader() const;
PodcastBackend* podcast_backend() const { return podcast_backend_; } Database* database() const;
Appearance* appearance() const { return appearance_; } DeviceManager* device_manager() const;
CoverProviders* cover_providers() const { return cover_providers_; } GlobalSearch* global_search() const;
TaskManager* task_manager() const { return task_manager_; } GPodderSync* gpodder_sync() const;
Player* player() const { return player_; } InternetModel* internet_model() const;
PlaylistManager* playlist_manager() const { return playlist_manager_; } Library* library() const;
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_; }
LibraryBackend* library_backend() const; LibraryBackend* library_backend() const;
LibraryModel* library_model() 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 MoveToNewThread(QObject* object);
void MoveToThread(QObject* object, QThread* thread); void MoveToThread(QObject* object, QThread* thread);
@ -106,40 +106,14 @@ class Application : public QObject {
void ReloadSettings(); void ReloadSettings();
void OpenSettingsDialogAtPage(SettingsDialog::Page page); void OpenSettingsDialogAtPage(SettingsDialog::Page page);
signals: signals:
void ErrorAdded(const QString& message); void ErrorAdded(const QString& message);
void SettingsChanged(); void SettingsChanged();
void SettingsDialogRequested(SettingsDialog::Page page); void SettingsDialogRequested(SettingsDialog::Page page);
private: private:
QString language_name_; QString language_name_;
std::unique_ptr<ApplicationImpl> p_;
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_;
QList<QThread*> threads_; QList<QThread*> threads_;
}; };

View File

@ -512,7 +512,6 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
d->genre_ = QStringFromStdString(pb.genre()); d->genre_ = QStringFromStdString(pb.genre());
d->comment_ = QStringFromStdString(pb.comment()); d->comment_ = QStringFromStdString(pb.comment());
d->compilation_ = pb.compilation(); d->compilation_ = pb.compilation();
d->playcount_ = pb.playcount();
d->skipcount_ = pb.skipcount(); d->skipcount_ = pb.skipcount();
d->lastplayed_ = pb.lastplayed(); d->lastplayed_ = pb.lastplayed();
d->score_ = pb.score(); d->score_ = pb.score();
@ -536,6 +535,10 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
d->rating_ = pb.rating(); d->rating_ = pb.rating();
} }
if (pb.has_playcount()) {
d->playcount_ = pb.playcount();
}
InitArtManual(); InitArtManual();
} }

View File

@ -26,6 +26,7 @@
#include "core/application.h" #include "core/application.h"
#include "covers/albumcoverloader.h" #include "covers/albumcoverloader.h"
#include "playlist/playlistmanager.h" #include "playlist/playlistmanager.h"
#include "ui/iconloader.h"
CurrentArtLoader::CurrentArtLoader(Application* app, QObject* parent) CurrentArtLoader::CurrentArtLoader(Application* app, QObject* parent)
: QObject(parent), : QObject(parent),
@ -34,7 +35,9 @@ CurrentArtLoader::CurrentArtLoader(Application* app, QObject* parent)
id_(0) { id_(0) {
options_.scale_output_image_ = false; options_.scale_output_image_ = false;
options_.pad_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)), connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)),
SLOT(TempArtLoaded(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)), connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)),
SLOT(AlbumArtLoaded(quint64, QImage))); SLOT(AlbumArtLoaded(quint64, QImage)));
connect(this, SIGNAL(SearchAsyncSig(int,QString)),
this, SLOT(DoSearchAsync(int,QString)));
ConnectProvider(url_provider_); ConnectProvider(url_provider_);
} }
@ -82,6 +84,13 @@ void GlobalSearch::AddProvider(SearchProvider* provider) {
int GlobalSearch::SearchAsync(const QString& query) { int GlobalSearch::SearchAsync(const QString& query) {
const int id = next_id_++; const int id = next_id_++;
emit SearchAsyncSig(id, query);
return id;
}
void GlobalSearch::DoSearchAsync(int id, const QString& query) {
pending_search_providers_[id] = 0; pending_search_providers_[id] = 0;
int timer_id = -1; int timer_id = -1;
@ -106,8 +115,6 @@ int GlobalSearch::SearchAsync(const QString& query) {
} }
} }
} }
return id;
} }
void GlobalSearch::CancelSearch(int id) { void GlobalSearch::CancelSearch(int id) {

View File

@ -62,9 +62,11 @@ class GlobalSearch : public QObject {
bool is_provider_usable(SearchProvider* provider) const; bool is_provider_usable(SearchProvider* provider) const;
public slots: public slots:
void ReloadSettings(); void ReloadSettings();
signals: signals:
void SearchAsyncSig(int id, const QString& query);
void ResultsAvailable(int id, const SearchProvider::ResultList& results); void ResultsAvailable(int id, const SearchProvider::ResultList& results);
void ProviderSearchFinished(int id, const SearchProvider* provider); void ProviderSearchFinished(int id, const SearchProvider* provider);
void SearchFinished(int id); void SearchFinished(int id);
@ -79,6 +81,7 @@ signals:
void timerEvent(QTimerEvent* e); void timerEvent(QTimerEvent* e);
private slots: private slots:
void DoSearchAsync(int id, const QString& query);
void ResultsAvailableSlot(int id, SearchProvider::ResultList results); void ResultsAvailableSlot(int id, SearchProvider::ResultList results);
void SearchFinishedSlot(int id); void SearchFinishedSlot(int id);

View File

@ -33,8 +33,10 @@ GlobalSearchModel::GlobalSearchModel(GlobalSearch* engine, QObject* parent)
group_by_[1] = LibraryModel::GroupBy_Album; group_by_[1] = LibraryModel::GroupBy_Album;
group_by_[2] = LibraryModel::GroupBy_None; group_by_[2] = LibraryModel::GroupBy_None;
no_cover_icon_ = QPixmap(":nocover.png").scaled( QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
LibraryModel::kPrettyCoverSize, LibraryModel::kPrettyCoverSize, no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(
LibraryModel::kPrettyCoverSize,
LibraryModel::kPrettyCoverSize,
Qt::KeepAspectRatio, Qt::SmoothTransformation); Qt::KeepAspectRatio, Qt::SmoothTransformation);
} }

View File

@ -30,7 +30,7 @@
#include "core/closure.h" #include "core/closure.h"
#include "core/logging.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; using std::numeric_limits;

View File

@ -42,10 +42,32 @@ InternetService::InternetService(const QString& name, Application* app,
app_(app), app_(app),
model_(model), model_(model),
name_(name), name_(name),
append_to_playlist_(nullptr), append_to_playlist_([&]() {
replace_playlist_(nullptr), QAction* action = new QAction(
open_in_new_playlist_(nullptr), IconLoader::Load("media-playback-start", IconLoader::Base),
separator_(nullptr) {} 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) { void InternetService::ShowUrlBox(const QString& title, const QString& url) {
QMessageBox url_box; QMessageBox url_box;
@ -64,50 +86,21 @@ void InternetService::ShowUrlBox(const QString& title, const QString& url) {
} }
QList<QAction*> InternetService::GetPlaylistActions() { QList<QAction*> InternetService::GetPlaylistActions() {
if (!separator_) {
separator_ = new QAction(this);
separator_->setSeparator(true);
}
return QList<QAction*>() << GetAppendToPlaylistAction() return QList<QAction*>() << GetAppendToPlaylistAction()
<< GetReplacePlaylistAction() << GetReplacePlaylistAction()
<< GetOpenInNewPlaylistAction() << separator_; << GetOpenInNewPlaylistAction() << separator_.get();
} }
QAction* InternetService::GetAppendToPlaylistAction() { QAction* InternetService::GetAppendToPlaylistAction() {
if (!append_to_playlist_) { return append_to_playlist_.get();
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_;
} }
QAction* InternetService::GetReplacePlaylistAction() { QAction* InternetService::GetReplacePlaylistAction() {
if (!replace_playlist_) { return replace_playlist_.get();
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_;
} }
QAction* InternetService::GetOpenInNewPlaylistAction() { QAction* InternetService::GetOpenInNewPlaylistAction() {
if (!open_in_new_playlist_) { return open_in_new_playlist_.get();
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_;
} }
void InternetService::AddItemToPlaylist(const QModelIndex& index, void InternetService::AddItemToPlaylist(const QModelIndex& index,

View File

@ -23,10 +23,12 @@
#ifndef INTERNET_CORE_INTERNETSERVICE_H_ #ifndef INTERNET_CORE_INTERNETSERVICE_H_
#define INTERNET_CORE_INTERNETSERVICE_H_ #define INTERNET_CORE_INTERNETSERVICE_H_
#include <QAction>
#include <QObject> #include <QObject>
#include <QList> #include <QList>
#include <QUrl> #include <QUrl>
#include "core/lazy.h"
#include "core/song.h" #include "core/song.h"
#include "playlist/playlistitem.h" #include "playlist/playlistitem.h"
#include "smartplaylists/generator.h" #include "smartplaylists/generator.h"
@ -83,7 +85,7 @@ class InternetService : public QObject {
virtual QString Icon() { return QString(); } virtual QString Icon() { return QString(); }
signals: signals:
void StreamError(const QString& message); void StreamError(const QString& message);
void StreamMetadataFound(const QUrl& original_url, const Song& song); void StreamMetadataFound(const QUrl& original_url, const Song& song);
@ -135,10 +137,10 @@ class InternetService : public QObject {
InternetModel* model_; InternetModel* model_;
QString name_; QString name_;
QAction* append_to_playlist_; Lazy<QAction> append_to_playlist_;
QAction* replace_playlist_; Lazy<QAction> replace_playlist_;
QAction* open_in_new_playlist_; Lazy<QAction> open_in_new_playlist_;
QAction* separator_; Lazy<QAction> separator_;
}; };
Q_DECLARE_METATYPE(InternetService*); Q_DECLARE_METATYPE(InternetService*);

View File

@ -32,7 +32,6 @@ InternetView::InternetView(QWidget* parent) : AutoExpandingTreeView(parent) {
SetExpandOnReset(false); SetExpandOnReset(false);
setAttribute(Qt::WA_MacShowFocusRect, false); setAttribute(Qt::WA_MacShowFocusRect, false);
setSelectionMode(QAbstractItemView::ExtendedSelection); setSelectionMode(QAbstractItemView::ExtendedSelection);
setAnimated(true);
} }
void InternetView::contextMenuEvent(QContextMenuEvent* e) { void InternetView::contextMenuEvent(QContextMenuEvent* e) {
@ -47,11 +46,6 @@ void InternetView::contextMenuEvent(QContextMenuEvent* e) {
e->globalPos()); e->globalPos());
} }
void InternetView::currentChanged(const QModelIndex& current,
const QModelIndex&) {
emit CurrentIndexChanged(current);
}
void InternetView::setModel(QAbstractItemModel* model) { void InternetView::setModel(QAbstractItemModel* model) {
AutoExpandingTreeView::setModel(model); AutoExpandingTreeView::setModel(model);

View File

@ -33,11 +33,7 @@ class InternetView : public AutoExpandingTreeView {
void contextMenuEvent(QContextMenuEvent* e); void contextMenuEvent(QContextMenuEvent* e);
// QTreeView // QTreeView
void currentChanged(const QModelIndex& current, const QModelIndex& previous);
void setModel(QAbstractItemModel* model); void setModel(QAbstractItemModel* model);
signals:
void CurrentIndexChanged(const QModelIndex& index);
}; };
#endif // INTERNET_CORE_INTERNETVIEW_H_ #endif // INTERNET_CORE_INTERNETVIEW_H_

View File

@ -53,7 +53,6 @@ DigitallyImportedServiceBase::DigitallyImportedServiceBase(
api_service_name_(api_service_name), api_service_name_(api_service_name),
network_(new NetworkAccessManager(this)), network_(new NetworkAccessManager(this)),
url_handler_(new DigitallyImportedUrlHandler(app, this)), url_handler_(new DigitallyImportedUrlHandler(app, this)),
basic_audio_type_(1),
premium_audio_type_(2), premium_audio_type_(2),
has_premium_(has_premium), has_premium_(has_premium),
root_(nullptr), root_(nullptr),
@ -66,10 +65,6 @@ DigitallyImportedServiceBase::DigitallyImportedServiceBase(
model->app()->global_search()->AddProvider( model->app()->global_search()->AddProvider(
new DigitallyImportedSearchProvider(this, app_, this)); 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" premium_playlists_ << "http://%1/premium_high/%2.pls?hash=%3"
<< "http://%1/premium_medium/%2.pls?hash=%3" << "http://%1/premium_medium/%2.pls?hash=%3"
<< "http://%1/premium/%2.pls?hash=%3" << "http://%1/premium/%2.pls?hash=%3"
@ -130,14 +125,18 @@ void DigitallyImportedServiceBase::RefreshStreamsFinished(QNetworkReply* reply,
void DigitallyImportedServiceBase::PopulateStreams() { void DigitallyImportedServiceBase::PopulateStreams() {
if (root_->hasChildren()) root_->removeRows(0, root_->rowCount()); if (root_->hasChildren()) root_->removeRows(0, root_->rowCount());
if (!is_premium_account()) {
ShowSettingsDialog();
return;
}
// Add each stream to the model // Add each stream to the model
for (const DigitallyImportedClient::Channel& channel : saved_channels_) { for (const DigitallyImportedClient::Channel& channel : saved_channels_) {
Song song; Song song;
SongFromChannel(channel, &song); SongFromChannel(channel, &song);
QStandardItem* item = QStandardItem* item = new QStandardItem(
new QStandardItem(IconLoader::Load("icon_radio", IconLoader::Load("icon_radio", IconLoader::Lastfm), song.title());
IconLoader::Lastfm), song.title());
item->setData(channel.description_, Qt::ToolTipRole); item->setData(channel.description_, Qt::ToolTipRole);
item->setData(InternetModel::PlayBehaviour_SingleItem, item->setData(InternetModel::PlayBehaviour_SingleItem,
InternetModel::Role_PlayBehaviour); InternetModel::Role_PlayBehaviour);
@ -162,7 +161,6 @@ void DigitallyImportedServiceBase::ReloadSettings() {
QSettings s; QSettings s;
s.beginGroup(kSettingsGroup); s.beginGroup(kSettingsGroup);
basic_audio_type_ = s.value("basic_audio_type", 1).toInt();
premium_audio_type_ = s.value("premium_audio_type", 2).toInt(); premium_audio_type_ = s.value("premium_audio_type", 2).toInt();
username_ = s.value("username").toString(); username_ = s.value("username").toString();
listen_hash_ = s.value("listen_hash").toString(); listen_hash_ = s.value("listen_hash").toString();
@ -180,8 +178,8 @@ void DigitallyImportedServiceBase::ShowContextMenu(const QPoint& global_pos) {
tr("Refresh streams"), this, tr("Refresh streams"), this,
SLOT(ForceRefreshStreams())); SLOT(ForceRefreshStreams()));
context_menu_->addSeparator(); context_menu_->addSeparator();
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base), context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
tr("Configure..."), this, tr("Configure..."), this,
SLOT(ShowSettingsDialog())); SLOT(ShowSettingsDialog()));
} }
@ -224,12 +222,8 @@ void DigitallyImportedServiceBase::LoadStation(const QString& key) {
// Replace "www." with "listen." in the hostname. // Replace "www." with "listen." in the hostname.
const QString host = "listen." + homepage_url_.host().remove("www."); const QString host = "listen." + homepage_url_.host().remove("www.");
if (is_premium_account()) { playlist_url = QUrl(
playlist_url = QUrl( premium_playlists_[premium_audio_type_].arg(host, key, listen_hash_));
premium_playlists_[premium_audio_type_].arg(host, key, listen_hash_));
} else {
playlist_url = QUrl(basic_playlists_[basic_audio_type_].arg(host, key));
}
qLog(Debug) << "Getting playlist URL" << playlist_url; qLog(Debug) << "Getting playlist URL" << playlist_url;
@ -241,32 +235,28 @@ void DigitallyImportedServiceBase::LoadStation(const QString& key) {
DigitallyImportedService::DigitallyImportedService(Application* app, DigitallyImportedService::DigitallyImportedService(Application* app,
InternetModel* model, InternetModel* model,
QObject* parent) QObject* parent)
: DigitallyImportedServiceBase("DigitallyImported", "Digitally Imported", : DigitallyImportedServiceBase(
QUrl("http://www.di.fm"), "DigitallyImported", "Digitally Imported", QUrl("http://www.di.fm"),
IconLoader::Load("digitallyimported", IconLoader::Load("digitallyimported", IconLoader::Provider), "di",
IconLoader::Provider), app, model, true, parent) {}
"di", app, model, true, parent) {}
RadioTunesService::RadioTunesService(Application* app, InternetModel* model, RadioTunesService::RadioTunesService(Application* app, InternetModel* model,
QObject* parent) QObject* parent)
: DigitallyImportedServiceBase("RadioTunes", "RadioTunes.com", : DigitallyImportedServiceBase(
QUrl("http://www.radiotunes.com/"), "RadioTunes", "RadioTunes.com", QUrl("http://www.radiotunes.com/"),
IconLoader::Load("radiotunes", IconLoader::Load("radiotunes", IconLoader::Provider), "radiotunes",
IconLoader::Provider), app, model, true, parent) {}
"radiotunes", app, model, true, parent) {}
JazzRadioService::JazzRadioService(Application* app, InternetModel* model, JazzRadioService::JazzRadioService(Application* app, InternetModel* model,
QObject* parent) QObject* parent)
: DigitallyImportedServiceBase("JazzRadio", "JAZZRADIO.com", : DigitallyImportedServiceBase(
QUrl("http://www.jazzradio.com"), "JazzRadio", "JAZZRADIO.com", QUrl("http://www.jazzradio.com"),
IconLoader::Load("jazzradio", IconLoader::Load("jazzradio", IconLoader::Provider), "jazzradio", app,
IconLoader::Provider), model, true, parent) {}
"jazzradio", app, model, true, parent) {}
RockRadioService::RockRadioService(Application* app, InternetModel* model, RockRadioService::RockRadioService(Application* app, InternetModel* model,
QObject* parent) QObject* parent)
: DigitallyImportedServiceBase("RockRadio", "ROCKRADIO.com", : DigitallyImportedServiceBase(
QUrl("http://www.rockradio.com"), "RockRadio", "ROCKRADIO.com", QUrl("http://www.rockradio.com"),
IconLoader::Load("rockradio", IconLoader::Load("rockradio", IconLoader::Provider), "rockradio", app,
IconLoader::Provider), model, false, parent) {}
"rockradio", app, model, false, parent) {}

View File

@ -89,13 +89,11 @@ signals:
QString service_description_; QString service_description_;
QString api_service_name_; QString api_service_name_;
QStringList basic_playlists_;
QStringList premium_playlists_; QStringList premium_playlists_;
QNetworkAccessManager* network_; QNetworkAccessManager* network_;
DigitallyImportedUrlHandler* url_handler_; DigitallyImportedUrlHandler* url_handler_;
int basic_audio_type_;
int premium_audio_type_; int premium_audio_type_;
QString username_; QString username_;
QString listen_hash_; QString listen_hash_;

View File

@ -45,9 +45,7 @@ DigitallyImportedSettingsPage::DigitallyImportedSettingsPage(
ui_->login_state->AddCredentialField(ui_->password); ui_->login_state->AddCredentialField(ui_->password);
ui_->login_state->AddCredentialGroup(ui_->credential_group); ui_->login_state->AddCredentialGroup(ui_->credential_group);
ui_->login_state->SetAccountTypeText( ui_->login_state->SetAccountTypeText(tr("A premium account is required"));
tr("You can listen for free without an account, but Premium members can "
"listen to higher quality streams without advertisements."));
ui_->login_state->SetAccountTypeVisible(true); ui_->login_state->SetAccountTypeVisible(true);
} }

View File

@ -56,6 +56,12 @@ UrlHandler::LoadResult DigitallyImportedUrlHandler::StartLoading(
return ret; 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 // Start loading the station
const QString key = url.host(); const QString key = url.host();
qLog(Info) << "Loading station" << key; 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::kAudioscrobblerClientId = "tng";
const char* LastFMService::kApiKey = "75d20fb472be99275392aefa2760ea09"; const char* LastFMService::kApiKey = "75d20fb472be99275392aefa2760ea09";
const char* LastFMService::kSecret = "d3072b60ae626be12be69448f5c46e70"; 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) LastFMService::LastFMService(Application* app, QObject* parent)
: Scrobbler(parent), : Scrobbler(parent),
scrobbler_(nullptr),
already_scrobbled_(false), already_scrobbled_(false),
scrobbling_enabled_(false), scrobbling_enabled_(false),
connection_problems_(false), connection_problems_(false),
app_(app) { app_(app) {
#ifdef HAVE_LIBLASTFM1
lastfm::ws::setScheme(lastfm::ws::Https);
#endif
ReloadSettings(); ReloadSettings();
// we emit the signal the first time to be sure the buttons are in the right // we emit the signal the first time to be sure the buttons are in the right
// state // state
emit ScrobblingEnabledChanged(scrobbling_enabled_); emit ScrobblingEnabledChanged(scrobbling_enabled_);
app_->cover_providers()->AddProvider(new LastFmCoverProvider(this));
} }
LastFMService::~LastFMService() {} LastFMService::~LastFMService() {}
@ -120,13 +123,32 @@ bool LastFMService::IsSubscriber() const {
return settings.value("Subscriber", false).toBool(); return settings.value("Subscriber", false).toBool();
} }
void LastFMService::Authenticate(const QString& username, void LastFMService::GetToken() {
const QString& password) {
QMap<QString, QString> params; QMap<QString, QString> params;
params["method"] = "auth.getMobileSession"; params["method"] = "auth.getToken";
params["username"] = username; QNetworkReply* reply = lastfm::ws::post(params);
params["authToken"] = NewClosure(reply, SIGNAL(finished()), this,
lastfm::md5((username + lastfm::md5(password.toUtf8())).toUtf8()); 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); QNetworkReply* reply = lastfm::ws::post(params);
NewClosure(reply, SIGNAL(finished()), this, 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 // 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) { void LastFMService::AuthenticateReplyFinished(QNetworkReply* reply) {
reply->deleteLater(); reply->deleteLater();
@ -168,12 +179,22 @@ void LastFMService::AuthenticateReplyFinished(QNetworkReply* reply) {
} }
// Invalidate the scrobbler - it will get recreated later // Invalidate the scrobbler - it will get recreated later
delete scrobbler_; scrobbler_.reset(nullptr);
scrobbler_ = nullptr;
emit AuthenticationComplete(true, QString()); 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() { void LastFMService::UpdateSubscriberStatus() {
QMap<QString, QString> params; QMap<QString, QString> params;
params["method"] = "user.getInfo"; params["method"] = "user.getInfo";
@ -261,16 +282,16 @@ bool LastFMService::InitScrobbler() {
if (!IsAuthenticated() || !IsScrobblingEnabled()) return false; if (!IsAuthenticated() || !IsScrobblingEnabled()) return false;
if (!scrobbler_) if (!scrobbler_)
scrobbler_ = new lastfm::Audioscrobbler(kAudioscrobblerClientId); scrobbler_.reset(new lastfm::Audioscrobbler(kAudioscrobblerClientId));
// reemit the signal since the sender is private // reemit the signal since the sender is private
#ifdef HAVE_LIBLASTFM1 #ifdef HAVE_LIBLASTFM1
connect(scrobbler_, SIGNAL(scrobblesSubmitted(QList<lastfm::Track>)), connect(scrobbler_.get(), SIGNAL(scrobblesSubmitted(QList<lastfm::Track>)),
SIGNAL(ScrobbleSubmitted())); SIGNAL(ScrobbleSubmitted()));
connect(scrobbler_, SIGNAL(nowPlayingError(int, QString)), connect(scrobbler_.get(), SIGNAL(nowPlayingError(int, QString)),
SIGNAL(ScrobbleError(int))); SIGNAL(ScrobbleError(int)));
#else #else
connect(scrobbler_, SIGNAL(status(int)), SLOT(ScrobblerStatus(int))); connect(scrobbler_.get(), SIGNAL(status(int)), SLOT(ScrobblerStatus(int)));
#endif #endif
return true; return true;
} }

View File

@ -54,6 +54,7 @@ class LastFMService : public Scrobbler {
static const char* kAudioscrobblerClientId; static const char* kAudioscrobblerClientId;
static const char* kApiKey; static const char* kApiKey;
static const char* kSecret; static const char* kSecret;
static const char* kAuthLoginUrl;
void ReloadSettings(); void ReloadSettings();
@ -68,7 +69,8 @@ class LastFMService : public Scrobbler {
bool PreferAlbumArtist() const { return prefer_albumartist_; } bool PreferAlbumArtist() const { return prefer_albumartist_; }
bool HasConnectionProblems() const { return connection_problems_; } bool HasConnectionProblems() const { return connection_problems_; }
void Authenticate(const QString& username, const QString& password); void GetToken();
void Authenticate(const QString& token);
void SignOut(); void SignOut();
void UpdateSubscriberStatus(); void UpdateSubscriberStatus();
@ -80,7 +82,8 @@ class LastFMService : public Scrobbler {
void ShowConfig(); void ShowConfig();
void ToggleScrobbling(); void ToggleScrobbling();
signals: signals:
void TokenReceived(bool success, const QString& token);
void AuthenticationComplete(bool success, const QString& error_message); void AuthenticationComplete(bool success, const QString& error_message);
void ScrobblingEnabledChanged(bool value); void ScrobblingEnabledChanged(bool value);
void ButtonVisibilityChanged(bool value); void ButtonVisibilityChanged(bool value);
@ -94,6 +97,7 @@ class LastFMService : public Scrobbler {
void SavedItemsChanged(); void SavedItemsChanged();
private slots: private slots:
void GetTokenReplyFinished(QNetworkReply* reply);
void AuthenticateReplyFinished(QNetworkReply* reply); void AuthenticateReplyFinished(QNetworkReply* reply);
void UpdateSubscriberStatusFinished(QNetworkReply* reply); void UpdateSubscriberStatusFinished(QNetworkReply* reply);
@ -107,7 +111,7 @@ class LastFMService : public Scrobbler {
static QUrl FixupUrl(const QUrl& url); static QUrl FixupUrl(const QUrl& url);
private: private:
lastfm::Audioscrobbler* scrobbler_; std::unique_ptr<lastfm::Audioscrobbler> scrobbler_;
lastfm::Track last_track_; lastfm::Track last_track_;
lastfm::Track next_metadata_; lastfm::Track next_metadata_;
bool already_scrobbled_; bool already_scrobbled_;

View File

@ -24,6 +24,7 @@
#include <lastfm5/ws.h> #include <lastfm5/ws.h>
#include <QDesktopServices>
#include <QMessageBox> #include <QMessageBox>
#include <QSettings> #include <QSettings>
@ -42,17 +43,16 @@ LastFMSettingsPage::LastFMSettingsPage(SettingsDialog* dialog)
// Icons // Icons
setWindowIcon(IconLoader::Load("lastfm", IconLoader::Provider)); setWindowIcon(IconLoader::Load("lastfm", IconLoader::Provider));
connect(service_, SIGNAL(TokenReceived(bool,QString)),
SLOT(TokenReceived(bool,QString)));
connect(service_, SIGNAL(AuthenticationComplete(bool, QString)), connect(service_, SIGNAL(AuthenticationComplete(bool, QString)),
SLOT(AuthenticationComplete(bool, QString))); SLOT(AuthenticationComplete(bool, QString)));
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout())); connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout()));
connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(Login())); connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(Login()));
connect(ui_->login, SIGNAL(clicked()), SLOT(Login())); connect(ui_->login, SIGNAL(clicked()), SLOT(Login()));
ui_->login_state->AddCredentialField(ui_->username); ui_->login_state->AddCredentialGroup(ui_->login_container);
ui_->login_state->AddCredentialField(ui_->password);
ui_->login_state->AddCredentialGroup(ui_->groupBox);
ui_->username->setMinimumWidth(QFontMetrics(QFont()).width("WWWWWWWWWWWW"));
resize(sizeHint()); resize(sizeHint());
} }
@ -62,7 +62,22 @@ void LastFMSettingsPage::Login() {
waiting_for_auth_ = true; waiting_for_auth_ = true;
ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress); 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, void LastFMSettingsPage::AuthenticationComplete(bool success,
@ -72,8 +87,6 @@ void LastFMSettingsPage::AuthenticationComplete(bool success,
waiting_for_auth_ = false; waiting_for_auth_ = false;
if (success) { if (success) {
// Clear password just to be sure
ui_->password->clear();
// Save settings // Save settings
Save(); Save();
} else { } else {
@ -109,8 +122,6 @@ void LastFMSettingsPage::Save() {
} }
void LastFMSettingsPage::Logout() { void LastFMSettingsPage::Logout() {
ui_->username->clear();
ui_->password->clear();
RefreshControls(false); RefreshControls(false);
service_->SignOut(); service_->SignOut();

View File

@ -38,6 +38,7 @@ class LastFMSettingsPage : public SettingsPage {
private slots: private slots:
void Login(); void Login();
void TokenReceived(bool success, const QString& token);
void AuthenticationComplete(bool success, const QString& error_message); void AuthenticationComplete(bool success, const QString& error_message);
void Logout(); void Logout();

View File

@ -18,26 +18,19 @@
<widget class="LoginStateWidget" name="login_state" native="true"/> <widget class="LoginStateWidget" name="login_state" native="true"/>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="groupBox"> <widget class="QWidget" name="login_container" native="true">
<property name="title"> <layout class="QVBoxLayout" name="verticalLayout_3">
<string>Account details</string> <property name="leftMargin">
</property> <number>28</number>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property> </property>
<item row="1" column="0"> <property name="topMargin">
<widget class="QLabel" name="label_2"> <number>0</number>
<property name="text"> </property>
<string>Last.fm username</string> <property name="bottomMargin">
</property> <number>0</number>
</widget> </property>
</item> <item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="username"/>
</item>
<item> <item>
<widget class="QPushButton" name="login"> <widget class="QPushButton" name="login">
<property name="text"> <property name="text">
@ -45,19 +38,28 @@
</property> </property>
</widget> </widget>
</item> </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> </layout>
</item> </item>
<item row="2" column="0"> <item>
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_2">
<property name="text"> <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> </property>
</widget> <property name="wordWrap">
</item> <bool>true</bool>
<item row="2" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property> </property>
</widget> </widget>
</item> </item>
@ -112,6 +114,9 @@
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
</property> </property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>20</width>
@ -131,9 +136,6 @@
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>username</tabstop>
<tabstop>password</tabstop>
<tabstop>login</tabstop>
<tabstop>scrobble</tabstop> <tabstop>scrobble</tabstop>
<tabstop>love_ban_</tabstop> <tabstop>love_ban_</tabstop>
<tabstop>scrobble_button</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 // Parse the feed and add it to this container
Podcast podcast; Podcast podcast;
podcast.set_description(attributes.value("description").toString()); 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( podcast.set_image_url_large(QUrl::fromEncoded(
attributes.value("imageHref").toString().toLatin1())); attributes.value("imageHref").toString().toLatin1()));
podcast.set_url(QUrl::fromEncoded( podcast.set_url(QUrl::fromEncoded(

View File

@ -354,6 +354,10 @@ void SoundCloudService::EnsureMenuCreated() {
context_menu_->addAction(IconLoader::Load("download", IconLoader::Base), context_menu_->addAction(IconLoader::Load("download", IconLoader::Base),
tr("Open %1 in browser").arg("soundcloud.com"), tr("Open %1 in browser").arg("soundcloud.com"),
this, SLOT(Homepage())); 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 = const QStringList filenames =
QStringList() << "blob" QStringList() << "blob"
<< "blob" + QString(kSignatureSuffix) << "blob" + QString(kSignatureSuffix)
<< "libspotify.so.12.1.45" << "libspotify.so.12.1.51"
<< "libspotify.so.12.1.45" + QString(kSignatureSuffix); << "libspotify.so.12.1.51" + QString(kSignatureSuffix);
for (const QString& filename : filenames) { for (const QString& filename : filenames) {
const QUrl url(SpotifyService::kBlobDownloadUrl + version_ + "/" + const QUrl url(SpotifyService::kBlobDownloadUrl + version_ + "/" +

View File

@ -63,7 +63,7 @@ Q_DECLARE_METATYPE(QStandardItem*);
const char* SpotifyService::kServiceName = "Spotify"; const char* SpotifyService::kServiceName = "Spotify";
const char* SpotifyService::kSettingsGroup = "Spotify"; const char* SpotifyService::kSettingsGroup = "Spotify";
const char* SpotifyService::kBlobDownloadUrl = const char* SpotifyService::kBlobDownloadUrl =
"http://spotify.clementine-player.org/"; "https://spotify.clementine-player.org/";
const int SpotifyService::kSearchDelayMsec = 400; const int SpotifyService::kSearchDelayMsec = 400;
SpotifyService::SpotifyService(Application* app, InternetModel* parent) SpotifyService::SpotifyService(Application* app, InternetModel* parent)
@ -413,7 +413,8 @@ void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) {
search_->setData(InternetModel::PlayBehaviour_MultipleItems, search_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour); 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(Type_StarredPlaylist, InternetModel::Role_Type);
starred_->setData(true, InternetModel::Role_CanLazyLoad); starred_->setData(true, InternetModel::Role_CanLazyLoad);
starred_->setData(InternetModel::PlayBehaviour_MultipleItems, starred_->setData(InternetModel::PlayBehaviour_MultipleItems,
@ -610,7 +611,8 @@ QList<QAction*> SpotifyService::playlistitem_actions(const Song& song) {
} }
QAction* add_to_starred = 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()), connect(add_to_starred, SIGNAL(triggered()),
SLOT(AddCurrentSongToStarredPlaylist())); SLOT(AddCurrentSongToStarredPlaylist()));
playlistitem_actions_.append(add_to_starred); playlistitem_actions_.append(add_to_starred);

View File

@ -579,19 +579,21 @@ void VkService::ChangeConnectionState(Vreen::Client::State state) {
switch (state) { switch (state) {
case Vreen::Client::StateOnline: case Vreen::Client::StateOnline:
emit LoginSuccess(true); emit LoginSuccess(true);
UpdateRoot();
break; break;
case Vreen::Client::StateInvalid: case Vreen::Client::StateInvalid:
case Vreen::Client::StateOffline: case Vreen::Client::StateOffline:
emit LoginSuccess(false); emit LoginSuccess(false);
UpdateRoot();
break; break;
case Vreen::Client::StateConnecting: case Vreen::Client::StateConnecting:
break; return;
default: default:
qLog(Error) << "Wrong connection state " << state; 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 "ui/settingsdialog.h"
#include <QActionGroup> #include <QActionGroup>
#include <QInputDialog>
#include <QKeyEvent> #include <QKeyEvent>
#include <QMenu> #include <QMenu>
#include <QRegExp> #include <QRegExp>
@ -100,11 +101,17 @@ LibraryFilterWidget::LibraryFilterWidget(QWidget* parent)
connect(group_by_group_, SIGNAL(triggered(QAction*)), connect(group_by_group_, SIGNAL(triggered(QAction*)),
SLOT(GroupByClicked(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 config menu
library_menu_ = new QMenu(tr("Display options"), this); library_menu_ = new QMenu(tr("Display options"), this);
library_menu_->setIcon(ui_->options->icon()); library_menu_->setIcon(ui_->options->icon());
library_menu_->addMenu(filter_age_menu_); library_menu_->addMenu(filter_age_menu_);
library_menu_->addMenu(group_by_menu_); library_menu_->addMenu(group_by_menu_);
library_menu_->addAction(ui_->save_grouping);
library_menu_->addAction(ui_->manage_groupings);
library_menu_->addSeparator(); library_menu_->addSeparator();
ui_->options->setMenu(library_menu_); ui_->options->setMenu(library_menu_);
@ -114,6 +121,22 @@ LibraryFilterWidget::LibraryFilterWidget(QWidget* parent)
LibraryFilterWidget::~LibraryFilterWidget() { delete ui_; } 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* LibraryFilterWidget::CreateGroupByActions(QObject* parent) {
QActionGroup* ret = new QActionGroup(parent); QActionGroup* ret = new QActionGroup(parent);
ret->addAction(CreateGroupByAction( ret->addAction(CreateGroupByAction(
@ -139,6 +162,27 @@ QActionGroup* LibraryFilterWidget::CreateGroupByActions(QObject* parent) {
LibraryModel::Grouping(LibraryModel::GroupBy_Genre, LibraryModel::Grouping(LibraryModel::GroupBy_Genre,
LibraryModel::GroupBy_Artist, LibraryModel::GroupBy_Artist,
LibraryModel::GroupBy_Album))); 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, ret->addAction(CreateGroupByAction(tr("Advanced grouping..."), parent,
LibraryModel::Grouping())); LibraryModel::Grouping()));
@ -158,6 +202,24 @@ QAction* LibraryFilterWidget::CreateGroupByAction(
return ret; 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) { void LibraryFilterWidget::FocusOnFilter(QKeyEvent* event) {
ui_->filter->setFocus(); ui_->filter->setFocus();
QApplication::sendEvent(ui_->filter, event); QApplication::sendEvent(ui_->filter, event);
@ -220,6 +282,11 @@ void LibraryFilterWidget::GroupingChanged(const LibraryModel::Grouping& g) {
} }
// Now make sure the correct action is checked // Now make sure the correct action is checked
CheckCurrentGrouping(g);
}
void LibraryFilterWidget::CheckCurrentGrouping(
const LibraryModel::Grouping& g) {
for (QAction* action : group_by_group_->actions()) { for (QAction* action : group_by_group_->actions()) {
if (action->property("group_by").isNull()) continue; if (action->property("group_by").isNull()) continue;

View File

@ -23,6 +23,7 @@
#include <QWidget> #include <QWidget>
#include "librarymodel.h" #include "librarymodel.h"
#include "savedgroupingmanager.h"
class GroupByDialog; class GroupByDialog;
class SettingsDialog; class SettingsDialog;
@ -51,6 +52,7 @@ class LibraryFilterWidget : public QWidget {
static QActionGroup* CreateGroupByActions(QObject* parent); static QActionGroup* CreateGroupByActions(QObject* parent);
void UpdateGroupByActions();
void SetFilterHint(const QString& hint); void SetFilterHint(const QString& hint);
void SetApplyFilterToLibrary(bool filter_applies_to_model) { void SetApplyFilterToLibrary(bool filter_applies_to_model) {
filter_applies_to_model_ = filter_applies_to_model; filter_applies_to_model_ = filter_applies_to_model;
@ -84,6 +86,8 @@ signals:
private slots: private slots:
void GroupingChanged(const LibraryModel::Grouping& g); void GroupingChanged(const LibraryModel::Grouping& g);
void GroupByClicked(QAction* action); void GroupByClicked(QAction* action);
void SaveGroupBy();
void ShowGroupingManager();
void FilterTextChanged(const QString& text); void FilterTextChanged(const QString& text);
void FilterDelayTimeout(); void FilterDelayTimeout();
@ -91,12 +95,14 @@ signals:
private: private:
static QAction* CreateGroupByAction(const QString& text, QObject* parent, static QAction* CreateGroupByAction(const QString& text, QObject* parent,
const LibraryModel::Grouping& grouping); const LibraryModel::Grouping& grouping);
void CheckCurrentGrouping(const LibraryModel::Grouping& g);
private: private:
Ui_LibraryFilterWidget* ui_; Ui_LibraryFilterWidget* ui_;
LibraryModel* model_; LibraryModel* model_;
std::unique_ptr<GroupByDialog> group_by_dialog_; std::unique_ptr<GroupByDialog> group_by_dialog_;
std::unique_ptr<SavedGroupingManager> groupings_manager_;
SettingsDialog* settings_dialog_; SettingsDialog* settings_dialog_;
QMenu* filter_age_menu_; QMenu* filter_age_menu_;

View File

@ -98,6 +98,16 @@
<string>Added this month</string> <string>Added this month</string>
</property> </property>
</action> </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> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

View File

@ -59,6 +59,7 @@ const char* LibraryModel::kSmartPlaylistsMimeType =
"application/x-clementine-smart-playlist-generator"; "application/x-clementine-smart-playlist-generator";
const char* LibraryModel::kSmartPlaylistsSettingsGroup = const char* LibraryModel::kSmartPlaylistsSettingsGroup =
"SerialisedSmartPlaylists"; "SerialisedSmartPlaylists";
const char* LibraryModel::kSavedGroupingsSettingsGroup = "SavedGroupings";
const int LibraryModel::kSmartPlaylistsVersion = 4; const int LibraryModel::kSmartPlaylistsVersion = 4;
const int LibraryModel::kPrettyCoverSize = 32; const int LibraryModel::kPrettyCoverSize = 32;
const qint64 LibraryModel::kIconCacheSize = 100000000; //~100MB const qint64 LibraryModel::kIconCacheSize = 100000000; //~100MB
@ -106,9 +107,11 @@ LibraryModel::LibraryModel(LibraryBackend* backend, Application* app,
Utilities::GetConfigPath(Utilities::Path_CacheRoot) + "/pixmapcache"); Utilities::GetConfigPath(Utilities::Path_CacheRoot) + "/pixmapcache");
icon_cache_->setMaximumCacheSize(LibraryModel::kIconCacheSize); icon_cache_->setMaximumCacheSize(LibraryModel::kIconCacheSize);
no_cover_icon_ = QPixmap(":nocover.png") QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
.scaled(kPrettyCoverSize, kPrettyCoverSize, no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(
Qt::KeepAspectRatio, Qt::SmoothTransformation); kPrettyCoverSize, kPrettyCoverSize,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
connect(backend_, SIGNAL(SongsDiscovered(SongList)), connect(backend_, SIGNAL(SongsDiscovered(SongList)),
SLOT(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) { void LibraryModel::Init(bool async) {
if (async) { if (async) {
// Show a loading indicator in the model. // Show a loading indicator in the model.
@ -1491,3 +1506,19 @@ void LibraryModel::TotalSongCountUpdatedSlot(int count) {
total_song_count_ = count; total_song_count_ = count;
emit TotalSongCountUpdated(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* kSmartPlaylistsMimeType;
static const char* kSmartPlaylistsSettingsGroup; static const char* kSmartPlaylistsSettingsGroup;
static const char* kSmartPlaylistsArray; static const char* kSmartPlaylistsArray;
static const char* kSavedGroupingsSettingsGroup;
static const int kSmartPlaylistsVersion; static const int kSmartPlaylistsVersion;
static const int kPrettyCoverSize; static const int kPrettyCoverSize;
static const qint64 kIconCacheSize; static const qint64 kIconCacheSize;
@ -161,6 +162,9 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
// Whether or not to show letters heading in the library view // Whether or not to show letters heading in the library view
void set_show_dividers(bool show_dividers); void set_show_dividers(bool show_dividers);
// Save the current grouping
void SaveGrouping(QString name);
// Utility functions for manipulating text // Utility functions for manipulating text
static QString TextOrUnknown(const QString& text); static QString TextOrUnknown(const QString& text);
static QString PrettyYearAlbum(int year, const QString& album); static QString PrettyYearAlbum(int year, const QString& album);
@ -179,6 +183,7 @@ signals:
void SetFilterQueryMode(QueryOptions::QueryMode query_mode); void SetFilterQueryMode(QueryOptions::QueryMode query_mode);
void SetGroupBy(const LibraryModel::Grouping& g); void SetGroupBy(const LibraryModel::Grouping& g);
const LibraryModel::Grouping& GetGroupBy() const { return group_by_; }
void Init(bool async = true); void Init(bool async = true);
void Reset(); void Reset();
void ResetAsync(); void ResetAsync();
@ -300,4 +305,7 @@ signals:
Q_DECLARE_METATYPE(LibraryModel::Grouping); Q_DECLARE_METATYPE(LibraryModel::Grouping);
QDataStream& operator<<(QDataStream& s, const LibraryModel::Grouping& g);
QDataStream& operator>>(QDataStream& s, LibraryModel::Grouping& g);
#endif // LIBRARYMODEL_H #endif // LIBRARYMODEL_H

View File

@ -173,9 +173,10 @@ LibraryView::LibraryView(QWidget* parent)
app_(nullptr), app_(nullptr),
filter_(nullptr), filter_(nullptr),
total_song_count_(-1), total_song_count_(-1),
nomusic_(":nomusic.png"),
context_menu_(nullptr), context_menu_(nullptr),
is_in_keyboard_search_(false) { is_in_keyboard_search_(false) {
QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
nomusic_ = nocover.pixmap(nocover.availableSizes().last());
setItemDelegate(new LibraryItemDelegate(this)); setItemDelegate(new LibraryItemDelegate(this));
setAttribute(Qt::WA_MacShowFocusRect, false); setAttribute(Qt::WA_MacShowFocusRect, false);
setHeaderHidden(true); setHeaderHidden(true);
@ -185,7 +186,6 @@ LibraryView::LibraryView(QWidget* parent)
setSelectionMode(QAbstractItemView::ExtendedSelection); setSelectionMode(QAbstractItemView::ExtendedSelection);
setStyleSheet("QTreeView::item{padding-top:1px;}"); setStyleSheet("QTreeView::item{padding-top:1px;}");
setAnimated(true);
} }
LibraryView::~LibraryView() {} 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 // Ignore FILEs pointing to other media files. Also, watch out for incorrect
// media files. Playlist parser for CUEs considers every entry in sheet // media files. Playlist parser for CUEs considers every entry in sheet
// valid and we don't want invalid media getting into library! // 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)) { 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)) { if (TagReaderClient::Instance()->IsMediaFileBlocking(file)) {
song_list << cue_song; 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/song.h"
#include "core/ubuntuunityhack.h" #include "core/ubuntuunityhack.h"
#include "core/utilities.h" #include "core/utilities.h"
#include "covers/amazoncoverprovider.h"
#include "covers/coverproviders.h"
#include "covers/musicbrainzcoverprovider.h"
#include "engines/enginebase.h" #include "engines/enginebase.h"
#include "smartplaylists/generator.h" #include "smartplaylists/generator.h"
#include "ui/iconloader.h" #include "ui/iconloader.h"
@ -435,11 +432,6 @@ int main(int argc, char* argv[]) {
QNetworkProxyFactory::setApplicationProxyFactory( QNetworkProxyFactory::setApplicationProxyFactory(
NetworkProxyFactory::Instance()); 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 #ifdef Q_OS_LINUX
// In 11.04 Ubuntu decided that the system tray should be reserved for certain // In 11.04 Ubuntu decided that the system tray should be reserved for certain
// whitelisted applications. Clementine will override this setting and insert // 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, QRect MoodbarProxyStyle::subControlRect(ComplexControl cc,
const QStyleOptionComplex* opt, const QStyleOptionComplex* opt,
SubControl sc, SubControl sc,
@ -270,29 +263,23 @@ QRect MoodbarProxyStyle::subControlRect(ComplexControl cc,
case MoodbarOn: case MoodbarOn:
case FadingToOn: case FadingToOn:
switch (sc) { switch (sc) {
case SC_SliderGroove: { case SC_SliderGroove:
int margin_leftright = GetExtraSpace(opt) / 2; return opt->rect.adjusted(kMarginSize, kMarginSize, -kMarginSize,
return opt->rect.adjusted(margin_leftright, kMarginSize, -kMarginSize);
-margin_leftright, -kMarginSize);
}
case SC_SliderHandle: { case SC_SliderHandle: {
const QStyleOptionSlider* slider_opt = const QStyleOptionSlider* slider_opt =
qstyleoption_cast<const QStyleOptionSlider*>(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; 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) * x = (slider_opt->sliderValue - slider_opt->minimum) *
(space_available - kArrowWidth) / (opt->rect.width() - kArrowWidth) /
(slider_opt->maximum - slider_opt->minimum); (slider_opt->maximum - slider_opt->minimum);
} }
x += margin;
return QRect(QPoint(opt->rect.left() + x, opt->rect.top()), return QRect(QPoint(opt->rect.left() + x, opt->rect.top()),
QSize(kArrowWidth, kArrowHeight)); QSize(kArrowWidth, kArrowHeight));
} }
@ -330,14 +317,9 @@ QPixmap MoodbarProxyStyle::MoodbarPixmap(const ColorVector& colors,
const QSize& size, const QSize& size,
const QPalette& palette, const QPalette& palette,
const QStyleOptionSlider* opt) { const QStyleOptionSlider* opt) {
int margin_leftright = GetExtraSpace(opt); QRect rect(QPoint(0, 0), size);
const QRect rect(QPoint(0, 0), size);
QRect border_rect(rect); QRect border_rect(rect);
// I would expect we need to adjust by margin_lr/2, so the extra space is border_rect.adjust(kMarginSize, kMarginSize, -kMarginSize, -kMarginSize);
// 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);
QRect inner_rect(border_rect); QRect inner_rect(border_rect);
inner_rect.adjust(kBorderSize, kBorderSize, -kBorderSize, -kBorderSize); inner_rect.adjust(kBorderSize, kBorderSize, -kBorderSize, -kBorderSize);
@ -356,14 +338,8 @@ QPixmap MoodbarProxyStyle::MoodbarPixmap(const ColorVector& colors,
// Draw the outer bit // Draw the outer bit
p.setPen(QPen(palette.brush(QPalette::Active, QPalette::Background), p.setPen(QPen(palette.brush(QPalette::Active, QPalette::Background),
kMarginSize, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin)); kMarginSize, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
// First: a rectangle around the slider
p.drawRect(rect.adjusted(1, 1, -2, -2)); 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(); p.end();

View File

@ -79,10 +79,6 @@ class MoodbarProxyStyle : public QProxyStyle {
void ChangeStyle(QAction* action); void ChangeStyle(QAction* action);
private: 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_; Application* app_;
QSlider* slider_; QSlider* slider_;

View File

@ -17,15 +17,18 @@
#include "networkremote.h" #include "networkremote.h"
#include "core/logging.h"
#include "covers/currentartloader.h"
#include "networkremote/zeroconf.h"
#include "playlist/playlistmanager.h"
#include <QDataStream> #include <QDataStream>
#include <QSettings> #include <QSettings>
#include <QHostInfo> #include <QHostInfo>
#include <QNetworkProxy> #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 char* NetworkRemote::kSettingsGroup = "NetworkRemote";
const quint16 NetworkRemote::kDefaultServerPort = 5500; const quint16 NetworkRemote::kDefaultServerPort = 5500;

View File

@ -3,14 +3,17 @@
#include <memory> #include <memory>
#include <QTcpServer> #include <QList>
#include <QTcpSocket> #include <QObject>
#include "core/player.h" class Application;
#include "core/application.h" class IncomingDataParser;
#include "incomingdataparser.h" class OutgoingDataCreator;
#include "outgoingdatacreator.h" class QHostAddress;
#include "remoteclient.h" class QImage;
class QTcpServer;
class QTcpSocket;
class RemoteClient;
class NetworkRemote : public QObject { class NetworkRemote : public QObject {
Q_OBJECT Q_OBJECT

View File

@ -15,10 +15,12 @@
along with Clementine. If not, see <http://www.gnu.org/licenses/>. along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "core/logging.h" #include "networkremote/networkremotehelper.h"
#include "networkremote.h" #include "core/application.h"
#include "networkremotehelper.h" #include "core/logging.h"
#include "networkremote/networkremote.h"
#include "playlist/playlistmanager.h"
NetworkRemoteHelper* NetworkRemoteHelper::sInstance = nullptr; NetworkRemoteHelper* NetworkRemoteHelper::sInstance = nullptr;

View File

@ -17,14 +17,15 @@
#include "songsender.h" #include "songsender.h"
#include "networkremote.h"
#include <QFileInfo> #include <QFileInfo>
#include "core/application.h" #include "core/application.h"
#include "core/logging.h" #include "core/logging.h"
#include "core/utilities.h" #include "core/utilities.h"
#include "library/librarybackend.h" #include "library/librarybackend.h"
#include "networkremote/networkremote.h"
#include "networkremote/outgoingdatacreator.h"
#include "networkremote/remoteclient.h"
#include "playlist/playlistitem.h" #include "playlist/playlistitem.h"
const quint32 SongSender::kFileChunkSize = 100000; // in Bytes const quint32 SongSender::kFileChunkSize = 100000; // in Bytes
@ -32,16 +33,18 @@ const quint32 SongSender::kFileChunkSize = 100000; // in Bytes
SongSender::SongSender(Application* app, RemoteClient* client) SongSender::SongSender(Application* app, RemoteClient* client)
: app_(app), : app_(app),
client_(client), client_(client),
transcoder_(new Transcoder(this)) { transcoder_(
new Transcoder(this, NetworkRemote::kTranscoderSettingPostfix)) {
QSettings s; QSettings s;
s.beginGroup(NetworkRemote::kSettingsGroup); s.beginGroup(NetworkRemote::kSettingsGroup);
transcode_lossless_files_ = s.value("convert_lossless", false).toBool(); transcode_lossless_files_ = s.value("convert_lossless", false).toBool();
// Load preset // 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(); 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_) { if (last_output_format == presets.at(i).codec_mimetype_) {
transcoder_preset_ = presets.at(i); transcoder_preset_ = presets.at(i);
break; break;
@ -58,8 +61,9 @@ SongSender::SongSender(Application* app, RemoteClient* client)
SongSender::~SongSender() { SongSender::~SongSender() {
disconnect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)), this, disconnect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)), this,
SLOT(TranscodeJobComplete(QString, QString, bool))); SLOT(TranscodeJobComplete(QString, QString, bool)));
disconnect(transcoder_, SIGNAL(AllJobsComplete()), this, SLOT(StartTransfer())); disconnect(transcoder_, SIGNAL(AllJobsComplete()), this,
SLOT(StartTransfer()));
transcoder_->Cancel(); transcoder_->Cancel();
} }
@ -102,8 +106,7 @@ void SongSender::SendSongs(const pb::remote::RequestDownloadSongs& request) {
void SongSender::TranscodeLosslessFiles() { void SongSender::TranscodeLosslessFiles() {
for (DownloadItem item : download_queue_) { for (DownloadItem item : download_queue_) {
// Check only lossless files // Check only lossless files
if (!item.song_.IsFileLossless()) if (!item.song_.IsFileLossless()) continue;
continue;
// Add the file to the transcoder // Add the file to the transcoder
QString local_file = item.song_.url().toLocalFile(); 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; qLog(Debug) << input << "transcoded to" << output << success;
// If it wasn't successful send original file // If it wasn't successful send original file
@ -204,7 +208,8 @@ void SongSender::OfferNextSong() {
chunk->set_file_number(item.song_no_); chunk->set_file_number(item.song_no_);
chunk->set_size(file.size()); 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); client_->SendData(&msg);
@ -215,8 +220,7 @@ void SongSender::ResponseSongOffer(bool accepted) {
// Get the item and send the single song // Get the item and send the single song
DownloadItem item = download_queue_.dequeue(); DownloadItem item = download_queue_.dequeue();
if (accepted) if (accepted) SendSingleSong(item);
SendSingleSong(item);
// And offer the next song // And offer the next song
OfferNextSong(); OfferNextSong();
@ -273,7 +277,8 @@ void SongSender::SendSingleSong(DownloadItem download_item) {
int i = app_->playlist_manager()->active()->current_row(); int i = app_->playlist_manager()->active()->current_row();
pb::remote::SongMetadata* song_metadata = pb::remote::SongMetadata* song_metadata =
msg.mutable_response_song_file_chunk()->mutable_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 the file was transcoded, we have to change the filename and filesize
if (is_transcoded) { 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; SongList song_list;
// First gather all valid songs // 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, void Playlist::SongSaveComplete(TagReaderReply* reply,
const QPersistentModelIndex& index) { const QPersistentModelIndex& index) {
if (reply->is_successful() && index.isValid()) { if (reply->is_successful() && index.isValid()) {
QFuture<void> future = item_at(index.row())->BackgroundReload(); if (reply->message().save_file_response().success()) {
NewClosure(future, this, SLOT(ItemReloadComplete(QPersistentModelIndex)), QFuture<void> future = item_at(index.row())->BackgroundReload();
index); 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(); reply->deleteLater();
} }
@ -691,7 +697,7 @@ void Playlist::set_current_row(int i, bool is_stopping) {
void Playlist::InsertDynamicItems(int count) { void Playlist::InsertDynamicItems(int count) {
GeneratorInserter* inserter = GeneratorInserter* inserter =
new GeneratorInserter(task_manager_, library_, this); 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)), connect(inserter, SIGNAL(PlayRequested(QModelIndex)),
SIGNAL(PlayRequested(QModelIndex))); SIGNAL(PlayRequested(QModelIndex)));
@ -819,7 +825,7 @@ bool Playlist::dropMimeData(const QMimeData* data, Qt::DropAction action,
} else if (data->hasFormat(kCddaMimeType)) { } else if (data->hasFormat(kCddaMimeType)) {
SongLoaderInserter* inserter = new SongLoaderInserter( SongLoaderInserter* inserter = new SongLoaderInserter(
task_manager_, library_, backend_->app()->player()); 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); inserter->LoadAudioCD(this, row, play_now, enqueue_now);
} else if (data->hasUrls()) { } else if (data->hasUrls()) {
// URL list dragged from the file list or some other app // 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) { bool enqueue) {
SongLoaderInserter* inserter = new SongLoaderInserter( SongLoaderInserter* inserter = new SongLoaderInserter(
task_manager_, library_, backend_->app()->player()); 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); inserter->Load(this, pos, play_now, enqueue, urls);
} }
@ -847,7 +853,7 @@ void Playlist::InsertSmartPlaylist(GeneratorPtr generator, int pos,
GeneratorInserter* inserter = GeneratorInserter* inserter =
new GeneratorInserter(task_manager_, library_, this); 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); inserter->Load(this, pos, play_now, enqueue, generator);

View File

@ -348,7 +348,7 @@ signals:
void PlaylistChanged(); void PlaylistChanged();
void DynamicModeChanged(bool dynamic); 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 // Signals that the queue has changed, meaning that the remaining queued
// items should update their position. // 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_palette.setColor(QPalette::Inactive, QPalette::WindowText,
no_matches_color); no_matches_color);
no_matches_label_->setPalette(no_matches_palette); no_matches_label_->setPalette(no_matches_palette);
// Remove QFrame border
ui_->toolbar->setStyleSheet("QFrame { border: 0px; }");
// Make it bold // Make it bold
QFont no_matches_font = no_matches_label_->font(); QFont no_matches_font = no_matches_label_->font();
no_matches_font.setBold(true); no_matches_font.setBold(true);
@ -224,11 +227,11 @@ void PlaylistContainer::SetViewModel(Playlist* playlist) {
} }
void PlaylistContainer::ActivePlaying() { void PlaylistContainer::ActivePlaying() {
UpdateActiveIcon(QIcon(":tiny-start.png")); UpdateActiveIcon(IconLoader::Load("tiny-start", IconLoader::Other));
} }
void PlaylistContainer::ActivePaused() { void PlaylistContainer::ActivePaused() {
UpdateActiveIcon(QIcon(":tiny-pause.png")); UpdateActiveIcon(IconLoader::Load("tiny-pause", IconLoader::Other));
} }
void PlaylistContainer::ActiveStopped() { UpdateActiveIcon(QIcon()); } void PlaylistContainer::ActiveStopped() { UpdateActiveIcon(QIcon()); }

View File

@ -104,13 +104,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item> <item>
<widget class="QSearchField" name="filter" native="true"/> <widget class="QSearchField" name="filter" native="true"/>
</item> </item>

View File

@ -117,7 +117,7 @@ Playlist* PlaylistManager::AddPlaylist(int id, const QString& name,
connect(ret, SIGNAL(PlaylistChanged()), SLOT(UpdateSummaryText())); connect(ret, SIGNAL(PlaylistChanged()), SLOT(UpdateSummaryText()));
connect(ret, SIGNAL(EditingFinished(QModelIndex)), connect(ret, SIGNAL(EditingFinished(QModelIndex)),
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)), connect(ret, SIGNAL(PlayRequested(QModelIndex)),
SIGNAL(PlayRequested(QModelIndex))); SIGNAL(PlayRequested(QModelIndex)));
connect(playlist_container_->view(), connect(playlist_container_->view(),

View File

@ -26,6 +26,7 @@
#include "core/player.h" #include "core/player.h"
#include "covers/currentartloader.h" #include "covers/currentartloader.h"
#include "ui/qt_blurimage.h" #include "ui/qt_blurimage.h"
#include "ui/iconloader.h"
#include <QCommonStyle> #include <QCommonStyle>
#include <QClipboard> #include <QClipboard>
@ -128,8 +129,6 @@ PlaylistView::PlaylistView(QWidget* parent)
inhibit_autoscroll_(false), inhibit_autoscroll_(false),
currently_autoscrolling_(false), currently_autoscrolling_(false),
row_height_(-1), row_height_(-1),
currenttrack_play_(":currenttrack_play.png"),
currenttrack_pause_(":currenttrack_pause.png"),
cached_current_row_row_(-1), cached_current_row_row_(-1),
drop_indicator_row_(-1), drop_indicator_row_(-1),
drag_over_(false), drag_over_(false),
@ -139,6 +138,17 @@ PlaylistView::PlaylistView(QWidget* parent)
setStyle(style_); setStyle(style_);
setMouseTracking(true); 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(sectionResized(int, int, int)), SLOT(SaveGeometry()));
connect(header_, SIGNAL(sectionMoved(int, int, int)), SLOT(SaveGeometry())); connect(header_, SIGNAL(sectionMoved(int, int, int)), SLOT(SaveGeometry()));
connect(header_, SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), connect(header_, SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)),
@ -497,6 +507,9 @@ void PlaylistView::drawRow(QPainter* painter,
is_paused ? currenttrack_pause_ : currenttrack_play_); is_paused ? currenttrack_pause_ : currenttrack_play_);
// Set the font // 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( opt.palette.setColor(QPalette::Text, QApplication::palette().color(
QPalette::HighlightedText)); QPalette::HighlightedText));
opt.palette.setColor(QPalette::Highlight, Qt::transparent); opt.palette.setColor(QPalette::Highlight, Qt::transparent);

View File

@ -17,46 +17,132 @@
#include "echonestimages.h" #include "echonestimages.h"
#include <algorithm>
#include <memory> #include <memory>
#include <QJsonObject>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonValue>
#include <Artist.h> #include <Artist.h>
#include "core/closure.h"
#include "core/logging.h" #include "core/logging.h"
#include "core/network.h"
struct EchoNestImages::Request { namespace {
Request(int id) : id_(id), artist_(new Echonest::Artist) {} static const char* kSpotifyBucket = "spotify";
static const char* kSpotifyArtistUrl = "https://api.spotify.com/v1/artists/%1";
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;
} }
void EchoNestImages::RequestFinished() { EchoNestImages::EchoNestImages() : network_(new NetworkAccessManager) {}
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
if (!reply || !requests_.contains(reply)) return; 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(); reply->deleteLater();
RequestPtr request = requests_.take(reply);
try { try {
request->artist_->parseProfile(reply); artist.parseProfile(reply);
} } catch (Echonest::ParseError e) {
catch (Echonest::ParseError e) {
qLog(Warning) << "Error parsing echonest reply:" << e.errorType() qLog(Warning) << "Error parsing echonest reply:" << e.errorType()
<< e.what(); << e.what();
} }
for (const Echonest::ArtistImage& image : request->artist_->images()) { for (const Echonest::ArtistImage& image : artist.images()) {
emit ImageReady(request->id_, image.url()); // 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 <memory>
#include "songinfoprovider.h" #include <QMultiMap>
#include <echonest/Artist.h>
#include "songinfo/songinfoprovider.h"
class NetworkAccessManager;
class QNetworkReply; class QNetworkReply;
class EchoNestImages : public SongInfoProvider { class EchoNestImages : public SongInfoProvider {
Q_OBJECT Q_OBJECT
public: public:
EchoNestImages();
virtual ~EchoNestImages();
void FetchInfo(int id, const Song& metadata); void FetchInfo(int id, const Song& metadata);
private slots: private slots:
void RequestFinished(); void RequestFinished(QNetworkReply*, int id, Echonest::Artist artist);
void IdsFound(QNetworkReply* reply, int id);
private: private:
struct Request; void DoSpotifyImageRequest(const QString& id, int request_id);
typedef std::shared_ptr<Request> RequestPtr;
QMap<QNetworkReply*, RequestPtr> requests_; void RegisterReply(QNetworkReply* reply, int id);
QMultiMap<int, QNetworkReply*> replies_;
std::unique_ptr<NetworkAccessManager> network_;
}; };
#endif // ECHONESTIMAGES_H #endif // ECHONESTIMAGES_H

View File

@ -25,13 +25,14 @@
#include <QJsonArray> #include <QJsonArray>
#include <Artist.h> #include <Artist.h>
#include <TypeInformation.h>
#include "core/closure.h" #include "core/closure.h"
#include "core/logging.h" #include "core/logging.h"
#include "songkickconcertwidget.h" #include "songkickconcertwidget.h"
#include "ui/iconloader.h" #include "ui/iconloader.h"
const char* SongkickConcerts::kSongkickArtistBucket = "id:songkick"; const char* SongkickConcerts::kSongkickArtistBucket = "songkick";
const char* SongkickConcerts::kSongkickArtistCalendarUrl = const char* SongkickConcerts::kSongkickArtistCalendarUrl =
"https://api.songkick.com/api/3.0/artists/%1/calendar.json?" "https://api.songkick.com/api/3.0/artists/%1/calendar.json?"
"per_page=5&" "per_page=5&"
@ -50,10 +51,11 @@ void SongkickConcerts::FetchInfo(int id, const Song& metadata) {
Echonest::Artist::SearchParams params; Echonest::Artist::SearchParams params;
params.push_back( params.push_back(
qMakePair(Echonest::Artist::Name, QVariant(metadata.artist()))); qMakePair(Echonest::Artist::Name, QVariant(metadata.artist())));
params.push_back(
qMakePair(Echonest::Artist::IdSpace, QVariant(kSongkickArtistBucket)));
qLog(Debug) << "Params:" << params; 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(); qLog(Debug) << reply->request().url();
NewClosure(reply, SIGNAL(finished()), this, NewClosure(reply, SIGNAL(finished()), this,
SLOT(ArtistSearchFinished(QNetworkReply*, int)), reply, id); SLOT(ArtistSearchFinished(QNetworkReply*, int)), reply, id);
@ -93,8 +95,7 @@ void SongkickConcerts::ArtistSearchFinished(QNetworkReply* reply, int id) {
} }
FetchSongkickCalendar(split[2], id); FetchSongkickCalendar(split[2], id);
} } catch (Echonest::ParseError& e) {
catch (Echonest::ParseError& e) {
qLog(Error) << "Error parsing echonest reply:" << e.errorType() << e.what(); qLog(Error) << "Error parsing echonest reply:" << e.errorType() << e.what();
emit Finished(id); emit Finished(id);
} }

View File

@ -14,7 +14,7 @@
<string>Transcode Music</string> <string>Transcode Music</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset> <iconset resource="../../data/data.qrc">
<normaloff>:/icon.png</normaloff>:/icon.png</iconset> <normaloff>:/icon.png</normaloff>:/icon.png</iconset>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_3">
@ -64,7 +64,17 @@
<item> <item>
<widget class="QPushButton" name="add"> <widget class="QPushButton" name="add">
<property name="text"> <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> </property>
</widget> </widget>
</item> </item>
@ -88,16 +98,6 @@
</property> </property>
</spacer> </spacer>
</item> </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> </layout>
</item> </item>
</layout> </layout>
@ -216,10 +216,16 @@
<tabstops> <tabstops>
<tabstop>files</tabstop> <tabstop>files</tabstop>
<tabstop>add</tabstop> <tabstop>add</tabstop>
<tabstop>import</tabstop>
<tabstop>remove</tabstop> <tabstop>remove</tabstop>
<tabstop>format</tabstop> <tabstop>format</tabstop>
<tabstop>button_box</tabstop> <tabstop>options</tabstop>
<tabstop>destination</tabstop>
<tabstop>select</tabstop>
<tabstop>details</tabstop>
</tabstops> </tabstops>
<resources/> <resources>
<include location="../../data/data.qrc"/>
</resources>
<connections/> <connections/>
</ui> </ui>

View File

@ -82,10 +82,10 @@ GstElement* Transcoder::CreateElementForMimeType(const QString& element_type,
GstElement* bin) { GstElement* bin) {
if (mime_type.isEmpty()) return nullptr; 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") { if (mime_type == "audio/mp4") {
LogLine(QString("Using '%1' (rank %2)").arg("ffmux_mp4").arg(-1)); LogLine(QString("Using '%1' (rank %2)").arg("mp4mux").arg(-1));
return CreateElement("ffmux_mp4", bin); return CreateElement("mp4mux", bin);
} }
// Keep track of all the suitable elements we find and figure out which // 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