File indexing completed on 2024-05-19 04:49:26

0001 /****************************************************************************************
0002  * Copyright (c) 2007-2008 Ian Monroe <ian@monroe.nu>                                   *
0003  * Copyright (c) 2013 Matěj Laitl <matej@laitl.cz>                                      *
0004  *                                                                                      *
0005  * This program is free software; you can redistribute it and/or modify it under        *
0006  * the terms of the GNU General Public License as published by the Free Software        *
0007  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
0008  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
0009  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
0010  * version 3 of the license.                                                            *
0011  *                                                                                      *
0012  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0013  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0014  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0015  *                                                                                      *
0016  * You should have received a copy of the GNU General Public License along with         *
0017  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0018  ****************************************************************************************/
0019 
0020 #include "TrackLoader.h"
0021 
0022 #include "core/playlists/PlaylistFormat.h"
0023 #include "core/support/Debug.h"
0024 #include "core-impl/meta/file/File.h"
0025 #include "core-impl/meta/proxy/MetaProxy.h"
0026 #include "core-impl/meta/multi/MultiTrack.h"
0027 #include "core-impl/playlists/types/file/PlaylistFileSupport.h"
0028 
0029 #include <KIO/Job>
0030 #include <KFileItem>
0031 
0032 #include <QFileInfo>
0033 #include <QTimer>
0034 
0035 TrackLoader::TrackLoader( Flags flags, int timeout )
0036     : m_status( LoadingTracks )
0037     , m_flags( flags )
0038     , m_timeout( timeout )
0039 {
0040 }
0041 
0042 TrackLoader::~TrackLoader()
0043 {
0044 }
0045 
0046 void
0047 TrackLoader::init( const QUrl &url )
0048 {
0049     init( QList<QUrl>() << url );
0050 }
0051 
0052 void
0053 TrackLoader::init( const QList<QUrl> &qurls )
0054 {
0055     m_sourceUrls = qurls;
0056     QTimer::singleShot( 0, this, &TrackLoader::processNextSourceUrl );
0057 }
0058 
0059 void
0060 TrackLoader::init( const Playlists::PlaylistList &playlists )
0061 {
0062     m_resultPlaylists = playlists;
0063     // no need to process source urls here, short-cut to result urls (just playlists)
0064     QTimer::singleShot( 0, this, &TrackLoader::processNextResultUrl );
0065 }
0066 
0067 void
0068 TrackLoader::processNextSourceUrl()
0069 {
0070     if( m_sourceUrls.isEmpty() )
0071     {
0072         QTimer::singleShot( 0, this, &TrackLoader::processNextResultUrl );
0073         return;
0074     }
0075 
0076     QUrl sourceUrl = m_sourceUrls.takeFirst();
0077     if( !sourceUrl.isValid() )
0078     {
0079         error() << "Url is invalid:" << sourceUrl;
0080         QTimer::singleShot( 0, this, &TrackLoader::processNextSourceUrl );
0081         return;
0082     }
0083     if( sourceUrl.isLocalFile() && QFileInfo( sourceUrl.toLocalFile() ).isDir() )
0084     {
0085         // KJobs delete themselves
0086         KIO::ListJob *lister = KIO::listRecursive( sourceUrl );
0087         connect( lister, &KIO::ListJob::result, this, &TrackLoader::processNextSourceUrl );
0088         connect( lister, &KIO::ListJob::entries, this, &TrackLoader::directoryListResults );
0089         return;
0090     }
0091     else
0092         m_resultUrls.append( sourceUrl );
0093 
0094     QTimer::singleShot( 0, this, &TrackLoader::processNextSourceUrl );
0095 }
0096 
0097 void
0098 TrackLoader::directoryListResults( KIO::Job *job, const KIO::UDSEntryList &list )
0099 {
0100     //dfaure says that job->redirectionUrl().isValid() ? job->redirectionUrl() : job->url(); might be needed
0101     //but to wait until an issue is actually found, since it might take more work
0102     const QUrl dir = static_cast<KIO::SimpleJob *>( job )->url();
0103     foreach( const KIO::UDSEntry &entry, list )
0104     {
0105         KFileItem item( entry, dir, true, true );
0106         QUrl url = item.url();
0107         if( MetaFile::Track::isTrack( url ) )
0108         {
0109             auto insertIter = std::upper_bound( m_resultUrls.begin(), m_resultUrls.end(), url, directorySensitiveLessThan );
0110             m_resultUrls.insert( insertIter, url );
0111         }
0112     }
0113 }
0114 
0115 void
0116 TrackLoader::processNextResultUrl()
0117 {
0118     using namespace Playlists;
0119     if( !m_resultPlaylists.isEmpty() )
0120     {
0121         PlaylistPtr playlist = m_resultPlaylists.takeFirst();
0122         PlaylistObserver::subscribeTo( playlist );
0123         playlist->triggerTrackLoad(); // playlist track loading is on demand.
0124         // will trigger tracksLoaded() which in turn calls processNextResultUrl(),
0125         // therefore we shouldn't call trigger processNextResultUrl() here:
0126         return;
0127     }
0128 
0129     if( m_resultUrls.isEmpty() )
0130     {
0131         mayFinish();
0132         return;
0133     }
0134 
0135     QUrl resultUrl = m_resultUrls.takeFirst();
0136     if( isPlaylist( resultUrl ) )
0137     {
0138         PlaylistFilePtr playlist = loadPlaylistFile( resultUrl );
0139         if( playlist )
0140         {
0141             PlaylistObserver::subscribeTo( PlaylistPtr::staticCast( playlist ) );
0142             playlist->triggerTrackLoad(); // playlist track loading is on demand.
0143             // will trigger tracksLoaded() which in turn calls processNextResultUrl(),
0144             // therefore we shouldn't call trigger processNextResultUrl() here:
0145             return;
0146         }
0147         else
0148             warning() << __PRETTY_FUNCTION__ << "cannot load playlist" << resultUrl;
0149     }
0150     else if( MetaFile::Track::isTrack( resultUrl ) )
0151     {
0152         MetaProxy::TrackPtr proxyTrack( new MetaProxy::Track( resultUrl ) );
0153         proxyTrack->setTitle( resultUrl.fileName() ); // set temporary name
0154         Meta::TrackPtr track( proxyTrack.data() );
0155         m_tracks << Meta::TrackPtr( track );
0156 
0157         if( m_flags.testFlag( FullMetadataRequired ) && !proxyTrack->isResolved() )
0158         {
0159             m_unresolvedTracks.insert( track );
0160             Observer::subscribeTo( track );
0161         }
0162     }
0163     else
0164         warning() << __PRETTY_FUNCTION__ << resultUrl
0165                   << "is neither a playlist or a track, skipping";
0166 
0167                   QTimer::singleShot( 0, this, &TrackLoader::processNextResultUrl );
0168 }
0169 
0170 void
0171 TrackLoader::tracksLoaded( Playlists::PlaylistPtr playlist )
0172 {
0173     // this method needs to be thread-safe!
0174 
0175     // some playlists used to Q_EMIT tracksLoaded() in ->tracks(), prevent infinite
0176     // recursion by unsubscribing early
0177     PlaylistObserver::unsubscribeFrom( playlist );
0178 
0179     // accessing m_tracks is thread-safe as nothing else is happening in this class in
0180     // the main thread while we are waiting for tracksLoaded() to trigger:
0181     Meta::TrackList tracks = playlist->tracks();
0182     if( m_flags.testFlag( FullMetadataRequired ) )
0183     {
0184         foreach( const Meta::TrackPtr &track, tracks )
0185         {
0186             MetaProxy::TrackPtr proxyTrack = MetaProxy::TrackPtr::dynamicCast( track );
0187             if( !proxyTrack )
0188             {
0189                 debug() << __PRETTY_FUNCTION__ << "strange, playlist" << playlist->name()
0190                         << "doesn't use MetaProxy::Tracks";
0191                 continue;
0192             }
0193             if( !proxyTrack->isResolved() )
0194             {
0195                 m_unresolvedTracks.insert( track );
0196                 Observer::subscribeTo( track );
0197             }
0198         }
0199     }
0200 
0201     static const QSet<QString> remoteProtocols = QSet<QString>()
0202             << "http" << "https" << "mms" << "smb"; // consider unifying with CollectionManager::trackForUrl()
0203     if( m_flags.testFlag( RemotePlaylistsAreStreams ) && tracks.count() > 1
0204         && remoteProtocols.contains( playlist->uidUrl().scheme() ) )
0205     {
0206         m_tracks << Meta::TrackPtr( new Meta::MultiTrack( playlist ) );
0207     }
0208     else
0209         m_tracks << tracks;
0210 
0211     // this also ensures that processNextResultUrl() will resume in the main thread
0212     QTimer::singleShot( 0, this, &TrackLoader::processNextResultUrl );
0213 }
0214 
0215 void
0216 TrackLoader::metadataChanged( const Meta::TrackPtr &track )
0217 {
0218     // first metadataChanged() from a MetaProxy::Track means that it has found the real track
0219     bool isEmpty;
0220     {
0221         QMutexLocker locker( &m_unresolvedTracksMutex );
0222         m_unresolvedTracks.remove( track );
0223         isEmpty = m_unresolvedTracks.isEmpty();
0224     }
0225 
0226     Observer::unsubscribeFrom( track );
0227     if( m_status == MayFinish && isEmpty )
0228         QTimer::singleShot( 0, this, &TrackLoader::finish );
0229 }
0230 
0231 void
0232 TrackLoader::mayFinish()
0233 {
0234     m_status = MayFinish;
0235     bool isEmpty;
0236     {
0237         QMutexLocker locker( &m_unresolvedTracksMutex );
0238         isEmpty = m_unresolvedTracks.isEmpty();
0239     }
0240     if( isEmpty )
0241     {
0242         finish();
0243         return;
0244     }
0245 
0246     // we must wait for tracks to resolve, but with a timeout
0247     QTimer::singleShot( m_timeout, this, &TrackLoader::finish );
0248 }
0249 
0250 void
0251 TrackLoader::finish()
0252 {
0253     // prevent double Q_EMIT of finished(), race between singleshot QTimers from mayFinish()
0254     // and metadataChanged()
0255     if( m_status != MayFinish )
0256         return;
0257 
0258     m_status = Finished;
0259     Q_EMIT finished( m_tracks );
0260     deleteLater();
0261 }
0262 
0263 bool
0264 TrackLoader::directorySensitiveLessThan( const QUrl &left, const QUrl &right )
0265 {
0266     QString leftDir = left.adjusted(QUrl::RemoveFilename).path();
0267     QString rightDir = right.adjusted(QUrl::RemoveFilename).path();
0268 
0269     // filter out tracks from same directories:
0270     if( leftDir == rightDir )
0271         return QString::localeAwareCompare( left.fileName(), right.fileName() ) < 0;
0272 
0273     // left is "/a/b/c/", right is "/a/b/"
0274     if( leftDir.startsWith( rightDir ) )
0275         return true; // we sort directories above files
0276     // left is "/a/b/", right is "/a/b/c/"
0277     if( rightDir.startsWith( leftDir ) )
0278         return false;
0279 
0280     return QString::localeAwareCompare( leftDir, rightDir ) < 0;
0281 }