diff --git a/CMakeLists.txt b/CMakeLists.txt index 14e645ab..64797dce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,189 +1,189 @@ cmake_minimum_required(VERSION 3.1 FATAL_ERROR) # KDE Application Version, managed by KDE release team automatically # https://community.kde.org/Guidelines_and_HOWTOs/Application_Versioning set (KDE_APPLICATIONS_VERSION_MAJOR "19") set (KDE_APPLICATIONS_VERSION_MINOR "07") set (KDE_APPLICATIONS_VERSION_MICRO "80") set (KDE_APPLICATIONS_VERSION "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}.${KDE_APPLICATIONS_VERSION_MICRO}") project(juk VERSION ${KDE_APPLICATIONS_VERSION}) set(QT_MIN_VERSION "5.6.0") set(KF5_MIN_VERSION "5.35.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake") include(CMakePushCheckState) include(CheckIncludeFileCXX) include(KDEInstallDirs) include(KDECompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings NO_POLICY_SCOPE) include(FeatureSummary) include(ECMInstallIcons) include(ECMAddAppIcon) include(ECMQtDeclareLoggingCategory) find_package(Qt5 ${QT_MIN_VERSION} REQUIRED COMPONENTS Concurrent Gui Svg Network Test Widgets) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS CoreAddons Completion Config Crash GlobalAccel I18n IconThemes DocTools KIO JobWidgets Notifications TextWidgets XmlGui Wallet WidgetsAddons WindowSystem) find_package(Phonon4Qt5 4.6.60 REQUIRED NO_MODULE) find_package(Taglib 1.6 REQUIRED) include_directories(${CMAKE_BINARY_DIR} ${PHONON_INCLUDES}) add_subdirectory( doc ) add_subdirectory( tests ) add_subdirectory( pics ) #set_package_properties(FEATURE PROPERTIES DESCRIPTION "A library for developing MusicBrainz enabled tagging applications" URL "http://www.musicbrainz.org/products/tunepimp" TYPE OPTIONAL PURPOSE "Provides MusicBrainz tagging in Juk.") # TODO: tunepimp is fully unsupported, replace this when tunepimp is replaced # with whatever is actually current this decade. set(HAVE_TUNEPIMP 0) ########### next target ############### include_directories( SYSTEM ${TAGLIB_INCLUDES} ) add_definitions(-DQT_STL -DQT_NO_URL_CAST_FROM_STRING) # Look for Ogg Opus support in taglib (not released yet) cmake_push_check_state() set(CMAKE_REQUIRED_INCLUDES ${CMAKE_REQUIRED_INCLUDES} ${TAGLIB_INCLUDES}) check_include_file_cxx(opusfile.h TAGLIB_HAS_OPUSFILE) cmake_pop_check_state() configure_file (config-juk.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-juk.h ) set(juk_SRCS advancedsearchdialog.cpp slider.cpp svghandler.cpp volumepopupbutton.cpp actioncollection.cpp cache.cpp categoryreaderinterface.cpp collectionlist.cpp coverdialog.cpp covericonview.cpp coverinfo.cpp covermanager.cpp coverproxy.cpp dbuscollectionproxy.cpp deletedialog.cpp directorylist.cpp directoryloader.cpp dynamicplaylist.cpp exampleoptions.cpp folderplaylist.cpp filehandle.cpp filerenamer.cpp filerenameroptions.cpp filerenamerconfigdlg.cpp webimagefetcher.cpp historyplaylist.cpp juk.cpp + juktag.cpp keydialog.cpp lyricswidget.cpp main.cpp mediafiles.cpp mpris2/mediaplayer2.cpp mpris2/mediaplayer2player.cpp mpris2/mpris2.cpp nowplaying.cpp playermanager.cpp playlist.cpp playlistbox.cpp playlistcollection.cpp playlistinterface.cpp playlistitem.cpp playlistsearch.cpp playlistsharedsettings.cpp playlistsplitter.cpp scrobbler.cpp scrobbleconfigdlg.cpp searchplaylist.cpp searchwidget.cpp slideraction.cpp statuslabel.cpp stringshare.cpp systemtray.cpp - tag.cpp tageditor.cpp tagguesser.cpp tagguesserconfigdlg.cpp tagrenameroptions.cpp tagtransactionmanager.cpp tracksequenceiterator.cpp tracksequencemanager.cpp treeviewitemplaylist.cpp upcomingplaylist.cpp viewmode.cpp ) ecm_qt_declare_logging_category(juk_SRCS HEADER juk_debug.h IDENTIFIER JUK_LOG CATEGORY_NAME org.kde.juk) qt5_add_dbus_adaptor( juk_SRCS org.kde.juk.collection.xml dbuscollectionproxy.h DBusCollectionProxy ) qt5_add_dbus_adaptor( juk_SRCS org.kde.juk.player.xml playermanager.h PlayerManager) qt5_add_dbus_adaptor( juk_SRCS org.kde.juk.search.xml searchwidget.h SearchWidget) ki18n_wrap_ui(juk_SRCS filerenamerbase.ui filerenameroptionsbase.ui directorylistbase.ui tagguesserconfigdlgwidget.ui exampleoptionsbase.ui coverdialogbase.ui deletedialogbase.ui tageditor.ui ) file(GLOB ICONS_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/*-apps-juk.png") ecm_add_app_icon(juk_SRCS ICONS ${ICONS_SRCS}) add_executable(juk ${juk_SRCS}) kde_target_enable_exceptions(juk PRIVATE) target_compile_definitions(juk PRIVATE QT_USE_QSTRINGBUILDER) if(NOT MSVC AND NOT ( WIN32 AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Intel" ) ) set( LIBMATH m ) endif() target_link_libraries(juk ${LIBMATH} Qt5::Gui Qt5::Svg Qt5::Widgets Qt5::Network KF5::ConfigCore KF5::CoreAddons KF5::Completion KF5::Crash KF5::GlobalAccel KF5::KIOCore KF5::KIOWidgets KF5::KIOFileWidgets KF5::JobWidgets KF5::Notifications KF5::I18n KF5::IconThemes KF5::TextWidgets KF5::XmlGui KF5::WindowSystem KF5::WidgetsAddons KF5::Wallet Phonon::phonon4qt5 ${TAGLIB_LIBRARIES}) if(TUNEPIMP_FOUND) target_link_libraries(juk ${TUNEPIMP_LIBRARIES}) endif(TUNEPIMP_FOUND) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) install(TARGETS juk ${INSTALL_TARGETS_DEFAULT_ARGS} ) ########### install files ############### install( PROGRAMS org.kde.juk.desktop DESTINATION ${XDG_APPS_INSTALL_DIR} ) install( FILES juk.notifyrc jukui.rc jukui-rtl.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/juk ) install( FILES org.kde.juk.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) install( FILES jukservicemenu.desktop DESTINATION ${SERVICES_INSTALL_DIR}/ServiceMenus ) install( FILES org.kde.juk.collection.xml org.kde.juk.player.xml org.kde.juk.search.xml DESTINATION ${DBUS_INTERFACES_INSTALL_DIR} ) ecm_install_icons(ICONS 128-apps-juk.png 16-apps-juk.png 32-apps-juk.png 48-apps-juk.png 64-apps-juk.png DESTINATION ${ICON_INSTALL_DIR} THEME hicolor ) diff --git a/cache.cpp b/cache.cpp index 50be5db7..245f34d9 100644 --- a/cache.cpp +++ b/cache.cpp @@ -1,351 +1,351 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2008, 2013 Michael Pyne * * 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 "cache.h" #include "juk-exception.h" #include #include #include #include #include #include #include #include -#include "tag.h" +#include "juktag.h" #include "searchplaylist.h" #include "historyplaylist.h" #include "upcomingplaylist.h" #include "folderplaylist.h" #include "playlistcollection.h" #include "actioncollection.h" #include "juk.h" #include "juk_debug.h" using namespace ActionCollection; const int Cache::playlistListCacheVersion = 3; const int Cache::playlistItemsCacheVersion = 2; enum PlaylistType { Normal = 0, Search = 1, History = 2, Upcoming = 3, Folder = 4 }; //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// Cache *Cache::instance() { static Cache cache; return &cache; } static void parsePlaylistStream(QDataStream &s, PlaylistCollection *collection) { while(!s.atEnd()) { qint32 playlistType; s >> playlistType; Playlist *playlist = nullptr; switch(playlistType) { case Search: { SearchPlaylist *p = new SearchPlaylist(collection); s >> *p; playlist = p; break; } case History: { action("showHistory")->setChecked(true); collection->setHistoryPlaylistEnabled(true); s >> *collection->historyPlaylist(); playlist = collection->historyPlaylist(); break; } case Upcoming: { /* collection->setUpcomingPlaylistEnabled(true); Playlist *p = collection->upcomingPlaylist(); action("saveUpcomingTracks")->setChecked(true); s >> *p; playlist = p; */ break; } case Folder: { FolderPlaylist *p = new FolderPlaylist(collection); s >> *p; playlist = p; break; } default: Playlist *p = new Playlist(collection, true); s >> *p; // We may have already read this playlist from the folder // scanner, if an .m3u playlist if(collection->containsPlaylistFile(p->fileName())) { delete p; p = nullptr; } playlist = p; break; } // switch qint32 sortColumn; s >> sortColumn; if(playlist) playlist->sortByColumn(sortColumn); } } void Cache::loadPlaylists(PlaylistCollection *collection) // static { const QString playlistsFile = playlistsCacheFileName(); QFile f(playlistsFile); if(!f.open(QIODevice::ReadOnly)) return; QDataStream fs(&f); qint32 version; fs >> version; if(version != 3 || fs.status() != QDataStream::Ok) { // Either the file is corrupt or is from a truly ancient version // of JuK. qCWarning(JUK_LOG) << "Found the playlist cache but it was clearly corrupt."; return; } // Our checksum is only for the values after the version and checksum so // we want to get a byte array with just the checksummed data. QByteArray data; quint16 checksum; fs >> checksum >> data; if(fs.status() != QDataStream::Ok || checksum != qChecksum(data.data(), data.size())) return; QDataStream s(&data, QIODevice::ReadOnly); s.setVersion(QDataStream::Qt_4_3); try { // Loading failures are indicated by an exception parsePlaylistStream(s, collection); } catch(BICStreamException &) { qCCritical(JUK_LOG) << "Exception loading playlists - binary incompatible stream."; // TODO Restructure the Playlist data model and PlaylistCollection data model // to be separate from the view/controllers. return; } } void Cache::savePlaylists(const PlaylistList &playlists) { QString playlistsFile = playlistsCacheFileName(); QSaveFile f(playlistsFile); if(!f.open(QIODevice::WriteOnly)) { qCCritical(JUK_LOG) << "Error saving collection:" << f.errorString(); return; } QByteArray data; QDataStream s(&data, QIODevice::WriteOnly); s.setVersion(QDataStream::Qt_4_3); for(const auto &it : playlists) { if(!(it)) { continue; } // TODO back serialization type into Playlist itself if(dynamic_cast(it)) { s << qint32(History) << *static_cast(it); } else if(dynamic_cast(it)) { s << qint32(Search) << *static_cast(it); } else if(dynamic_cast(it)) { if(!action("saveUpcomingTracks")->isChecked()) continue; s << qint32(Upcoming) << *static_cast(it); } else if(dynamic_cast(it)) { s << qint32(Folder) << *static_cast(it); } else { s << qint32(Normal) << *(it); } s << qint32(it->sortColumn()); } QDataStream fs(&f); fs << qint32(playlistListCacheVersion); fs << qChecksum(data.data(), data.size()); fs << data; if(!f.commit()) qCCritical(JUK_LOG) << "Error saving collection:" << f.errorString(); } void Cache::ensureAppDataStorageExists() // static { QString dirPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); QDir appDataDir(dirPath); if(!appDataDir.exists() && !appDataDir.mkpath(dirPath)) qCCritical(JUK_LOG) << "Unable to create appdata storage in" << dirPath; } bool Cache::cacheFileExists() // static { return QFile::exists(fileHandleCacheFileName()); } // Despite the 'Cache' class name, these data files are not regenerable and so // should not be stored in cache directory. QString Cache::fileHandleCacheFileName() // static { return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/cache"; } QString Cache::playlistsCacheFileName() // static { return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/playlists"; } //////////////////////////////////////////////////////////////////////////////// // private methods //////////////////////////////////////////////////////////////////////////////// Cache::Cache() { } bool Cache::prepareToLoadCachedItems() { m_loadFile.setFileName(fileHandleCacheFileName()); if(!m_loadFile.open(QIODevice::ReadOnly)) return false; m_loadDataStream.setDevice(&m_loadFile); int dataStreamVersion = CacheDataStream::Qt_3_3; qint32 version; m_loadDataStream >> version; switch(version) { case 2: dataStreamVersion = CacheDataStream::Qt_4_3; #if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) Q_FALLTHROUGH(); #endif // Other than that we're compatible with cache v1, so fallthrough // to setCacheVersion case 1: { m_loadDataStream.setCacheVersion(1); m_loadDataStream.setVersion(dataStreamVersion); qint32 checksum; m_loadDataStream >> checksum >> m_loadFileBuffer.buffer(); m_loadFileBuffer.open(QIODevice::ReadOnly); m_loadDataStream.setDevice(&m_loadFileBuffer); qint32 checksumExpected = qChecksum( m_loadFileBuffer.data(), m_loadFileBuffer.size()); if(m_loadDataStream.status() != CacheDataStream::Ok || checksum != checksumExpected) { qCCritical(JUK_LOG) << "Music cache checksum expected to get" << checksumExpected << "actually was" << checksum; KMessageBox::sorry(0, i18n("The music data cache has been corrupted. JuK " "needs to rescan it now. This may take some time.")); return false; } break; } default: { m_loadDataStream.device()->reset(); m_loadDataStream.setCacheVersion(0); // This cache is so old that this is just a wild guess here that 3.3 // is compatible. m_loadDataStream.setVersion(CacheDataStream::Qt_3_3); break; } } return true; } FileHandle Cache::loadNextCachedItem() { if(!m_loadFile.isOpen() || !m_loadDataStream.device()) { qCWarning(JUK_LOG) << "Already completed reading cache file."; return FileHandle(); } if(m_loadDataStream.status() == QDataStream::ReadCorruptData) { qCCritical(JUK_LOG) << "Attempted to read file handle from corrupt cache file."; return FileHandle(); } if(!m_loadDataStream.atEnd()) { QString fileName; m_loadDataStream >> fileName; fileName.squeeze(); return FileHandle(fileName, m_loadDataStream); } else { m_loadDataStream.setDevice(0); m_loadFile.close(); return FileHandle(); } } // vim: set et sw=4 tw=0 sta: diff --git a/collectionlist.cpp b/collectionlist.cpp index a90d0481..be885af1 100644 --- a/collectionlist.cpp +++ b/collectionlist.cpp @@ -1,598 +1,598 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * * 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 "collectionlist.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "playlistcollection.h" #include "stringshare.h" #include "cache.h" #include "actioncollection.h" -#include "tag.h" +#include "juktag.h" #include "viewmode.h" #include "juk_debug.h" using ActionCollection::action; //////////////////////////////////////////////////////////////////////////////// // static methods //////////////////////////////////////////////////////////////////////////////// CollectionList *CollectionList::m_list = 0; CollectionList *CollectionList::instance() { return m_list; } static QTime stopwatch; void CollectionList::startLoadingCachedItems() { if(!m_list) return; qCDebug(JUK_LOG) << "Starting to load cached items"; stopwatch.start(); if(!Cache::instance()->prepareToLoadCachedItems()) { qCCritical(JUK_LOG) << "Unable to setup to load cache... perhaps it doesn't exist?"; completedLoadingCachedItems(); return; } qCDebug(JUK_LOG) << "Kicked off first batch"; QTimer::singleShot(0, this, SLOT(loadNextBatchCachedItems())); } void CollectionList::loadNextBatchCachedItems() { Cache *cache = Cache::instance(); bool done = false; for(int i = 0; i < 20; ++i) { FileHandle cachedItem(cache->loadNextCachedItem()); if(cachedItem.isNull()) { done = true; break; } // This may have already been created via a loaded playlist. if(!m_itemsDict.contains(cachedItem.absFilePath())) { CollectionListItem *newItem = new CollectionListItem(this, cachedItem); setupItem(newItem); } } if(!done) { QTimer::singleShot(0, this, SLOT(loadNextBatchCachedItems())); } else { completedLoadingCachedItems(); } } void CollectionList::completedLoadingCachedItems() { // The CollectionList is created with sorting disabled for speed. Re-enable // it here, and perform the sort. KConfigGroup config(KSharedConfig::openConfig(), "Playlists"); Qt::SortOrder order = Qt::DescendingOrder; if(config.readEntry("CollectionListSortAscending", true)) order = Qt::AscendingOrder; m_list->sortByColumn(config.readEntry("CollectionListSortColumn", 1), order); qCDebug(JUK_LOG) << "Finished loading cached items, took" << stopwatch.elapsed() << "ms"; qCDebug(JUK_LOG) << m_itemsDict.size() << "items are in the CollectionList"; emit cachedItemsLoaded(); } void CollectionList::initialize(PlaylistCollection *collection) { if(m_list) return; // We have to delay initialization here because dynamic_cast or comparing to // the collection instance won't work in the PlaylistBox::Item initialization // won't work until the CollectionList is fully constructed. m_list = new CollectionList(collection); m_list->setName(i18n("Collection List")); collection->setupPlaylist(m_list, "folder-sound"); } //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// CollectionListItem *CollectionList::createItem(const FileHandle &file, QTreeWidgetItem *) { // It's probably possible to optimize the line below away, but, well, right // now it's more important to not load duplicate items. if(m_itemsDict.contains(file.absFilePath())) return nullptr; CollectionListItem *item = new CollectionListItem(this, file); if(!item->isValid()) { qCCritical(JUK_LOG) << "CollectionList::createItem() -- A valid tag was not created for \"" << file.absFilePath() << "\""; delete item; return nullptr; } setupItem(item); return item; } void CollectionList::clearItems(const PlaylistItemList &items) { foreach(PlaylistItem *item, items) { delete item; } playlistItemsChanged(); } void CollectionList::setupTreeViewEntries(ViewMode *viewMode) const { TreeViewMode *treeViewMode = dynamic_cast(viewMode); if(!treeViewMode) { qCWarning(JUK_LOG) << "Can't setup entries on a non-tree-view mode!\n"; return; } QList columnList; columnList << PlaylistItem::ArtistColumn; columnList << PlaylistItem::GenreColumn; columnList << PlaylistItem::AlbumColumn; foreach(int column, columnList) treeViewMode->addItems(m_columnTags[column]->keys(), column); } void CollectionList::slotNewItems(const KFileItemList &items) { QStringList files; for(KFileItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) files.append((*it).url().path()); addFiles(files); update(); } void CollectionList::slotRefreshItems(const QList > &items) { for(int i = 0; i < items.count(); ++i) { const KFileItem fileItem = items[i].second; CollectionListItem *item = lookup(fileItem.url().path()); if(item) { item->refreshFromDisk(); // If the item is no longer on disk, remove it from the collection. if(item->file().fileInfo().exists()) item->repaint(); else delete item; } } update(); } void CollectionList::slotDeleteItems(const KFileItemList &items) { for(const auto &item : items) { delete lookup(item.url().path()); } } void CollectionList::saveItemsToCache() const { qCDebug(JUK_LOG) << "Saving collection list to cache"; QSaveFile f(Cache::fileHandleCacheFileName()); if(!f.open(QIODevice::WriteOnly)) { qCCritical(JUK_LOG) << "Error saving cache:" << f.errorString(); return; } QByteArray data; QDataStream s(&data, QIODevice::WriteOnly); s.setVersion(QDataStream::Qt_4_3); QHash::const_iterator it; for(it = m_itemsDict.begin(); it != m_itemsDict.end(); ++it) { s << it.key(); s << (*it)->file(); } QDataStream fs(&f); qint32 checksum = qChecksum(data.data(), data.size()); fs << qint32(Cache::playlistItemsCacheVersion) << checksum << data; if(!f.commit()) qCCritical(JUK_LOG) << "Error saving cache:" << f.errorString(); } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void CollectionList::clear() { int result = KMessageBox::warningContinueCancel(this, i18n("Removing an item from the collection will also remove it from " "all of your playlists. Are you sure you want to continue?\n\n" "Note, however, that if the directory that these files are in is in " "your \"scan on startup\" list, they will be readded on startup.")); if(result == KMessageBox::Continue) { Playlist::clear(); emit signalCollectionChanged(); } } void CollectionList::slotCheckCache() { PlaylistItemList invalidItems; qCDebug(JUK_LOG) << "Starting to check cached items for consistency"; stopwatch.start(); int i = 0; foreach(CollectionListItem *item, m_itemsDict) { if(!item->checkCurrent()) invalidItems.append(item); if(++i == (m_itemsDict.size() / 2)) qCDebug(JUK_LOG) << "Checkpoint"; } clearItems(invalidItems); qCDebug(JUK_LOG) << "Finished consistency check, took" << stopwatch.elapsed() << "ms"; } void CollectionList::slotRemoveItem(const QString &file) { delete m_itemsDict[file]; } void CollectionList::slotRefreshItem(const QString &file) { if(m_itemsDict[file]) m_itemsDict[file]->refresh(); } //////////////////////////////////////////////////////////////////////////////// // protected methods //////////////////////////////////////////////////////////////////////////////// CollectionList::CollectionList(PlaylistCollection *collection) : Playlist(collection, true), m_columnTags(15, 0) { QAction *spaction = ActionCollection::actions()->addAction("showPlaying"); spaction->setText(i18n("Show Playing")); connect(spaction, SIGNAL(triggered(bool)), SLOT(slotShowPlaying())); connect(action("back")->menu(), SIGNAL(aboutToShow()), this, SLOT(slotPopulateBackMenu())); connect(action("back")->menu(), SIGNAL(triggered(QAction*)), this, SLOT(slotPlayFromBackMenu(QAction*))); setSortingEnabled(false); // Temporarily disable sorting to add items faster. m_columnTags[PlaylistItem::ArtistColumn] = new TagCountDict; m_columnTags[PlaylistItem::AlbumColumn] = new TagCountDict; m_columnTags[PlaylistItem::GenreColumn] = new TagCountDict; // Even set to true it wouldn't work with this class due to other checks setAllowDuplicates(false); } CollectionList::~CollectionList() { KConfigGroup config(KSharedConfig::openConfig(), "Playlists"); config.writeEntry("CollectionListSortColumn", header()->sortIndicatorSection()); config.writeEntry("CollectionListSortAscending", header()->sortIndicatorOrder() == Qt::AscendingOrder); // In some situations the dataChanged signal from clearItems will cause observers to // subsequently try to access a deleted item. Since we're going away just remove all // observers. clearObservers(); // The CollectionListItems will try to remove themselves from the // m_columnTags member, so we must make sure they're gone before we // are. clearItems(items()); qDeleteAll(m_columnTags); m_columnTags.clear(); } void CollectionList::dropEvent(QDropEvent *e) { if(e->source() == this) return; // Don't rearrange in the CollectionList. else Playlist::dropEvent(e); } void CollectionList::dragMoveEvent(QDragMoveEvent *e) { if(e->source() != this) Playlist::dragMoveEvent(e); else e->setAccepted(false); } QString CollectionList::addStringToDict(const QString &value, int column) { if(column > m_columnTags.count() || value.trimmed().isEmpty()) return QString(); if(m_columnTags[column]->contains(value)) ++((*m_columnTags[column])[value]); else { m_columnTags[column]->insert(value, 1); emit signalNewTag(value, column); } return value; } QStringList CollectionList::uniqueSet(UniqueSetType t) const { int column; switch(t) { case Artists: column = PlaylistItem::ArtistColumn; break; case Albums: column = PlaylistItem::AlbumColumn; break; case Genres: column = PlaylistItem::GenreColumn; break; default: return QStringList(); } return m_columnTags[column]->keys(); } CollectionListItem *CollectionList::lookup(const QString &file) const { return m_itemsDict.value(file, nullptr); } void CollectionList::removeStringFromDict(const QString &value, int column) { if(column > m_columnTags.count() || value.trimmed().isEmpty()) return; if(m_columnTags[column]->contains(value) && --((*m_columnTags[column])[value])) // If the decrement goes to 0... { emit signalRemovedTag(value, column); m_columnTags[column]->remove(value); } } void CollectionList::addWatched(const QString &file) { m_dirWatch->addFile(file); } void CollectionList::removeWatched(const QString &file) { m_dirWatch->removeFile(file); } //////////////////////////////////////////////////////////////////////////////// // CollectionListItem public methods //////////////////////////////////////////////////////////////////////////////// void CollectionListItem::refresh() { int offset = CollectionList::instance()->columnOffset(); int columns = lastColumn() + offset + 1; sharedData()->metadata.resize(columns); sharedData()->cachedWidths.resize(columns); for(int i = offset; i < columns; i++) { setText(i, text(i)); int id = i - offset; if(id != TrackNumberColumn && id != LengthColumn) { // All columns other than track num and length need local-encoded data for sorting QString toLower = text(i).toLower(); // For some columns, we may be able to share some strings if((id == ArtistColumn) || (id == AlbumColumn) || (id == GenreColumn) || (id == YearColumn) || (id == CommentColumn)) { toLower = StringShare::tryShare(toLower); if(id != YearColumn && id != CommentColumn && sharedData()->metadata[id] != toLower) { CollectionList::instance()->removeStringFromDict(sharedData()->metadata[id], id); CollectionList::instance()->addStringToDict(text(i), id); } } sharedData()->metadata[id] = toLower; } #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) int newWidth = treeWidget()->fontMetrics().horizontalAdvance(text(i)); #else // .width is deprecated in Qt 5.11 or later int newWidth = treeWidget()->fontMetrics().width(text(i)); #endif if(newWidth != sharedData()->cachedWidths[i]) playlist()->slotWeightDirty(i); sharedData()->cachedWidths[i] = newWidth; } for(PlaylistItemList::Iterator it = m_children.begin(); it != m_children.end(); ++it) { (*it)->playlist()->update(); (*it)->playlist()->playlistItemsChanged(); } if(treeWidget()->isVisible()) treeWidget()->viewport()->update(); CollectionList::instance()->playlistItemsChanged(); emit CollectionList::instance()->signalCollectionChanged(); } PlaylistItem *CollectionListItem::itemForPlaylist(const Playlist *playlist) { if(playlist == CollectionList::instance()) return this; PlaylistItemList::ConstIterator it; for(it = m_children.constBegin(); it != m_children.constEnd(); ++it) if((*it)->playlist() == playlist) return *it; return 0; } void CollectionListItem::updateCollectionDict(const QString &oldPath, const QString &newPath) { CollectionList *collection = CollectionList::instance(); if(!collection) return; collection->removeFromDict(oldPath); collection->addToDict(newPath, this); } void CollectionListItem::repaint() const { // FIXME repaint /*QItemDelegate::repaint(); for(PlaylistItemList::ConstIterator it = m_children.constBegin(); it != m_children.constEnd(); ++it) (*it)->repaint();*/ } //////////////////////////////////////////////////////////////////////////////// // CollectionListItem protected methods //////////////////////////////////////////////////////////////////////////////// CollectionListItem::CollectionListItem(CollectionList *parent, const FileHandle &file) : PlaylistItem(parent), m_shuttingDown(false) { parent->addToDict(file.absFilePath(), this); sharedData()->fileHandle = file; if(file.tag()) { refresh(); parent->playlistItemsChanged(); } else { qCCritical(JUK_LOG) << "CollectionListItem::CollectionListItem() -- Tag() could not be created."; } } CollectionListItem::~CollectionListItem() { m_shuttingDown = true; foreach(PlaylistItem *item, m_children) delete item; CollectionList *l = CollectionList::instance(); if(l) { l->removeFromDict(file().absFilePath()); l->removeStringFromDict(file().tag()->album(), AlbumColumn); l->removeStringFromDict(file().tag()->artist(), ArtistColumn); l->removeStringFromDict(file().tag()->genre(), GenreColumn); } } void CollectionListItem::addChildItem(PlaylistItem *child) { m_children.append(child); } void CollectionListItem::removeChildItem(PlaylistItem *child) { if(!m_shuttingDown) m_children.removeAll(child); } bool CollectionListItem::checkCurrent() { if(!file().fileInfo().exists() || !file().fileInfo().isFile()) return false; if(!file().current()) { file().refresh(); refresh(); } return true; } // vim: set et sw=4 tw=0 sta: diff --git a/coverinfo.cpp b/coverinfo.cpp index 77261c6c..e311afa1 100644 --- a/coverinfo.cpp +++ b/coverinfo.cpp @@ -1,494 +1,495 @@ /** * Copyright (C) 2004 Nathan Toone * Copyright (C) 2005, 2008, 2018 Michael Pyne * * 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 "coverinfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include -#include -#include -#include -#include -#include +// Taglib includes +#include +#include +#include +#include +#include +#include #ifdef TAGLIB_WITH_MP4 -#include -#include -#include -#include +#include +#include +#include +#include #endif #include "mediafiles.h" #include "collectionlist.h" #include "playlistsearch.h" #include "playlistitem.h" -#include "tag.h" +#include "juktag.h" #include "juk_debug.h" struct CoverPopup : public QWidget { CoverPopup(QPixmap &image, const QPoint &p) : QWidget(0, Qt::WindowFlags(Qt::WA_DeleteOnClose | Qt::X11BypassWindowManagerHint)) { QHBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(this); layout->addWidget(label); const auto pixRatio = this->devicePixelRatioF(); QSizeF imageSize(label->width(), label->height()); if (!qFuzzyCompare(pixRatio, 1.0)) { imageSize /= pixRatio; image.setDevicePixelRatio(pixRatio); } label->setFrameStyle(QFrame::Box | QFrame::Raised); label->setLineWidth(1); label->setPixmap(image); setGeometry(QRect(p, imageSize.toSize())); show(); } virtual void leaveEvent(QEvent *) override { close(); } virtual void mouseReleaseEvent(QMouseEvent *) override { close(); } }; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// CoverInfo::CoverInfo(const FileHandle &file) : m_file(file), m_hasCover(false), m_hasAttachedCover(false), m_haveCheckedForCover(false), m_coverKey(CoverManager::NoMatch) { } bool CoverInfo::hasCover() const { if(m_haveCheckedForCover) return m_hasCover || m_hasAttachedCover; m_haveCheckedForCover = true; // Check for new-style covers. First let's determine what our coverKey is // if it's not already set, as that's also tracked by the CoverManager. if(m_coverKey == CoverManager::NoMatch) m_coverKey = CoverManager::idForTrack(m_file.absFilePath()); // We were assigned a key, let's see if we already have a cover. Notice // that due to the way the CoverManager is structured, we should have a // cover if we have a cover key. If we don't then either there's a logic // error, or the user has been mucking around where they shouldn't. if(m_coverKey != CoverManager::NoMatch) m_hasCover = CoverManager::hasCover(m_coverKey); // Check if it's embedded in the file itself. m_hasAttachedCover = hasEmbeddedAlbumArt(); if(m_hasAttachedCover) return true; // Look for cover.jpg or cover.png in the directory. if(QFile::exists(m_file.fileInfo().absolutePath() + "/cover.jpg") || QFile::exists(m_file.fileInfo().absolutePath() + "/cover.png")) { m_hasCover = true; } return m_hasCover; } void CoverInfo::clearCover() { m_hasCover = false; m_hasAttachedCover = false; // Re-search for cover since we may still have a different type of cover. m_haveCheckedForCover = false; // We don't need to call removeCover because the CoverManager will // automatically unlink the cover if we were the last track to use it. CoverManager::setIdForTrack(m_file.absFilePath(), CoverManager::NoMatch); m_coverKey = CoverManager::NoMatch; } void CoverInfo::setCover(const QImage &image) { if(image.isNull()) return; m_haveCheckedForCover = true; m_hasCover = true; QPixmap cover = QPixmap::fromImage(image); // If we use replaceCover we'll change the cover for every other track // with the same coverKey, which we don't want since that case will be // handled by Playlist. Instead just replace this track's cover. m_coverKey = CoverManager::addCover(cover, m_file.tag()->artist(), m_file.tag()->album()); if(m_coverKey != CoverManager::NoMatch) CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey); } void CoverInfo::setCoverId(coverKey id) { m_coverKey = id; m_haveCheckedForCover = true; m_hasCover = id != CoverManager::NoMatch; // Inform CoverManager of the change. CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey); } void CoverInfo::applyCoverToWholeAlbum(bool overwriteExistingCovers) const { QString artist = m_file.tag()->artist(); QString album = m_file.tag()->album(); PlaylistSearch::ComponentList components; ColumnList columns; columns.append(PlaylistItem::ArtistColumn); components.append(PlaylistSearch::Component(artist, false, columns, PlaylistSearch::Component::Exact)); columns.clear(); columns.append(PlaylistItem::AlbumColumn); components.append(PlaylistSearch::Component(album, false, columns, PlaylistSearch::Component::Exact)); PlaylistList playlists; playlists.append(CollectionList::instance()); PlaylistSearch search(playlists, components, PlaylistSearch::MatchAll); // Search done, iterate through results. PlaylistItemList results = search.matchedItems(); PlaylistItemList::ConstIterator it = results.constBegin(); for(; it != results.constEnd(); ++it) { // Don't worry about files that somehow already have a tag, // unless the conversion is forced. if(!overwriteExistingCovers && (*it)->file().coverInfo()->coverId() != CoverManager::NoMatch) continue; (*it)->file().coverInfo()->setCoverId(m_coverKey); } } coverKey CoverInfo::coverId() const { if(m_coverKey == CoverManager::NoMatch) m_coverKey = CoverManager::idForTrack(m_file.absFilePath()); return m_coverKey; } QPixmap CoverInfo::pixmap(CoverSize size) const { if(hasCover() && m_coverKey != CoverManager::NoMatch) { return CoverManager::coverFromId(m_coverKey, size == Thumbnail ? CoverManager::Thumbnail : CoverManager::FullSize); } QImage cover; // If m_hasCover is still true we must have a directory cover image. if(m_hasCover) { QString fileName = m_file.fileInfo().absolutePath() + "/cover.jpg"; if(!cover.load(fileName)) { fileName = m_file.fileInfo().absolutePath() + "/cover.png"; if(!cover.load(fileName)) return QPixmap(); } return QPixmap::fromImage(cover); } // If we get here, see if there is an embedded cover. cover = embeddedAlbumArt(); if(!cover.isNull() && size == Thumbnail) cover = scaleCoverToThumbnail(cover); if(cover.isNull()) { return QPixmap(); } return QPixmap::fromImage(cover); } QString CoverInfo::localPathToCover(const QString &fallbackFileName) const { if(m_coverKey != CoverManager::NoMatch) { QString path = CoverManager::coverInfo(m_coverKey).path; if(!path.isEmpty()) return path; } if(hasEmbeddedAlbumArt()) { QFile albumArtFile(fallbackFileName); if(!albumArtFile.open(QIODevice::ReadWrite)) { return QString(); } QImage albumArt = embeddedAlbumArt(); albumArt.save(&albumArtFile, "PNG"); return fallbackFileName; } QString basePath = m_file.fileInfo().absolutePath(); if(QFile::exists(basePath + "/cover.jpg")) return basePath + "/cover.jpg"; else if(QFile::exists(basePath + "/cover.png")) return basePath + "/cover.png"; return QString(); } bool CoverInfo::hasEmbeddedAlbumArt() const { QScopedPointer fileTag( MediaFiles::fileFactoryByType(m_file.absFilePath())); if (TagLib::MPEG::File *mpegFile = dynamic_cast(fileTag.data())) { TagLib::ID3v2::Tag *id3tag = mpegFile->ID3v2Tag(false); if (!id3tag) { qCCritical(JUK_LOG) << m_file.absFilePath() << "seems to have invalid ID3 tag"; return false; } // Look for attached picture frames. TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"]; return !frames.isEmpty(); } else if (TagLib::Ogg::XiphComment *oggTag = dynamic_cast(fileTag->tag())) { return !oggTag->pictureList().isEmpty(); } else if (TagLib::FLAC::File *flacFile = dynamic_cast(fileTag.data())) { // Look if images are embedded. return !flacFile->pictureList().isEmpty(); } #ifdef TAGLIB_WITH_MP4 else if(TagLib::MP4::File *mp4File = dynamic_cast(fileTag.data())) { TagLib::MP4::Tag *tag = mp4File->tag(); if (tag) { TagLib::MP4::ItemListMap &items = tag->itemListMap(); return items.contains("covr"); } } #endif return false; } static QImage embeddedMPEGAlbumArt(TagLib::ID3v2::Tag *id3tag) { if(!id3tag) return QImage(); // Look for attached picture frames. TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"]; if(frames.isEmpty()) return QImage(); // According to the spec attached picture frames have different types. // So we should look for the corresponding picture depending on what // type of image (i.e. front cover, file info) we want. If only 1 // frame, just return that (scaled if necessary). TagLib::ID3v2::AttachedPictureFrame *selectedFrame = 0; if(frames.size() != 1) { TagLib::ID3v2::FrameList::Iterator it = frames.begin(); for(; it != frames.end(); ++it) { // This must be dynamic_cast<>, TagLib will return UnknownFrame in APIC for // encrypted frames. TagLib::ID3v2::AttachedPictureFrame *frame = dynamic_cast(*it); // Both thumbnail and full size should use FrontCover, as // FileIcon may be too small even for thumbnail. if(frame && frame->type() != TagLib::ID3v2::AttachedPictureFrame::FrontCover) continue; selectedFrame = frame; break; } } // If we get here we failed to pick a picture, or there was only one, // so just use the first picture. if(!selectedFrame) selectedFrame = dynamic_cast(frames.front()); if(!selectedFrame) // Could occur for encrypted picture frames. return QImage(); TagLib::ByteVector picture = selectedFrame->picture(); return QImage::fromData( reinterpret_cast(picture.data()), picture.size()); } static QImage embeddedFLACAlbumArt(const TagLib::List &flacPictures) { if(flacPictures.isEmpty()) { return QImage(); } // Always use first picture - even if multiple are embedded. TagLib::ByteVector coverData = flacPictures[0]->data(); // Will return an image or a null image on error, works either way return QImage::fromData( reinterpret_cast(coverData.data()), coverData.size()); } #ifdef TAGLIB_WITH_MP4 static QImage embeddedMP4AlbumArt(TagLib::MP4::Tag *tag) { TagLib::MP4::ItemListMap &items = tag->itemListMap(); if(!items.contains("covr")) return QImage(); TagLib::MP4::CoverArtList covers = items["covr"].toCoverArtList(); TagLib::MP4::CoverArtList::ConstIterator end = covers.end(); for(TagLib::MP4::CoverArtList::ConstIterator it = covers.begin(); it != end; ++it) { TagLib::MP4::CoverArt cover = *it; TagLib::ByteVector coverData = cover.data(); QImage result = QImage::fromData( reinterpret_cast(coverData.data()), coverData.size()); if(!result.isNull()) return result; } // No appropriate image found return QImage(); } #endif void CoverInfo::popup() const { QPixmap image = pixmap(FullSize); QPoint mouse = QCursor::pos(); QScreen *primaryScreen = QApplication::primaryScreen(); QRect desktop = primaryScreen->availableGeometry(); int x = mouse.x(); int y = mouse.y(); int height = image.size().height() + 4; int width = image.size().width() + 4; // Detect the right direction to pop up (always towards the center of the // screen), try to pop up with the mouse pointer 10 pixels into the image in // both directions. If we're too close to the screen border for this margin, // show it at the screen edge, accounting for the four pixels (two on each // side) for the window border. if(x - desktop.x() < desktop.width() / 2) x = (x - desktop.x() < 10) ? desktop.x() : (x - 10); else x = (x - desktop.x() > desktop.width() - 10) ? desktop.width() - width +desktop.x() : (x - width + 10); if(y - desktop.y() < desktop.height() / 2) y = (y - desktop.y() < 10) ? desktop.y() : (y - 10); else y = (y - desktop.y() > desktop.height() - 10) ? desktop.height() - height + desktop.y() : (y - height + 10); new CoverPopup(image, QPoint(x, y)); } QImage CoverInfo::embeddedAlbumArt() const { QScopedPointer fileTag( MediaFiles::fileFactoryByType(m_file.absFilePath())); if (auto *mpegFile = dynamic_cast(fileTag.data())) { return embeddedMPEGAlbumArt(mpegFile->ID3v2Tag(false)); } else if (auto *oggTag = dynamic_cast(fileTag->tag())) { return embeddedFLACAlbumArt(oggTag->pictureList()); } else if (auto *flacFile = dynamic_cast(fileTag.data())) { return embeddedFLACAlbumArt(flacFile->pictureList()); } #ifdef TAGLIB_WITH_MP4 else if(auto *mp4File = dynamic_cast(fileTag.data())) { auto *tag = mp4File->tag(); if (tag) { return embeddedMP4AlbumArt(tag); } } #endif return QImage(); } QImage CoverInfo::scaleCoverToThumbnail(const QImage &image) const { return image.scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation); } // vim: set et sw=4 tw=0 sta: diff --git a/filehandle.cpp b/filehandle.cpp index b89c4195..05d94992 100644 --- a/filehandle.cpp +++ b/filehandle.cpp @@ -1,223 +1,223 @@ /** * Copyright (C) 2004 Scott Wheeler * Copyright (C) 2017 Michael Pyne * * 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 "filehandle.h" #include #include #include #include "filehandleproperties.h" -#include "tag.h" +#include "juktag.h" #include "cache.h" #include "coverinfo.h" #include "juk_debug.h" AddProperty(Title, tag()->title()) AddProperty(Artist, tag()->artist()) AddProperty(Album, tag()->album()) AddProperty(Genre, tag()->genre()) AddNumberProperty(Track, tag()->track()) AddNumberProperty(Year, tag()->year()) AddProperty(Comment, tag()->comment()) AddNumberProperty(Seconds, tag()->seconds()) AddNumberProperty(Bitrate, tag()->bitrate()) AddProperty(Path, absFilePath()) AddNumberProperty(Size, fileInfo().size()) AddProperty(Extension, fileInfo().suffix()) class FileHandle::FileHandlePrivate : public QSharedData { public: FileHandlePrivate(QFileInfo fInfo) : tag(nullptr) , coverInfo(nullptr) , fileInfo(fInfo) , absFilePath(fInfo.canonicalFilePath()) { baseModificationTime = fileInfo.lastModified(); } mutable QScopedPointer tag; mutable QScopedPointer coverInfo; QFileInfo fileInfo; QString absFilePath; QDateTime baseModificationTime; mutable QDateTime lastModified; }; //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// FileHandle::FileHandle(const FileHandle &f) : d(f.d) { } FileHandle::FileHandle(const QFileInfo &info) : d(new FileHandlePrivate(info)) { } FileHandle::FileHandle() : FileHandle(QFileInfo()) // delegating ctor { } FileHandle::FileHandle(const QString &path) : FileHandle(QFileInfo(path)) // delegating ctor { } FileHandle::FileHandle(const QString &path, CacheDataStream &s) : FileHandle(QFileInfo(path)) // delegating ctor { if(d->fileInfo.exists()) read(s); } FileHandle::~FileHandle() = default; void FileHandle::refresh() { d->fileInfo.refresh(); d->tag.reset(new Tag(d->absFilePath)); } void FileHandle::setFile(const QString &path) { if(path.isEmpty()) { qCCritical(JUK_LOG) << "trying to set an empty path"; return; } if(!QFile::exists(path)) { qCCritical(JUK_LOG) << "trying to set non-existent file: " << path; return; } d = new FileHandlePrivate(QFileInfo(path)); } Tag *FileHandle::tag() const { if(Q_UNLIKELY(!d->tag)) d->tag.reset(new Tag(d->absFilePath)); return d->tag.data(); } CoverInfo *FileHandle::coverInfo() const { if(Q_UNLIKELY(!d->coverInfo)) d->coverInfo.reset(new CoverInfo(*this)); return d->coverInfo.data(); } QString FileHandle::absFilePath() const { return d->absFilePath; } const QFileInfo &FileHandle::fileInfo() const { return d->fileInfo; } bool FileHandle::isNull() const { return d->absFilePath.isEmpty(); } bool FileHandle::current() const { return (d->baseModificationTime.isValid() && lastModified().isValid() && d->baseModificationTime >= lastModified()); } const QDateTime &FileHandle::lastModified() const { if(d->lastModified.isNull()) d->lastModified = d->fileInfo.lastModified(); return d->lastModified; } void FileHandle::read(CacheDataStream &s) { switch(s.cacheVersion()) { case 1: default: if(!d->tag) d->tag.reset(new Tag(d->absFilePath, true)); s >> *(d->tag); s >> d->baseModificationTime; break; } } FileHandle &FileHandle::operator=(const FileHandle &f) { if(&f != this) d = f.d; return *this; } bool FileHandle::operator==(const FileHandle &f) const { return d == f.d; } bool FileHandle::operator!=(const FileHandle &f) const { return d != f.d; } QStringList FileHandle::properties() // static { return FileHandleProperties::properties(); } QString FileHandle::property(const QString &name) const { return FileHandleProperties::property(*this, name.toUtf8()); } //////////////////////////////////////////////////////////////////////////////// // related functions //////////////////////////////////////////////////////////////////////////////// QDataStream &operator<<(QDataStream &s, const FileHandle &f) { s << *(f.tag()) << f.lastModified(); return s; } CacheDataStream &operator>>(CacheDataStream &s, FileHandle &f) { f.read(s); return s; } // vim: set et sw=4 tw=0 sta: diff --git a/filerenamer.cpp b/filerenamer.cpp index 3b70e44d..30468d08 100644 --- a/filerenamer.cpp +++ b/filerenamer.cpp @@ -1,1051 +1,1051 @@ /** * Copyright (C) 2004, 2007, 2009 Michael Pyne * Copyright (C) 2003 Frerich Raabe * Copyright (C) 2014 Arnold Dumas * * 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 "filerenamer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include "tag.h" +#include "juktag.h" #include "filerenameroptions.h" #include "filehandle.h" #include "exampleoptions.h" #include "playlistitem.h" #include "playlist.h" // processEvents() #include "coverinfo.h" #include "juk_debug.h" class ConfirmationDialog : public QDialog { public: ConfirmationDialog(const QMap &files, QWidget *parent = nullptr) : QDialog(parent) { setModal(true); setWindowTitle(i18nc("warning about mass file rename", "Warning")); auto vboxLayout = new QVBoxLayout(this); auto hbox = new QWidget(this); auto hboxVLayout = new QVBoxLayout(hbox); vboxLayout->addWidget(hbox); QLabel *l = new QLabel(hbox); l->setPixmap(QIcon::fromTheme("dialog-warning").pixmap(KIconLoader::SizeLarge)); hboxVLayout->addWidget(l); l = new QLabel(i18n("You are about to rename the following files. " "Are you sure you want to continue?"), hbox); hboxVLayout->addWidget(l); QTreeWidget *lv = new QTreeWidget(this); QStringList headers; headers << i18n("Original Name"); headers << i18n("New Name"); lv->setHeaderLabels(headers); lv->setRootIsDecorated(false); vboxLayout->addWidget(lv, 1); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Yes | QDialogButtonBox::Cancel, this); vboxLayout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); int lvHeight = 0; QMap::ConstIterator it = files.constBegin(); for(; it != files.constEnd(); ++it) { QTreeWidgetItem *item = new QTreeWidgetItem(lv); item->setText(0, it.key()); if (it.key() != it.value()) { item->setText(1, it.value()); } else { item->setText(1, i18n("No Change")); } lvHeight += lv->visualItemRect(item).height(); } lvHeight += lv->horizontalScrollBar()->height() + lv->header()->height(); lv->setMinimumHeight(qMin(lvHeight, 400)); resize(qMin(width(), 500), qMin(minimumHeight(), 400)); show(); } }; // // Implementation of ConfigCategoryReader // ConfigCategoryReader::ConfigCategoryReader() : CategoryReaderInterface(), m_currentItem(0) { KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); QList categoryOrder = config.readEntry("CategoryOrder", QList()); int categoryCount[NumTypes] = { 0 }; // Keep track of each category encountered. // Set a default: if(categoryOrder.isEmpty()) categoryOrder << Artist << Album << Title << Track; QList::ConstIterator catIt = categoryOrder.constBegin(); for(; catIt != categoryOrder.constEnd(); ++catIt) { int catCount = categoryCount[*catIt]++; TagType category = static_cast(*catIt); CategoryID catId(category, catCount); m_options[catId] = TagRenamerOptions(catId); m_categoryOrder << catId; } m_folderSeparators.fill(false, m_categoryOrder.count() - 1); QList checkedSeparators = config.readEntry("CheckedDirSeparators", QList()); QList::ConstIterator it = checkedSeparators.constBegin(); for(; it != checkedSeparators.constEnd(); ++it) { if(*it < m_folderSeparators.count()) m_folderSeparators[*it] = true; } m_musicFolder = config.readPathEntry("MusicFolder", "${HOME}/music"); m_separator = config.readEntry("Separator", " - "); } QString ConfigCategoryReader::categoryValue(TagType type) const { if(!m_currentItem) return QString(); Tag *tag = m_currentItem->file().tag(); switch(type) { case Track: return QString::number(tag->track()); case Year: return QString::number(tag->year()); case Title: return tag->title(); case Artist: return tag->artist(); case Album: return tag->album(); case Genre: return tag->genre(); default: return QString(); } } QString ConfigCategoryReader::prefix(const CategoryID &category) const { return m_options[category].prefix(); } QString ConfigCategoryReader::suffix(const CategoryID &category) const { return m_options[category].suffix(); } TagRenamerOptions::EmptyActions ConfigCategoryReader::emptyAction(const CategoryID &category) const { return m_options[category].emptyAction(); } QString ConfigCategoryReader::emptyText(const CategoryID &category) const { return m_options[category].emptyText(); } QList ConfigCategoryReader::categoryOrder() const { return m_categoryOrder; } QString ConfigCategoryReader::separator() const { return m_separator; } QString ConfigCategoryReader::musicFolder() const { return m_musicFolder; } int ConfigCategoryReader::trackWidth(int categoryNum) const { return m_options[CategoryID(Track, categoryNum)].trackWidth(); } bool ConfigCategoryReader::hasFolderSeparator(int index) const { if(index >= m_folderSeparators.count()) return false; return m_folderSeparators[index]; } bool ConfigCategoryReader::isDisabled(const CategoryID &category) const { return m_options[category].disabled(); } // // Implementation of FileRenamerWidget // FileRenamerWidget::FileRenamerWidget(QWidget *parent) : QWidget(parent), CategoryReaderInterface(), m_ui(new Ui::FileRenamerBase), m_exampleFromFile(false) { m_ui->setupUi(this); // This must be created before createTagRows() is called. m_exampleDialog = new ExampleOptionsDialog(this); createTagRows(); loadConfig(); // Add correct text to combo box. m_ui->m_category->clear(); for(int i = StartTag; i < NumTypes; ++i) { QString category = TagRenamerOptions::tagTypeText(static_cast(i)); m_ui->m_category->addItem(category); } connect(m_exampleDialog, &ExampleOptionsDialog::signalShown, this, &FileRenamerWidget::exampleDialogShown); connect(m_exampleDialog, &ExampleOptionsDialog::signalHidden, this, &FileRenamerWidget::exampleDialogHidden); connect(m_exampleDialog, &ExampleOptionsDialog::dataChanged, this, &FileRenamerWidget::dataSelected); connect(m_exampleDialog, &ExampleOptionsDialog::fileChanged, this, &FileRenamerWidget::fileSelected); connect(m_ui->dlgButtonBox, &QDialogButtonBox::accepted, this, [this]() { emit accepted(); }); connect(m_ui->dlgButtonBox, &QDialogButtonBox::rejected, this, [this]() { emit rejected(); }); exampleTextChanged(); } void FileRenamerWidget::loadConfig() { QList checkedSeparators; KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); for(int i = 0; i < m_rows.count(); ++i) m_rows[i].options = TagRenamerOptions(m_rows[i].category); checkedSeparators = config.readEntry("CheckedDirSeparators", QList()); foreach(int separator, checkedSeparators) { if(separator < m_folderSwitches.count()) m_folderSwitches[separator]->setChecked(true); } QString path = config.readEntry("MusicFolder", "${HOME}/music"); m_ui->m_musicFolder->setUrl(QUrl::fromLocalFile(path)); m_ui->m_musicFolder->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); m_ui->m_separator->setEditText(config.readEntry("Separator", " - ")); } void FileRenamerWidget::saveConfig() { KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); QList checkedSeparators; QList categoryOrder; for(int i = 0; i < m_rows.count(); ++i) { int rowId = idOfPosition(i); // Write out in GUI order, not m_rows order m_rows[rowId].options.saveConfig(m_rows[rowId].category.categoryNumber); categoryOrder += m_rows[rowId].category.category; } for(int i = 0; i < m_folderSwitches.count(); ++i) if(m_folderSwitches[i]->isChecked() == true) checkedSeparators += i; config.writeEntry("CheckedDirSeparators", checkedSeparators); config.writeEntry("CategoryOrder", categoryOrder); config.writePathEntry("MusicFolder", m_ui->m_musicFolder->url().path()); config.writeEntry("Separator", m_ui->m_separator->currentText()); config.sync(); } FileRenamerWidget::~FileRenamerWidget() { } int FileRenamerWidget::addRowCategory(TagType category) { static QIcon up = QIcon::fromTheme("go-up"); static QIcon down = QIcon::fromTheme("go-down"); // Find number of categories already of this type. int categoryCount = 0; for(const auto &row : m_rows) if(row.category.category == category) ++categoryCount; Row row; row.category = CategoryID(category, categoryCount); row.position = m_rows.count(); QFrame *frame = new QFrame(m_mainFrame); QHBoxLayout *frameLayout = new QHBoxLayout(frame); frameLayout->setMargin(3); row.widget = frame; frame->setFrameShape(QFrame::Box); frame->setLineWidth(1); QBoxLayout *mainFrameLayout = static_cast(m_mainFrame->layout()); mainFrameLayout->addWidget(frame, 1); QFrame *buttons = new QFrame(frame); QVBoxLayout *buttonLayout = new QVBoxLayout(buttons); frameLayout->addWidget(buttons); buttons->setFrameStyle(QFrame::Plain | QFrame::Box); buttons->setLineWidth(1); row.upButton = new QPushButton(buttons); row.downButton = new QPushButton(buttons); row.upButton->setIcon(up); row.downButton->setIcon(down); row.upButton->setFlat(true); row.downButton->setFlat(true); buttonLayout->addWidget(row.upButton); buttonLayout->addWidget(row.downButton); QString labelText = QString("%1").arg(TagRenamerOptions::tagTypeText(category)); QLabel *label = new QLabel(labelText, frame); frameLayout->addWidget(label, 1); label->setAlignment(Qt::AlignCenter); QVBoxLayout *optionLayout = new QVBoxLayout; frameLayout->addLayout(optionLayout); row.enableButton = new QPushButton(i18nc("remove music genre from file renamer", "Remove"), frame); optionLayout->addWidget(row.enableButton); row.optionsButton = new QPushButton(i18nc("file renamer genre options", "Options"), frame); optionLayout->addWidget(row.optionsButton); row.widget->show(); m_rows.append(row); assignPositionHandlerForRow(row); // Disable add button if there's too many rows. if(m_rows.count() == MAX_CATEGORIES) m_ui->m_insertCategory->setEnabled(false); return row.position; } void FileRenamerWidget::assignPositionHandlerForRow(Row &row) { const auto id = row.position; disconnect(row.upButton); disconnect(row.downButton); disconnect(row.enableButton); disconnect(row.optionsButton); connect(row.upButton, &QPushButton::clicked, this, [this, id]() { this->moveItemUp(id); }); connect(row.downButton, &QPushButton::clicked, this, [this, id]() { this->moveItemDown(id); }); connect(row.enableButton, &QPushButton::clicked, this, [this, id]() { this->slotRemoveRow(id); }); connect(row.optionsButton, &QPushButton::clicked, this, [this, id]() { this->showCategoryOption(id); }); } bool FileRenamerWidget::removeRow(int id) { if(id >= m_rows.count()) { qCWarning(JUK_LOG) << "Trying to remove row, but " << id << " is out-of-range.\n"; return false; } if(m_rows.count() == 1) { qCCritical(JUK_LOG) << "Can't remove last row of File Renamer.\n"; return false; } delete m_rows[id].widget; m_rows[id].widget = nullptr; m_rows[id].enableButton = nullptr; m_rows[id].upButton = nullptr; m_rows[id].optionsButton = nullptr; m_rows[id].downButton = nullptr; int checkboxPosition = 0; // Remove first checkbox. // If not the first row, remove the checkbox before it. if(m_rows[id].position > 0) checkboxPosition = m_rows[id].position - 1; // The checkbox is contained within a layout widget, so the layout // widget is the one the needs to die. delete m_folderSwitches[checkboxPosition]->parent(); m_folderSwitches.erase(&m_folderSwitches[checkboxPosition]); // Go through all the rows and if they have the same category and a // higher categoryNumber, decrement the number. Also update the // position identifier. for(int i = 0; i < m_rows.count(); ++i) { if(i == id) continue; // Don't mess with ourself. if((m_rows[id].category.category == m_rows[i].category.category) && (m_rows[id].category.categoryNumber < m_rows[i].category.categoryNumber)) { --m_rows[i].category.categoryNumber; } // Items are moving up. if(m_rows[id].position < m_rows[i].position) --m_rows[i].position; } // Every row after the one we delete will have a different identifier, since // the identifier is simply its index into m_rows. So we need to re-do the // signal mappings for the affected rows after updating its position. for(int i = id + 1; i < m_rows.count(); ++i) assignPositionHandlerForRow(m_rows[i]); m_rows.erase(&m_rows[id]); // Make sure we update the buttons of affected rows. m_rows[idOfPosition(0)].upButton->setEnabled(false); m_rows[idOfPosition(m_rows.count() - 1)].downButton->setEnabled(false); // We can insert another row now, make sure GUI is updated to match. m_ui->m_insertCategory->setEnabled(true); QTimer::singleShot(0, this, &FileRenamerWidget::exampleTextChanged); return true; } void FileRenamerWidget::addFolderSeparatorCheckbox() { QWidget *temp = new QWidget(m_mainFrame); m_mainFrame->layout()->addWidget(temp); QHBoxLayout *l = new QHBoxLayout(temp); QCheckBox *cb = new QCheckBox(i18n("Insert folder separator"), temp); m_folderSwitches.append(cb); l->addWidget(cb, 0, Qt::AlignCenter); cb->setChecked(false); connect(cb, &QCheckBox::toggled, this, &FileRenamerWidget::exampleTextChanged); temp->show(); } void FileRenamerWidget::createTagRows() { KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); QList categoryOrder = config.readEntry("CategoryOrder", QList()); if(categoryOrder.isEmpty()) categoryOrder << Artist << Album << Title << Track; // Setup arrays. m_rows.reserve(categoryOrder.count()); m_folderSwitches.reserve(categoryOrder.count() - 1); m_mainFrame = new QFrame(m_ui->m_mainView); m_ui->m_mainView->setWidget(m_mainFrame); m_ui->m_mainView->setWidgetResizable(true); QVBoxLayout *frameLayout = new QVBoxLayout(m_mainFrame); frameLayout->setMargin(10); frameLayout->setSpacing(5); // OK, the deal with the categoryOrder variable is that we need to create // the rows in the order that they were saved in (the order given by categoryOrder). // The signal mappers operate according to the row identifier. To find the position of // a row given the identifier, use m_rows[id].position. To find the id of a given // position, use idOfPosition(position). for(auto it = categoryOrder.cbegin(); it != categoryOrder.cend(); ++it) { if(*it < StartTag || *it >= NumTypes) { qCCritical(JUK_LOG) << "Invalid category encountered in file renamer configuration.\n"; continue; } if(m_rows.count() == MAX_CATEGORIES) { qCCritical(JUK_LOG) << "Maximum number of File Renamer tags reached, bailing.\n"; break; } addRowCategory(static_cast(*it)); // Insert the directory separator checkbox if this isn't the last // item. if((it + 1) != categoryOrder.constEnd()) addFolderSeparatorCheckbox(); } m_rows.first().upButton->setEnabled(false); m_rows.last().downButton->setEnabled(false); // If we have maximum number of categories already, don't let the user // add more. if(m_rows.count() >= MAX_CATEGORIES) m_ui->m_insertCategory->setEnabled(false); } void FileRenamerWidget::exampleTextChanged() { // Just use .mp3 as an example if(m_exampleFromFile && (m_exampleFile.isEmpty() || !FileHandle(m_exampleFile).tag()->isValid())) { m_ui->m_exampleText->setText(i18n("No file selected, or selected file has no tags.")); return; } m_ui->m_exampleText->setText(FileRenamer::fileName(*this) + ".mp3"); } QString FileRenamerWidget::fileCategoryValue(TagType category) const { FileHandle file(m_exampleFile); Tag *tag = file.tag(); switch(category) { case Track: return QString::number(tag->track()); case Year: return QString::number(tag->year()); case Title: return tag->title(); case Artist: return tag->artist(); case Album: return tag->album(); case Genre: return tag->genre(); default: return QString(); } } QString FileRenamerWidget::categoryValue(TagType category) const { if(m_exampleFromFile) return fileCategoryValue(category); const ExampleOptions *example = m_exampleDialog->widget(); switch (category) { case Track: return example->m_exampleTrack->text(); case Year: return example->m_exampleYear->text(); case Title: return example->m_exampleTitle->text(); case Artist: return example->m_exampleArtist->text(); case Album: return example->m_exampleAlbum->text(); case Genre: return example->m_exampleGenre->text(); default: return QString(); } } QList FileRenamerWidget::categoryOrder() const { QList list; // Iterate in GUI row order. for(int i = 0; i < m_rows.count(); ++i) { int rowId = idOfPosition(i); list += m_rows[rowId].category; } return list; } bool FileRenamerWidget::hasFolderSeparator(int index) const { if(index >= m_folderSwitches.count()) return false; return m_folderSwitches[index]->isChecked(); } void FileRenamerWidget::moveItem(int id, MovementDirection direction) { QWidget *l = m_rows[id].widget; int bottom = m_rows.count() - 1; int pos = m_rows[id].position; int newPos = (direction == MoveUp) ? pos - 1 : pos + 1; // Item we're moving can't go further down after this. if((pos == (bottom - 1) && direction == MoveDown) || (pos == bottom && direction == MoveUp)) { int idBottomRow = idOfPosition(bottom); int idAboveBottomRow = idOfPosition(bottom - 1); m_rows[idBottomRow].downButton->setEnabled(true); m_rows[idAboveBottomRow].downButton->setEnabled(false); } // We're moving the top item, do some button switching. if((pos == 0 && direction == MoveDown) || (pos == 1 && direction == MoveUp)) { int idTopItem = idOfPosition(0); int idBelowTopItem = idOfPosition(1); m_rows[idTopItem].upButton->setEnabled(true); m_rows[idBelowTopItem].upButton->setEnabled(false); } // This is the item we're swapping with. int idSwitchWith = idOfPosition(newPos); QWidget *w = m_rows[idSwitchWith].widget; // Update the table of widget rows. std::swap(m_rows[id].position, m_rows[idSwitchWith].position); // Move the item two spaces above/below its previous position. It has to // be 2 spaces because of the checkbox. QBoxLayout *layout = dynamic_cast(m_mainFrame->layout()); if ( !layout ) return; layout->removeWidget(l); layout->insertWidget(2 * newPos, l); // Move the top item two spaces in the opposite direction, for a similar // reason. layout->removeWidget(w); layout->insertWidget(2 * pos, w); layout->invalidate(); QTimer::singleShot(0, this, &FileRenamerWidget::exampleTextChanged); } int FileRenamerWidget::idOfPosition(int position) const { if(position >= m_rows.count()) { qCCritical(JUK_LOG) << "Search for position " << position << " out-of-range.\n"; return -1; } for(int i = 0; i < m_rows.count(); ++i) if(m_rows[i].position == position) return i; qCCritical(JUK_LOG) << "Unable to find identifier for position " << position; return -1; } int FileRenamerWidget::findIdentifier(const CategoryID &category) const { for(int index = 0; index < m_rows.count(); ++index) if(m_rows[index].category == category) return index; qCCritical(JUK_LOG) << "Unable to find match for category " << TagRenamerOptions::tagTypeText(category.category) << ", number " << category.categoryNumber; return MAX_CATEGORIES; } void FileRenamerWidget::showCategoryOption(int id) { TagOptionsDialog *dialog = new TagOptionsDialog(this, m_rows[id].options, m_rows[id].category.categoryNumber); if(dialog->exec() == QDialog::Accepted) { m_rows[id].options = dialog->options(); exampleTextChanged(); } delete dialog; } void FileRenamerWidget::moveItemUp(int id) { moveItem(id, MoveUp); } void FileRenamerWidget::moveItemDown(int id) { moveItem(id, MoveDown); } void FileRenamerWidget::toggleExampleDialog() { m_exampleDialog->setHidden(!m_exampleDialog->isHidden()); } void FileRenamerWidget::insertCategory() { TagType category = static_cast(m_ui->m_category->currentIndex()); if(m_ui->m_category->currentIndex() < 0 || category >= NumTypes) { qCCritical(JUK_LOG) << "Trying to add unknown category somehow.\n"; return; } // We need to enable the down button of the current bottom row since it // can now move down. int idBottom = idOfPosition(m_rows.count() - 1); m_rows[idBottom].downButton->setEnabled(true); addFolderSeparatorCheckbox(); // Identifier of new row. int id = addRowCategory(category); // Set its down button to be disabled. m_rows[id].downButton->setEnabled(false); m_mainFrame->layout()->invalidate(); m_ui->m_mainView->update(); // Now update according to the code in loadConfig(). m_rows[id].options = TagRenamerOptions(m_rows[id].category); exampleTextChanged(); } void FileRenamerWidget::exampleDialogShown() { m_ui->m_showExample->setText(i18n("Hide Renamer Test Dialog")); } void FileRenamerWidget::exampleDialogHidden() { m_ui->m_showExample->setText(i18n("Show Renamer Test Dialog")); } void FileRenamerWidget::fileSelected(const QString &file) { m_exampleFromFile = true; m_exampleFile = file; exampleTextChanged(); } void FileRenamerWidget::dataSelected() { m_exampleFromFile = false; exampleTextChanged(); } QString FileRenamerWidget::separator() const { return m_ui->m_separator->currentText(); } QString FileRenamerWidget::musicFolder() const { return m_ui->m_musicFolder->url().path(); } void FileRenamerWidget::slotRemoveRow(int id) { // Remove the given identified row. if(!removeRow(id)) qCCritical(JUK_LOG) << "Unable to remove row " << id; } // // Implementation of FileRenamer // FileRenamer::FileRenamer() { } void FileRenamer::rename(PlaylistItem *item) { PlaylistItemList list; list.append(item); rename(list); } void FileRenamer::rename(const PlaylistItemList &items) { ConfigCategoryReader reader; QStringList errorFiles; QMap map; QMap itemMap; for(PlaylistItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) { reader.setPlaylistItem(*it); QString oldFile = (*it)->file().absFilePath(); QString extension = (*it)->file().fileInfo().suffix(); QString newFile = fileName(reader) + '.' + extension; if(oldFile != newFile) { map[oldFile] = newFile; itemMap[oldFile] = *it; } } if(itemMap.isEmpty() || ConfirmationDialog(map).exec() != QDialog::Accepted) return; QApplication::setOverrideCursor(Qt::WaitCursor); for(QMap::ConstIterator it = map.constBegin(); it != map.constEnd(); ++it) { if(moveFile(it.key(), it.value())) { itemMap[it.key()]->setFile(it.value()); itemMap[it.key()]->refresh(); setFolderIcon(QUrl::fromLocalFile(it.value()), itemMap[it.key()]); } else errorFiles << i18n("%1 to %2", it.key(), it.value()); processEvents(); } QApplication::restoreOverrideCursor(); if(!errorFiles.isEmpty()) KMessageBox::errorList(0, i18n("The following rename operations failed:\n"), errorFiles); } bool FileRenamer::moveFile(const QString &src, const QString &dest) { qCDebug(JUK_LOG) << "Moving file " << src << " to " << dest; QUrl srcURL = QUrl::fromLocalFile(src); QUrl dstURL = QUrl::fromLocalFile(dest); if(!srcURL.isValid() || !dstURL.isValid() || srcURL == dstURL) return false; QUrl dir = dstURL.resolved(QUrl::fromUserInput(".")); // resolves to path w/out filename if(!QDir().mkpath(dir.path())) { qCCritical(JUK_LOG) << "Unable to create directory " << dir.path(); return false; } // Move the file. KIO::Job *job = KIO::file_move(srcURL, dstURL); return job->exec(); } void FileRenamer::setFolderIcon(const QUrl &dstURL, const PlaylistItem *item) { if(item->file().tag()->album().isEmpty() || !item->file().coverInfo()->hasCover()) { return; } // Split path, and go through each path element. If a path element has // the album information, set its folder icon. QStringList elements = dstURL.path().split('/', QString::SkipEmptyParts); QString path; for(QStringList::ConstIterator it = elements.constBegin(); it != elements.constEnd(); ++it) { path.append('/' + (*it)); qCDebug(JUK_LOG) << "Checking path: " << path; if((*it).contains(item->file().tag()->album()) && QDir(path).exists() && !QFile::exists(path + "/.directory")) { // Seems to be a match, let's set the folder icon for the current // path. First we should write out the file. QPixmap thumb = item->file().coverInfo()->pixmap(CoverInfo::Thumbnail); thumb.save(path + "/.juk-thumbnail.png", "PNG"); KDesktopFile dirFile(path + "/.directory"); KConfigGroup desktopGroup(dirFile.desktopGroup()); if(!desktopGroup.hasKey("Icon")) { desktopGroup.writePathEntry("Icon", QString("%1/.juk-thumbnail.png").arg(path)); dirFile.sync(); } return; } } } /** * Returns iterator pointing to the last item enabled in the given list with * a non-empty value (or is required to be included). */ QList::ConstIterator lastEnabledItem(const QList &list, const CategoryReaderInterface &interface) { QList::ConstIterator it = list.constBegin(); QList::ConstIterator last = list.constEnd(); for(; it != list.constEnd(); ++it) { if(interface.isRequired(*it) || (!interface.isDisabled(*it) && !interface.categoryValue((*it).category).isEmpty())) { last = it; } } return last; } QString FileRenamer::fileName(const CategoryReaderInterface &interface) { const QList categoryOrder = interface.categoryOrder(); const QString separator = interface.separator(); const QString folder = interface.musicFolder(); QList::ConstIterator lastEnabled; int i = 0; QStringList list; QChar dirSeparator (QDir::separator()); // Use lastEnabled to properly handle folder separators. lastEnabled = lastEnabledItem(categoryOrder, interface); bool pastLast = false; // Toggles to true once we've passed lastEnabled. for(QList::ConstIterator it = categoryOrder.constBegin(); it != categoryOrder.constEnd(); ++it, ++i) { if(it == lastEnabled) pastLast = true; if(interface.isDisabled(*it)) continue; QString value = interface.value(*it); // The user can use the folder separator checkbox to add folders, so don't allow // slashes that slip in to accidentally create new folders. Should we filter this // back out when showing it in the GUI? value.replace('/', "%2f"); if(!pastLast && interface.hasFolderSeparator(i)) value.append(dirSeparator); if(interface.isRequired(*it) || !value.isEmpty()) list.append(value); } // Construct a single string representation, handling strings ending in // '/' specially QString result; for(QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); /* Empty */) { result += *it; ++it; // Manually advance iterator to check for end-of-list. // Add separator unless at a directory boundary if(it != list.constEnd() && !(*it).startsWith(dirSeparator) && // Check beginning of next item. !result.endsWith(dirSeparator)) { result += separator; } } return QString(folder + dirSeparator + result); } // vim: set et sw=4 tw=0 sta: diff --git a/tag.cpp b/juktag.cpp similarity index 99% rename from tag.cpp rename to juktag.cpp index 79090eb2..ed7bc330 100644 --- a/tag.cpp +++ b/juktag.cpp @@ -1,247 +1,247 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2009 Michael Pyne * * 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 "tag.h" +#include "juktag.h" #include #include -#include +#include //taglib #include #include #include #include "cache.h" #include "mediafiles.h" #include "stringshare.h" #include "juk_debug.h" //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// Tag::Tag(const QString &fileName) : m_fileName(fileName), m_track(0), m_year(0), m_seconds(0), m_bitrate(0), m_isValid(false) { if(fileName.isEmpty()) { qCCritical(JUK_LOG) << "Trying to add empty file"; return; } TagLib::File *file = MediaFiles::fileFactoryByType(fileName); if(file && file->isValid()) { setup(file); delete file; } else { qCCritical(JUK_LOG) << "Couldn't resolve the mime type of \"" << fileName << "\" -- this shouldn't happen."; } } bool Tag::save() const { bool result; TagLib::ID3v2::FrameFactory::instance()->setDefaultTextEncoding(TagLib::String::UTF8); TagLib::File *file = MediaFiles::fileFactoryByType(m_fileName); if(file && !file->readOnly() && file->isValid() && file->tag()) { file->tag()->setTitle(TagLib::String(m_title.toUtf8().constData(), TagLib::String::UTF8)); file->tag()->setArtist(TagLib::String(m_artist.toUtf8().constData(), TagLib::String::UTF8)); file->tag()->setAlbum(TagLib::String(m_album.toUtf8().constData(), TagLib::String::UTF8)); file->tag()->setGenre(TagLib::String(m_genre.toUtf8().constData(), TagLib::String::UTF8)); file->tag()->setComment(TagLib::String(m_comment.toUtf8().constData(), TagLib::String::UTF8)); file->tag()->setTrack(m_track); file->tag()->setYear(m_year); result = file->save(); } else { qCCritical(JUK_LOG) << "Couldn't save file."; result = false; } delete file; return result; } QString Tag::playingString() const { QString str; if(artist().isEmpty()) str = title(); else { str = i18nc("a playing track, %1 is artist, %2 is song title", "%1 - %2", artist(), title()); } return str; } CacheDataStream &Tag::read(CacheDataStream &s) { switch(s.cacheVersion()) { case 1: { qint32 track; qint32 year; qint32 bitrate; qint32 seconds; s >> m_title >> m_artist >> m_album >> m_genre >> track >> year >> m_comment >> bitrate >> m_lengthString >> seconds; m_track = track; m_year = year; m_bitrate = bitrate; m_seconds = seconds; break; } default: { static QString dummyString; static int dummyInt; QString bitrateString; s >> dummyInt >> m_title >> m_artist >> m_album >> m_genre >> dummyInt >> m_track >> dummyString >> m_year >> dummyString >> m_comment >> bitrateString >> m_lengthString >> m_seconds >> dummyString; bool ok; m_bitrate = bitrateString.toInt(&ok); if(!ok) m_bitrate = 0; break; } } minimizeMemoryUsage(); return s; } //////////////////////////////////////////////////////////////////////////////// // private methods //////////////////////////////////////////////////////////////////////////////// Tag::Tag(const QString &fileName, bool) : m_fileName(fileName), m_track(0), m_year(0), m_seconds(0), m_bitrate(0), m_isValid(true) { } void Tag::setup(TagLib::File *file) { if(!file || !file->tag()) { qCWarning(JUK_LOG) << "Can't setup invalid file" << m_fileName; return; } m_title = TStringToQString(file->tag()->title()).trimmed(); m_artist = TStringToQString(file->tag()->artist()).trimmed(); m_album = TStringToQString(file->tag()->album()).trimmed(); m_genre = TStringToQString(file->tag()->genre()).trimmed(); m_comment = TStringToQString(file->tag()->comment()).trimmed(); m_track = file->tag()->track(); m_year = file->tag()->year(); m_seconds = file->audioProperties()->length(); m_bitrate = file->audioProperties()->bitrate(); const int seconds = m_seconds % 60; const int minutes = (m_seconds - seconds) / 60; m_lengthString = QString::number(minutes) + (seconds >= 10 ? ":" : ":0") + QString::number(seconds); if(m_title.isEmpty()) { int i = m_fileName.lastIndexOf('/'); int j = m_fileName.lastIndexOf('.'); m_title = i > 0 ? m_fileName.mid(i + 1, j - i - 1) : m_fileName; } minimizeMemoryUsage(); m_isValid = true; } void Tag::minimizeMemoryUsage() { // Try to reduce memory usage: share tags that frequently repeat, squeeze others m_title.squeeze(); m_lengthString.squeeze(); m_comment = StringShare::tryShare(m_comment); m_artist = StringShare::tryShare(m_artist); m_album = StringShare::tryShare(m_album); m_genre = StringShare::tryShare(m_genre); } //////////////////////////////////////////////////////////////////////////////// // related functions //////////////////////////////////////////////////////////////////////////////// QDataStream &operator<<(QDataStream &s, const Tag &t) { s << t.title() << t.artist() << t.album() << t.genre() << qint32(t.track()) << qint32(t.year()) << t.comment() << qint32(t.bitrate()) << t.lengthString() << qint32(t.seconds()); return s; } CacheDataStream &operator>>(CacheDataStream &s, Tag &t) { return t.read(s); } // vim: set et sw=4 tw=0 sta: diff --git a/tag.h b/juktag.h similarity index 100% rename from tag.h rename to juktag.h diff --git a/lyricswidget.cpp b/lyricswidget.cpp index d928a44a..31abfaf0 100644 --- a/lyricswidget.cpp +++ b/lyricswidget.cpp @@ -1,170 +1,170 @@ /** * Copyright (C) 2012 Martin Sandsmark * * 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 "lyricswidget.h" #include #include #include #include #include #include #include #include #include #include #include -#include "tag.h" +#include "juktag.h" #include "actioncollection.h" #include "juk_debug.h" LyricsWidget::LyricsWidget(QWidget* parent): QTextBrowser(parent), m_networkAccessManager(new QNetworkAccessManager), m_lyricsCurrent(false) { setMinimumWidth(200); setReadOnly(true); setWordWrapMode(QTextOption::WordWrap); setOpenExternalLinks(true); KToggleAction *show = new KToggleAction(QIcon::fromTheme(QLatin1String("view-media-lyrics")), i18n("Show &Lyrics"), this); ActionCollection::actions()->addAction("showLyrics", show); connect(show, SIGNAL(toggled(bool)), this, SLOT(setVisible(bool))); KConfigGroup config(KSharedConfig::openConfig(), "LyricsWidget"); bool shown = config.readEntry("Show", true); show->setChecked(shown); setVisible(shown); } LyricsWidget::~LyricsWidget() { delete m_networkAccessManager; saveConfig(); } void LyricsWidget::saveConfig() { KConfigGroup config(KSharedConfig::openConfig(), "LyricsWidget"); config.writeEntry("Show", ActionCollection::action("showLyrics")->isChecked()); } void LyricsWidget::makeLyricsRequest() { m_lyricsCurrent = true; if(m_playingFile.isNull()) { setHtml(i18n("No file playing.")); return; } setHtml(i18n("Loading...")); // TODO time for https (as long as it doesn't break this) QUrl listUrl("http://lyrics.wikia.com/api.php"); QUrlQuery listUrlQuery; listUrlQuery.addQueryItem("action", "lyrics"); listUrlQuery.addQueryItem("func", "getSong"); listUrlQuery.addQueryItem("fmt", "xml"); listUrlQuery.addQueryItem("artist", m_playingFile.tag()->artist()); listUrlQuery.addQueryItem("song", m_playingFile.tag()->title()); listUrl.setQuery(listUrlQuery); m_title = m_playingFile.tag()->artist() + " – " + m_playingFile.tag()->title(); connect(m_networkAccessManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(receiveListReply(QNetworkReply*))); m_networkAccessManager->get(QNetworkRequest(listUrl)); } void LyricsWidget::playing(const FileHandle &file) { m_playingFile = file; m_lyricsCurrent = false; if(isVisible()) { makeLyricsRequest(); } } void LyricsWidget::showEvent(QShowEvent *) { if(!m_lyricsCurrent) { makeLyricsRequest(); } } void LyricsWidget::receiveListReply(QNetworkReply* reply) { disconnect(m_networkAccessManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(receiveListReply(QNetworkReply*))); if (reply->error() != QNetworkReply::NoError) { qCWarning(JUK_LOG) << "Error while fetching lyrics: " << reply->errorString(); setHtml(i18n("Error while retrieving lyrics!")); return; } QDomDocument document; document.setContent(reply); QString artist = document.elementsByTagName("artist").at(0).toElement().text(); QString title = document.elementsByTagName("song").at(0).toElement().text(); // TODO time for https (as long as it doesn't break this) QUrl url("http://lyrics.wikia.com/api.php"); QUrlQuery urlQuery; urlQuery.addQueryItem("action", "query"); urlQuery.addQueryItem("prop", "revisions"); urlQuery.addQueryItem("rvprop", "content"); urlQuery.addQueryItem("format", "xml"); urlQuery.addQueryItem("titles", artist + ':' + title); url.setQuery(urlQuery); connect(m_networkAccessManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(receiveLyricsReply(QNetworkReply*))); m_networkAccessManager->get(QNetworkRequest(url)); } void LyricsWidget::receiveLyricsReply(QNetworkReply* reply) { disconnect(m_networkAccessManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(receiveLyricsReply(QNetworkReply*))); if (reply->error() != QNetworkReply::NoError) { qCWarning(JUK_LOG) << "Error while fetching lyrics: " << reply->errorString(); setHtml(i18n("Error while retrieving lyrics!")); return; } const QUrlQuery replyUrlQuery(reply->url()); QString titlesUrlPart = replyUrlQuery.queryItemValue(QStringLiteral("titles"), QUrl::FullyEncoded); if (titlesUrlPart.isEmpty()) { // default homepage, but this code path should never happen at this point. titlesUrlPart = QStringLiteral("Lyrics_Wiki"); } const QString lyricsUrl = QStringLiteral("http://lyrics.wikia.com/wiki/") + titlesUrlPart; QString content = QString::fromUtf8(reply->readAll()); int lIndex = content.indexOf("<lyrics>"); int rIndex = content.indexOf("</lyrics>"); if (lIndex == -1 || rIndex == -1) { qCWarning(JUK_LOG) << Q_FUNC_INFO << "Unable to find lyrics in text"; setText(i18n("No lyrics available.")); return; } lIndex += 15; // We skip the tag content = content.mid(lIndex, rIndex - lIndex).trimmed(); content.replace('\n', "
"); //setText(content); setHtml("

" + m_title + "

" + content + i18n("

Lyrics provided by LyricWiki", lyricsUrl)); } diff --git a/mpris2/mediaplayer2player.cpp b/mpris2/mediaplayer2player.cpp index 3103ef1e..436f5c22 100644 --- a/mpris2/mediaplayer2player.cpp +++ b/mpris2/mediaplayer2player.cpp @@ -1,336 +1,336 @@ /*********************************************************************** * Copyright 2012 Eike Hein * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . ***********************************************************************/ #include "mpris2/mediaplayer2player.h" #include "juk.h" #include "playermanager.h" #include "coverinfo.h" #include "playlist.h" #include "playlistitem.h" -#include "tag.h" +#include "juktag.h" #include "filehandle.h" #include #include #include #include #include #include #include static QByteArray idFromPlaylistItem(const PlaylistItem *item) { return QByteArray("/org/kde/juk/tid_") + QByteArray::number(item->trackId(), 16).rightJustified(8, '0'); } MediaPlayer2Player::MediaPlayer2Player(QObject* parent) : QDBusAbstractAdaptor(parent) , m_player(JuK::JuKInstance()->playerManager()) { //FIXME: Workaround for GCC 4.8, remove .data() in 2019 connect(m_player.data(), &PlayerManager::signalItemChanged, this, &MediaPlayer2Player::currentSourceChanged); connect(m_player.data(), &PlayerManager::signalPlay, this, &MediaPlayer2Player::stateUpdated); connect(m_player.data(), &PlayerManager::signalPause, this, &MediaPlayer2Player::stateUpdated); connect(m_player.data(), &PlayerManager::signalStop, this, &MediaPlayer2Player::stateUpdated); connect(m_player.data(), &PlayerManager::totalTimeChanged, this, &MediaPlayer2Player::totalTimeChanged); connect(m_player.data(), &PlayerManager::seekableChanged, this, &MediaPlayer2Player::seekableChanged); connect(m_player.data(), &PlayerManager::volumeChanged, this, &MediaPlayer2Player::volumeChanged); connect(m_player.data(), &PlayerManager::seeked, this, &MediaPlayer2Player::seeked); } MediaPlayer2Player::~MediaPlayer2Player() { } bool MediaPlayer2Player::CanGoNext() const { return true; } void MediaPlayer2Player::Next() const { m_player->forward(); } bool MediaPlayer2Player::CanGoPrevious() const { return true; } void MediaPlayer2Player::Previous() const { m_player->back(); } bool MediaPlayer2Player::CanPause() const { return true; } void MediaPlayer2Player::Pause() const { m_player->pause(); } void MediaPlayer2Player::PlayPause() const { m_player->playPause(); } void MediaPlayer2Player::Stop() const { m_player->stop(); } bool MediaPlayer2Player::CanPlay() const { return true; } void MediaPlayer2Player::Play() const { m_player->play(); } void MediaPlayer2Player::SetPosition(const QDBusObjectPath& TrackId, qlonglong Position) const { PlaylistItem *playingItem = Playlist::playingItem(); if (!playingItem) { return; } // Verify the SetPosition call is against the currently playing track QByteArray currentTrackId = idFromPlaylistItem(playingItem); if (TrackId.path().toLatin1() == currentTrackId) { m_player->seek(Position / 1000); } } void MediaPlayer2Player::OpenUri(QString Uri) const { QUrl url = QUrl::fromUserInput(Uri); // JuK does not yet support KIO if (url.isLocalFile()) { m_player->play(url.toLocalFile()); } } QString MediaPlayer2Player::PlaybackStatus() const { if (m_player->playing()) { return QLatin1String("Playing"); } else if (m_player->paused()) { return QLatin1String("Paused"); } return QLatin1String("Stopped"); } QString MediaPlayer2Player::LoopStatus() const { // TODO: Implement, although this is orthogonal to the PlayerManager return "None"; } void MediaPlayer2Player::setLoopStatus(const QString& loopStatus) const { Q_UNUSED(loopStatus) } double MediaPlayer2Player::Rate() const { return 1.0; } void MediaPlayer2Player::setRate(double rate) const { Q_UNUSED(rate) } bool MediaPlayer2Player::Shuffle() const { // TODO: Implement return false; } void MediaPlayer2Player::setShuffle(bool shuffle) const { Q_UNUSED(shuffle) // TODO: Implement } QVariantMap MediaPlayer2Player::Metadata() const { QVariantMap metaData; // The track ID is annoying since it must result in a valid DBus object // path, and the regex for that is, and I quote: [a-zA-Z0-9_]*, along with // the normal / delimiters for paths. PlaylistItem *item = Playlist::playingItem(); if(!item) return metaData; FileHandle playingFile = item->file(); QByteArray playingTrackFileId = idFromPlaylistItem(item); metaData["mpris:trackid"] = QVariant::fromValue( QDBusObjectPath(playingTrackFileId.constData())); const Tag *tag = playingFile.tag(); auto strValue = tag->album(); if(!strValue.isEmpty()) metaData["xesam:album"] = strValue; strValue = tag->title(); if(!strValue.isEmpty()) metaData["xesam:title"] = strValue; strValue = tag->artist(); if(!strValue.isEmpty()) metaData["xesam:artist"] = QStringList(strValue); strValue = tag->genre(); if(!strValue.isEmpty()) metaData["xesam:genre"] = QStringList(strValue); metaData["mpris:length"] = qint64(playingFile.tag()->seconds() * 1000000); metaData["xesam:url"] = QString::fromUtf8( QUrl::fromLocalFile(playingFile.absFilePath()).toEncoded()); if(playingFile.coverInfo()->hasCover()) { const QString fallbackFileName = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QStringLiteral("/") + QString("juk-cover-%1.png").arg(item->trackId()); QString path = fallbackFileName; if(!QFile::exists(path)) { path = playingFile.coverInfo()->localPathToCover(fallbackFileName); } metaData["mpris:artUrl"] = QString::fromUtf8( QUrl::fromLocalFile(path).toEncoded()); } return metaData; } double MediaPlayer2Player::Volume() const { return m_player->volume(); } void MediaPlayer2Player::setVolume(double volume) const { if (volume < 0.0) volume = 0.0; if (volume > 1.0) volume = 1.0; m_player->setVolume(volume); } qlonglong MediaPlayer2Player::Position() const { return m_player->currentTimeMSecs() * 1000; } double MediaPlayer2Player::MinimumRate() const { return 1.0; } double MediaPlayer2Player::MaximumRate() const { return 1.0; } bool MediaPlayer2Player::CanSeek() const { return m_player->seekable(); } void MediaPlayer2Player::Seek(qlonglong Offset) const { m_player->seek(((m_player->currentTimeMSecs() * 1000) + Offset) / 1000); } bool MediaPlayer2Player::CanControl() const { return true; } void MediaPlayer2Player::currentSourceChanged() const { QVariantMap properties; properties["Metadata"] = Metadata(); properties["CanSeek"] = CanSeek(); signalPropertiesChange(properties); } void MediaPlayer2Player::stateUpdated() const { QVariantMap properties; properties["PlaybackStatus"] = PlaybackStatus(); signalPropertiesChange(properties); } void MediaPlayer2Player::totalTimeChanged() const { QVariantMap properties; properties["Metadata"] = Metadata(); signalPropertiesChange(properties); } void MediaPlayer2Player::seekableChanged(bool seekable) const { QVariantMap properties; properties["CanSeek"] = seekable; signalPropertiesChange(properties); } void MediaPlayer2Player::volumeChanged(float newVol) const { Q_UNUSED(newVol) QVariantMap properties; properties["Volume"] = Volume(); signalPropertiesChange(properties); } void MediaPlayer2Player::seeked(int newPos) const { // casts int to uint64 emit Seeked(newPos); } void MediaPlayer2Player::signalPropertiesChange(const QVariantMap& properties) const { QDBusMessage msg = QDBusMessage::createSignal("/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged" ); msg << "org.mpris.MediaPlayer2.Player"; msg << properties; msg << QStringList(); QDBusConnection::sessionBus().send(msg); } diff --git a/nowplaying.cpp b/nowplaying.cpp index 75b8d64b..0893507e 100644 --- a/nowplaying.cpp +++ b/nowplaying.cpp @@ -1,341 +1,341 @@ /** * Copyright (C) 2004 Scott Wheeler * Copyright (C) 2009 Michael Pyne * * 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 "nowplaying.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "playlistcollection.h" #include "playlistitem.h" #include "coverinfo.h" #include "covermanager.h" -#include "tag.h" +#include "juktag.h" #include "collectionlist.h" #include "juk_debug.h" //////////////////////////////////////////////////////////////////////////////// // NowPlaying //////////////////////////////////////////////////////////////////////////////// NowPlaying::NowPlaying(QWidget *parent, PlaylistCollection *collection) : QWidget(parent), m_observer(this, collection), // Also watch the collection m_collectionListObserver(this, CollectionList::instance()), m_collection(collection) { setObjectName(QLatin1String("NowPlaying")); QHBoxLayout *layout = new QHBoxLayout(this); setLayout(layout); layout->setMargin(0); layout->setSpacing(3); // With HiDPI the text might actually be bigger... try to account for // that. const QFont defaultLargeFont(QFontDatabase::systemFont(QFontDatabase::TitleFont)); const QFontMetrics fm(defaultLargeFont, this); const int coverIconHeight = qMax(64, fm.lineSpacing() * 2); setFixedHeight(coverIconHeight + 4); layout->addWidget(new CoverItem(this), 0); layout->addWidget(new TrackItem(this), 2); hide(); } void NowPlaying::addItem(NowPlayingItem *item) { m_items.append(item); } PlaylistCollection *NowPlaying::collection() const { return m_collection; } void NowPlaying::slotUpdate(const FileHandle &file) { m_file = file; if(file.isNull()) { hide(); emit nowPlayingHidden(); return; } else show(); foreach(NowPlayingItem *item, m_items) item->update(file); } void NowPlaying::slotReloadCurrentItem() { foreach(NowPlayingItem *item, m_items) item->update(m_file); } //////////////////////////////////////////////////////////////////////////////// // CoverItem //////////////////////////////////////////////////////////////////////////////// CoverItem::CoverItem(NowPlaying *parent) : QLabel(parent), NowPlayingItem(parent) { setObjectName(QLatin1String("CoverItem")); setFixedHeight(parent->height() - parent->layout()->margin() * 2); setMargin(1); setAcceptDrops(true); } void CoverItem::update(const FileHandle &file) { m_file = file; if(!file.isNull() && file.coverInfo()->hasCover()) { show(); const auto pixRatio = this->devicePixelRatioF(); const QSizeF logicalSize = QSizeF(this->height(), this->height()); const QSizeF scaledSize = logicalSize * pixRatio; QPixmap pix = file.coverInfo()->pixmap(CoverInfo::FullSize) .scaled(scaledSize.toSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation); if (!qFuzzyCompare(pixRatio, 1.0)) { pix.setDevicePixelRatio(pixRatio); } setPixmap(pix); } else hide(); } void CoverItem::mouseReleaseEvent(QMouseEvent *event) { if(m_dragging) { m_dragging = false; return; } if(event->x() >= 0 && event->y() >= 0 && event->x() < width() && event->y() < height() && event->button() == Qt::LeftButton && m_file.coverInfo()->hasCover()) { m_file.coverInfo()->popup(); } QLabel::mousePressEvent(event); } void CoverItem::mousePressEvent(QMouseEvent *e) { m_dragging = false; m_dragStart = e->globalPos(); } void CoverItem::mouseMoveEvent(QMouseEvent *e) { if(m_dragging) return; QPoint diff = m_dragStart - e->globalPos(); if(diff.manhattanLength() > QApplication::startDragDistance()) { // Start a drag. m_dragging = true; QDrag *drag = new QDrag(this); CoverDrag *data = new CoverDrag(m_file.coverInfo()->coverId()); drag->setMimeData(data); drag->exec(Qt::CopyAction); } } void CoverItem::dragEnterEvent(QDragEnterEvent *e) { e->setAccepted(CoverDrag::isCover(e->mimeData()) || e->mimeData()->hasUrls()); } void CoverItem::dropEvent(QDropEvent *e) { QImage image; QList urls; coverKey key; if(e->source() == this) return; key = CoverDrag::idFromData(e->mimeData()); if(key != CoverManager::NoMatch) { m_file.coverInfo()->setCoverId(key); update(m_file); } else if(e->mimeData()->hasImage()) { m_file.coverInfo()->setCover(qvariant_cast(e->mimeData()->imageData())); update(m_file); } else { urls = e->mimeData()->urls(); if(urls.isEmpty()) return; QString fileName; auto getJob = KIO::storedGet(urls.front()); KJobWidgets::setWindow(getJob, this); if(getJob->exec()) { if(image.loadFromData(getJob->data())) { m_file.coverInfo()->setCover(image); update(m_file); } else qCCritical(JUK_LOG) << "Unable to load image from " << urls.front(); } else qCCritical(JUK_LOG) << "Unable to download " << urls.front(); } } //////////////////////////////////////////////////////////////////////////////// // TrackItem //////////////////////////////////////////////////////////////////////////////// TrackItem::TrackItem(NowPlaying *parent) : QWidget(parent), NowPlayingItem(parent) { setObjectName(QLatin1String("TrackItem")); setFixedHeight(parent->height() - parent->layout()->margin() * 2); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); QVBoxLayout *layout = new QVBoxLayout(this); m_label = new QLabel(this); m_label->setWordWrap(true); m_label->setTextInteractionFlags(Qt::LinksAccessibleByMouse|Qt::LinksAccessibleByKeyboard|Qt::TextSelectableByMouse); layout->addStretch(); layout->addWidget(m_label, 1); layout->addStretch(); connect(m_label, SIGNAL(linkActivated(QString)), this, SLOT(slotOpenLink(QString))); // Ensure that if we're filtering results, that the filtering is cleared if we // hide the now playing bar so that the user can select tracks normally. connect(parent, SIGNAL(nowPlayingHidden()), SLOT(slotClearShowMore())); } void TrackItem::update(const FileHandle &file) { m_file = file; QTimer::singleShot(0, this, SLOT(slotUpdate())); } void TrackItem::slotOpenLink(const QString &link) { PlaylistCollection *collection = parentManager()->collection(); if(link == "artist") collection->showMore(m_file.tag()->artist()); else if(link == "album") collection->showMore(m_file.tag()->artist(), m_file.tag()->album()); else if(link == "clear") collection->clearShowMore(); update(m_file); } void TrackItem::slotUpdate() { if(m_file.isNull()) { m_label->setText(QString()); return; } QString title = m_file.tag()->title().toHtmlEscaped(); QString artist = m_file.tag()->artist().toHtmlEscaped(); QString album = m_file.tag()->album().toHtmlEscaped(); QString separator = (artist.isEmpty() || album.isEmpty()) ? QString() : QString(" - "); // This block-o-nastiness makes the font smaller and smaller until it actually fits. int size = 4; QString format = "%2" "
" "%4%5%6"; if(parentManager()->collection()->showMoreActive()) format.append(QString(" (%1)").arg(i18n("back to playlist"))); format.append(""); int parentHeight = parentManager()->contentsRect().height(); int neededHeight = 0; do { m_label->setText(format.arg(size).arg(title).arg(size - 2) .arg(artist).arg(separator).arg(album)); --size; neededHeight = m_label->heightForWidth(m_label->width()); } while(neededHeight > parentHeight && size >= -1); m_label->setFixedHeight(qMin(neededHeight, parentHeight)); } void TrackItem::slotClearShowMore() { PlaylistCollection *collection = parentManager()->collection(); Q_ASSERT(collection); collection->clearShowMore(); } // vim: set et sw=4 tw=0 sta: diff --git a/playermanager.cpp b/playermanager.cpp index abc4f670..1feef5b9 100644 --- a/playermanager.cpp +++ b/playermanager.cpp @@ -1,517 +1,517 @@ /** * Copyright (C) 2004 Scott Wheeler * Copyright (C) 2007 Matthias Kretz * Copyright (C) 2008, 2009, 2018 Michael Pyne * * 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 "playermanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "playlistinterface.h" #include "playeradaptor.h" #include "slideraction.h" #include "statuslabel.h" #include "actioncollection.h" #include "collectionlist.h" #include "coverinfo.h" -#include "tag.h" +#include "juktag.h" #include "scrobbler.h" #include "juk.h" #include "juk_debug.h" using namespace ActionCollection; enum PlayerManagerStatus { StatusStopped = -1, StatusPaused = 1, StatusPlaying = 2 }; //////////////////////////////////////////////////////////////////////////////// // static functions //////////////////////////////////////////////////////////////////////////////// static void updateWindowTitle(const FileHandle &file) { JuK::JuKInstance()->setWindowTitle(i18nc( "%1 is the artist and %2 is the title of the currently playing track.", "%1 - %2 :: JuK", file.tag()->artist(), file.tag()->title())); } //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// PlayerManager::PlayerManager() : QObject(), m_playlistInterface(nullptr), m_output(new Phonon::AudioOutput(Phonon::MusicCategory, this)), m_media( new Phonon::MediaObject(this)), m_audioPath(Phonon::createPath(m_media, m_output)) { setupAudio(); new PlayerAdaptor(this); QDBusConnection::sessionBus().registerObject("/Player", this); } //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// bool PlayerManager::playing() const { Phonon::State state = m_media->state(); return (state == Phonon::PlayingState || state == Phonon::BufferingState); } bool PlayerManager::paused() const { return m_media->state() == Phonon::PausedState; } bool PlayerManager::muted() const { return m_output->isMuted(); } float PlayerManager::volume() const { return m_output->volume(); } int PlayerManager::status() const { if(paused()) return StatusPaused; if(playing()) return StatusPlaying; return StatusStopped; } int PlayerManager::totalTime() const { return totalTimeMSecs() / 1000; } int PlayerManager::currentTime() const { return currentTimeMSecs() / 1000; } int PlayerManager::totalTimeMSecs() const { return m_media->totalTime(); } int PlayerManager::currentTimeMSecs() const { return m_media->currentTime(); } bool PlayerManager::seekable() const { return m_media->isSeekable(); } QStringList PlayerManager::trackProperties() { return FileHandle::properties(); } QString PlayerManager::trackProperty(const QString &property) const { if(!playing() && !paused()) return QString(); return m_file.property(property); } QPixmap PlayerManager::trackCover(const QString &size) const { if(!playing() && !paused()) return QPixmap(); if(size.toLower() == "small") return m_file.coverInfo()->pixmap(CoverInfo::Thumbnail); if(size.toLower() == "large") return m_file.coverInfo()->pixmap(CoverInfo::FullSize); return QPixmap(); } FileHandle PlayerManager::playingFile() const { return m_file; } QString PlayerManager::playingString() const { if(!playing() || m_file.isNull()) return QString(); return m_file.tag()->playingString(); } void PlayerManager::setPlaylistInterface(PlaylistInterface *interface) { m_playlistInterface = interface; } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void PlayerManager::play(const FileHandle &file) { if(!m_media || !m_playlistInterface || file.isNull()) return; m_media->setCurrentSource(QUrl::fromLocalFile(file.absFilePath())); m_media->play(); if(m_file != file) emit signalItemChanged(file); m_file = file; // Our state changed handler will perform the follow up actions necessary // once we actually start playing. } void PlayerManager::play(const QString &file) { CollectionListItem *item = CollectionList::instance()->lookup(file); if(item) { Playlist::setPlaying(item); play(item->file()); } } void PlayerManager::play() { if(paused()) m_media->play(); else if(playing()) { m_media->seek(0); emit seeked(0); } else { m_playlistInterface->playNext(); const auto file = m_playlistInterface->currentFile(); play(file); } } void PlayerManager::pause() { if(paused()) return; action("pause")->setEnabled(false); m_media->pause(); } void PlayerManager::stop() { if(!m_playlistInterface) return; action("pause")->setEnabled(false); action("stop")->setEnabled(false); action("back")->setEnabled(false); action("forward")->setEnabled(false); action("forwardAlbum")->setEnabled(false); if(!m_file.isNull()) { m_file = FileHandle(); emit signalItemChanged(m_file); } m_media->stop(); } void PlayerManager::setVolume(float volume) { m_output->setVolume(volume); } void PlayerManager::seek(int seekTime) { if(m_media->currentTime() == seekTime) return; m_media->seek(seekTime); emit seeked(seekTime); } void PlayerManager::seekForward() { const qint64 total = m_media->totalTime(); const qint64 newtime = m_media->currentTime() + total / 100; const qint64 seekTo = qMin(total, newtime); m_media->seek(seekTo); emit seeked(seekTo); } void PlayerManager::seekBack() { const qint64 total = m_media->totalTime(); const qint64 newtime = m_media->currentTime() - total / 100; const qint64 seekTo = qMax(qint64(0), newtime); m_media->seek(seekTo); emit seeked(seekTo); } void PlayerManager::playPause() { playing() ? action("pause")->trigger() : action("play")->trigger(); } void PlayerManager::forward() { m_playlistInterface->playNext(); FileHandle file = m_playlistInterface->currentFile(); if(!file.isNull()) play(file); else stop(); } void PlayerManager::back() { m_playlistInterface->playPrevious(); FileHandle file = m_playlistInterface->currentFile(); if(!file.isNull()) play(file); else stop(); } void PlayerManager::volumeUp() { const auto newVolume = std::min(m_output->volume() + 0.04, 1.0); m_output->setVolume(newVolume); // 4% up } void PlayerManager::volumeDown() { const auto newVolume = std::max(m_output->volume() - 0.04, 0.0); m_output->setVolume(newVolume); // 4% down } void PlayerManager::setMuted(bool m) { m_output->setMuted(m); } bool PlayerManager::mute() { bool newState = !muted(); setMuted(newState); return newState; } //////////////////////////////////////////////////////////////////////////////// // private slots //////////////////////////////////////////////////////////////////////////////// void PlayerManager::slotFinished() { // It is possible to end up in this function if a file simply fails to play or if the // user moves the slider all the way to the end, therefore see if we can keep playing // and if we can, do so. Otherwise, stop. m_playlistInterface->playNext(); play(m_playlistInterface->currentFile()); } void PlayerManager::slotLength(qint64 msec) { emit totalTimeChanged(msec); } void PlayerManager::slotTick(qint64 msec) { emit tick(msec); } void PlayerManager::slotStateChanged(Phonon::State newstate, Phonon::State) { if(newstate == Phonon::ErrorState) { QString errorMessage = i18nc( "%1 will be the /path/to/file, %2 will be some string from Phonon describing the error", "JuK is unable to play the audio file%1" "for the following reason:%2", m_file.absFilePath(), m_media->errorString() ); qCWarning(JUK_LOG) << "Phonon is in error state" << m_media->errorString() << "while playing" << m_file.absFilePath(); switch(m_media->errorType()) { case Phonon::NoError: qCDebug(JUK_LOG) << "received a state change to ErrorState but errorType is NoError!?"; break; case Phonon::NormalError: KMessageBox::information(0, errorMessage); break; case Phonon::FatalError: KMessageBox::sorry(0, errorMessage); break; } stop(); return; } // "normal" path if(newstate == Phonon::StoppedState && m_file.isNull()) { JuK::JuKInstance()->setWindowTitle(i18n("JuK")); emit signalStop(); } else if(newstate == Phonon::PausedState) { emit signalPause(); } else { // PlayingState or BufferingState action("pause")->setEnabled(true); action("stop")->setEnabled(true); action("forward")->setEnabled(true); if(action("albumRandomPlay")->isChecked()) action("forwardAlbum")->setEnabled(true); action("back")->setEnabled(true); updateWindowTitle(m_file); emit signalPlay(); } } void PlayerManager::slotSeekableChanged(bool isSeekable) { emit seekableChanged(isSeekable); } void PlayerManager::slotMutedChanged(bool muted) { emit mutedChanged(muted); } void PlayerManager::slotVolumeChanged(qreal volume) { if(qFuzzyCompare(m_output->volume(), volume)) { return; } emit volumeChanged(volume); } //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void PlayerManager::setupAudio() { using namespace Phonon; connect(m_output, &AudioOutput::mutedChanged, this, &PlayerManager::slotMutedChanged); connect(m_output, &AudioOutput::volumeChanged, this, &PlayerManager::slotVolumeChanged); connect(m_media, &MediaObject::stateChanged, this, &PlayerManager::slotStateChanged); connect(m_media, &MediaObject::currentSourceChanged, this, &PlayerManager::trackHasChanged); connect(m_media, &MediaObject::totalTimeChanged, this, &PlayerManager::slotLength); connect(m_media, &MediaObject::tick, this, &PlayerManager::slotTick); connect(m_media, &MediaObject::aboutToFinish, this, &PlayerManager::trackAboutToFinish); connect(m_media, &MediaObject::finished, this, &PlayerManager::slotFinished); connect(m_media, &MediaObject::seekableChanged, this, &PlayerManager::slotSeekableChanged); m_media->setTickInterval(100); } QString PlayerManager::randomPlayMode() const { if(action("randomPlay")->isChecked()) return "Random"; if(action("albumRandomPlay")->isChecked()) return "AlbumRandom"; return "NoRandom"; } void PlayerManager::setRandomPlayMode(const QString &randomMode) { if(randomMode.toLower() == "random") action("randomPlay")->setChecked(true); if(randomMode.toLower() == "albumrandom") action("albumRandomPlay")->setChecked(true); if(randomMode.toLower() == "norandom") action("disableRandomPlay")->setChecked(true); } void PlayerManager::trackHasChanged(const Phonon::MediaSource &newSource) { if(newSource.type() == Phonon::MediaSource::Url) { const auto item = CollectionList::instance()->lookup(newSource.url().path()); if(item) { const auto newFile = item->file(); if(m_file != newFile) emit signalItemChanged(newFile); m_file = newFile; updateWindowTitle(m_file); emit seeked(0); } } else { qCWarning(JUK_LOG) << "Track has changed so something we didn't set???"; return; } } void PlayerManager::trackAboutToFinish() { // Called when playback is in progress and a track is about to finish, gives us a // chance to keep audio playback going without Phonon entering StoppedState if(!m_playlistInterface) return; m_playlistInterface->playNext(); const auto file = m_playlistInterface->currentFile(); if(!file.isNull()) m_media->enqueue(QUrl::fromLocalFile(file.absFilePath())); } // vim: set et sw=4 tw=0 sta: diff --git a/playlist.cpp b/playlist.cpp index c03e3339..881c82e2 100644 --- a/playlist.cpp +++ b/playlist.cpp @@ -1,2082 +1,2082 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2008-2018 Michael Pyne * * 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 "playlist.h" #include "juk-exception.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "directoryloader.h" #include "playlistitem.h" #include "playlistcollection.h" #include "playlistsearch.h" #include "playlistsharedsettings.h" #include "mediafiles.h" #include "collectionlist.h" #include "filerenamer.h" #include "actioncollection.h" #include "tracksequencemanager.h" -#include "tag.h" +#include "juktag.h" #include "upcomingplaylist.h" #include "deletedialog.h" #include "webimagefetcher.h" #include "coverinfo.h" #include "coverdialog.h" #include "tagtransactionmanager.h" #include "cache.h" #include "juk_debug.h" using namespace ActionCollection; /** * Used to give every track added in the program a unique identifier. See * PlaylistItem */ quint32 g_trackID = 0; /** * Just a shortcut of sorts. */ static bool manualResize() { return action("resizeColumnsManually")->isChecked(); } //////////////////////////////////////////////////////////////////////////////// // static members //////////////////////////////////////////////////////////////////////////////// bool Playlist::m_visibleChanged = false; bool Playlist::m_shuttingDown = false; PlaylistItemList Playlist::m_history; QVector Playlist::m_backMenuItems; int Playlist::m_leftColumn = 0; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// Playlist::Playlist( bool delaySetup, const QString &name, PlaylistCollection *collection, const QString &iconName, int extraCols) : QTreeWidget(collection->playlistStack()) , m_collection(collection) , m_playlistName(name) , m_fetcher(new WebImageFetcher(this)) { // Any added columns must precede normal ones, which are normally added // in setup() for(int i = 0; i < extraCols; ++i) { addColumn(i18n("JuK")); // Placeholder text } setup(); // Some subclasses need to do even more handling but will remember to // call setupPlaylist if(!delaySetup) { collection->setupPlaylist(this, iconName); } } Playlist::Playlist(PlaylistCollection *collection, const QString &name, const QString &iconName) : Playlist(false, name, collection, iconName, 0) { } Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items, const QString &name, const QString &iconName) : Playlist(false, name, collection, iconName, 0) { createItems(items); } Playlist::Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile, const QString &iconName) : Playlist(true, QString(), collection, iconName, 0) { m_fileName = playlistFile.canonicalFilePath(); loadFile(m_fileName, playlistFile); collection->setupPlaylist(this, iconName); } Playlist::Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns) : Playlist(delaySetup, QString(), collection, QStringLiteral("audio-midi"), extraColumns) { } Playlist::~Playlist() { // In some situations the dataChanged signal from clearItems will cause observers to // subsequently try to access a deleted item. Since we're going away just remove all // observers. clearObservers(); // clearItem() will take care of removing the items from the history, // so call clearItems() to make sure it happens. clearItems(items()); if(!m_shuttingDown) m_collection->removePlaylist(this); } QString Playlist::name() const { if(m_playlistName.isEmpty()) return m_fileName.section(QDir::separator(), -1).section('.', 0, -2); else return m_playlistName; } FileHandle Playlist::currentFile() const { return playingItem() ? playingItem()->file() : FileHandle(); } void Playlist::playFirst() { TrackSequenceManager::instance()->setNextItem(static_cast( *QTreeWidgetItemIterator(const_cast(this), QTreeWidgetItemIterator::NotHidden))); action("forward")->trigger(); } void Playlist::playNextAlbum() { PlaylistItem *current = TrackSequenceManager::instance()->currentItem(); if(!current) return; // No next album if we're not already playing. QString currentAlbum = current->file().tag()->album(); current = TrackSequenceManager::instance()->nextItem(); while(current && current->file().tag()->album() == currentAlbum) current = TrackSequenceManager::instance()->nextItem(); TrackSequenceManager::instance()->setNextItem(current); action("forward")->trigger(); } void Playlist::playNext() { TrackSequenceManager::instance()->setCurrentPlaylist(this); setPlaying(TrackSequenceManager::instance()->nextItem()); } void Playlist::stop() { m_history.clear(); setPlaying(nullptr); } void Playlist::playPrevious() { if(!playingItem()) return; bool random = action("randomPlay") && action("randomPlay")->isChecked(); PlaylistItem *previous = nullptr; if(random && !m_history.isEmpty()) { PlaylistItemList::Iterator last = m_history.end() - 1; previous = *last; m_history.erase(last); } else { m_history.clear(); previous = TrackSequenceManager::instance()->previousItem(); } if(!previous) previous = static_cast(playingItem()->itemAbove()); setPlaying(previous, false); } void Playlist::setName(const QString &n) { m_collection->addNameToDict(n); m_collection->removeNameFromDict(m_playlistName); m_playlistName = n; emit signalNameChanged(m_playlistName); } void Playlist::save() { if(m_fileName.isEmpty()) return saveAs(); QFile file(m_fileName); if(!file.open(QIODevice::WriteOnly)) return KMessageBox::error(this, i18n("Could not save to file %1.", m_fileName)); QTextStream stream(&file); QStringList fileList = files(); foreach(const QString &file, fileList) stream << file << endl; file.close(); } void Playlist::saveAs() { m_collection->removeFileFromDict(m_fileName); m_fileName = MediaFiles::savePlaylistDialog(name(), this); if(!m_fileName.isEmpty()) { m_collection->addFileToDict(m_fileName); // If there's no playlist name set, use the file name. if(m_playlistName.isEmpty()) emit signalNameChanged(name()); save(); } } void Playlist::updateDeletedItem(PlaylistItem *item) { m_members.remove(item->file().absFilePath()); m_search.clearItem(item); m_history.removeAll(item); } void Playlist::clearItem(PlaylistItem *item) { // Automatically updates internal structs via updateDeletedItem delete item; playlistItemsChanged(); } void Playlist::clearItems(const PlaylistItemList &items) { foreach(PlaylistItem *item, items) delete item; playlistItemsChanged(); } PlaylistItem *Playlist::playingItem() // static { return PlaylistItem::playingItems().isEmpty() ? 0 : PlaylistItem::playingItems().front(); } QStringList Playlist::files() const { QStringList list; for(QTreeWidgetItemIterator it(const_cast(this)); *it; ++it) list.append(static_cast(*it)->file().absFilePath()); return list; } PlaylistItemList Playlist::items() { return items(QTreeWidgetItemIterator::IteratorFlag(0)); } PlaylistItemList Playlist::visibleItems() { return items(QTreeWidgetItemIterator::NotHidden); } PlaylistItemList Playlist::selectedItems() { return items(QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden); } PlaylistItem *Playlist::firstChild() const { return static_cast(topLevelItem(0)); } void Playlist::updateLeftColumn() { int newLeftColumn = leftMostVisibleColumn(); if(m_leftColumn != newLeftColumn) { updatePlaying(); m_leftColumn = newLeftColumn; } } void Playlist::setItemsVisible(const PlaylistItemList &items, bool visible) // static { m_visibleChanged = true; foreach(PlaylistItem *playlistItem, items) playlistItem->setHidden(!visible); } void Playlist::setSearch(const PlaylistSearch &s) { m_search = s; if(!m_searchEnabled) return; setItemsVisible(s.matchedItems(), true); setItemsVisible(s.unmatchedItems(), false); TrackSequenceManager::instance()->iterator()->playlistChanged(); } void Playlist::setSearchEnabled(bool enabled) { if(m_searchEnabled == enabled) return; m_searchEnabled = enabled; if(enabled) { setItemsVisible(m_search.matchedItems(), true); setItemsVisible(m_search.unmatchedItems(), false); } else setItemsVisible(items(), true); } // Mostly seems to be for DynamicPlaylist // TODO: See if this can't all be eliminated by making 'is-playing' a predicate // of the playlist item itself void Playlist::synchronizePlayingItems(const PlaylistList &sources, bool setMaster) { foreach(const Playlist *p, sources) { if(p->playing()) { CollectionListItem *base = playingItem()->collectionItem(); for(QTreeWidgetItemIterator itemIt(this); *itemIt; ++itemIt) { PlaylistItem *item = static_cast(*itemIt); if(base == item->collectionItem()) { item->setPlaying(true, setMaster); PlaylistItemList playing = PlaylistItem::playingItems(); TrackSequenceManager::instance()->setCurrent(item); return; } } return; } } } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void Playlist::copy() { PlaylistItemList items = selectedItems(); QList urls; foreach(PlaylistItem *item, items) { urls << QUrl::fromLocalFile(item->file().absFilePath()); } QMimeData *mimeData = new QMimeData; mimeData->setUrls(urls); QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } void Playlist::paste() { addFilesFromMimeData( QApplication::clipboard()->mimeData(), static_cast(currentItem())); } void Playlist::clear() { PlaylistItemList l = selectedItems(); if(l.isEmpty()) l = items(); clearItems(l); } void Playlist::slotRefresh() { PlaylistItemList l = selectedItems(); if(l.isEmpty()) l = visibleItems(); QApplication::setOverrideCursor(Qt::WaitCursor); foreach(PlaylistItem *item, l) { item->refreshFromDisk(); if(!item->file().tag() || !item->file().fileInfo().exists()) { qCDebug(JUK_LOG) << "Error while trying to refresh the tag. " << "This file has probably been removed."; delete item->collectionItem(); } processEvents(); } QApplication::restoreOverrideCursor(); } void Playlist::slotRenameFile() { FileRenamer renamer; PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; emit signalEnableDirWatch(false); m_blockDataChanged = true; renamer.rename(items); m_blockDataChanged = false; playlistItemsChanged(); emit signalEnableDirWatch(true); } void Playlist::slotViewCover() { const PlaylistItemList items = selectedItems(); if (items.isEmpty()) return; foreach(const PlaylistItem *item, items) item->file().coverInfo()->popup(); } void Playlist::slotRemoveCover() { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; int button = KMessageBox::warningContinueCancel(this, i18n("Are you sure you want to delete these covers?"), QString(), KGuiItem(i18n("&Delete Covers"))); if(button == KMessageBox::Continue) refreshAlbums(items); } void Playlist::slotShowCoverManager() { static CoverDialog *managerDialog = 0; if(!managerDialog) managerDialog = new CoverDialog(this); managerDialog->show(); } void Playlist::slotAddCover(bool retrieveLocal) { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; if(!retrieveLocal) { m_fetcher->setFile((*items.begin())->file()); m_fetcher->searchCover(); return; } QUrl file = QFileDialog::getOpenFileUrl( this, i18n("Select Cover Image File"), QUrl::fromLocalFile(QDir::home().path()), i18n("Images (*.png *.jpg)"), nullptr, 0, QStringList() << QStringLiteral("file") ); if(file.isEmpty()) return; QString artist = items.front()->file().tag()->artist(); QString album = items.front()->file().tag()->album(); coverKey newId = CoverManager::addCover(file, artist, album); if(newId != CoverManager::NoMatch) refreshAlbums(items, newId); } // Called when image fetcher has added a new cover. void Playlist::slotCoverChanged(int coverId) { qCDebug(JUK_LOG) << "Refreshing information for newly changed covers.\n"; refreshAlbums(selectedItems(), coverId); } void Playlist::slotGuessTagInfo(TagGuesser::Type type) { QApplication::setOverrideCursor(Qt::WaitCursor); const PlaylistItemList items = selectedItems(); setDynamicListsFrozen(true); m_blockDataChanged = true; foreach(PlaylistItem *item, items) { item->guessTagInfo(type); processEvents(); } // MusicBrainz queries automatically commit at this point. What would // be nice is having a signal emitted when the last query is completed. if(type == TagGuesser::FileName) TagTransactionManager::instance()->commit(); m_blockDataChanged = false; playlistItemsChanged(); setDynamicListsFrozen(false); QApplication::restoreOverrideCursor(); } void Playlist::slotReload() { QFileInfo fileInfo(m_fileName); if(!fileInfo.exists() || !fileInfo.isFile() || !fileInfo.isReadable()) return; clearItems(items()); loadFile(m_fileName, fileInfo); } void Playlist::slotWeightDirty(int column) { if(column < 0) { m_weightDirty.clear(); for(int i = 0; i < columnCount(); i++) { if(!isColumnHidden(i)) m_weightDirty.append(i); } return; } if(!m_weightDirty.contains(column)) m_weightDirty.append(column); } void Playlist::slotShowPlaying() { if(!playingItem()) return; Playlist *l = playingItem()->playlist(); l->clearSelection(); // Raise the playlist before selecting the items otherwise the tag editor // will not update when it gets the selectionChanged() notification // because it will think the user is choosing a different playlist but not // selecting a different item. m_collection->raise(l); l->setCurrentItem(playingItem()); l->scrollToItem(playingItem()); } void Playlist::slotColumnResizeModeChanged() { if(manualResize()) { header()->setSectionResizeMode(QHeaderView::Interactive); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } else { header()->setSectionResizeMode(QHeaderView::Fixed); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } if(!manualResize()) slotUpdateColumnWidths(); SharedSettings::instance()->sync(); } void Playlist::playlistItemsChanged() { if(m_blockDataChanged) return; PlaylistInterface::playlistItemsChanged(); } //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// void Playlist::removeFromDisk(const PlaylistItemList &items) { if(isVisible() && !items.isEmpty()) { QStringList files; foreach(const PlaylistItem *item, items) files.append(item->file().absFilePath()); DeleteDialog dialog(this); m_blockDataChanged = true; if(dialog.confirmDeleteList(files)) { bool shouldDelete = dialog.shouldDelete(); QStringList errorFiles; foreach(PlaylistItem *item, items) { if(playingItem() == item) action("forward")->trigger(); QString removePath = item->file().absFilePath(); QUrl removeUrl = QUrl::fromLocalFile(removePath); if((!shouldDelete && KIO::trash(removeUrl)->exec()) || (shouldDelete && QFile::remove(removePath))) { delete item->collectionItem(); } else errorFiles.append(item->file().absFilePath()); } if(!errorFiles.isEmpty()) { QString errorMsg = shouldDelete ? i18n("Could not delete these files") : i18n("Could not move these files to the Trash"); KMessageBox::errorList(this, errorMsg, errorFiles); } } m_blockDataChanged = false; playlistItemsChanged(); } } void Playlist::synchronizeItemsTo(const PlaylistItemList &itemList) { // direct call to ::items to avoid infinite loop, bug 402355 clearItems(Playlist::items()); createItems(itemList); } void Playlist::dragEnterEvent(QDragEnterEvent *e) { if(CoverDrag::isCover(e->mimeData())) { setDropIndicatorShown(false); e->accept(); return; } if(e->mimeData()->hasUrls() && !e->mimeData()->urls().isEmpty()) { setDropIndicatorShown(true); e->acceptProposedAction(); } else e->ignore(); } void Playlist::addFilesFromMimeData(const QMimeData *urls, PlaylistItem *after) { if(!urls->hasUrls()) { return; } addFiles(QUrl::toStringList(urls->urls(), QUrl::PreferLocalFile), after); } bool Playlist::eventFilter(QObject *watched, QEvent *e) { if(watched == header()) { switch(e->type()) { case QEvent::MouseMove: { if((static_cast(e)->modifiers() & Qt::LeftButton) == Qt::LeftButton && !action("resizeColumnsManually")->isChecked()) { m_columnWidthModeChanged = true; action("resizeColumnsManually")->setChecked(true); slotColumnResizeModeChanged(); } break; } case QEvent::MouseButtonPress: { if(static_cast(e)->button() == Qt::RightButton) m_headerMenu->popup(QCursor::pos()); break; } case QEvent::MouseButtonRelease: { if(m_columnWidthModeChanged) { m_columnWidthModeChanged = false; notifyUserColumnWidthModeChanged(); } if(!manualResize() && m_widthsDirty) QTimer::singleShot(0, this, SLOT(slotUpdateColumnWidths())); break; } default: break; } } return QTreeWidget::eventFilter(watched, e); } void Playlist::keyPressEvent(QKeyEvent *event) { if(event->key() == Qt::Key_Up) { const auto topItem = topLevelItem(0); if(topItem && topItem == currentItem()) { QTreeWidgetItemIterator visible(this, QTreeWidgetItemIterator::NotHidden); if(topItem == *visible) { emit signalMoveFocusAway(); event->accept(); } } } QTreeWidget::keyPressEvent(event); } QStringList Playlist::mimeTypes() const { return QStringList("text/uri-list"); } QMimeData* Playlist::mimeData(const QList items) const { QList urls; foreach(QTreeWidgetItem *item, items) { urls << QUrl::fromLocalFile(static_cast(item)->file().absFilePath()); } QMimeData *urlDrag = new QMimeData(); urlDrag->setUrls(urls); return urlDrag; } bool Playlist::dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action) { // TODO: Re-add DND Q_UNUSED(parent); Q_UNUSED(index); Q_UNUSED(data); Q_UNUSED(action); return false; } void Playlist::dropEvent(QDropEvent *e) { QPoint vp = e->pos(); PlaylistItem *item = static_cast(itemAt(vp)); // First see if we're dropping a cover, if so we can get it out of the // way early. if(item && CoverDrag::isCover(e->mimeData())) { coverKey id = CoverDrag::idFromData(e->mimeData()); // If the item we dropped on is selected, apply cover to all selected // items, otherwise just apply to the dropped item. if(item->isSelected()) { const PlaylistItemList selItems = selectedItems(); foreach(PlaylistItem *playlistItem, selItems) { playlistItem->file().coverInfo()->setCoverId(id); playlistItem->refresh(); } } else { item->file().coverInfo()->setCoverId(id); item->refresh(); } return; } // When dropping on the toUpper half of an item, insert before this item. // This is what the user expects, and also allows the insertion at // top of the list QRect rect = visualItemRect(item); if(!item) item = static_cast(topLevelItem(topLevelItemCount() - 1)); else if(vp.y() < rect.y() + rect.height() / 2) item = static_cast(item->itemAbove()); m_blockDataChanged = true; if(e->source() == this) { // Since we're trying to arrange things manually, turn off sorting. sortItems(columnCount() + 1, Qt::AscendingOrder); const QList items = QTreeWidget::selectedItems(); foreach(QTreeWidgetItem *listViewItem, items) { if(!item) { // Insert the item at the top of the list. This is a bit ugly, // but I don't see another way. takeItem(listViewItem); insertItem(listViewItem); } //else // listViewItem->moveItem(item); item = static_cast(listViewItem); } } else addFilesFromMimeData(e->mimeData(), item); m_blockDataChanged = false; playlistItemsChanged(); emit signalPlaylistItemsDropped(this); QTreeWidget::dropEvent(e); } void Playlist::showEvent(QShowEvent *e) { if(m_applySharedSettings) { SharedSettings::instance()->apply(this); m_applySharedSettings = false; } QTreeWidget::showEvent(e); } void Playlist::applySharedSettings() { m_applySharedSettings = true; } void Playlist::read(QDataStream &s) { s >> m_playlistName >> m_fileName; // m_fileName is probably empty. if(m_playlistName.isEmpty()) throw BICStreamException(); // Do not sort. Add the files in the order they were saved. setSortingEnabled(false); QStringList files; s >> files; QTreeWidgetItem *after = 0; m_blockDataChanged = true; foreach(const QString &file, files) { if(file.isEmpty()) throw BICStreamException(); after = createItem(FileHandle(file), after); } m_blockDataChanged = false; playlistItemsChanged(); m_collection->setupPlaylist(this, "audio-midi"); } void Playlist::paintEvent(QPaintEvent *pe) { // If there are columns that need to be updated, well, update them. if(!m_weightDirty.isEmpty() && !manualResize()) { calculateColumnWeights(); slotUpdateColumnWidths(); } QTreeWidget::paintEvent(pe); } void Playlist::resizeEvent(QResizeEvent *re) { // If the width of the view has changed, manually update the column // widths. if(re->size().width() != re->oldSize().width() && !manualResize()) slotUpdateColumnWidths(); QTreeWidget::resizeEvent(re); } // Reimplemented to show a visual indication of which of the view's playlist // items is actually playing. void Playlist::drawRow(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { PlaylistItem *item = static_cast(itemFromIndex(index)); if(Q_LIKELY(!PlaylistItem::playingItems().contains(item))) { return QTreeWidget::drawRow(p, option, index); } // Seems that the view draws the background now so we have to do this // manually p->fillRect(option.rect, QPalette{}.midlight()); QStyleOptionViewItem newOption {option}; newOption.font.setBold(true); QTreeWidget::drawRow(p, newOption, index); } void Playlist::insertItem(QTreeWidgetItem *item) { QTreeWidget::insertTopLevelItem(0, item); } void Playlist::takeItem(QTreeWidgetItem *item) { int index = indexOfTopLevelItem(item); QTreeWidget::takeTopLevelItem(index); } void Playlist::addColumn(const QString &label, int) { m_columns.append(label); setHeaderLabels(m_columns); } PlaylistItem *Playlist::createItem(const FileHandle &file, QTreeWidgetItem *after) { return createItem(file, after); } void Playlist::createItems(const PlaylistItemList &siblings, PlaylistItem *after) { createItems(siblings, after); } void Playlist::addFiles(const QStringList &files, PlaylistItem *after) { if(Q_UNLIKELY(files.isEmpty())) { return; } m_blockDataChanged = true; setEnabled(false); QVector> pendingFutures; for(const auto &file : files) { // some files added here will launch threads that we must wait until // they're done to cleanup auto pendingResult = addUntypedFile(file, after); if(!pendingResult.isFinished()) { pendingFutures.push_back(pendingResult); ++m_itemsLoading; } } // It's possible for no async threads to be launched, and also possible // for this function to be called while there were other threads in flight if(pendingFutures.isEmpty() && m_itemsLoading == 0) { cleanupAfterAllFileLoadsCompleted(); return; } // Build handlers for all the still-active loaders on the heap and then // return to the event loop. for(const auto &future : qAsConst(pendingFutures)) { auto loadWatcher = new QFutureWatcher(this); loadWatcher->setFuture(future); connect(loadWatcher, &QFutureWatcher::finished, this, [=]() { if(--m_itemsLoading == 0) { cleanupAfterAllFileLoadsCompleted(); } loadWatcher->deleteLater(); }); } } void Playlist::refreshAlbums(const PlaylistItemList &items, coverKey id) { QList< QPair > albums; bool setAlbumCovers = items.count() == 1; foreach(const PlaylistItem *item, items) { QString artist = item->file().tag()->artist(); QString album = item->file().tag()->album(); if(!albums.contains(qMakePair(artist, album))) albums.append(qMakePair(artist, album)); item->file().coverInfo()->setCoverId(id); if(setAlbumCovers) item->file().coverInfo()->applyCoverToWholeAlbum(true); } for(QList< QPair >::ConstIterator it = albums.constBegin(); it != albums.constEnd(); ++it) { refreshAlbum((*it).first, (*it).second); } } void Playlist::updatePlaying() const { foreach(const PlaylistItem *item, PlaylistItem::playingItems()) item->treeWidget()->viewport()->update(); } void Playlist::refreshAlbum(const QString &artist, const QString &album) { ColumnList columns; columns.append(PlaylistItem::ArtistColumn); PlaylistSearch::Component artistComponent(artist, false, columns, PlaylistSearch::Component::Exact); columns.clear(); columns.append(PlaylistItem::AlbumColumn); PlaylistSearch::Component albumComponent(album, false, columns, PlaylistSearch::Component::Exact); PlaylistSearch::ComponentList components; components.append(artist); components.append(album); PlaylistList playlists; playlists.append(CollectionList::instance()); PlaylistSearch search(playlists, components); const PlaylistItemList matches = search.matchedItems(); foreach(PlaylistItem *item, matches) item->refresh(); } void Playlist::hideColumn(int c, bool updateSearch) { foreach (QAction *action, m_headerMenu->actions()) { if(!action) continue; if (action->data().toInt() == c) { action->setChecked(false); break; } } if(isColumnHidden(c)) return; QTreeWidget::hideColumn(c); if(c == m_leftColumn) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); viewport()->update(); } if(this != CollectionList::instance()) CollectionList::instance()->hideColumn(c, false); if(updateSearch) redisplaySearch(); } void Playlist::showColumn(int c, bool updateSearch) { foreach (QAction *action, m_headerMenu->actions()) { if(!action) continue; if (action->data().toInt() == c) { action->setChecked(true); break; } } if(!isColumnHidden(c)) return; QTreeWidget::showColumn(c); if(c == leftMostVisibleColumn()) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); viewport()->update(); } if(this != CollectionList::instance()) CollectionList::instance()->showColumn(c, false); if(updateSearch) redisplaySearch(); } void Playlist::sortByColumn(int column, Qt::SortOrder order) { setSortingEnabled(true); QTreeWidget::sortByColumn(column, order); } void Playlist::slotInitialize() { addColumn(i18n("Track Name")); addColumn(i18n("Artist")); addColumn(i18n("Album")); addColumn(i18n("Cover")); addColumn(i18nc("cd track number", "Track")); addColumn(i18n("Genre")); addColumn(i18n("Year")); addColumn(i18n("Length")); addColumn(i18n("Bitrate")); addColumn(i18n("Comment")); addColumn(i18n("File Name")); addColumn(i18n("File Name (full path)")); setAllColumnsShowFocus(true); setSelectionMode(QTreeWidget::ExtendedSelection); header()->setSortIndicatorShown(true); m_columnFixedWidths.resize(columnCount()); ////////////////////////////////////////////////// // setup header RMB menu ////////////////////////////////////////////////// QAction *showAction; const auto sharedSettings = SharedSettings::instance(); for(int i = 0; i < header()->count(); ++i) { if(i - columnOffset() == PlaylistItem::FileNameColumn) m_headerMenu->addSeparator(); showAction = new QAction(headerItem()->text(i), m_headerMenu); showAction->setData(i); showAction->setCheckable(true); showAction->setChecked(sharedSettings->isColumnVisible(i)); m_headerMenu->addAction(showAction); resizeColumnToContents(i); } connect(m_headerMenu, SIGNAL(triggered(QAction*)), this, SLOT(slotToggleColumnVisible(QAction*))); connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowRMBMenu(QPoint))); connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(slotPlayCurrent())); // Disabled for now because adding new items (File->Open) causes Qt to send // an itemChanged signal for unrelated playlist items which can cause the // inline editor done slot to mistakenly overwrite tags associated to // *other* playlist items. I haven't found a way to determine whether the // itemChanged signal is really coming from the inline editor so instead // users will need to use the tag editor. :( // -- mpyne 2018-12-20 //connect(this, &QTreeWidget::itemChanged, // this, &Playlist::slotInlineEditDone); connect(action("resizeColumnsManually"), SIGNAL(triggered()), this, SLOT(slotColumnResizeModeChanged())); if(action("resizeColumnsManually")->isChecked()) { header()->setSectionResizeMode(QHeaderView::Interactive); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } else { header()->setSectionResizeMode(QHeaderView::Fixed); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setDragEnabled(true); m_disableColumnWidthUpdates = false; } void Playlist::setupItem(PlaylistItem *item) { item->setTrackId(g_trackID); g_trackID++; if(!m_search.isEmpty()) item->setHidden(!m_search.checkItem(item)); if(topLevelItemCount() <= 2 && !manualResize()) { slotWeightDirty(); slotUpdateColumnWidths(); viewport()->update(); } } void Playlist::setDynamicListsFrozen(bool frozen) { m_collection->setDynamicListsFrozen(frozen); } CollectionListItem *Playlist::collectionListItem(const FileHandle &file) { CollectionListItem *item = CollectionList::instance()->lookup(file.absFilePath()); if(!item) { if(!QFile::exists(file.absFilePath())) { qCCritical(JUK_LOG) << "File" << file.absFilePath() << "does not exist."; return nullptr; } item = CollectionList::instance()->createItem(file); } return item; } //////////////////////////////////////////////////////////////////////////////// // protected slots //////////////////////////////////////////////////////////////////////////////// void Playlist::slotPopulateBackMenu() const { if(!playingItem()) return; QMenu *menu = action("back")->menu(); menu->clear(); m_backMenuItems.clear(); m_backMenuItems.reserve(10); int count = 0; PlaylistItemList::ConstIterator it = m_history.constEnd(); QAction *action; while(it != m_history.constBegin() && count < 10) { ++count; --it; action = new QAction((*it)->file().tag()->title(), menu); action->setData(count - 1); menu->addAction(action); m_backMenuItems << *it; } } void Playlist::slotPlayFromBackMenu(QAction *backAction) const { int number = backAction->data().toInt(); if(number >= m_backMenuItems.size()) return; TrackSequenceManager::instance()->setNextItem(m_backMenuItems[number]); action("forward")->trigger(); } //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void Playlist::setup() { setAlternatingRowColors(true); setRootIsDecorated(false); setContextMenuPolicy(Qt::CustomContextMenu); setUniformRowHeights(true); setEditTriggers(QAbstractItemView::EditKeyPressed); // Don't edit on double-click connect(header(), SIGNAL(sectionMoved(int,int,int)), this, SLOT(slotColumnOrderChanged(int,int,int))); connect(m_fetcher, SIGNAL(signalCoverChanged(int)), this, SLOT(slotCoverChanged(int))); // Prevent list of selected items from changing while internet search is in // progress. connect(this, SIGNAL(itemSelectionChanged()), m_fetcher, SLOT(abortSearch())); sortByColumn(1, Qt::AscendingOrder); // Should this be itemActivated? It is quite annoying when I try it... connect(this, &QTreeWidget::itemDoubleClicked, this, &Playlist::slotPlayCurrent); // Use a timer to soak up the multiple dataChanged signals we're going to get auto updateRequestor = new QTimer(this); updateRequestor->setSingleShot(true); updateRequestor->setInterval(10); connect(model(), &QAbstractItemModel::dataChanged, updateRequestor, static_cast(&QTimer::start)); connect(updateRequestor, &QTimer::timeout, this, &Playlist::slotUpdateTime); // This apparently must be created very early in initialization for other // Playlist code requiring m_headerMenu. m_columnVisibleAction = new KActionMenu(i18n("&Show Columns"), this); ActionCollection::actions()->addAction("showColumns", m_columnVisibleAction); m_headerMenu = m_columnVisibleAction->menu(); header()->installEventFilter(this); // TODO: Determine if other stuff in setup must happen before slotInitialize(). // Explicitly call slotInitialize() so that the columns are added before // SharedSettings::apply() sets the visible and hidden ones. slotInitialize(); } void Playlist::loadFile(const QString &fileName, const QFileInfo &fileInfo) { QFile file(fileName); if(!file.open(QIODevice::ReadOnly)) return; QTextStream stream(&file); // Turn off non-explicit sorting. setSortingEnabled(false); m_disableColumnWidthUpdates = true; m_blockDataChanged = true; PlaylistItem *after = nullptr; while(!stream.atEnd()) { QString itemName = stream.readLine().trimmed(); QFileInfo item(itemName); if(item.isRelative()) item.setFile(QDir::cleanPath(fileInfo.absolutePath() + '/' + itemName)); if(item.exists() && item.isFile() && item.isReadable() && MediaFiles::isMediaFile(item.fileName())) { after = createItem(FileHandle(item), after); } } m_blockDataChanged = false; m_disableColumnWidthUpdates = false; file.close(); playlistItemsChanged(); } void Playlist::setPlaying(PlaylistItem *item, bool addToHistory) { if(playingItem() == item) return; if(playingItem()) { if(addToHistory) { if(playingItem()->playlist() == playingItem()->playlist()->m_collection->upcomingPlaylist()) m_history.append(playingItem()->collectionItem()); else m_history.append(playingItem()); } playingItem()->setPlaying(false); } TrackSequenceManager::instance()->setCurrent(item); // TODO is this replaced by MPRIS2? //kapp->dcopClient()->emitDCOPSignal("Player", "trackChanged()", data); if(!item) return; item->setPlaying(true); bool enableBack = !m_history.isEmpty(); action("back")->menu()->setEnabled(enableBack); } bool Playlist::playing() const { return playingItem() && this == playingItem()->playlist(); } int Playlist::leftMostVisibleColumn() const { int i = 0; while(i < PlaylistItem::lastColumn() && isColumnHidden(i)) i++; return i < PlaylistItem::lastColumn() ? i : 0; } PlaylistItemList Playlist::items(QTreeWidgetItemIterator::IteratorFlags flags) { PlaylistItemList list; for(QTreeWidgetItemIterator it(this, flags); *it; ++it) list.append(static_cast(*it)); return list; } void Playlist::calculateColumnWeights() { if(m_disableColumnWidthUpdates) return; const PlaylistItemList l = items(); QVector averageWidth(columnCount()); double itemCount = l.size(); QVector cachedWidth; // Here we're not using a real average, but averaging the squares of the // column widths and then using the square root of that value. This gives // a nice weighting to the longer columns without doing something arbitrary // like adding a fixed amount of padding. foreach(PlaylistItem *item, l) { cachedWidth = item->cachedWidths(); // Extra columns start at 0, but those weights aren't shared with all // items. for(int i = 0; i < columnOffset(); ++i) { averageWidth[i] += std::pow(double(columnWidth(i)), 2.0) / itemCount; } for(int column = columnOffset(); column < columnCount(); ++column) { averageWidth[column] += std::pow(double(cachedWidth[column - columnOffset()]), 2.0) / itemCount; } } if(m_columnWeights.isEmpty()) m_columnWeights.fill(-1, columnCount()); foreach(int column, m_weightDirty) { m_columnWeights[column] = int(std::sqrt(averageWidth[column]) + 0.5); } m_weightDirty.clear(); } void Playlist::addPlaylistFile(const QString &m3uFile) { if (!m_collection->containsPlaylistFile(m3uFile)) { new Playlist(m_collection, QFileInfo(m3uFile)); } } QFuture Playlist::addFilesFromDirectory(const QString &dirPath) { auto loader = new DirectoryLoader(dirPath); connect(loader, &DirectoryLoader::loadedPlaylist, this, [this](const QString &m3uFile) { addPlaylistFile(m3uFile); } ); connect(loader, &DirectoryLoader::loadedFiles, this, [this](const FileHandleList &newFiles) { for(const auto newFile : newFiles) { createItem(newFile); } } ); auto future = QtConcurrent::run(loader, &DirectoryLoader::startLoading); auto loadWatcher = new QFutureWatcher(this); connect(loadWatcher, &QFutureWatcher::finished, this, [=]() { loader->deleteLater(); loadWatcher->deleteLater(); }); return future; } // Returns a future since some codepaths will result in an async operation. QFuture Playlist::addUntypedFile(const QString &file, PlaylistItem *after) { if(hasItem(file) && !m_allowDuplicates) return {}; const QFileInfo fileInfo(file); const QString canonicalPath = fileInfo.canonicalFilePath(); if(fileInfo.isFile() && fileInfo.isReadable() && MediaFiles::isMediaFile(file)) { FileHandle f(fileInfo); f.tag(); createItem(f, after); return {}; } if(MediaFiles::isPlaylistFile(file)) { addPlaylistFile(canonicalPath); return {}; } if(fileInfo.isDir()) { foreach(const QString &directory, m_collection->excludedFolders()) { if(canonicalPath.startsWith(directory)) return {}; // Exclude it } return addFilesFromDirectory(canonicalPath); } return {}; } // Called directly or after a threaded directory load has completed, managed by // m_itemsLoading void Playlist::cleanupAfterAllFileLoadsCompleted() { m_blockDataChanged = false; setEnabled(true); // Even if doing a manual column weights we'll generally start off with // incorrect column sizes so at least figure out a reasonable column size // and let user adjust from there. if(manualResize()) { auto manualResizeAction = action("resizeColumnsManually"); manualResizeAction->toggle(); calculateColumnWeights(); slotUpdateColumnWidths(); manualResizeAction->toggle(); } playlistItemsChanged(); } //////////////////////////////////////////////////////////////////////////////// // private slots //////////////////////////////////////////////////////////////////////////////// void Playlist::slotUpdateColumnWidths() { if(m_disableColumnWidthUpdates || manualResize()) return; // Make sure that the column weights have been initialized before trying to // update the columns. QList visibleColumns; for(int i = 0; i < columnCount(); i++) { if(!isColumnHidden(i)) visibleColumns.append(i); } // convenience handler for deprecated text metrics const auto textWidth = [](const QFontMetrics &fm, const QString &text) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) return fm.horizontalAdvance(text); #else return fm.width(text); #endif }; if(count() == 0) { foreach(int column, visibleColumns) setColumnWidth(column, textWidth(header()->fontMetrics(),headerItem()->text(column)) + 10); return; } if(m_columnWeights.isEmpty()) return; // First build a list of minimum widths based on the strings in the listview // header. We won't let the width of the column go below this width. QVector minimumWidth(columnCount(), 0); int minimumWidthTotal = 0; // Also build a list of either the minimum *or* the fixed width -- whichever is // greater. QVector minimumFixedWidth(columnCount(), 0); int minimumFixedWidthTotal = 0; foreach(int column, visibleColumns) { minimumWidth[column] = textWidth(header()->fontMetrics(), headerItem()->text(column)) + 10; minimumWidthTotal += minimumWidth[column]; minimumFixedWidth[column] = qMax(minimumWidth[column], m_columnFixedWidths[column]); minimumFixedWidthTotal += minimumFixedWidth[column]; } // Make sure that the width won't get any smaller than this. We have to // account for the scrollbar as well. Since this method is called from the // resize event this will set a pretty hard toLower bound on the size. setMinimumWidth(minimumWidthTotal + verticalScrollBar()->width()); // If we've got enough room for the fixed widths (larger than the minimum // widths) then instead use those for our "minimum widths". if(minimumFixedWidthTotal < viewport()->width()) { minimumWidth = minimumFixedWidth; minimumWidthTotal = minimumFixedWidthTotal; } // We've got a list of columns "weights" based on some statistics gathered // about the widths of the items in that column. We need to find the total // useful weight to use as a divisor for each column's weight. double totalWeight = 0; foreach(int column, visibleColumns) totalWeight += m_columnWeights[column]; // Computed a "weighted width" for each visible column. This would be the // width if we didn't have to handle the cases of minimum and maximum widths. QVector weightedWidth(columnCount(), 0); foreach(int column, visibleColumns) weightedWidth[column] = int(double(m_columnWeights[column]) / totalWeight * viewport()->width() + 0.5); // The "extra" width for each column. This is the weighted width less the // minimum width or zero if the minimum width is greater than the weighted // width. QVector extraWidth(columnCount(), 0); // This is used as an indicator if we have any columns where the weighted // width is less than the minimum width. If this is false then we can // just use the weighted width with no problems, otherwise we have to // "readjust" the widths. bool readjust = false; // If we have columns where the weighted width is less than the minimum width // we need to steal that space from somewhere. The amount that we need to // steal is the "neededWidth". int neededWidth = 0; // While we're on the topic of stealing -- we have to have somewhere to steal // from. availableWidth is the sum of the amount of space beyond the minimum // width that each column has been allocated -- the sum of the values of // extraWidth[]. int availableWidth = 0; // Fill in the values discussed above. foreach(int column, visibleColumns) { if(weightedWidth[column] < minimumWidth[column]) { readjust = true; extraWidth[column] = 0; neededWidth += minimumWidth[column] - weightedWidth[column]; } else { extraWidth[column] = weightedWidth[column] - minimumWidth[column]; availableWidth += extraWidth[column]; } } // The adjustmentRatio is the amount of the "extraWidth[]" that columns will // actually be given. double adjustmentRatio = (double(availableWidth) - double(neededWidth)) / double(availableWidth); // This will be the sum of the total space that we actually use. Because of // rounding error this won't be the exact available width. int usedWidth = 0; // Now set the actual column widths. If the weighted widths are all greater // than the minimum widths, just use those, otherwise use the "readjusted // weighted width". foreach(int column, visibleColumns) { int width; if(readjust) { int adjustedExtraWidth = int(double(extraWidth[column]) * adjustmentRatio + 0.5); width = minimumWidth[column] + adjustedExtraWidth; } else width = weightedWidth[column]; setColumnWidth(column, width); usedWidth += width; } // Fill the remaining gap for a clean fit into the available space. int remainingWidth = viewport()->width() - usedWidth; setColumnWidth(visibleColumns.back(), columnWidth(visibleColumns.back()) + remainingWidth); m_widthsDirty = false; } void Playlist::slotAddToUpcoming() { m_collection->setUpcomingPlaylistEnabled(true); m_collection->upcomingPlaylist()->appendItems(selectedItems()); } void Playlist::slotShowRMBMenu(const QPoint &point) { QTreeWidgetItem *item = itemAt(point); int column = columnAt(point.x()); if(!item) return; // Create the RMB menu on demand. if(!m_rmbMenu) { // Probably more of these actions should be ported over to using KActions. m_rmbMenu = new QMenu(this); m_rmbMenu->addAction(QIcon::fromTheme("go-jump-today"), i18n("Add to Play Queue"), this, SLOT(slotAddToUpcoming())); m_rmbMenu->addSeparator(); if(!readOnly()) { m_rmbMenu->addAction( action("edit_cut") ); m_rmbMenu->addAction( action("edit_copy") ); m_rmbMenu->addAction( action("edit_paste") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( action("removeFromPlaylist") ); } else m_rmbMenu->addAction( action("edit_copy") ); m_rmbEdit = m_rmbMenu->addAction(i18n("Edit")); m_rmbMenu->addAction( action("refresh") ); m_rmbMenu->addAction( action("removeItem") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( action("guessTag") ); m_rmbMenu->addAction( action("renameFile") ); m_rmbMenu->addAction( action("coverManager") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( QIcon::fromTheme("folder-new"), i18n("Create Playlist From Selected Items..."), this, SLOT(slotCreateGroup())); } // Ignore any columns added by subclasses. const int adjColumn = column - columnOffset(); bool showEdit = (adjColumn == PlaylistItem::TrackColumn) || (adjColumn == PlaylistItem::ArtistColumn) || (adjColumn == PlaylistItem::AlbumColumn) || (adjColumn == PlaylistItem::TrackNumberColumn) || (adjColumn == PlaylistItem::GenreColumn) || (adjColumn == PlaylistItem::YearColumn); if(showEdit) { m_rmbEdit->setText(i18n("Edit '%1'", item->text(column))); m_rmbEdit->disconnect(this); connect(m_rmbEdit, &QAction::triggered, this, [this, item, column]() { this->editItem(item, column); }); } m_rmbEdit->setVisible(showEdit); // Disable edit menu if only one file is selected, and it's read-only FileHandle file = static_cast(item)->file(); m_rmbEdit->setEnabled(file.fileInfo().isWritable() || selectedItems().count() > 1); // View cover is based on if there is a cover to see. We should only have // the remove cover option if the cover is in our database (and not directly // embedded in the file, for instance). action("viewCover")->setEnabled(file.coverInfo()->hasCover()); action("removeCover")->setEnabled(file.coverInfo()->coverId() != CoverManager::NoMatch); m_rmbMenu->popup(mapToGlobal(point)); } bool Playlist::editTag(PlaylistItem *item, const QString &text, int column) { Tag *newTag = TagTransactionManager::duplicateTag(item->file().tag()); switch(column - columnOffset()) { case PlaylistItem::TrackColumn: newTag->setTitle(text); break; case PlaylistItem::ArtistColumn: newTag->setArtist(text); break; case PlaylistItem::AlbumColumn: newTag->setAlbum(text); break; case PlaylistItem::TrackNumberColumn: { bool ok; int value = text.toInt(&ok); if(ok) newTag->setTrack(value); break; } case PlaylistItem::GenreColumn: newTag->setGenre(text); break; case PlaylistItem::YearColumn: { bool ok; int value = text.toInt(&ok); if(ok) newTag->setYear(value); break; } } TagTransactionManager::instance()->changeTagOnItem(item, newTag); return true; } void Playlist::slotInlineEditDone(QTreeWidgetItem *item, int column) { // The column we get is as passed from QTreeWidget so it does not need // adjustment to get the right text from the QTreeWidgetItem QString text = item->text(column); const PlaylistItemList l = selectedItems(); // See if any of the files have a tag different from the input. const int adjColumn = column - columnOffset(); bool changed = std::any_of(l.cbegin(), l.cend(), [text, adjColumn] (const PlaylistItem *item) { return item->text(adjColumn) != text; } ); if(!changed || (l.count() > 1 && KMessageBox::warningContinueCancel( 0, i18n("This will edit multiple files. Are you sure?"), QString(), KGuiItem(i18n("Edit")), KStandardGuiItem::cancel(), "DontWarnMultipleTags") == KMessageBox::Cancel)) { return; } for(auto &item : l) { editTag(item, text, column); } TagTransactionManager::instance()->commit(); CollectionList::instance()->playlistItemsChanged(); playlistItemsChanged(); } void Playlist::slotColumnOrderChanged(int, int from, int to) { if(from == 0 || to == 0) { updatePlaying(); m_leftColumn = header()->sectionPosition(0); } SharedSettings::instance()->setColumnOrder(this); } void Playlist::slotToggleColumnVisible(QAction *action) { int column = action->data().toInt(); if(isColumnHidden(column)) { int fileNameColumn = PlaylistItem::FileNameColumn + columnOffset(); int fullPathColumn = PlaylistItem::FullPathColumn + columnOffset(); if(column == fileNameColumn && !isColumnHidden(fullPathColumn)) { hideColumn(fullPathColumn, false); SharedSettings::instance()->toggleColumnVisible(fullPathColumn); } if(column == fullPathColumn && !isColumnHidden(fileNameColumn)) { hideColumn(fileNameColumn, false); SharedSettings::instance()->toggleColumnVisible(fileNameColumn); } } if(!isColumnHidden(column)) hideColumn(column); else showColumn(column); if(column >= columnOffset()) { SharedSettings::instance()->toggleColumnVisible(column - columnOffset()); } } void Playlist::slotCreateGroup() { QString name = m_collection->playlistNameDialog(i18n("Create New Playlist")); if(!name.isEmpty()) new Playlist(m_collection, selectedItems(), name); } void Playlist::notifyUserColumnWidthModeChanged() { KMessageBox::information(this, i18n("Manual column widths have been enabled. You can " "switch back to automatic column sizes in the view " "menu."), i18n("Manual Column Widths Enabled"), "ShowManualColumnWidthInformation"); } void Playlist::columnResized(int column, int, int newSize) { m_widthsDirty = true; m_columnFixedWidths[column] = newSize; } void Playlist::slotInlineCompletionModeChanged(KCompletion::CompletionMode mode) { SharedSettings::instance()->setInlineCompletionMode(mode); } void Playlist::slotPlayCurrent() { QTreeWidgetItemIterator it(this, QTreeWidgetItemIterator::Selected); PlaylistItem *next = static_cast(*it); TrackSequenceManager::instance()->setNextItem(next); action("forward")->trigger(); } void Playlist::slotUpdateTime() { int newTime = 0; QTreeWidgetItemIterator it(this); while(*it) { const auto item = static_cast(*it); ++it; newTime += item->file().tag()->seconds(); } m_time = newTime; } //////////////////////////////////////////////////////////////////////////////// // helper functions //////////////////////////////////////////////////////////////////////////////// QDataStream &operator<<(QDataStream &s, const Playlist &p) { s << p.name(); s << p.fileName(); s << p.files(); return s; } QDataStream &operator>>(QDataStream &s, Playlist &p) { p.read(s); return s; } bool processEvents() { static QTime time = QTime::currentTime(); if(time.elapsed() > 100) { time.restart(); qApp->processEvents(); return true; } return false; } // vim: set et sw=4 tw=0 sta: diff --git a/playlistitem.cpp b/playlistitem.cpp index 627c6e31..80e3f1c3 100644 --- a/playlistitem.cpp +++ b/playlistitem.cpp @@ -1,440 +1,440 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * * 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 "playlistitem.h" #include #include #include #include #include #include #include #include "collectionlist.h" -#include "tag.h" +#include "juktag.h" #include "coverinfo.h" #include "covermanager.h" #include "tagtransactionmanager.h" #include "juk_debug.h" PlaylistItemList PlaylistItem::m_playingItems; // static static int naturalCompare(const QString &first, const QString &second) { static QCollator collator; collator.setNumericMode(true); collator.setCaseSensitivity(Qt::CaseInsensitive); return collator.compare(first, second); } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem public methods //////////////////////////////////////////////////////////////////////////////// PlaylistItem::~PlaylistItem() { // Although this isn't the most efficient way to accomplish the task of // stopping playback when deleting the item being played, it has the // stark advantage of working reliably. I'll tell anyone who tries to // optimize this, the timing issues can be *hard*. -- mpyne m_collectionItem->removeChildItem(this); if(m_playingItems.contains(this)) { m_playingItems.removeAll(this); if(m_playingItems.isEmpty()) playlist()->setPlaying(0); } playlist()->updateDeletedItem(this); emit playlist()->signalAboutToRemove(this); if(m_watched) Pointer::clear(this); } void PlaylistItem::setFile(const FileHandle &file) { m_collectionItem->updateCollectionDict(d->fileHandle.absFilePath(), file.absFilePath()); d->fileHandle = file; refresh(); } void PlaylistItem::setFile(const QString &file) { QString oldPath = d->fileHandle.absFilePath(); d->fileHandle.setFile(file); m_collectionItem->updateCollectionDict(oldPath, d->fileHandle.absFilePath()); refresh(); } FileHandle PlaylistItem::file() const { return d->fileHandle; } QString PlaylistItem::text(int column) const { if(!d->fileHandle.tag()) return QString(); int offset = playlist()->columnOffset(); switch(column - offset) { case TrackColumn: return d->fileHandle.tag()->title(); case ArtistColumn: return d->fileHandle.tag()->artist(); case AlbumColumn: return d->fileHandle.tag()->album(); case CoverColumn: return QString(); case TrackNumberColumn: return d->fileHandle.tag()->track() > 0 ? QString::number(d->fileHandle.tag()->track()) : QString(); case GenreColumn: return d->fileHandle.tag()->genre(); case YearColumn: return d->fileHandle.tag()->year() > 0 ? QString::number(d->fileHandle.tag()->year()) : QString(); case LengthColumn: return d->fileHandle.tag()->lengthString(); case BitrateColumn: return QString::number(d->fileHandle.tag()->bitrate()); case CommentColumn: return d->fileHandle.tag()->comment(); case FileNameColumn: return d->fileHandle.fileInfo().fileName(); case FullPathColumn: return d->fileHandle.fileInfo().absoluteFilePath(); default: return QTreeWidgetItem::text(column); } } void PlaylistItem::setText(int column, const QString &text) { QTreeWidgetItem::setText(column, text); playlist()->slotWeightDirty(column); } void PlaylistItem::setPlaying(bool playing, bool master) { m_playingItems.removeAll(this); if(playing) { if(master) m_playingItems.prepend(this); else m_playingItems.append(this); } else { // This is a tricky little recursion, but it // in fact does clear the list. if(!m_playingItems.isEmpty()) m_playingItems.front()->setPlaying(false); } treeWidget()->viewport()->update(); } void PlaylistItem::guessTagInfo(TagGuesser::Type type) { switch(type) { case TagGuesser::FileName: { TagGuesser guesser(d->fileHandle.absFilePath()); Tag *tag = TagTransactionManager::duplicateTag(d->fileHandle.tag()); if(!guesser.title().isNull()) tag->setTitle(guesser.title()); if(!guesser.artist().isNull()) tag->setArtist(guesser.artist()); if(!guesser.album().isNull()) tag->setAlbum(guesser.album()); if(!guesser.track().isNull()) tag->setTrack(guesser.track().toInt()); if(!guesser.comment().isNull()) tag->setComment(guesser.comment()); TagTransactionManager::instance()->changeTagOnItem(this, tag); break; } case TagGuesser::MusicBrainz: qCDebug(JUK_LOG) << "Ignoring MusicBrainz query request until support is reimplemented."; break; } } Playlist *PlaylistItem::playlist() const { return static_cast(treeWidget()); } QVector PlaylistItem::cachedWidths() const { return d->cachedWidths; } void PlaylistItem::refresh() { m_collectionItem->refresh(); } void PlaylistItem::refreshFromDisk() { d->fileHandle.refresh(); refresh(); } void PlaylistItem::clear() { playlist()->clearItem(this); } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem protected methods //////////////////////////////////////////////////////////////////////////////// PlaylistItem::PlaylistItem(CollectionListItem *item, Playlist *parent) : QTreeWidgetItem(parent), d(0), m_watched(0) { setup(item); } PlaylistItem::PlaylistItem(CollectionListItem *item, Playlist *parent, QTreeWidgetItem *after) : QTreeWidgetItem(parent, after), d(0), m_watched(0) { setup(item); } // This constructor should only be used by the CollectionList subclass. PlaylistItem::PlaylistItem(CollectionList *parent) : QTreeWidgetItem(parent), m_watched(0) { d = new Data; m_collectionItem = static_cast(this); setFlags(flags() | Qt::ItemIsEditable | Qt::ItemIsDragEnabled); } int PlaylistItem::compare(const QTreeWidgetItem *item, int column, bool ascending) const { // reimplemented from QListViewItem int offset = playlist()->columnOffset(); if(!item) return 0; const PlaylistItem *playlistItem = static_cast(item); // The following statments first check to see if you can sort based on the // specified column. If the values for the two PlaylistItems are the same // in that column it then tries to sort based on columns 1, 2, 3 and 0, // (artist, album, track number, track name) in that order. int c = compare(this, playlistItem, column, ascending); if(c != 0) return c; else { // Loop through the columns doing comparisons until something is differnt. // If all else is the same, compare the track name. int last = !playlist()->isColumnHidden(AlbumColumn + offset) ? TrackNumberColumn : ArtistColumn; for(int i = ArtistColumn; i <= last; i++) { if(!playlist()->isColumnHidden(i + offset)) { c = compare(this, playlistItem, i, ascending); if(c != 0) return c; } } return compare(this, playlistItem, TrackColumn + offset, ascending); } } int PlaylistItem::compare(const PlaylistItem *firstItem, const PlaylistItem *secondItem, int column, bool) const { int offset = playlist()->columnOffset(); if(column < 0 || column > lastColumn() + offset || !firstItem->d || !secondItem->d) return 0; if(column < offset) { QString first = firstItem->text(column); QString second = secondItem->text(column); return naturalCompare(first, second); } switch(column - offset) { case TrackNumberColumn: if(firstItem->d->fileHandle.tag()->track() > secondItem->d->fileHandle.tag()->track()) return 1; else if(firstItem->d->fileHandle.tag()->track() < secondItem->d->fileHandle.tag()->track()) return -1; else return 0; break; case LengthColumn: if(firstItem->d->fileHandle.tag()->seconds() > secondItem->d->fileHandle.tag()->seconds()) return 1; else if(firstItem->d->fileHandle.tag()->seconds() < secondItem->d->fileHandle.tag()->seconds()) return -1; else return 0; break; case BitrateColumn: if(firstItem->d->fileHandle.tag()->bitrate() > secondItem->d->fileHandle.tag()->bitrate()) return 1; else if(firstItem->d->fileHandle.tag()->bitrate() < secondItem->d->fileHandle.tag()->bitrate()) return -1; else return 0; break; case CoverColumn: if(firstItem->d->fileHandle.coverInfo()->coverId() == secondItem->d->fileHandle.coverInfo()->coverId()) return 0; else if (firstItem->d->fileHandle.coverInfo()->coverId() != CoverManager::NoMatch) return -1; else return 1; break; default: return naturalCompare(firstItem->d->metadata[column - offset], secondItem->d->metadata[column - offset]); } } bool PlaylistItem::operator<(const QTreeWidgetItem &other) const { bool ascending = playlist()->header()->sortIndicatorOrder() == Qt::AscendingOrder; return compare(&other, playlist()->sortColumn(), ascending) == -1; } bool PlaylistItem::isValid() const { return bool(d->fileHandle.tag()); } void PlaylistItem::setTrackId(quint32 id) { m_trackId = id; } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem private methods //////////////////////////////////////////////////////////////////////////////// void PlaylistItem::setup(CollectionListItem *item) { m_collectionItem = item; d = item->d; item->addChildItem(this); setFlags(flags() | Qt::ItemIsEditable | Qt::ItemIsDragEnabled); int offset = playlist()->columnOffset(); int columns = lastColumn() + offset + 1; for(int i = offset; i < columns; i++) { setText(i, text(i)); } } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem::Pointer implementation //////////////////////////////////////////////////////////////////////////////// QMap > PlaylistItem::Pointer::m_map; // static PlaylistItem::Pointer::Pointer(PlaylistItem *item) : m_item(item) { if(!m_item) return; m_item->m_watched = true; m_map[m_item].append(this); } PlaylistItem::Pointer::Pointer(const Pointer &p) : m_item(p.m_item) { m_map[m_item].append(this); } PlaylistItem::Pointer::~Pointer() { if(!m_item) return; m_map[m_item].removeAll(this); if(m_map[m_item].isEmpty()) { m_map.remove(m_item); m_item->m_watched = false; } } PlaylistItem::Pointer &PlaylistItem::Pointer::operator=(PlaylistItem *item) { if(item == m_item) return *this; if(m_item) { m_map[m_item].removeAll(this); if(m_map[m_item].isEmpty()) { m_map.remove(m_item); m_item->m_watched = false; } } if(item) { m_map[item].append(this); item->m_watched = true; } m_item = item; return *this; } void PlaylistItem::Pointer::clear(PlaylistItem *item) // static { if(!item) return; QVector l = m_map[item]; foreach(Pointer *pointer, l) pointer->m_item = 0; m_map.remove(item); item->m_watched = false; } // vim: set et sw=4 tw=0 sta: diff --git a/scrobbler.cpp b/scrobbler.cpp index d0c2fce2..2c206d50 100644 --- a/scrobbler.cpp +++ b/scrobbler.cpp @@ -1,324 +1,324 @@ /** * Copyright (C) 2012 Martin Sandsmark * Copyright (C) 2014 Arnold Dumas * * 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 "scrobbler.h" #include #include #include #include #include #include #include #include #include #include #include -#include "tag.h" +#include "juktag.h" #include "juk.h" #include "juk_debug.h" Scrobbler::Scrobbler(QObject* parent) : QObject(parent) , m_networkAccessManager(new QNetworkAccessManager(this)) , m_wallet(Scrobbler::openKWallet()) { QByteArray sessionKey; if (m_wallet) { m_wallet->readEntry("SessionKey", sessionKey); } else { KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling"); sessionKey.append(config.readEntry("SessionKey", "").toLatin1()); } if(sessionKey.isEmpty()) getAuthToken(); } bool Scrobbler::isScrobblingEnabled() // static { QString username, password; // checks without prompting to open the wallet if (Wallet::folderDoesNotExist(Wallet::LocalWallet(), "JuK")) { KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling"); username = config.readEntry("Username", ""); password = config.readEntry("Password", ""); } else { auto wallet = Scrobbler::openKWallet(); if (wallet) { QMap scrobblingCredentials; wallet->readMap("Scrobbling", scrobblingCredentials); if (scrobblingCredentials.contains("Username") && scrobblingCredentials.contains("Password")) { username = scrobblingCredentials["Username"]; password = scrobblingCredentials["Password"]; } } } return (!username.isEmpty() && !password.isEmpty()); } std::unique_ptr Scrobbler::openKWallet() // static { using KWallet::Wallet; const QString walletFolderName(QStringLiteral("JuK")); const auto walletName = Wallet::LocalWallet(); // checks without prompting to open the wallet if (Wallet::folderDoesNotExist(walletName, walletFolderName)) { return nullptr; } std::unique_ptr wallet( Wallet::openWallet(walletName, JuK::JuKInstance()->winId())); if(!wallet || (!wallet->hasFolder(walletFolderName) && !wallet->createFolder(walletFolderName)) || !wallet->setFolder(walletFolderName)) { return nullptr; } return wallet; } QByteArray Scrobbler::md5(QByteArray data) { return QCryptographicHash::hash(data, QCryptographicHash::Md5) .toHex().rightJustified(32, '0').toLower(); } void Scrobbler::sign(QMap< QString, QString >& params) { params["api_key"] = "3e6ecbd7284883089e8f2b5b53b0aecd"; QString s; QMapIterator i(params); while(i.hasNext()) { i.next(); s += i.key() + i.value(); } s += "2cab3957b1f70d485e9815ac1ac94096"; //shared secret params["api_sig"] = md5(s.toUtf8()); } void Scrobbler::getAuthToken(QString username, QString password) { qCDebug(JUK_LOG) << "Getting new auth token for user:" << username; QByteArray authToken = md5((username + md5(password.toUtf8())).toUtf8()); QMap params; params["method"] = "auth.getMobileSession"; params["authToken"] = authToken; params["username"] = username; QUrl url("http://ws.audioscrobbler.com/2.0/?"); sign(params); QUrlQuery urlQuery; foreach(QString key, params.keys()) { urlQuery.addQueryItem(key, params[key]); } url.setQuery(urlQuery); QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); connect(reply, SIGNAL(finished()), this, SLOT(handleAuthenticationReply())); } void Scrobbler::getAuthToken() { QString username, password; if (m_wallet) { QMap scrobblingCredentials; m_wallet->readMap("Scrobbling", scrobblingCredentials); if (scrobblingCredentials.contains("Username") && scrobblingCredentials.contains("Password")) { username = scrobblingCredentials["Username"]; password = scrobblingCredentials["Password"]; } } else { KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling"); username = config.readEntry("Username", ""); password = config.readEntry("Password", ""); } if(username.isEmpty() || password.isEmpty()) return; getAuthToken(username, password); } void Scrobbler::handleAuthenticationReply() { QNetworkReply* reply = qobject_cast(sender()); qCDebug(JUK_LOG) << "got authentication reply"; if(reply->error() != QNetworkReply::NoError) { emit invalidAuth(); qCWarning(JUK_LOG) << "Error while getting authentication reply" << reply->errorString(); return; } QDomDocument doc; QByteArray data = reply->readAll(); doc.setContent(data); QString sessionKey = doc.documentElement() .firstChildElement("session") .firstChildElement("key").text(); if(sessionKey.isEmpty()) { emit invalidAuth(); qCWarning(JUK_LOG) << "Unable to get session key" << data; return; } if (m_wallet) { m_wallet->writeEntry("SessionKey", sessionKey.toUtf8()); } else { KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling"); config.writeEntry("SessionKey", sessionKey); } emit validAuth(); } void Scrobbler::nowPlaying(const FileHandle& file) { QString sessionKey; if (m_wallet) { QByteArray sessionKeyByteArray; m_wallet->readEntry("SessionKey", sessionKeyByteArray); sessionKey = QString::fromLatin1(sessionKeyByteArray); } else { KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling"); sessionKey = config.readEntry("SessionKey", ""); } if (!m_file.isNull()) { scrobble(); // Update time-played info for last track } QMap params; params["method"] = "track.updateNowPlaying"; params["sk"] = sessionKey; params["track"] = file.tag()->title(); params["artist"] = file.tag()->artist(); params["album"] = file.tag()->album(); params["trackNumber"] = QString::number(file.tag()->track()); params["duration"] = QString::number(file.tag()->seconds()); sign(params); post(params); m_file = file; // May be empty FileHandle m_playbackTimer = QDateTime::currentDateTime(); } void Scrobbler::scrobble() { QString sessionKey; if (m_wallet) { QByteArray sessionKeyByteArray; m_wallet->readEntry("SessionKey", sessionKeyByteArray); sessionKey = QString::fromLatin1(sessionKeyByteArray); } else { KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling"); sessionKey = config.readEntry("SessionKey", ""); } if(sessionKey.isEmpty()) { getAuthToken(); return; } int halfDuration = m_file.tag()->seconds() / 2; int timeElapsed = m_playbackTimer.secsTo(QDateTime::currentDateTime()); if (timeElapsed < 30 || timeElapsed < halfDuration) { return; // API says not to scrobble if the user didn't play long enough } qCDebug(JUK_LOG) << "Scrobbling" << m_file.tag()->title(); QMap params; params["method"] = "track.scrobble"; params["sk"] = sessionKey; params["track"] = m_file.tag()->title(); params["artist"] = m_file.tag()->artist(); params["album"] = m_file.tag()->album(); params["timestamp"] = QString::number(m_playbackTimer.toTime_t()); params["trackNumber"] = QString::number(m_file.tag()->track()); params["duration"] = QString::number(m_file.tag()->seconds()); sign(params); post(params); } void Scrobbler::post(QMap ¶ms) { QUrl url("http://ws.audioscrobbler.com/2.0/"); QByteArray data; foreach(QString key, params.keys()) { data += QUrl::toPercentEncoding(key) + '=' + QUrl::toPercentEncoding(params[key]) + '&'; } QNetworkRequest req(url); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QNetworkReply *reply = m_networkAccessManager->post(req, data); connect(reply, SIGNAL(finished()), this, SLOT(handleResults())); } void Scrobbler::handleResults() { QNetworkReply* reply = qobject_cast(sender()); QByteArray data = reply->readAll(); if(data.contains("code=\"9\"")) // We need a new token getAuthToken(); } diff --git a/statuslabel.cpp b/statuslabel.cpp index 725765bf..8b47ebb2 100644 --- a/statuslabel.cpp +++ b/statuslabel.cpp @@ -1,158 +1,158 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * * 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 "statuslabel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "filehandle.h" #include "playlistinterface.h" #include "actioncollection.h" #include "playermanager.h" -#include "tag.h" +#include "juktag.h" #include "juk_debug.h" using namespace ActionCollection; //////////////////////////////////////////////////////////////////////////////// // static helpers //////////////////////////////////////////////////////////////////////////////// static QString formatTime(qint64 milliseconds) { static const KFormat fmt; return fmt.formatDuration(milliseconds); } //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// StatusLabel::StatusLabel(const PlaylistInterface ¤tPlaylist, QStatusBar *parent) : QWidget(parent) { m_playlistLabel = new KSqueezedTextLabel(this); m_playlistLabel->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred, QSizePolicy::Label)); m_playlistLabel->setTextFormat(Qt::PlainText); m_playlistLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); parent->addWidget(m_playlistLabel, 1); m_trackLabel = new QLabel(this); m_trackLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); m_trackLabel->setTextFormat(Qt::PlainText); parent->addPermanentWidget(m_trackLabel); m_itemTimeLabel = new QLabel(this); QFontMetrics fontMetrics(font()); m_itemTimeLabel->setAlignment(Qt::AlignCenter); m_itemTimeLabel->setMinimumWidth(fontMetrics.boundingRect("000:00 / 000:00").width()); m_itemTimeLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); m_itemTimeLabel->setFrameStyle(QFrame::Box | QFrame::Sunken); m_itemTimeLabel->installEventFilter(this); parent->addPermanentWidget(m_itemTimeLabel); setItemTotalTime(0); setItemCurrentTime(0); QPushButton *jumpButton = new QPushButton(this); jumpButton->setIcon(QIcon::fromTheme("go-jump")); jumpButton->setFlat(true); jumpButton->setToolTip(i18n("Jump to the currently playing item")); connect(jumpButton, &QPushButton::clicked, action("showPlaying"), &QAction::trigger); parent->addPermanentWidget(jumpButton); installEventFilter(this); slotCurrentPlaylistHasChanged(currentPlaylist); } void StatusLabel::slotPlayingItemHasChanged(const FileHandle &file) { const Tag *tag = file.tag(); const QString mid = (tag->artist().isEmpty() || tag->title().isEmpty()) ? QString() : QStringLiteral(" - "); setItemTotalTime(tag->seconds()); setItemCurrentTime(0); m_trackLabel->setText(tag->artist() + mid + tag->title()); } void StatusLabel::slotCurrentPlaylistHasChanged(const PlaylistInterface ¤tPlaylist) { if(!currentPlaylist.playing()) { return; } m_playlistLabel->setText(currentPlaylist.name()); m_trackLabel->setText( i18np("1 item", "%1 items", currentPlaylist.count()) + QStringLiteral(" - ") + formatTime(qint64(1000) * currentPlaylist.time()) ); } //////////////////////////////////////////////////////////////////////////////// // private methods //////////////////////////////////////////////////////////////////////////////// void StatusLabel::updateTime() { const qint64 milliseconds = m_showTimeRemaining ? m_itemTotalTime - m_itemCurrentTime : m_itemCurrentTime; const QString timeString = formatTime(milliseconds) + QStringLiteral(" / ") + formatTime(m_itemTotalTime); m_itemTimeLabel->setText(timeString); } bool StatusLabel::eventFilter(QObject *o, QEvent *e) { if(!o || !e) return false; QMouseEvent *mouseEvent = static_cast(e); if(e->type() == QEvent::MouseButtonRelease && mouseEvent->button() == Qt::LeftButton) { if(o == m_itemTimeLabel) { m_showTimeRemaining = !m_showTimeRemaining; updateTime(); } else action("showPlaying")->trigger(); return true; } return false; } // vim: set et sw=4 tw=0 sta: diff --git a/systemtray.cpp b/systemtray.cpp index c97b8651..9ccdf104 100644 --- a/systemtray.cpp +++ b/systemtray.cpp @@ -1,528 +1,528 @@ /** * Copyright (C) 2002 Daniel Molkentin * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2004-2009 Michael Pyne * * 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 "systemtray.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include "tag.h" +#include "juktag.h" #include "actioncollection.h" #include "playermanager.h" #include "coverinfo.h" #include "juk_debug.h" using namespace ActionCollection; PassiveInfo::PassiveInfo() : QFrame(nullptr, Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint ), m_timer(new QTimer(this)), m_layout(new QVBoxLayout(this)), m_justDie(false) { connect(m_timer, SIGNAL(timeout()), SLOT(timerExpired())); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); // Workaround transparent background in Oxygen when (ab-)using Qt::ToolTip setAutoFillBackground(true); setFrameStyle(StyledPanel | Plain); setLineWidth(2); } void PassiveInfo::startTimer(int delay) { m_timer->start(delay); } void PassiveInfo::show() { m_timer->start(3500); setWindowOpacity(1.0); QFrame::show(); } void PassiveInfo::setView(QWidget *view) { m_layout->addWidget(view); view->show(); // We are still hidden though. adjustSize(); positionSelf(); } void PassiveInfo::timerExpired() { // If m_justDie is set, we should just go, otherwise we should emit the // signal and wait for the system tray to delete us. if(m_justDie) hide(); else emit timeExpired(); } void PassiveInfo::enterEvent(QEvent *) { m_timer->stop(); emit mouseEntered(); } void PassiveInfo::leaveEvent(QEvent *) { m_justDie = true; m_timer->start(50); } void PassiveInfo::hideEvent(QHideEvent *) { } void PassiveInfo::wheelEvent(QWheelEvent *e) { if(e->delta() >= 0) { emit nextSong(); } else { emit previousSong(); } e->accept(); } void PassiveInfo::positionSelf() { // Start with a QRect of our size, move it to the right spot. QRect r(rect()); QRect curScreen(KWindowSystem::workArea()); // Try to position in lower right of the screen QPoint anchor(curScreen.right() * 7 / 8, curScreen.bottom()); // Now make our rect hit that anchor. r.moveBottomRight(anchor); move(r.topLeft()); } //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// SystemTray::SystemTray(PlayerManager *player, QWidget *parent) : KStatusNotifierItem(parent), m_popup(0), m_player(player), m_fadeTimer(0), m_fade(true), m_hasCompositionManager(false) { using ActionCollection::action; // Override the KSNI::action call introduced in KF5 // This should be initialized to the number of labels that are used. m_labels.fill(0, 3); setIconByName("juk"); setCategory(ApplicationStatus); setStatus(Active); // We were told to dock in systray by user, force us visible m_forwardPix = QIcon::fromTheme("media-skip-forward"); m_backPix = QIcon::fromTheme("media-skip-backward"); // Just create this here so that it show up in the DBus interface and the // key bindings dialog. QAction *rpaction = new QAction(i18n("Redisplay Popup"), this); ActionCollection::actions()->addAction("showPopup", rpaction); connect(rpaction, SIGNAL(triggered(bool)), SLOT(slotPlay())); QMenu *cm = contextMenu(); connect(m_player, SIGNAL(signalPlay()), this, SLOT(slotPlay())); connect(m_player, SIGNAL(signalPause()), this, SLOT(slotPause())); connect(m_player, SIGNAL(signalStop()), this, SLOT(slotStop())); cm->addAction( action("play") ); cm->addAction( action("pause") ); cm->addAction( action("stop") ); cm->addAction( action("forward") ); cm->addAction( action("back") ); cm->addSeparator(); // Pity the actionCollection doesn't keep track of what sub-menus it has. KActionMenu *menu = new KActionMenu(i18n("&Random Play"), this); // FIXME //actionCollection()->addAction("randomplay", menu); menu->addAction(action("disableRandomPlay")); menu->addAction(action("randomPlay")); menu->addAction(action("albumRandomPlay")); cm->addAction( menu ); cm->addAction( action("togglePopups") ); m_fadeTimer = new QTimer(this); m_fadeTimer->setObjectName( QLatin1String("systrayFadeTimer" )); connect(m_fadeTimer, SIGNAL(timeout()), SLOT(slotNextStep())); // Handle wheel events connect(this, SIGNAL(scrollRequested(int,Qt::Orientation)), SLOT(scrollEvent(int,Qt::Orientation))); // Add a quick hook for play/pause toggle connect(this, SIGNAL(secondaryActivateRequested(QPoint)), action("playPause"), SLOT(trigger())); if(m_player->playing()) slotPlay(); else if(m_player->paused()) slotPause(); } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void SystemTray::slotPlay() { if(!m_player->playing()) return; QPixmap cover = m_player->playingFile().coverInfo()->pixmap(CoverInfo::FullSize); setOverlayIconByName("media-playback-start"); setToolTip(m_player->playingString(), cover); createPopup(); } void SystemTray::slotPause() { setOverlayIconByName("media-playback-pause"); } void SystemTray::slotPopupLargeCover() { if(!m_player->playing()) return; FileHandle playingFile = m_player->playingFile(); playingFile.coverInfo()->popup(); } void SystemTray::slotStop() { setToolTip(); setOverlayIconByName(QString()); delete m_popup; m_popup = 0; m_fadeTimer->stop(); } void SystemTray::slotPopupDestroyed() { for(int i = 0; i < m_labels.size(); ++i) m_labels[i] = 0; } void SystemTray::slotNextStep() { // Could happen I guess if the timeout event were queued while we're deleting m_popup if(!m_popup) return; ++m_step; // If we're not fading, immediately stop the fadeout if(!m_fade || m_step == STEPS) { m_step = 0; m_fadeTimer->stop(); emit fadeDone(); return; } if(m_hasCompositionManager) { m_popup->setWindowOpacity((1.0 * STEPS - m_step) / STEPS); } else { QColor result = interpolateColor(m_step); for(int i = 0; i < m_labels.size() && m_labels[i]; ++i) { QPalette palette; palette.setColor(m_labels[i]->foregroundRole(), result); m_labels[i]->setPalette(palette); } } } void SystemTray::slotFadeOut() { m_startColor = m_labels[0]->palette().color( QPalette::Text ); //textColor(); m_endColor = m_labels[0]->palette().color( QPalette::Window ); //backgroundColor(); m_hasCompositionManager = KWindowSystem::compositingActive(); connect(this, SIGNAL(fadeDone()), m_popup, SLOT(hide())); connect(m_popup, SIGNAL(mouseEntered()), this, SLOT(slotMouseInPopup())); m_fadeTimer->start(1500 / STEPS); } // If we receive this signal, it's because we were called during fade out. // That means there is a single shot timer about to call slotNextStep, so we // don't have to do it ourselves. void SystemTray::slotMouseInPopup() { m_endColor = m_labels[0]->palette().color( QPalette::Text ); //textColor(); disconnect(SIGNAL(fadeDone())); if(m_hasCompositionManager) m_popup->setWindowOpacity(1.0); m_step = STEPS - 1; // Simulate end of fade to solid text slotNextStep(); } //////////////////////////////////////////////////////////////////////////////// // private methods //////////////////////////////////////////////////////////////////////////////// QWidget *SystemTray::createInfoBox(QBoxLayout *parentLayout, const FileHandle &file) { // We always show the popup on the right side of the current screen, so // this logic assumes that. Earlier revisions had logic for popup being // wherever the systray icon is, so if it's decided to go that route again, // dig into the source control history. --mpyne if(file.coverInfo()->hasCover()) { addCoverButton(parentLayout, file.coverInfo()->pixmap(CoverInfo::Thumbnail)); addSeparatorLine(parentLayout); } auto infoBox = new QWidget; auto infoBoxVLayout = new QVBoxLayout(infoBox); infoBoxVLayout->setSpacing(3); infoBoxVLayout->setMargin(3); parentLayout->addWidget(infoBox); addSeparatorLine(parentLayout); createButtonBox(parentLayout); return infoBox; } void SystemTray::createPopup() { FileHandle playingFile = m_player->playingFile(); Tag *playingInfo = playingFile.tag(); // If the action exists and it's checked, do our stuff if(!ActionCollection::action("togglePopups")->isChecked()) return; delete m_popup; m_popup = 0; m_fadeTimer->stop(); // This will be reset after this function call by slot(Forward|Back) // so it's safe to set it true here. m_fade = true; m_step = 0; m_popup = new PassiveInfo; connect(m_popup, SIGNAL(destroyed()), SLOT(slotPopupDestroyed())); connect(m_popup, SIGNAL(timeExpired()), SLOT(slotFadeOut())); connect(m_popup, SIGNAL(nextSong()), SLOT(slotForward())); connect(m_popup, SIGNAL(previousSong()), SLOT(slotBack())); auto box = new QWidget; auto boxHLayout = new QHBoxLayout(box); boxHLayout->setSpacing(15); // Add space between text and buttons QWidget *infoBox = createInfoBox(boxHLayout, playingFile); QLayout *infoBoxLayout = infoBox->layout(); for(int i = 0; i < m_labels.size(); ++i) { QLabel *l = new QLabel(" "); l->setAlignment(Qt::AlignRight | Qt::AlignVCenter); m_labels[i] = l; infoBoxLayout->addWidget(l); } // We have to set the text of the labels after all of the // widgets have been added in order for the width to be calculated // correctly. int labelCount = 0; QString title = playingInfo->title().toHtmlEscaped(); m_labels[labelCount++]->setText(QString("

%1

").arg(title)); if(!playingInfo->artist().isEmpty()) m_labels[labelCount++]->setText(playingInfo->artist()); if(!playingInfo->album().isEmpty()) { QString album = playingInfo->album().toHtmlEscaped(); QString s = playingInfo->year() > 0 ? QString("%1 (%2)").arg(album).arg(playingInfo->year()) : QString("%1").arg(album); m_labels[labelCount++]->setText(s); } m_popup->setView(box); m_popup->show(); } void SystemTray::createButtonBox(QBoxLayout *parentLayout) { auto buttonBox = new QWidget; auto buttonBoxVLayout = new QVBoxLayout(buttonBox); buttonBoxVLayout->setSpacing(3); QPushButton *forwardButton = new QPushButton(m_forwardPix, QString()); forwardButton->setObjectName(QLatin1String("popup_forward")); connect(forwardButton, SIGNAL(clicked()), SLOT(slotForward())); QPushButton *backButton = new QPushButton(m_backPix, QString()); backButton->setObjectName(QLatin1String("popup_back")); connect(backButton, SIGNAL(clicked()), SLOT(slotBack())); buttonBoxVLayout->addWidget(forwardButton); buttonBoxVLayout->addWidget(backButton); parentLayout->addWidget(buttonBox); } /** * What happens here is that the action->trigger() call will end up invoking * createPopup(), which sets m_fade to true. Before the text starts fading * control returns to this function, which resets m_fade to false. */ void SystemTray::slotBack() { ActionCollection::action("back")->trigger(); m_fade = false; } void SystemTray::slotForward() { ActionCollection::action("forward")->trigger(); m_fade = false; } void SystemTray::addSeparatorLine(QBoxLayout *parentLayout) { QFrame *line = new QFrame; line->setFrameShape(QFrame::VLine); // Cover art takes up 80 pixels, make sure we take up at least 80 pixels // even if we don't show the cover art for consistency. line->setMinimumHeight(80); parentLayout->addWidget(line); } void SystemTray::addCoverButton(QBoxLayout *parentLayout, const QPixmap &cover) { QPushButton *coverButton = new QPushButton; coverButton->setIconSize(cover.size()); coverButton->setIcon(cover); coverButton->setFixedSize(cover.size()); coverButton->setFlat(true); connect(coverButton, SIGNAL(clicked()), this, SLOT(slotPopupLargeCover())); parentLayout->addWidget(coverButton); } QColor SystemTray::interpolateColor(int step, int steps) { if(step < 0) return m_startColor; if(step >= steps) return m_endColor; // TODO: Perhaps the algorithm here could be better? For example, it might // make sense to go rather quickly from start to end and then slow down // the progression. return QColor( (step * m_endColor.red() + (steps - step) * m_startColor.red()) / steps, (step * m_endColor.green() + (steps - step) * m_startColor.green()) / steps, (step * m_endColor.blue() + (steps - step) * m_startColor.blue()) / steps ); } void SystemTray::setToolTip(const QString &tip, const QPixmap &cover) { if(tip.isEmpty()) KStatusNotifierItem::setToolTip("juk", i18n("JuK"), QString()); else { QIcon myCover; if(cover.isNull()) { myCover = QIcon::fromTheme("juk"); } else { //Scale to proper icon size, otherwise KStatusNotifierItem will show an unknown icon const int iconSize = KIconLoader::global()->currentSize(KIconLoader::Desktop); myCover = QIcon(cover.scaled(iconSize, iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } KStatusNotifierItem::setToolTip(myCover, i18n("JuK"), tip); } } void SystemTray::scrollEvent(int delta, Qt::Orientation orientation) { if(orientation == Qt::Horizontal) return; switch(QApplication::queryKeyboardModifiers()) { case Qt::ShiftModifier: if(delta > 0) ActionCollection::action("volumeUp")->trigger(); else ActionCollection::action("volumeDown")->trigger(); break; default: if(delta > 0) ActionCollection::action("forward")->trigger(); else ActionCollection::action("back")->trigger(); break; } } // vim: set et sw=4 tw=0 sta: diff --git a/tageditor.cpp b/tageditor.cpp index daec5575..c762b72a 100644 --- a/tageditor.cpp +++ b/tageditor.cpp @@ -1,658 +1,658 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * * 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 "tageditor.h" #include "collectionlist.h" #include "playlistitem.h" -#include "tag.h" +#include "juktag.h" #include "actioncollection.h" #include "tagtransactionmanager.h" #include "juk_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #undef KeyRelease class FileNameValidator final : public QValidator { public: FileNameValidator(QObject *parent, const char *name = 0) : QValidator(parent) { setObjectName( QLatin1String( name ) ); } virtual void fixup(QString &s) const override { s.remove('/'); } virtual State validate(QString &s, int &) const override { if(s.contains('/')) return Invalid; return Acceptable; } }; class FixedHLayout final : public QHBoxLayout { public: FixedHLayout(QWidget *parent, int margin = 0, int spacing = -1, const char *name = 0) : QHBoxLayout(parent), m_width(-1) { setMargin(margin); setSpacing(spacing); setObjectName(QLatin1String(name)); } FixedHLayout(QLayout *parentLayout, int spacing = -1, const char *name = 0) : QHBoxLayout(), m_width(-1) { parentLayout->addItem(this); setSpacing(spacing); setObjectName(QLatin1String(name)); } void setWidth(int w = -1) { m_width = w == -1 ? QHBoxLayout::minimumSize().width() : w; } virtual QSize minimumSize() const override { QSize s = QHBoxLayout::minimumSize(); s.setWidth(m_width); return s; } private: int m_width; }; class CollectionObserver final : public PlaylistObserver { public: CollectionObserver(TagEditor *parent) : PlaylistObserver(CollectionList::instance()), m_parent(parent) { } virtual void playlistItemDataHasChanged() override { if(m_parent && m_parent->m_currentPlaylist && m_parent->isVisible()) m_parent->slotSetItems(m_parent->m_currentPlaylist->selectedItems()); } private: TagEditor *m_parent; }; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// TagEditor::TagEditor(QWidget *parent) : QWidget(parent), m_currentPlaylist(0), m_observer(0), m_performingSave(false) { setupActions(); setupLayout(); readConfig(); m_dataChanged = false; m_collectionChanged = false; setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); } TagEditor::~TagEditor() { delete m_observer; saveConfig(); } void TagEditor::setupObservers() { m_observer = new CollectionObserver(this); } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void TagEditor::slotSetItems(const PlaylistItemList &list) { if(m_performingSave) return; // Store the playlist that we're setting because saveChangesPrompt // can delete the PlaylistItems in list. Playlist *itemPlaylist = 0; if(!list.isEmpty()) itemPlaylist = list.first()->playlist(); bool hadPlaylist = m_currentPlaylist != 0; saveChangesPrompt(); if(m_currentPlaylist) { disconnect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem*)), this, SLOT(slotItemRemoved(PlaylistItem*))); } if((hadPlaylist && !m_currentPlaylist) || !itemPlaylist) { m_currentPlaylist = 0; m_items.clear(); } else { m_currentPlaylist = itemPlaylist; // We can't use list here, it may not be valid m_items = itemPlaylist->selectedItems(); } if(m_currentPlaylist) { connect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem*)), this, SLOT(slotItemRemoved(PlaylistItem*))); connect(m_currentPlaylist, SIGNAL(destroyed()), this, SLOT(slotPlaylistRemoved())); } if(isVisible()) slotRefresh(); else m_collectionChanged = true; } void TagEditor::slotRefresh() { // This method takes the list of currently selected m_items and tries to // figure out how to show that in the tag editor. The current strategy -- // the most common case -- is to just process the first item. Then we // check after that to see if there are other m_items and adjust accordingly. if(m_items.isEmpty() || !m_items.first()->file().tag()) { slotClear(); setEnabled(false); return; } setEnabled(true); PlaylistItem *item = m_items.first(); Q_ASSERT(item); Tag *tag = item->file().tag(); QFileInfo fi(item->file().absFilePath()); if(!fi.isWritable() && m_items.count() == 1) setEnabled(false); artistNameBox->setEditText(tag->artist()); trackNameBox->setText(tag->title()); albumNameBox->setEditText(tag->album()); fileNameBox->setText(item->file().fileInfo().fileName()); fileNameBox->setToolTip(item->file().absFilePath()); bitrateBox->setText(QString::number(tag->bitrate())); lengthBox->setText(tag->lengthString()); if(m_genreList.indexOf(tag->genre()) >= 0) genreBox->setCurrentIndex(m_genreList.indexOf(tag->genre()) + 1); else { genreBox->setCurrentIndex(0); genreBox->setEditText(tag->genre()); } trackSpin->setValue(tag->track()); yearSpin->setValue(tag->year()); commentBox->setPlainText(tag->comment()); // Start at the second item, since we've already processed the first. PlaylistItemList::Iterator it = m_items.begin(); ++it; // If there is more than one item in the m_items that we're dealing with... QList disabledForMulti; disabledForMulti << fileNameLabel << fileNameBox << lengthLabel << lengthBox << bitrateLabel << bitrateBox; foreach(QWidget *w, disabledForMulti) { w->setDisabled(m_items.size() > 1); if(m_items.size() > 1 && !w->inherits("QLabel")) QMetaObject::invokeMethod(w, "clear"); } if(it != m_items.end()) { foreach(QCheckBox *box, m_enableBoxes) { box->setChecked(true); box->show(); } // Yep, this is ugly. Loop through all of the files checking to see // if their fields are the same. If so, by default, enable their // checkbox. // Also, if there are more than 50 m_items, don't scan all of them. if(m_items.count() > 50) { m_enableBoxes[artistNameBox]->setChecked(false); m_enableBoxes[trackNameBox]->setChecked(false); m_enableBoxes[albumNameBox]->setChecked(false); m_enableBoxes[genreBox]->setChecked(false); m_enableBoxes[trackSpin]->setChecked(false); m_enableBoxes[yearSpin]->setChecked(false); m_enableBoxes[commentBox]->setChecked(false); } else { for(; it != m_items.end(); ++it) { tag = (*it)->file().tag(); if(tag) { if(artistNameBox->currentText() != tag->artist() && m_enableBoxes.contains(artistNameBox)) { artistNameBox->lineEdit()->clear(); m_enableBoxes[artistNameBox]->setChecked(false); } if(trackNameBox->text() != tag->title() && m_enableBoxes.contains(trackNameBox)) { trackNameBox->clear(); m_enableBoxes[trackNameBox]->setChecked(false); } if(albumNameBox->currentText() != tag->album() && m_enableBoxes.contains(albumNameBox)) { albumNameBox->lineEdit()->clear(); m_enableBoxes[albumNameBox]->setChecked(false); } if(genreBox->currentText() != tag->genre() && m_enableBoxes.contains(genreBox)) { genreBox->lineEdit()->clear(); m_enableBoxes[genreBox]->setChecked(false); } if(trackSpin->value() != tag->track() && m_enableBoxes.contains(trackSpin)) { trackSpin->setValue(0); m_enableBoxes[trackSpin]->setChecked(false); } if(yearSpin->value() != tag->year() && m_enableBoxes.contains(yearSpin)) { yearSpin->setValue(0); m_enableBoxes[yearSpin]->setChecked(false); } if(commentBox->toPlainText() != tag->comment() && m_enableBoxes.contains(commentBox)) { commentBox->clear(); m_enableBoxes[commentBox]->setChecked(false); } } } } } else { foreach(QCheckBox *box, m_enableBoxes) { box->setChecked(true); box->hide(); } } m_dataChanged = false; } void TagEditor::slotClear() { artistNameBox->lineEdit()->clear(); trackNameBox->clear(); albumNameBox->lineEdit()->clear(); genreBox->setCurrentIndex(0); fileNameBox->clear(); fileNameBox->setToolTip(QString()); trackSpin->setValue(0); yearSpin->setValue(0); lengthBox->clear(); bitrateBox->clear(); commentBox->clear(); } void TagEditor::slotUpdateCollection() { if(isVisible()) updateCollection(); else m_collectionChanged = true; } void TagEditor::updateCollection() { m_collectionChanged = false; CollectionList *list = CollectionList::instance(); if(!list) return; QStringList artistList = list->uniqueSet(CollectionList::Artists); artistList.sort(); artistNameBox->clear(); artistNameBox->addItems(artistList); artistNameBox->completionObject()->setItems(artistList); QStringList albumList = list->uniqueSet(CollectionList::Albums); albumList.sort(); albumNameBox->clear(); albumNameBox->addItems(albumList); albumNameBox->completionObject()->setItems(albumList); // Merge the list of genres found in tags with the standard ID3v1 set. StringHash genreHash; m_genreList = list->uniqueSet(CollectionList::Genres); foreach(const QString &genre, m_genreList) genreHash.insert(genre); TagLib::StringList genres = TagLib::ID3v1::genreList(); for(TagLib::StringList::Iterator it = genres.begin(); it != genres.end(); ++it) genreHash.insert(TStringToQString((*it))); m_genreList = genreHash.values(); m_genreList.sort(); genreBox->clear(); genreBox->addItem(QString()); genreBox->addItems(m_genreList); genreBox->completionObject()->setItems(m_genreList); // We've cleared out the original entries of these list boxes, re-read // the current item if one is selected. slotRefresh(); } //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void TagEditor::readConfig() { // combo box completion modes KConfigGroup config(KSharedConfig::openConfig(), "TagEditor"); if(artistNameBox && albumNameBox) { readCompletionMode(config, artistNameBox, "ArtistNameBoxMode"); readCompletionMode(config, albumNameBox, "AlbumNameBoxMode"); readCompletionMode(config, genreBox, "GenreBoxMode"); } bool show = config.readEntry("Show", false); ActionCollection::action("showEditor")->setChecked(show); setVisible(show); TagLib::StringList genres = TagLib::ID3v1::genreList(); for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it) m_genreList.append(TStringToQString((*it))); m_genreList.sort(); genreBox->clear(); genreBox->addItem(QString()); genreBox->addItems(m_genreList); genreBox->completionObject()->setItems(m_genreList); } void TagEditor::readCompletionMode(const KConfigGroup &config, KComboBox *box, const QString &key) { KCompletion::CompletionMode mode = KCompletion::CompletionMode(config.readEntry(key, (int)KCompletion::CompletionAuto)); box->setCompletionMode(mode); } void TagEditor::saveConfig() { // combo box completion modes KConfigGroup config(KSharedConfig::openConfig(), "TagEditor"); if(artistNameBox && albumNameBox) { config.writeEntry("ArtistNameBoxMode", (int)artistNameBox->completionMode()); config.writeEntry("AlbumNameBoxMode", (int)albumNameBox->completionMode()); config.writeEntry("GenreBoxMode", (int)genreBox->completionMode()); } config.writeEntry("Show", ActionCollection::action("showEditor")->isChecked()); } void TagEditor::setupActions() { KToggleAction *show = new KToggleAction(QIcon::fromTheme(QLatin1String("document-properties")), i18n("Show &Tag Editor"), this); ActionCollection::actions()->addAction("showEditor", show); connect(show, &QAction::toggled, this, &TagEditor::setVisible); QAction *act = new QAction(QIcon::fromTheme(QLatin1String( "document-save")), i18n("&Save"), this); ActionCollection::actions()->addAction("saveItem", act); ActionCollection::actions()->setDefaultShortcut(act, QKeySequence(Qt::CTRL + Qt::Key_T)); connect(act, &QAction::triggered, this, &TagEditor::slotSave); } void TagEditor::setupLayout() { setupUi(this); // Do some meta-programming to find the matching enable boxes const auto enableCheckBoxes = findChildren(QRegularExpression("Enable$")); for(auto enable : enableCheckBoxes) { enable->hide(); // These are shown only when multiple items are being edited // Each enable checkbox is identified by having its objectName end in "Enable". // The corresponding widget to be adjusted is identified by assigning a custom // property in Qt Designer "associatedObjectName", the value of which is the name // for the widget to be enabled (or not). auto associatedVariantValue = enable->property("associatedObjectName"); Q_ASSERT(associatedVariantValue.isValid()); QWidget *associatedWidget = findChild(associatedVariantValue.toString()); Q_ASSERT(associatedWidget != nullptr); m_enableBoxes[associatedWidget] = enable; } // Make sure that the labels are as tall as the enable boxes so that the // layout doesn't jump around as the enable boxes are shown/hidden. const auto editorLabels = findChildren(); for(auto label : editorLabels) { if(m_enableBoxes.contains(label->buddy())) label->setMinimumHeight(m_enableBoxes[label->buddy()]->height()); } tagEditorLayout->setColumnMinimumWidth(1, 200); } void TagEditor::save(const PlaylistItemList &list) { if(!list.isEmpty() && m_dataChanged) { QApplication::setOverrideCursor(Qt::WaitCursor); m_dataChanged = false; m_performingSave = true; // The list variable can become corrupted if the playlist holding its // items dies, which is possible as we edit tags. So we need to copy // the end marker. PlaylistItemList::ConstIterator end = list.end(); for(PlaylistItemList::ConstIterator it = list.begin(); it != end; /* Deliberately missing */ ) { // Process items before we being modifying tags, as the dynamic // playlists will try to modify the file we edit if the tag changes // due to our alterations here. qApp->processEvents(QEventLoop::ExcludeUserInputEvents); PlaylistItem *item = *it; // The playlist can be deleted from under us if this is the last // item and we edit it so that it doesn't match the search, which // means we can't increment the iterator, so let's do it now. ++it; QString fileName = item->file().fileInfo().path() + QDir::separator() + fileNameBox->text(); if(list.count() > 1) fileName = item->file().fileInfo().absoluteFilePath(); Tag *tag = TagTransactionManager::duplicateTag(item->file().tag(), fileName); // A bit more ugliness. If there are multiple files that are // being modified, they each have a "enabled" checkbox that // says if that field is to be respected for the multiple // files. We have to check to see if that is enabled before // each field that we write. if(m_enableBoxes[artistNameBox]->isChecked()) tag->setArtist(artistNameBox->currentText()); if(m_enableBoxes[trackNameBox]->isChecked()) tag->setTitle(trackNameBox->text()); if(m_enableBoxes[albumNameBox]->isChecked()) tag->setAlbum(albumNameBox->currentText()); if(m_enableBoxes[trackSpin]->isChecked()) { if(trackSpin->text().isEmpty()) trackSpin->setValue(0); tag->setTrack(trackSpin->value()); } if(m_enableBoxes[yearSpin]->isChecked()) { if(yearSpin->text().isEmpty()) yearSpin->setValue(0); tag->setYear(yearSpin->value()); } if(m_enableBoxes[commentBox]->isChecked()) tag->setComment(commentBox->toPlainText()); if(m_enableBoxes[genreBox]->isChecked()) tag->setGenre(genreBox->currentText()); TagTransactionManager::instance()->changeTagOnItem(item, tag); } TagTransactionManager::instance()->commit(); CollectionList::instance()->playlistItemsChanged(); m_performingSave = false; QApplication::restoreOverrideCursor(); } } void TagEditor::saveChangesPrompt() { if(!isVisible() || !m_dataChanged || m_items.isEmpty()) return; QStringList files; foreach(const PlaylistItem *item, m_items) files.append(item->file().absFilePath()); if(KMessageBox::questionYesNoList(this, i18n("Do you want to save your changes to:\n"), files, i18n("Save Changes"), KStandardGuiItem::save(), KStandardGuiItem::discard(), "tagEditor_showSaveChangesBox") == KMessageBox::Yes) { save(m_items); } } void TagEditor::showEvent(QShowEvent *e) { if(m_collectionChanged) { updateCollection(); } QWidget::showEvent(e); } //////////////////////////////////////////////////////////////////////////////// // private slots //////////////////////////////////////////////////////////////////////////////// void TagEditor::slotDataChanged() { m_dataChanged = true; } void TagEditor::slotItemRemoved(PlaylistItem *item) { m_items.removeAll(item); if(m_items.isEmpty()) slotRefresh(); } void TagEditor::slotPlaylistDestroyed(Playlist *p) { if(m_currentPlaylist == p) { m_currentPlaylist = 0; slotSetItems(PlaylistItemList()); } } // vim: set et sw=4 tw=0 sta: diff --git a/tagtransactionmanager.cpp b/tagtransactionmanager.cpp index 04547e41..738f9940 100644 --- a/tagtransactionmanager.cpp +++ b/tagtransactionmanager.cpp @@ -1,186 +1,186 @@ /** * Copyright (C) 2004 Michael Pyne * * 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 "tagtransactionmanager.h" #include #include #include #include #include #include #include #include "playlistitem.h" #include "collectionlist.h" -#include "tag.h" +#include "juktag.h" #include "actioncollection.h" #include "juk_debug.h" using ActionCollection::action; Q_GLOBAL_STATIC(TagTransactionManager, g_tagManager) TagTransactionManager *TagTransactionManager::instance() { return g_tagManager; } TagTransactionAtom::TagTransactionAtom(PlaylistItem *item, Tag *tag) : m_item(item) , m_tag(tag) { } void TagTransactionManager::changeTagOnItem(PlaylistItem *item, Tag *newTag) { if(!item) { qCWarning(JUK_LOG) << "Trying to change tag on null PlaylistItem.\n"; return; } // Save the CollectionListItem, as it is the most likely to survive long // enough for the commit(). I should probably intercept the item deleted // signals from CollectionList to ensure that the commit list and the // playlists stay in sync. m_list.emplace_back(item->collectionItem(), newTag); } Tag *TagTransactionManager::duplicateTag(const Tag *tag, const QString &fileName) { Q_ASSERT(tag); QString name = fileName.isEmpty() ? tag->fileName() : fileName; Tag *newTag = new Tag(*tag); newTag->setFileName(name); return newTag; } bool TagTransactionManager::commit() { m_undoList.clear(); bool result = processChangeList(); m_list.clear(); return result; } void TagTransactionManager::forget() { m_list.clear(); } bool TagTransactionManager::undo() { qCDebug(JUK_LOG) << "Undoing " << m_undoList.size() << " changes.\n"; forget(); // Scrap our old changes (although the list should be empty // anyways. bool result = processChangeList(true); m_undoList.clear(); action("edit_undo")->setEnabled(false); return result; } bool TagTransactionManager::renameFile(const QFileInfo &from, const QFileInfo &to) const { if(!QFileInfo(to.path()).isWritable() || !from.exists()) return false; if(!to.exists() || KMessageBox::warningContinueCancel( static_cast(parent()), i18n("This file already exists.\nDo you want to replace it?"), i18n("File Exists"),KGuiItem(i18n("Replace"))) == KMessageBox::Continue) { qCDebug(JUK_LOG) << "Renaming " << from.absoluteFilePath() << " to " << to.absoluteFilePath(); QDir currentDir; return currentDir.rename(from.absoluteFilePath(), to.absoluteFilePath()); } return false; } bool TagTransactionManager::processChangeList(bool undo) { TagAlterationList::const_iterator it, end; QStringList errorItems; it = undo ? m_undoList.cbegin() : m_list.cbegin(); end = undo ? m_undoList.cend() : m_list.cend(); emit signalAboutToModifyTags(); for(; it != end; ++it) { PlaylistItem *item = (*it).item(); const Tag *tag = (*it).tag(); QFileInfo newFile(tag->fileName()); if(item->file().fileInfo().fileName() != newFile.fileName()) { if(!renameFile(item->file().fileInfo(), newFile)) { errorItems.append(item->text(1) + QString(" - ") + item->text(0)); continue; } } if(tag->save()) { if(!undo) m_undoList.emplace_back(item, duplicateTag(item->file().tag())); item->file().setFile(tag->fileName()); item->refreshFromDisk(); //FIXME repaint //item->repaint(); item->playlist()->playlistItemsChanged(); item->playlist()->update(); } else { Tag *errorTag = item->file().tag(); QString str = errorTag->artist() + " - " + errorTag->title(); if(errorTag->artist().isEmpty()) str = errorTag->title(); errorItems.append(str); } qApp->processEvents(); } undo ? m_undoList.clear() : m_list.clear(); if(!undo && !m_undoList.empty()) action("edit_undo")->setEnabled(true); else action("edit_undo")->setEnabled(false); if(!errorItems.isEmpty()) KMessageBox::errorList(static_cast(parent()), i18n("The following files were unable to be changed."), errorItems, i18n("Error")); emit signalDoneModifyingTags(); return errorItems.isEmpty(); } // vim: set et sw=4 tw=0 sta: diff --git a/tagtransactionmanager.h b/tagtransactionmanager.h index 3866e368..9c9779e8 100644 --- a/tagtransactionmanager.h +++ b/tagtransactionmanager.h @@ -1,173 +1,173 @@ /** * Copyright (C) 2004, 2017 Michael Pyne * * 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 TAGTRANSACTIONMANAGER_H #define TAGTRANSACTIONMANAGER_H #include #include #include -#include "tag.h" +#include "juktag.h" class PlaylistItem; class QWidget; class QFileInfo; using std::unique_ptr; /** * Class to encapsulate a change to the tag, and optionally the file name, of * a PlaylistItem. * * @author Michael Pyne * @see TagTransactionManager */ struct TagTransactionAtom { public: /** * Creates an atom detailing a change made by \p tag to \p item. * * @param item The PlaylistItem to change. * @param tag Contains the new tag to apply to item. */ TagTransactionAtom(PlaylistItem *item, Tag *tag); PlaylistItem *item() const { return m_item; } const Tag *tag() const { return m_tag.get(); } PlaylistItem *m_item; unique_ptr m_tag; }; // Qt's containers don't play well with std::unique_ptr, but being able to use // unique_ptr avoids a bunch of custom code on my part. using TagAlterationList = std::vector; /** * This class manages alterations of a group of PlaylistItem's FileHandles. What this * means in practice is that you will use this class to change the tags and/or * filename of a PlaylistItem. * * This class supports a limited transactional interface. Once you commit a * group of changes, you can call the undo() method to revert back to the way * things were (except possibly for file renames). You can call forget() to * forget a series of changes as well. * * @author Michael Pyne */ class TagTransactionManager : public QObject { Q_OBJECT public: /** * Returns the global TagTransactionManager instance. * * @return The global TagTransactionManager. */ static TagTransactionManager *instance(); /** * Adds a change to the list of changes to apply. Internally this * function extracts the CollectionListItem of @p item, and uses that * instead, so there is no need to do so yourself. * * @param item The PlaylistItem to change. * @param newTag The Tag containing the changed data. */ void changeTagOnItem(PlaylistItem *item, Tag *newTag); /** * Convenience function to duplicate a Tag object, since the Tag * object doesn't have a decent copy constructor. * * @param tag The Tag to duplicate. * @param fileName The filename to assign to the tag. If an empty QString * (the default) is passed, the filename of the existing tag is * used. * @bug Tag should have a correct copy ctor and assignment operator. * @return The duplicate Tag. */ static Tag *duplicateTag(const Tag *tag, const QString &fileName = QString()); /** * Commits the changes to the PlaylistItems. It is important that the * PlaylistItems still exist when you call this function, although this * shouldn't be a problem in practice. After altering the tags, and * renaming the files if necessary, you can call undo() to back out the * changes. * * If any errors have occurred, the user will be notified with a dialog * box, and those files which were unabled to be altered will be excluded * from the undo set. * * @return true if no errors occurred, false otherwise. */ bool commit(); /** * Clears the current update list. The current undo list is unaffected. */ void forget(); /** * Undoes the changes caused by commit(). Like commit(), if any errors * occur changing the state back (for example, it may be impossible to * rename a file back to its original name), the user will be shown notified * via a dialog box. * * After performing the undo operation, it is impossible to call undo() * again on the same set of files. Namely, you can't repeatedly call * undo() to switch between two different file states. * * @return true if no errors occurred, false otherwise. */ bool undo(); signals: void signalAboutToModifyTags(); void signalDoneModifyingTags(); private: /** * Renames the file identified by @p from to have the name given by @p to, * prompting the user to confirm if necessary. * * @param from QFileInfo with the filename of the original file. * @param to QFileInfo with the new filename. * @return true if no errors occurred, false otherwise. */ bool renameFile(const QFileInfo &from, const QFileInfo &to) const; /** * Used internally by commit() and undo(). Performs the work of updating * the PlaylistItems and then updating the various GUI elements that need * to be updated. * * @param undo true if operating in undo mode, false otherwise. */ bool processChangeList(bool undo = false); TagAlterationList m_list; ///< holds a list of changes to commit TagAlterationList m_undoList; ///< holds a list of changes to undo }; #endif /* TAGTRANSACTIONMANAGER_H */ // vim: set et sw=4 tw=0 sta: diff --git a/tracksequenceiterator.cpp b/tracksequenceiterator.cpp index 42f6b6ec..56e9ed96 100644 --- a/tracksequenceiterator.cpp +++ b/tracksequenceiterator.cpp @@ -1,334 +1,334 @@ /** * Copyright (C) 2002-2004 Michael Pyne * * 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 "tracksequenceiterator.h" #include #include #include #include "playlist.h" #include "actioncollection.h" -#include "tag.h" +#include "juktag.h" #include "filehandle.h" #include "juk_debug.h" using namespace ActionCollection; TrackSequenceIterator::TrackSequenceIterator() : m_current(0) { } TrackSequenceIterator::TrackSequenceIterator(const TrackSequenceIterator &other) : m_current(other.m_current) { } TrackSequenceIterator::~TrackSequenceIterator() { } void TrackSequenceIterator::setCurrent(PlaylistItem *current) { m_current = current; } void TrackSequenceIterator::playlistChanged() { } void TrackSequenceIterator::itemAboutToDie(const PlaylistItem *) { } DefaultSequenceIterator::DefaultSequenceIterator() : TrackSequenceIterator() { } DefaultSequenceIterator::DefaultSequenceIterator(const DefaultSequenceIterator &other) : TrackSequenceIterator(other) { } DefaultSequenceIterator::~DefaultSequenceIterator() { } // Helper function to return a random number up to (but not including) a given max with // a truly equal probability of each integer in [0, max) being selected. // When Qt 5.10 can be required we can use QRandomGenerator for this, but for now need to // fixup KRandom. // See https://twitter.com/colmmacc/status/1012723779708088320 static int boundedRandom(const int upperBound) { while (1) { const int candidate = KRandom::random(); // this check excludes integers above the highest multiple of // upperBound that is still below RAND_MAX to remove bias if (candidate < (RAND_MAX - (RAND_MAX % upperBound))) { return candidate % upperBound; } } } void DefaultSequenceIterator::advance() { if(!current()) return; bool isRandom = action("randomPlay") && action("randomPlay")->isChecked(); bool loop = action("loopPlaylist") && action("loopPlaylist")->isChecked(); bool albumRandom = action("albumRandomPlay") && action("albumRandomPlay")->isChecked(); if(isRandom || albumRandom) { // TODO: This should probably use KRandomSequence's ability to shuffle // items instead of making a new random choice each time through. if(m_randomItems.isEmpty() && loop) { // Since refillRandomList will remove the currently playing item, // we should clear it out first since that's not good for e.g. // lists with 1-2 items. We need to remember the Playlist though. Playlist *playlist = current()->playlist(); setCurrent(0); refillRandomList(playlist); } if(m_randomItems.isEmpty()) { setCurrent(0); return; } PlaylistItem *item; if(albumRandom) { if(m_albumSearch.isNull() || m_albumSearch.matchedItems().isEmpty()) { item = m_randomItems[boundedRandom(m_randomItems.count())]; initAlbumSearch(item); } // This can be null if initAlbumSearch() left the m_albumSearch // empty because the album text was empty. Since we initAlbumSearch() // with an item, the matchedItems() should never be empty. if(!m_albumSearch.isNull()) { PlaylistItemList albumMatches = m_albumSearch.matchedItems(); if(albumMatches.isEmpty()) { qCCritical(JUK_LOG) << "Unable to initialize album random play.\n"; qCCritical(JUK_LOG) << "List of potential results is empty.\n"; return; // item is still set to random song from a few lines earlier. } item = albumMatches[0]; // Pick first song remaining in list. for(int i = 0; i < albumMatches.count(); ++i) if(albumMatches[i]->file().tag()->track() < item->file().tag()->track()) item = albumMatches[i]; m_albumSearch.clearItem(item); if(m_albumSearch.matchedItems().isEmpty()) { m_albumSearch.clearComponents(); m_albumSearch.search(); } } else qCCritical(JUK_LOG) << "Unable to perform album random play on " << *item; } else item = m_randomItems[boundedRandom(m_randomItems.count())]; setCurrent(item); m_randomItems.removeAll(item); } else { PlaylistItem *next = current()->itemBelow(); if(!next && loop) { Playlist *p = current()->playlist(); next = p->firstChild(); // FIXME playlist iterator (nextSibling) /*while(next && !next->isVisible()) next = static_cast(next->nextSibling());*/ } setCurrent(next); } } void DefaultSequenceIterator::backup() { if(!current()) return; PlaylistItem *item = current()->itemAbove(); if(item) setCurrent(item); } void DefaultSequenceIterator::prepareToPlay(Playlist *playlist) { bool random = action("randomPlay") && action("randomPlay")->isChecked(); bool albumRandom = action("albumRandomPlay") && action("albumRandomPlay")->isChecked(); if(random || albumRandom) { PlaylistItemList items = playlist->selectedItems(); if(items.isEmpty()) items = playlist->visibleItems(); PlaylistItem *newItem = 0; if(!items.isEmpty()) newItem = items[KRandom::random() % items.count()]; setCurrent(newItem); refillRandomList(); } else { QTreeWidgetItemIterator it(playlist, QTreeWidgetItemIterator::NotHidden | QTreeWidgetItemIterator::Selected); if(!*it) it = QTreeWidgetItemIterator(playlist, QTreeWidgetItemIterator::NotHidden); setCurrent(static_cast(*it)); } } void DefaultSequenceIterator::reset() { m_randomItems.clear(); m_albumSearch.clearComponents(); m_albumSearch.search(); setCurrent(0); } void DefaultSequenceIterator::playlistChanged() { refillRandomList(); } void DefaultSequenceIterator::itemAboutToDie(const PlaylistItem *item) { PlaylistItem *stfu_gcc = const_cast(item); m_randomItems.removeAll(stfu_gcc); } void DefaultSequenceIterator::setCurrent(PlaylistItem *current) { PlaylistItem *oldCurrent = DefaultSequenceIterator::current(); TrackSequenceIterator::setCurrent(current); bool random = action("randomPlay") && action("randomPlay")->isChecked(); bool albumRandom = action("albumRandomPlay") && action("albumRandomPlay")->isChecked(); if((albumRandom || random) && current && m_randomItems.isEmpty()) { // We're setting a current item, refill the random list now, and remove // the current item. refillRandomList(); } m_randomItems.removeAll(current); if(albumRandom && current && !oldCurrent) { // Same idea as above initAlbumSearch(current); m_albumSearch.clearItem(current); } } DefaultSequenceIterator *DefaultSequenceIterator::clone() const { return new DefaultSequenceIterator(*this); } void DefaultSequenceIterator::refillRandomList(Playlist *p) { if(!p) { if (!current()) return; p = current()->playlist(); if(!p) { qCCritical(JUK_LOG) << "Item has no playlist!\n"; return; } } m_randomItems = p->visibleItems(); m_randomItems.removeAll(current()); m_albumSearch.clearComponents(); m_albumSearch.search(); } void DefaultSequenceIterator::initAlbumSearch(PlaylistItem *searchItem) { if(!searchItem) return; m_albumSearch.clearPlaylists(); m_albumSearch.addPlaylist(searchItem->playlist()); ColumnList columns; m_albumSearch.setSearchMode(PlaylistSearch::MatchAll); m_albumSearch.clearComponents(); // If the album name is empty, it will mess up the search, // so ignore empty album names. if(searchItem->file().tag()->album().isEmpty()) return; columns.append(PlaylistItem::AlbumColumn); m_albumSearch.addComponent(PlaylistSearch::Component( searchItem->file().tag()->album(), true, columns, PlaylistSearch::Component::Exact) ); // If there is an Artist tag with the track, match against it as well // to avoid things like multiple "Greatest Hits" albums matching the // search. if(!searchItem->file().tag()->artist().isEmpty()) { qCDebug(JUK_LOG) << "Searching both artist and album."; columns[0] = PlaylistItem::ArtistColumn; m_albumSearch.addComponent(PlaylistSearch::Component( searchItem->file().tag()->artist(), true, columns, PlaylistSearch::Component::Exact) ); } m_albumSearch.search(); } // vim: set et sw=4 tw=0 sta: diff --git a/tracksequencemanager.cpp b/tracksequencemanager.cpp index 4a5c38d5..48ea111f 100644 --- a/tracksequencemanager.cpp +++ b/tracksequencemanager.cpp @@ -1,182 +1,182 @@ /** * Copyright (C) 2002-2004 Michael Pyne * * 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 "tracksequencemanager.h" #include #include #include #include "actioncollection.h" #include "playlist.h" #include "playlistitem.h" #include "tracksequenceiterator.h" -#include "tag.h" +#include "juktag.h" #include "filehandle.h" #include "collectionlist.h" ///////////////////////////////////////////////////////////////////////////// // public functions ///////////////////////////////////////////////////////////////////////////// TrackSequenceManager::~TrackSequenceManager() { // m_playlist doesn't belong to us, don't try to delete if(m_iterator == m_defaultIterator) m_iterator = 0; delete m_iterator; delete m_defaultIterator; } bool TrackSequenceManager::installIterator(TrackSequenceIterator *iterator) { PlaylistItem *oldItem = m_iterator ? m_iterator->current() : 0; if(m_iterator != m_defaultIterator) delete m_iterator; m_iterator = m_defaultIterator; if(iterator) m_iterator = iterator; m_iterator->setCurrent(oldItem); return true; } PlaylistItem *TrackSequenceManager::currentItem() const { return m_iterator->current(); } TrackSequenceIterator *TrackSequenceManager::takeIterator() { TrackSequenceIterator *temp = m_iterator; m_iterator = nullptr; return temp; } TrackSequenceManager *TrackSequenceManager::instance() { static TrackSequenceManager manager; if(!manager.m_initialized) manager.initialize(); return &manager; } PlaylistItem *TrackSequenceManager::nextItem() { if(m_playNextItem) { // Force the iterator to reset state (such as random item lists) m_iterator->reset(); m_iterator->prepareToPlay(m_playNextItem->playlist()); m_iterator->setCurrent(m_playNextItem); m_playNextItem = nullptr; } else if(m_iterator->current()) m_iterator->advance(); else if(currentPlaylist()) m_iterator->prepareToPlay(currentPlaylist()); else m_iterator->prepareToPlay(CollectionList::instance()); return m_iterator->current(); } PlaylistItem *TrackSequenceManager::previousItem() { m_iterator->backup(); return m_iterator->current(); } ///////////////////////////////////////////////////////////////////////////// // public slots ///////////////////////////////////////////////////////////////////////////// void TrackSequenceManager::setNextItem(PlaylistItem *item) { m_playNextItem = item; } void TrackSequenceManager::setCurrentPlaylist(Playlist *list) { if(m_playlist) m_playlist->disconnect(this); m_playlist = list; connect(m_playlist, SIGNAL(signalAboutToRemove(PlaylistItem*)), this, SLOT(slotItemAboutToDie(PlaylistItem*))); } void TrackSequenceManager::setCurrent(PlaylistItem *item) { if(item != m_iterator->current()) { m_iterator->setCurrent(item); if(item) setCurrentPlaylist(item->playlist()); else m_iterator->reset(); } } ///////////////////////////////////////////////////////////////////////////// // private functions ///////////////////////////////////////////////////////////////////////////// void TrackSequenceManager::initialize() { CollectionList *collection = CollectionList::instance(); if(!collection) return; // Make sure we don't use m_playNextItem if it's invalid. connect(collection, SIGNAL(signalAboutToRemove(PlaylistItem*)), this, SLOT(slotItemAboutToDie(PlaylistItem*))); m_initialized = true; } TrackSequenceManager::TrackSequenceManager() : QObject(), m_playlist(0), m_playNextItem(0), m_iterator(0), m_initialized(false) { m_defaultIterator = new DefaultSequenceIterator(); m_iterator = m_defaultIterator; } ///////////////////////////////////////////////////////////////////////////// // protected slots ///////////////////////////////////////////////////////////////////////////// void TrackSequenceManager::slotItemAboutToDie(PlaylistItem *item) { if(item == m_playNextItem) m_playNextItem = 0; m_iterator->itemAboutToDie(item); } // vim: set et sw=4 tw=0 sta: diff --git a/treeviewitemplaylist.cpp b/treeviewitemplaylist.cpp index be924d9b..45c25e02 100644 --- a/treeviewitemplaylist.cpp +++ b/treeviewitemplaylist.cpp @@ -1,94 +1,94 @@ /** * Copyright (C) 2004 Michael Pyne * * 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 "treeviewitemplaylist.h" #include #include #include #include "collectionlist.h" -#include "tag.h" +#include "juktag.h" #include "playlistitem.h" #include "playlistsearch.h" #include "tagtransactionmanager.h" #include "juk_debug.h" TreeViewItemPlaylist::TreeViewItemPlaylist(PlaylistCollection *collection, const PlaylistSearch &search, const QString &name) : SearchPlaylist(collection, search, name, false) { PlaylistSearch::Component component = *(search.components().begin()); m_columnType = static_cast(*(component.columns().begin())); } void TreeViewItemPlaylist::retag(const QStringList &files, Playlist *) { CollectionList *collection = CollectionList::instance(); if(files.isEmpty()) return; QString changedTag = i18n("artist"); if(m_columnType == PlaylistItem::GenreColumn) changedTag = i18n("genre"); else if(m_columnType == PlaylistItem::AlbumColumn) changedTag = i18n("album"); if(KMessageBox::warningContinueCancelList( this, i18n("You are about to change the %1 on these files.", changedTag), files, i18n("Changing Track Tags"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "dragDropRetagWarn" ) == KMessageBox::Cancel) { return; } QStringList::ConstIterator it; for(it = files.begin(); it != files.end(); ++it) { CollectionListItem *item = collection->lookup(*it); if(!item) continue; Tag *tag = TagTransactionManager::duplicateTag(item->file().tag()); switch(m_columnType) { case PlaylistItem::ArtistColumn: tag->setArtist(name()); break; case PlaylistItem::AlbumColumn: tag->setAlbum(name()); break; case PlaylistItem::GenreColumn: tag->setGenre(name()); break; default: qCDebug(JUK_LOG) << "Unhandled column type editing " << *it; } TagTransactionManager::instance()->changeTagOnItem(item, tag); } } // vim: set et sw=4 tw=0 sta: diff --git a/webimagefetcher.cpp b/webimagefetcher.cpp index ff28775d..c0f6c909 100644 --- a/webimagefetcher.cpp +++ b/webimagefetcher.cpp @@ -1,258 +1,258 @@ /** * Copyright (C) 2004 Nathan Toone * Copyright (C) 2007, 2017 Michael Pyne * Copyright (C) 2012 Martin Sandsmark * * 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 "webimagefetcher.h" #include #include #include #include #include "covermanager.h" #include "filehandle.h" -#include "tag.h" +#include "juktag.h" #include "juk.h" #include "juk_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include class WebImageFetcher::Private { friend class WebImageFetcher; FileHandle file; QString artist; QString albumName; QPointer connection; QDialog *dialog = nullptr; QUrl url; }; WebImageFetcher::WebImageFetcher(QObject *parent) : QObject(parent) , d(new Private) { } WebImageFetcher::~WebImageFetcher() { delete d; } void WebImageFetcher::setFile(const FileHandle &file) { d->file = file; d->artist = file.tag()->artist(); d->albumName = file.tag()->album(); } void WebImageFetcher::abortSearch() { if (d->connection) d->connection->kill(); } void WebImageFetcher::searchCover() { QStatusBar *statusBar = JuK::JuKInstance()->statusBar(); statusBar->showMessage(i18n("Searching for cover. Please Wait...")); QUrlQuery urlQuery; urlQuery.addQueryItem("method", "album.getInfo"); urlQuery.addQueryItem("api_key", "3e6ecbd7284883089e8f2b5b53b0aecd"); urlQuery.addQueryItem("artist", d->artist); urlQuery.addQueryItem("album", d->albumName); QUrl url("http://ws.audioscrobbler.com/2.0/"); url.setQuery(urlQuery); qCDebug(JUK_LOG) << "Using request " << url.toDisplayString(); d->connection = KIO::storedGet(url, KIO::Reload /* reload always */, KIO::HideProgressInfo); connect(d->connection, SIGNAL(result(KJob*)), SLOT(slotWebRequestFinished(KJob*))); // Wait for the results... } void WebImageFetcher::slotWebRequestFinished(KJob *job) { if (job != d->connection) return; QStatusBar *statusBar = JuK::JuKInstance()->statusBar(); if (!job || job->error()) { qCCritical(JUK_LOG) << "Error reading image results from last.fm!\n"; qCCritical(JUK_LOG) << d->connection->errorString(); return; } if (d->connection->data().isEmpty()) { qCCritical(JUK_LOG) << "last.fm returned an empty result!\n"; return; } QDomDocument results("ResultSet"); QString errorStr; int errorCol, errorLine; if (!results.setContent(d->connection->data(), &errorStr, &errorLine, &errorCol)) { qCCritical(JUK_LOG) << "Unable to create XML document from results.\n"; qCCritical(JUK_LOG) << "Line " << errorLine << ", " << errorStr; return; } QDomElement n = results.documentElement(); if (n.isNull()) { qCDebug(JUK_LOG) << "No document root in XML results??\n"; return; } if (n.nodeName() != QLatin1String("lfm")) { qCDebug(JUK_LOG) << "Invalid resulting XML document, not "; return; } if (n.attribute(QStringLiteral("status")) != QLatin1String("ok")) { const QDomElement err = n.firstChildElement(QStringLiteral("error")); const int errCode = err.attribute(QStringLiteral("code")).toInt(); if (errCode == 6) { KMessageBox::information(nullptr, i18n("Album '%1' not found.", d->albumName), i18nc("@title:window", "Album not Found")); } else { KMessageBox::error(nullptr, i18n("Error %1 when searching for cover:\n%2", errCode, err.text())); } statusBar->clearMessage(); return; } n = n.firstChildElement("album"); //FIXME: We assume they have a sane sorting (smallest -> largest) const QString imageUrl = n.lastChildElement("image").text(); if (imageUrl.isEmpty()) { KMessageBox::information(nullptr, i18n("No available cover for the album '%1'.", d->albumName), i18nc("@title:window", "Cover not Available")); statusBar->clearMessage(); return; } d->url = QUrl::fromEncoded(imageUrl.toLatin1()); //TODO: size attribute can have the values mega, extralarge, large, medium and small qCDebug(JUK_LOG) << "Got cover:" << d->url; statusBar->showMessage(i18n("Downloading cover. Please Wait...")); KIO::StoredTransferJob *newJob = KIO::storedGet(d->url, KIO::Reload /* reload always */, KIO::HideProgressInfo); connect(newJob, SIGNAL(result(KJob*)), SLOT(slotImageFetched(KJob*))); } void WebImageFetcher::slotImageFetched(KJob* j) { QStatusBar *statusBar = JuK::JuKInstance()->statusBar(); statusBar->clearMessage(); KIO::StoredTransferJob *job = qobject_cast(j); if (d->dialog) return; d->dialog = new QDialog; d->dialog->setWindowTitle(i18n("Cover found")); auto dlgVLayout = new QVBoxLayout(d->dialog); if(job->error()) { qCCritical(JUK_LOG) << "Unable to grab image" << job->errorText(); KMessageBox::sorry(nullptr, i18n("Failed to download requested cover art: %1", job->errorString()), i18nc("@title:window", "Could not download cover art")); return; } // TODO: 150x150 seems inconsistent with HiDPI, figure out something better QPixmap iconImage, realImage(150, 150); iconImage.loadFromData(job->data()); realImage.fill(Qt::transparent); if(iconImage.isNull()) { qCCritical(JUK_LOG) << "Thumbnail image is not of a supported format\n"; return; } // Scale down if necesssary if(iconImage.width() > 150 || iconImage.height() > 150) iconImage = iconImage.scaled(150, 150, Qt::KeepAspectRatio, Qt::SmoothTransformation); QLabel *cover = new QLabel(d->dialog); cover->setPixmap(iconImage); dlgVLayout->addWidget(cover); QLabel *infoLabel = new QLabel(i18n("Cover fetched from last.fm."), d->dialog); infoLabel->setOpenExternalLinks(true); infoLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); dlgVLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding)); dlgVLayout->addWidget(infoLabel); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, d->dialog); dlgVLayout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, d->dialog, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, d->dialog, &QDialog::reject); connect(d->dialog, &QDialog::accepted, this, &WebImageFetcher::slotCoverChosen); connect(d->dialog, &QDialog::rejected, this, &WebImageFetcher::destroyDialog); d->dialog->setWindowIcon(realImage); d->dialog->show(); } void WebImageFetcher::slotCoverChosen() { qCDebug(JUK_LOG) << "Adding new cover for " << d->file.tag()->fileName() << "from URL" << d->url; coverKey newId = CoverManager::addCover(d->url, d->file.tag()->artist(), d->file.tag()->album()); if (newId != CoverManager::NoMatch) { emit signalCoverChanged(newId); destroyDialog(); } } void WebImageFetcher::destroyDialog() { if (!d->dialog) return; d->dialog->close(); d->dialog->deleteLater(); d->dialog = 0; } // vim: set et sw=4 tw=0 sta: