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