1 /****************************************************************************************
2  * Copyright (c) 2007-2008 Ian Monroe <ian@monroe.nu>                                   *
3  * Copyright (c) 2013 Matěj Laitl <matej@laitl.cz>                                      *
4  *                                                                                      *
5  * This program is free software; you can redistribute it and/or modify it under        *
6  * the terms of the GNU General Public License as published by the Free Software        *
7  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
8  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
9  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
10  * version 3 of the license.                                                            *
11  *                                                                                      *
12  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
13  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
14  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
15  *                                                                                      *
16  * You should have received a copy of the GNU General Public License along with         *
17  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
18  ****************************************************************************************/
19 
20 #include "TrackLoader.h"
21 
22 #include "core/playlists/PlaylistFormat.h"
23 #include "core/support/Debug.h"
24 #include "core-impl/meta/file/File.h"
25 #include "core-impl/meta/proxy/MetaProxy.h"
26 #include "core-impl/meta/multi/MultiTrack.h"
27 #include "core-impl/playlists/types/file/PlaylistFileSupport.h"
28 
29 #include <KIO/Job>
30 #include <KFileItem>
31 
32 #include <QFileInfo>
33 #include <QTimer>
34 
TrackLoader(Flags flags,int timeout)35 TrackLoader::TrackLoader( Flags flags, int timeout )
36     : m_status( LoadingTracks )
37     , m_flags( flags )
38     , m_timeout( timeout )
39 {
40 }
41 
~TrackLoader()42 TrackLoader::~TrackLoader()
43 {
44 }
45 
46 void
init(const QUrl & url)47 TrackLoader::init( const QUrl &url )
48 {
49     init( QList<QUrl>() << url );
50 }
51 
52 void
init(const QList<QUrl> & qurls)53 TrackLoader::init( const QList<QUrl> &qurls )
54 {
55     m_sourceUrls = qurls;
56     QTimer::singleShot( 0, this, &TrackLoader::processNextSourceUrl );
57 }
58 
59 void
init(const Playlists::PlaylistList & playlists)60 TrackLoader::init( const Playlists::PlaylistList &playlists )
61 {
62     m_resultPlaylists = playlists;
63     // no need to process source urls here, short-cut to result urls (just playlists)
64     QTimer::singleShot( 0, this, &TrackLoader::processNextResultUrl );
65 }
66 
67 void
processNextSourceUrl()68 TrackLoader::processNextSourceUrl()
69 {
70     if( m_sourceUrls.isEmpty() )
71     {
72         QTimer::singleShot( 0, this, &TrackLoader::processNextResultUrl );
73         return;
74     }
75 
76     QUrl sourceUrl = m_sourceUrls.takeFirst();
77     if( !sourceUrl.isValid() )
78     {
79         error() << "Url is invalid:" << sourceUrl;
80         QTimer::singleShot( 0, this, &TrackLoader::processNextSourceUrl );
81         return;
82     }
83     if( sourceUrl.isLocalFile() && QFileInfo( sourceUrl.toLocalFile() ).isDir() )
84     {
85         // KJobs delete themselves
86         KIO::ListJob *lister = KIO::listRecursive( sourceUrl );
87         connect( lister, &KIO::ListJob::result, this, &TrackLoader::processNextSourceUrl );
88         connect( lister, &KIO::ListJob::entries, this, &TrackLoader::directoryListResults );
89         return;
90     }
91     else
92         m_resultUrls.append( sourceUrl );
93 
94     QTimer::singleShot( 0, this, &TrackLoader::processNextSourceUrl );
95 }
96 
97 void
directoryListResults(KIO::Job * job,const KIO::UDSEntryList & list)98 TrackLoader::directoryListResults( KIO::Job *job, const KIO::UDSEntryList &list )
99 {
100     //dfaure says that job->redirectionUrl().isValid() ? job->redirectionUrl() : job->url(); might be needed
101     //but to wait until an issue is actually found, since it might take more work
102     const QUrl dir = static_cast<KIO::SimpleJob *>( job )->url();
103     foreach( const KIO::UDSEntry &entry, list )
104     {
105         KFileItem item( entry, dir, true, true );
106         QUrl url = item.url();
107         if( MetaFile::Track::isTrack( url ) )
108         {
109             auto insertIter = std::upper_bound( m_resultUrls.begin(), m_resultUrls.end(), url, directorySensitiveLessThan );
110             m_resultUrls.insert( insertIter, url );
111         }
112     }
113 }
114 
115 void
processNextResultUrl()116 TrackLoader::processNextResultUrl()
117 {
118     using namespace Playlists;
119     if( !m_resultPlaylists.isEmpty() )
120     {
121         PlaylistPtr playlist = m_resultPlaylists.takeFirst();
122         PlaylistObserver::subscribeTo( playlist );
123         playlist->triggerTrackLoad(); // playlist track loading is on demand.
124         // will trigger tracksLoaded() which in turn calls processNextResultUrl(),
125         // therefore we shouldn't call trigger processNextResultUrl() here:
126         return;
127     }
128 
129     if( m_resultUrls.isEmpty() )
130     {
131         mayFinish();
132         return;
133     }
134 
135     QUrl resultUrl = m_resultUrls.takeFirst();
136     if( isPlaylist( resultUrl ) )
137     {
138         PlaylistFilePtr playlist = loadPlaylistFile( resultUrl );
139         if( playlist )
140         {
141             PlaylistObserver::subscribeTo( PlaylistPtr::staticCast( playlist ) );
142             playlist->triggerTrackLoad(); // playlist track loading is on demand.
143             // will trigger tracksLoaded() which in turn calls processNextResultUrl(),
144             // therefore we shouldn't call trigger processNextResultUrl() here:
145             return;
146         }
147         else
148             warning() << __PRETTY_FUNCTION__ << "cannot load playlist" << resultUrl;
149     }
150     else if( MetaFile::Track::isTrack( resultUrl ) )
151     {
152         MetaProxy::TrackPtr proxyTrack( new MetaProxy::Track( resultUrl ) );
153         proxyTrack->setTitle( resultUrl.fileName() ); // set temporary name
154         Meta::TrackPtr track( proxyTrack.data() );
155         m_tracks << Meta::TrackPtr( track );
156 
157         if( m_flags.testFlag( FullMetadataRequired ) && !proxyTrack->isResolved() )
158         {
159             m_unresolvedTracks.insert( track );
160             Observer::subscribeTo( track );
161         }
162     }
163     else
164         warning() << __PRETTY_FUNCTION__ << resultUrl
165                   << "is neither a playlist or a track, skipping";
166 
167                   QTimer::singleShot( 0, this, &TrackLoader::processNextResultUrl );
168 }
169 
170 void
tracksLoaded(Playlists::PlaylistPtr playlist)171 TrackLoader::tracksLoaded( Playlists::PlaylistPtr playlist )
172 {
173     // this method needs to be thread-safe!
174 
175     // some playlists used to Q_EMIT tracksLoaded() in ->tracks(), prevent infinite
176     // recursion by unsubscribing early
177     PlaylistObserver::unsubscribeFrom( playlist );
178 
179     // accessing m_tracks is thread-safe as nothing else is happening in this class in
180     // the main thread while we are waiting for tracksLoaded() to trigger:
181     Meta::TrackList tracks = playlist->tracks();
182     if( m_flags.testFlag( FullMetadataRequired ) )
183     {
184         foreach( const Meta::TrackPtr &track, tracks )
185         {
186             MetaProxy::TrackPtr proxyTrack = MetaProxy::TrackPtr::dynamicCast( track );
187             if( !proxyTrack )
188             {
189                 debug() << __PRETTY_FUNCTION__ << "strange, playlist" << playlist->name()
190                         << "doesn't use MetaProxy::Tracks";
191                 continue;
192             }
193             if( !proxyTrack->isResolved() )
194             {
195                 m_unresolvedTracks.insert( track );
196                 Observer::subscribeTo( track );
197             }
198         }
199     }
200 
201     static const QSet<QString> remoteProtocols = QSet<QString>()
202             << "http" << "https" << "mms" << "smb"; // consider unifying with CollectionManager::trackForUrl()
203     if( m_flags.testFlag( RemotePlaylistsAreStreams ) && tracks.count() > 1
204         && remoteProtocols.contains( playlist->uidUrl().scheme() ) )
205     {
206         m_tracks << Meta::TrackPtr( new Meta::MultiTrack( playlist ) );
207     }
208     else
209         m_tracks << tracks;
210 
211     // this also ensures that processNextResultUrl() will resume in the main thread
212     QTimer::singleShot( 0, this, &TrackLoader::processNextResultUrl );
213 }
214 
215 void
metadataChanged(const Meta::TrackPtr & track)216 TrackLoader::metadataChanged( const Meta::TrackPtr &track )
217 {
218     // first metadataChanged() from a MetaProxy::Track means that it has found the real track
219     bool isEmpty;
220     {
221         QMutexLocker locker( &m_unresolvedTracksMutex );
222         m_unresolvedTracks.remove( track );
223         isEmpty = m_unresolvedTracks.isEmpty();
224     }
225 
226     Observer::unsubscribeFrom( track );
227     if( m_status == MayFinish && isEmpty )
228         QTimer::singleShot( 0, this, &TrackLoader::finish );
229 }
230 
231 void
mayFinish()232 TrackLoader::mayFinish()
233 {
234     m_status = MayFinish;
235     bool isEmpty;
236     {
237         QMutexLocker locker( &m_unresolvedTracksMutex );
238         isEmpty = m_unresolvedTracks.isEmpty();
239     }
240     if( isEmpty )
241     {
242         finish();
243         return;
244     }
245 
246     // we must wait for tracks to resolve, but with a timeout
247     QTimer::singleShot( m_timeout, this, &TrackLoader::finish );
248 }
249 
250 void
finish()251 TrackLoader::finish()
252 {
253     // prevent double Q_EMIT of finished(), race between singleshot QTimers from mayFinish()
254     // and metadataChanged()
255     if( m_status != MayFinish )
256         return;
257 
258     m_status = Finished;
259     Q_EMIT finished( m_tracks );
260     deleteLater();
261 }
262 
263 bool
directorySensitiveLessThan(const QUrl & left,const QUrl & right)264 TrackLoader::directorySensitiveLessThan( const QUrl &left, const QUrl &right )
265 {
266     QString leftDir = left.adjusted(QUrl::RemoveFilename).path();
267     QString rightDir = right.adjusted(QUrl::RemoveFilename).path();
268 
269     // filter out tracks from same directories:
270     if( leftDir == rightDir )
271         return QString::localeAwareCompare( left.fileName(), right.fileName() ) < 0;
272 
273     // left is "/a/b/c/", right is "/a/b/"
274     if( leftDir.startsWith( rightDir ) )
275         return true; // we sort directories above files
276     // left is "/a/b/", right is "/a/b/c/"
277     if( rightDir.startsWith( leftDir ) )
278         return false;
279 
280     return QString::localeAwareCompare( leftDir, rightDir ) < 0;
281 }
282