diff --git a/CMakeLists.txt b/CMakeLists.txt index f3853c8429..a8eed73a63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,320 +1,321 @@ cmake_minimum_required(VERSION 3.4) project(Amarok) # Remove all warnings, ease the porting to cmake 3.x if (POLICY CMP0028) cmake_policy(SET CMP0028 NEW) endif() ############### set(KF5_MIN_VERSION "5.41.0") set(QT_REQUIRED_VERSION "5.8.0") find_package(PkgConfig REQUIRED) find_package(ECM ${KF5_MIN_VERSION} REQUIRED CONFIG) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECMakeSettings) +#include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(KDECompilerSettings NO_POLICY_SCOPE) include(FeatureSummary) include(ECMInstallIcons) include(ECMSetupVersion) include(ECMAddTests) include(ECMAddAppIcon) include(FindPkgConfig) include(CMakePushCheckState) include(GenerateExportHeader) find_package( Qt5 ${QT_REQUIRED_VERSION} COMPONENTS QuickControls2 WebEngine ) set_package_properties( Qt5QuickControls2 PROPERTIES TYPE RUNTIME PURPOSE "Needed by the player's context area" ) find_package( Qt5 5.8.0 REQUIRED COMPONENTS Core DBus Gui QuickWidgets Qml Script ScriptTools Sql Svg Test Widgets Xml ) set_package_properties( Qt5WebEngine PROPERTIES TYPE OPTIONAL PURPOSE "Needed by the wikipedia applet" ) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Archive Attica Codecs Config ConfigWidgets CoreAddons Crash DBusAddons Declarative DNSSD GlobalAccel GuiAddons I18n IconThemes KCMUtils KIO NewStuff Notifications NotifyConfig Package Solid TextEditor ThreadWeaver WidgetsAddons WindowSystem ) find_package( KF5 ${KF5_MIN_VERSION} COMPONENTS Kirigami2 ) set_package_properties( KF5Kirigami2 PROPERTIES TYPE RUNTIME PURPOSE "Needed by the player's context area" ) ############### 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) cmake_push_check_state() 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) cmake_pop_check_state() 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 find_package(Googlemock) set_package_properties( GOOGLEMOCK PROPERTIES DESCRIPTION "Used in Amarok's tests." URL "https://github.com/google/googlemock" TYPE OPTIONAL ) if(NOT GOOGLEMOCK_FOUND) set(BUILD_TESTING OFF) endif() 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 () #################################################################################### # CMAKE_AUTOMOC if(NOT CMAKE_VERSION VERSION_LESS "3.10.0") # CMake 3.9+ warns about automoc on files without Q_OBJECT, and doesn't know about other macros. # 3.10+ lets us provide more macro names that require automoc. list(APPEND CMAKE_AUTOMOC_MACRO_NAMES AMAROK_EXPORT_SIMPLE_IMPORTER_PLUGIN) endif() add_definitions(-DQT_NO_URL_CAST_FROM_STRING) find_package(Phonon4Qt5 4.6.60 REQUIRED NO_MODULE) 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() 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(Threads REQUIRED) find_package(MySQL 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 ) # 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 ) find_package(FFmpeg) set_package_properties(FFmpeg PROPERTIES DESCRIPTION "Libraries and tools for handling multimedia data" URL "https://www.ffmpeg.org/" TYPE OPTIONAL PURPOSE "Enable MusicDNS service" ) find_package(LibOFA) set_package_properties(LibOFA PROPERTIES DESCRIPTION "Open-source audio fingerprint by MusicIP" URL "http://code.google.com/p/musicip-libofa/" TYPE OPTIONAL PURPOSE "Enable MusicDNS service" ) ## gpodder Service find_package(Mygpo-qt5 1.1.0 CONFIG) set_package_properties(Mygpo-qt5 PROPERTIES DESCRIPTION "A Qt/C++ library wrapping the gpodder.net Webservice." URL "http://wiki.gpodder.org/wiki/Libmygpo-qt" TYPE OPTIONAL PURPOSE "Synchronize podcast subscriptions with gpodder.net" ) 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 1.0.0) set_package_properties(Mtp PROPERTIES TYPE OPTIONAL PURPOSE "Enable Support for portable media devices that use the media transfer protocol" ) 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_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 ) find_package(FFTW3 REQUIRED) 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 ) add_subdirectory( utilities ) endif() if( WITH_PLAYGROUND ) add_subdirectory( playground ) message(STATUS "Included playground subdirectory in configuration") endif() include(CTest) diff --git a/shared/FileTypeResolver.cpp b/shared/FileTypeResolver.cpp index 2cdfe897c8..2d51f98bc2 100644 --- a/shared/FileTypeResolver.cpp +++ b/shared/FileTypeResolver.cpp @@ -1,202 +1,202 @@ /**************************************************************************************** * Copyright (c) 2005 Martin Aumueller * * Copyright (c) 2011 Ralf Engels * * * * 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 "FileTypeResolver.h" #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #ifdef TAGLIB_EXTRAS_FOUND #include #include #endif // TAGLIB_EXTRAS_FOUND #include #include #include #include #include #include #include #ifdef TAGLIB_OPUS_FOUND #include #endif // TAGLIB_OPUS_FOUND #include #include #include #include #include #include #ifdef TAGLIB_MOD_FOUND #include #include #include #include #include #include #endif // TAGLIB_MOD_FOUND #pragma GCC diagnostic pop TagLib::File *Meta::Tag::FileTypeResolver::createFile(TagLib::FileName fileName, bool readProperties, TagLib::AudioProperties::ReadStyle propertiesStyle) const { TagLib::File* result = 0; QMimeDatabase db; QString fn = QFile::decodeName( fileName ); QString suffix = QFileInfo( fn ).suffix(); QMimeType mimetype = db.mimeTypeForFile( fn ); // -- check by mime type if( mimetype.inherits( QStringLiteral("audio/mpeg") ) || mimetype.inherits( QStringLiteral("audio/x-mpegurl") ) || mimetype.inherits( QStringLiteral("audio/mpeg") )) { result = new TagLib::MPEG::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/mp4") ) || mimetype.inherits( QStringLiteral("video/mp4") ) ) { result = new TagLib::MP4::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-ms-wma") ) || mimetype.inherits( QStringLiteral("video/x-ms-asf") ) || mimetype.inherits( QStringLiteral("video/x-msvideo") ) || mimetype.inherits( QStringLiteral("video/x-ms-wmv") ) ) { result = new TagLib::ASF::File(fileName, readProperties, propertiesStyle); } #ifdef TAGLIB_EXTRAS_FOUND else if( mimetype.inherits( QLatin1String("audio/vnd.rn-realaudio") ) || mimetype.inherits( QLatin1String("audio/x-pn-realaudioplugin") ) || mimetype.inherits( QLatin1String("audio/vnd.rn-realvideo") ) ) { result = new TagLibExtras::RealMedia::File(fileName, readProperties, propertiesStyle); } #endif #ifdef TAGLIB_OPUS_FOUND else if( mimetype.inherits( QStringLiteral("audio/opus") ) || mimetype.inherits( QStringLiteral("audio/x-opus+ogg") ) ) { result = new TagLib::Ogg::Opus::File(fileName, readProperties, propertiesStyle); } #endif else if( mimetype.inherits( QStringLiteral("audio/vorbis") ) || mimetype.inherits( QStringLiteral("audio/x-vorbis+ogg") ) ) { result = new TagLib::Ogg::Vorbis::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-flac+ogg") ) ) { result = new TagLib::Ogg::FLAC::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-aiff") ) ) { result = new TagLib::RIFF::AIFF::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-flac") ) ) { result = new TagLib::FLAC::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-musepack") ) ) { result = new TagLib::MPC::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-wav") ) ) { result = new TagLib::RIFF::WAV::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-wavpack") ) ) { result = new TagLib::WavPack::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-tta") ) ) { result = new TagLib::TrueAudio::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-speex") ) || mimetype.inherits( QStringLiteral("audio/x-speex+ogg") ) ) { result = new TagLib::Ogg::Speex::File(fileName, readProperties, propertiesStyle); } #ifdef TAGLIB_MOD_FOUND else if( mimetype.inherits( QStringLiteral("audio/x-mod") ) ) { result = new TagLib::Mod::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-s3m") ) ) { result = new TagLib::S3M::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-it") ) ) { result = new TagLib::IT::File(fileName, readProperties, propertiesStyle); } else if( mimetype.inherits( QStringLiteral("audio/x-xm") ) ) { result = new TagLib::XM::File(fileName, readProperties, propertiesStyle); } #endif // -- check by extension else if( suffix == QLatin1String("m4a") || suffix == QLatin1String("m4b") || suffix == QLatin1String("m4p") || suffix == QLatin1String("mp4") || suffix == QLatin1String("m4v") || suffix == QLatin1String("mp4v") ) { result = new TagLib::MP4::File(fileName, readProperties, propertiesStyle); } else if( suffix == QLatin1String("wav") ) { result = new TagLib::RIFF::WAV::File(fileName, readProperties, propertiesStyle); } else if( suffix == QLatin1String("wma") || suffix == QLatin1String("asf") ) { result = new TagLib::ASF::File(fileName, readProperties, propertiesStyle); } #ifdef TAGLIB_OPUS_FOUND // this is currently needed because shared-mime-info database doesn't have opus entry (2013-01) else if( suffix == QLatin1String("opus") ) { result = new TagLib::Ogg::Opus::File(fileName, readProperties, propertiesStyle); } #endif #ifndef Q_WS_WIN if( !result ) - qDebug() << QString( "FileTypeResolver: file %1 (mimetype %2) not recognized as " - "Amarok-compatible" ).arg( fileName, mimetype.name() ).toLocal8Bit().data(); + qDebug() << QStringLiteral( "FileTypeResolver: file %1 (mimetype %2) not recognized as " + "Amarok-compatible" ).arg( QString::fromLatin1(fileName), mimetype.name() ); #endif if( result && !result->isValid() ) { delete result; - result = 0; + result = nullptr; } return result; } diff --git a/shared/MetaReplayGain.cpp b/shared/MetaReplayGain.cpp index b3cc721180..319cba14c9 100644 --- a/shared/MetaReplayGain.cpp +++ b/shared/MetaReplayGain.cpp @@ -1,350 +1,350 @@ /**************************************************************************************** * Copyright (c) 2009 Alex Merry * * * * 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 . * ****************************************************************************************/ // NOTE: this file is used by amarokcollectionscanner and CANNOT use any amaroklib // code [this includes debug()] #include "MetaReplayGain.h" #include #include // Taglib #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #pragma GCC diagnostic pop // converts a peak value from the normal digital scale form to the more useful decibel form // decibels are relative to the /adjusted/ waveform static qreal peakToDecibels( qreal scaleVal ) { if ( scaleVal > 0 ) return 20 * log10( scaleVal ); else return 0; } // NOTE: representation is taken to be a binary value with units in the first column, // 1/2 in the second and so on. static qreal readRVA2PeakValue( const TagLib::ByteVector &data, int bits, bool *ok ) { qreal peak = 0.0; // discarding digits at the end reduces precision, but doesn't otherwise change the value if ( bits > 32 ) bits = 32; // the +7 makes sure we round up when we divide by 8 unsigned int bytes = (bits + 7) / 8; // normalize appears not to write a peak at all, and hence sets bits to 0 if ( bits == 0 ) { if ( ok ) *ok = true; } else if ( bits >= 4 && data.size() >= bytes ) // fewer than 4 bits would just be daft { // excessBits is the number of bits we have to discard at the end unsigned int excessBits = (8 * bytes) - bits; // mask has 1s everywhere but the last /excessBits/ bits quint32 mask = 0xffffffff << excessBits; quint32 rawValue = 0; for ( unsigned int i = 0; i < bytes; ++i ) { rawValue <<= 8; rawValue += (unsigned char)data[i]; } rawValue &= mask; peak = rawValue; // amount we need to "shift" the value right to make the first digit the unit column unsigned int rightShift = (8 * bytes) - 1; peak /= (qreal)(1 << rightShift); if ( ok ) *ok = true; } else { if ( ok ) *ok = false; } return peak; } // adds the converted version of the scale value if it is a valid, non-negative float static void maybeAddPeak( const TagLib::String &scaleVal, Meta::ReplayGainTag key, Meta::ReplayGainTagMap *map ) { // scale value is >= 0, and typically not much bigger than 1 QString value = TStringToQString( scaleVal ); bool ok = false; qreal peak = value.toFloat( &ok ); if ( ok && peak >= 0 ) (*map)[key] = peakToDecibels( peak ); } static void maybeAddGain( const TagLib::String &input, Meta::ReplayGainTag key, Meta::ReplayGainTagMap *map ) { QString value = TStringToQString( input ).remove( QStringLiteral(" dB") ); bool ok = false; qreal gain = value.toFloat( &ok ); if (ok) (*map)[key] = gain; } static Meta::ReplayGainTagMap readID3v2Tags( TagLib::ID3v2::Tag *tag ) { Meta::ReplayGainTagMap map; { // ID3v2.4.0 native replay gain tag support (as written by Quod Libet, for example). TagLib::ID3v2::FrameList frames = tag->frameListMap()["RVA2"]; frames.append(tag->frameListMap()["XRVA"]); if ( !frames.isEmpty() ) { for ( unsigned int i = 0; i < frames.size(); ++i ) { // we have to parse this frame ourselves // ID3v2 frame header is 10 bytes, so skip that TagLib::ByteVector data = frames[i]->render().mid( 10 ); unsigned int offset = 0; - QString desc( data.data() ); + QString desc( QString::fromLatin1(data.data()) ); offset += desc.count() + 1; unsigned int channel = data.mid( offset, 1 ).toUInt( true ); // channel 1 is the main volume - the only one we care about if ( channel == 1 ) { ++offset; qint16 adjustment512 = data.mid( offset, 2 ).toShort( true ); qreal adjustment = ( (qreal)adjustment512 ) / 512.0; offset += 2; unsigned int peakBits = data.mid( offset, 1 ).toUInt( true ); ++offset; bool ok = false; qreal peak = readRVA2PeakValue( data.mid( offset ), peakBits, &ok ); if ( ok ) { if ( desc.toLower() == QLatin1String("album") ) { map[Meta::ReplayGain_Album_Gain] = adjustment; map[Meta::ReplayGain_Album_Peak] = peakToDecibels( peak ); } else if ( desc.toLower() == QLatin1String("track") || !map.contains( Meta::ReplayGain_Track_Gain ) ) { map[Meta::ReplayGain_Track_Gain] = adjustment; map[Meta::ReplayGain_Track_Peak] = peakToDecibels( peak ); } } } } if ( !map.isEmpty() ) return map; } } { // Foobar2000-style ID3v2.3.0 tags TagLib::ID3v2::FrameList frames = tag->frameListMap()["TXXX"]; for ( TagLib::ID3v2::FrameList::Iterator it = frames.begin(); it != frames.end(); ++it ) { TagLib::ID3v2::UserTextIdentificationFrame* frame = dynamic_cast( *it ); if ( frame && frame->fieldList().size() >= 2 ) { QString desc = TStringToQString( frame->description() ).toLower(); if ( desc == QLatin1String("replaygain_album_gain") ) maybeAddGain( frame->fieldList()[1], Meta::ReplayGain_Album_Gain, &map ); if ( desc == QLatin1String("replaygain_album_peak") ) maybeAddPeak( frame->fieldList()[1], Meta::ReplayGain_Album_Peak, &map ); if ( desc == QLatin1String("replaygain_track_gain") ) maybeAddGain( frame->fieldList()[1], Meta::ReplayGain_Track_Gain, &map ); if ( desc == QLatin1String("replaygain_track_peak") ) maybeAddPeak( frame->fieldList()[1], Meta::ReplayGain_Track_Peak, &map ); } } } return map; } static Meta::ReplayGainTagMap readAPETags( TagLib::APE::Tag *tag ) { Meta::ReplayGainTagMap map; const TagLib::APE::ItemListMap &items = tag->itemListMap(); if ( items.contains("REPLAYGAIN_TRACK_GAIN") ) { maybeAddGain( items["REPLAYGAIN_TRACK_GAIN"].values()[0], Meta::ReplayGain_Track_Gain, &map ); if ( items.contains("REPLAYGAIN_TRACK_PEAK") ) maybeAddPeak( items["REPLAYGAIN_TRACK_PEAK"].values()[0], Meta::ReplayGain_Track_Peak, &map ); } if ( items.contains("REPLAYGAIN_ALBUM_GAIN") ) { maybeAddGain( items["REPLAYGAIN_ALBUM_GAIN"].values()[0], Meta::ReplayGain_Album_Gain, &map ); if ( items.contains("REPLAYGAIN_ALBUM_PEAK") ) maybeAddPeak( items["REPLAYGAIN_ALBUM_PEAK"].values()[0], Meta::ReplayGain_Album_Peak, &map ); } return map; } static Meta::ReplayGainTagMap readXiphTags( TagLib::Ogg::XiphComment *tag ) { const TagLib::Ogg::FieldListMap &tagMap = tag->fieldListMap(); Meta::ReplayGainTagMap outputMap; if ( !tagMap["REPLAYGAIN_TRACK_GAIN"].isEmpty() ) { maybeAddGain( tagMap["REPLAYGAIN_TRACK_GAIN"].front(), Meta::ReplayGain_Track_Gain, &outputMap ); if ( !tagMap["REPLAYGAIN_TRACK_PEAK"].isEmpty() ) maybeAddPeak( tagMap["REPLAYGAIN_TRACK_PEAK"].front(), Meta::ReplayGain_Track_Peak, &outputMap ); } if ( !tagMap["REPLAYGAIN_ALBUM_GAIN"].isEmpty() ) { maybeAddGain( tagMap["REPLAYGAIN_ALBUM_GAIN"].front(), Meta::ReplayGain_Album_Gain, &outputMap ); if ( !tagMap["REPLAYGAIN_ALBUM_PEAK"].isEmpty() ) maybeAddPeak( tagMap["REPLAYGAIN_ALBUM_PEAK"].front(), Meta::ReplayGain_Album_Peak, &outputMap ); } return outputMap; } static Meta::ReplayGainTagMap readASFTags( TagLib::ASF::Tag *tag ) { const TagLib::ASF::AttributeListMap &tagMap = tag->attributeListMap(); Meta::ReplayGainTagMap outputMap; if ( !tagMap["REPLAYGAIN_TRACK_GAIN"].isEmpty() ) { maybeAddGain( tagMap["REPLAYGAIN_TRACK_GAIN"].front().toString(), Meta::ReplayGain_Track_Gain, &outputMap ); if ( !tagMap["REPLAYGAIN_TRACK_PEAK"].isEmpty() ) maybeAddPeak( tagMap["REPLAYGAIN_TRACK_PEAK"].front().toString(), Meta::ReplayGain_Track_Peak, &outputMap ); } if ( !tagMap["REPLAYGAIN_ALBUM_GAIN"].isEmpty() ) { maybeAddGain( tagMap["REPLAYGAIN_ALBUM_GAIN"].front().toString(), Meta::ReplayGain_Album_Gain, &outputMap ); if ( !tagMap["REPLAYGAIN_ALBUM_PEAK"].isEmpty() ) maybeAddPeak( tagMap["REPLAYGAIN_ALBUM_PEAK"].front().toString(), Meta::ReplayGain_Album_Peak, &outputMap ); } return outputMap; } // Bad news: ReplayGain in MP4 is not actually standardized in any way. Maybe reimplement at some point...maybe. See // http://www.hydrogenaudio.org/forums/lofiversion/index.php/t14322.html #ifdef DO_NOT_USE_THIS_UNTIL_FIXED static Meta::ReplayGainTagMap readMP4Tags( TagLib::MP4::Tag *tag ) { Meta::ReplayGainTagMap outputMap; if ( !tag->trackReplayGain().isNull() ) { maybeAddGain( tag->trackReplayGain(), Meta::ReplayGain_Track_Gain, &outputMap ); if ( !tag->trackReplayGainPeak().isNull() ) maybeAddPeak( tag->trackReplayGainPeak(), Meta::ReplayGain_Track_Peak, &outputMap ); } if ( !tag->albumReplayGain().isNull() ) { maybeAddGain( tag->albumReplayGain(), Meta::ReplayGain_Album_Gain, &outputMap ); if ( !tag->albumReplayGainPeak().isNull() ) maybeAddPeak( tag->albumReplayGainPeak(), Meta::ReplayGain_Album_Peak, &outputMap ); } return outputMap; } #endif Meta::ReplayGainTagMap Meta::readReplayGainTags( const TagLib::FileRef &fileref ) { Meta::ReplayGainTagMap map; // NB: we can't get replay gain info from MPC files, since it's stored in some magic place // and not in the APE tags, and taglib doesn't let us access the information (unless // we want to parse the file ourselves). // FIXME: should we try getting the info from the MPC APE tag just in case? if ( TagLib::MPEG::File *file = dynamic_cast( fileref.file() ) ) { if ( file->ID3v2Tag() ) map = readID3v2Tags( file->ID3v2Tag() ); if ( map.isEmpty() && file->APETag() ) map = readAPETags( file->APETag() ); } else if ( TagLib::Ogg::Vorbis::File *file = dynamic_cast( fileref.file() ) ) { if ( file->tag() ) map = readXiphTags( file->tag() ); } else if ( TagLib::FLAC::File *file = dynamic_cast( fileref.file() ) ) { if ( file->xiphComment() ) map = readXiphTags( file->xiphComment() ); if ( map.isEmpty() && file->ID3v2Tag() ) map = readID3v2Tags( file->ID3v2Tag() ); } else if ( TagLib::Ogg::FLAC::File *file = dynamic_cast( fileref.file() ) ) { if ( file->tag() ) map = readXiphTags( file->tag() ); } else if ( TagLib::WavPack::File *file = dynamic_cast( fileref.file() ) ) { if ( file->APETag() ) map = readAPETags( file->APETag() ); } else if ( TagLib::TrueAudio::File *file = dynamic_cast( fileref.file() ) ) { if ( file->ID3v2Tag() ) map = readID3v2Tags( file->ID3v2Tag() ); } else if ( TagLib::Ogg::Speex::File *file = dynamic_cast( fileref.file() ) ) { if ( file->tag() ) map = readXiphTags( file->tag() ); } else if ( TagLib::MPC::File *file = dynamic_cast( fileref.file() ) ) { // This is NOT the correct way to get replay gain tags from MPC files, but // taglib doesn't allow us access to the real information. // This allows people to work around this issue by copying their replay gain // information to the APE tag. if ( file->APETag() ) map = readAPETags( file->APETag() ); } else if ( TagLib::ASF::File *file = dynamic_cast( fileref.file() ) ) { if ( file->tag() ) map = readASFTags( file->tag() ); } // See comment above #ifdef DO_NOT_USE_THIS_UNTIL_FIXED else if ( TagLib::MP4::File *file = dynamic_cast( fileref.file() ) ) { if ( file->tag() ) map = readMP4Tags( file->getMP4Tag() ); } #endif return map; } diff --git a/shared/MetaTagLib.cpp b/shared/MetaTagLib.cpp index fea9bd0853..97220a0945 100644 --- a/shared/MetaTagLib.cpp +++ b/shared/MetaTagLib.cpp @@ -1,351 +1,351 @@ /*************************************************************************** * Copyright (C) 2003-2005 Max Howell * * (C) 2003-2010 Mark Kretschmann * * (C) 2005-2007 Alexandre Oliveira * * (C) 2008 Dan Meltzer * * (C) 2008-2009 Jeff Mitchell * * (C) 2010 Ralf Engels * * (C) 2010 Sergey Ivanov <123kash@gmail.com> * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "MetaTagLib.h" #include "FileType.h" #include "TagsFromFileNameGuesser.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "FileTypeResolver.h" #include "MetaReplayGain.h" #include "tag_helpers/TagHelper.h" #include "tag_helpers/StringHelper.h" //Taglib: #include #ifdef TAGLIB_EXTRAS_FOUND #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #include #include #pragma GCC diagnostic pop #endif // TAGLIB_EXTRAS_FOUND namespace Meta { namespace Tag { QMutex s_mutex; static void addRandomness( QCryptographicHash *md5 ); /** Get a taglib fileref for a path */ static TagLib::FileRef getFileRef( const QString &path ); /** Returns a byte vector that can be used to generate the unique id based on the tags. */ static TagLib::ByteVector generatedUniqueIdHelper( const TagLib::FileRef &fileref ); static QString generateUniqueId( const QString &path ); } } TagLib::FileRef Meta::Tag::getFileRef( const QString &path ) { #ifdef Q_OS_WIN32 const wchar_t *encodedName = reinterpret_cast< const wchar_t * >( path.utf16() ); #else #ifdef COMPLEX_TAGLIB_FILENAME const wchar_t *encodedName = reinterpret_cast< const wchar_t * >( path.utf16() ); #else QByteArray fileName = QFile::encodeName( path ); const char *encodedName = fileName.constData(); // valid as long as fileName exists #endif #endif // Tests reveal the following: // // TagLib::AudioProperties Relative Time Taken // // No AudioProp Reading 1 // Fast 1.18 // Average Untested // Accurate Untested return TagLib::FileRef( encodedName, true, TagLib::AudioProperties::Fast ); } // ----------------------- unique id ------------------------ void Meta::Tag::addRandomness( QCryptographicHash *md5 ) { //md5 has size of file already added for some little extra randomness for the hash qsrand( QTime::currentTime().msec() ); md5->addData( QString::number( qrand() ).toLatin1() ); md5->addData( QString::number( qrand() ).toLatin1() ); md5->addData( QString::number( qrand() ).toLatin1() ); md5->addData( QString::number( qrand() ).toLatin1() ); md5->addData( QString::number( qrand() ).toLatin1() ); md5->addData( QString::number( qrand() ).toLatin1() ); md5->addData( QString::number( qrand() ).toLatin1() ); } TagLib::ByteVector Meta::Tag::generatedUniqueIdHelper( const TagLib::FileRef &fileref ) { TagLib::ByteVector bv; TagHelper *tagHelper = selectHelper( fileref ); if( tagHelper ) { bv = tagHelper->render(); delete tagHelper; } return bv; } QString Meta::Tag::generateUniqueId( const QString &path ) { QCryptographicHash md5( QCryptographicHash::Md5 ); QFile qfile( path ); QByteArray size; md5.addData( size.setNum( qfile.size() ) ); TagLib::FileRef fileref = getFileRef( path ); TagLib::ByteVector bv = generatedUniqueIdHelper( fileref ); md5.addData( bv.data(), bv.size() ); char databuf[16384]; int readlen = 0; if( qfile.open( QIODevice::ReadOnly ) ) { if( ( readlen = qfile.read( databuf, 16384 ) ) > 0 ) { md5.addData( databuf, readlen ); qfile.close(); } else { qfile.close(); addRandomness( &md5 ); } } else addRandomness( &md5 ); - return QString( md5.result().toHex() ); + return QString::fromLatin1( md5.result().toHex() ); } // --------- file type resolver ---------- /** Will ensure that we have our file type resolvers added */ static void ensureFileTypeResolvers() { static bool alreadyAdded = false; if( !alreadyAdded ) { alreadyAdded = true; #ifdef TAGLIB_EXTRAS_FOUND TagLib::FileRef::addFileTypeResolver(new AudibleFileTypeResolver); TagLib::FileRef::addFileTypeResolver(new RealMediaFileTypeResolver); #endif TagLib::FileRef::addFileTypeResolver(new Meta::Tag::FileTypeResolver()); } } // ----------------------- reading ------------------------ Meta::FieldHash Meta::Tag::readTags( const QString &path, bool /*useCharsetDetector*/ ) { Meta::FieldHash result; // we do not rely on taglib being thread safe especially when writing the same file from different threads. QMutexLocker locker( &s_mutex ); ensureFileTypeResolvers(); TagLib::FileRef fileref = getFileRef( path ); if( fileref.isNull() ) return result; Meta::ReplayGainTagMap replayGainTags = Meta::readReplayGainTags( fileref ); if( replayGainTags.contains( Meta::ReplayGain_Track_Gain ) ) result.insert( Meta::valTrackGain, replayGainTags[Meta::ReplayGain_Track_Gain] ); if( replayGainTags.contains( Meta::ReplayGain_Track_Peak ) ) result.insert( Meta::valTrackGainPeak, replayGainTags[Meta::ReplayGain_Track_Peak] ); // strangely: the album gain defaults to the track gain if( replayGainTags.contains( Meta::ReplayGain_Album_Gain ) ) result.insert( Meta::valAlbumGain, replayGainTags[Meta::ReplayGain_Album_Gain] ); else if( replayGainTags.contains( Meta::ReplayGain_Track_Gain ) ) result.insert( Meta::valAlbumGain, replayGainTags[Meta::ReplayGain_Track_Gain] ); if( replayGainTags.contains( Meta::ReplayGain_Album_Peak ) ) result.insert( Meta::valAlbumGainPeak, replayGainTags[Meta::ReplayGain_Album_Peak] ); else if( replayGainTags.contains( Meta::ReplayGain_Track_Peak ) ) result.insert( Meta::valAlbumGainPeak, replayGainTags[Meta::ReplayGain_Track_Peak] ); TagHelper *tagHelper = selectHelper( fileref ); if( tagHelper ) { if( 0/* useCharsetDetector */ ) { KEncodingProber prober; if( prober.feed( tagHelper->testString() ) != KEncodingProber::NotMe ) Meta::Tag::setCodecByName( prober.encoding() ); } result.insert( Meta::valFormat, tagHelper->fileType() ); result.unite( tagHelper->tags() ); delete tagHelper; } TagLib::AudioProperties *properties = fileref.audioProperties(); if( properties ) { if( !result.contains( Meta::valBitrate ) && properties->bitrate() ) result.insert( Meta::valBitrate, properties->bitrate() ); if( !result.contains( Meta::valLength ) && properties->length() ) result.insert( Meta::valLength, properties->length() * 1000 ); if( !result.contains( Meta::valSamplerate ) && properties->sampleRate() ) result.insert( Meta::valSamplerate, properties->sampleRate() ); } //If tags doesn't contains title and artist, try to guess It from file name if( !result.contains( Meta::valTitle ) || result.value( Meta::valTitle ).toString().isEmpty() ) result.unite( TagGuesser::guessTags( path ) ); //we didn't set a FileType till now, let's look it up via FileExtension if( !result.contains( Meta::valFormat ) ) { QString ext = path.mid( path.lastIndexOf( QLatin1Char('.') ) + 1 ); result.insert( Meta::valFormat, Amarok::FileTypeSupport::fileType( ext ) ); } QFileInfo fileInfo( path ); result.insert( Meta::valFilesize, fileInfo.size() ); result.insert( Meta::valModified, fileInfo.lastModified() ); if( !result.contains( Meta::valUniqueId ) ) result.insert( Meta::valUniqueId, generateUniqueId( path ) ); // compute bitrate if it is not already set and we know length if( !result.contains( Meta::valBitrate ) && result.contains( Meta::valLength ) ) result.insert( Meta::valBitrate, ( fileInfo.size() * 8 * 1000 ) / ( result.value( Meta::valLength ).toInt() * 1024 ) ); return result; } QImage Meta::Tag::embeddedCover( const QString &path ) { // we do not rely on taglib being thread safe especially when writing the same file from different threads. QMutexLocker locker( &s_mutex ); ensureFileTypeResolvers(); TagLib::FileRef fileref = getFileRef( path ); if( fileref.isNull() ) return QImage(); QImage img; TagHelper *tagHelper = selectHelper( fileref ); if( tagHelper ) { img = tagHelper->embeddedCover(); delete tagHelper; } return img; } void Meta::Tag::writeTags( const QString &path, const FieldHash &changes, bool writeStatistics ) { FieldHash data = changes; if( !writeStatistics ) { data.remove( Meta::valFirstPlayed ); data.remove( Meta::valLastPlayed ); data.remove( Meta::valPlaycount ); data.remove( Meta::valScore ); data.remove( Meta::valRating ); } // we do not rely on taglib being thread safe especially when writing the same file from different threads. QMutexLocker locker( &s_mutex ); ensureFileTypeResolvers(); TagLib::FileRef fileref = getFileRef( path ); if( fileref.isNull() || data.isEmpty() ) return; QScopedPointer tagHelper( selectHelper( fileref, true ) ); if( !tagHelper ) return; if( tagHelper->setTags( data ) ) fileref.save(); } void Meta::Tag::setEmbeddedCover( const QString &path, const QImage &cover ) { // we do not rely on taglib being thread safe especially when writing the same file from different threads. QMutexLocker locker( &s_mutex ); ensureFileTypeResolvers(); TagLib::FileRef fileref = getFileRef( path ); if( fileref.isNull() ) return; TagHelper *tagHelper = selectHelper( fileref, true ); if( !tagHelper ) return; if( tagHelper->setEmbeddedCover( cover ) ) fileref.save(); delete tagHelper; } #undef Qt4QStringToTString diff --git a/shared/TagsFromFileNameGuesser.cpp b/shared/TagsFromFileNameGuesser.cpp index d7c61d02fa..79d0c5d997 100644 --- a/shared/TagsFromFileNameGuesser.cpp +++ b/shared/TagsFromFileNameGuesser.cpp @@ -1,143 +1,143 @@ /**************************************************************************************** * Copyright (c) 2010 Sergey Ivanov <123kash@gmail.com> * * * * 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 "TagsFromFileNameGuesser.h" #include const QStringList m_schemes( QStringList() //01 Artist - Title.ext << QStringLiteral("^%track%\\W*-?\\W*%artist%\\W*-\\W*%title%\\.+(?:\\w{2,5})$") //01 Title.ext << QStringLiteral("^%track%\\W*-?\\W*%title%\\.+?:\\w{2,5}$") //Album - 01 - Artist - Title.ext << QStringLiteral("^%album%\\W*-\\W*%track%\\W*-\\W*%artist%\\W*-\\W*%title%\\.+(?:\\w{2,5})$") //Artist - Album - 01 - Title.ext << QStringLiteral("^%artist%\\W*-\\W*%album%\\W*-\\W*%track%\\W*-\\W*%title%\\.+(?:\\w{2,5})$") // Artist - Album - Title.ext << QStringLiteral("^%artist%\\W*-\\W*%album%\\W*-\\W*%title%\\.+(?:\\w{2,5})$") //Artist - Title.ext << QStringLiteral("^%artist%\\W*-\\W*%title%\\.+(?:\\w{2,5})$") //Title.ext << QStringLiteral("^%title%\\.+(?:\\w{2,5})$") ); -const QRegExp m_digitalFields( "(%(?:discnumber|track|year)%)" ); -const QRegExp m_literalFields( "(%(?:album|albumartist|artist|comment|composer|genre|title)%)" ); +const QRegExp m_digitalFields( QStringLiteral("(%(?:discnumber|track|year)%)") ); +const QRegExp m_literalFields( QStringLiteral("(%(?:album|albumartist|artist|comment|composer|genre|title)%)") ); quint64 fieldName( const QString &field ) { if( field == QLatin1String("album") ) return Meta::valAlbum; else if( field == QLatin1String("albumartist") ) return Meta::valAlbumArtist; else if( field == QLatin1String("artist") ) return Meta::valArtist; else if( field == QLatin1String("comment") ) return Meta::valComment; else if( field == QLatin1String("composer") ) return Meta::valComposer; else if( field == QLatin1String("discnumber") ) return Meta::valDiscNr; else if( field == QLatin1String("genre") ) return Meta::valGenre; else if( field == QLatin1String("title") ) return Meta::valTitle; else if( field == QLatin1String("track") ) return Meta::valTrackNr; else if( field == QLatin1String("year") ) return Meta::valYear; return 0; } QList< qint64 > parseTokens( const QString &scheme ) { - QRegExp rxm( "%(\\w+)%" ); + QRegExp rxm( QStringLiteral("%(\\w+)%") ); QList< qint64 > tokens; int pos = 0; qint64 field; while( ( pos = rxm.indexIn( scheme, pos ) ) != -1 ) { field = fieldName( rxm.cap( 1 ) ); if( field ) tokens << field; pos += rxm.matchedLength(); } return tokens; } Meta::FieldHash Meta::Tag::TagGuesser::guessTagsByScheme( const QString &fileName, const QString &scheme, bool cutTrailingSpaces, bool convertUnderscores, bool isRegExp ) { Meta::FieldHash metadata; QRegExp rx; QString m_fileName = fileName; QString m_scheme = scheme; QList< qint64 > tokens = parseTokens( m_scheme ); // Screen all special symbols if( !isRegExp ) - m_scheme = m_scheme.replace( QRegExp( "([~!\\^&*()\\-+\\[\\]{}\\\\:\"?\\.])" ),"\\\\1" ); + m_scheme = m_scheme.replace( QRegExp( QStringLiteral("([~!\\^&*()\\-+\\[\\]{}\\\\:\"?\\.])" )), QStringLiteral("\\\\1") ); - QRegExp spaces( "(\\s+)" ); + QRegExp spaces( QStringLiteral("(\\s+)") ); rx.setPattern( m_scheme.replace( spaces, QStringLiteral("\\s+") ) .replace( m_digitalFields, QStringLiteral("(\\d+)") ) .replace( m_literalFields, QStringLiteral("(.+)") ) .replace( QLatin1String("%ignore%"), QLatin1String("(?:.+)") ) ); if( !rx.exactMatch( m_fileName ) ) return metadata; QString value; for( int i = 0; i < tokens.count(); i++ ) { value = rx.cap( i + 1 ); if( convertUnderscores ) - value.replace( '_', ' ' ); + value.replace( QLatin1Char('_'), QLatin1Char(' ') ); if( cutTrailingSpaces ) value = value.trimmed(); metadata.insert( tokens[i], value ); } return metadata; } Meta::FieldHash Meta::Tag::TagGuesser::guessTags( const QString &fileName ) { QString tmpStr = fileName; int pos = 0; - if( ( pos = fileName.lastIndexOf( '/' ) ) != -1 ) + if( ( pos = fileName.lastIndexOf( QLatin1Char('/') ) ) != -1 ) tmpStr = fileName.mid( pos + 1 ); foreach( const QString &scheme, m_schemes ) { Meta::FieldHash metadata = guessTagsByScheme( tmpStr, scheme, true, true, true ); if( !metadata.isEmpty() ) return metadata; } return Meta::FieldHash(); } diff --git a/shared/collectionscanner/BatchFile.cpp b/shared/collectionscanner/BatchFile.cpp index ab42ba2680..5b81a2a3dd 100644 --- a/shared/collectionscanner/BatchFile.cpp +++ b/shared/collectionscanner/BatchFile.cpp @@ -1,155 +1,155 @@ /*************************************************************************** * Copyright (C) 2010 Ralf Engels * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "BatchFile.h" #include "Version.h" // for AMAROK_VERSION #include #include #include #include #include CollectionScanner::BatchFile::BatchFile() { } CollectionScanner::BatchFile::BatchFile( const QString &batchPath ) { QFile batchFile( batchPath ); if( !batchFile.exists() || !batchFile.open( QIODevice::ReadOnly ) ) return; QString path; uint mtime = 0; bool haveMtime = false; QXmlStreamReader reader( &batchFile ); // very simple parser while (!reader.atEnd()) { reader.readNext(); if( reader.isStartElement() ) { QStringRef name = reader.name(); if( name == QLatin1String("scanner") ) { ; // just recurse into the element } else if( name == QLatin1String("directory") ) { path.clear(); mtime = 0; haveMtime = false; } else if( name == QLatin1String("path") ) path = reader.readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("mtime") ) { mtime = reader.readElementText(QXmlStreamReader::SkipChildElements).toUInt(); haveMtime = true; } else { reader.skipCurrentElement(); } } else if( reader.isEndElement() ) { QStringRef name = reader.name(); if( name == QLatin1String("directory") ) { if( !path.isEmpty() ) { if( haveMtime ) m_timeDefinitions.append( TimeDefinition( path, mtime ) ); else m_directories.append( path ); } } } } } const QStringList& CollectionScanner::BatchFile::directories() const { return m_directories; } void CollectionScanner::BatchFile::setDirectories( const QStringList &value ) { m_directories = value; } const QList& CollectionScanner::BatchFile::timeDefinitions() const { return m_timeDefinitions; } void CollectionScanner::BatchFile::setTimeDefinitions( const QList &value ) { m_timeDefinitions = value; } bool CollectionScanner::BatchFile::write( const QString &batchPath ) { QFile batchFile( batchPath ); if( !batchFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) return false; QXmlStreamWriter writer( &batchFile ); writer.setAutoFormatting( true ); writer.writeStartDocument(); writer.writeStartElement( QStringLiteral("scanner") ); - writer.writeComment("Batch file for amarokcollectionscanner " AMAROK_VERSION " created on "+QDateTime::currentDateTime().toString()); + writer.writeComment(QStringLiteral("Batch file for amarokcollectionscanner ") + QLatin1String(AMAROK_VERSION) + QStringLiteral(" created on ") + QDateTime::currentDateTime().toString()); foreach( const QString &dir, m_directories ) { writer.writeStartElement( QStringLiteral("directory") ); writer.writeTextElement( QStringLiteral("path"), dir ); writer.writeEndElement(); } foreach( const TimeDefinition &pair, m_timeDefinitions ) { QString path( pair.first ); uint mtime = pair.second; writer.writeStartElement( QStringLiteral("directory") ); writer.writeTextElement( QStringLiteral("path"), path ); // note: some file systems return an mtime of 0 writer.writeTextElement( QStringLiteral("mtime"), QString::number( mtime ) ); writer.writeEndElement(); } writer.writeEndElement(); writer.writeEndDocument(); return true; } diff --git a/shared/collectionscanner/Directory.cpp b/shared/collectionscanner/Directory.cpp index 79c79a33a8..6077733e8c 100644 --- a/shared/collectionscanner/Directory.cpp +++ b/shared/collectionscanner/Directory.cpp @@ -1,247 +1,247 @@ /*************************************************************************** * Copyright (C) 2003-2005 Max Howell * * (C) 2003-2010 Mark Kretschmann * * (C) 2005-2007 Alexandre Oliveira * * (C) 2008 Dan Meltzer * * (C) 2008-2009 Jeff Mitchell * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "Directory.h" #include "collectionscanner/ScanningState.h" #include "collectionscanner/Track.h" #include "collectionscanner/utils.h" #include #include #include #include #include #include #include #include #include #include CollectionScanner::Directory::Directory( const QString &path, CollectionScanner::ScanningState *state, bool skip ) : m_ignored( false ) { m_path = path; m_rpath = QDir::current().relativeFilePath( path ); m_mtime = QFileInfo( path ).lastModified().toTime_t(); m_skipped = skip; if( m_skipped ) return; QDir dir( path ); if( dir.exists( QStringLiteral("fmps_ignore") ) ) { m_ignored = true; return; } QStringList validImages; validImages << QStringLiteral("jpg") << QStringLiteral("png") << QStringLiteral("gif") << QStringLiteral("jpeg") << QStringLiteral("bmp") << QStringLiteral("svg") << QStringLiteral("xpm"); QStringList validPlaylists; validPlaylists << QStringLiteral("m3u") << QStringLiteral("pls") << QStringLiteral("xspf"); // --- check if we were restarted and failed at a file QStringList badFiles; if( state->lastDirectory() == path ) { badFiles << state->badFiles(); QString lastFile = state->lastFile(); if( !lastFile.isEmpty() ) { badFiles << state->lastFile(); state->setBadFiles( badFiles ); } } else state->setLastDirectory( path ); state->setLastFile( QString() ); // reset so we don't add a leftover file dir.setFilter( QDir::NoDotAndDotDot | QDir::Files ); QFileInfoList fileInfos = dir.entryInfoList(); foreach( const QFileInfo &fi, fileInfos ) { if( !fi.exists() ) continue; const QFileInfo &f = fi.isSymLink() ? QFileInfo( fi.symLinkTarget() ) : fi; if( badFiles.contains( f.absoluteFilePath() ) ) continue; const QString suffix = fi.suffix().toLower(); const QString filePath = f.absoluteFilePath(); // -- cover image ? if( validImages.contains( suffix ) ) m_covers.append( filePath ); // -- playlist ? else if( validPlaylists.contains( suffix ) ) m_playlists.append( CollectionScanner::Playlist( filePath ) ); // -- audio track ? else { // remember the last file before it get's dangerous. Before starting taglib state->setLastFile( f.absoluteFilePath() ); CollectionScanner::Track *newTrack = new CollectionScanner::Track( filePath, this ); if( newTrack->isValid() ) m_tracks.append( newTrack ); else delete newTrack; } } } CollectionScanner::Directory::Directory( QXmlStreamReader *reader ) : m_mtime( 0 ) , m_skipped( false ) , m_ignored( false ) { // improve scanner with skipCurrentElement as soon as Amarok requires Qt 4.6 while (!reader->atEnd()) { reader->readNext(); if( reader->isStartElement() ) { QStringRef name = reader->name(); - if( name == "path" ) + if( name == QLatin1String("path") ) m_path = reader->readElementText(QXmlStreamReader::SkipChildElements); - else if( name == "rpath" ) + else if( name == QLatin1String("rpath") ) m_rpath = reader->readElementText(QXmlStreamReader::SkipChildElements); - else if( name == "mtime" ) + else if( name == QLatin1String("mtime") ) m_mtime = reader->readElementText(QXmlStreamReader::SkipChildElements).toUInt(); - else if( name == "cover" ) + else if( name == QLatin1String("cover") ) m_covers.append(reader->readElementText(QXmlStreamReader::SkipChildElements)); - else if( name == "skipped" ) + else if( name == QLatin1String("skipped") ) { m_skipped = true; reader->skipCurrentElement(); } - else if( name == "ignored" ) + else if( name == QLatin1String("ignored") ) { m_ignored = true; reader->skipCurrentElement(); } - else if( name == "track" ) + else if( name == QLatin1String("track") ) m_tracks.append( new CollectionScanner::Track( reader, this ) ); - else if( name == "playlist" ) + else if( name == QLatin1String("playlist") ) m_playlists.append( CollectionScanner::Playlist( reader ) ); else { qDebug() << "Unexpected xml start element"<skipCurrentElement(); } } else if( reader->isEndElement() ) { break; } } } CollectionScanner::Directory::~Directory() { foreach( CollectionScanner::Track *track, m_tracks ) delete track; } QString CollectionScanner::Directory::path() const { return m_path; } QString CollectionScanner::Directory::rpath() const { return m_rpath; } uint CollectionScanner::Directory::mtime() const { return m_mtime; } bool CollectionScanner::Directory::isSkipped() const { return m_skipped; } const QStringList& CollectionScanner::Directory::covers() const { return m_covers; } const QList& CollectionScanner::Directory::tracks() const { return m_tracks; } const QList& CollectionScanner::Directory::playlists() const { return m_playlists; } void CollectionScanner::Directory::toXml( QXmlStreamWriter *writer ) const { writer->writeTextElement( QStringLiteral("path"), escapeXml10(m_path) ); writer->writeTextElement( QStringLiteral("rpath"), escapeXml10(m_rpath) ); writer->writeTextElement( QStringLiteral("mtime"), QString::number( m_mtime ) ); if( m_skipped ) writer->writeEmptyElement( QStringLiteral("skipped") ); if( m_ignored ) writer->writeEmptyElement( QStringLiteral("ignored") ); foreach( const QString &cover, m_covers ) { writer->writeTextElement( QStringLiteral("cover"), escapeXml10(cover) ); } foreach( CollectionScanner::Track *track, m_tracks ) { writer->writeStartElement( QStringLiteral("track") ); track->toXml( writer ); writer->writeEndElement(); } foreach( const CollectionScanner::Playlist &playlist, m_playlists ) { writer->writeStartElement( QStringLiteral("playlist") ); playlist.toXml( writer ); writer->writeEndElement(); } } diff --git a/shared/collectionscanner/Track.cpp b/shared/collectionscanner/Track.cpp index f8c430d856..77e2d03a5f 100644 --- a/shared/collectionscanner/Track.cpp +++ b/shared/collectionscanner/Track.cpp @@ -1,530 +1,530 @@ /*************************************************************************** * Copyright (C) 2003-2005 Max Howell * * (C) 2003-2010 Mark Kretschmann * * (C) 2005-2007 Alexandre Oliveira * * (C) 2008 Dan Meltzer * * (C) 2008-2009 Jeff Mitchell * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "Track.h" #include "utils.h" #include "MetaTagLib.h" #include "MetaReplayGain.h" #include #include #include #include #include #include bool CollectionScanner::Track::s_useCharsetDetector = false; CollectionScanner::Track::Track( const QString &path, CollectionScanner::Directory* directory ) : m_valid( true ) , m_directory( directory ) , m_filetype( Amarok::Unknown ) , m_compilation( false ) , m_noCompilation( false ) , m_hasCover( false ) , m_year( -1 ) , m_disc( -1 ) , m_track( -1 ) , m_bpm( -1.0 ) , m_bitrate( -1 ) , m_length( -1.0 ) , m_samplerate( -1 ) , m_filesize( -1 ) , m_trackGain( -1.0 ) , m_trackPeakGain( -1.0 ) , m_albumGain( -1.0 ) , m_albumPeakGain( -1.0 ) , m_rating( -1.0 ) , m_score( -1.0 ) , m_playcount( -1.0 ) { static const int MAX_SENSIBLE_LENGTH = 1023; // the maximum length for normal strings. // in corner cases a longer string might cause problems see BUG:276894 // for the unit test. // in a debug build a file called "crash_amarok_here.ogg" will crash the collection // scanner if( path.contains(QLatin1String("crash_amarok_here.ogg")) ) { qDebug() << "Crashing at"<atEnd()) { reader->readNext(); if( reader->isStartElement() ) { QStringRef name = reader->name(); if( name == QLatin1String("uniqueid") ) m_uniqueid = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("path") ) m_path = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("rpath") ) m_rpath = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("filetype") ) m_filetype = (Amarok::FileType)reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("title") ) m_title = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("artist") ) m_artist = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("albumArtist") ) m_albumArtist = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("album") ) m_album = reader->readElementText(); else if( name == QLatin1String("compilation") ) { m_compilation = true; reader->skipCurrentElement(); } else if( name == QLatin1String("noCompilation") ) { m_noCompilation = true; reader->skipCurrentElement(); } else if( name == QLatin1String("hasCover") ) { m_hasCover = true; reader->skipCurrentElement(); } else if( name == QLatin1String("comment") ) m_comment = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("genre") ) m_genre = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("year") ) m_year = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("disc") ) m_disc = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("track") ) m_track = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("bpm") ) m_bpm = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("bitrate") ) m_bitrate = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("length") ) m_length = reader->readElementText(QXmlStreamReader::SkipChildElements).toLong(); else if( name == QLatin1String("samplerate") ) m_samplerate = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("filesize") ) m_filesize = reader->readElementText(QXmlStreamReader::SkipChildElements).toLong(); else if( name == QLatin1String("mtime") ) m_modified = QDateTime::fromTime_t(reader->readElementText(QXmlStreamReader::SkipChildElements).toLong()); else if( name == QLatin1String("trackGain") ) m_trackGain = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("trackPeakGain") ) m_trackPeakGain = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("albumGain") ) m_albumGain = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("albumPeakGain") ) m_albumPeakGain = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("composer") ) m_composer = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("rating") ) m_rating = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("score") ) m_score = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("playcount") ) m_playcount = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else { qDebug() << "Unexpected xml start element"<skipCurrentElement(); } } else if( reader->isEndElement() ) { break; } } } void CollectionScanner::Track::write( QXmlStreamWriter *writer, const QString &tag, const QString &str ) const { if( !str.isEmpty() ) writer->writeTextElement( tag, escapeXml10(str) ); } void CollectionScanner::Track::toXml( QXmlStreamWriter *writer ) const { if( !m_valid ) return; write( writer, QStringLiteral("uniqueid"), m_uniqueid ); write( writer, QStringLiteral("path"), m_path ); write( writer, QStringLiteral("rpath"), m_rpath ); write(writer, QStringLiteral("filetype"), QString::number( (int)m_filetype ) ); write( writer, QStringLiteral("title"), m_title); write( writer, QStringLiteral("artist"), m_artist); write( writer, QStringLiteral("albumArtist"), m_albumArtist); write( writer, QStringLiteral("album"), m_album); if( m_compilation ) writer->writeEmptyElement( QStringLiteral("compilation") ); if( m_noCompilation ) writer->writeEmptyElement( QStringLiteral("noCompilation") ); if( m_hasCover ) writer->writeEmptyElement( QStringLiteral("hasCover") ); write( writer, QStringLiteral("comment"), m_comment); write( writer, QStringLiteral("genre"), m_genre); if( m_year != -1 ) write(writer, QStringLiteral("year"), QString::number( m_year ) ); if( m_disc != -1 ) write(writer, QStringLiteral("disc"), QString::number( m_disc ) ); if( m_track != -1 ) write(writer, QStringLiteral("track"), QString::number( m_track ) ); if( m_bpm != -1 ) write(writer, QStringLiteral("bpm"), QString::number( m_bpm ) ); if( m_bitrate != -1 ) write(writer, QStringLiteral("bitrate"), QString::number( m_bitrate ) ); if( m_length != -1 ) write(writer, QStringLiteral("length"), QString::number( m_length ) ); if( m_samplerate != -1 ) write(writer, QStringLiteral("samplerate"), QString::number( m_samplerate ) ); if( m_filesize != -1 ) write(writer, QStringLiteral("filesize"), QString::number( m_filesize ) ); if( m_modified.isValid() ) write(writer, QStringLiteral("mtime"), QString::number( m_modified.toTime_t() ) ); if( m_trackGain != 0 ) write(writer, QStringLiteral("trackGain"), QString::number( m_trackGain ) ); if( m_trackPeakGain != 0 ) write(writer, QStringLiteral("trackPeakGain"), QString::number( m_trackPeakGain ) ); if( m_albumGain != 0 ) write(writer, QStringLiteral("albumGain"), QString::number( m_albumGain ) ); if( m_albumPeakGain != 0 ) write(writer, QStringLiteral("albumPeakGain"), QString::number( m_albumPeakGain ) ); write( writer, QStringLiteral("composer"), m_composer); if( m_rating != -1 ) write(writer, QStringLiteral("rating"), QString::number( m_rating ) ); if( m_score != -1 ) write(writer, QStringLiteral("score"), QString::number( m_score ) ); if( m_playcount != -1 ) write(writer, QStringLiteral("playcount"), QString::number( m_playcount ) ); } bool CollectionScanner::Track::isValid() const { return m_valid; } CollectionScanner::Directory* CollectionScanner::Track::directory() const { return m_directory; } QString CollectionScanner::Track::uniqueid() const { return m_uniqueid; } QString CollectionScanner::Track::path() const { return m_path; } QString CollectionScanner::Track::rpath() const { return m_rpath; } Amarok::FileType CollectionScanner::Track::filetype() const { return m_filetype; } QString CollectionScanner::Track::title() const { return m_title; } QString CollectionScanner::Track::artist() const { return m_artist; } QString CollectionScanner::Track::albumArtist() const { return m_albumArtist; } QString CollectionScanner::Track::album() const { return m_album; } bool CollectionScanner::Track::isCompilation() const { return m_compilation; } bool CollectionScanner::Track::isNoCompilation() const { return m_noCompilation; } bool CollectionScanner::Track::hasCover() const { return m_hasCover; } QString CollectionScanner::Track::comment() const { return m_comment; } QString CollectionScanner::Track::genre() const { return m_genre; } int CollectionScanner::Track::year() const { return m_year; } int CollectionScanner::Track::disc() const { return m_disc; } int CollectionScanner::Track::track() const { return m_track; } int CollectionScanner::Track::bpm() const { return m_bpm; } int CollectionScanner::Track::bitrate() const { return m_bitrate; } qint64 CollectionScanner::Track::length() const { return m_length; } int CollectionScanner::Track::samplerate() const { return m_samplerate; } qint64 CollectionScanner::Track::filesize() const { return m_filesize; } QDateTime CollectionScanner::Track::modified() const { return m_modified; } QString CollectionScanner::Track::composer() const { return m_composer; } qreal CollectionScanner::Track::replayGain( Meta::ReplayGainTag mode ) const { switch( mode ) { case Meta::ReplayGain_Track_Gain: return m_trackGain; case Meta::ReplayGain_Track_Peak: return m_trackPeakGain; case Meta::ReplayGain_Album_Gain: return m_albumGain; case Meta::ReplayGain_Album_Peak: return m_albumPeakGain; } return 0.0; } qreal CollectionScanner::Track::rating() const { return m_rating; } qreal CollectionScanner::Track::score() const { return m_score; } int CollectionScanner::Track::playcount() const { return m_playcount; } void CollectionScanner::Track::setUseCharsetDetector( bool value ) { s_useCharsetDetector = value; } diff --git a/shared/tag_helpers/TagHelper.cpp b/shared/tag_helpers/TagHelper.cpp index b196e928f6..bf55da0e76 100644 --- a/shared/tag_helpers/TagHelper.cpp +++ b/shared/tag_helpers/TagHelper.cpp @@ -1,381 +1,381 @@ /**************************************************************************************** * Copyright (c) 2010 Sergey Ivanov <123kash@gmail.com> * * * * 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 "TagHelper.h" #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #include #include #include #include #include #include #include #include #ifdef TAGLIB_OPUS_FOUND #include #endif #include #include #include #include #include #include #include #ifdef TAGLIB_MOD_FOUND #include #include #include #include #endif #pragma GCC diagnostic pop #include "APETagHelper.h" #include "ASFTagHelper.h" #include "ID3v2TagHelper.h" #include "MP4TagHelper.h" #include "VorbisCommentTagHelper.h" #include "StringHelper.h" using namespace Meta::Tag; TagHelper::TagHelper( TagLib::Tag *tag, Amarok::FileType fileType ) : m_tag( tag ) , m_fileType( fileType ) { } TagHelper::TagHelper( TagLib::ID3v1::Tag *tag, Amarok::FileType fileType ) : m_tag( tag ) , m_fileType( fileType ) { } TagHelper::~TagHelper() { } Meta::FieldHash TagHelper::tags() const { Meta::FieldHash data; data.insert( Meta::valTitle, TStringToQString( m_tag->title() ) ); data.insert( Meta::valArtist, TStringToQString( m_tag->artist() ) ); data.insert( Meta::valAlbum, TStringToQString( m_tag->album() ) ); data.insert( Meta::valTrackNr, m_tag->track() ); data.insert( Meta::valYear, m_tag->year() ); data.insert( Meta::valGenre, TStringToQString( m_tag->genre() ) ); data.insert( Meta::valComment, TStringToQString( m_tag->comment() ) ); return data; } bool TagHelper::setTags( const Meta::FieldHash &changes ) { bool modified = false; if( changes.contains( Meta::valTitle ) ) { m_tag->setTitle( Qt4QStringToTString( changes.value( Meta::valTitle ).toString() ) ); modified = true; } if( changes.contains( Meta::valArtist ) ) { m_tag->setArtist( Qt4QStringToTString( changes.value( Meta::valArtist ).toString() ) ); modified = true; } if( changes.contains( Meta::valAlbum ) ) { m_tag->setAlbum( Qt4QStringToTString( changes.value( Meta::valAlbum ).toString() ) ); modified = true; } if( changes.contains( Meta::valTrackNr ) ) { m_tag->setTrack( changes.value( Meta::valTrackNr ).toUInt() ); modified = true; } if( changes.contains( Meta::valYear ) ) { m_tag->setYear( changes.value( Meta::valYear ).toUInt() ); modified = true; } if( changes.contains( Meta::valGenre ) ) { m_tag->setGenre( Qt4QStringToTString( changes.value( Meta::valGenre ).toString() ) ); modified = true; } if( changes.contains( Meta::valComment ) ) { m_tag->setComment( Qt4QStringToTString( changes.value( Meta::valComment ).toString() ) ); modified = true; } return modified; } TagLib::ByteVector TagHelper::render() const { QByteArray byteArray; QDataStream stream( &byteArray, QIODevice::WriteOnly ); stream << tags(); return TagLib::ByteVector( byteArray.constData(), byteArray.size() ); } bool TagHelper::hasEmbeddedCover() const { return false; } QImage TagHelper::embeddedCover() const { return QImage(); } bool TagHelper::setEmbeddedCover( const QImage &cover ) { Q_UNUSED( cover ) return false; } TagLib::String TagHelper::fieldName( const qint64 field ) const { return m_fieldMap.value( field ); } qint64 TagHelper::fieldName( const TagLib::String &field ) const { return m_fieldMap.key( field ); } QPair< TagHelper::UIDType, QString > TagHelper::splitUID( const QString &uidUrl ) const { TagHelper::UIDType type = UIDInvalid; QString uid = uidUrl; if( uid.startsWith( QLatin1String("amarok-") ) ) - uid = uid.remove( QRegExp( "^(amarok-\\w+://).+$" ) ); + uid = uid.remove( QRegExp( QStringLiteral("^(amarok-\\w+://).+$") ) ); if( isValidUID( uid, UIDAFT ) ) type = UIDAFT; return qMakePair( type, uid ); } QPair< int, int > TagHelper::splitDiscNr( const QString &value ) const { int disc; int count = 0; - if( value.indexOf( '/' ) != -1 ) + if( value.indexOf( QLatin1Char('/') ) != -1 ) { - QStringList list = value.split( '/', QString::SkipEmptyParts ); + QStringList list = value.split( QLatin1Char('/'), QString::SkipEmptyParts ); disc = list.value( 0 ).toInt(); count = list.value( 1 ).toInt(); } - else if( value.indexOf( ':' ) != -1 ) + else if( value.indexOf( QLatin1Char(':') ) != -1 ) { - QStringList list = value.split( ':', QString::SkipEmptyParts ); + QStringList list = value.split( QLatin1Char(':'), QString::SkipEmptyParts ); disc = list.value( 0 ).toInt(); count = list.value( 1 ).toInt(); } else disc = value.toInt(); return qMakePair( disc, count ); } bool TagHelper::isValidUID( const QString &uid, const TagHelper::UIDType type ) const { if( uid.length() >= 127 ) // the database can't handle longer uids return false; - QRegExp regexp( "^$" ); + QRegExp regexp( QStringLiteral("^$") ); if( type == UIDAFT ) regexp.setPattern( QStringLiteral("^[0-9a-fA-F]{32}$") ); return regexp.exactMatch( uid ); } TagLib::String TagHelper::uidFieldName( const TagHelper::UIDType type ) const { return m_uidFieldMap.value( type ); } TagLib::String TagHelper::fmpsFieldName( const TagHelper::FMPS field ) const { return m_fmpsFieldMap.value( field ); } Amarok::FileType TagHelper::fileType() const { return m_fileType; } QByteArray TagHelper::testString() const { TagLib::String string = m_tag->album() + m_tag->artist() + m_tag->comment() + m_tag->genre() + m_tag->title(); return QByteArray( string.toCString( true ) ); } Meta::Tag::TagHelper * Meta::Tag::selectHelper( const TagLib::FileRef &fileref, bool forceCreation ) { TagHelper *tagHelper = nullptr; if( TagLib::MPEG::File *file = dynamic_cast< TagLib::MPEG::File * >( fileref.file() ) ) { if( file->ID3v2Tag( forceCreation ) ) tagHelper = new ID3v2TagHelper( fileref.tag(), file->ID3v2Tag(), Amarok::Mp3 ); else if( file->APETag() ) tagHelper = new APETagHelper( fileref.tag(), file->APETag(), Amarok::Mp3 ); else if( file->ID3v1Tag() ) tagHelper = new TagHelper( fileref.tag(), Amarok::Mp3 ); } else if( TagLib::Ogg::Vorbis::File *file = dynamic_cast< TagLib::Ogg::Vorbis::File * >( fileref.file() ) ) { if( file->tag() ) tagHelper = new VorbisCommentTagHelper( fileref.tag(), file->tag(), Amarok::Ogg ); } else if( TagLib::Ogg::FLAC::File *file = dynamic_cast< TagLib::Ogg::FLAC::File * >( fileref.file() ) ) { if( file->tag() ) tagHelper = new VorbisCommentTagHelper( fileref.tag(), file->tag(), Amarok::Flac ); } else if( TagLib::Ogg::Speex::File *file = dynamic_cast< TagLib::Ogg::Speex::File * >( fileref.file() ) ) { if( file->tag() ) tagHelper = new VorbisCommentTagHelper( fileref.tag(), file->tag(), Amarok::Speex ); } #ifdef TAGLIB_OPUS_FOUND else if( TagLib::Ogg::Opus::File *file = dynamic_cast< TagLib::Ogg::Opus::File * >( fileref.file() ) ) { if( file->tag() ) tagHelper = new VorbisCommentTagHelper( fileref.tag(), file->tag(), Amarok::Opus ); } #endif else if( TagLib::FLAC::File *file = dynamic_cast< TagLib::FLAC::File * >( fileref.file() ) ) { if( file->xiphComment() ) tagHelper = new VorbisCommentTagHelper( fileref.tag(), file->xiphComment(), Amarok::Flac, file ); else if( file->ID3v2Tag() ) tagHelper = new ID3v2TagHelper( fileref.tag(), file->ID3v2Tag(), Amarok::Flac ); else if( file->ID3v1Tag() ) tagHelper = new TagHelper( fileref.tag(), Amarok::Flac ); } else if( TagLib::MP4::File *file = dynamic_cast< TagLib::MP4::File * >( fileref.file() ) ) { TagLib::MP4::Tag *tag = dynamic_cast< TagLib::MP4::Tag * >( file->tag() ); if( tag ) { Amarok::FileType specificType = Amarok::Mp4; - QString filename = QString( fileref.file()->name() ); + QString filename = QString::fromLatin1( fileref.file()->name() ); foreach( Amarok::FileType type, QList() << Amarok::M4a << Amarok::M4v ) { if( filename.endsWith( Amarok::FileTypeSupport::toString( type ), Qt::CaseInsensitive ) ) specificType = type; } tagHelper = new MP4TagHelper( fileref.tag(), tag, specificType ); } } else if( TagLib::MPC::File *file = dynamic_cast< TagLib::MPC::File * >( fileref.file() ) ) { if( file->APETag( forceCreation ) ) tagHelper = new APETagHelper( fileref.tag(), file->APETag(), Amarok::Mpc ); else if( file->ID3v1Tag() ) tagHelper = new TagHelper( fileref.tag(), Amarok::Mpc ); } else if( TagLib::RIFF::AIFF::File *file = dynamic_cast< TagLib::RIFF::AIFF::File * >( fileref.file() ) ) { if( file->tag() ) tagHelper = new ID3v2TagHelper( fileref.tag(), file->tag(), Amarok::Aiff ); } else if( TagLib::RIFF::WAV::File *file = dynamic_cast< TagLib::RIFF::WAV::File * >( fileref.file() ) ) { if( file->tag() ) tagHelper = new ID3v2TagHelper( fileref.tag(), file->tag(), Amarok::Wav ); } else if( TagLib::ASF::File *file = dynamic_cast< TagLib::ASF::File * >( fileref.file() ) ) { TagLib::ASF::Tag *tag = dynamic_cast< TagLib::ASF::Tag * >( file->tag() ); if( tag ) tagHelper = new ASFTagHelper( fileref.tag(), tag, Amarok::Wma ); } else if( TagLib::TrueAudio::File *file = dynamic_cast< TagLib::TrueAudio::File * >( fileref.file() ) ) { if( file->ID3v2Tag( forceCreation ) ) tagHelper = new ID3v2TagHelper( fileref.tag(), file->ID3v2Tag(), Amarok::TrueAudio ); else if( file->ID3v1Tag() ) tagHelper = new TagHelper( fileref.tag(), Amarok::TrueAudio ); } else if( TagLib::WavPack::File *file = dynamic_cast< TagLib::WavPack::File * >( fileref.file() ) ) { if( file->APETag( forceCreation ) ) tagHelper = new APETagHelper( fileref.tag(), file->APETag(), Amarok::WavPack ); else if( file->ID3v1Tag() ) tagHelper = new TagHelper( fileref.tag(), Amarok::WavPack ); } #ifdef TAGLIB_MOD_FOUND else if( TagLib::Mod::File *file = dynamic_cast< TagLib::Mod::File * >( fileref.file() ) ) { if( file->tag() ) tagHelper = new TagHelper( fileref.tag(), Amarok::Mod ); } else if( TagLib::S3M::File *file = dynamic_cast< TagLib::S3M::File * >( fileref.file() ) ) { if( file->tag() ) tagHelper = new TagHelper( fileref.tag(), Amarok::S3M ); } else if( TagLib::IT::File *file = dynamic_cast< TagLib::IT::File * >( fileref.file() ) ) { if( file->tag() ) tagHelper = new TagHelper( fileref.tag(), Amarok::IT ); } else if( TagLib::XM::File *file = dynamic_cast< TagLib::XM::File * >( fileref.file() ) ) { if( file->tag() ) tagHelper = new TagHelper( fileref.tag(), Amarok::XM ); } #endif return tagHelper; } diff --git a/src/EngineController.cpp b/src/EngineController.cpp index 603af793aa..331fe9d461 100644 --- a/src/EngineController.cpp +++ b/src/EngineController.cpp @@ -1,1376 +1,1376 @@ /**************************************************************************************** * Copyright (c) 2004 Frederik Holljen * * Copyright (c) 2004,2005 Max Howell * * Copyright (c) 2004-2013 Mark Kretschmann * * Copyright (c) 2006,2008 Ian Monroe * * Copyright (c) 2008 Jason A. Donenfeld * * Copyright (c) 2009 Nikolaj Hald Nielsen * * Copyright (c) 2009 Artur Szymiec * * * * 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 "EngineController" #include "EngineController.h" #include "MainWindow.h" #include "amarokconfig.h" #include "core-impl/collections/support/CollectionManager.h" #include "core/capabilities/MultiPlayableCapability.h" #include "core/capabilities/MultiSourceCapability.h" #include "core/capabilities/SourceInfoCapability.h" #include "core/logger/Logger.h" #include "core/meta/Meta.h" #include "core/meta/support/MetaConstants.h" #include "core/meta/support/MetaUtility.h" #include "core/support/Amarok.h" #include "core/support/Components.h" #include "core/support/Debug.h" #include "playback/DelayedDoers.h" #include "playback/Fadeouter.h" #include "playback/PowerManager.h" #include "playlist/PlaylistActions.h" #include #include #include #include #include #include #include #include #include #include // for slotMetaDataChanged() typedef QPair FieldPair; namespace The { EngineController* engineController() { return EngineController::instance(); } } EngineController * EngineController::instance() { return Amarok::Components::engineController(); } EngineController::EngineController() : m_boundedPlayback( 0 ) , m_multiPlayback( 0 ) , m_multiSource( 0 ) , m_playWhenFetched( true ) , m_volume( 0 ) , m_currentAudioCdTrack( 0 ) , m_pauseTimer( new QTimer( this ) ) , m_lastStreamStampPosition( -1 ) , m_ignoreVolumeChangeAction ( false ) , m_ignoreVolumeChangeObserve ( false ) , m_tickInterval( 0 ) , m_lastTickPosition( -1 ) , m_lastTickCount( 0 ) , m_mutex( QMutex::Recursive ) { DEBUG_BLOCK // ensure this object is created in a main thread Q_ASSERT( thread() == QCoreApplication::instance()->thread() ); connect( this, &EngineController::fillInSupportedMimeTypes, this, &EngineController::slotFillInSupportedMimeTypes ); connect( this, &EngineController::trackFinishedPlaying, this, &EngineController::slotTrackFinishedPlaying ); new PowerManager( this ); // deals with inhibiting suspend etc. m_pauseTimer->setSingleShot( true ); connect( m_pauseTimer, &QTimer::timeout, this, &EngineController::slotPause ); m_equalizerController = new EqualizerController( this ); } EngineController::~EngineController() { DEBUG_BLOCK //we like to know when singletons are destroyed // don't do any of the after-processing that normally happens when // the media is stopped - that's what endSession() is for if( m_media ) { m_media->blockSignals(true); m_media->stop(); } delete m_boundedPlayback; m_boundedPlayback = 0; delete m_multiPlayback; // need to get a new instance of multi if played again m_multiPlayback = 0; delete m_media.data(); delete m_audio.data(); delete m_audioDataOutput.data(); } void EngineController::initializePhonon() { DEBUG_BLOCK m_path.disconnect(); m_dataPath.disconnect(); // QWeakPointers reset themselves to null if the object is deleted delete m_media.data(); delete m_controller.data(); delete m_audio.data(); delete m_audioDataOutput.data(); delete m_preamp.data(); delete m_fader.data(); using namespace Phonon; PERF_LOG( "EngineController: loading phonon objects" ) m_media = new MediaObject( this ); // Enable zeitgeist support on linux //TODO: make this configurable by the user. m_media->setProperty( "PlaybackTracking", true ); m_audio = new AudioOutput( MusicCategory, this ); m_audioDataOutput = new AudioDataOutput( this ); m_audioDataOutput->setDataSize( DATAOUTPUT_DATA_SIZE ); // The number of samples that Phonon sends per signal m_path = createPath( m_media.data(), m_audio.data() ); m_controller = new MediaController( m_media.data() ); m_equalizerController->initialize( m_path ); // HACK we turn off replaygain manually on OSX, until the phonon coreaudio backend is fixed. // as the default is specified in the .cfg file, we can't just tell it to be a different default on OSX #ifdef Q_WS_MAC AmarokConfig::setReplayGainMode( AmarokConfig::EnumReplayGainMode::Off ); AmarokConfig::setFadeoutOnStop( false ); #endif // we now try to create pre-amp unconditionally, however we check that it is valid. // So now m_preamp is null equals not available at all QScopedPointer preamp( new VolumeFaderEffect( this ) ); if( preamp->isValid() ) { m_preamp = preamp.take(); m_path.insertEffect( m_preamp.data() ); } QScopedPointer fader( new VolumeFaderEffect( this ) ); if( fader->isValid() ) { fader->setFadeCurve( VolumeFaderEffect::Fade9Decibel ); m_fader = fader.take(); m_path.insertEffect( m_fader.data() ); m_dataPath = createPath( m_fader.data(), m_audioDataOutput.data() ); } else m_dataPath = createPath( m_media.data(), m_audioDataOutput.data() ); m_media.data()->setTickInterval( 100 ); m_tickInterval = m_media.data()->tickInterval(); debug() << "Tick Interval (actual): " << m_tickInterval; PERF_LOG( "EngineController: loaded phonon objects" ) // Get the next track when there is 2 seconds left on the current one. m_media.data()->setPrefinishMark( 2000 ); connect( m_media.data(), &MediaObject::finished, this, &EngineController::slotFinished ); connect( m_media.data(), &MediaObject::aboutToFinish, this, &EngineController::slotAboutToFinish ); connect( m_media.data(), &MediaObject::metaDataChanged, this, &EngineController::slotMetaDataChanged ); connect( m_media.data(), &MediaObject::stateChanged, this, &EngineController::slotStateChanged ); connect( m_media.data(), &MediaObject::tick, this, &EngineController::slotTick ); connect( m_media.data(), &MediaObject::totalTimeChanged, this, &EngineController::slotTrackLengthChanged ); connect( m_media.data(), &MediaObject::currentSourceChanged, this, &EngineController::slotNewTrackPlaying ); connect( m_media.data(), &MediaObject::seekableChanged, this, &EngineController::slotSeekableChanged ); connect( m_audio.data(), &AudioOutput::volumeChanged, this, &EngineController::slotVolumeChanged ); connect( m_audio.data(), &AudioOutput::mutedChanged, this, &EngineController::slotMutedChanged ); connect( m_audioDataOutput.data(), &AudioDataOutput::dataReady, this, &EngineController::audioDataReady ); connect( m_controller.data(), &MediaController::titleChanged, this, &EngineController::slotTitleChanged ); // Read the volume from phonon m_volume = qBound( 0, qRound(m_audio.data()->volume()*100), 100 ); if( m_currentTrack ) { unsubscribeFrom( m_currentTrack ); m_currentTrack.clear(); } if( m_currentAlbum ) { unsubscribeFrom( m_currentAlbum ); m_currentAlbum.clear(); } } ////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC ////////////////////////////////////////////////////////////////////////////////////////// QStringList EngineController::supportedMimeTypes() { // this ensures that slotFillInSupportedMimeTypes() is called in the main thread. It // will be called directly if we are called in the main thread (so that no deadlock // can occur) and indirectly if we are called in non-main thread. Q_EMIT fillInSupportedMimeTypes(); // ensure slotFillInSupportedMimeTypes() called above has already finished: m_supportedMimeTypesSemaphore.acquire(); return m_supportedMimeTypes; } void EngineController::slotFillInSupportedMimeTypes() { // we assume non-empty == already filled in if( !m_supportedMimeTypes.isEmpty() ) { // unblock waiting for the semaphore in supportedMimeTypes(): m_supportedMimeTypesSemaphore.release(); return; } QRegExp avFilter( "^(audio|video)/", Qt::CaseInsensitive ); m_supportedMimeTypes = Phonon::BackendCapabilities::availableMimeTypes().filter( avFilter ); // Add whitelist hacks // MP4 Audio Books have a different extension that KFileItem/Phonon don't grok if( !m_supportedMimeTypes.contains( "audio/x-m4b" ) ) m_supportedMimeTypes << "audio/x-m4b"; // technically, "audio/flac" is not a valid mimetype (not on IANA list), but some things expect it if( m_supportedMimeTypes.contains( "audio/x-flac" ) && !m_supportedMimeTypes.contains( "audio/flac" ) ) m_supportedMimeTypes << "audio/flac"; // technically, "audio/mp4" is the official mime type, but sometimes Phonon returns audio/x-m4a if( m_supportedMimeTypes.contains( "audio/x-m4a" ) && !m_supportedMimeTypes.contains( "audio/mp4" ) ) m_supportedMimeTypes << "audio/mp4"; // unblock waiting for the semaphore in supportedMimeTypes(). We can over-shoot // resource number so that next call to supportedMimeTypes won't have to // wait for main loop; this is however just an optimization and we could have safely // released just one resource. Note that this code-path is reached only once, so // overflow cannot happen. m_supportedMimeTypesSemaphore.release( 100000 ); } void EngineController::restoreSession() { //here we restore the session //however, do note, this is always done, KDE session management is not involved if( AmarokConfig::resumePlayback() ) { const QUrl url = QUrl::fromUserInput(AmarokConfig::resumeTrack()); Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( url ); // Only give a resume time for local files, because resuming remote protocols can have weird side effects. // See: http://bugs.kde.org/show_bug.cgi?id=172897 if( url.isLocalFile() ) play( track, AmarokConfig::resumeTime(), AmarokConfig::resumePaused() ); else play( track, 0, AmarokConfig::resumePaused() ); } } void EngineController::endSession() { //only update song stats, when we're not going to resume it if ( !AmarokConfig::resumePlayback() && m_currentTrack ) { Q_EMIT stopped( trackPositionMs(), m_currentTrack->length() ); unsubscribeFrom( m_currentTrack ); if( m_currentAlbum ) unsubscribeFrom( m_currentAlbum ); Q_EMIT trackChanged( Meta::TrackPtr( 0 ) ); } Q_EMIT sessionEnded( AmarokConfig::resumePlayback() && m_currentTrack ); } EqualizerController* EngineController::equalizerController() const { return m_equalizerController; } ////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC SLOTS ////////////////////////////////////////////////////////////////////////////////////////// void EngineController::play() //SLOT { DEBUG_BLOCK if( isPlaying() ) return; if( isPaused() ) { if( m_currentTrack && m_currentTrack->type() == "stream" ) { debug() << "This is a stream that cannot be resumed after pausing. Restarting instead."; play( m_currentTrack ); return; } else { m_pauseTimer->stop(); if( supportsFadeout() ) m_fader->setVolume( 1.0 ); m_media->play(); Q_EMIT trackPlaying( m_currentTrack ); return; } } The::playlistActions()->play(); } void EngineController::play( Meta::TrackPtr track, uint offset, bool startPaused ) { DEBUG_BLOCK if( !track ) // Guard return; // clear the current track without sending playbackEnded or trackChangeNotify yet stop( /* forceInstant */ true, /* playingWillContinue */ true ); // we grant exclusive access to setting new m_currentTrack to newTrackPlaying() m_nextTrack = track; debug() << "play: bounded is "<name(); m_boundedPlayback = track->create(); m_multiPlayback = track->create(); track->prepareToPlay(); m_nextUrl = track->playableUrl(); if( m_multiPlayback ) { connect( m_multiPlayback, &Capabilities::MultiPlayableCapability::playableUrlFetched, this, &EngineController::slotPlayableUrlFetched ); m_multiPlayback->fetchFirst(); } else if( m_boundedPlayback ) { debug() << "Starting bounded playback of url " << track->playableUrl() << " at position " << m_boundedPlayback->startPosition(); playUrl( track->playableUrl(), m_boundedPlayback->startPosition(), startPaused ); } else { debug() << "Just a normal, boring track... :-P"; playUrl( track->playableUrl(), offset, startPaused ); } } void EngineController::replay() // slot { DEBUG_BLOCK seekTo( 0 ); Q_EMIT trackPositionChanged( 0, true ); } void EngineController::playUrl( const QUrl &url, uint offset, bool startPaused ) { DEBUG_BLOCK m_media->stop(); debug() << "URL: " << url << url.url(); debug() << "Offset: " << offset; m_currentAudioCdTrack = 0; if( url.scheme() == "audiocd" ) { - QStringList pathItems = url.path().split( '/', QString::KeepEmptyParts ); + QStringList pathItems = url.path().split( QLatin1Char('/'), QString::KeepEmptyParts ); if( pathItems.count() != 3 ) { error() << __PRETTY_FUNCTION__ << url.url() << "is not in expected format"; return; } bool ok = false; int trackNumber = pathItems.at( 2 ).toInt( &ok ); if( !ok || trackNumber <= 0 ) { error() << __PRETTY_FUNCTION__ << "failed to get positive track number from" << url.url(); return; } QString device = QUrlQuery(url).queryItemValue( "device" ); m_media->setCurrentSource( Phonon::MediaSource( Phonon::Cd, device ) ); m_currentAudioCdTrack = trackNumber; } else { // keep in sync with setNextTrack(), slotPlayableUrlFetched() m_media->setCurrentSource( url ); } m_media->clearQueue(); if( m_currentAudioCdTrack ) { // call to play() is asynchronous and ->setCurrentTitle() can be only called on // playing, buffering or paused media. m_media->pause(); DelayedTrackChanger *trackChanger = new DelayedTrackChanger( m_media.data(), m_controller.data(), m_currentAudioCdTrack, offset, startPaused ); connect( trackChanger, &DelayedTrackChanger::trackPositionChanged, this, &EngineController::trackPositionChanged ); } else if( offset ) { // call to play() is asynchronous and ->seek() can be only called on playing, // buffering or paused media. Calling play() would lead to audible glitches, // so call pause() that doesn't suffer from such problem. m_media->pause(); DelayedSeeker *seeker = new DelayedSeeker( m_media.data(), offset, startPaused ); connect( seeker, &DelayedSeeker::trackPositionChanged, this, &EngineController::trackPositionChanged ); } else { if( startPaused ) { m_media->pause(); } else { m_pauseTimer->stop(); if( supportsFadeout() ) m_fader->setVolume( 1.0 ); m_media->play(); } } } void EngineController::pause() //SLOT { if( supportsFadeout() && AmarokConfig::fadeoutOnPause() ) { m_fader->fadeOut( AmarokConfig::fadeoutLength() ); m_pauseTimer->start( AmarokConfig::fadeoutLength() + 500 ); return; } slotPause(); } void EngineController::slotPause() { if( supportsFadeout() && AmarokConfig::fadeoutOnPause() ) { // Reset VolumeFaderEffect to full volume m_fader->setVolume( 1.0 ); // Wait a bit before pausing the pipeline. Necessary for the new fader setting to take effect. QTimer::singleShot( 1000, m_media.data(), &Phonon::MediaObject::pause ); } else { m_media->pause(); } Q_EMIT paused(); } void EngineController::stop( bool forceInstant, bool playingWillContinue ) //SLOT { DEBUG_BLOCK /* Only do fade-out when all conditions are met: * a) instant stop is not requested * b) we aren't already in a fadeout * c) we are currently playing (not paused etc.) * d) Amarok is configured to fadeout at all * e) configured fadeout length is positive * f) Phonon fader to do it is actually available */ bool doFadeOut = !forceInstant && !m_fadeouter && m_media->state() == Phonon::PlayingState && AmarokConfig::fadeoutOnStop() && AmarokConfig::fadeoutLength() > 0 && m_fader; // let Amarok know that the previous track is no longer playing; if we will fade-out // ::stop() is called after the fade by Fadeouter. if( m_currentTrack && !doFadeOut ) { unsubscribeFrom( m_currentTrack ); if( m_currentAlbum ) unsubscribeFrom( m_currentAlbum ); const qint64 pos = trackPositionMs(); // updateStreamLength() intentionally not here, we're probably in the middle of a track const qint64 length = trackLength(); Q_EMIT trackFinishedPlaying( m_currentTrack, pos / qMax( length, pos ) ); m_currentTrack = 0; m_currentAlbum = 0; if( !playingWillContinue ) { Q_EMIT stopped( pos, length ); Q_EMIT trackChanged( m_currentTrack ); } } { QMutexLocker locker( &m_mutex ); delete m_boundedPlayback; m_boundedPlayback = 0; delete m_multiPlayback; // need to get a new instance of multi if played again m_multiPlayback = 0; m_multiSource.reset(); m_nextTrack.clear(); m_nextUrl.clear(); m_media->clearQueue(); } if( doFadeOut ) { m_fadeouter = new Fadeouter( m_media, m_fader, AmarokConfig::fadeoutLength() ); // even though we don't pass forceInstant, doFadeOut will be false because // m_fadeouter will be still valid connect( m_fadeouter.data(), &Fadeouter::fadeoutFinished, this, &EngineController::regularStop ); } else { m_media->stop(); m_media->setCurrentSource( Phonon::MediaSource() ); } } void EngineController::regularStop() { stop( false, false ); } bool EngineController::isPaused() const { return m_media->state() == Phonon::PausedState; } bool EngineController::isPlaying() const { return !isPaused() && !isStopped(); } bool EngineController::isStopped() const { return m_media->state() == Phonon::StoppedState || m_media->state() == Phonon::LoadingState || m_media->state() == Phonon::ErrorState; } void EngineController::playPause() //SLOT { DEBUG_BLOCK debug() << "PlayPause: EngineController state" << m_media->state(); if( isPlaying() ) pause(); else play(); } void EngineController::seekTo( int ms ) //SLOT { DEBUG_BLOCK if( m_media->isSeekable() ) { debug() << "seek to: " << ms; int seekTo; if( m_boundedPlayback ) { seekTo = m_boundedPlayback->startPosition() + ms; if( seekTo < m_boundedPlayback->startPosition() ) seekTo = m_boundedPlayback->startPosition(); else if( seekTo > m_boundedPlayback->startPosition() + trackLength() ) seekTo = m_boundedPlayback->startPosition() + trackLength(); } else seekTo = ms; m_media->seek( static_cast( seekTo ) ); Q_EMIT trackPositionChanged( seekTo, true ); /* User seek */ } else debug() << "Stream is not seekable."; } void EngineController::seekBy( int ms ) //SLOT { qint64 newPos = m_media->currentTime() + ms; seekTo( newPos <= 0 ? 0 : newPos ); } int EngineController::increaseVolume( int ticks ) //SLOT { return setVolume( volume() + ticks ); } int EngineController::decreaseVolume( int ticks ) //SLOT { return setVolume( volume() - ticks ); } int EngineController::setVolume( int percent ) //SLOT { percent = qBound( 0, percent, 100 ); m_volume = percent; const qreal volume = percent / 100.0; if ( !m_ignoreVolumeChangeAction && m_audio->volume() != volume ) { m_ignoreVolumeChangeObserve = true; m_audio->setVolume( volume ); AmarokConfig::setMasterVolume( percent ); Q_EMIT volumeChanged( percent ); } m_ignoreVolumeChangeAction = false; return percent; } int EngineController::volume() const { return m_volume; } bool EngineController::isMuted() const { return m_audio->isMuted(); } void EngineController::setMuted( bool mute ) //SLOT { m_audio->setMuted( mute ); // toggle mute if( !isMuted() ) setVolume( m_volume ); AmarokConfig::setMuteState( mute ); Q_EMIT muteStateChanged( mute ); } void EngineController::toggleMute() //SLOT { setMuted( !isMuted() ); } Meta::TrackPtr EngineController::currentTrack() const { return m_currentTrack; } qint64 EngineController::trackLength() const { //When starting a last.fm stream, Phonon still shows the old track's length--trust //Meta::Track over Phonon if( m_currentTrack && m_currentTrack->length() > 0 ) return m_currentTrack->length(); else return m_media->totalTime(); //may return -1 } void EngineController::setNextTrack( Meta::TrackPtr track ) { DEBUG_BLOCK if( !track ) return; track->prepareToPlay(); QUrl url = track->playableUrl(); if( url.isEmpty() ) return; QMutexLocker locker( &m_mutex ); if( isPlaying() ) { m_media->clearQueue(); // keep in sync with playUrl(), slotPlayableUrlFetched() if( url.scheme() != "audiocd" ) // we don't support gapless for CD, bug 305708 m_media->enqueue( url ); m_nextTrack = track; m_nextUrl = url; } else play( track ); } bool EngineController::isStream() { Phonon::MediaSource::Type type = Phonon::MediaSource::Invalid; if( m_media ) // type is determined purely from the MediaSource constructor used in // setCurrentSource(). For streams we use the QUrl one, see playUrl() type = m_media->currentSource().type(); return type == Phonon::MediaSource::Url || type == Phonon::MediaSource::Stream; } bool EngineController::isSeekable() const { if( m_media ) return m_media->isSeekable(); return false; } int EngineController::trackPosition() const { return trackPositionMs() / 1000; } qint64 EngineController::trackPositionMs() const { return m_media->currentTime(); } bool EngineController::supportsFadeout() const { return m_fader; } bool EngineController::supportsGainAdjustments() const { return m_preamp; } bool EngineController::supportsAudioDataOutput() const { const Phonon::AudioDataOutput out; return out.isValid(); } ////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE SLOTS ////////////////////////////////////////////////////////////////////////////////////////// void EngineController::slotTick( qint64 position ) { if( m_boundedPlayback ) { qint64 newPosition = position; Q_EMIT trackPositionChanged( static_cast( position - m_boundedPlayback->startPosition() ), false ); // Calculate a better position. Sometimes the position doesn't update // with a good resolution (for example, 1 sec for TrueAudio files in the // Xine-1.1.18 backend). This tick function, in those cases, just gets // called multiple times with the same position. We count how many // times this has been called prior, and adjust for it. if( position == m_lastTickPosition ) newPosition += ++m_lastTickCount * m_tickInterval; else m_lastTickCount = 0; m_lastTickPosition = position; //don't go beyond the stop point if( newPosition >= m_boundedPlayback->endPosition() ) { slotAboutToFinish(); } } else { m_lastTickPosition = position; Q_EMIT trackPositionChanged( static_cast( position ), false ); } } void EngineController::slotAboutToFinish() { DEBUG_BLOCK if( m_fadeouter ) { debug() << "slotAboutToFinish(): a fadeout is in progress, don't queue new track"; return; } if( m_multiPlayback ) { DEBUG_LINE_INFO m_mutex.lock(); m_playWhenFetched = false; m_mutex.unlock(); m_multiPlayback->fetchNext(); debug() << "The queue has: " << m_media->queue().size() << " tracks in it"; } else if( m_multiSource ) { debug() << "source finished, lets get the next one"; QUrl nextSource = m_multiSource->nextUrl(); if( !nextSource.isEmpty() ) { //more sources m_mutex.lock(); m_playWhenFetched = false; m_mutex.unlock(); debug() << "playing next source: " << nextSource; slotPlayableUrlFetched( nextSource ); } else if( m_media->queue().isEmpty() ) { debug() << "no more sources, skip to next track"; m_multiSource.reset(); // don't confuse slotFinished The::playlistActions()->requestNextTrack(); } } else if( m_boundedPlayback ) { debug() << "finished a track that consists of part of another track, go to next track even if this url is technically not done yet"; //stop this track, now, as the source track might go on and on, and //there might not be any more tracks in the playlist... stop( true ); The::playlistActions()->requestNextTrack(); } else if( m_media->queue().isEmpty() ) The::playlistActions()->requestNextTrack(); } void EngineController::slotFinished() { DEBUG_BLOCK // paranoia checking, m_currentTrack shouldn't really be null if( m_currentTrack ) { debug() << "Track finished completely, updating statistics"; unsubscribeFrom( m_currentTrack ); // don't bother with trackMetadataChanged() stampStreamTrackLength(); // update track length in stream for accurate scrobbling Q_EMIT trackFinishedPlaying( m_currentTrack, 1.0 ); subscribeTo( m_currentTrack ); } if( !m_multiPlayback && !m_multiSource ) { // again. at this point the track is finished so it's trackPositionMs is 0 if( !m_nextTrack && m_nextUrl.isEmpty() ) Q_EMIT stopped( m_currentTrack ? m_currentTrack->length() : 0, m_currentTrack ? m_currentTrack->length() : 0 ); unsubscribeFrom( m_currentTrack ); if( m_currentAlbum ) unsubscribeFrom( m_currentAlbum ); m_currentTrack = 0; m_currentAlbum = 0; if( !m_nextTrack && m_nextUrl.isEmpty() ) // we will the trackChanged signal later Q_EMIT trackChanged( Meta::TrackPtr() ); m_media->setCurrentSource( Phonon::MediaSource() ); } m_mutex.lock(); // in case setNextTrack is being handled right now. // Non-local urls are not enqueued so we must play them explicitly. if( m_nextTrack ) { DEBUG_LINE_INFO play( m_nextTrack ); } else if( !m_nextUrl.isEmpty() ) { DEBUG_LINE_INFO playUrl( m_nextUrl, 0 ); } else { DEBUG_LINE_INFO // possibly we are waiting for a fetch m_playWhenFetched = true; } m_mutex.unlock(); } static const qreal log10over20 = 0.1151292546497022842; // ln(10) / 20 void EngineController::slotNewTrackPlaying( const Phonon::MediaSource &source ) { DEBUG_BLOCK if( source.type() == Phonon::MediaSource::Empty ) { debug() << "Empty MediaSource (engine stop)"; return; } if( m_currentTrack ) { unsubscribeFrom( m_currentTrack ); if( m_currentAlbum ) unsubscribeFrom( m_currentAlbum ); } // only update stats if we are called for something new, some phonon back-ends (at // least phonon-gstreamer-4.6.1) call slotNewTrackPlaying twice with the same source if( m_currentTrack && ( m_nextTrack || !m_nextUrl.isEmpty() ) ) { debug() << "Previous track finished completely, updating statistics"; stampStreamTrackLength(); // update track length in stream for accurate scrobbling Q_EMIT trackFinishedPlaying( m_currentTrack, 1.0 ); if( m_multiSource ) // advance source of a multi-source track m_multiSource->setSource( m_multiSource->current() + 1 ); } m_nextUrl.clear(); if( m_nextTrack ) { // already unsubscribed m_currentTrack = m_nextTrack; m_nextTrack.clear(); m_multiSource.reset( m_currentTrack->create() ); if( m_multiSource ) { debug() << "Got a MultiSource Track with" << m_multiSource->sources().count() << "sources"; connect( m_multiSource.data(), &Capabilities::MultiSourceCapability::urlChanged, this, &EngineController::slotPlayableUrlFetched ); } } if( m_currentTrack && AmarokConfig::replayGainMode() != AmarokConfig::EnumReplayGainMode::Off ) { Meta::ReplayGainTag mode; // gain is usually negative (but may be positive) mode = ( AmarokConfig::replayGainMode() == AmarokConfig::EnumReplayGainMode::Track) ? Meta::ReplayGain_Track_Gain : Meta::ReplayGain_Album_Gain; qreal gain = m_currentTrack->replayGain( mode ); // peak is usually positive and smaller than gain (but may be negative) mode = ( AmarokConfig::replayGainMode() == AmarokConfig::EnumReplayGainMode::Track) ? Meta::ReplayGain_Track_Peak : Meta::ReplayGain_Album_Peak; qreal peak = m_currentTrack->replayGain( mode ); if( gain + peak > 0.0 ) { debug() << "Gain of" << gain << "would clip at absolute peak of" << gain + peak; gain -= gain + peak; } if( m_preamp ) { debug() << "Using gain of" << gain << "with relative peak of" << peak; // we calculate the volume change ourselves, because m_preamp->setVolumeDecibel is // a little confused about minus signs m_preamp->setVolume( qExp( gain * log10over20 ) ); } else warning() << "Would use gain of" << gain << ", but current Phonon backend" << "doesn't seem to support pre-amplifier (VolumeFaderEffect)"; } else if( m_preamp ) { m_preamp->setVolume( 1.0 ); } bool useTrackWithinStreamDetection = false; if( m_currentTrack ) { subscribeTo( m_currentTrack ); Meta::AlbumPtr m_currentAlbum = m_currentTrack->album(); if( m_currentAlbum ) subscribeTo( m_currentAlbum ); /** We only use detect-tracks-in-stream for tracks that have stream type * (exactly, we purposely exclude stream/lastfm) *and* that don't have length * already filled in. Bug 311852 */ if( m_currentTrack->type() == "stream" && m_currentTrack->length() == 0 ) useTrackWithinStreamDetection = true; } m_lastStreamStampPosition = useTrackWithinStreamDetection ? 0 : -1; Q_EMIT trackChanged( m_currentTrack ); Q_EMIT trackPlaying( m_currentTrack ); } void EngineController::slotStateChanged( Phonon::State newState, Phonon::State oldState ) //SLOT { debug() << "slotStateChanged from" << oldState << "to" << newState; static const int maxErrors = 5; static int errorCount = 0; // Sanity checks: if( newState == oldState ) return; if( newState == Phonon::ErrorState ) // If media is borked, skip to next track { Q_EMIT trackError( m_currentTrack ); warning() << "Phonon failed to play this URL. Error: " << m_media->errorString(); warning() << "Forcing phonon engine reinitialization."; /* In case of error Phonon MediaObject automatically switches to KioMediaSource, which cause problems: runs StopAfterCurrentTrack mode, force PlayPause button to reply the track (can't be paused). So we should reinitiate Phonon after each Error. */ initializePhonon(); errorCount++; if ( errorCount >= maxErrors ) { // reset error count errorCount = 0; Amarok::Logger::longMessage( i18n( "Too many errors encountered in playlist. Playback stopped." ), Amarok::Logger::Warning ); error() << "Stopping playlist."; } else // and start the next song, even if the current failed to start playing The::playlistActions()->requestUserNextTrack(); } else if( newState == Phonon::PlayingState ) { errorCount = 0; Q_EMIT playbackStateChanged(); } else if( newState == Phonon::StoppedState || newState == Phonon::PausedState ) { Q_EMIT playbackStateChanged(); } } void EngineController::slotPlayableUrlFetched( const QUrl &url ) { DEBUG_BLOCK debug() << "Fetched url: " << url; if( url.isEmpty() ) { DEBUG_LINE_INFO The::playlistActions()->requestNextTrack(); return; } if( !m_playWhenFetched ) { DEBUG_LINE_INFO m_mutex.lock(); m_media->clearQueue(); // keep synced with setNextTrack(), playUrl() m_media->enqueue( url ); m_nextTrack.clear(); m_nextUrl = url; debug() << "The next url we're playing is: " << m_nextUrl; // reset this flag each time m_playWhenFetched = true; m_mutex.unlock(); } else { DEBUG_LINE_INFO m_mutex.lock(); playUrl( url, 0 ); m_mutex.unlock(); } } void EngineController::slotTrackLengthChanged( qint64 milliseconds ) { debug() << "slotTrackLengthChanged(" << milliseconds << ")"; Q_EMIT trackLengthChanged( ( !m_multiPlayback || !m_boundedPlayback ) ? trackLength() : milliseconds ); } void EngineController::slotMetaDataChanged() { QVariantMap meta; meta.insert( Meta::Field::URL, m_media->currentSource().url() ); static const QList fieldPairs = QList() << FieldPair( Phonon::ArtistMetaData, Meta::Field::ARTIST ) << FieldPair( Phonon::AlbumMetaData, Meta::Field::ALBUM ) << FieldPair( Phonon::TitleMetaData, Meta::Field::TITLE ) << FieldPair( Phonon::GenreMetaData, Meta::Field::GENRE ) << FieldPair( Phonon::TracknumberMetaData, Meta::Field::TRACKNUMBER ) << FieldPair( Phonon::DescriptionMetaData, Meta::Field::COMMENT ); foreach( const FieldPair &pair, fieldPairs ) { QStringList values = m_media->metaData( pair.first ); if( !values.isEmpty() ) meta.insert( pair.second, values.first() ); } // note: don't rely on m_currentTrack here. At least some Phonon backends first Q_EMIT // totalTimeChanged(), then metaDataChanged() and only then currentSourceChanged() // which currently sets correct m_currentTrack. if( isInRecentMetaDataHistory( meta ) ) { // slotMetaDataChanged() triggered by phonon, but we've already seen // exactly the same metadata recently. Ignoring for now. return; } // following is an implementation of song end (and length) within a stream detection. // This normally fires minutes after the track has started playing so m_currentTrack // should be accurate if( m_currentTrack && m_lastStreamStampPosition >= 0 ) { stampStreamTrackLength(); Q_EMIT trackFinishedPlaying( m_currentTrack, 1.0 ); // update track length to 0 because length emitted by stampStreamTrackLength() // is for the previous song meta.insert( Meta::Field::LENGTH, 0 ); } debug() << "slotMetaDataChanged(): new meta-data:" << meta; Q_EMIT currentMetadataChanged( meta ); } void EngineController::slotSeekableChanged( bool seekable ) { Q_EMIT seekableChanged( seekable ); } void EngineController::slotTitleChanged( int titleNumber ) { DEBUG_BLOCK if ( titleNumber != m_currentAudioCdTrack ) { The::playlistActions()->requestNextTrack(); slotFinished(); } } void EngineController::slotVolumeChanged( qreal newVolume ) { int percent = qBound( 0, qRound(newVolume * 100), 100 ); if ( !m_ignoreVolumeChangeObserve && m_volume != percent ) { m_ignoreVolumeChangeAction = true; m_volume = percent; AmarokConfig::setMasterVolume( percent ); Q_EMIT volumeChanged( percent ); } else m_volume = percent; m_ignoreVolumeChangeObserve = false; } void EngineController::slotMutedChanged( bool mute ) { AmarokConfig::setMuteState( mute ); Q_EMIT muteStateChanged( mute ); } void EngineController::slotTrackFinishedPlaying( Meta::TrackPtr track, double playedFraction ) { Q_ASSERT( track ); debug() << "slotTrackFinishedPlaying(" << ( track->artist() ? track->artist()->name() : QStringLiteral( "[no artist]" ) ) << "-" << ( track->album() ? track->album()->name() : QStringLiteral( "[no album]" ) ) << "-" << track->name() << "," << playedFraction << ")"; // Track::finishedPlaying is thread-safe and can take a long time to finish. std::thread thread( &Meta::Track::finishedPlaying, track, playedFraction ); thread.detach(); } void EngineController::metadataChanged( const Meta::TrackPtr &track ) { Meta::AlbumPtr album = m_currentTrack->album(); if( m_currentAlbum != album ) { if( m_currentAlbum ) unsubscribeFrom( m_currentAlbum ); m_currentAlbum = album; if( m_currentAlbum ) subscribeTo( m_currentAlbum ); } Q_EMIT trackMetadataChanged( track ); } void EngineController::metadataChanged( const Meta::AlbumPtr &album ) { Q_EMIT albumMetadataChanged( album ); } QString EngineController::prettyNowPlaying( bool progress ) const { Meta::TrackPtr track = currentTrack(); if( track ) { QString title = track->name().toHtmlEscaped(); QString prettyTitle = track->prettyName().toHtmlEscaped(); QString artist = track->artist() ? track->artist()->name().toHtmlEscaped() : QString(); QString album = track->album() ? track->album()->name().toHtmlEscaped() : QString(); // ugly because of translation requirements if( !title.isEmpty() && !artist.isEmpty() && !album.isEmpty() ) title = i18nc( "track by artist on album", "%1 by %2 on %3", title, artist, album ); else if( !title.isEmpty() && !artist.isEmpty() ) title = i18nc( "track by artist", "%1 by %2", title, artist ); else if( !album.isEmpty() ) // we try for pretty title as it may come out better title = i18nc( "track on album", "%1 on %2", prettyTitle, album ); else title = "" + prettyTitle + ""; if( title.isEmpty() ) title = i18n( "Unknown track" ); QScopedPointer sic( track->create() ); if( sic ) { QString source = sic->sourceName(); if( !source.isEmpty() ) title += ' ' + i18nc( "track from source", "from %1", source ); } if( track->length() > 0 ) { QString length = Meta::msToPrettyTime( track->length() ).toHtmlEscaped(); title += " ("; if( progress ) title += Meta::msToPrettyTime( m_lastTickPosition ).toHtmlEscaped() + '/'; title += length + ')'; } return title; } else return i18n( "No track playing" ); } bool EngineController::isInRecentMetaDataHistory( const QVariantMap &meta ) { // search for Metadata in history for( int i = 0; i < m_metaDataHistory.size(); i++) { if( m_metaDataHistory.at( i ) == meta ) // we already had that one -> spam! { m_metaDataHistory.move( i, 0 ); // move spam to the beginning of the list return true; } } if( m_metaDataHistory.size() == 12 ) m_metaDataHistory.removeLast(); m_metaDataHistory.insert( 0, meta ); return false; } void EngineController::stampStreamTrackLength() { if( m_lastStreamStampPosition < 0 ) return; qint64 currentPosition = trackPositionMs(); debug() << "stampStreamTrackLength(): m_lastStreamStampPosition:" << m_lastStreamStampPosition << "currentPosition:" << currentPosition; if( currentPosition == m_lastStreamStampPosition ) return; qint64 length = qMax( currentPosition - m_lastStreamStampPosition, qint64( 0 ) ); updateStreamLength( length ); m_lastStreamStampPosition = currentPosition; } void EngineController::updateStreamLength( qint64 length ) { if( !m_currentTrack ) { warning() << __PRETTY_FUNCTION__ << "called with cull m_currentTrack"; return; } // Last.fm scrobbling needs to know track length before it can scrobble: QVariantMap lengthMetaData; // we cannot use m_media->currentSource()->url() here because it is already empty, bug 309976 lengthMetaData.insert( Meta::Field::URL, QUrl( m_currentTrack->playableUrl() ) ); lengthMetaData.insert( Meta::Field::LENGTH, length ); debug() << "updateStreamLength(): emitting currentMetadataChanged(" << lengthMetaData << ")"; Q_EMIT currentMetadataChanged( lengthMetaData ); } diff --git a/src/QStringx.cpp b/src/QStringx.cpp index c37e9495ba..e92d1a2a97 100644 --- a/src/QStringx.cpp +++ b/src/QStringx.cpp @@ -1,361 +1,361 @@ /**************************************************************************************** * Copyright (c) 2004 Shintaro Matsuoka * * Copyright (c) 2006 Martin Aumueller * * Copyright (c) 2011 Sergey Ivanov <123kash@gmail.com> * * * * 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 "QStringx.h" Amarok::QStringx::QStringx() { } Amarok::QStringx::QStringx( QChar ch ) : QString( ch ) { } Amarok::QStringx::QStringx( const QString &s ) : QString( s ) { } Amarok::QStringx::QStringx( const QByteArray &ba ) : QString( ba ) { } Amarok::QStringx::QStringx( const QChar *unicode, uint length ) : QString( unicode, length ) { } Amarok::QStringx::QStringx( const char *str ) : QString( str ) { } Amarok::QStringx::~QStringx() { } QString Amarok::QStringx::args( const QStringList &args ) const { const QStringList text = (*this).split( QRegExp( "%\\d+" ), QString::KeepEmptyParts ); QList::ConstIterator itrText = text.constBegin(); QList::ConstIterator itrArgs = args.constBegin(); QList::ConstIterator endText = text.constEnd(); QList::ConstIterator endArgs = args.constEnd(); QString merged = (*itrText); ++itrText; while( itrText != endText && itrArgs != endArgs ) { merged += (*itrArgs) + (*itrText); ++itrText; ++itrArgs; } Q_ASSERT( itrText == text.end() || itrArgs == args.end() ); return merged; } QString Amarok::QStringx::namedArgs( const QMap &args, bool opt ) const { // Screen all kinds of brackets and format string with namedOptArgs. QString formatString = *this; formatString.replace( QRegExp( "([\\[\\]{}])" ),"\\\\1" ); // Legacy code returned empty string if any token was empty, so do the same if( opt ) formatString = QLatin1Char( '{' ) + formatString + QLatin1Char( '}' ); QStringx fmtx( formatString ); return fmtx.namedOptArgs( args ); } QString Amarok::QStringx::namedOptArgs( const QMap &args ) const { int pos = 0; return parse( &pos, args ); } Amarok::QStringx::CharType Amarok::QStringx::testChar( int *pos ) const { if( *pos >= length() ) return CTNone; QChar c = this->at( *pos ); if( c == QLatin1Char( '\\' ) ) { ( *pos )++; return ( *pos >= length() ) ? CTNone : CTRegular; } if( c == QLatin1Char( '{' ) ) return CTBraceOpen; if( c == QLatin1Char( '}' ) ) return CTBraceClose; if( c == QLatin1Char( '[' ) ) return CTBracketOpen; - if( c == QLatin1Char( ':' ) ) + if( c == QLatin1Char( QLatin1Char(':') ) ) return CTBracketSeparator; if( c == QLatin1Char( ']' ) ) return CTBracketClose; if( c == QLatin1Char( '%' ) ) return CTToken; return CTRegular; } QString Amarok::QStringx::parseToken( int *pos, const QMap &dict ) const { if( testChar( pos ) != CTToken ) return QString(); ( *pos )++; CharType ct = testChar( pos ); QString key; while( ct == CTRegular ) { key += this->at( *pos ); ( *pos )++; ct = testChar( pos ); } if( ct == CTToken ) { ( *pos )++; return dict.value( key ); } *pos -= key.length(); return QLatin1String( "%" ); } QString Amarok::QStringx::parseBraces( int *pos, const QMap &dict ) const { if( testChar( pos ) != CTBraceOpen ) return QString(); ( *pos )++; int retPos = *pos; QString result; bool isPritable = true; CharType ct = testChar( pos ); while( ct != CTNone && ct != CTBraceClose ) { switch( ct ) { case CTBraceOpen: { result += parseBraces( pos, dict ); break; } case CTBracketOpen: { result += parseBrackets( pos, dict ); break; } case CTToken: { QString part = parseToken( pos, dict ); if( part.isEmpty() ) isPritable = false; result += part; break; } default: { result += this->at( *pos ); ( *pos )++; } } ct = testChar( pos ); } if( ct == CTBraceClose ) { ( *pos )++; if( isPritable ) return result; return QString(); } *pos = retPos; return QLatin1String( "{" ); } QString Amarok::QStringx::parseBrackets( int *pos, const QMap &dict ) const { if( testChar( pos ) != CTBracketOpen ) return QString(); ( *pos )++; // Check if next char is % if( testChar( pos ) != CTToken ) return QLatin1String( "[" ); int retPos = *pos; ( *pos )++; // Parse token manually (not by calling parseToken), because we need token name. CharType ct = testChar( pos ); QString key; while( ct == CTRegular ) { key += this->at( *pos ); ( *pos )++; ct = testChar( pos ); } if( ct != CTToken || key.isEmpty() ) { *pos = retPos; return QLatin1String( "[" ); } ( *pos )++; QString replacement; // Parse replacement string if we have one if( testChar( pos ) == CTBracketSeparator ) { ( *pos )++; ct = testChar( pos ); while( ct != CTNone && ct != CTBracketClose ) { switch( ct ) { case CTBraceOpen: { replacement += parseBraces( pos, dict ); break; } case CTBracketOpen: { replacement += parseBrackets( pos, dict ); break; } case CTToken: { replacement += parseToken( pos, dict );; break; } default: { replacement += this->at( *pos ); ( *pos )++; } } ct = testChar( pos ); } if( ct == CTNone ) { *pos = retPos; return QLatin1String( "[" ); } } if( testChar( pos ) == CTBracketClose ) { ( *pos )++; if( !dict.value( key ).isEmpty() ) return dict.value( key ); if( !replacement.isEmpty() ) return replacement; if( !dict.value( QLatin1String( "default_" ) + key ).isEmpty() ) return dict.value( QLatin1String( "default_" ) + key ); return QLatin1String( "Unknown " ) + key; } *pos = retPos; return QLatin1String( "[" ); } QString Amarok::QStringx::parse( int *pos, const QMap &dict ) const { CharType ct = testChar( pos ); QString result; while( ct != CTNone ) { switch( ct ) { case CTBraceOpen: { result += parseBraces( pos, dict ); break; } case CTBracketOpen: { result += parseBrackets( pos, dict ); break; } case CTToken: { result += parseToken( pos, dict ); break; } default: { result += this->at( *pos ); ( *pos )++; } } ct = testChar( pos ); } return result; } diff --git a/src/amarokurls/AmarokUrl.cpp b/src/amarokurls/AmarokUrl.cpp index 50322ba8aa..caae014665 100644 --- a/src/amarokurls/AmarokUrl.cpp +++ b/src/amarokurls/AmarokUrl.cpp @@ -1,249 +1,249 @@ /**************************************************************************************** * Copyright (c) 2009 Nikolaj Hald Nielsen * * * * 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 "AmarokUrl.h" #include "AmarokUrlHandler.h" #include "BookmarkGroup.h" #include "core-impl/storage/StorageManager.h" #include "core/support/Debug.h" #include #include #include AmarokUrl::AmarokUrl() : m_id( -1 ) , m_parent( 0 ) {} AmarokUrl::AmarokUrl( const QString & urlString, const BookmarkGroupPtr &parent ) : m_id( -1 ) , m_parent( parent ) { initFromString( urlString ); } AmarokUrl::AmarokUrl( const QStringList & resultRow, const BookmarkGroupPtr &parent ) : m_parent( parent ) { m_id = resultRow[0].toInt(); m_name = resultRow[2]; const QString urlString = resultRow[3]; m_description = resultRow[4]; m_customValue = resultRow[5]; initFromString( urlString ); } AmarokUrl::~AmarokUrl() {} void AmarokUrl::initFromString( const QString & urlString ) { //first, strip amarok:// QString strippedUrlString = urlString; strippedUrlString = strippedUrlString.replace( QLatin1String("amarok://"), QLatin1String("") ); //separate path from arguments - QStringList parts = strippedUrlString.split( '?' ); + QStringList parts = strippedUrlString.split( QLatin1Char('?') ); QString commandAndPath = parts.at( 0 ); QString argumentsString; if ( parts.size() == 2 ) argumentsString = parts.at( 1 ); if ( !argumentsString.isEmpty() ) { parts = argumentsString.split( '&' ); foreach( const QString &argument, parts ) { QStringList argParts = argument.split( '=' ); debug() << "argument: " << argument << " unescaped: " << unescape( argParts.at( 1 ) ); setArg( argParts.at( 0 ), unescape( argParts.at( 1 ) ) ); } } //get the command parts = commandAndPath.split( QLatin1Char('/') ); m_command = parts.takeFirst(); m_path = parts.join( QLatin1Char('/') ); m_path = unescape( m_path ); } void AmarokUrl::setCommand( const QString & command ) { m_command = command; } QString AmarokUrl::command() const { return m_command; } QString AmarokUrl::prettyCommand() const { return The::amarokUrlHandler()->prettyCommand( command() ); } QMap AmarokUrl::args() const { return m_arguments; } void AmarokUrl::setArg( const QString &name, const QString &value ) { m_arguments.insert( name, value ); } bool AmarokUrl::run() { DEBUG_BLOCK return The::amarokUrlHandler()->run( *this ); } QString AmarokUrl::url() const { QUrl url; url.setScheme( QStringLiteral("amarok") ); url.setHost( m_command ); url.setPath( '/' + m_path ); // the path must begin by / QUrlQuery query; foreach( const QString &argName, m_arguments.keys() ) query.addQueryItem( argName, m_arguments[argName] ); url.setQuery( query ); return url.toEncoded(); } bool AmarokUrl::saveToDb() { DEBUG_BLOCK if ( isNull() ) return false; const int parentId = m_parent ? m_parent->id() : -1; auto sql = StorageManager::instance()->sqlStorage(); if( m_id != -1 ) { //update existing debug() << "Updating bookmark"; QString query = QStringLiteral("UPDATE bookmarks SET parent_id=%1, name='%2', url='%3', description='%4', custom='%5' WHERE id=%6;"); query = query.arg( QString::number( parentId ), sql->escape( m_name ), sql->escape( url() ), sql->escape( m_description ), sql->escape( m_customValue ), QString::number( m_id ) ); StorageManager::instance()->sqlStorage()->query( query ); } else { //insert new debug() << "Creating new bookmark in the db"; QString query = QStringLiteral("INSERT INTO bookmarks ( parent_id, name, url, description, custom ) VALUES ( %1, '%2', '%3', '%4', '%5' );"); query = query.arg( QString::number( parentId ), sql->escape( m_name ), sql->escape( url() ), sql->escape( m_description ), sql->escape( m_customValue ) ); m_id = StorageManager::instance()->sqlStorage()->insert( query, nullptr ); } return true; } void AmarokUrl::setName( const QString & name ) { m_name = name; } QString AmarokUrl::name() const { return m_name; } void AmarokUrl::setDescription( const QString & description ) { m_description = description; } QString AmarokUrl::description() const { return m_description; } void AmarokUrl::removeFromDb() { QString query = QStringLiteral("DELETE FROM bookmarks WHERE id=%1"); query = query.arg( QString::number( m_id ) ); StorageManager::instance()->sqlStorage()->query( query ); } void AmarokUrl::rename( const QString &name ) { m_name = name; if ( m_id != -1 ) saveToDb(); } void AmarokUrl::reparent( const BookmarkGroupPtr &parent ) { m_parent = parent; saveToDb(); } void AmarokUrl::setCustomValue( const QString & custom ) { m_customValue = custom; } QString AmarokUrl::customValue() const { return m_customValue; } QString AmarokUrl::escape( const QString & in ) { return QUrl::toPercentEncoding( in.toUtf8() ); } QString AmarokUrl::unescape( const QString & in ) { return QUrl::fromPercentEncoding( in.toUtf8() ); } bool AmarokUrl::isNull() const { return m_command.isEmpty(); } QString AmarokUrl::path() const { return m_path; } void AmarokUrl::setPath( const QString &path ) { m_path = path; } diff --git a/src/browsers/filebrowser/FileBrowser.cpp b/src/browsers/filebrowser/FileBrowser.cpp index ef6717c365..5b8d61ab50 100644 --- a/src/browsers/filebrowser/FileBrowser.cpp +++ b/src/browsers/filebrowser/FileBrowser.cpp @@ -1,637 +1,637 @@ /**************************************************************************************** * Copyright (c) 2010 Nikolaj Hald Nielsen * * Copyright (c) 2010 Casey Link * * Copyright (c) 2010 Téo Mrnjavac * * * * 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 "FileBrowser" #include "FileBrowser_p.h" #include "FileBrowser.h" #include "amarokconfig.h" #include "EngineController.h" #include "core/support/Amarok.h" #include "core/support/Components.h" #include "core/support/Debug.h" #include "core-impl/meta/file/File.h" #include "browsers/BrowserBreadcrumbItem.h" #include "browsers/BrowserCategoryList.h" #include "browsers/filebrowser/DirPlaylistTrackFilterProxyModel.h" #include "browsers/filebrowser/FileView.h" #include "playlist/PlaylistController.h" #include "widgets/SearchWidget.h" #include #include #include #include #include #include #include #include #include #include #include static const QString placesString( "places://" ); static const QUrl placesUrl( placesString ); FileBrowser::Private::Private( FileBrowser *parent ) : placesModel( 0 ) , q( parent ) { BoxWidget *topHBox = new BoxWidget( q ); KToolBar *navigationToolbar = new KToolBar( topHBox ); navigationToolbar->setToolButtonStyle( Qt::ToolButtonIconOnly ); navigationToolbar->setIconDimensions( 16 ); backAction = KStandardAction::back( q, &FileBrowser::back, topHBox ); forwardAction = KStandardAction::forward( q, &FileBrowser::forward, topHBox ); backAction->setEnabled( false ); forwardAction->setEnabled( false ); upAction = KStandardAction::up( q, &FileBrowser::up, topHBox ); homeAction = KStandardAction::home( q, &FileBrowser::home, topHBox ); refreshAction = new QAction( QIcon::fromTheme("view-refresh"), i18n( "Refresh" ), topHBox ); QObject::connect( refreshAction, &QAction::triggered, q, &FileBrowser::refresh ); navigationToolbar->addAction( backAction ); navigationToolbar->addAction( forwardAction ); navigationToolbar->addAction( upAction ); navigationToolbar->addAction( homeAction ); navigationToolbar->addAction( refreshAction ); searchWidget = new SearchWidget( topHBox, false ); searchWidget->setClickMessage( i18n( "Filter Files" ) ); fileView = new FileView( q ); } FileBrowser::Private::~Private() { writeConfig(); } void FileBrowser::Private::readConfig() { const QUrl homeUrl = QUrl::fromLocalFile( QDir::homePath() ); const QUrl savedUrl = Amarok::config( "File Browser" ).readEntry( "Current Directory", homeUrl ); bool useHome( true ); // fall back to $HOME if the saved dir has since disappeared or is a remote one if( savedUrl.isLocalFile() ) { QDir dir( savedUrl.path() ); if( dir.exists() ) useHome = false; } else { KIO::StatJob *statJob = KIO::stat( savedUrl, KIO::StatJob::DestinationSide, 0 ); statJob->exec(); if( statJob->statResult().isDir() ) { useHome = false; } } currentPath = useHome ? homeUrl : savedUrl; } void FileBrowser::Private::writeConfig() { Amarok::config( "File Browser" ).writeEntry( "Current Directory", kdirModel->dirLister()->url() ); } BreadcrumbSiblingList FileBrowser::Private::siblingsForDir( const QUrl &path ) { BreadcrumbSiblingList siblings; if( path.scheme() == "places" ) { for( int i = 0; i < placesModel->rowCount(); i++ ) { QModelIndex idx = placesModel->index( i, 0 ); QString name = idx.data( Qt::DisplayRole ).toString(); QString url = idx.data( KFilePlacesModel::UrlRole ).toString(); if( url.isEmpty() ) // the place perhaps needs mounting, use places url instead url = placesString + name; siblings << BreadcrumbSibling( idx.data( Qt::DecorationRole ).value(), name, url ); } } else if( path.isLocalFile() ) { QDir dir( path.toLocalFile() ); dir.cdUp(); foreach( const QString &item, dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) ) { siblings << BreadcrumbSibling( QIcon::fromTheme( "folder-amarok" ), item, dir.absoluteFilePath( item ) ); } } return siblings; } void FileBrowser::Private::updateNavigateActions() { backAction->setEnabled( !backStack.isEmpty() ); forwardAction->setEnabled( !forwardStack.isEmpty() ); upAction->setEnabled( currentPath != placesUrl ); } void FileBrowser::Private::restoreDefaultHeaderState() { fileView->hideColumn( 3 ); fileView->hideColumn( 4 ); fileView->hideColumn( 5 ); fileView->hideColumn( 6 ); fileView->sortByColumn( 0, Qt::AscendingOrder ); } void FileBrowser::Private::restoreHeaderState() { QFile file( Amarok::saveLocation() + "file_browser_layout" ); if( !file.open( QIODevice::ReadOnly ) ) { restoreDefaultHeaderState(); return; } if( !fileView->header()->restoreState( file.readAll() ) ) { warning() << "invalid header state saved, unable to restore. Restoring defaults"; restoreDefaultHeaderState(); return; } } void FileBrowser::Private::saveHeaderState() { //save the state of the header (column size and order). Yay, another QByteArray thingie... QFile file( Amarok::saveLocation() + "file_browser_layout" ); if( !file.open( QIODevice::WriteOnly ) ) { warning() << "unable to save header state"; return; } if( file.write( fileView->header()->saveState() ) < 0 ) { warning() << "unable to save header state, writing failed"; return; } } void FileBrowser::Private::updateHeaderState() { // this slot is triggered right after model change, when currentPath is not yet updated if( fileView->model() == mimeFilterProxyModel && currentPath == placesUrl ) // we are transitioning from places to files restoreHeaderState(); } FileBrowser::FileBrowser( const char *name, QWidget *parent ) : BrowserCategory( name, parent ) , d( new FileBrowser::Private( this ) ) { setLongDescription( i18n( "The file browser lets you browse files anywhere on your system, " "regardless of whether these files are part of your local collection. " "You can then add these files to the playlist as well as perform basic " "file operations." ) ); setImagePath( QStandardPaths::locate( QStandardPaths::GenericDataLocation, "amarok/images/hover_info_files.png" ) ); // set background if( AmarokConfig::showBrowserBackgroundImage() ) setBackgroundImage( imagePath() ); initView(); } void FileBrowser::initView() { d->bottomPlacesModel = new FilePlacesModel( this ); connect( d->bottomPlacesModel, &KFilePlacesModel::setupDone, this, &FileBrowser::setupDone ); d->placesModel = new QSortFilterProxyModel( this ); d->placesModel->setSourceModel( d->bottomPlacesModel ); d->placesModel->setSortRole( -1 ); d->placesModel->setDynamicSortFilter( true ); d->placesModel->setFilterRole( KFilePlacesModel::HiddenRole ); // HiddenRole is bool, but QVariant( false ).toString() gives "false" d->placesModel->setFilterFixedString( "false" ); d->placesModel->setObjectName( "PLACESMODEL"); d->kdirModel = new DirBrowserModel( this ); d->mimeFilterProxyModel = new DirPlaylistTrackFilterProxyModel( this ); d->mimeFilterProxyModel->setSourceModel( d->kdirModel ); d->mimeFilterProxyModel->setSortCaseSensitivity( Qt::CaseInsensitive ); d->mimeFilterProxyModel->setFilterCaseSensitivity( Qt::CaseInsensitive ); d->mimeFilterProxyModel->setDynamicSortFilter( true ); connect( d->searchWidget, &SearchWidget::filterChanged, d->mimeFilterProxyModel, &DirPlaylistTrackFilterProxyModel::setFilterFixedString ); d->fileView->setModel( d->mimeFilterProxyModel ); d->fileView->header()->setContextMenuPolicy( Qt::ActionsContextMenu ); d->fileView->header()->setVisible( true ); d->fileView->setDragEnabled( true ); d->fileView->setSortingEnabled( true ); d->fileView->setSelectionMode( QAbstractItemView::ExtendedSelection ); d->readConfig(); d->restoreHeaderState(); setDir( d->currentPath ); for( int i = 0, columns = d->fileView->model()->columnCount(); i < columns ; ++i ) { QAction *action = new QAction( d->fileView->model()->headerData( i, Qt::Horizontal ).toString(), d->fileView->header() ); d->fileView->header()->addAction( action ); d->columnActions.append( action ); action->setCheckable( true ); if( !d->fileView->isColumnHidden( i ) ) action->setChecked( true ); connect( action, &QAction::toggled, this, &FileBrowser::toggleColumn ); } connect( d->fileView->header(), &QHeaderView::geometriesChanged, this, &FileBrowser::updateHeaderState ); connect( d->fileView, &FileView::navigateToDirectory, this, &FileBrowser::slotNavigateToDirectory ); connect( d->fileView, &FileView::refreshBrowser, this, &FileBrowser::refresh ); } void FileBrowser::updateHeaderState() { d->updateHeaderState(); } FileBrowser::~FileBrowser() { if( d->fileView->model() == d->mimeFilterProxyModel && d->currentPath != placesUrl ) d->saveHeaderState(); delete d; } void FileBrowser::toggleColumn( bool toggled ) { int index = d->columnActions.indexOf( qobject_cast< QAction* >( sender() ) ); if( index != -1 ) { if( toggled ) d->fileView->showColumn( index ); else d->fileView->hideColumn( index ); } } QString FileBrowser::currentDir() const { if( d->currentPath.isLocalFile() ) return d->currentPath.toLocalFile(); else return d->currentPath.url(); } void FileBrowser::slotNavigateToDirectory( const QModelIndex &index ) { if( d->currentPath == placesUrl ) { QString url = index.data( KFilePlacesModel::UrlRole ).value(); if( !url.isEmpty() ) { d->backStack.push( d->currentPath ); d->forwardStack.clear(); // navigating resets forward stack setDir( QUrl( url ) ); } else { //check if this url needs setup/mounting if( index.data( KFilePlacesModel::SetupNeededRole ).value() ) { d->bottomPlacesModel->requestSetup( d->placesModel->mapToSource( index ) ); } else warning() << __PRETTY_FUNCTION__ << "empty places url that doesn't need setup?"; } } else { KFileItem file = index.data( KDirModel::FileItemRole ).value(); if( file.isDir() ) { d->backStack.push( d->currentPath ); d->forwardStack.clear(); // navigating resets forward stack setDir( file.url() ); } else warning() << __PRETTY_FUNCTION__ << "called for non-directory"; } } void FileBrowser::addItemActivated( const QString &callbackString ) { if( callbackString.isEmpty() ) return; QUrl newPath; // we have been called with a places name, it means that we'll probably have to mount // the place if( callbackString.startsWith( placesString ) ) { QString name = callbackString.mid( placesString.length() ); for( int i = 0; i < d->placesModel->rowCount(); i++ ) { QModelIndex idx = d->placesModel->index( i, 0 ); if( idx.data().toString() == name ) { if( idx.data( KFilePlacesModel::SetupNeededRole ).toBool() ) { d->bottomPlacesModel->requestSetup( d->placesModel->mapToSource( idx ) ); return; } newPath = QUrl::fromUserInput(idx.data( KFilePlacesModel::UrlRole ).toString()); break; } } if( newPath.isEmpty() ) { warning() << __PRETTY_FUNCTION__ << "name" << name << "not found under Places"; return; } } else newPath = QUrl::fromUserInput(callbackString); d->backStack.push( d->currentPath ); d->forwardStack.clear(); // navigating resets forward stack setDir( QUrl( newPath ) ); } void FileBrowser::setupAddItems() { clearAdditionalItems(); if( d->currentPath == placesUrl ) return; // no more items to add QString workingUrl = d->currentPath.toDisplayString( QUrl::StripTrailingSlash ); int currentPosition = 0; QString name; QString callback; BreadcrumbSiblingList siblings; // find QModelIndex of the NON-HIDDEN closestItem QModelIndex placesIndex; QUrl tempUrl = d->currentPath; do { placesIndex = d->bottomPlacesModel->closestItem( tempUrl ); if( !placesIndex.isValid() ) break; // no valid index even in the bottom model placesIndex = d->placesModel->mapFromSource( placesIndex ); if( placesIndex.isValid() ) break; // found shown placesindex, good! if( KIO::upUrl(tempUrl) == tempUrl ) break; // prevent infinite loop tempUrl = KIO::upUrl(tempUrl); } while( true ); // special handling for the first additional item if( placesIndex.isValid() ) { name = placesIndex.data( Qt::DisplayRole ).toString(); callback = placesIndex.data( KFilePlacesModel::UrlRole ).toString(); QUrl currPlaceUrl = d->placesModel->data( placesIndex, KFilePlacesModel::UrlRole ).toUrl(); currPlaceUrl.setPath( QDir::toNativeSeparators(currPlaceUrl.path() + '/') ); currentPosition = currPlaceUrl.toString().length(); } else { QRegExp threeSlashes( "^[^/]*/[^/]*/[^/]*/" ); if( workingUrl.indexOf( threeSlashes ) == 0 ) currentPosition = threeSlashes.matchedLength(); else currentPosition = workingUrl.length(); callback = workingUrl.left( currentPosition ); name = callback; if( name == "file:///" ) name = '/'; // just niceness else name.remove( QRegExp( "/$" ) ); } /* always provide siblings for places, regardless of what first item is; this also * work-arounds bug 312639, where creating QUrl with accented chars crashes */ siblings = d->siblingsForDir( placesUrl ); addAdditionalItem( new BrowserBreadcrumbItem( name, callback, siblings, this ) ); // other additional items while( !workingUrl.midRef( currentPosition ).isEmpty() ) { - int nextPosition = workingUrl.indexOf( '/', currentPosition ) + 1; + int nextPosition = workingUrl.indexOf( QLatin1Char('/'), currentPosition ) + 1; if( nextPosition <= 0 ) nextPosition = workingUrl.length(); name = workingUrl.mid( currentPosition, nextPosition - currentPosition ); name.remove( QRegExp( "/$" ) ); callback = workingUrl.left( nextPosition ); siblings = d->siblingsForDir( QUrl::fromLocalFile( callback ) ); addAdditionalItem( new BrowserBreadcrumbItem( name, callback, siblings, this ) ); currentPosition = nextPosition; } if( parentList() ) parentList()->childViewChanged(); // emits viewChanged() which causes breadCrumb update } void FileBrowser::reActivate() { d->backStack.push( d->currentPath ); d->forwardStack.clear(); // navigating resets forward stack setDir( placesUrl ); } void FileBrowser::setDir( const QUrl &dir ) { if( dir == placesUrl ) { if( d->currentPath != placesUrl ) { d->saveHeaderState(); d->fileView->setModel( d->placesModel ); d->fileView->setSelectionMode( QAbstractItemView::SingleSelection ); d->fileView->header()->setVisible( false ); d->fileView->setDragEnabled( false ); } } else { // if we are currently showing "places" we need to remember to change the model // back to the regular file model if( d->currentPath == placesUrl ) { d->fileView->setModel( d->mimeFilterProxyModel ); d->fileView->setSelectionMode( QAbstractItemView::ExtendedSelection ); d->fileView->setDragEnabled( true ); d->fileView->header()->setVisible( true ); } d->kdirModel->dirLister()->openUrl( dir ); } d->currentPath = dir; d->updateNavigateActions(); setupAddItems(); // set the first item as current so that keyboard navigation works new DelayedActivator( d->fileView ); } void FileBrowser::back() { if( d->backStack.isEmpty() ) return; d->forwardStack.push( d->currentPath ); setDir( d->backStack.pop() ); } void FileBrowser::forward() { if( d->forwardStack.isEmpty() ) return; d->backStack.push( d->currentPath ); // no clearing forward stack here! setDir( d->forwardStack.pop() ); } void FileBrowser::up() { if( d->currentPath == placesUrl ) return; // nothing to do, we consider places as the root view QUrl upUrl = KIO::upUrl(d->currentPath); if( upUrl == d->currentPath ) // apparently, we cannot go up withn url upUrl = placesUrl; d->backStack.push( d->currentPath ); d->forwardStack.clear(); // navigating resets forward stack setDir( upUrl ); } void FileBrowser::home() { d->backStack.push( d->currentPath ); d->forwardStack.clear(); // navigating resets forward stack setDir( QUrl::fromLocalFile( QDir::homePath() ) ); } void FileBrowser::refresh() { setDir( d->currentPath ); } void FileBrowser::setupDone( const QModelIndex &index, bool success ) { if( success ) { QString url = index.data( KFilePlacesModel::UrlRole ).value(); if( !url.isEmpty() ) { d->backStack.push( d->currentPath ); d->forwardStack.clear(); // navigating resets forward stack setDir( QUrl::fromLocalFile(url) ); } } } DelayedActivator::DelayedActivator( QAbstractItemView *view ) : QObject( view ) , m_view( view ) { QAbstractItemModel *model = view->model(); if( !model ) { deleteLater(); return; } // short-cut for already-filled models if( model->rowCount() > 0 ) { slotRowsInserted( QModelIndex(), 0 ); return; } connect( model, &QAbstractItemModel::rowsInserted, this, &DelayedActivator::slotRowsInserted ); connect( model, &QAbstractItemModel::destroyed, this, &DelayedActivator::deleteLater ); connect( model, &QAbstractItemModel::layoutChanged, this, &DelayedActivator::deleteLater ); connect( model, &QAbstractItemModel::modelReset, this, &DelayedActivator::deleteLater ); } void DelayedActivator::slotRowsInserted( const QModelIndex &parent, int start ) { QAbstractItemModel *model = m_view->model(); if( model ) { // prevent duplicate calls, deleteLater() may fire REAL later disconnect( model, 0, this, 0 ); QModelIndex idx = model->index( start, 0, parent ); m_view->selectionModel()->setCurrentIndex( idx, QItemSelectionModel::NoUpdate ); } deleteLater(); } #include "moc_FileBrowser.cpp" diff --git a/src/core-impl/collections/audiocd/AudioCdCollectionLocation.cpp b/src/core-impl/collections/audiocd/AudioCdCollectionLocation.cpp index 3ad44506e4..41897ed0b1 100644 --- a/src/core-impl/collections/audiocd/AudioCdCollectionLocation.cpp +++ b/src/core-impl/collections/audiocd/AudioCdCollectionLocation.cpp @@ -1,89 +1,89 @@ /**************************************************************************************** * Copyright (c) 2009 Nikolaj Hald Nielsen * * * * 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 "AudioCdCollectionLocation.h" #include "AudioCdMeta.h" #include "core/support/Debug.h" #include "FormatSelectionDialog.h" using namespace Collections; AudioCdCollectionLocation::AudioCdCollectionLocation( AudioCdCollection *parentCollection ) : CollectionLocation( parentCollection ) , m_collection( parentCollection ) { } AudioCdCollectionLocation::~AudioCdCollectionLocation() { } void AudioCdCollectionLocation::getKIOCopyableUrls( const Meta::TrackList & tracks ) { DEBUG_BLOCK QMap resultMap; foreach( Meta::TrackPtr trackPtr, tracks ) { Meta::AudioCdTrackPtr cdTrack = Meta::AudioCdTrackPtr::staticCast( trackPtr ); - const QString path = m_collection->copyableFilePath( cdTrack->fileNameBase() + '.' + m_collection->encodingFormat() ); + const QString path = m_collection->copyableFilePath( cdTrack->fileNameBase() + QLatin1Char('.') + m_collection->encodingFormat() ); resultMap.insert( trackPtr, QUrl::fromLocalFile( path ) ); } slotGetKIOCopyableUrlsDone( resultMap ); } void AudioCdCollectionLocation::showSourceDialog( const Meta::TrackList &tracks, bool removeSources ) { DEBUG_BLOCK Q_UNUSED( tracks ) Q_UNUSED( removeSources ) FormatSelectionDialog * dlg = new FormatSelectionDialog(); connect( dlg, &FormatSelectionDialog::formatSelected, this, &AudioCdCollectionLocation::onFormatSelected ); connect( dlg, &FormatSelectionDialog::rejected, this, &AudioCdCollectionLocation::onCancel ); dlg->show(); } void AudioCdCollectionLocation::formatSelected( int format ) { Q_UNUSED( format ) } void AudioCdCollectionLocation::formatSelectionCancelled() { } void AudioCdCollectionLocation::onFormatSelected( int format ) { DEBUG_BLOCK m_collection->setEncodingFormat( format ); slotShowSourceDialogDone(); } void AudioCdCollectionLocation::onCancel() { DEBUG_BLOCK abort(); } diff --git a/src/core-impl/collections/daap/DaapCollection.cpp b/src/core-impl/collections/daap/DaapCollection.cpp index e586e59c0a..a9fbae319d 100644 --- a/src/core-impl/collections/daap/DaapCollection.cpp +++ b/src/core-impl/collections/daap/DaapCollection.cpp @@ -1,312 +1,312 @@ /**************************************************************************************** * Copyright (c) 2006 Ian Monroe * * Copyright (c) 2006 Seb Ruiz * * 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 . * ****************************************************************************************/ #define DEBUG_PREFIX "DaapCollection" #include "DaapCollection.h" #include "amarokconfig.h" #include "core/logger/Logger.h" #include "core/support/Components.h" #include "core/support/Debug.h" #include "DaapMeta.h" #include "MemoryQueryMaker.h" #include "daapreader/Reader.h" #include #include #include #include #include #include using namespace Collections; DaapCollectionFactory::DaapCollectionFactory() : Collections::CollectionFactory() , m_browser( 0 ) { } DaapCollectionFactory::~DaapCollectionFactory() { delete m_browser; } void DaapCollectionFactory::init() { DEBUG_BLOCK switch( KDNSSD::ServiceBrowser::isAvailable() ) { case KDNSSD::ServiceBrowser::Working: //don't block Amarok's startup by connecting to DAAP servers QTimer::singleShot( 1000, this, &DaapCollectionFactory::connectToManualServers ); m_browser = new KDNSSD::ServiceBrowser("_daap._tcp"); m_browser->setObjectName("daapServiceBrowser"); connect( m_browser, &KDNSSD::ServiceBrowser::serviceAdded, this, &DaapCollectionFactory::foundDaap ); connect( m_browser, &KDNSSD::ServiceBrowser::serviceRemoved, this, &DaapCollectionFactory::serverOffline ); m_browser->startBrowse(); break; case KDNSSD::ServiceBrowser::Stopped: debug() << "The Zeroconf daemon is not running"; break; case KDNSSD::ServiceBrowser::Unsupported: debug() << "Zeroconf support is not available"; break; default: debug() << "Unknown error with Zeroconf"; } m_initialized = true; } void DaapCollectionFactory::connectToManualServers() { DEBUG_BLOCK QStringList sl = AmarokConfig::manuallyAddedServers(); foreach( const QString &server, sl ) { debug() << "Adding server " << server; - QStringList current = server.split( ':', QString::KeepEmptyParts ); + QStringList current = server.split( QLatin1Char(':'), QString::KeepEmptyParts ); //handle invalid urls gracefully if( current.count() < 2 ) continue; QString host = current.first(); quint16 port = current.last().toUShort(); Amarok::Logger::longMessage( i18n( "Loading remote collection from host %1", host), Amarok::Logger::Information ); int lookup_id = QHostInfo::lookupHost( host, this, &DaapCollectionFactory::resolvedManualServerIp ); m_lookupHash.insert( lookup_id, port ); } } void DaapCollectionFactory::serverOffline( KDNSSD::RemoteService::Ptr service ) { DEBUG_BLOCK QString key = serverKey( service->hostName(), service->port() ); if( m_collectionMap.contains( key ) ) { auto coll = m_collectionMap[ key ]; if( coll ) coll->serverOffline(); //collection will be deleted by collectionmanager else warning() << "collection already null"; m_collectionMap.remove( key ); } else warning() << "removing non-existent service"; } void DaapCollectionFactory::foundDaap( KDNSSD::RemoteService::Ptr service ) { DEBUG_BLOCK connect( service.data(), &KDNSSD::RemoteService::resolved, this, &DaapCollectionFactory::resolvedDaap ); service->resolveAsync(); } void DaapCollectionFactory::resolvedDaap( bool success ) { const KDNSSD::RemoteService* service = dynamic_cast(sender()); if( !success || !service ) return; debug() << service->serviceName() << ' ' << service->hostName() << ' ' << service->domain() << ' ' << service->type(); int lookup_id = QHostInfo::lookupHost( service->hostName(), this, &DaapCollectionFactory::resolvedServiceIp ); m_lookupHash.insert( lookup_id, service->port() ); } QString DaapCollectionFactory::serverKey( const QString& host, quint16 port) const { return host + QLatin1Char(':') + QString::number( port ); } void DaapCollectionFactory::slotCollectionReady() { DEBUG_BLOCK DaapCollection *collection = dynamic_cast( sender() ); if( collection ) { disconnect( collection, &DaapCollection::remove, this, &DaapCollectionFactory::slotCollectionDownloadFailed ); Q_EMIT newCollection( collection ); } } void DaapCollectionFactory::slotCollectionDownloadFailed() { DEBUG_BLOCK DaapCollection *collection = qobject_cast( sender() ); if( !collection ) return; disconnect( collection, &DaapCollection::collectionReady, this, &DaapCollectionFactory::slotCollectionReady ); for( const auto &it : m_collectionMap ) { if( it.data() == collection ) { m_collectionMap.remove( m_collectionMap.key( it ) ); break; } } collection->deleteLater(); } void DaapCollectionFactory::resolvedManualServerIp( const QHostInfo &hostInfo ) { if ( !m_lookupHash.contains(hostInfo.lookupId()) ) return; if ( hostInfo.addresses().isEmpty() ) return; QString host = hostInfo.hostName(); QString ip = hostInfo.addresses().at(0).toString(); quint16 port = m_lookupHash.value( hostInfo.lookupId() ); //adding manual servers to the collectionMap doesn't make sense DaapCollection *coll = new DaapCollection( host, ip, port ); connect( coll, &DaapCollection::collectionReady, this, &DaapCollectionFactory::slotCollectionReady ); connect( coll, &DaapCollection::remove, this, &DaapCollectionFactory::slotCollectionDownloadFailed ); } void DaapCollectionFactory::resolvedServiceIp( const QHostInfo &hostInfo ) { DEBUG_BLOCK // debug() << "got address:" << hostInfo.addresses() << "and lookup hash contains id" << hostInfo.lookupId() << "?" << m_lookupHash.contains(hostInfo.lookupId()); if ( !m_lookupHash.contains(hostInfo.lookupId()) ) return; if ( hostInfo.addresses().isEmpty() ) return; QString host = hostInfo.hostName(); QString ip = hostInfo.addresses().at(0).toString(); quint16 port = m_lookupHash.value( hostInfo.lookupId() ); // debug() << "already added server?" << m_collectionMap.contains(serverKey( host, port )); if( m_collectionMap.contains(serverKey( host, port )) ) //same server from multiple interfaces return; // debug() << "creating daap collection with" << host << ip << port; QPointer coll( new DaapCollection( host, ip, port ) ); connect( coll, &DaapCollection::collectionReady, this, &DaapCollectionFactory::slotCollectionReady ); connect( coll, &DaapCollection::remove, this, &DaapCollectionFactory::slotCollectionDownloadFailed ); m_collectionMap.insert( serverKey( host, port ), coll.data() ); } //DaapCollection DaapCollection::DaapCollection( const QString &host, const QString &ip, quint16 port ) : Collection() , m_host( host ) , m_port( port ) , m_ip( ip ) , m_reader( nullptr ) , m_mc( new MemoryCollection() ) { debug() << "Host: " << host << " port: " << port; m_reader = new Daap::Reader( this, host, port, QString(), this, "DaapReader" ); connect( m_reader, &Daap::Reader::passwordRequired,this, &DaapCollection::passwordRequired ); connect( m_reader, &Daap::Reader::httpError, this, &DaapCollection::httpError ); m_reader->loginRequest(); } DaapCollection::~DaapCollection() { } QueryMaker* DaapCollection::queryMaker() { return new MemoryQueryMaker( m_mc.toWeakRef(), collectionId() ); } QString DaapCollection::collectionId() const { return QString( QStringLiteral("daap://") + m_ip + QLatin1Char(':') ) + QString::number( m_port ); } QString DaapCollection::prettyName() const { QString host = m_host; // No need to be overly verbose if( host.endsWith( ".local" ) ) host = host.remove( QRegExp(".local$") ); return i18n("Music share at %1", host); } void DaapCollection::passwordRequired() { //get password QString password; delete m_reader; m_reader = new Daap::Reader( this, m_host, m_port, password, this, "DaapReader" ); connect( m_reader, &Daap::Reader::passwordRequired, this, &DaapCollection::passwordRequired ); connect( m_reader, &Daap::Reader::httpError, this, &DaapCollection::httpError ); m_reader->loginRequest(); } void DaapCollection::httpError( const QString &error ) { DEBUG_BLOCK debug() << "Http error in DaapReader: " << error; Q_EMIT remove(); } void DaapCollection::serverOffline() { Q_EMIT remove(); } void DaapCollection::loadedDataFromServer() { DEBUG_BLOCK Q_EMIT collectionReady(); } void DaapCollection::parsingFailed() { DEBUG_BLOCK Q_EMIT remove(); } diff --git a/src/core-impl/collections/mediadevicecollection/playlist/MediaDeviceUserPlaylistProvider.h b/src/core-impl/collections/mediadevicecollection/playlist/MediaDeviceUserPlaylistProvider.h index 5ee46d628c..148eeba971 100644 --- a/src/core-impl/collections/mediadevicecollection/playlist/MediaDeviceUserPlaylistProvider.h +++ b/src/core-impl/collections/mediadevicecollection/playlist/MediaDeviceUserPlaylistProvider.h @@ -1,75 +1,75 @@ /**************************************************************************************** * Copyright (c) 2009 Alejandro Wainzinger * * * * 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 . * ****************************************************************************************/ #ifndef AMAROK_COLLECTION_MEDIADEVICEUSERPLAYLISTPROVIDER_H #define AMAROK_COLLECTION_MEDIADEVICEUSERPLAYLISTPROVIDER_H #include "core-impl/playlists/providers/user/UserPlaylistProvider.h" #include "MediaDevicePlaylist.h" #include #include namespace Collections { class MediaDeviceCollection; } namespace Playlists { class AMAROK_EXPORT MediaDeviceUserPlaylistProvider : public Playlists::UserPlaylistProvider { Q_OBJECT public: explicit MediaDeviceUserPlaylistProvider( Collections::MediaDeviceCollection *collection ); ~MediaDeviceUserPlaylistProvider() override; /* PlaylistProvider functions */ QString prettyName() const override { return i18n( "Media Device playlists" ); } - QIcon icon() const override { return QIcon::fromTheme( "multimedia-player" ); } + QIcon icon() const override { return QIcon::fromTheme( QStringLiteral("multimedia-player") ); } /* Playlists::UserPlaylistProvider functions */ Playlists::PlaylistList playlists() override; virtual Playlists::PlaylistPtr save( const Meta::TrackList &tracks ); Playlists::PlaylistPtr save( const Meta::TrackList &tracks, const QString& name ) override; bool isWritable() override { return true; } void renamePlaylist(Playlists::PlaylistPtr playlist, const QString &newName ) override; bool deletePlaylists( const Playlists::PlaylistList &playlistlist ) override; /// MediaDevice-specific Functions void addMediaDevicePlaylist( Playlists::MediaDevicePlaylistPtr &playlist ); void removePlaylist( Playlists::MediaDevicePlaylistPtr &playlist ); public Q_SLOTS: void sendUpdated() { Q_EMIT updated(); } Q_SIGNALS: void playlistSaved( const Playlists::MediaDevicePlaylistPtr &playlist, const QString &name ); void playlistRenamed( const Playlists::MediaDevicePlaylistPtr &playlist ); void playlistsDeleted( const Playlists::MediaDevicePlaylistList &playlistlist ); private: MediaDevicePlaylistList m_playlists; Collections::MediaDeviceCollection *m_collection; }; } //namespace Playlists #endif diff --git a/src/core-impl/collections/upnpcollection/UpnpCache.cpp b/src/core-impl/collections/upnpcollection/UpnpCache.cpp index a40d02f9fc..afe4e69b4d 100644 --- a/src/core-impl/collections/upnpcollection/UpnpCache.cpp +++ b/src/core-impl/collections/upnpcollection/UpnpCache.cpp @@ -1,176 +1,176 @@ /**************************************************************************************** * Copyright (c) 2010 Nikhil Marathe * * * * 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 "UpnpCache.h" #include #include "upnptypes.h" #include "UpnpMeta.h" #include "UpnpCollectionBase.h" // TODO : move this to CollectionBase static qint64 duration( const QString &duration ) { if( duration.isEmpty() ) return 0; - QStringList parts = duration.split( ':' ); + QStringList parts = duration.split( QLatin1Char(':') ); int hours = parts.takeFirst().toInt(); int minutes = parts.takeFirst().toInt(); QString rest = parts.takeFirst(); int seconds = 0; int mseconds = 0; if( rest.contains( QLatin1Char('.') ) ) { int dotIndex = rest.indexOf( "." ); seconds = rest.leftRef( dotIndex ).toInt(); QString frac = rest.mid( dotIndex + 1 ); - if( frac.contains( '/' ) ) { - int slashIndex = frac.indexOf( '/' ); - int num = frac.leftRef( frac.indexOf( '/' ) ).toInt(); + if( frac.contains( QLatin1Char('/') ) ) { + int slashIndex = frac.indexOf( QLatin1Char('/') ); + int num = frac.leftRef( frac.indexOf( QLatin1Char('/') ) ).toInt(); int den = frac.midRef( slashIndex + 1 ).toInt(); mseconds = num * 1000 / den; } else { mseconds = QString( '.' + frac ).toFloat() * 1000; } } else { seconds = rest.toInt(); } return hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000 + mseconds; } namespace Collections { UpnpCache::UpnpCache( UpnpCollectionBase* collection ) : m_collection( collection ) { } Meta::TrackPtr UpnpCache::getTrack( const KIO::UDSEntry &entry, bool refresh ) { QMutexLocker lock( &m_cacheMutex ); // a little indirection to get the nicely formatted track uidUrl Meta::UpnpTrackPtr track( new Meta::UpnpTrack( m_collection ) ); track->setUidUrl( entry.stringValue( KIO::UPNP_ID ) ); // if we have a reference ID search for that // in either case the original ID (refID) becomes our UID URL instead of the UPNP_ID if( entry.contains( KIO::UPNP_REF_ID ) ) { track->setUidUrl( entry.stringValue( KIO::UPNP_REF_ID ) ); } QString uidUrl = track->uidUrl(); if( m_trackMap.contains( uidUrl ) && !refresh ) { return m_trackMap[uidUrl]; } // UDS_NAME is the plain ASCII, relative path prefixed name // but UDS_DISPLAY_NAME is the unicode, 'file' name. track->setTitle( entry.stringValue( KIO::UDSEntry::UDS_DISPLAY_NAME ) ); track->setPlayableUrl( entry.stringValue(KIO::UDSEntry::UDS_TARGET_URL) ); track->setTrackNumber( entry.stringValue(KIO::UPNP_TRACK_NUMBER).toInt() ); // TODO validate and then convert to kbps track->setBitrate( entry.stringValue( KIO::UPNP_BITRATE ).toInt() / 1024 ); track->setLength( duration( entry.stringValue( KIO::UPNP_DURATION ) ) ); Meta::UpnpArtistPtr artist = Meta::UpnpArtistPtr::staticCast( getArtist( entry.stringValue( KIO::UPNP_ARTIST ) ) ); artist->addTrack( track ); track->setArtist( artist ); Meta::UpnpAlbumPtr album = Meta::UpnpAlbumPtr::staticCast( getAlbum( entry.stringValue( KIO::UPNP_ALBUM ), artist->name() ) ); album->setAlbumArtist( artist ); album->addTrack( track ); track->setAlbum( album ); // album art if( ! album->imageLocation().isValid() ) album->setAlbumArtUrl( QUrl(entry.stringValue( KIO::UPNP_ALBUMART_URI )) ); Meta::UpnpGenrePtr genre = Meta::UpnpGenrePtr::staticCast( getGenre( entry.stringValue( KIO::UPNP_GENRE ) ) ); genre->addTrack( track ); track->setGenre( genre ); // TODO this is plain WRONG! the UPNP_DATE will not have year of the album // it will have year of addition to the collection //QString yearStr = yearForDate( entry.stringValue( KIO::UPNP_DATE ) ); // //Meta::UpnpYearPtr year = Meta::UpnpYearPtr::staticCast( getYear( yearStr ) ); //year->addTrack( track ); //track->setYear( year ); m_trackMap.insert( uidUrl, Meta::TrackPtr::staticCast( track ) ); return Meta::TrackPtr::staticCast( track ); } Meta::ArtistPtr UpnpCache::getArtist( const QString& name ) { if( m_artistMap.contains( name ) ) return m_artistMap[name]; Meta::UpnpArtistPtr artist( new Meta::UpnpArtist( name ) ); m_artistMap.insert( name, Meta::ArtistPtr::staticCast( artist ) ); return m_artistMap[name]; } Meta::AlbumPtr UpnpCache::getAlbum(const QString& name, const QString &artist ) { if( m_albumMap.contains( name, artist ) ) return m_albumMap.value( name, artist ); Meta::UpnpAlbumPtr album( new Meta::UpnpAlbum( name ) ); album->setAlbumArtist( Meta::UpnpArtistPtr::staticCast( getArtist( artist ) ) ); m_albumMap.insert( Meta::AlbumPtr::staticCast( album ) ); return Meta::AlbumPtr::staticCast( album ); } Meta::GenrePtr UpnpCache::getGenre(const QString& name) { if( m_genreMap.contains( name ) ) return m_genreMap[name]; Meta::UpnpGenrePtr genre( new Meta::UpnpGenre( name ) ); m_genreMap.insert( name, Meta::GenrePtr::staticCast( genre ) ); return m_genreMap[name]; } Meta::YearPtr UpnpCache::getYear(int name) { if( m_yearMap.contains( name ) ) return m_yearMap[name]; Meta::UpnpYearPtr year( new Meta::UpnpYear( name ) ); m_yearMap.insert( name, Meta::YearPtr::staticCast( year ) ); return m_yearMap[name]; } void UpnpCache::removeTrack( const Meta::TrackPtr &t ) { #define DOWNCAST( Type, item ) Meta::Upnp##Type##Ptr::staticCast( item ) Meta::UpnpTrackPtr track = DOWNCAST( Track, t ); DOWNCAST( Artist, m_artistMap[ track->artist()->name() ] )->removeTrack( track ); DOWNCAST( Album, m_albumMap.value( track->album() ) )->removeTrack( track ); DOWNCAST( Genre, m_genreMap[ track->genre()->name() ] )->removeTrack( track ); DOWNCAST( Year, m_yearMap[ track->year()->year() ] )->removeTrack( track ); #undef DOWNCAST m_trackMap.remove( track->uidUrl() ); } } diff --git a/src/core-impl/meta/cue/CueFileSupport.cpp b/src/core-impl/meta/cue/CueFileSupport.cpp index 7014a283dd..312f04739a 100644 --- a/src/core-impl/meta/cue/CueFileSupport.cpp +++ b/src/core-impl/meta/cue/CueFileSupport.cpp @@ -1,462 +1,462 @@ /**************************************************************************************** * Copyright (c) 2009 Nikolaj Hald Nielsen * * * * 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 Pulic License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "CueFileSupport.h" #include "core/support/Debug.h" #include "core-impl/meta/timecode/TimecodeMeta.h" #include #include #include #include using namespace MetaCue; /** * Parses a cue sheet file into CueFileItems and inserts them in a QMap * @return a map of CueFileItems. If the cue file was not successfully loaded * the map is empty. * @author (C) 2005 by Martin Ehmke */ CueFileItemMap CueFileSupport::loadCueFile( const QUrl &cuefile, const Meta::TrackPtr &track ) { return loadCueFile( cuefile, track->playableUrl(), track->length() ); } CueFileItemMap CueFileSupport::loadCueFile( const QUrl &cuefile, const QUrl &trackUrl, qint64 trackLen ) { DEBUG_BLOCK CueFileItemMap cueItems; debug() << "CUEFILE: " << cuefile.toDisplayString(); if ( QFile::exists ( cuefile.toLocalFile() ) ) { debug() << " EXISTS!"; QFile file ( cuefile.toLocalFile() ); int trackNr = 0; QString defaultArtist; QString defaultAlbum; QString artist; QString title; long length = 0; long prevIndex = -1; bool index00Present = false; long index = -1; bool filesSection = false; bool fileFound = false; int mode = BEGIN; if ( file.open ( QIODevice::ReadOnly ) ) { QTextStream stream ( &file ); QString line; KEncodingProber prober; KEncodingProber::ProberState result = prober.feed( file.readAll() ); file.seek( 0 ); if( result != KEncodingProber::NotMe ) stream.setCodec( QTextCodec::codecForName( prober.encoding() ) ); debug() << "Encoding: " << prober.encoding(); while ( !stream.atEnd() ) { line = stream.readLine().simplified(); if ( line.startsWith ( QLatin1String("title"), Qt::CaseInsensitive ) ) { title = line.mid ( 6 ).remove ( '"' ); if ( mode == BEGIN && !filesSection ) { defaultAlbum = title; title.clear(); debug() << "Album: " << defaultAlbum; } else if( !fileFound ) { title.clear(); continue; } else debug() << "Title: " << title; } else if ( line.startsWith ( QLatin1String("performer"), Qt::CaseInsensitive ) ) { artist = line.mid ( 10 ).remove ( '"' ); if ( mode == BEGIN && !filesSection ) { defaultArtist = artist; artist.clear(); debug() << "Album Artist: " << defaultArtist; } else if( !fileFound ) { artist.clear(); continue; } else debug() << "Artist: " << artist; } else if ( line.startsWith ( QLatin1String("track"), Qt::CaseInsensitive ) && fileFound ) { if ( mode == TRACK_FOUND ) { // not valid, because we have to have an index for the previous track file.close(); debug() << "Mode is TRACK_FOUND, abort."; return CueFileItemMap(); } else if ( mode == INDEX_FOUND ) { if ( artist.isNull() ) artist = defaultArtist; debug() << "Inserting item: " << title << " - " << artist << " on " << defaultAlbum << " (" << trackNr << ")"; // add previous entry to map cueItems.insert ( index, CueFileItem ( title, artist, defaultAlbum, trackNr, index ) ); prevIndex = index; title.clear(); artist.clear(); trackNr = 0; } trackNr = line.section ( ' ',1,1 ).toInt(); debug() << "Track: " << trackNr; mode = TRACK_FOUND; } else if ( line.startsWith ( QLatin1String("index"), Qt::CaseInsensitive ) && fileFound ) { if ( mode == TRACK_FOUND ) { int indexNo = line.section ( ' ',1,1 ).toInt(); if ( indexNo == 1 ) { - QStringList time = line.section ( ' ', -1, -1 ).split ( ':' ); + QStringList time = line.section ( ' ', -1, -1 ).split ( QLatin1Char(':') ); index = time[0].toLong() *60*1000 + time[1].toLong() *1000 + time[2].toLong() *1000/75; //75 frames per second if ( prevIndex != -1 && !index00Present ) // set the prev track's length if there is INDEX01 present, but no INDEX00 { length = index - prevIndex; debug() << "Setting length of track " << cueItems[prevIndex].title() << " to " << length << " msecs."; cueItems[prevIndex].setLength ( length ); } index00Present = false; mode = INDEX_FOUND; length = 0; } else if ( indexNo == 0 ) // gap, use to calc prev track length { - QStringList time = line.section ( ' ', -1, -1 ).split ( ':' ); + QStringList time = line.section ( ' ', -1, -1 ).split ( QLatin1Char(':') ); length = time[0].toLong() * 60 * 1000 + time[1].toLong() * 1000 + time[2].toLong() *1000/75; //75 frames per second if ( prevIndex != -1 ) { length -= prevIndex; //this[prevIndex].getIndex(); debug() << "Setting length of track " << cueItems[prevIndex].title() << " to " << length << " msecs."; cueItems[prevIndex].setLength ( length ); index00Present = true; } else length = 0; } else { debug() << "Skipping unsupported INDEX " << indexNo; } } else { // not valid, because we don't have an associated track file.close(); debug() << "Mode is not TRACK_FOUND but encountered INDEX, abort."; return CueFileItemMap(); } debug() << "index: " << index; } else if( line.startsWith ( QLatin1String("file"), Qt::CaseInsensitive ) ) { QString file = line.mid ( 5 ).remove ( '"' ); if( fileFound ) break; fileFound = file.contains ( trackUrl.fileName(), Qt::CaseInsensitive ); filesSection = true; } } if ( artist.isNull() ) artist = defaultArtist; debug() << "Inserting item: " << title << " - " << artist << " on " << defaultAlbum << " (" << trackNr << ")"; // add previous entry to map cueItems.insert ( index, CueFileItem ( title, artist, defaultAlbum, trackNr, index ) ); file.close(); } /** * Because there is no way to set the length for the last track in a normal way, * we have to do some magic here. Having the total length of the media file given * we can set the length for the last track after all the cue file was loaded into array. */ cueItems[index].setLength ( trackLen - index ); debug() << "Setting length of track " << cueItems[index].title() << " to " << trackLen - index << " msecs."; return cueItems; } return CueFileItemMap(); } QUrl CueFileSupport::locateCueSheet ( const QUrl &trackurl ) { if ( !trackurl.isValid() || !trackurl.isLocalFile() ) return QUrl(); // look for the cue file that matches the media file QString path = trackurl.path(); QString cueFile = path.left ( path.lastIndexOf ( QLatin1Char('.') ) ) + ".cue"; if ( validateCueSheet ( cueFile ) ) { debug() << "[CUEFILE]: " << cueFile << " - Shoot blindly, found and loaded. "; return QUrl::fromLocalFile( cueFile ); } debug() << "[CUEFILE]: " << cueFile << " - Shoot blindly and missed, searching for other cue files."; bool foundCueFile = false; QDir dir ( trackurl.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path() ); QStringList filters; filters << QStringLiteral("*.cue") << QStringLiteral("*.CUE"); dir.setNameFilters ( filters ); QStringList cueFilesList = dir.entryList(); if ( !cueFilesList.empty() ) for ( QStringList::Iterator it = cueFilesList.begin(); it != cueFilesList.end() && !foundCueFile; ++it ) { QFile file ( dir.filePath ( *it ) ); if ( file.open ( QIODevice::ReadOnly ) ) { debug() << "[CUEFILE]: " << *it << " - Opened, looking for the matching FILE stanza." << endl; QTextStream stream ( &file ); QString line; while ( !stream.atEnd() && !foundCueFile ) { line = stream.readLine().simplified(); if ( line.startsWith ( QLatin1String("file"), Qt::CaseInsensitive ) ) { line = line.mid ( 5 ).remove ( '"' ); if ( line.contains ( trackurl.fileName(), Qt::CaseInsensitive ) ) { cueFile = dir.filePath ( *it ); if ( validateCueSheet ( cueFile ) ) { debug() << "[CUEFILE]: " << cueFile << " - Looked inside cue files, found and loaded proper one" << endl; foundCueFile = true; } } } } file.close(); } } if ( foundCueFile ) return QUrl::fromLocalFile( cueFile ); debug() << "[CUEFILE]: - Didn't find any matching cue file." << endl; return QUrl(); } bool CueFileSupport::validateCueSheet ( const QString& cuefile ) { if ( !QFile::exists ( cuefile ) ) return false; QFile file ( cuefile ); int track = 0; QString defaultArtist; QString defaultAlbum; QString artist; QString title; long length = 0; long prevIndex = -1; bool index00Present = false; long index = -1; int mode = BEGIN; if ( file.open ( QIODevice::ReadOnly ) ) { QTextStream stream ( &file ); QString line; while ( !stream.atEnd() ) { line = stream.readLine().simplified(); if ( line.startsWith ( QLatin1String("title"), Qt::CaseInsensitive ) ) { title = line.mid ( 6 ).remove ( '"' ); if ( mode == BEGIN ) { defaultAlbum = title; title.clear(); debug() << "Album: " << defaultAlbum; } else debug() << "Title: " << title; } else if ( line.startsWith ( QLatin1String("performer"), Qt::CaseInsensitive ) ) { artist = line.mid ( 10 ).remove ( '"' ); if ( mode == BEGIN ) { defaultArtist = artist; artist.clear(); debug() << "Album Artist: " << defaultArtist; } else debug() << "Artist: " << artist; } else if ( line.startsWith ( QLatin1String("track"), Qt::CaseInsensitive ) ) { if ( mode == TRACK_FOUND ) { // not valid, because we have to have an index for the previous track file.close(); debug() << "Mode is TRACK_FOUND, abort."; return false; } if ( mode == INDEX_FOUND ) { if ( artist.isNull() ) artist = defaultArtist; prevIndex = index; title.clear(); artist.clear(); track = 0; } track = line.section ( ' ',1,1 ).toInt(); debug() << "Track: " << track; mode = TRACK_FOUND; } else if ( line.startsWith ( QLatin1String("index"), Qt::CaseInsensitive ) ) { if ( mode == TRACK_FOUND ) { int indexNo = line.section ( ' ',1,1 ).toInt(); if ( indexNo == 1 ) { - QStringList time = line.section ( ' ', -1, -1 ).split ( ':' ); + QStringList time = line.section ( ' ', -1, -1 ).split ( QLatin1Char(':') ); index = time[0].toLong() *60*1000 + time[1].toLong() *1000 + time[2].toLong() *1000/75; //75 frames per second if ( prevIndex != -1 && !index00Present ) // set the prev track's length if there is INDEX01 present, but no INDEX00 { length = index - prevIndex; } index00Present = false; mode = INDEX_FOUND; length = 0; } else if ( indexNo == 0 ) // gap, use to calc prev track length { - QStringList time = line.section ( ' ', -1, -1 ).split ( ':' ); + QStringList time = line.section ( ' ', -1, -1 ).split ( QLatin1Char(':') ); length = time[0].toLong() *60*1000 + time[1].toLong() *1000 + time[2].toLong() *1000/75; //75 frames per second if ( prevIndex != -1 ) { length -= prevIndex; //this[prevIndex].getIndex(); index00Present = true; } else length = 0; } else { debug() << "Skipping unsupported INDEX " << indexNo; } } else { // not valid, because we don't have an associated track file.close(); debug() << "Mode is not TRACK_FOUND but encountered INDEX, abort."; return false; } debug() << "index: " << index; } } if( mode == BEGIN ) { file.close(); debug() << "Cue file is invalid"; return false; } if ( artist.isNull() ) artist = defaultArtist; file.close(); } return true; } Meta::TrackList CueFileSupport::generateTimeCodeTracks( Meta::TrackPtr baseTrack, CueFileItemMap itemMap ) { Meta::TrackList trackList; foreach( const CueFileItem &item, itemMap ) { Meta::TimecodeTrack *track = new Meta::TimecodeTrack( item.title(), baseTrack->playableUrl(), item.index(), item.index() + item.length() ); track->beginUpdate(); track->setArtist( item.artist() ); track->setAlbum( item.album() ); track->setTrackNumber( item.trackNumber() ); track->endUpdate(); trackList << Meta::TrackPtr( track ); } return trackList; } diff --git a/src/core-impl/playlists/types/file/PlaylistFile.cpp b/src/core-impl/playlists/types/file/PlaylistFile.cpp index 2dc0b77a12..9a920bea17 100644 --- a/src/core-impl/playlists/types/file/PlaylistFile.cpp +++ b/src/core-impl/playlists/types/file/PlaylistFile.cpp @@ -1,196 +1,196 @@ /**************************************************************************************** * Copyright (c) 2011 Bart Cerneels * * * * 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 "PlaylistFile.h" #include "core/support/Debug.h" #include "core-impl/playlists/types/file/PlaylistFileLoaderJob.h" #include "playlistmanager/file/PlaylistFileProvider.h" #include "playlistmanager/PlaylistManager.h" #include #include #include using namespace Playlists; PlaylistFile::PlaylistFile( const QUrl &url, PlaylistProvider *provider ) : Playlist() , m_provider( provider ) , m_url( url ) , m_tracksLoaded( false ) , m_name( m_url.fileName() ) , m_relativePaths( false ) , m_loadingDone( 0 ) { } void PlaylistFile::saveLater() { PlaylistFileProvider *fileProvider = qobject_cast( m_provider ); if( !fileProvider ) return; fileProvider->saveLater( PlaylistFilePtr( this ) ); } void PlaylistFile::triggerTrackLoad() { if( m_tracksLoaded ) { notifyObserversTracksLoaded(); return; } PlaylistFileLoaderJob *worker = new PlaylistFileLoaderJob( PlaylistFilePtr( this ) ); ThreadWeaver::Queue::instance()->enqueue( QSharedPointer(worker) ); if ( !isLoadingAsync() ) m_loadingDone.acquire(); // after loading is finished worker will release semapore } bool PlaylistFile::isWritable() const { if( m_url.isEmpty() ) return false; return QFileInfo( m_url.path() ).isWritable(); } int PlaylistFile::trackCount() const { if( m_tracksLoaded ) return m_tracks.count(); else return -1; } void PlaylistFile::addTrack( const Meta::TrackPtr &track, int position ) { if( !track ) // playlists might contain invalid tracks. see BUG: 303056 return; int trackPos = position < 0 ? m_tracks.count() : position; if( trackPos > m_tracks.count() ) trackPos = m_tracks.count(); m_tracks.insert( trackPos, track ); // set in case no track was in the playlist before m_tracksLoaded = true; notifyObserversTrackAdded( track, trackPos ); if( !m_url.isEmpty() ) saveLater(); } void PlaylistFile::removeTrack( int position ) { if( position < 0 || position >= m_tracks.count() ) return; m_tracks.removeAt( position ); notifyObserversTrackRemoved( position ); if( !m_url.isEmpty() ) saveLater(); } bool PlaylistFile::save( bool relative ) { m_relativePaths = relative; QMutexLocker locker( &m_saveLock ); //if the location is a directory append the name of this playlist. if( m_url.fileName().isNull() ) { m_url = m_url.adjusted(QUrl::RemoveFilename); m_url.setPath(m_url.path() + name()); } QFile file( m_url.path() ); if( !file.open( QIODevice::WriteOnly ) ) { warning() << QStringLiteral( "Cannot write playlist (%1)." ).arg( file.fileName() ) << file.errorString(); return false; } savePlaylist( file ); file.close(); return true; } void PlaylistFile::setName( const QString &name ) { //can't save to a new file if we don't know where. if( !m_url.isEmpty() && !name.isEmpty() ) { QString exten = QStringLiteral( ".%1" ).arg(extension()); m_url = m_url.adjusted(QUrl::RemoveFilename); m_url.setPath(m_url.path() + name + ( name.endsWith( exten, Qt::CaseInsensitive ) ? QLatin1String("") : exten )); } } void PlaylistFile::addProxyTrack( const Meta::TrackPtr &proxyTrack ) { m_tracks << proxyTrack; notifyObserversTrackAdded( m_tracks.last(), m_tracks.size() - 1 ); } QUrl PlaylistFile::getAbsolutePath( const QUrl &url ) { QUrl absUrl = url; if( url.scheme().isEmpty() ) absUrl.setScheme( QStringLiteral( "file" ) ); if( !absUrl.isLocalFile() ) return url; - if( !url.path().startsWith( '/' ) ) + if( !url.path().startsWith( QLatin1Char('/') ) ) { m_relativePaths = true; // example: url = QUrl( "file://../tunes/tune.ogg" ) absUrl = m_url.adjusted(QUrl::RemoveFilename); // file:///playlists/ absUrl = absUrl.adjusted(QUrl::StripTrailingSlash); absUrl.setPath( absUrl.path() + QLatin1Char('/') + url.path() ); absUrl.setPath( QDir::cleanPath(absUrl.path()) ); // file:///playlists/tunes/tune.ogg } return absUrl; } QString PlaylistFile::trackLocation( const Meta::TrackPtr &track ) const { QUrl path = track->playableUrl(); if( path.isEmpty() ) return track->uidUrl(); if( !m_relativePaths || m_url.isEmpty() || !path.isLocalFile() || !m_url.isLocalFile() ) return path.toEncoded(); QDir playlistDir( m_url.adjusted(QUrl::RemoveFilename).path() ); return QUrl::toPercentEncoding( playlistDir.relativeFilePath( path.path() ), "/" ); } diff --git a/src/core-impl/playlists/types/file/m3u/M3UPlaylist.cpp b/src/core-impl/playlists/types/file/m3u/M3UPlaylist.cpp index f6aee9f4d6..facb243e74 100644 --- a/src/core-impl/playlists/types/file/m3u/M3UPlaylist.cpp +++ b/src/core-impl/playlists/types/file/m3u/M3UPlaylist.cpp @@ -1,115 +1,115 @@ /**************************************************************************************** * Copyright (c) 2007 Bart Cerneels * * * * 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 "M3UPlaylist.h" #include "core/support/Debug.h" #include using namespace Playlists; M3UPlaylist::M3UPlaylist( const QUrl &url, PlaylistProvider *provider ) : PlaylistFile( url, provider ) { } bool M3UPlaylist::loadM3u( QTextStream &stream ) { if( m_tracksLoaded ) return true; m_tracksLoaded = true; int length = -1; QString extinfTitle; do { QString line = stream.readLine(); if( line.startsWith( QLatin1String("#EXTINF") ) ) { - const QString extinf = line.section( ':', 1 ); + const QString extinf = line.section( QLatin1Char(':'), 1 ); bool ok; length = extinf.section( ',', 0, 0 ).toInt( &ok ); if( !ok ) length = -1; extinfTitle = extinf.section( ',', 1 ); } else if( !line.startsWith( '#' ) && !line.isEmpty() ) { line = line.replace( QLatin1String("\\"), QLatin1String("/") ); QUrl url = getAbsolutePath( QUrl( line ) ); MetaProxy::TrackPtr proxyTrack( new MetaProxy::Track( url ) ); QString artist = extinfTitle.section( QStringLiteral(" - "), 0, 0 ); QString title = extinfTitle.section( QStringLiteral(" - "), 1, 1 ); //if title and artist are saved such as in M3UPlaylist::save() if( !title.isEmpty() && !artist.isEmpty() ) { proxyTrack->setTitle( title ); proxyTrack->setArtist( artist ); } else { proxyTrack->setTitle( extinfTitle ); } proxyTrack->setLength( length ); Meta::TrackPtr track( proxyTrack.data() ); addProxyTrack( track ); } } while( !stream.atEnd() ); //TODO: return false if stream is not readable, empty or has errors return true; } void M3UPlaylist::savePlaylist( QFile &file ) { QTextStream stream( &file ); stream << "#EXTM3U\n"; QList urls; QStringList titles; QList lengths; foreach( const Meta::TrackPtr &track, m_tracks ) { if( !track ) // see BUG: 303056 continue; const QUrl &url = track->playableUrl(); int length = track->length() / 1000; const QString &title = track->name(); const QString &artist = track->artist()->name(); if( !title.isEmpty() && !artist.isEmpty() && length ) { stream << "#EXTINF:"; stream << QString::number( length ); stream << ','; stream << artist << " - " << title; stream << '\n'; } if( url.scheme() == QLatin1String("file") ) stream << trackLocation( track ); else stream << url.url(); stream << "\n"; } } diff --git a/src/core-impl/playlists/types/file/pls/PLSPlaylist.cpp b/src/core-impl/playlists/types/file/pls/PLSPlaylist.cpp index 0448ee1b08..1db5f5448e 100644 --- a/src/core-impl/playlists/types/file/pls/PLSPlaylist.cpp +++ b/src/core-impl/playlists/types/file/pls/PLSPlaylist.cpp @@ -1,216 +1,216 @@ /**************************************************************************************** * Copyright (c) 2007 Bart Cerneels * * * * 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 "PLSPlaylist.h" #include "core/support/Debug.h" #include using namespace Playlists; PLSPlaylist::PLSPlaylist( const QUrl &url, PlaylistProvider *provider ) : PlaylistFile( url, provider ) { } bool PLSPlaylist::loadPls( QTextStream &textStream ) { if( m_tracksLoaded ) return true; m_tracksLoaded = true; // Counted number of "File#=" lines. unsigned int entryCnt = 0; // Value of the "NumberOfEntries=#" line. unsigned int numberOfEntries = 0; // Does the file have a "[playlist]" section? (as it's required by the standard) bool havePlaylistSection = false; QString tmp; QStringList lines; QRegExp regExp_NumberOfEntries("^NumberOfEntries\\s*=\\s*\\d+$"); regExp_NumberOfEntries.setCaseSensitivity( Qt::CaseInsensitive ); // It seems many playlists use numberofentries const QRegExp regExp_File("^File\\d+\\s*="); const QRegExp regExp_Title("^Title\\d+\\s*="); const QRegExp regExp_Length("^Length\\d+\\s*=\\s*-?\\d+$"); // Length Can be -1 const QRegExp regExp_Version("^Version\\s*=\\s*\\d+$"); const QString section_playlist(QStringLiteral("[playlist]")); /* Preprocess the input data. * Read the lines into a buffer; Cleanup the line strings; * Count the entries manually and read "NumberOfEntries". */ while( !textStream.atEnd() ) { tmp = textStream.readLine(); tmp = tmp.trimmed(); if( tmp.isEmpty() ) continue; lines.append( tmp ); if( tmp.contains( regExp_File ) ) { entryCnt++; continue; } if( tmp == section_playlist ) { havePlaylistSection = true; continue; } if( tmp.contains( regExp_NumberOfEntries ) ) { - numberOfEntries = tmp.section( '=', -1 ).trimmed().toUInt(); + numberOfEntries = tmp.section( QLatin1Char('='), -1 ).trimmed().toUInt(); continue; } } if( numberOfEntries != entryCnt ) { warning() << ".pls playlist: Invalid \"NumberOfEntries\" value. " << "NumberOfEntries=" << numberOfEntries << " counted=" << entryCnt << endl; /* Corrupt file. The "NumberOfEntries" value is * not correct. Fix it by setting it to the manually * counted number and go on parsing. */ numberOfEntries = entryCnt; } if( numberOfEntries == 0 ) { return true; } unsigned int index; bool ok = false; bool inPlaylistSection = false; MetaProxy::TrackPtr proxyTrack; /* Now iterate through all beautified lines in the buffer * and parse the playlist data. */ QStringList::const_iterator i = lines.constBegin(), end = lines.constEnd(); for( ; i != end; ++i ) { if( !inPlaylistSection && havePlaylistSection ) { /* The playlist begins with the "[playlist]" tag. * Skip everything before this. */ if( (*i) == section_playlist ) inPlaylistSection = true; continue; } if( (*i).contains( regExp_File ) ) { // Have a "File#=XYZ" line. index = loadPls_extractIndex( *i ); if( index > numberOfEntries || index == 0 ) continue; - tmp = (*i).section( '=', 1 ).trimmed(); + tmp = (*i).section( QLatin1Char('='), 1 ).trimmed(); QUrl url = getAbsolutePath( QUrl( tmp ) ); proxyTrack = new MetaProxy::Track( url ); Meta::TrackPtr track( proxyTrack.data() ); addProxyTrack( track ); continue; } if( (*i).contains(regExp_Title) && proxyTrack ) { // Have a "Title#=XYZ" line. index = loadPls_extractIndex(*i); if( index > numberOfEntries || index == 0 ) continue; - tmp = (*i).section( '=', 1 ).trimmed(); + tmp = (*i).section( QLatin1Char('='), 1 ).trimmed(); proxyTrack->setTitle( tmp ); continue; } if( (*i).contains( regExp_Length ) && proxyTrack ) { // Have a "Length#=XYZ" line. index = loadPls_extractIndex(*i); if( index > numberOfEntries || index == 0 ) continue; - tmp = (*i).section( '=', 1 ).trimmed(); + tmp = (*i).section( QLatin1Char('='), 1 ).trimmed(); bool ok = false; int seconds = tmp.toInt( &ok ); if( ok ) proxyTrack->setLength( seconds * 1000 ); //length is in milliseconds continue; } if( (*i).contains( regExp_NumberOfEntries ) ) { // Have the "NumberOfEntries=#" line. continue; } if( (*i).contains( regExp_Version ) ) { // Have the "Version=#" line. - tmp = (*i).section( '=', 1 ).trimmed(); + tmp = (*i).section( QLatin1Char('='), 1 ).trimmed(); // We only support Version=2 if (tmp.toUInt( &ok ) != 2) warning() << ".pls playlist: Unsupported version." << endl; continue; } warning() << ".pls playlist: Unrecognized line: \"" << *i << "\"" << endl; } return true; } unsigned int PLSPlaylist::loadPls_extractIndex( const QString &str ) const { /* Extract the index number out of a .pls line. * Example: * loadPls_extractIndex("File2=foobar") == 2 */ bool ok = false; unsigned int ret; - QString tmp( str.section( '=', 0, 0 ) ); + QString tmp( str.section( QLatin1Char('='), 0, 0 ) ); tmp.remove( QRegExp( "^\\D*" ) ); ret = tmp.trimmed().toUInt( &ok ); Q_ASSERT(ok); return ret; } void PLSPlaylist::savePlaylist( QFile &file ) { //Format: http://en.wikipedia.org/wiki/PLS_(file_format) QTextStream stream( &file ); //header stream << "[Playlist]\n"; //body int i = 1; //PLS starts at File1= foreach( Meta::TrackPtr track, m_tracks ) { if( !track ) // see BUG: 303056 continue; stream << "File" << i << "=" << trackLocation( track ); stream << "\nTitle" << i << "="; stream << track->name(); stream << "\nLength" << i << "="; stream << track->length() / 1000; stream << "\n"; i++; } //footer stream << "NumberOfEntries=" << m_tracks.count() << endl; stream << "Version=2\n"; } diff --git a/src/core/podcasts/PodcastImageFetcher.cpp b/src/core/podcasts/PodcastImageFetcher.cpp index 0a1d35340b..7f23c29b64 100644 --- a/src/core/podcasts/PodcastImageFetcher.cpp +++ b/src/core/podcasts/PodcastImageFetcher.cpp @@ -1,166 +1,166 @@ /**************************************************************************************** * Copyright (c) 2009 Bart Cerneels * * * * 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 "core/podcasts/PodcastImageFetcher.h" #include "core/support/Debug.h" #include #include #include PodcastImageFetcher::PodcastImageFetcher() { } void PodcastImageFetcher::addChannel( Podcasts::PodcastChannelPtr channel ) { DEBUG_BLOCK if( channel->imageUrl().isEmpty() ) { debug() << channel->title() << " does not have an imageUrl"; return; } if( hasCachedImage( channel ) ) { debug() << "using cached image for " << channel->title(); QString imagePath = cachedImagePath( channel ).toLocalFile(); QImage image( imagePath ); if( image.isNull() ) error() << "could not load pixmap from " << imagePath; else channel->setImage( image ); return; } if( m_channels.contains( channel ) ) { debug() << "Channel already queued:" << channel->title(); return; } if( m_jobChannelMap.values().contains( channel ) ) { debug() << "Copy job already running for channel:" << channel->title(); return; } debug() << "Adding " << channel->title() << " to fetch queue"; m_channels.append( channel ); } void PodcastImageFetcher::addEpisode( const Podcasts::PodcastEpisodePtr &episode ) { Q_UNUSED( episode ); } QUrl PodcastImageFetcher::cachedImagePath( const Podcasts::PodcastChannelPtr &channel ) { return cachedImagePath( channel.data() ); } QUrl PodcastImageFetcher::cachedImagePath( Podcasts::PodcastChannel *channel ) { QUrl imagePath = channel->saveLocation(); if( imagePath.isEmpty() || !imagePath.isLocalFile() ) imagePath = QUrl::fromLocalFile( Amarok::saveLocation( QStringLiteral("podcasts") ) ); QCryptographicHash md5( QCryptographicHash::Md5 ); md5.addData( channel->url().url().toLocal8Bit() ); QString extension = Amarok::extension( channel->imageUrl().fileName() ); imagePath = imagePath.adjusted( QUrl::StripTrailingSlash ); - imagePath.setPath( imagePath.path() + QLatin1Char('/') + ( md5.result().toHex() + '.' + extension ) ); + imagePath.setPath( imagePath.path() + QLatin1Char('/') + ( md5.result().toHex() + QLatin1Char('.') + extension ) ); return imagePath; } bool PodcastImageFetcher::hasCachedImage( const Podcasts::PodcastChannelPtr &channel ) { DEBUG_BLOCK return QFile( PodcastImageFetcher::cachedImagePath( Podcasts::PodcastChannelPtr::dynamicCast( channel ) ).toLocalFile() ).exists(); } void PodcastImageFetcher::run() { if( m_channels.isEmpty() && m_episodes.isEmpty() && m_jobChannelMap.isEmpty() && m_jobEpisodeMap.isEmpty() ) { //nothing to do Q_EMIT( done( this ) ); return; } QNetworkConfigurationManager mgr; if( !mgr.isOnline() ) { debug() << "QNetworkConfigurationManager reports we are not online, canceling podcast image download"; Q_EMIT( done( this ) ); //TODO: schedule another run after Solid reports we are online again return; } foreach( Podcasts::PodcastChannelPtr channel, m_channels ) { QUrl cachedPath = cachedImagePath( channel ); KIO::mkdir( cachedPath.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash) ); KIO::FileCopyJob *job = KIO::file_copy( channel->imageUrl(), cachedPath, -1, KIO::HideProgressInfo | KIO::Overwrite ); //remove channel from the todo list m_channels.removeAll( channel ); m_jobChannelMap.insert( job, channel ); connect( job, &KIO::FileCopyJob::finished, this, &PodcastImageFetcher::slotDownloadFinished ); } //TODO: episodes } void PodcastImageFetcher::slotDownloadFinished( KJob *job ) { DEBUG_BLOCK //QMap::take() also removes the entry from the map. Podcasts::PodcastChannelPtr channel = m_jobChannelMap.take( job ); if( channel.isNull() ) { error() << "got null PodcastChannelPtr " << __FILE__ << ":" << __LINE__; return; } if( job->error() ) { error() << "downloading podcast image " << job->errorString(); } else { QString imagePath = cachedImagePath( channel ).toLocalFile(); QImage image( imagePath ); if( image.isNull() ) error() << "could not load pixmap from " << imagePath; else channel->setImage( image ); } //call run again to start the next batch of transfers. run(); } diff --git a/src/core/podcasts/PodcastReader.cpp b/src/core/podcasts/PodcastReader.cpp index be34e8e233..5addb94a3a 100644 --- a/src/core/podcasts/PodcastReader.cpp +++ b/src/core/podcasts/PodcastReader.cpp @@ -1,1688 +1,1688 @@ /**************************************************************************************** * Copyright (c) 2007 Bart Cerneels * * 2009 Mathias Panzenböck * * 2013 Ralf Engels * * * * 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 "core/podcasts/PodcastReader.h" #include "core/support/Amarok.h" #include "core/support/Components.h" #include "core/support/Debug.h" #include "core/meta/support/MetaUtility.h" #include #include #include using namespace Podcasts; #define ITUNES_NS "http://www.itunes.com/dtds/podcast-1.0.dtd" #define RDF_NS "http://www.w3.org/1999/02/22-rdf-syntax-ns#" #define RSS10_NS "http://purl.org/rss/1.0/" #define RSS20_NS "" #define ATOM_NS "http://www.w3.org/2005/Atom" #define ENC_NS "http://purl.oclc.org/net/rss_2.0/enc#" #define CONTENT_NS "http://purl.org/rss/1.0/modules/content" #define DC_NS "http://purl.org/dc/elements/1.1/" // regular expressions for linkification: #define RE_USER "[-+_%\\.\\w]+" #define RE_PASSWD RE_USER #define RE_DOMAIN "[-a-zA-Z0-9]+(?:\\.[-a-zA-Z0-9]+)*" #define RE_PROT "[a-zA-Z]+://" #define RE_URL RE_PROT "(?:" RE_USER "(?::" RE_PASSWD ")?@)?" RE_DOMAIN \ "(?::\\d+)?(?:/[-\\w\\?&=%+.,;:_#~/!@]*)?" #define RE_MAIL RE_USER "@" RE_DOMAIN const PodcastReader::StaticData PodcastReader::sd; PodcastReader::PodcastReader( PodcastProvider *podcastProvider, QObject *parent ) : QObject( parent ) , m_xmlReader() , m_podcastProvider( podcastProvider ) , m_transferJob( ) , m_current( 0 ) , m_actionStack() , m_contentType( TextContent ) , m_buffer() {} void PodcastReader::Action::begin( PodcastReader *podcastReader ) const { if( m_begin ) (( *podcastReader ).*m_begin )(); } void PodcastReader::Action::end( PodcastReader *podcastReader ) const { if( m_end ) (( *podcastReader ).*m_end )(); } void PodcastReader::Action::characters( PodcastReader *podcastReader ) const { if( m_characters ) (( *podcastReader ).*m_characters )(); } // initialization of the feed parser automata: PodcastReader::StaticData::StaticData() - : removeScripts( "|]*>", Qt::CaseInsensitive ) + : removeScripts( QStringLiteral("|]*>"), Qt::CaseInsensitive ) , mightBeHtml( "<\\?xml[^>]*\\?>|]*>|]*>|<|>|&|"|" "<([-:\\w\\d]+)[^>]*(/>|>.*)|]*>|&#\\d+;|&#x[a-fA-F\\d]+;", Qt::CaseInsensitive ) , linkify( "\\b(" RE_URL ")|\\b(" RE_MAIL ")|(\n)" ) , startAction( rootMap ) , docAction( docMap, 0, &PodcastReader::endDocument ) , xmlAction( xmlMap, &PodcastReader::beginXml, &PodcastReader::endXml, &PodcastReader::readEscapedCharacters ) , skipAction( skipMap ) , noContentAction( noContentMap, &PodcastReader::beginNoElement, 0, &PodcastReader::readNoCharacters ) , rdfAction( rdfMap, &PodcastReader::beginRdf ) , rssAction( rssMap, &PodcastReader::beginRss ) , feedAction( feedMap, &PodcastReader::beginFeed ) , htmlAction( skipMap, &PodcastReader::beginHtml ) , unknownFeedTypeAction( skipMap, &PodcastReader::beginUnknownFeedType ) // RSS 1.0+2.0 , rss10ChannelAction( rss10ChannelMap, &PodcastReader::beginChannel ) , rss20ChannelAction( rss20ChannelMap, &PodcastReader::beginChannel ) , titleAction( textMap, &PodcastReader::beginText, &PodcastReader::endTitle, &PodcastReader::readCharacters ) , subtitleAction( textMap, &PodcastReader::beginText, &PodcastReader::endSubtitle, &PodcastReader::readCharacters ) , descriptionAction( textMap, &PodcastReader::beginText, &PodcastReader::endDescription, &PodcastReader::readCharacters ) , encodedAction( textMap, &PodcastReader::beginText, &PodcastReader::endEncoded, &PodcastReader::readCharacters ) , bodyAction( xmlMap, &PodcastReader::beginText, &PodcastReader::endBody, &PodcastReader::readEscapedCharacters ) , linkAction( textMap, &PodcastReader::beginText, &PodcastReader::endLink, &PodcastReader::readCharacters ) , imageAction( imageMap, &PodcastReader::beginImage ) , itemAction( itemMap, &PodcastReader::beginItem, &PodcastReader::endItem ) , urlAction( textMap, &PodcastReader::beginText, &PodcastReader::endImageUrl, &PodcastReader::readCharacters ) , authorAction( textMap, &PodcastReader::beginText, &PodcastReader::endAuthor, &PodcastReader::readCharacters ) , creatorAction( textMap, &PodcastReader::beginText, &PodcastReader::endCreator, &PodcastReader::readCharacters ) , enclosureAction( noContentMap, &PodcastReader::beginEnclosure ) , guidAction( textMap, &PodcastReader::beginText, &PodcastReader::endGuid, &PodcastReader::readCharacters ) , pubDateAction( textMap, &PodcastReader::beginText, &PodcastReader::endPubDate, &PodcastReader::readCharacters ) , keywordsAction( textMap, &PodcastReader::beginText, &PodcastReader::endKeywords, &PodcastReader::readCharacters ) , newFeedUrlAction( textMap, &PodcastReader::beginText, &PodcastReader::endNewFeedUrl, &PodcastReader::readCharacters ) // Atom , atomLogoAction( textMap, &PodcastReader::beginText, &PodcastReader::endImageUrl, &PodcastReader::readCharacters ) , atomIconAction( textMap, &PodcastReader::beginText, &PodcastReader::endAtomIcon, &PodcastReader::readCharacters ) , atomEntryAction( atomEntryMap, &PodcastReader::beginItem, &PodcastReader::endItem ) , atomTitleAction( atomTextMap, &PodcastReader::beginAtomText, &PodcastReader::endAtomTitle, &PodcastReader::readAtomTextCharacters ) , atomSubtitleAction( atomTextMap, &PodcastReader::beginAtomText, &PodcastReader::endAtomSubtitle, &PodcastReader::readAtomTextCharacters ) , atomAuthorAction( atomAuthorMap ) , atomFeedLinkAction( noContentMap, &PodcastReader::beginAtomFeedLink, 0, &PodcastReader::readNoCharacters ) , atomEntryLinkAction( noContentMap, &PodcastReader::beginAtomEntryLink, 0, &PodcastReader::readNoCharacters ) , atomIdAction( textMap, &PodcastReader::beginText, &PodcastReader::endGuid, &PodcastReader::readCharacters ) , atomPublishedAction( textMap, &PodcastReader::beginText, &PodcastReader::endAtomPublished, &PodcastReader::readCharacters ) , atomUpdatedAction( textMap, &PodcastReader::beginText, &PodcastReader::endAtomUpdated, &PodcastReader::readCharacters ) , atomSummaryAction( atomTextMap, &PodcastReader::beginAtomText, &PodcastReader::endAtomSummary, &PodcastReader::readAtomTextCharacters ) , atomContentAction( atomTextMap, &PodcastReader::beginAtomText, &PodcastReader::endAtomContent, &PodcastReader::readAtomTextCharacters ) , atomTextAction( atomTextMap, &PodcastReader::beginAtomTextChild, &PodcastReader::endAtomTextChild, &PodcastReader::readAtomTextCharacters ) { // known elements: knownElements[ QStringLiteral("rss") ] = Rss; knownElements[ QStringLiteral("RDF") ] = Rdf; knownElements[ QStringLiteral("feed") ] = Feed; knownElements[ QStringLiteral("channel") ] = Channel; knownElements[ QStringLiteral("item") ] = Item; knownElements[ QStringLiteral("image") ] = Image; knownElements[ QStringLiteral("link") ] = Link; knownElements[ QStringLiteral("url") ] = Url; knownElements[ QStringLiteral("title") ] = Title; knownElements[ QStringLiteral("author") ] = Author; knownElements[ QStringLiteral("enclosure") ] = EnclosureElement; knownElements[ QStringLiteral("guid") ] = Guid; knownElements[ QStringLiteral("pubDate") ] = PubDate; knownElements[ QStringLiteral("description") ] = Description; knownElements[ QStringLiteral("summary") ] = Summary; knownElements[ QStringLiteral("body") ] = Body; knownElements[ QStringLiteral("entry") ] = Entry; knownElements[ QStringLiteral("content") ] = Content; knownElements[ QStringLiteral("name") ] = Name; knownElements[ QStringLiteral("id") ] = Id; knownElements[ QStringLiteral("subtitle") ] = Subtitle; knownElements[ QStringLiteral("updated") ] = Updated; knownElements[ QStringLiteral("published") ] = Published; knownElements[ QStringLiteral("logo") ] = Logo; knownElements[ QStringLiteral("icon") ] = Icon; knownElements[ QStringLiteral("encoded") ] = Encoded; knownElements[ QStringLiteral("creator") ] = Creator; knownElements[ QStringLiteral("keywords") ] = Keywords; knownElements[ QStringLiteral("new-feed-url") ] = NewFeedUrl; knownElements[ QStringLiteral("html") ] = Html; knownElements[ QStringLiteral("HTML") ] = Html; // before start document/after end document rootMap.insert( Document, &docAction ); // parse document docMap.insert( Rss, &rssAction ); docMap.insert( Html, &htmlAction ); docMap.insert( Rdf, &rdfAction ); docMap.insert( Feed, &feedAction ); docMap.insert( Any, &unknownFeedTypeAction ); // parse "RSS 2.0" rssMap.insert( Channel, &rss20ChannelAction ); // parse "RSS 1.0" rdfMap.insert( Channel, &rss10ChannelAction ); rdfMap.insert( Item, &itemAction ); // parse "RSS 2.0" rss20ChannelMap.insert( Title, &titleAction ); rss20ChannelMap.insert( ItunesSubtitle, &subtitleAction ); rss20ChannelMap.insert( ItunesAuthor, &authorAction ); rss20ChannelMap.insert( Creator, &creatorAction ); rss20ChannelMap.insert( Description, &descriptionAction ); rss20ChannelMap.insert( Encoded, &encodedAction ); rss20ChannelMap.insert( ItunesSummary, &descriptionAction ); rss20ChannelMap.insert( Body, &bodyAction ); rss20ChannelMap.insert( Link, &linkAction ); rss20ChannelMap.insert( Image, &imageAction ); rss20ChannelMap.insert( ItunesKeywords, &keywordsAction ); rss20ChannelMap.insert( NewFeedUrl, &newFeedUrlAction ); rss20ChannelMap.insert( Item, &itemAction ); // parse "RSS 1.0" rss10ChannelMap.insert( Title, &titleAction ); rss10ChannelMap.insert( ItunesSubtitle, &subtitleAction ); rss10ChannelMap.insert( ItunesAuthor, &authorAction ); rss10ChannelMap.insert( Creator, &creatorAction ); rss10ChannelMap.insert( Description, &descriptionAction ); rss10ChannelMap.insert( Encoded, &encodedAction ); rss10ChannelMap.insert( ItunesSummary, &descriptionAction ); rss10ChannelMap.insert( Body, &bodyAction ); rss10ChannelMap.insert( Link, &linkAction ); rss10ChannelMap.insert( Image, &imageAction ); rss10ChannelMap.insert( ItunesKeywords, &keywordsAction ); rss10ChannelMap.insert( NewFeedUrl, &newFeedUrlAction ); // parse imageMap.insert( Title, &skipAction ); imageMap.insert( Link, &skipAction ); imageMap.insert( Url, &urlAction ); // parse itemMap.insert( Title, &titleAction ); itemMap.insert( ItunesSubtitle, &subtitleAction ); itemMap.insert( Author, &authorAction ); itemMap.insert( ItunesAuthor, &authorAction ); itemMap.insert( Creator, &creatorAction ); itemMap.insert( Description, &descriptionAction ); itemMap.insert( Encoded, &encodedAction ); itemMap.insert( ItunesSummary, &descriptionAction ); itemMap.insert( Body, &bodyAction ); itemMap.insert( EnclosureElement, &enclosureAction ); itemMap.insert( Guid, &guidAction ); itemMap.insert( PubDate, &pubDateAction ); itemMap.insert( ItunesKeywords, &keywordsAction ); // TODO: move the link field from PodcastChannel to PodcastMetaCommon // itemMap.insert( Link, &linkAction ); // parse "Atom" feedMap.insert( Title, &atomTitleAction ); feedMap.insert( Subtitle, &atomSubtitleAction ); feedMap.insert( Icon, &atomIconAction ); feedMap.insert( Logo, &atomLogoAction ); feedMap.insert( Author, &atomAuthorAction ); feedMap.insert( Link, &atomFeedLinkAction ); feedMap.insert( Entry, &atomEntryAction ); // parse "Atom" atomEntryMap.insert( Title, &atomTitleAction ); atomEntryMap.insert( Subtitle, &atomSubtitleAction ); atomEntryMap.insert( Author, &atomAuthorAction ); atomEntryMap.insert( Id, &atomIdAction ); atomEntryMap.insert( Published, &atomPublishedAction ); atomEntryMap.insert( Updated, &atomUpdatedAction ); atomEntryMap.insert( Summary, &atomSummaryAction ); atomEntryMap.insert( Link, &atomEntryLinkAction ); atomEntryMap.insert( SupportedContent, &atomContentAction ); // parse "Atom" atomAuthorMap.insert( Name, &authorAction ); // parse atom text atomTextMap.insert( Any, &atomTextAction ); // parse arbitrary xml xmlMap.insert( Any, &xmlAction ); // skip elements skipMap.insert( Any, &skipAction ); } PodcastReader::~PodcastReader() { DEBUG_BLOCK } bool PodcastReader::mightBeHtml( const QString& text ) //Static { return sd.mightBeHtml.indexIn( text ) != -1; } bool PodcastReader::read( QIODevice *device ) { DEBUG_BLOCK m_xmlReader.setDevice( device ); return read(); } bool PodcastReader::read( const QUrl &url ) { DEBUG_BLOCK m_url = url; m_transferJob = KIO::get( m_url, KIO::Reload, KIO::HideProgressInfo ); connect( m_transferJob, &KIO::TransferJob::data, this, &PodcastReader::slotAddData ); connect( m_transferJob, &KIO::TransferJob::result, this, &PodcastReader::downloadResult ); connect( m_transferJob, &KIO::TransferJob::redirection, this, &PodcastReader::slotRedirection ); connect( m_transferJob, &KIO::TransferJob::permanentRedirection, this, &PodcastReader::slotPermanentRedirection ); QString description = i18n( "Importing podcast channel from %1", url.url() ); if( m_channel ) { description = m_channel->title().isEmpty() ? i18n( "Updating podcast channel" ) : i18n( "Updating \"%1\"", m_channel->title() ); } Q_EMIT statusBarNewProgressOperation( m_transferJob, description, this ); // parse data return read(); } void PodcastReader::slotAbort() { DEBUG_BLOCK } bool PodcastReader::update( const PodcastChannelPtr &channel ) { DEBUG_BLOCK m_channel = channel; return read( m_channel->url() ); } void PodcastReader::slotAddData( KIO::Job *job, const QByteArray &data ) { DEBUG_BLOCK Q_UNUSED( job ) m_xmlReader.addData( data ); // parse more data continueRead(); } void PodcastReader::downloadResult( KJob * job ) { DEBUG_BLOCK // parse more data continueRead(); KIO::TransferJob *transferJob = dynamic_cast( job ); if( transferJob && transferJob->isErrorPage() ) { QString errorMessage = i18n( "Importing podcast from %1 failed with error:\n", m_url.url() ); if( m_channel ) { errorMessage = m_channel->title().isEmpty() ? i18n( "Updating podcast from %1 failed with error:\n", m_url.url() ) : i18n( "Updating \"%1\" failed with error:\n", m_channel->title() ); } errorMessage = errorMessage.append( job->errorString() ); Q_EMIT statusBarSorryMessage( errorMessage ); } else if( job->error() ) { QString errorMessage = i18n( "Importing podcast from %1 failed with error:\n", m_url.url() ); if( m_channel ) { errorMessage = m_channel->title().isEmpty() ? i18n( "Updating podcast from %1 failed with error:\n", m_url.url() ) : i18n( "Updating \"%1\" failed with error:\n", m_channel->title() ); } errorMessage = errorMessage.append( job->errorString() ); Q_EMIT statusBarSorryMessage( errorMessage ); } - m_transferJob = 0; + m_transferJob = nullptr; } PodcastReader::ElementType PodcastReader::elementType() const { if( m_xmlReader.isEndDocument() || m_xmlReader.isStartDocument() ) return Document; if( m_xmlReader.isCDATA() || m_xmlReader.isCharacters() ) return CharacterData; ElementType elementType = sd.knownElements[ m_xmlReader.name().toString()]; // This is a bit hacky because my automata does not support conditions. // Therefore I put the decision logic in here and declare some pseudo elements. // I don't think it is worth it to extend the automata to support such conditions. switch( elementType ) { case Summary: if( m_xmlReader.namespaceUri() == ITUNES_NS ) { elementType = ItunesSummary; } break; case Subtitle: if( m_xmlReader.namespaceUri() == ITUNES_NS ) { elementType = ItunesSubtitle; } break; case Author: if( m_xmlReader.namespaceUri() == ITUNES_NS ) { elementType = ItunesAuthor; } break; case Keywords: if( m_xmlReader.namespaceUri() == ITUNES_NS ) { elementType = ItunesKeywords; } break; case Content: if( m_xmlReader.namespaceUri() == ATOM_NS && // ignore atom:content elements that do not // have content but only refer to some url: !hasAttribute( ATOM_NS, "src" ) ) { // Atom supports arbitrary Base64 encoded content. // Because we can only something with text/html/xhtml I ignore // anything else. // See: // http://tools.ietf.org/html/rfc4287#section-4.1.3 if( hasAttribute( ATOM_NS, "type" ) ) { QStringRef type( attribute( ATOM_NS, "type" ) ); if( type == "text" || type == "html" || type == "xhtml" ) { elementType = SupportedContent; } } else { elementType = SupportedContent; } } break; default: break; } return elementType; } bool PodcastReader::read() { DEBUG_BLOCK m_current = 0; m_item = 0; m_contentType = TextContent; m_buffer.clear(); m_actionStack.clear(); m_actionStack.push( &( PodcastReader::sd.startAction ) ); m_xmlReader.setNamespaceProcessing( true ); return continueRead(); } bool PodcastReader::continueRead() { // this is some kind of pushdown automata // with this it should be possible to parse feeds in parallel // without using threads DEBUG_BLOCK while( !m_xmlReader.atEnd() && m_xmlReader.error() != QXmlStreamReader::CustomError ) { QXmlStreamReader::TokenType token = m_xmlReader.readNext(); if( m_xmlReader.error() == QXmlStreamReader::PrematureEndOfDocumentError && m_transferJob ) { return true; } if( m_xmlReader.hasError() ) { Q_EMIT finished( this ); return false; } if( m_actionStack.isEmpty() ) { debug() << "expected element on stack!"; return false; } const Action* action = m_actionStack.top(); const Action* subAction = 0; switch( token ) { case QXmlStreamReader::Invalid: return false; case QXmlStreamReader::StartDocument: case QXmlStreamReader::StartElement: subAction = action->actionMap()[ elementType()]; if( !subAction ) subAction = action->actionMap()[ Any ]; if( !subAction ) subAction = &( PodcastReader::sd.skipAction ); m_actionStack.push( subAction ); subAction->begin( this ); break; case QXmlStreamReader::EndDocument: case QXmlStreamReader::EndElement: action->end( this ); if( m_actionStack.pop() != action ) { debug() << "popped other element than expected!"; } break; case QXmlStreamReader::Characters: if( !m_xmlReader.isWhitespace() || m_xmlReader.isCDATA() ) { action->characters( this ); } // ignorable whitespaces case QXmlStreamReader::Comment: case QXmlStreamReader::EntityReference: case QXmlStreamReader::ProcessingInstruction: case QXmlStreamReader::DTD: case QXmlStreamReader::NoToken: // ignore break; } } return !m_xmlReader.hasError(); } void PodcastReader::stopWithError( const QString &message ) { m_xmlReader.raiseError( message ); if( m_transferJob ) { m_transferJob->kill(KJob::EmitResult); m_transferJob = 0; } Q_EMIT finished( this ); } void PodcastReader::beginText() { m_buffer.clear(); } void PodcastReader::endTitle() { m_current->setTitle( m_buffer.trimmed() ); } void PodcastReader::endSubtitle() { m_current->setSubtitle( m_buffer.trimmed() ); } QString PodcastReader::atomTextAsText() { switch( m_contentType ) { case HtmlContent: case XHtmlContent: // TODO: strip tags (there should not be any non-xml entities here) return unescape( m_buffer ); case TextContent: default: return m_buffer; } } QString PodcastReader::atomTextAsHtml() { switch( m_contentType ) { case HtmlContent: case XHtmlContent: // strip