diff --git a/CMakeLists.txt b/CMakeLists.txt index c88cd39627..b692de17af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,260 +1,260 @@ cmake_minimum_required(VERSION 2.8.12) project(Amarok) # Remove all warnings, ease the porting to cmake 3.x if (POLICY CMP0028) cmake_policy(SET CMP0028 NEW) endif() ############### find_package(PkgConfig REQUIRED) find_package(ECM 1.7.0 REQUIRED CONFIG) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(FeatureSummary) include(ECMInstallIcons) include(ECMSetupVersion) include(ECMAddTests) include(ECMAddAppIcon) include(FindPkgConfig) -find_package( Qt5 REQUIRED COMPONENTS Core DBus Gui Quick QuickWidgets Qml Script ScriptTools Sql Svg Test Widgets Xml ) +find_package( Qt5 5.8.0 REQUIRED COMPONENTS Core DBus Gui Quick QuickWidgets Qml Script ScriptTools Sql Svg Test Widgets Xml ) find_package( KF5 REQUIRED COMPONENTS Archive Codecs CoreAddons DBusAddons Declarative DNSSD GlobalAccel GuiAddons I18n IconThemes KCMUtils KIO NewStuff Notifications NotifyConfig Package Solid TextEditor ThreadWeaver WindowSystem ) ############### option(WITH_UTILITIES "Enable building of utilities" ON) option(WITH_PLAYER "Enable building of main Amarok player" ON) option(WITH_MP3Tunes "Enable mp3tunes in the Amarok player, requires multiple extra dependencies" ON) option(WITH_IPOD "Enable iPod support in Amarok" ON) option(WITH_MYSQL_EMBEDDED "Build the embedded database library -- highly recommended" ON) option(WITH_PLAYGROUND "Enable building of playground scripts and applets (WARNING: some of them might have legal issues!)" OFF) ############### Taglib set(TAGLIB_MIN_VERSION "1.7") find_package(Taglib REQUIRED) set_package_properties( Taglib PROPERTIES DESCRIPTION "Support for Audio metadata." URL "http://developer.kde.org/~wheeler/taglib.html" TYPE REQUIRED PURPOSE "Required for tag reading" ) # Check if TagLib is built with ASF and MP4 support include(CheckCXXSourceCompiles) set(CMAKE_REQUIRED_INCLUDES "${TAGLIB_INCLUDES}") set(CMAKE_REQUIRED_LIBRARIES "${TAGLIB_LIBRARIES}") check_cxx_source_compiles("#include int main() { TagLib::ASF::Tag tag; return 0;}" TAGLIB_ASF_FOUND) if( NOT TAGLIB_ASF_FOUND ) message(FATAL_ERROR "TagLib does not have ASF support compiled in.") endif() check_cxx_source_compiles("#include int main() { TagLib::MP4::Tag tag(0, 0); return 0;}" TAGLIB_MP4_FOUND) if( NOT TAGLIB_MP4_FOUND ) message(FATAL_ERROR "TagLib does not have MP4 support compiled in.") endif() check_cxx_source_compiles("#include #include #include #include #include using namespace TagLib; int main() { char *s; Mod::Tag tag; Mod::File modfile(s); S3M::File s3mfile(s); IT::File itfile(s); XM::File xmfile(s); return 0; }" TAGLIB_MOD_FOUND) check_cxx_source_compiles("#include int main() { char *s; TagLib::Ogg::Opus::File opusfile(s); return 0;}" TAGLIB_OPUS_FOUND) set(CMAKE_REQUIRED_INCLUDES) set(CMAKE_REQUIRED_LIBRARIES) set(TAGLIB-EXTRAS_MIN_VERSION "1.0") find_package(Taglib-Extras) set(TAGLIB_EXTRAS_FOUND ${TAGLIB-EXTRAS_FOUND}) # we need a c-compatible name for the include file include(CheckTagLibFileName) check_taglib_filename(COMPLEX_TAGLIB_FILENAME) ############### #Needed to conditionally build tests and gui if(BUILD_TESTING) add_definitions(-DDEBUG) endif() if(WITH_DESKTOP_UI) add_definitions(-DDESKTOP_UI) endif() set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fmessage-length=0") if (CMAKE_COMPILER_IS_GNUCXX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fmessage-length=0") if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,--as-needed") endif() endif () include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/shared ${CMAKE_CURRENT_BINARY_DIR}/shared ) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") # Require C++11 # WORKAROUND for Clang bug: http://llvm.org/bugs/show_bug.cgi?id=15651 if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND WIN32) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-delayed-template-parsing") endif () add_definitions(-DQT_NO_URL_CAST_FROM_STRING) find_package(Phonon4Qt5 4.6.60 REQUIRED NO_MODULE) include_directories(BEFORE ${PHONON_INCLUDES}) find_package( LibLastFm ) set( LIBLASTFM_MIN_VERSION "1.0.0" ) if( LIBLASTFM_FOUND ) if ( ${LIBLASTFM_MIN_VERSION} VERSION_LESS ${LIBLASTFM_VERSION} ) set( LIBLASTFM_FOUND TRUE ) endif() endif() find_package( LibOFA ) if( LIBOFA_FOUND ) PKG_SEARCH_MODULE(AVCODEC libavcodec) PKG_SEARCH_MODULE(AVFORMAT libavformat) PKG_SEARCH_MODULE(AVUTIL libavutil) endif() string( TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_TOLOWER ) if( CMAKE_BUILD_TYPE_TOLOWER MATCHES debug ) set( DEBUG_BUILD_TYPE ON ) add_definitions(-Wall -Wextra) endif() # this needs to be here because also code in shared/ needs config.h. This is also the # reason why various checks are above why they belong under if( WITH_PLAYER ) configure_file( shared/config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/shared/config.h ) add_subdirectory( data ) add_subdirectory( images ) add_subdirectory( shared ) if( WITH_PLAYER ) find_package(X11) find_package(MySQLAmarok REQUIRED) if( WITH_MYSQL_EMBEDDED ) set( BUILD_MYSQLE_COLLECTION TRUE ) set_package_properties( MYSQLD PROPERTIES DESCRIPTION "Embedded MySQL Libraries" URL "http://www.mysql.com" TYPE REQUIRED ) else() add_definitions( "-DNO_MYSQL_EMBEDDED" ) endif() set_package_properties( MYSQL PROPERTIES DESCRIPTION "MySQL Server Libraries" URL "http://www.mysql.com" TYPE REQUIRED ) # zlib is required for mysql embedded find_package(ZLIB REQUIRED) set_package_properties( ZLIB PROPERTIES DESCRIPTION "zlib" TYPE REQUIRED ) # QJson is required for the PlaydarCollection find_package(QJSON) set_package_properties( QJSON PROPERTIES DESCRIPTION "Qt JSON Parser used for the Playdar Collection" URL "http://qjson.sourceforge.net/" TYPE OPTIONAL ) # We tell users that we need 1.0.3, but we really check just >= 1.0.0. This is because # upstream forgot to update version in lastfm/global.h, so it looks like 1.0.2. :-( # will be fixed in liblastfm-1.0.4 set( LIBLASTFM_MIN_VERSION "1.0.3" ) set_package_properties( LibLastFm PROPERTIES DESCRIPTION "Enable Last.Fm service, including scrobbling, song submissions, and suggested song dynamic playlists" URL "http://cdn.last.fm/client/liblastfm-${LIBLASTFM_MIN_VERSION}.tar.gz" TYPE OPTIONAL ) if( LIBOFA_FOUND ) set_package_properties(LibOFA PROPERTIES DESCRIPTION "Enable MusicDNS service" URL "http://code.google.com/p/musicip-libofa/" TYPE OPTIONAL) endif() ## gpodder Service find_package( Mygpo-qt5 ) set_package_properties( Mygpo-qt5 PROPERTIES DESCRIPTION "Enable gpodder.net service" URL "http://wiki.gpodder.org/wiki/Libmygpo-qt" TYPE OPTIONAL ) if( WITH_IPOD ) find_package(Ipod) set(IPOD_MIN_VERSION "0.8.2") if( IPOD_FOUND AND NOT WIN32 ) if ( ${IPOD_MIN_VERSION} VERSION_LESS ${IPOD_VERSION} ) set( IPOD_FOUND TRUE ) endif() endif() set_package_properties( Ipod PROPERTIES DESCRIPTION "Support Apple iPod/iPad/iPhone audio devices" URL "http://sourceforge.net/projects/gtkpod/" TYPE OPTIONAL ) find_package(GDKPixBuf) set_package_properties( GDKPixBuf PROPERTIES DESCRIPTION "Support for artwork on iPod audio devices via GDK-PixBuf" URL "http://developer.gnome.org/arch/imaging/gdkpixbuf.html" TYPE OPTIONAL ) endif() find_package(Mtp) set_package_properties( Mtp PROPERTIES DESCRIPTION "Enable Support for portable media devices that use the media transfer protocol" URL "http://libmtp.sourceforge.net/" TYPE OPTIONAL ) if( WITH_MP3Tunes ) find_package(CURL) set_package_properties( CURL PROPERTIES DESCRIPTION "Used to transfer data with URLs" URL "https://curl.haxx.se/" TYPE OPTIONAL ) find_package(LibXml2) set_package_properties( LibXml2 PROPERTIES DESCRIPTION "LibXML2 is an XML parser required by mp3tunes." URL "http://www.xmlsoft.org" TYPE OPTIONAL ) find_package(OpenSSL) find_package(Libgcrypt) if ( OPENSSL_FOUND OR LIBGCRYPT_FOUND ) set (_mp3tunes_crypto TRUE ) else () message( SEND_ERROR "Building with mp3tunes support REQUIRES either OpenSSL or GNU Libgcrypt" ) endif () set_package_properties( OpenSSL PROPERTIES DESCRIPTION "OpenSSL or GNU Libgcrypt provides cryptographic functions required by mp3tunes." URL "http://www.openssl.org/ or http://www.gnupg.org/download/#libgcrypt" TYPE OPTIONAL ) set_package_properties( Libgcrypt PROPERTIES DESCRIPTION "OpenSSL or GNU Libgcrypt provides cryptographic functions required by mp3tunes." URL "http://www.openssl.org/ or http://www.gnupg.org/download/#libgcrypt" TYPE OPTIONAL ) find_package(Loudmouth) set_package_properties( Loudmouth PROPERTIES DESCRIPTION "Loudmouth is the communication backend needed by mp3tunes for syncing." URL "http://www.loudmouth-project.org" TYPE OPTIONAL ) include(CheckQtGlib) set_package_properties( QT5_GLIB PROPERTIES DESCRIPTION "Qt5 must be compiled with glib support for mp3tunes" URL "http://www.trolltech.com" TYPE OPTIONAL ) endif() if( WITH_IPOD OR WITH_MP3Tunes ) pkg_search_module( GOBJECT REQUIRED gobject-2.0 ) set_package_properties( GOBJECT PROPERTIES DESCRIPTION "Required by libgpod and mp3tunes." URL "http://www.gtk.org" TYPE OPTIONAL ) pkg_search_module( GLIB2 glib-2.0 ) set_package_properties( GLIB2 PROPERTIES DESCRIPTION "Required by libgpod and mp3tunes" URL "http://www.gtk.org" TYPE OPTIONAL ) endif() find_program( CLAMZ_FOUND clamz PATH ) set_package_properties( CLAMZ PROPERTIES DESCRIPTION "Optional requirement to download songs from the Amazon MP3 store. Highly recommended on Linux, as the official downloader from Amazon is quite broken on many systems." URL "https://code.google.com/p/clamz/" TYPE OPTIONAL ) find_package(PythonInterp) set_package_properties( PYTHON PROPERTIES DESCRIPTION "Required for generating the autocompletion file for the script console" URL "https://www.python.org" TYPE OPTIONAL ) if( BUILD_TESTING AND NOT WIN32 ) enable_testing() # add_subdirectory( tests ) endif() add_subdirectory( src ) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) #Do not remove or modify these. The release script substitutes in for these #comments with appropriate doc and translation directories. #PO_SUBDIR #DOC_SUBDIR endif() if( WITH_UTILITIES ) set(EXEC_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX} CACHE PATH "Base directory for executables and libraries" FORCE) set(BIN_INSTALL_DIR "${EXEC_INSTALL_PREFIX}/bin" CACHE PATH "The subdirectory to the binaries prefix (default prefix/bin)" FORCE) add_subdirectory( utilities ) endif() if( WITH_PLAYGROUND ) add_subdirectory( playground ) message(STATUS "Included playground subdirectory in configuration") endif() include(CTest) diff --git a/src/core-impl/collections/db/sql/DatabaseUpdater.cpp b/src/core-impl/collections/db/sql/DatabaseUpdater.cpp index 2798605909..dadd53f6e7 100644 --- a/src/core-impl/collections/db/sql/DatabaseUpdater.cpp +++ b/src/core-impl/collections/db/sql/DatabaseUpdater.cpp @@ -1,1110 +1,1110 @@ /**************************************************************************************** * Copyright (c) 2007 Maximilian Kossick * * * * This program 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 2 of the License, or (at your option) any later * * version. * * * * This program 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 * * this program. If not, see . * ****************************************************************************************/ #include "DatabaseUpdater.h" #include "amarokconfig.h" #include "core/support/Amarok.h" #include "core/support/Debug.h" #include #include "SqlCollection.h" #include #include #include #include #include #include static const int DB_VERSION = 15; int DatabaseUpdater::expectedDatabaseVersion() { return DB_VERSION; } DatabaseUpdater::DatabaseUpdater( Collections::SqlCollection *collection ) : m_collection( collection ) , m_debugDatabaseContent( false ) { m_debugDatabaseContent = Amarok::config( "SqlCollection" ).readEntry( "DebugDatabaseContent", false ); } DatabaseUpdater::~DatabaseUpdater() { //nothing to do } bool DatabaseUpdater::needsUpdate() const { return adminValue( "DB_VERSION" ) != DB_VERSION; } bool DatabaseUpdater::schemaExists() const { return adminValue( "DB_VERSION" ) != 0; } bool DatabaseUpdater::update() { DEBUG_BLOCK int dbVersion = adminValue( "DB_VERSION" ); debug() << "Database version: " << dbVersion; if( dbVersion == 0 ) { createTables(); QString query = QString( "INSERT INTO admin(component, version) VALUES ('DB_VERSION', %1);" ).arg( DB_VERSION ); m_collection->sqlStorage()->query( query ); return true; } if( dbVersion < DB_VERSION ) { debug() << "Database out of date: database version is" << dbVersion << ", current version is" << DB_VERSION; switch( dbVersion ) { case 1: upgradeVersion1to2(); - /* Falls through. */ + Q_FALLTHROUGH(); case 2: upgradeVersion2to3(); - /* Falls through. */ + Q_FALLTHROUGH(); case 3: upgradeVersion3to4(); - /* Falls through. */ + Q_FALLTHROUGH(); case 4: upgradeVersion4to5(); - /* Falls through. */ + Q_FALLTHROUGH(); case 5: upgradeVersion5to6(); - /* Falls through. */ + Q_FALLTHROUGH(); case 6: upgradeVersion6to7(); - /* Falls through. */ + Q_FALLTHROUGH(); case 7: upgradeVersion7to8(); - /* Falls through. */ + Q_FALLTHROUGH(); case 8: //removes stray rows from albums that were caused by the initial full scan upgradeVersion8to9(); - /* Falls through. */ + Q_FALLTHROUGH(); case 9: //removes stray rows from albums that were caused by the initial full scan upgradeVersion9to10(); - /* Falls through. */ + Q_FALLTHROUGH(); case 10: upgradeVersion10to11(); - /* Falls through. */ + Q_FALLTHROUGH(); case 11: upgradeVersion11to12(); - /* Falls through. */ + Q_FALLTHROUGH(); case 12: upgradeVersion12to13(); - /* Falls through. */ + Q_FALLTHROUGH(); case 13: upgradeVersion13to14(); - /* Falls through. */ + Q_FALLTHROUGH(); case 14: upgradeVersion14to15(); dbVersion = 15; // be sure to update this manually when introducing new version! } QString query = QString( "UPDATE admin SET version = %1 WHERE component = 'DB_VERSION';" ).arg( dbVersion ); m_collection->sqlStorage()->query( query ); //NOTE: A rescan will be triggered automatically as a result of an upgrade. Don't trigger it here, as the //collection isn't fully initialized and this will trigger a crash/assert. return true; } if( dbVersion > DB_VERSION ) { KMessageBox::error(0, "

The Amarok collection database was created by a newer version of Amarok, " "and this version of Amarok cannot use it.

", "Database Type Unknown"); // FIXME: maybe we should tell them how to delete the database? // FIXME: exit() may be a little harsh, but QCoreApplication::exit() doesn't seem to work exit(1); } return false; } void DatabaseUpdater::upgradeVersion1to2() { DEBUG_BLOCK m_collection->sqlStorage()->query( "ALTER TABLE tracks " "ADD COLUMN albumgain FLOAT, " "ADD COLUMN albumpeakgain FLOAT, " "ADD COLUMN trackgain FLOAT," "ADD COLUMN trackpeakgain FLOAT;" ); } void DatabaseUpdater::upgradeVersion2to3() { DEBUG_BLOCK; auto storage = m_collection->sqlStorage(); storage->query( "DROP TABLE devices;" ); QString create = "CREATE TABLE devices " "(id " + storage->idType() + ",type " + storage->textColumnType() + ",label " + storage->textColumnType() + ",lastmountpoint " + storage->textColumnType() + ",uuid " + storage->textColumnType() + ",servername " + storage->textColumnType() + ",sharename " + storage->textColumnType() + ");"; storage->query( create ); storage->query( "CREATE INDEX devices_type ON devices( type );" ); storage->query( "CREATE UNIQUE INDEX devices_uuid ON devices( uuid );" ); storage->query( "CREATE INDEX devices_rshare ON devices( servername, sharename );" ); } void DatabaseUpdater::upgradeVersion3to4() { auto storage = m_collection->sqlStorage(); storage->query( "CREATE TABLE statistics_permanent " "(url " + storage->exactTextColumnType() + ",firstplayed DATETIME" ",lastplayed DATETIME" ",score FLOAT" ",rating INTEGER DEFAULT 0" ",playcount INTEGER)" ); storage->query( "CREATE UNIQUE INDEX ON statistics_permanent(url)" ); //Note: the above index query is invalid, but kept here for posterity storage->query( "CREATE TABLE statistics_tag " "(name " + storage->textColumnType() + ",artist " + storage->textColumnType() + ",album " + storage->textColumnType() + ",firstplayed DATETIME" ",lastplayed DATETIME" ",score FLOAT" ",rating INTEGER DEFAULT 0" ",playcount INTEGER)" ); storage->query( "CREATE UNIQUE INDEX ON statistics_tag(name,artist,album)" ); //Note: the above index query is invalid, but kept here for posterity } void DatabaseUpdater::upgradeVersion4to5() { auto storage = m_collection->sqlStorage(); //first the database storage->query( "ALTER DATABASE amarok DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci" ); //now the tables //first, drop tables that can easily be recreated by doing an update QStringList dropTables; dropTables << "jamendo_albums" << "jamendo_artists" << "jamendo_genre" << "jamendo_tracks"; dropTables << "magnatune_albums" << "magnatune_artists" << "magnatune_genre" << "magnatune_moods" << "magnatune_tracks"; dropTables << "opmldirectory_albums" << "opmldirectory_artists" << "opmldirectory_genre" << "opmldirectory_tracks"; foreach( const QString &table, dropTables ) storage->query( "DROP TABLE " + table ); //now, the rest of them QStringList tables; tables << "admin" << "albums" << "amazon" << "artists" << "bookmark_groups" << "bookmarks"; tables << "composers" << "devices" << "directories" << "genres" << "images" << "labels" << "lyrics"; tables << "playlist_groups" << "playlist_tracks" << "playlists"; tables << "podcastchannels" << "podcastepisodes"; tables << "statistics" << "statistics_permanent" << "statistics_tag"; tables << "tracks" << "urls" << "urls_labels" << "years"; foreach( const QString &table, tables ) storage->query( "ALTER TABLE " + table + " DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci" ); //now the columns (ugh) //first, varchar typedef QPair vcpair; QMultiMap columns; columns.insert( "admin", vcpair( "component", 255 ) ); columns.insert( "albums", vcpair( "name", textColumnLength() ) ); columns.insert( "amazon", vcpair( "asin", 20 ) ); columns.insert( "amazon", vcpair( "locale", 2 ) ); columns.insert( "amazon", vcpair( "filename", 33 ) ); columns.insert( "artists", vcpair( "name", textColumnLength() ) ); columns.insert( "bookmark_groups", vcpair( "name", 255 ) ); columns.insert( "bookmark_groups", vcpair( "description", 255 ) ); columns.insert( "bookmark_groups", vcpair( "custom", 255 ) ); columns.insert( "bookmarks", vcpair( "name", 255 ) ); columns.insert( "bookmarks", vcpair( "url", 1024 ) ); columns.insert( "bookmarks", vcpair( "description", 1024 ) ); columns.insert( "bookmarks", vcpair( "custom", 255 ) ); columns.insert( "composers", vcpair( "name", textColumnLength() ) ); columns.insert( "devices", vcpair( "type", 255 ) ); columns.insert( "devices", vcpair( "label", 255 ) ); columns.insert( "devices", vcpair( "lastmountpoint", 255 ) ); columns.insert( "devices", vcpair( "uuid", 255 ) ); columns.insert( "devices", vcpair( "servername", 255 ) ); columns.insert( "devices", vcpair( "sharename", 255 ) ); columns.insert( "directories", vcpair( "dir", 1024 ) ); columns.insert( "genres", vcpair( "name", 255 ) ); columns.insert( "images", vcpair( "path", 255 ) ); columns.insert( "labels", vcpair( "label", textColumnLength() ) ); columns.insert( "lyrics", vcpair( "url", 1024 ) ); columns.insert( "playlist_groups", vcpair( "name", 255 ) ); columns.insert( "playlist_groups", vcpair( "description", 255 ) ); columns.insert( "playlist_tracks", vcpair( "url", 1024 ) ); columns.insert( "playlist_tracks", vcpair( "title", 255 ) ); columns.insert( "playlist_tracks", vcpair( "album", 255 ) ); columns.insert( "playlist_tracks", vcpair( "artist", 255 ) ); columns.insert( "playlist_tracks", vcpair( "uniqueid", 128 ) ); columns.insert( "playlists", vcpair( "name", 255 ) ); columns.insert( "playlists", vcpair( "description", 255 ) ); columns.insert( "playlists", vcpair( "urlid", 1024 ) ); columns.insert( "podcastchannels", vcpair( "copyright", 255 ) ); columns.insert( "podcastchannels", vcpair( "directory", 255 ) ); columns.insert( "podcastchannels", vcpair( "labels", 255 ) ); columns.insert( "podcastchannels", vcpair( "subscribedate", 255 ) ); columns.insert( "podcastepisodes", vcpair( "guid", 1024 ) ); columns.insert( "podcastepisodes", vcpair( "mimetype", 255 ) ); columns.insert( "podcastepisodes", vcpair( "pubdate", 255 ) ); columns.insert( "statistics_permanent", vcpair( "url", 1024 ) ); columns.insert( "statistics_tag", vcpair( "name", 255 ) ); columns.insert( "statistics_tag", vcpair( "artist", 255 ) ); columns.insert( "tracks", vcpair( "title", textColumnLength() ) ); columns.insert( "urls", vcpair( "rpath", 1024 ) ); columns.insert( "urls", vcpair( "uniqueid", 128 ) ); columns.insert( "years", vcpair( "name", textColumnLength() ) ); QMultiMap::const_iterator i, iEnd; for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) { storage->query( "ALTER TABLE " + i.key() + " MODIFY " + i.value().first + " VARBINARY(" + QString::number( i.value().second ) + ')' ); storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " VARCHAR(" + QString::number( i.value().second ) + ") CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL" ); } columns.clear(); //text fields, not varchars columns.insert( "lyrics", vcpair( "lyrics", 0 ) ); columns.insert( "podcastchannels", vcpair( "url", 0 ) ); columns.insert( "podcastchannels", vcpair( "title", 0 ) ); columns.insert( "podcastchannels", vcpair( "weblink", 0 ) ); columns.insert( "podcastchannels", vcpair( "image", 0 ) ); columns.insert( "podcastchannels", vcpair( "description", 0 ) ); columns.insert( "podcastepisodes", vcpair( "url", 0 ) ); columns.insert( "podcastepisodes", vcpair( "localurl", 0 ) ); columns.insert( "podcastepisodes", vcpair( "title", 0 ) ); columns.insert( "podcastepisodes", vcpair( "subtitle", 0 ) ); columns.insert( "podcastepisodes", vcpair( "description", 0 ) ); columns.insert( "tracks", vcpair( "comment", 0 ) ); storage->query( "DROP INDEX url_podchannel ON podcastchannels" ); storage->query( "DROP INDEX url_podepisode ON podcastepisodes" ); storage->query( "DROP INDEX localurl_podepisode ON podcastepisodes" ); for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) { storage->query( "ALTER TABLE " + i.key() + " MODIFY " + i.value().first + " BLOB" ); storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " TEXT CHARACTER SET utf8 NOT NULL" ); } storage->query( "CREATE FULLTEXT INDEX url_podchannel ON podcastchannels( url )" ); storage->query( "CREATE FULLTEXT INDEX url_podepisode ON podcastepisodes( url )" ); storage->query( "CREATE FULLTEXT INDEX localurl_podepisode ON podcastepisodes( localurl )" ); } void DatabaseUpdater::upgradeVersion5to6() { DEBUG_BLOCK auto storage = m_collection->sqlStorage(); //first, drop tables that can easily be recreated by doing an update QStringList dropTables; dropTables << "jamendo_albums" << "jamendo_artists" << "jamendo_genre" << "jamendo_tracks"; dropTables << "magnatune_albums" << "magnatune_artists" << "magnatune_genre" << "magnatune_moods" << "magnatune_tracks"; dropTables << "opmldirectory_albums" << "opmldirectory_artists" << "opmldirectory_genre" << "opmldirectory_tracks"; foreach( const QString &table, dropTables ) storage->query( "DROP TABLE " + table ); //now, the rest of them QStringList tables; tables << "admin" << "albums" << "amazon" << "artists" << "bookmark_groups" << "bookmarks"; tables << "composers" << "devices" << "directories" << "genres" << "images" << "labels" << "lyrics"; tables << "playlist_groups" << "playlist_tracks" << "playlists"; tables << "podcastchannels" << "podcastepisodes"; tables << "statistics" << "statistics_permanent" << "statistics_tag"; tables << "tracks" << "urls" << "urls_labels" << "years"; foreach( const QString &table, tables ) storage->query( "ALTER TABLE " + table + " ENGINE = MyISAM" ); typedef QPair vcpair; QMultiMap columns; columns.insert( "bookmarks", vcpair( "url", 1000 ) ); columns.insert( "bookmarks", vcpair( "description", 1000 ) ); columns.insert( "directories", vcpair( "dir", 1000 ) ); columns.insert( "lyrics", vcpair( "url", 324 ) ); columns.insert( "playlist_tracks", vcpair( "url", 1000 ) ); columns.insert( "playlists", vcpair( "urlid", 1000 ) ); columns.insert( "podcastepisodes", vcpair( "guid", 1000 ) ); columns.insert( "statistics_permanent", vcpair( "url", 324 ) ); columns.insert( "urls", vcpair( "rpath", 324 ) ); columns.insert( "devices", vcpair( "servername", 80 ) ); columns.insert( "devices", vcpair( "sharename", 240 ) ); columns.insert( "statistics_tag", vcpair( "name", 108 ) ); columns.insert( "statistics_tag", vcpair( "artist", 108 ) ); columns.insert( "statistics_tag", vcpair( "album", 108 ) ); QMultiMap::const_iterator i, iEnd; for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " VARCHAR(" + QString::number( i.value().second ) + ") " ); storage->query( "CREATE INDEX devices_rshare ON devices( servername, sharename );" ); storage->query( "CREATE UNIQUE INDEX lyrics_url ON lyrics(url);" ); storage->query( "CREATE UNIQUE INDEX urls_id_rpath ON urls(deviceid, rpath);" ); storage->query( "CREATE UNIQUE INDEX stats_tag_name_artist_album ON statistics_tag(name,artist,album)" ); } void DatabaseUpdater::upgradeVersion6to7() { DEBUG_BLOCK auto storage = m_collection->sqlStorage(); typedef QPair vcpair; QMultiMap columns; columns.insert( "directories", vcpair( "dir", 1000 ) ); columns.insert( "urls", vcpair( "rpath", 324 ) ); columns.insert( "statistics_permanent", vcpair( "url", 324 ) ); QMultiMap::const_iterator i, iEnd; for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) { storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " VARCHAR(" + QString::number( i.value().second ) + ") COLLATE utf8_bin NOT NULL" ); } columns.clear(); } void DatabaseUpdater::upgradeVersion7to8() { DEBUG_BLOCK auto storage = m_collection->sqlStorage(); QHash< int, int > trackLengthHash; // First, get the lengths from the db and insert them into a hash const QStringList result = storage->query( "SELECT id, length FROM tracks" ); QListIterator iter(result); while( iter.hasNext() ) trackLengthHash.insert( iter.next().toInt(), iter.next().toInt() ); // Now Iterate over the hash, and insert each track back in, changing the length to milliseconds QHashIterator iter2( trackLengthHash ); const QString updateString = QString( "UPDATE tracks SET length=%1 WHERE id=%2 ;"); while( iter2.hasNext() ) { iter2.next(); debug() << "Running the following query: " << updateString.arg( QString::number( iter2.value() * 1000 ), QString::number( iter2.key() ) ); storage->query( updateString.arg( QString::number( iter2.value() * 1000 ), QString::number( iter2.key() ) ) ); } } void DatabaseUpdater::upgradeVersion8to9() { deleteAllRedundant( "album" ); } void DatabaseUpdater::upgradeVersion9to10() { DEBUG_BLOCK auto storage = m_collection->sqlStorage(); //first the database storage->query( "ALTER DATABASE amarok DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_bin" ); //now the tables //first, drop tables that can easily be recreated by doing an update QStringList dropTables; dropTables << "jamendo_albums" << "jamendo_artists" << "jamendo_genre" << "jamendo_tracks"; dropTables << "magnatune_albums" << "magnatune_artists" << "magnatune_genre" << "magnatune_moods" << "magnatune_tracks"; dropTables << "opmldirectory_albums" << "opmldirectory_artists" << "opmldirectory_genre" << "opmldirectory_tracks"; foreach( const QString &table, dropTables ) storage->query( "DROP TABLE " + table ); //now, the rest of them QStringList tables; tables << "admin" << "albums" << "amazon" << "artists" << "bookmark_groups" << "bookmarks"; tables << "composers" << "devices" << "directories" << "genres" << "images" << "labels" << "lyrics"; tables << "playlist_groups" << "playlist_tracks" << "playlists"; tables << "podcastchannels" << "podcastepisodes"; tables << "statistics" << "statistics_permanent" << "statistics_tag"; tables << "tracks" << "urls" << "urls_labels" << "years"; foreach( const QString &table, tables ) storage->query( "ALTER TABLE " + table + " DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_bin COLLATE utf8_bin ENGINE = MyISAM" ); //now the columns (ugh) //first, varchar typedef QPair vcpair; QMultiMap columns; columns.insert( "admin", vcpair( "component", 255 ) ); columns.insert( "albums", vcpair( "name", textColumnLength() ) ); columns.insert( "amazon", vcpair( "asin", 20 ) ); columns.insert( "amazon", vcpair( "locale", 2 ) ); columns.insert( "amazon", vcpair( "filename", 33 ) ); columns.insert( "artists", vcpair( "name", textColumnLength() ) ); columns.insert( "bookmark_groups", vcpair( "name", 255 ) ); columns.insert( "bookmark_groups", vcpair( "description", 255 ) ); columns.insert( "bookmark_groups", vcpair( "custom", 255 ) ); columns.insert( "bookmarks", vcpair( "name", 255 ) ); columns.insert( "bookmarks", vcpair( "url", 1000 ) ); columns.insert( "bookmarks", vcpair( "description", 1000 ) ); columns.insert( "bookmarks", vcpair( "custom", 255 ) ); columns.insert( "composers", vcpair( "name", textColumnLength() ) ); columns.insert( "devices", vcpair( "type", 255 ) ); columns.insert( "devices", vcpair( "label", 255 ) ); columns.insert( "devices", vcpair( "lastmountpoint", 255 ) ); columns.insert( "devices", vcpair( "uuid", 255 ) ); columns.insert( "devices", vcpair( "servername", 80 ) ); columns.insert( "devices", vcpair( "sharename", 240 ) ); columns.insert( "directories", vcpair( "dir", 1000 ) ); columns.insert( "genres", vcpair( "name", textColumnLength() ) ); columns.insert( "images", vcpair( "path", 255 ) ); columns.insert( "labels", vcpair( "label", textColumnLength() ) ); columns.insert( "lyrics", vcpair( "url", 324 ) ); columns.insert( "playlist_groups", vcpair( "name", 255 ) ); columns.insert( "playlist_groups", vcpair( "description", 255 ) ); columns.insert( "playlist_tracks", vcpair( "url", 1000 ) ); columns.insert( "playlist_tracks", vcpair( "title", 255 ) ); columns.insert( "playlist_tracks", vcpair( "album", 255 ) ); columns.insert( "playlist_tracks", vcpair( "artist", 255 ) ); columns.insert( "playlist_tracks", vcpair( "uniqueid", 128 ) ); columns.insert( "playlists", vcpair( "name", 255 ) ); columns.insert( "playlists", vcpair( "description", 255 ) ); columns.insert( "playlists", vcpair( "urlid", 1000 ) ); columns.insert( "podcastchannels", vcpair( "copyright", 255 ) ); columns.insert( "podcastchannels", vcpair( "directory", 255 ) ); columns.insert( "podcastchannels", vcpair( "labels", 255 ) ); columns.insert( "podcastchannels", vcpair( "subscribedate", 255 ) ); columns.insert( "podcastepisodes", vcpair( "guid", 1000 ) ); columns.insert( "podcastepisodes", vcpair( "mimetype", 255 ) ); columns.insert( "podcastepisodes", vcpair( "pubdate", 255 ) ); columns.insert( "statistics_permanent", vcpair( "url", 324 ) ); columns.insert( "statistics_tag", vcpair( "name", 108 ) ); columns.insert( "statistics_tag", vcpair( "artist", 108 ) ); columns.insert( "statistics_tag", vcpair( "album", 108 ) ); columns.insert( "tracks", vcpair( "title", textColumnLength() ) ); columns.insert( "urls", vcpair( "rpath", 324 ) ); columns.insert( "urls", vcpair( "uniqueid", 128 ) ); columns.insert( "years", vcpair( "name", textColumnLength() ) ); QMultiMap::const_iterator i, iEnd; for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) { storage->query( "ALTER TABLE " + i.key() + " MODIFY " + i.value().first + " VARBINARY(" + QString::number( i.value().second ) + ')' ); storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " VARCHAR(" + QString::number( i.value().second ) + ") CHARACTER SET utf8 COLLATE utf8_bin NOT NULL" ); } storage->query( "CREATE INDEX devices_rshare ON devices( servername, sharename );" ); storage->query( "CREATE UNIQUE INDEX lyrics_url ON lyrics(url);" ); storage->query( "CREATE UNIQUE INDEX urls_id_rpath ON urls(deviceid, rpath);" ); storage->query( "CREATE UNIQUE INDEX stats_tag_name_artist_album ON statistics_tag(name,artist,album)" ); columns.clear(); //text fields, not varchars columns.insert( "lyrics", vcpair( "lyrics", 0 ) ); columns.insert( "podcastchannels", vcpair( "url", 0 ) ); columns.insert( "podcastchannels", vcpair( "title", 0 ) ); columns.insert( "podcastchannels", vcpair( "weblink", 0 ) ); columns.insert( "podcastchannels", vcpair( "image", 0 ) ); columns.insert( "podcastchannels", vcpair( "description", 0 ) ); columns.insert( "podcastepisodes", vcpair( "url", 0 ) ); columns.insert( "podcastepisodes", vcpair( "localurl", 0 ) ); columns.insert( "podcastepisodes", vcpair( "title", 0 ) ); columns.insert( "podcastepisodes", vcpair( "subtitle", 0 ) ); columns.insert( "podcastepisodes", vcpair( "description", 0 ) ); columns.insert( "tracks", vcpair( "comment", 0 ) ); storage->query( "DROP INDEX url_podchannel ON podcastchannels" ); storage->query( "DROP INDEX url_podepisode ON podcastepisodes" ); storage->query( "DROP INDEX localurl_podepisode ON podcastepisodes" ); for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) { storage->query( "ALTER TABLE " + i.key() + " MODIFY " + i.value().first + " BLOB" ); storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " TEXT CHARACTER SET utf8 COLLATE utf8_bin NOT NULL" ); } storage->query( "CREATE FULLTEXT INDEX url_podchannel ON podcastchannels( url )" ); storage->query( "CREATE FULLTEXT INDEX url_podepisode ON podcastepisodes( url )" ); storage->query( "CREATE FULLTEXT INDEX localurl_podepisode ON podcastepisodes( localurl )" ); } void DatabaseUpdater::upgradeVersion10to11() { DEBUG_BLOCK //OK, this isn't really a database upgrade, but it does affect scanning. //New default is for the charset detector not to run; but those that have existing collection //won't like it if suddenly that changes their behavior, so set to true for existing collections AmarokConfig::setUseCharsetDetector( true ); } void DatabaseUpdater::upgradeVersion11to12() { DEBUG_BLOCK //Counteract the above -- force it off for everyone except those explicitly enabling it. AmarokConfig::setUseCharsetDetector( false ); } void DatabaseUpdater::upgradeVersion12to13() { DEBUG_BLOCK m_collection->sqlStorage()->query( "UPDATE urls SET uniqueid = REPLACE(uniqueid, 'MB_', 'mb-');" ); } void DatabaseUpdater::upgradeVersion13to14() { DEBUG_BLOCK auto storage = m_collection->sqlStorage(); /* Following commands transition lyrics table from text-based urls (in fact just rpath * parts) to references to urls table. */ // first, rename column storage->query( "ALTER TABLE lyrics CHANGE url rpath VARCHAR(324) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL" ); // add integer column for url id storage->query( "ALTER TABLE lyrics ADD COLUMN url INT NULL DEFAULT NULL FIRST" ); // try to extract url id from urls table using rpath storage->query( "UPDATE lyrics l SET l.url = (SELECT u.id FROM urls u WHERE u.rpath = l.rpath LIMIT 1)" ); // delete entries with no matches in urls table; these should be just stale ones storage->query( "DELETE FROM lyrics WHERE url IS NULL" ); // make the url columnt non-null storage->query( "ALTER TABLE lyrics MODIFY url INT NOT NULL" ); // select duplicate ids into temporary table storage->query( "CREATE TEMPORARY TABLE duplicate_lyrics_ids ( id INT NOT NULL ) " "ENGINE=MEMORY SELECT dupl.id FROM lyrics orig " "LEFT JOIN lyrics dupl ON dupl.url = orig.url AND dupl.id > orig.id" ); // delete duplicate lyrics entries storage->query( "DELETE FROM lyrics WHERE id IN (SELECT id FROM duplicate_lyrics_ids)" ); // drop unwanted columns along with indexes defined on them storage->query( "ALTER TABLE lyrics DROP id, DROP rpath" ); // add primary key; should definitely not fail as we have removed duplicate entries storage->query( "ALTER TABLE lyrics ADD PRIMARY KEY(url)" ); } void DatabaseUpdater::upgradeVersion14to15() { /* This update solves bug 302837. In short, updates * 4 -> 5, 5 -> 6, 6 -> 7 and 9 -> 10 ignored NULL status of some columns and replaced * them with NOT NULL columns, causing various consequences, one of them is Dynamic * Collection not working. Fix it back. * * A list of columns to fix was obtained by comparing a database created by * Amarok 2.1.1 and then upgraded to current version with a db freshly created by * Amarok 2.6-git. */ DEBUG_BLOCK auto storage = m_collection->sqlStorage(); // zero length = TEXT datatype typedef QPair vcpair; QMultiMap columns; columns.insert( "admin", vcpair( "component", 255 ) ); columns.insert( "devices", vcpair( "type", 255 ) ); columns.insert( "devices", vcpair( "label", 255 ) ); columns.insert( "devices", vcpair( "lastmountpoint", 255 ) ); columns.insert( "devices", vcpair( "uuid", 255 ) ); columns.insert( "devices", vcpair( "servername", 80 ) ); columns.insert( "devices", vcpair( "sharename", 240 ) ); columns.insert( "labels", vcpair( "label", textColumnLength() ) ); columns.insert( "lyrics", vcpair( "lyrics", 0 ) ); columns.insert( "playlists", vcpair( "name", 255 ) ); columns.insert( "playlists", vcpair( "description", 255 ) ); columns.insert( "playlists", vcpair( "urlid", 1000 ) ); columns.insert( "playlist_groups", vcpair( "name", 255 ) ); columns.insert( "playlist_groups", vcpair( "description", 255 ) ); columns.insert( "playlist_tracks", vcpair( "url", 1000 ) ); columns.insert( "playlist_tracks", vcpair( "title", 255 ) ); columns.insert( "playlist_tracks", vcpair( "album", 255 ) ); columns.insert( "playlist_tracks", vcpair( "artist", 255 ) ); columns.insert( "playlist_tracks", vcpair( "uniqueid", 128 ) ); columns.insert( "podcastchannels", vcpair( "url", 0 ) ); columns.insert( "podcastchannels", vcpair( "title", 0 ) ); columns.insert( "podcastchannels", vcpair( "weblink", 0 ) ); columns.insert( "podcastchannels", vcpair( "image", 0 ) ); columns.insert( "podcastchannels", vcpair( "description", 0 ) ); columns.insert( "podcastchannels", vcpair( "copyright", 255 ) ); columns.insert( "podcastchannels", vcpair( "directory", 255 ) ); columns.insert( "podcastchannels", vcpair( "labels", 255 ) ); columns.insert( "podcastchannels", vcpair( "subscribedate", 255 ) ); columns.insert( "podcastepisodes", vcpair( "url", 0 ) ); columns.insert( "podcastepisodes", vcpair( "localurl", 0 ) ); columns.insert( "podcastepisodes", vcpair( "guid", 1000 ) ); columns.insert( "podcastepisodes", vcpair( "title", 0 ) ); columns.insert( "podcastepisodes", vcpair( "subtitle", 0 ) ); columns.insert( "podcastepisodes", vcpair( "description", 0 ) ); columns.insert( "podcastepisodes", vcpair( "mimetype", 255 ) ); columns.insert( "podcastepisodes", vcpair( "pubdate", 255 ) ); columns.insert( "statistics_tag", vcpair( "name", 108 ) ); columns.insert( "statistics_tag", vcpair( "artist", 108 ) ); columns.insert( "statistics_tag", vcpair( "album", 108 ) ); columns.insert( "tracks", vcpair( "title", textColumnLength() ) ); columns.insert( "tracks", vcpair( "comment", 0 ) ); columns.insert( "urls", vcpair( "uniqueid", 128 ) ); QMapIterator it( columns ); while( it.hasNext() ) { it.next(); QString table = it.key(); QString column = it.value().first; int length = it.value().second; QString query; if( length > 0 ) query = QString( "ALTER TABLE `%1` CHANGE `%2` `%2` VARCHAR(%3) CHARACTER SET utf8 " "COLLATE utf8_bin NULL DEFAULT NULL" ).arg( table, column ).arg( length ); else query = QString( "ALTER TABLE `%1` CHANGE `%2` `%2` TEXT CHARACTER SET utf8 " "COLLATE utf8_bin" ).arg( table, column ); storage->query( query ); } // there may be a stale unique index on the urls table, remove it if it is there: QStringList results = storage->query( "SHOW CREATE TABLE urls" ); bool oldIndexFound = results.value( 1 ).contains( "UNIQUE KEY `uniqueid`" ); if( oldIndexFound ) { debug() << "dropping obsolete INDEX uniqueid on table urls"; storage->query( "DROP INDEX uniqueid ON urls" ); } } void DatabaseUpdater::cleanupDatabase() { // maybe clean up redundant information here? } void DatabaseUpdater::checkTables( bool full ) { DEBUG_BLOCK auto storage = m_collection->sqlStorage(); QStringList res = storage->query( "SHOW TABLES" ); if( res.count() > 0 ) { foreach( const QString &table, res ) storage->query( "CHECK TABLE " + table + ( full ? " EXTENDED;" : " MEDIUM;" ) ); } } void DatabaseUpdater::createTables() const { DEBUG_BLOCK auto storage = m_collection->sqlStorage(); // see docs/database/amarokTables.svg for documentation about database layout { QString c = "CREATE TABLE admin (component " + storage->textColumnType() + ", version INTEGER) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( c ); } { QString create = "CREATE TABLE devices " "(id " + storage->idType() + ",type " + storage->textColumnType() + ",label " + storage->textColumnType() + ",lastmountpoint " + storage->textColumnType() + ",uuid " + storage->textColumnType() + ",servername " + storage->textColumnType(80) + ",sharename " + storage->textColumnType(240) + ") COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE INDEX devices_type ON devices( type );" ); storage->query( "CREATE UNIQUE INDEX devices_uuid ON devices( uuid );" ); storage->query( "CREATE INDEX devices_rshare ON devices( servername, sharename );" ); } { QString create = "CREATE TABLE urls " "(id " + storage->idType() + ",deviceid INTEGER" ",rpath " + storage->exactIndexableTextColumnType() + " NOT NULL" + ",directory INTEGER" ",uniqueid " + storage->exactTextColumnType(128) + " UNIQUE) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX urls_id_rpath ON urls(deviceid, rpath);" ); storage->query( "CREATE INDEX urls_uniqueid ON urls(uniqueid);" ); storage->query( "CREATE INDEX urls_directory ON urls(directory);" ); } { QString create = "CREATE TABLE directories " "(id " + storage->idType() + ",deviceid INTEGER" ",dir " + storage->exactTextColumnType() + " NOT NULL" + ",changedate INTEGER) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE INDEX directories_deviceid ON directories(deviceid);" ); } { QString create = "CREATE TABLE artists " "(id " + storage->idType() + ",name " + storage->textColumnType() + " NOT NULL) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX artists_name ON artists(name);" ); } { QString create = "CREATE TABLE images " "(id " + storage->idType() + ",path " + storage->textColumnType() + " NOT NULL) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX images_name ON images(path);" ); } { QString c = "CREATE TABLE albums " "(id " + storage->idType() + ",name " + storage->textColumnType() + " NOT NULL" ",artist INTEGER" + ",image INTEGER) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( c ); storage->query( "CREATE INDEX albums_name ON albums(name);" ); storage->query( "CREATE INDEX albums_artist ON albums(artist);" ); storage->query( "CREATE INDEX albums_image ON albums(image);" ); storage->query( "CREATE UNIQUE INDEX albums_name_artist ON albums(name,artist);" ); //the index below should not be necessary. uncomment if a query plan shows it is //storage->query( "CREATE UNIQUE INDEX albums_artist_name ON albums(artist,name);" ); } { QString create = "CREATE TABLE genres " "(id " + storage->idType() + ",name " + storage->textColumnType() + " NOT NULL) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX genres_name ON genres(name);" ); } { QString create = "CREATE TABLE composers " "(id " + storage->idType() + ",name " + storage->textColumnType() + " NOT NULL) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX composers_name ON composers(name);" ); } { QString create = "CREATE TABLE years " "(id " + storage->idType() + ",name " + storage->textColumnType() + " NOT NULL) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX years_name ON years(name);" ); } { QString c = "CREATE TABLE tracks " "(id " + storage->idType() + ",url INTEGER" ",artist INTEGER" ",album INTEGER" ",genre INTEGER" ",composer INTEGER" ",year INTEGER" ",title " + storage->textColumnType() + ",comment " + storage->longTextColumnType() + ",tracknumber INTEGER" ",discnumber INTEGER" ",bitrate INTEGER" ",length INTEGER" ",samplerate INTEGER" ",filesize INTEGER" ",filetype INTEGER" //does this still make sense? ",bpm FLOAT" ",createdate INTEGER" // this is the track creation time ",modifydate INTEGER" // UNUSED currently ",albumgain FLOAT" ",albumpeakgain FLOAT" // decibels, relative to albumgain ",trackgain FLOAT" ",trackpeakgain FLOAT" // decibels, relative to trackgain ") COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( c ); storage->query( "CREATE UNIQUE INDEX tracks_url ON tracks(url);" ); QStringList indices; indices << "id" << "artist" << "album" << "genre" << "composer" << "year" << "title"; indices << "discnumber" << "createdate" << "length" << "bitrate" << "filesize"; foreach( const QString &index, indices ) { QString query = QString( "CREATE INDEX tracks_%1 ON tracks(%2);" ).arg( index, index ); storage->query( query ); } } { QString c = "CREATE TABLE statistics " "(id " + storage->idType() + ",url INTEGER NOT NULL" ",createdate INTEGER" // this is the first played time ",accessdate INTEGER" // this is the last played time ",score FLOAT" ",rating INTEGER NOT NULL DEFAULT 0" // the "default" undefined rating is 0. We cannot display anything else. ",playcount INTEGER NOT NULL DEFAULT 0" // a track is either played or not. ",deleted BOOL NOT NULL DEFAULT " + storage->boolFalse() + ") COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( c ); storage->query( "CREATE UNIQUE INDEX statistics_url ON statistics(url);" ); QStringList indices; indices << "createdate" << "accessdate" << "score" << "rating" << "playcount"; foreach( const QString &index, indices ) { QString q = QString( "CREATE INDEX statistics_%1 ON statistics(%2);" ).arg( index, index ); storage->query( q ); } } { QString q = "CREATE TABLE labels " "(id " + storage->idType() + ",label " + storage->textColumnType() + ") COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( q ); storage->query( "CREATE UNIQUE INDEX labels_label ON labels(label);" ); QString r = "CREATE TABLE urls_labels(url INTEGER, label INTEGER);"; storage->query( r ); storage->query( "CREATE INDEX urlslabels_url ON urls_labels(url);" ); storage->query( "CREATE INDEX urlslabels_label ON urls_labels(label);" ); } { QString q = "CREATE TABLE amazon (" "asin " + storage->textColumnType( 20 ) + ",locale " + storage->textColumnType( 2 ) + ",filename " + storage->textColumnType( 33 ) + ",refetchdate INTEGER ) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( q ); storage->query( "CREATE INDEX amazon_date ON amazon(refetchdate);" ); } { QString q = "CREATE TABLE lyrics (" "url INTEGER PRIMARY KEY" ",lyrics " + storage->longTextColumnType() + ") COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( q ); } storage->query( "INSERT INTO admin(component,version) " "VALUES('AMAROK_TRACK'," + QString::number( DB_VERSION ) + ");" ); { storage->query( "CREATE TABLE statistics_permanent " "(url " + storage->exactIndexableTextColumnType() + " NOT NULL" + ",firstplayed DATETIME" ",lastplayed DATETIME" ",score FLOAT" ",rating INTEGER DEFAULT 0" ",playcount INTEGER) COLLATE = utf8_bin ENGINE = MyISAM;" ); //Below query is invalid! Fix it, and then put the proper query in an upgrade function! storage->query( "CREATE UNIQUE INDEX stats_perm_url ON statistics_permanent(url)" ); storage->query( "CREATE TABLE statistics_tag " "(name " + storage->textColumnType(108) + ",artist " + storage->textColumnType(108) + ",album " + storage->textColumnType(108) + ",firstplayed DATETIME" ",lastplayed DATETIME" ",score FLOAT" ",rating INTEGER DEFAULT 0" ",playcount INTEGER) COLLATE = utf8_bin ENGINE = MyISAM" ); //Below query is invalid! Fix it, and then put the proper query in an upgrade function! storage->query( "CREATE UNIQUE INDEX stats_tag_name_artist_album ON statistics_tag(name,artist,album)" ); } } int DatabaseUpdater::adminValue( const QString &key ) const { auto storage = m_collection->sqlStorage(); QStringList columns = storage->query( QString( "SELECT column_name FROM INFORMATION_SCHEMA.columns " "WHERE table_name='admin'" ) ); if( columns.isEmpty() ) return 0; //no table with that name QStringList values = storage->query( QString( "SELECT version FROM admin WHERE component = '%1';") .arg(storage->escape( key ) ) ); if( values.isEmpty() ) return 0; return values.first().toInt(); } void DatabaseUpdater::deleteAllRedundant( const QString &type ) { auto storage = m_collection->sqlStorage(); const QString tablename = type + 's'; if( type == "artist" ) storage->query( QString( "DELETE FROM artists " "WHERE id NOT IN ( SELECT artist FROM tracks WHERE artist IS NOT NULL ) AND " "id NOT IN ( SELECT artist FROM albums WHERE artist IS NOT NULL )") ); else storage->query( QString( "DELETE FROM %1 " "WHERE id NOT IN ( SELECT %2 FROM tracks WHERE %2 IS NOT NULL )" ). arg( tablename, type ) ); } void DatabaseUpdater::deleteOrphanedByDirectory( const QString &table ) { auto storage = m_collection->sqlStorage(); QString query( "DELETE FROM %1 WHERE directory NOT IN ( SELECT id FROM directories )" ); storage->query( query.arg( table ) ); } void DatabaseUpdater::deleteOrphanedByUrl( const QString &table ) { auto storage = m_collection->sqlStorage(); QString query( "DELETE FROM %1 WHERE url NOT IN ( SELECT id FROM urls )" ); storage->query( query.arg( table ) ); } void DatabaseUpdater::removeFilesInDir( int deviceid, const QString &rdir ) { auto storage = m_collection->sqlStorage(); QString select = QString( "SELECT urls.id FROM urls LEFT JOIN directories ON urls.directory = directories.id " "WHERE directories.deviceid = %1 AND directories.dir = '%2';" ) .arg( QString::number( deviceid ), storage->escape( rdir ) ); QStringList idResult = storage->query( select ); if( !idResult.isEmpty() ) { QString id; QString ids; QStringList::ConstIterator it = idResult.constBegin(), end = idResult.constEnd(); while( it != end ) { id = (*(it++)); if( !ids.isEmpty() ) ids += ','; ids += id; } QString drop = QString( "DELETE FROM tracks WHERE url IN (%1);" ).arg( ids ); storage->query( drop ); } } void DatabaseUpdater::writeCSVFile( const QString &table, const QString &filename, bool forceDebug ) { auto storage = m_collection->sqlStorage(); if( !forceDebug && !m_debugDatabaseContent ) return; QString ctable = table; QStringList columns = storage->query( QString( "SELECT column_name FROM INFORMATION_SCHEMA.columns WHERE table_name='%1'" ) .arg( storage->escape( ctable ) ) ); if( columns.isEmpty() ) return; //no table with that name // ok. it was probably a little bit unlucky to name a table statistics // that clashes with INFORMATION_SCHEMA.statistics, a build in table. if( table == "statistics" && columns.count() > 15 ) { // delete all columns with full upper case name. Those are the buildins. for( int i = columns.count()-1; i>= 0; --i ) { if( columns[i].toUpper() == columns[i] ) columns.removeAt( i ); } } QString select; foreach( const QString &column, columns ) { if( !select.isEmpty() ) select.append( ',' ); select.append( column ); } QString query = "SELECT %1 FROM %2"; QStringList result = storage->query( query.arg( select, storage->escape( table ) ) ); QFile file( filename ); if( file.open( QFile::WriteOnly | QFile::Text | QFile::Truncate ) ) { QTextStream stream( &file ); int i = 0; QString line; //write header foreach( const QString &column, columns ) { stream << column; stream << ';'; } stream << '\n'; foreach( const QString &data, result ) { stream << data; stream << ';'; ++i; if( i % columns.count() == 0 ) stream << '\n'; } file.close(); } } diff --git a/src/core-impl/collections/support/MemoryMatcher.cpp b/src/core-impl/collections/support/MemoryMatcher.cpp index 1da6016c65..064df4dccd 100644 --- a/src/core-impl/collections/support/MemoryMatcher.cpp +++ b/src/core-impl/collections/support/MemoryMatcher.cpp @@ -1,366 +1,366 @@ /**************************************************************************************** * Copyright (c) 2007 Nikolaj Hald Nielsen * * Copyright (c) 2007 Maximilian Kossick * * * * This program 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 2 of the License, or (at your option) any later * * version. * * * * This program 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 * * this program. If not, see . * ****************************************************************************************/ #include "MemoryMatcher.h" using namespace Meta; MemoryMatcher::MemoryMatcher() : m_next( 0 ) { } MemoryMatcher::~MemoryMatcher() { delete m_next; } bool MemoryMatcher::isLast() const { return !m_next; } MemoryMatcher* MemoryMatcher::next() const { return m_next; } void MemoryMatcher::setNext( MemoryMatcher *next ) { delete m_next; m_next = next; } TrackMatcher::TrackMatcher( TrackPtr track ) : MemoryMatcher() , m_track( track ) {} TrackList TrackMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_track || !memColl ) return TrackList(); TrackMap trackMap = memColl->trackMap(); TrackList result; if ( trackMap.contains( m_track->uidUrl() ) ) result.append( trackMap.value( m_track->uidUrl() ) ); return result; //checking for another matcher is not necessary } TrackList TrackMatcher::match( const TrackList &tracks ) { if( !m_track ) return TrackList(); TrackList result; QString url = m_track->uidUrl(); foreach( TrackPtr track, tracks ) if ( track->uidUrl() == url ) { result.append( track ); break; } return result; //checking for another matcher is not necessary } ArtistMatcher::ArtistMatcher( ArtistPtr artist, Collections::QueryMaker::ArtistMatchBehaviour artistMode ) : MemoryMatcher() , m_artist( artist ) , m_queryMode( artistMode ) {} TrackList ArtistMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_artist || !memColl ) return TrackList(); if( !memColl->artistMap().contains( m_artist->name() ) ) return TrackList(); ArtistPtr artist = memColl->artistMap().value( m_artist->name() ); TrackList matchingTracks; switch( m_queryMode ) { case Collections::QueryMaker::AlbumOrTrackArtists: case Collections::QueryMaker::AlbumArtists: foreach( AlbumPtr album, memColl->albumMap() ) if( album->albumArtist() == artist ) matchingTracks.append( album->tracks() ); if( m_queryMode != Collections::QueryMaker::AlbumOrTrackArtists ) break; - /* Falls through. */ + Q_FALLTHROUGH(); case Collections::QueryMaker::TrackArtists: matchingTracks.append( artist->tracks() ); } if( isLast() || matchingTracks.isEmpty() ) return matchingTracks; else return next()->match( matchingTracks ); } TrackList ArtistMatcher::match( const TrackList &tracks ) { if( !m_artist ) return TrackList(); TrackList matchingTracks; QString name = m_artist->name(); foreach( TrackPtr track, tracks ) switch( m_queryMode ) { case Collections::QueryMaker::AlbumOrTrackArtists: case Collections::QueryMaker::AlbumArtists: if( track->album()->hasAlbumArtist() && track->album()->albumArtist()->name() == name ) matchingTracks.append( track ); if( m_queryMode != Collections::QueryMaker::AlbumOrTrackArtists ) break; - /* Falls through. */ + Q_FALLTHROUGH(); case Collections::QueryMaker::TrackArtists: if( track->artist()->name() == name ) matchingTracks.append( track ); } if( isLast() || matchingTracks.isEmpty() ) return matchingTracks; else return next()->match( matchingTracks ); } AlbumMatcher::AlbumMatcher( AlbumPtr album ) : MemoryMatcher() , m_album( album ) {} TrackList AlbumMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_album || !memColl ) return TrackList(); AlbumMap albumMap = memColl->albumMap(); if ( albumMap.contains( m_album ) ) // compares albums by value { AlbumPtr album = albumMap.value( m_album ); // compares albums by value, too TrackList matchingTracks = album->tracks(); if ( isLast() ) return matchingTracks; else return next()->match( matchingTracks ); } else return TrackList(); } TrackList AlbumMatcher::match( const TrackList &tracks ) { if( !m_album ) return TrackList(); TrackList matchingTracks; QString name = m_album->name(); foreach( TrackPtr track, tracks ) if ( track->album()->name() == name ) matchingTracks.append( track ); if ( isLast() || matchingTracks.count() == 0) return matchingTracks; else return next()->match( matchingTracks ); } GenreMatcher::GenreMatcher( GenrePtr genre ) : MemoryMatcher() , m_genre( genre ) {} TrackList GenreMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_genre || !memColl ) return TrackList(); GenreMap genreMap = memColl->genreMap(); if ( genreMap.contains( m_genre->name() ) ) { GenrePtr genre = genreMap.value( m_genre->name() ); TrackList matchingTracks = genre->tracks(); if ( isLast() ) return matchingTracks; else return next()->match( matchingTracks ); } else return TrackList(); } TrackList GenreMatcher::match( const TrackList &tracks ) { if( !m_genre ) return TrackList(); TrackList matchingTracks; QString name = m_genre->name(); foreach( TrackPtr track, tracks ) if ( track->genre()->name() == name ) matchingTracks.append( track ); if ( isLast() || matchingTracks.count() == 0) return matchingTracks; else return next()->match( matchingTracks ); } ComposerMatcher::ComposerMatcher( ComposerPtr composer ) : MemoryMatcher() , m_composer( composer ) {} TrackList ComposerMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_composer || !memColl ) return TrackList(); ComposerMap composerMap = memColl->composerMap(); if ( composerMap.contains( m_composer->name() ) ) { ComposerPtr composer = composerMap.value( m_composer->name() ); TrackList matchingTracks = composer->tracks(); if ( isLast() ) return matchingTracks; else return next()->match( matchingTracks ); } else return TrackList(); } TrackList ComposerMatcher::match( const TrackList &tracks ) { if( !m_composer ) return TrackList(); TrackList matchingTracks; QString name = m_composer->name(); foreach( TrackPtr track, tracks ) if ( track->composer()->name() == name ) matchingTracks.append( track ); if ( isLast() || matchingTracks.count() == 0) return matchingTracks; else return next()->match( matchingTracks ); } YearMatcher::YearMatcher( YearPtr year ) : MemoryMatcher() , m_year( year ) {} TrackList YearMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_year || !memColl ) return TrackList(); YearMap yearMap = memColl->yearMap(); if ( yearMap.contains( m_year->year() ) ) { YearPtr year = yearMap.value( m_year->year() ); TrackList matchingTracks = year->tracks(); if ( isLast() ) return matchingTracks; else return next()->match( matchingTracks ); } else return TrackList(); } TrackList YearMatcher::match( const TrackList &tracks ) { if( !m_year ) return TrackList(); TrackList matchingTracks; int year = m_year->year(); foreach( TrackPtr track, tracks ) if ( track->year()->year() == year ) matchingTracks.append( track ); if ( isLast() || matchingTracks.count() == 0) return matchingTracks; else return next()->match( matchingTracks ); } LabelMatcher::LabelMatcher( const Meta::LabelPtr &label ) : MemoryMatcher() , m_label( label ) { //nothing to do } Meta::TrackList LabelMatcher::match( const Meta::TrackList &tracks ) { if( !m_label ) return Meta::TrackList(); Meta::TrackList matchingTracks; QString name = m_label->name(); //not really efficient... foreach( const Meta::TrackPtr &track, tracks ) { foreach( const Meta::LabelPtr &label, track->labels() ) { if( name == label->name() ) { matchingTracks << track; break; } } } if( isLast() || matchingTracks.count() == 0 ) return matchingTracks; else return next()->match( matchingTracks ); } Meta::TrackList LabelMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_label ) return Meta::TrackList(); Meta::TrackList matchingTracks; if( memColl->labelMap().contains( m_label->name() ) ) { //m_label might actually be a proxy label Meta::LabelPtr realLabel = memColl->labelMap().value( m_label->name() ); matchingTracks = memColl->labelToTrackMap().value( realLabel ); } if( isLast() || matchingTracks.count() == 0 ) return matchingTracks; else return next()->match( matchingTracks ); } diff --git a/src/playlist/proxymodels/GroupingProxy.cpp b/src/playlist/proxymodels/GroupingProxy.cpp index 8b7401060d..138e0d1864 100644 --- a/src/playlist/proxymodels/GroupingProxy.cpp +++ b/src/playlist/proxymodels/GroupingProxy.cpp @@ -1,368 +1,368 @@ /**************************************************************************************** * Copyright (c) 2007-2008 Ian Monroe * * Copyright (c) 2007 Nikolaj Hald Nielsen * * Copyright (c) 2008 Seb Ruiz * * Copyright (c) 2008 Soren Harward * * Copyright (c) 2009 Téo Mrnjavac * * Copyright (c) 2010 Nanno Langstraat * * * * This program 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 2 of the License, or (at your option) version 3 or * * any later version accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program 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 * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "Playlist::GroupingProxy" #include "GroupingProxy.h" #include "core/collections/Collection.h" #include "core/meta/Meta.h" #include "core/meta/Statistics.h" #include "core/meta/support/MetaUtility.h" #include "core/capabilities/SourceInfoCapability.h" #include "core/support/Debug.h" #include "playlist/PlaylistDefines.h" #include #include Playlist::GroupingProxy::GroupingProxy( Playlist::AbstractModel *belowModel, QObject *parent ) : ProxyBase( belowModel, parent ) { setGroupingCategory( QString( "Album" ) ); // Adjust our internal state based on changes in the source model. // We connect to our own QAbstractItemModel signals, which are emitted by our // 'QSortFilterProxyModel' parent class. // // Connect to 'this' instead of 'sourceModel()' for 2 reasons: // - We happen to be a 1:1 passthrough proxy, but if we filtered/sorted rows, // we'd want to maintain state for the rows exported by the proxy. The rows // exported by the source model are of no direct interest to us. // // - Qt guarantees that our signal handlers on 'this' will be called earlier than // any other, because we're the first to call 'connect( this )' (hey, we're the // constructor!). So, we're guaranteed to be able to update our internal state // before we get any 'data()' calls from "upstream" signal handlers. // // If we connected to 'sourceModel()', there would be no such guarantee: it // would be highly likely that an "upstream" signal handler (connected to the // 'this' QSFPM signal) would get called earlier, would call our 'data()' // function, and we would return wrong answers from our stale internal state. // connect( this, &GroupingProxy::dataChanged, this, &GroupingProxy::proxyDataChanged ); connect( this, &GroupingProxy::layoutChanged, this, &GroupingProxy::proxyLayoutChanged ); connect( this, &GroupingProxy::modelReset, this, &GroupingProxy::proxyModelReset ); connect( this, &GroupingProxy::rowsInserted, this, &GroupingProxy::proxyRowsInserted ); connect( this, &GroupingProxy::rowsRemoved, this, &GroupingProxy::proxyRowsRemoved ); // No need to scan the pre-existing entries in sourceModel(), because we build our // internal state on-the-fly. setObjectName( "GroupingProxy" ); } Playlist::GroupingProxy::~GroupingProxy() { } QString Playlist::GroupingProxy::groupingCategory() const { return m_groupingCategory; } void Playlist::GroupingProxy::setGroupingCategory( const QString &groupingCategory ) { m_groupingCategory = groupingCategory; m_groupingCategoryIndex = groupableCategories().indexOf( columnForName( m_groupingCategory ) ); // May be -1 invalidateGrouping(); // Notify our client(s) that we may now give different answers to 'data()' calls. // - Not 'layoutChanged': that is for when rows have been moved around, which they haven't. // - Not 'modelReset': that is too heavy. E.g. it also invalidates QListView item selections, etc. if ( rowCount() > 0 ) emit dataChanged( index( 0, 0 ), index( rowCount() - 1, columnCount() - 1 ) ); } bool Playlist::GroupingProxy::isFirstInGroup( const QModelIndex & index ) { Grouping::GroupMode mode = groupModeForIndex( index ); return ( (mode == Grouping::Head) || (mode == Grouping::None) ); } bool Playlist::GroupingProxy::isLastInGroup( const QModelIndex & index ) { Grouping::GroupMode mode = groupModeForIndex( index ); return ( (mode == Grouping::Tail) || (mode == Grouping::None) ); } QModelIndex Playlist::GroupingProxy::firstIndexInSameGroup( const QModelIndex & index ) { QModelIndex currIndex = index; while ( ! isFirstInGroup( currIndex ) ) currIndex = currIndex.sibling( currIndex.row() - 1, currIndex.column() ); return currIndex; } QModelIndex Playlist::GroupingProxy::lastIndexInSameGroup( const QModelIndex & index ) { QModelIndex currIndex = index; while ( ! isLastInGroup( currIndex ) ) currIndex = currIndex.sibling( currIndex.row() + 1, currIndex.column() ); return currIndex; } int Playlist::GroupingProxy::groupRowCount( const QModelIndex & index ) { return ( lastIndexInSameGroup( index ).row() - firstIndexInSameGroup( index ).row() ) + 1; } int Playlist::GroupingProxy::groupPlayLength( const QModelIndex & index ) { int totalLength = 0; QModelIndex currIndex = firstIndexInSameGroup( index ); forever { Meta::TrackPtr track = currIndex.data( TrackRole ).value(); if ( track ) totalLength += track->length(); else warning() << "Playlist::GroupingProxy::groupPlayLength(): TrackPtr is 0! row =" << currIndex.row() << ", rowCount =" << rowCount(); if ( isLastInGroup( currIndex ) ) break; currIndex = currIndex.sibling( currIndex.row() + 1, currIndex.column() ); } return totalLength; } QVariant Playlist::GroupingProxy::data( const QModelIndex& index, int role ) const { if( !index.isValid() ) return QVariant(); // Qt forces 'const' in our signature, but'groupModeForRow()' wants to do caching. GroupingProxy* nonconst_this = const_cast( this ); switch ( role ) { case Playlist::GroupRole: return nonconst_this->groupModeForIndex( index ); case Playlist::GroupedTracksRole: return nonconst_this->groupRowCount( index ); case Qt::DisplayRole: case Qt::ToolTipRole: switch( index.column() ) { case GroupLength: return Meta::msToPrettyTime( nonconst_this->groupPlayLength( index ) ); case GroupTracks: return i18np ( "1 track", "%1 tracks", nonconst_this->groupRowCount( index ) ); } - // Fall-through!! + Q_FALLTHROUGH(); default: // Nothing to do with us: let our QSortFilterProxyModel parent class handle it. // (which will proxy the data() from the underlying model) return QSortFilterProxyModel::data( index, role ); } } // Note: being clever in this function is sometimes wasted effort, because 'dataChanged' // can cause SortProxy to nuke us with a 'layoutChanged' signal very soon anyway. void Playlist::GroupingProxy::proxyDataChanged( const QModelIndex& proxyTopLeft, const QModelIndex& proxyBottomRight ) { // The preceding and succeeding rows may get a different GroupMode too, when our // GroupMode changes. int invalidateFirstRow = proxyTopLeft.row() - 1; // May be an invalid row number int invalidateLastRow = proxyBottomRight.row() + 1; // May be an invalid row number for (int row = invalidateFirstRow; row <= invalidateLastRow; row++) m_cachedGroupModeForRow.remove( row ); // Won't choke on non-existent rows. } void Playlist::GroupingProxy::proxyLayoutChanged() { invalidateGrouping(); // Crude but sufficient. } void Playlist::GroupingProxy::proxyModelReset() { invalidateGrouping(); // Crude but sufficient. } void Playlist::GroupingProxy::proxyRowsInserted( const QModelIndex& parent, int proxyStart, int proxyEnd ) { Q_UNUSED( parent ); Q_UNUSED( proxyStart ); Q_UNUSED( proxyEnd ); invalidateGrouping(); // Crude but sufficient. } void Playlist::GroupingProxy::proxyRowsRemoved( const QModelIndex& parent, int proxyStart, int proxyEnd ) { Q_UNUSED( parent ); Q_UNUSED( proxyStart ); Q_UNUSED( proxyEnd ); invalidateGrouping(); // Crude but sufficient. } Playlist::Grouping::GroupMode Playlist::GroupingProxy::groupModeForIndex( const QModelIndex & thisIndex ) { Grouping::GroupMode groupMode; groupMode = m_cachedGroupModeForRow.value( thisIndex.row(), Grouping::Invalid ); // Try to get from cache if ( groupMode == Grouping::Invalid ) { // Not in our cache QModelIndex prevIndex = thisIndex.sibling( thisIndex.row() - 1, thisIndex.column() ); // May be invalid, if 'thisIndex' is the first playlist item. QModelIndex nextIndex = thisIndex.sibling( thisIndex.row() + 1, thisIndex.column() ); // May be invalid, if 'thisIndex' is the last playlist item. Meta::TrackPtr prevTrack = prevIndex.data( TrackRole ).value(); // Invalid index is OK: Meta::TrackPtr thisTrack = thisIndex.data( TrackRole ).value(); // will just give an Meta::TrackPtr nextTrack = nextIndex.data( TrackRole ).value(); // invalid TrackPtr. bool matchBefore = shouldBeGrouped( prevTrack, thisTrack ); // Accepts invalid TrackPtrs. bool matchAfter = shouldBeGrouped( thisTrack, nextTrack ); // if ( !matchBefore && matchAfter ) groupMode = Grouping::Head; else if ( matchBefore && matchAfter ) groupMode = Grouping::Body; else if ( matchBefore && !matchAfter ) groupMode = Grouping::Tail; else groupMode = Grouping::None; m_cachedGroupModeForRow.insert( thisIndex.row(), groupMode ); // Cache our decision } return groupMode; } /** * The current implementation is a bit of a hack, but is what gives the best * user experience. * If a track has no data in the grouping category, it generally causes a non-match. */ bool Playlist::GroupingProxy::shouldBeGrouped( Meta::TrackPtr track1, Meta::TrackPtr track2 ) { // If the grouping category is empty or invalid, 'm_groupingCategoryIndex' will be -1. // That will cause us to choose "no grouping". if( !track1 || !track2 ) return false; // DEBUG_BLOCK // debug() << m_groupingCategoryIndex; switch( m_groupingCategoryIndex ) { case 0: //Album if( track1->album() && track2->album() ) { // don't group albums without name if( track1->album()->prettyName().isEmpty() || track2->album()->prettyName().isEmpty() ) return false; else return ( *track1->album().data() ) == ( *track2->album().data() ) && ( track1->discNumber() == track2->discNumber() ); } return false; case 1: //Artist if( track1->artist() && track2->artist() ) return ( *track1->artist().data() ) == ( *track2->artist().data() ); return false; case 2: //Composer if( track1->composer() && track2->composer() ) return ( *track1->composer().data() ) == ( *track2->composer().data() ); return false; case 3: //Directory return ( QFileInfo( track1->playableUrl().path() ).path() ) == ( QFileInfo( track2->playableUrl().path() ).path() ); case 4: //Genre if( track1->genre() && track2->genre() ) { debug() << "gruping by genre. Comparing " << track1->genre()->prettyName() << " with " << track2->genre()->prettyName(); debug() << track1->genre().data() << " == " << track2->genre().data() << " : " << ( *track1->genre().data() == *track2->genre().data()); return ( *track1->genre().data() ) == ( *track2->genre().data() ); } return false; case 5: //Rating if( track1->statistics()->rating() && track2->statistics()->rating() ) return ( track1->statistics()->rating() ) == ( track2->statistics()->rating() ); return false; case 6: //Source { QString source1, source2; Capabilities::SourceInfoCapability *sic1 = track1->create< Capabilities::SourceInfoCapability >(); Capabilities::SourceInfoCapability *sic2 = track2->create< Capabilities::SourceInfoCapability >(); if( sic1 && sic2) { source1 = sic1->sourceName(); source2 = sic2->sourceName(); } delete sic1; delete sic2; if( sic1 && sic2 ) return source1 == source2; // fall back to collection return track1->collection() == track2->collection(); } case 7: //Year if( track1->year() && track2->year() ) return ( *track1->year().data() ) == ( *track2->year().data() ); return false; default: return false; } } void Playlist::GroupingProxy::invalidateGrouping() { m_cachedGroupModeForRow.clear(); } diff --git a/src/playlist/proxymodels/SortAlgorithms.cpp b/src/playlist/proxymodels/SortAlgorithms.cpp index 43e6884282..d8b8bb2c9b 100644 --- a/src/playlist/proxymodels/SortAlgorithms.cpp +++ b/src/playlist/proxymodels/SortAlgorithms.cpp @@ -1,175 +1,175 @@ /**************************************************************************************** * Copyright (c) 2009 Téo Mrnjavac * * Copyright (c) 2010 Nanno Langstraat * * Copyright (c) 2013 Konrad Zemek * * * * This program 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 2 of the License, or (at your option) any later * * version. * * * * This program 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 * * this program. If not, see . * ****************************************************************************************/ #include "SortAlgorithms.h" #include "core/meta/Meta.h" #include "core/meta/Statistics.h" #include "core/support/Debug.h" #include "playlist/proxymodels/AbstractModel.h" #include namespace Playlist { void multilevelLessThan::setSortScheme( const SortScheme & scheme ) { m_scheme = scheme; m_randomSalt = qrand(); //! Do a different random sort order every time. } bool multilevelLessThan::operator()( const QAbstractItemModel* sourceModel, int sourceModelRowA, int sourceModelRowB ) const { // Handle "Last Played" as a special case because the time since last played is not // reported as an int in the data columns. Handle Title, Album, Artist as special // cases with Meta::Base::sortableName(). This is necessary in order to have the same // sort order policy regarding "The" in both the playlist and the collection browser. QSet< Playlist::Column > specialCases; specialCases << Playlist::LastPlayed << Playlist::Title << Playlist::Album << Playlist::Artist << Playlist::AlbumArtist; foreach( const SortLevel &level, m_scheme ) { const bool inverted = ( level.order() == Qt::DescendingOrder ); const Playlist::Column currentCategory = level.category(); const QModelIndex indexA = sourceModel->index( sourceModelRowA, currentCategory ); const QModelIndex indexB = sourceModel->index( sourceModelRowB, currentCategory ); const Meta::TrackPtr trackA = indexA.data( TrackRole ).value(); const Meta::TrackPtr trackB = indexB.data( TrackRole ).value(); if( trackA && trackB && specialCases.contains( currentCategory ) ) { switch( currentCategory ) { case Playlist::LastPlayed: { const QDateTime lastPlayedA = trackA->statistics()->lastPlayed(); const QDateTime lastPlayedB = trackB->statistics()->lastPlayed(); // The track with higher lastPlayed value was played more recently // // '!=' is the XOR operation; it simply negates the result if 'inverted' // is true. It isn't necessarry to do it this way, although later on it will // ease figuring out what's actually being returned. if( lastPlayedA != lastPlayedB ) return ( lastPlayedA > lastPlayedB ) != inverted; break; } case Playlist::Title: { const int compareResult = compareBySortableName( trackA, trackB ); if( compareResult != 0 ) return ( compareResult < 0 ) != inverted; break; } case Playlist::Album: { const int compareResult = compareBySortableName( trackA->album(), trackB->album() ); if( compareResult != 0 ) return ( compareResult < 0 ) != inverted; // Fall through to sorting by album artist if albums have same name - __attribute__ ((fallthrough)); + Q_FALLTHROUGH(); } case Playlist::AlbumArtist: { const Meta::ArtistPtr artistA = (trackA->album() ? trackA->album()->albumArtist() : Meta::ArtistPtr()); const Meta::ArtistPtr artistB = (trackB->album() ? trackB->album()->albumArtist() : Meta::ArtistPtr()); const int compareResult = compareBySortableName( artistA, artistB ); if( compareResult != 0 ) return ( compareResult < 0 ) != inverted; break; } case Playlist::Artist: { const int compareResult = compareBySortableName( trackA->artist(), trackB->artist() ); if( compareResult != 0 ) return ( compareResult < 0 ) != inverted; break; } default: warning() << "One of the cases in specialCases set has not received special treatment!"; break; } } else // either it's not a special case, or we don't have means (TrackPtrs) to handle it { const QVariant dataA = indexA.data( Qt::DisplayRole ); const QVariant dataB = indexB.data( Qt::DisplayRole ); if( level.isString() ) { const int compareResult = dataA.toString().compare(dataB.toString(), Qt::CaseInsensitive); if( compareResult != 0 ) return ( compareResult < 0 ) != inverted; } else if( level.isFloat() ) { if( dataA.toDouble() != dataB.toDouble() ) return ( dataA.toDouble() < dataB.toDouble() ) != inverted; } else // if it's neither a string nor a float ==> it's an integer { if( dataA.toInt() != dataB.toInt() ) return ( dataA.toInt() < dataB.toInt() ) != inverted; } } } // Tie breaker: order by row number return ( sourceModelRowA < sourceModelRowB ); } template int multilevelLessThan::compareBySortableName( const AmarokSharedPointer &left, const AmarokSharedPointer &right ) const { if( !left && right ) return -1; else if( left && !right ) return 1; else if( left && right ) return left->sortableName().compare( right->sortableName(), Qt::CaseInsensitive ); return 0; } } //namespace Playlist diff --git a/src/services/lastfm/LastFmServiceConfig.cpp b/src/services/lastfm/LastFmServiceConfig.cpp index 5e371de8d2..cc2e805ca6 100644 --- a/src/services/lastfm/LastFmServiceConfig.cpp +++ b/src/services/lastfm/LastFmServiceConfig.cpp @@ -1,288 +1,288 @@ /**************************************************************************************** * Copyright (c) 2007 Shane King * * Copyright (c) 2009 Leo Franchi * * * * This program 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 2 of the License, or (at your option) any later * * version. * * * * This program 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 * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "lastfm" #include "LastFmServiceConfig.h" #include "App.h" #include "core/interfaces/Logger.h" #include "core/support/Amarok.h" #include "core/support/Components.h" #include "core/support/Debug.h" #include #include #include #include #include QWeakPointer LastFmServiceConfig::s_instance; LastFmServiceConfigPtr LastFmServiceConfig::instance() { Q_ASSERT( QThread::currentThread() == QCoreApplication::instance()->thread() ); LastFmServiceConfigPtr strongRef = s_instance.toStrongRef(); if( strongRef ) return strongRef; LastFmServiceConfigPtr newStrongRef( new LastFmServiceConfig() ); s_instance = newStrongRef; return newStrongRef; } LastFmServiceConfig::LastFmServiceConfig() : m_askDiag( 0 ) , m_wallet( 0 ) { DEBUG_BLOCK KConfigGroup config = Amarok::config( configSectionName() ); m_sessionKey = config.readEntry( "sessionKey", QString() ); m_scrobble = config.readEntry( "scrobble", defaultScrobble() ); m_fetchSimilar = config.readEntry( "fetchSimilar", defaultFetchSimilar() ); m_scrobbleComposer = config.readEntry( "scrobbleComposer", defaultScrobbleComposer() ); m_useFancyRatingTags = config.readEntry( "useFancyRatingTags", defaultUseFancyRatingTags() ); m_announceCorrections = config.readEntry( "announceCorrections", defaultAnnounceCorrections() ); m_filterByLabel = config.readEntry( "filterByLabel", defaultFilterByLabel() ); m_filteredLabel = config.readEntry( "filteredLabel", defaultFilteredLabel() ); if( config.hasKey( "kWalletUsage" ) ) m_kWalletUsage = KWalletUsage( config.readEntry( "kWalletUsage", int( NoPasswordEnteredYet ) ) ); else { // migrate from the old config that used "ignoreWallet" key set to yes/no if( config.readEntry( "ignoreWallet", "" ) == "yes" ) m_kWalletUsage = PasswordInAscii; else if( config.hasKey( "scrobble" ) ) // assume password was saved in KWallet if the config was once written m_kWalletUsage = PasswodInKWallet; else m_kWalletUsage = NoPasswordEnteredYet; // config not yet saved, assume unused } switch( m_kWalletUsage ) { case NoPasswordEnteredYet: break; case PasswodInKWallet: openWalletToRead(); break; case PasswordInAscii: m_username = config.readEntry( "username", QString() ); m_password = config.readEntry( "password", QString() ); break; } } LastFmServiceConfig::~LastFmServiceConfig() { DEBUG_BLOCK if( m_askDiag ) m_askDiag->deleteLater(); if( m_wallet ) m_wallet->deleteLater(); } void LastFmServiceConfig::save() { KConfigGroup config = Amarok::config( configSectionName() ); // if username and password is empty, reset to NoPasswordEnteredYet; this enables // going from PasswordInAscii to PasswodInKWallet if( m_username.isEmpty() && m_password.isEmpty() ) { m_kWalletUsage = NoPasswordEnteredYet; config.deleteEntry( "username" ); // prevent possible stray credentials config.deleteEntry( "password" ); } config.writeEntry( "sessionKey", m_sessionKey ); config.writeEntry( "scrobble", m_scrobble ); config.writeEntry( "fetchSimilar", m_fetchSimilar ); config.writeEntry( "scrobbleComposer", m_scrobbleComposer ); config.writeEntry( "useFancyRatingTags", m_useFancyRatingTags ); config.writeEntry( "announceCorrections", m_announceCorrections ); config.writeEntry( "kWalletUsage", int( m_kWalletUsage ) ); config.writeEntry( "filterByLabel", m_filterByLabel ); config.writeEntry( "filteredLabel", m_filteredLabel ); config.deleteEntry( "ignoreWallet" ); // remove old settings switch( m_kWalletUsage ) { case NoPasswordEnteredYet: if( m_username.isEmpty() && m_password.isEmpty() ) break; // stay in this state - /* Falls through. */ + Q_FALLTHROUGH(); case PasswodInKWallet: openWalletToWrite(); config.deleteEntry( "username" ); // prevent possible stray credentials config.deleteEntry( "password" ); break; case PasswordInAscii: config.writeEntry( "username", m_username ); config.writeEntry( "password", m_password ); break; } config.sync(); emit updated(); } void LastFmServiceConfig::openWalletToRead() { if( m_wallet && m_wallet->isOpen() ) { slotWalletOpenedToRead( true ); return; } if( m_wallet ) disconnect( m_wallet, 0, this, 0 ); else { openWalletAsync(); if( !m_wallet ) // can happen, see bug 322964 { slotWalletOpenedToRead( false ); return; } } connect( m_wallet, &KWallet::Wallet::walletOpened, this, &LastFmServiceConfig::slotWalletOpenedToRead ); } void LastFmServiceConfig::openWalletToWrite() { if( m_wallet && m_wallet->isOpen() ) { slotWalletOpenedToWrite( true ); return; } if( m_wallet ) disconnect( m_wallet, 0, this, 0 ); else { openWalletAsync(); if( !m_wallet ) // can happen, see bug 322964 { slotWalletOpenedToWrite( false ); return; } } connect( m_wallet, &KWallet::Wallet::walletOpened, this, &LastFmServiceConfig::slotWalletOpenedToWrite ); } void LastFmServiceConfig::openWalletAsync() { Q_ASSERT( !m_wallet ); using namespace KWallet; m_wallet = Wallet::openWallet( Wallet::NetworkWallet(), 0, Wallet::Asynchronous ); } void LastFmServiceConfig::prepareOpenedWallet() { if( !m_wallet->hasFolder( "Amarok" ) ) m_wallet->createFolder( "Amarok" ); m_wallet->setFolder( "Amarok" ); } void LastFmServiceConfig::slotWalletOpenedToRead( bool success ) { if( !success ) { warning() << __PRETTY_FUNCTION__ << "failed to open wallet"; QString message = i18n( "Failed to open KDE Wallet to read Last.fm credentials" ); Amarok::Components::logger()->longMessage( message, Amarok::Logger::Warning ); if( m_wallet ) m_wallet->deleteLater(); // no point in having invalid wallet around m_wallet = 0; return; } Q_ASSERT( m_wallet ); prepareOpenedWallet(); if( m_wallet->readPassword( "lastfm_password", m_password ) > 0 ) warning() << "Failed to read lastfm password from kwallet"; QByteArray rawUsername; if( m_wallet->readEntry( "lastfm_username", rawUsername ) > 0 ) warning() << "Failed to read last.fm username from kwallet"; else m_username = QString::fromUtf8( rawUsername ); emit updated(); } void LastFmServiceConfig::slotWalletOpenedToWrite( bool success ) { if( !success ) { askAboutMissingKWallet(); if( m_wallet ) m_wallet->deleteLater(); // no point in having invalid wallet around m_wallet = 0; return; } Q_ASSERT( m_wallet ); prepareOpenedWallet(); if( m_wallet->writePassword( "lastfm_password", m_password ) > 0 ) warning() << "Failed to save last.fm password to kwallet"; if( m_wallet->writeEntry( "lastfm_username", m_username.toUtf8() ) > 0 ) warning() << "Failed to save last.fm username to kwallet"; m_kWalletUsage = PasswodInKWallet; KConfigGroup config = Amarok::config( configSectionName() ); config.writeEntry( "kWalletUsage", int( m_kWalletUsage ) ); config.sync(); } void LastFmServiceConfig::askAboutMissingKWallet() { if ( !m_askDiag ) { m_askDiag = new QMessageBox; m_askDiag->setText( i18n( "No running KWallet found." ) ); m_askDiag->setInformativeText( i18n( "Would you like Amarok to save your Last.fm credentials in plaintext?" ) ); m_askDiag->setStandardButtons( QMessageBox::Yes | QMessageBox::No ); connect( m_askDiag, &QDialog::accepted, this, &LastFmServiceConfig::slotStoreCredentialsInAscii ); // maybe connect SIGNAL(noClicked()) to a message informing the user the password will // be forgotten on Amarok restart } m_askDiag->show(); } void LastFmServiceConfig::slotStoreCredentialsInAscii() //SLOT { DEBUG_BLOCK m_kWalletUsage = PasswordInAscii; save(); } diff --git a/src/services/lastfm/LastFmTreeModel.cpp b/src/services/lastfm/LastFmTreeModel.cpp index 96fdd0e607..8c0d803b93 100644 --- a/src/services/lastfm/LastFmTreeModel.cpp +++ b/src/services/lastfm/LastFmTreeModel.cpp @@ -1,713 +1,715 @@ /**************************************************************************************** * Copyright (c) 2008 Casey Link * * Copyright (c) 2009 Nikolaj Hald Nielsen * * Copyright (c) 2009 Mark Kretschmann * * * * This program 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 2 of the License, or (at your option) any later * * version. * * * * This program 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 * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "LastFmTreeModel" #include "core/support/Debug.h" #include "LastFmTreeModel.h" #include "AvatarDownloader.h" #include "core-impl/collections/support/CollectionManager.h" #include "AmarokMimeData.h" #include #include #include #include using namespace LastFm; LastFmTreeModel::LastFmTreeModel( QObject *parent ) : QAbstractItemModel( parent ) { m_rootItem = new LastFmTreeItem( LastFm::Root, "Hello" ); setupModelData( m_rootItem ); QNetworkReply *reply; reply = m_user.getNeighbours(); connect(reply, &QNetworkReply::finished, this, &LastFmTreeModel::slotAddNeighbors ); reply = m_user.getFriends(); connect( reply, &QNetworkReply::finished, this, &LastFmTreeModel::slotAddFriends ); reply = m_user.getTopTags(); connect( reply, &QNetworkReply::finished, this, &LastFmTreeModel::slotAddTags ); reply = m_user.getTopArtists(); connect( reply, &QNetworkReply::finished, this, &LastFmTreeModel::slotAddTopArtists ); } LastFmTreeModel::~LastFmTreeModel() { delete m_rootItem; } void LastFmTreeModel::slotAddNeighbors() { QNetworkReply *reply = qobject_cast( sender() ); if( !reply ) { debug() << __PRETTY_FUNCTION__ << "null reply!"; return; } reply->deleteLater(); lastfm::XmlQuery lfm; if( lfm.parse( reply->readAll() ) ) { QList children = lfm[ "neighbours" ].children( "user" ); int start = m_myNeighbors->childCount(); QModelIndex parent = index( m_myNeighbors->row(), 0 ); beginInsertRows( parent, start, start + children.size() ); foreach( const lastfm::XmlQuery &e, children ) { const QString name = e[ "name" ].text(); LastFmTreeItem* neighbor = new LastFmTreeItem( mapTypeToUrl(LastFm::NeighborsChild, name), LastFm::NeighborsChild, name, m_myNeighbors ); QUrl avatarUrl( e[ QLatin1String("image size=small") ].text() ); if( !avatarUrl.isEmpty() ) neighbor->setAvatarUrl( avatarUrl ); m_myNeighbors->appendChild( neighbor ); appendUserStations( neighbor, name ); } endInsertRows(); emit dataChanged( parent, parent ); } else { debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message(); return; } } void LastFmTreeModel::slotAddFriends() { QNetworkReply *reply = qobject_cast( sender() ); if( !reply ) { debug() << __PRETTY_FUNCTION__ << "null reply!"; return; } reply->deleteLater(); lastfm::XmlQuery lfm; if( lfm.parse( reply->readAll() ) ) { QList children = lfm[ "friends" ].children( "user" ); int start = m_myFriends->childCount(); QModelIndex parent = index( m_myFriends->row(), 0 ); beginInsertRows( parent, start, start + children.size() ); foreach( const lastfm::XmlQuery &e, children ) { const QString name = e[ "name" ].text(); LastFmTreeItem* afriend = new LastFmTreeItem( mapTypeToUrl(LastFm::FriendsChild, name), LastFm::FriendsChild, name, m_myFriends ); QUrl avatarUrl( e[ QLatin1String("image size=small") ].text() ); if( !avatarUrl.isEmpty() ) afriend->setAvatarUrl( avatarUrl ); m_myFriends->appendChild( afriend ); appendUserStations( afriend, name ); } endInsertRows(); emit dataChanged( parent, parent ); } else { debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message(); return; } } void LastFmTreeModel::slotAddTags() { QNetworkReply *reply = qobject_cast( sender() ); if( !reply ) { debug() << __PRETTY_FUNCTION__ << "null reply!"; return; } reply->deleteLater(); QMap listWithWeights = lastfm::Tag::list( reply ); int start = m_myTags->childCount(); QModelIndex parent = index( m_myTags->row(), 0 ); beginInsertRows( parent, start, start + listWithWeights.size() ); QMapIterator it( listWithWeights ); it.toBack(); while( it.hasPrevious() ) { it.previous(); int count = it.key(); QString text = it.value(); QString prettyText = i18nc( "%1 is Last.fm tag name, %2 is its usage count", "%1 (%2)", text, count ); LastFmTreeItem *tag = new LastFmTreeItem( mapTypeToUrl( LastFm::MyTagsChild, text ), LastFm::MyTagsChild, prettyText, m_myTags ); m_myTags->appendChild( tag ); } endInsertRows(); emit dataChanged( parent, parent ); } void LastFmTreeModel::slotAddTopArtists() { QNetworkReply *reply = qobject_cast( sender() ); if( !reply ) { debug() << __PRETTY_FUNCTION__ << "null reply!"; return; } reply->deleteLater(); QMultiMap playcountArtists; lastfm::XmlQuery lfm; if( lfm.parse( reply->readAll() ) ) { foreach( const lastfm::XmlQuery &e, lfm[ "topartists" ].children( "artist" ) ) { QString name = e[ "name" ].text(); int playcount = e[ "playcount" ].text().toInt(); playcountArtists.insert( playcount, name ); } } else { debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message(); return; } int start = m_myTopArtists->childCount(); QModelIndex parent = index( m_myTopArtists->row(), 0 ); beginInsertRows( parent, start, start + playcountArtists.size() ); QMapIterator it( playcountArtists ); it.toBack(); while( it.hasPrevious() ) { it.previous(); int count = it.key(); QString text = it.value(); QString prettyText = i18ncp( "%2 is artist name, %1 is number of plays", "%2 (%1 play)", "%2 (%1 plays)", count, text ); LastFmTreeItem *artist = new LastFmTreeItem( mapTypeToUrl( LastFm::ArtistsChild, text ), LastFm::ArtistsChild, prettyText, m_myTopArtists ); m_myTopArtists->appendChild( artist ); } endInsertRows(); emit dataChanged( parent, parent ); } void LastFmTreeModel::appendUserStations( LastFmTreeItem* item, const QString &user ) { // no need to call begin/endInsertRows() or dataChanged(), we're being called inside // beginInsertRows(). LastFmTreeItem* personal = new LastFmTreeItem( mapTypeToUrl( LastFm::UserChildPersonal, user ), LastFm::UserChildPersonal, i18n( "Personal Radio" ), item ); LastFmTreeItem* neigh = new LastFmTreeItem( mapTypeToUrl( LastFm::UserChildNeighborhood, user ), LastFm::UserChildNeighborhood, i18n( "Neighborhood" ), item ); item->appendChild( personal ); item->appendChild( neigh ); } void LastFmTreeModel::prepareAvatar( QPixmap &avatar, int size ) { // This code is here to stop Qt from crashing on certain weirdly shaped avatars. // We had a case were an avatar got a height of 1px after scaling and it would // crash in the rendering code. This here just fills in the background with // transparency first. if( avatar.width() < size || avatar.height() < size ) { QImage finalAvatar( size, size, QImage::Format_ARGB32 ); finalAvatar.fill( 0 ); QPainter p( &finalAvatar ); QRect r; if( avatar.width() < size ) r = QRect( ( size - avatar.width() ) / 2, 0, avatar.width(), avatar.height() ); else r = QRect( 0, ( size - avatar.height() ) / 2, avatar.width(), avatar.height() ); p.drawPixmap( r, avatar ); p.end(); avatar = QPixmap::fromImage( finalAvatar ); } } void LastFmTreeModel::onAvatarDownloaded( const QString &username, QPixmap avatar ) { sender()->deleteLater(); if( avatar.isNull() || avatar.height() <= 0 || avatar.width() <= 0 ) return; if( username == m_user.name() ) return; int m = avatarSize(); avatar = avatar.scaled( m, m, Qt::KeepAspectRatio, Qt::SmoothTransformation ); prepareAvatar( avatar, m ); m_avatars.insert( username, avatar ); // these 2 categories have a chance to be updated: QList categories; categories << m_myFriends << m_myNeighbors; // now go through all children of the categories and notify view as appropriate foreach( LastFmTreeItem *category, categories ) { QModelIndex parentIdx = index( category->row(), 0 ); for( int i = 0; i < category->childCount(); i++ ) { LastFmTreeItem *item = category->child( i ); if( !item ) continue; if( item->data() == username ) { QModelIndex idx = index( i, 0, parentIdx ); emit dataChanged( idx, idx ); break; // no user is twice in a single category } } } } QIcon LastFmTreeModel::avatar( const QString &username, const QUrl &avatarUrl ) const { QIcon defaultIcon( "filename-artist-amarok" ); if( username.isEmpty() ) return defaultIcon; if( m_avatars.contains(username) ) return m_avatars.value( username ); if( !avatarUrl.isValid() ) return defaultIcon; // insert placeholder so that we don't request the save avatar twice; const_cast( this )->m_avatars.insert( username, defaultIcon ); AvatarDownloader* downloader = new AvatarDownloader(); downloader->downloadAvatar( username, avatarUrl ); connect( downloader, &AvatarDownloader::avatarDownloaded, this, &LastFmTreeModel::onAvatarDownloaded ); return defaultIcon; } int LastFmTreeModel::columnCount( const QModelIndex &parent ) const { Q_UNUSED( parent ) return 1; } int LastFmTreeModel::avatarSize() { return 32; } QVariant LastFmTreeModel::data( const QModelIndex &index, int role ) const { if( !index.isValid() ) return QVariant(); LastFmTreeItem *i = static_cast( index.internalPointer() ); if( role == Qt::DisplayRole ) switch( i->type() ) { case MyRecommendations: return i18n( "My Recommendations" ); case PersonalRadio: return i18n( "My Radio Station" ); case MixRadio: return i18n( "My Mix Radio" ); case NeighborhoodRadio: return i18n( "My Neighborhood" ); // case RecentlyPlayed: return tr("Recently Played"); // case RecentlyLoved: return tr("Recently Loved"); // case RecentlyBanned: return tr("Recently Banned"); case TopArtists: return i18n( "My Top Artists" ); case MyTags: return i18n( "My Tags" ); case Friends: return i18n( "Friends" ); case Neighbors: return i18n( "Neighbors" ); // case History: return tr("History"); // case RecentlyPlayedTrack: return m_played.value( index.row() ); // case RecentlyLovedTrack: return m_loved.value( index.row() ); // case RecentlyBannedTrack: return m_banned.value( index.row() ); // case MyTagsChild: return m_tags.value( index.row() ); case FriendsChild: case ArtistsChild: case NeighborsChild: case UserChildPersonal: case UserChildNeighborhood: case MyTagsChild: return i->data(); default: break; } if( role == Qt::DecorationRole ) { switch( i->type() ) { case MyRecommendations: return QIcon::fromTheme( "lastfm-recommended-radio-amarok" ); case TopArtists: case PersonalRadio: return QIcon::fromTheme( "lastfm-personal-radio-amarok" ); case MixRadio: return QIcon::fromTheme( "lastfm-mix-radio-amarok" ); case NeighborhoodRadio: return QIcon::fromTheme( "lastfm-neighbour-radio-amarok" ); // case RecentlyPlayed: return QIcon::fromTheme( "lastfm-recent-tracks-amarok" ); // case RecentlyLoved: return QIcon::fromTheme( "lastfm-recently-loved-amarok" ); // case RecentlyBanned: return QIcon::fromTheme( "lastfm-recently-banned-amarok" ); case MyTags: return QIcon::fromTheme( "lastfm-my-tags-amarok" ); case Friends: return QIcon::fromTheme( "lastfm-my-friends-amarok" ); case Neighbors: return QIcon::fromTheme( "lastfm-my-neighbours-amarok" ); - case RecentlyPlayedTrack: //FALL THROUGH - case RecentlyLovedTrack: //FALL THROUGH + case RecentlyPlayedTrack: + Q_FALLTHROUGH(); + case RecentlyLovedTrack: + Q_FALLTHROUGH(); case RecentlyBannedTrack: return QIcon::fromTheme( "icon_track" ); case MyTagsChild: return QIcon::fromTheme( "lastfm-tag-amarok" ); case FriendsChild: return avatar( i->data().toString(), i->avatarUrl() ); case UserChildPersonal: return QIcon::fromTheme( "lastfm-personal-radio-amarok" ); case UserChildNeighborhood: return QIcon::fromTheme( "lastfm-neighbour-radio-amarok" ); case NeighborsChild: return avatar( i->data().toString(), i->avatarUrl() ); case HistoryStation: return QIcon::fromTheme( "icon_radio" ); default: break; } } if( role == LastFm::TrackRole ) { switch( i->type() ) { case LastFm::MyRecommendations: case LastFm::PersonalRadio: case LastFm::MixRadio: case LastFm::NeighborhoodRadio: case LastFm::FriendsChild: case LastFm::NeighborsChild: case LastFm::MyTagsChild: case LastFm::ArtistsChild: case LastFm::UserChildPersonal: case LastFm::UserChildNeighborhood: return QVariant::fromValue( i->track() ); default: break; } } if( role == LastFm::TypeRole ) return i->type(); return QVariant(); } Qt::ItemFlags LastFmTreeModel::flags( const QModelIndex &index ) const { if( !index.isValid() ) return 0; Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsDropEnabled; LastFmTreeItem *i = static_cast( index.internalPointer() ); switch( i->type() ) { case MyRecommendations: case PersonalRadio: case MixRadio: case NeighborhoodRadio: case RecentlyPlayedTrack: case RecentlyLovedTrack: case RecentlyBannedTrack: case MyTagsChild: case FriendsChild: case ArtistsChild: case NeighborsChild: case HistoryStation: case UserChildPersonal: case UserChildNeighborhood: flags |= Qt::ItemIsSelectable; break; default: break; } switch( i->type() ) { case UserChildPersonal: case UserChildNeighborhood: case MyTagsChild: case ArtistsChild: case MyRecommendations: case PersonalRadio: case MixRadio: case NeighborhoodRadio: flags |= Qt::ItemIsDragEnabled; default: break; } return flags; } QModelIndex LastFmTreeModel::index( int row, int column, const QModelIndex &parent ) const { if( !hasIndex( row, column, parent ) ) return QModelIndex(); LastFmTreeItem *parentItem; if( !parent.isValid() ) parentItem = m_rootItem; else parentItem = static_cast( parent.internalPointer() ); LastFmTreeItem *childItem = parentItem->child( row ); if( childItem ) return createIndex( row, column, childItem ); else return QModelIndex(); } QModelIndex LastFmTreeModel::parent( const QModelIndex &index ) const { if( !index.isValid() ) return QModelIndex(); LastFmTreeItem *childItem = static_cast( index.internalPointer() ); LastFmTreeItem *parentItem = childItem->parent(); if( parentItem == m_rootItem ) return QModelIndex(); return createIndex( parentItem->row(), 0, parentItem ); } int LastFmTreeModel::rowCount( const QModelIndex &parent ) const { LastFmTreeItem *parentItem; if( parent.column() > 0 ) return 0; if( !parent.isValid() ) parentItem = m_rootItem; else parentItem = static_cast( parent.internalPointer() ); return parentItem->childCount(); } void LastFmTreeModel::setupModelData( LastFmTreeItem *parent ) { // no need to call beginInsertRows() here, this is only called from constructor parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::MyRecommendations ), LastFm::MyRecommendations, parent ) ); parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::PersonalRadio ), LastFm::PersonalRadio, parent ) ); parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::MixRadio ), LastFm::MixRadio, parent ) ); parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::NeighborhoodRadio ), LastFm::NeighborhoodRadio, parent ) ); m_myTopArtists = new LastFmTreeItem( LastFm::TopArtists, parent ); parent->appendChild( m_myTopArtists ); m_myTags = new LastFmTreeItem( LastFm::MyTags, parent ); parent->appendChild( m_myTags ); m_myFriends = new LastFmTreeItem( LastFm::Friends, parent ); parent->appendChild( m_myFriends ); m_myNeighbors = new LastFmTreeItem( LastFm::Neighbors, parent ); parent->appendChild( m_myNeighbors ); } QString LastFmTreeModel::mapTypeToUrl( LastFm::Type type, const QString &key ) { QString const encoded_username = QUrl::toPercentEncoding( m_user.name() ); switch( type ) { case MyRecommendations: return "lastfm://user/" + encoded_username + "/recommended"; case PersonalRadio: return "lastfm://user/" + encoded_username + "/personal"; case MixRadio: return "lastfm://user/" + encoded_username + "/mix"; case NeighborhoodRadio: return "lastfm://user/" + encoded_username + "/neighbours"; case MyTagsChild: return "lastfm://usertags/" + encoded_username + "/" + QUrl::toPercentEncoding( key ); case FriendsChild: return "lastfm://user/" + QUrl::toPercentEncoding( key ) + "/personal"; case ArtistsChild: return "lastfm://artist/" + QUrl::toPercentEncoding( key ) + "/similarartists"; case NeighborsChild: return "lastfm://user/" + QUrl::toPercentEncoding( key ) + "/personal"; case UserChildPersonal: return "lastfm://user/" + QUrl::toPercentEncoding( key ) + "/personal"; case UserChildNeighborhood: return "lastfm://user/" + QUrl::toPercentEncoding( key ) + "/neighbours"; default: return ""; } } LastFmTreeItem::LastFmTreeItem( const LastFm::Type &type, const QVariant &data, LastFmTreeItem *parent ) : mType( type ), parentItem( parent ), itemData( data ) { } LastFmTreeItem::LastFmTreeItem( const LastFm::Type &type, LastFmTreeItem *parent ) : mType( type ), parentItem( parent ) { } LastFmTreeItem::LastFmTreeItem( const QString &url, const LastFm::Type &type, LastFmTreeItem *parent ) : mType( type ), parentItem( parent ), mUrl( url ) { } LastFmTreeItem::LastFmTreeItem( const QString &url, const LastFm::Type &type, const QVariant &data, LastFmTreeItem *parent ) : mType( type ), parentItem( parent ), itemData( data ), mUrl( url ) { } LastFmTreeItem::~LastFmTreeItem() { qDeleteAll( childItems ); } void LastFmTreeItem::appendChild( LastFmTreeItem *item ) { childItems.append( item ); } LastFmTreeItem * LastFmTreeItem::child( int row ) { return childItems.value( row ); } int LastFmTreeItem::childCount() const { return childItems.count(); } QVariant LastFmTreeItem::data() const { return itemData; } Meta::TrackPtr LastFmTreeItem::track() const { Meta::TrackPtr track; if( mUrl.isEmpty() ) return track; QUrl url( mUrl ); track = CollectionManager::instance()->trackForUrl( url ); return track; } LastFmTreeItem *LastFmTreeItem::parent() { return parentItem; } int LastFmTreeItem::row() const { if( parentItem ) return parentItem->childItems.indexOf( const_cast( this ) ); return 0; } QMimeData* LastFmTreeModel::mimeData( const QModelIndexList &indices ) const { debug() << "LASTFM drag items : " << indices.size(); Meta::TrackList list; foreach( const QModelIndex &item, indices ) { Meta::TrackPtr track = data( item, LastFm::TrackRole ).value< Meta::TrackPtr >(); if( track ) list << track; } qStableSort( list.begin(), list.end(), Meta::Track::lessThan ); AmarokMimeData *mimeData = new AmarokMimeData(); mimeData->setTracks( list ); return mimeData; } diff --git a/src/widgets/Osd.cpp b/src/widgets/Osd.cpp index 053053a7d6..3aabec8ddf 100644 --- a/src/widgets/Osd.cpp +++ b/src/widgets/Osd.cpp @@ -1,866 +1,866 @@ /**************************************************************************************** * Copyright (c) 2004 Christian Muehlhaeuser * * Copyright (c) 2004-2006 Seb Ruiz * * Copyright (c) 2004,2005 Max Howell * * Copyright (c) 2005 Gabor Lehel * * Copyright (c) 2008-2013 Mark Kretschmann * * * * This program 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 2 of the License, or (at your option) any later * * version. * * * * This program 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 * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "OSD" #include "Osd.h" #include "EngineController.h" #include "KNotificationBackend.h" #include "PaletteHandler.h" #include "SvgHandler.h" #include "amarokconfig.h" #include "core/meta/Meta.h" #include "core/meta/Statistics.h" #include "core/meta/support/MetaUtility.h" #include "core/support/Amarok.h" #include "core/support/Debug.h" #include "widgets/StarManager.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace ShadowEngine { QImage makeShadow( const QPixmap &textPixmap, const QColor &bgColor ); } namespace Amarok { inline QImage icon() { return QImage( KIconLoader::global()->iconPath( "amarok", -KIconLoader::SizeHuge ) ); } } OSDWidget::OSDWidget( QWidget *parent, const char *name ) : QWidget( parent ) , m_duration( 2000 ) , m_timer( new QTimer( this ) ) , m_alignment( Middle ) , m_screen( 0 ) , m_yOffset( MARGIN ) , m_rating( 0 ) , m_volume( The::engineController()->volume() ) , m_showVolume( false ) , m_hideWhenFullscreenWindowIsActive( false ) , m_fadeTimeLine( new QTimeLine( FADING_DURATION, this ) ) { Qt::WindowFlags flags; flags = Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint; // The best of both worlds. On Windows, setting the widget as a popup avoids a task manager entry. On linux, a popup steals focus. // Therefore we go need to do it platform specific :( //This is no longer true. Qt::Window steals focus on X11, Qt:Tool does not. Not sure if we even need the ifdefs any more... #ifdef Q_OS_WIN flags |= Qt::Tool; #else flags |= Qt::Tool | Qt::X11BypassWindowManagerHint; #endif setWindowFlags( flags ); setObjectName( name ); setFocusPolicy( Qt::NoFocus ); #ifdef Q_WS_X11 KWindowSystem::setType( winId(), NET::Notification ); #endif m_timer->setSingleShot( true ); connect( m_timer, &QTimer::timeout, this, &OSDWidget::hide ); m_fadeTimeLine->setUpdateInterval( 30 ); //~33 frames per second connect( m_fadeTimeLine, &QTimeLine::valueChanged, this, &OSDWidget::setFadeOpacity ); //or crashes, KWindowSystem bug I think, crashes in QWidget::icon() //kapp->setTopWidget( this ); } OSDWidget::~OSDWidget() { DEBUG_BLOCK } void OSDWidget::show( const QString &text, const QImage &newImage ) { DEBUG_BLOCK m_showVolume = false; if ( !newImage.isNull() ) { m_cover = newImage; int w = m_scaledCover.width(); int h = m_scaledCover.height(); m_scaledCover = QPixmap::fromImage( m_cover.scaled( w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ) ); } else m_cover = Amarok::icon(); m_text = text; show(); } void OSDWidget::show() { if ( !isTemporaryDisabled() ) { QWidget::show(); if( windowOpacity() == 0.0 && KWindowSystem::compositingActive() ) { m_fadeTimeLine->setDirection( QTimeLine::Forward ); m_fadeTimeLine->start(); } // Skip fading if OSD is already visible or if compositing is disabled else { m_fadeTimeLine->stop(); setWindowOpacity( maxOpacity() ); } } } void OSDWidget::hide() { if( KWindowSystem::compositingActive() ) { m_fadeTimeLine->setDirection( QTimeLine::Backward ); m_fadeTimeLine->start(); } else { QWidget::hide(); } } bool OSDWidget::isTemporaryDisabled() const { // Check if the OSD should not be shown, // if a fullscreen window is focused. if ( m_hideWhenFullscreenWindowIsActive ) { return Amarok::KNotificationBackend::instance()->isFullscreenWindowActive(); } return false; } void OSDWidget::ratingChanged( const QString& path, int rating ) { Meta::TrackPtr track = The::engineController()->currentTrack(); if( !track ) return; if( track->playableUrl().isLocalFile() && track->playableUrl().path() == path ) ratingChanged( rating ); } void OSDWidget::ratingChanged( const short rating ) { m_text = '\n' + i18n( "Rating changed" ); setRating( rating ); //Checks isEnabled() before doing anything show(); } void OSDWidget::volumeChanged( int volume ) { m_volume = volume; if ( isEnabled() ) { m_showVolume = true; const KLocalizedString text = The::engineController()->isMuted() ? ki18n( "Volume: %1% (muted)" ) : ki18n( "Volume: %1%" ); m_text = text.subs( m_volume ).toString(); show(); } } void OSDWidget::setVisible( bool visible ) { if ( visible ) { if ( !isEnabled() || m_text.isEmpty() ) return; const uint margin = fontMetrics().width( 'x' ); const QRect newGeometry = determineMetrics( margin ); if( newGeometry.width() > 0 && newGeometry.height() > 0 ) { m_margin = margin; m_size = newGeometry.size(); setGeometry( newGeometry ); QWidget::setVisible( visible ); if( m_duration ) //duration 0 -> stay forever m_timer->start( m_duration ); //calls hide() } else warning() << "Attempted to make an invalid sized OSD\n"; update(); } else QWidget::setVisible( visible ); } QRect OSDWidget::determineMetrics( const int M ) { // sometimes we only have a tiddly cover const QSize minImageSize = m_cover.size().boundedTo( QSize( 100, 100 ) ); // determine a sensible maximum size, don't cover the whole desktop or cross the screen const QSize margin( ( M + MARGIN ) * 2, ( M + MARGIN ) * 2 ); //margins const QSize image = m_cover.isNull() ? QSize( 0, 0 ) : minImageSize; const QSize max = QApplication::desktop()->screen( m_screen )->size() - margin; // If we don't do that, the boundingRect() might not be suitable for drawText() (Qt issue N67674) m_text.replace( QRegExp( " +\n" ), "\n" ); // remove consecutive line breaks m_text.replace( QRegExp( "\n+" ), "\n" ); // The osd cannot be larger than the screen QRect rect = fontMetrics().boundingRect( 0, 0, max.width() - image.width(), max.height(), Qt::AlignCenter, m_text ); rect.adjust( 0, 0, SHADOW_SIZE * 2, SHADOW_SIZE * 2 ); // the shadow needs some space if( m_showVolume ) { static const QString tmp = QString ("******").insert( 3, ( i18n("Volume: 100% (muted)" ) ) ); QRect tmpRect = fontMetrics().boundingRect( 0, 0, max.width() - image.width(), max.height() - fontMetrics().height(), Qt::AlignCenter, tmp ); tmpRect.setHeight( tmpRect.height() + fontMetrics().height() / 2 ); rect = tmpRect; if ( The::engineController()->isMuted() ) m_cover = The::svgHandler()->renderSvg( "Muted", 100, 100, "Muted" ).toImage(); else if( m_volume > 66 ) m_cover = The::svgHandler()->renderSvg( "Volume", 100, 100, "Volume" ).toImage(); else if ( m_volume > 33 ) m_cover = The::svgHandler()->renderSvg( "Volume_mid", 100, 100, "Volume_mid" ).toImage(); else m_cover = The::svgHandler()->renderSvg( "Volume_low", 100, 100, "Volume_low" ).toImage(); } // Don't show both volume and rating else if( m_rating ) { QPixmap* star = StarManager::instance()->getStar( 1 ); if( rect.width() < star->width() * 5 ) rect.setWidth( star->width() * 5 ); //changes right edge position rect.setHeight( rect.height() + star->height() + M ); //changes bottom edge pos } if( !m_cover.isNull() ) { const int availableWidth = max.width() - rect.width() - M; //WILL be >= (minImageSize.width() - M) m_scaledCover = QPixmap::fromImage( m_cover.scaled( qMin( availableWidth, m_cover.width() ), qMin( rect.height(), m_cover.height() ), Qt::KeepAspectRatio, Qt::SmoothTransformation ) ); //this will force us to be with our bounds const int widthIncludingImage = rect.width() + m_scaledCover.width() + M; //margin between text + image rect.setWidth( widthIncludingImage ); } // expand in all directions by M rect.adjust( -M, -M, M, M ); const QSize newSize = rect.size(); const QRect screen = QApplication::desktop()->screenGeometry( m_screen ); QPoint newPos( MARGIN, m_yOffset ); switch( m_alignment ) { case Left: break; case Right: newPos.rx() = screen.width() - MARGIN - newSize.width(); break; case Center: newPos.ry() = ( screen.height() - newSize.height() ) / 2; - //FALL THROUGH + Q_FALLTHROUGH(); case Middle: newPos.rx() = ( screen.width() - newSize.width() ) / 2; break; } //ensure we don't dip below the screen if ( newPos.y() + newSize.height() > screen.height() - MARGIN ) newPos.ry() = screen.height() - MARGIN - newSize.height(); // correct for screen position newPos += screen.topLeft(); return QRect( newPos, rect.size() ); } void OSDWidget::paintEvent( QPaintEvent *e ) { QRect rect( QPoint(), m_size ); QColor shadowColor; { int h, s, v; palette().color( QPalette::Normal, QPalette::WindowText ).getHsv( &h, &s, &v ); shadowColor = v > 128 ? Qt::black : Qt::white; } const int align = Qt::AlignCenter; QPainter p( this ); p.setRenderHints( QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform | QPainter::HighQualityAntialiasing ); p.setClipRect( e->rect() ); QPixmap background = The::svgHandler()->renderSvgWithDividers( "service_list_item", width(), height(), "service_list_item" ); p.drawPixmap( 0, 0, background ); //p.setPen( Qt::white ); // Revert this when the background can be colorized again. rect.adjust( m_margin, m_margin, -m_margin, -m_margin ); // subtract margins if( !m_cover.isNull() ) { QRect r( rect ); r.setTop( ( m_size.height() - m_scaledCover.height() ) / 2 ); r.setSize( m_scaledCover.size() ); p.drawPixmap( r.topLeft(), m_scaledCover ); rect.setLeft( rect.left() + m_scaledCover.width() + m_margin ); } int graphicsHeight = 0; if( !m_showVolume && m_rating > 0 && !m_paused ) { // TODO: Check if we couldn't use a KRatingPainter instead QPixmap* star = StarManager::instance()->getStar( m_rating/2 ); QRect r( rect ); //Align to center... r.setLeft( ( rect.left() + rect.width() / 2 ) - star->width() * m_rating / 4 ); r.setTop( rect.bottom() - star->height() ); graphicsHeight += star->height() + m_margin; const bool half = m_rating % 2; if( half ) { QPixmap* halfStar = StarManager::instance()->getHalfStar( m_rating / 2 + 1 ); p.drawPixmap( r.left() + star->width() * ( m_rating / 2 ), r.top(), *halfStar ); star = StarManager::instance()->getStar( m_rating / 2 + 1 ); } for( int i = 0; i < m_rating / 2; i++ ) { p.drawPixmap( r.left() + i * star->width(), r.top(), *star ); } } rect.setBottom( rect.bottom() - graphicsHeight ); // Draw "shadow" text effect (black outline) (currently it's up to five pixel in every dir.) QPixmap pixmap( rect.size() ); pixmap.fill( Qt::black ); QPainter p2( &pixmap ); p2.setFont( font() ); p2.setPen( Qt::white ); p2.setBrush( Qt::white ); p2.drawText( QRect( QPoint( SHADOW_SIZE, SHADOW_SIZE ), QSize( rect.size().width() - SHADOW_SIZE * 2, rect.size().height() - SHADOW_SIZE * 2 ) ), align, m_text ); p2.end(); p.drawImage( rect.topLeft(), ShadowEngine::makeShadow( pixmap, shadowColor ) ); p.setPen( palette().color( QPalette::Active, QPalette::WindowText ) ); p.drawText( rect.adjusted( SHADOW_SIZE, SHADOW_SIZE, -SHADOW_SIZE, -SHADOW_SIZE ), align, m_text ); } void OSDWidget::changeEvent( QEvent *event ) { QWidget::changeEvent( event ); if( event->type() == QEvent::PaletteChange ) if( !AmarokConfig::osdUseCustomColors() ) unsetColors(); // Use new palette's colors } void OSDWidget::mousePressEvent( QMouseEvent* ) { hide(); } void OSDWidget::unsetColors() { setPalette( The::paletteHandler()->palette() ); } void OSDWidget::setTextColor(const QColor& color) { QPalette palette = this->palette(); palette.setColor( QPalette::Active, QPalette::WindowText, color ); setPalette(palette); } void OSDWidget::setScreen( int screen ) { const int n = QApplication::desktop()->numScreens(); m_screen = ( screen >= n ) ? n - 1 : screen; } void OSDWidget::setFadeOpacity( qreal value ) { setWindowOpacity( value * maxOpacity() ); if( value == 0.0 ) { QWidget::hide(); } } void OSDWidget::setFontScale( int scale ) { double fontScale = static_cast( scale ) / 100.0; // update font, reuse old one QFont newFont( font() ); newFont.setPointSizeF( defaultPointSize() * fontScale ); setFont( newFont ); } void OSDWidget::setHideWhenFullscreenWindowIsActive( bool hide ) { m_hideWhenFullscreenWindowIsActive = hide; } ///////////////////////////////////////////////////////////////////////////////////////// // Class OSDPreviewWidget ///////////////////////////////////////////////////////////////////////////////////////// OSDPreviewWidget::OSDPreviewWidget( QWidget *parent ) : OSDWidget( parent ) , m_dragging( false ) { setObjectName( "osdpreview" ); setDuration( 0 ); setImage( Amarok::icon() ); setTranslucent( AmarokConfig::osdUseTranslucency() ); setText( i18n( "On-Screen-Display preview\nDrag to reposition" ) ); } void OSDPreviewWidget::mousePressEvent( QMouseEvent *event ) { m_dragYOffset = event->pos(); if( event->button() == Qt::LeftButton && !m_dragging ) { grabMouse( Qt::SizeAllCursor ); m_dragging = true; } } void OSDPreviewWidget::setUseCustomColors(const bool use, const QColor& fg) { if( use ) setTextColor( fg ); else unsetColors(); } void OSDPreviewWidget::mouseReleaseEvent( QMouseEvent * /*event*/ ) { if( m_dragging ) { m_dragging = false; releaseMouse(); emit positionChanged(); } } void OSDPreviewWidget::mouseMoveEvent( QMouseEvent *e ) { if( m_dragging && this == mouseGrabber() ) { // Here we implement a "snap-to-grid" like positioning system for the preview widget const QRect screenRect = QApplication::desktop()->screenGeometry( screen() ); const uint hcenter = screenRect.width() / 2; const uint eGlobalPosX = e->globalPos().x() - screenRect.left(); const uint snapZone = screenRect.width() / 24; QPoint destination = e->globalPos() - m_dragYOffset - screenRect.topLeft(); int maxY = screenRect.height() - height() - MARGIN; if( destination.y() < MARGIN ) destination.ry() = MARGIN; if( destination.y() > maxY ) destination.ry() = maxY; if( eGlobalPosX < ( hcenter - snapZone ) ) { setAlignment(Left); destination.rx() = MARGIN; } else if( eGlobalPosX > ( hcenter + snapZone ) ) { setAlignment(Right); destination.rx() = screenRect.width() - MARGIN - width(); } else { const uint eGlobalPosY = e->globalPos().y() - screenRect.top(); const uint vcenter = screenRect.height() / 2; destination.rx() = hcenter - width() / 2; if( eGlobalPosY >= ( vcenter - snapZone ) && eGlobalPosY <= ( vcenter + snapZone ) ) { setAlignment(Center); destination.ry() = vcenter - height() / 2; } else setAlignment(Middle); } destination += screenRect.topLeft(); move( destination ); // compute current Position && Y-offset QDesktopWidget *desktop = QApplication::desktop(); const int currentScreen = desktop->screenNumber( pos() ); // set new data OSDWidget::setScreen( currentScreen ); setYOffset( y() ); } } ///////////////////////////////////////////////////////////////////////////////////////// // Class OSD ///////////////////////////////////////////////////////////////////////////////////////// Amarok::OSD* Amarok::OSD::s_instance = 0; Amarok::OSD* Amarok::OSD::instance() { return s_instance ? s_instance : new OSD(); } void Amarok::OSD::destroy() { if ( s_instance ) { delete s_instance; s_instance = 0; } } Amarok::OSD::OSD() : OSDWidget( 0 ) { s_instance = this; EngineController* const engine = The::engineController(); if( engine->isPlaying() ) trackPlaying( engine->currentTrack() ); connect( engine, &EngineController::trackPlaying, this, &Amarok::OSD::trackPlaying ); connect( engine, &EngineController::stopped, this, &Amarok::OSD::stopped ); connect( engine, &EngineController::paused, this, &Amarok::OSD::paused ); connect( engine, &EngineController::trackMetadataChanged, this, &Amarok::OSD::metadataChanged ); connect( engine, &EngineController::albumMetadataChanged, this, &Amarok::OSD::metadataChanged ); connect( engine, &EngineController::volumeChanged, this, &Amarok::OSD::volumeChanged ); connect( engine, &EngineController::muteStateChanged, this, &Amarok::OSD::muteStateChanged ); } Amarok::OSD::~OSD() {} void Amarok::OSD::show( Meta::TrackPtr track ) //slot { setAlignment( static_cast( AmarokConfig::osdAlignment() ) ); setYOffset( AmarokConfig::osdYOffset() ); QString text; if( !track || track->playableUrl().isEmpty() ) { text = i18n( "No track playing" ); setRating( 0 ); // otherwise stars from last rating change are visible } else { setRating( track->statistics()->rating() ); text = track->prettyName(); if( track->artist() && !track->artist()->prettyName().isEmpty() ) text = track->artist()->prettyName() + " - " + text; if( track->album() && !track->album()->prettyName().isEmpty() ) text += "\n (" + track->album()->prettyName() + ") "; else text += '\n'; if( track->length() > 0 ) text += Meta::msToPrettyTime( track->length() ); } if( text.isEmpty() ) text = track->playableUrl().fileName(); if( text.startsWith( "- " ) ) //When we only have a title tag, _something_ prepends a fucking hyphen. Remove that. text = text.mid( 2 ); if( text.isEmpty() ) //still text = i18n("No information available for this track"); QImage image; if( track && track->album() ) image = The::svgHandler()->imageWithBorder( track->album(), 100, 5 ).toImage(); OSDWidget::show( text, image ); } void Amarok::OSD::applySettings() { DEBUG_BLOCK setAlignment( static_cast( AmarokConfig::osdAlignment() ) ); setDuration( AmarokConfig::osdDuration() ); setEnabled( AmarokConfig::osdEnabled() ); setYOffset( AmarokConfig::osdYOffset() ); setScreen( AmarokConfig::osdScreen() ); setFontScale( AmarokConfig::osdFontScaling() ); setHideWhenFullscreenWindowIsActive( AmarokConfig::osdHideOnFullscreen() ); if( AmarokConfig::osdUseCustomColors() ) setTextColor( AmarokConfig::osdTextColor() ); else unsetColors(); setTranslucent( AmarokConfig::osdUseTranslucency() ); } void Amarok::OSD::forceToggleOSD() { if ( !isVisible() ) { const bool b = isEnabled(); setEnabled( true ); show( The::engineController()->currentTrack() ); setEnabled( b ); } else hide(); } void Amarok::OSD::muteStateChanged( bool mute ) { Q_UNUSED( mute ) volumeChanged( The::engineController()->volume() ); } void Amarok::OSD::trackPlaying( Meta::TrackPtr track ) { m_currentTrack = track; setPaused(false); show( m_currentTrack ); } void Amarok::OSD::stopped() { setImage( QImage( KIconLoader::global()->iconPath( "amarok", -KIconLoader::SizeHuge ) ) ); setRating( 0 ); // otherwise stars from last rating change are visible OSDWidget::show( i18n( "Stopped" ) ); setPaused(false); } void Amarok::OSD::paused() { setImage( QImage( KIconLoader::global()->iconPath( "amarok", -KIconLoader::SizeHuge ) ) ); setRating( 0 ); // otherwise stars from last rating change are visible OSDWidget::show( i18n( "Paused" ) ); setPaused(true); } void Amarok::OSD::metadataChanged() { // this also covers all cases where a stream get's new metadata. show( m_currentTrack ); } /* Code copied from kshadowengine.cpp * * Copyright (C) 2003 Laur Ivan * * Many thanks to: * - Bernardo Hung for the enhanced shadow * algorithm (currently used) * - Tim Jansen for the API updates and fixes. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ namespace ShadowEngine { // Not sure, doesn't work above 10 static const int MULTIPLICATION_FACTOR = 3; // Multiplication factor for pixels directly above, under, or next to the text static const double AXIS_FACTOR = 2.0; // Multiplication factor for pixels diagonal to the text static const double DIAGONAL_FACTOR = 0.1; // Self explanatory static const int MAX_OPACITY = 200; double decay( QImage&, int, int ); QImage makeShadow( const QPixmap& textPixmap, const QColor &bgColor ) { const int w = textPixmap.width(); const int h = textPixmap.height(); const int bgr = bgColor.red(); const int bgg = bgColor.green(); const int bgb = bgColor.blue(); int alphaShadow; // This is the source pixmap QImage img = textPixmap.toImage(); QImage result( w, h, QImage::Format_ARGB32 ); result.fill( 0 ); // fill with black static const int M = OSDWidget::SHADOW_SIZE; for( int i = M; i < w - M; i++) { for( int j = M; j < h - M; j++ ) { alphaShadow = (int) decay( img, i, j ); result.setPixel( i,j, qRgba( bgr, bgg , bgb, qMin( MAX_OPACITY, alphaShadow ) ) ); } } return result; } double decay( QImage& source, int i, int j ) { //if ((i < 1) || (j < 1) || (i > source.width() - 2) || (j > source.height() - 2)) // return 0; double alphaShadow; alphaShadow =(qGray(source.pixel(i-1,j-1)) * DIAGONAL_FACTOR + qGray(source.pixel(i-1,j )) * AXIS_FACTOR + qGray(source.pixel(i-1,j+1)) * DIAGONAL_FACTOR + qGray(source.pixel(i ,j-1)) * AXIS_FACTOR + 0 + qGray(source.pixel(i ,j+1)) * AXIS_FACTOR + qGray(source.pixel(i+1,j-1)) * DIAGONAL_FACTOR + qGray(source.pixel(i+1,j )) * AXIS_FACTOR + qGray(source.pixel(i+1,j+1)) * DIAGONAL_FACTOR) / MULTIPLICATION_FACTOR; return alphaShadow; } }