1 /****************************************************************************************
2 * Copyright (c) 2010 Nikhil Marathe <nsm.nikhil@gmail.com> *
3 * *
4 * This program is free software; you can redistribute it and/or modify it under *
5 * the terms of the GNU General Public License as published by the Free Software *
6 * Foundation; either version 2 of the License, or (at your option) any later *
7 * version. *
8 * *
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
11 * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
12 * *
13 * You should have received a copy of the GNU General Public License along with *
14 * this program. If not, see <http://www.gnu.org/licenses/>. *
15 ****************************************************************************************/
16
17 #define DEBUG_PREFIX "UpnpQueryMaker"
18
19 #include "UpnpQueryMaker.h"
20
21 #include "upnptypes.h"
22
23 #include <QUrlQuery>
24
25 #include <KIO/Scheduler>
26 #include <KIO/UDSEntry>
27
28 #include "core/support/Debug.h"
29 #include "UpnpSearchCollection.h"
30 #include "UpnpQueryMakerInternal.h"
31 #include "UpnpMeta.h"
32 #include "UpnpCache.h"
33
34 namespace Collections {
35
UpnpQueryMaker(UpnpSearchCollection * collection)36 UpnpQueryMaker::UpnpQueryMaker( UpnpSearchCollection *collection )
37 : QueryMaker()
38 , m_collection( collection )
39 , m_internalQM( new UpnpQueryMakerInternal( collection ) )
40 {
41 reset();
42 connect( m_internalQM, &UpnpQueryMakerInternal::done, this, &UpnpQueryMaker::slotDone );
43
44 connect( m_internalQM, &UpnpQueryMakerInternal::newTracksReady,
45 this, &UpnpQueryMaker::handleTracks );
46 connect( m_internalQM, &UpnpQueryMakerInternal::newArtistsReady,
47 this, &UpnpQueryMaker::handleArtists );
48 connect( m_internalQM, &UpnpQueryMakerInternal::newAlbumsReady,
49 this, &UpnpQueryMaker::handleAlbums );
50 // connect( m_internalQM, &UpnpQueryMakerInternal::newResultReady,
51 // this, &UpnpQueryMaker::handleCustom );
52 }
53
~UpnpQueryMaker()54 UpnpQueryMaker::~UpnpQueryMaker()
55 {
56 m_internalQM->deleteLater();
57 }
58
59
reset()60 QueryMaker* UpnpQueryMaker::reset()
61 {
62 // TODO kill all jobs here too
63 m_queryType = None;
64 m_albumMode = AllAlbums;
65 m_query.reset();
66 m_jobCount = 0;
67
68 m_numericFilters.clear();
69 m_internalQM->reset();
70
71 // the Amarok Collection Model expects at least one entry
72 // otherwise it will harass us continuously for more entries.
73 // of course due to the poor quality of UPnP servers I've
74 // had experience with :P, some may not have sub-results
75 // for something ( they may have a track with an artist, but
76 // not be able to give any album for it )
77 m_noResults = true;
78 return this;
79 }
80
run()81 void UpnpQueryMaker::run()
82 {
83 DEBUG_BLOCK
84
85 QUrl baseUrl( m_collection->collectionId() );
86 QUrlQuery query( baseUrl );
87 query.addQueryItem( "search", "1" );
88 baseUrl.setQuery( query );
89
90 if( m_queryType == Custom ) {
91 switch( m_returnFunction ) {
92 case Count:
93 {
94 m_query.reset();
95 m_query.setType( "( upnp:class derivedfrom \"object.item.audioItem\" )" );
96 QUrlQuery query( baseUrl );
97 query.addQueryItem( "getCount", "1" );
98 baseUrl.setQuery( query );
99 break;
100 }
101 case Sum:
102 case Max:
103 case Min:
104 break;
105 }
106 }
107 // we don't deal with compilations
108 else if( m_queryType == Album && m_albumMode == OnlyCompilations ) {
109 // we don't support any other attribute
110 Q_EMIT newTracksReady( Meta::TrackList() );
111 Q_EMIT newArtistsReady( Meta::ArtistList() );
112 Q_EMIT newAlbumsReady( Meta::AlbumList() );
113 Q_EMIT newGenresReady( Meta::GenreList() );
114 Q_EMIT newComposersReady( Meta::ComposerList() );
115 Q_EMIT newYearsReady( Meta::YearList() );
116 Q_EMIT newResultReady( QStringList() );
117 Q_EMIT newLabelsReady( Meta::LabelList() );
118 Q_EMIT queryDone();
119 return;
120 }
121
122 QStringList queryList;
123 if( m_query.hasMatchFilter() || !m_numericFilters.empty() ) {
124 queryList = m_query.queries();
125 }
126 else {
127 switch( m_queryType ) {
128 case Artist:
129 debug() << this << "Query type Artist";
130 queryList << "( upnp:class derivedfrom \"object.container.person.musicArtist\" )";
131 break;
132 case Album:
133 debug() << this << "Query type Album";
134 queryList << "( upnp:class derivedfrom \"object.container.album.musicAlbum\" )";
135 break;
136 case Track:
137 debug() << this << "Query type Track";
138 queryList << "( upnp:class derivedfrom \"object.item.audioItem\" )";
139 break;
140 case Genre:
141 debug() << this << "Query type Genre";
142 queryList << "( upnp:class derivedfrom \"object.container.genre.musicGenre\" )";
143 break;
144 case Custom:
145 debug() << this << "Query type Custom";
146 queryList << "( upnp:class derivedfrom \"object.item.audioItem\" )";
147 break;
148 default:
149 debug() << this << "Default case: Query type";
150 // we don't support any other attribute
151 Q_EMIT newTracksReady( Meta::TrackList() );
152 Q_EMIT newArtistsReady( Meta::ArtistList() );
153 Q_EMIT newAlbumsReady( Meta::AlbumList() );
154 Q_EMIT newGenresReady( Meta::GenreList() );
155 Q_EMIT newComposersReady( Meta::ComposerList() );
156 Q_EMIT newYearsReady( Meta::YearList() );
157 Q_EMIT newResultReady( QStringList() );
158 Q_EMIT newLabelsReady( Meta::LabelList() );
159 Q_EMIT queryDone();
160 return;
161 }
162 }
163
164 // and experiment in using the filter only for the query
165 // and checking the returned upnp:class
166 // based on your query types.
167 for( int i = 0; i < queryList.length() ; i++ ) {
168 if( queryList[i].isEmpty() )
169 continue;
170
171 QUrl url( baseUrl );
172 QUrlQuery query( url );
173 query.addQueryItem( "query", queryList[i] );
174 url.setQuery( query );
175
176 debug() << this << "Running query" << url;
177 m_internalQM->runQuery( url );
178 }
179 }
180
abortQuery()181 void UpnpQueryMaker::abortQuery()
182 {
183 DEBUG_BLOCK
184 Q_ASSERT( false );
185 // TODO implement this to kill job
186 }
187
setQueryType(QueryType type)188 QueryMaker* UpnpQueryMaker::setQueryType( QueryType type )
189 {
190 DEBUG_BLOCK
191 // TODO allow all, based on search capabilities
192 // which should be passed on by the factory
193 m_queryType = type;
194 m_query.setType( "( upnp:class derivedfrom \"object.item.audioItem\" )" );
195 m_internalQM->setQueryType( type );
196
197 return this;
198 }
199
addReturnValue(qint64 value)200 QueryMaker* UpnpQueryMaker::addReturnValue( qint64 value )
201 {
202 DEBUG_BLOCK
203 debug() << this << "Add return value" << value;
204 m_returnValue = value;
205 return this;
206 }
207
addReturnFunction(ReturnFunction function,qint64 value)208 QueryMaker* UpnpQueryMaker::addReturnFunction( ReturnFunction function, qint64 value )
209 {
210 DEBUG_BLOCK
211 Q_UNUSED( function )
212 debug() << this << "Return function with value" << value;
213 m_returnFunction = function;
214 m_returnValue = value;
215 return this;
216 }
217
orderBy(qint64 value,bool descending)218 QueryMaker* UpnpQueryMaker::orderBy( qint64 value, bool descending )
219 {
220 DEBUG_BLOCK
221 debug() << this << "Order by " << value << "Descending?" << descending;
222 return this;
223 }
224
addMatch(const Meta::TrackPtr & track)225 QueryMaker* UpnpQueryMaker::addMatch( const Meta::TrackPtr &track )
226 {
227 DEBUG_BLOCK
228 debug() << this << "Adding track match" << track->name();
229 // TODO: CHECK query type before searching by dc:title?
230 m_query.addMatch( "( dc:title = \"" + track->name() + "\" )" );
231 return this;
232 }
233
addMatch(const Meta::ArtistPtr & artist,QueryMaker::ArtistMatchBehaviour behaviour)234 QueryMaker* UpnpQueryMaker::addMatch( const Meta::ArtistPtr &artist, QueryMaker::ArtistMatchBehaviour behaviour )
235 {
236 DEBUG_BLOCK
237 Q_UNUSED( behaviour ); // TODO: does UPnP tell between track and album artists?
238 debug() << this << "Adding artist match" << artist->name();
239 m_query.addMatch( "( upnp:artist = \"" + artist->name() + "\" )" );
240 return this;
241 }
242
addMatch(const Meta::AlbumPtr & album)243 QueryMaker* UpnpQueryMaker::addMatch( const Meta::AlbumPtr &album )
244 {
245 DEBUG_BLOCK
246 debug() << this << "Adding album match" << album->name();
247 m_query.addMatch( "( upnp:album = \"" + album->name() + "\" )" );
248 return this;
249 }
250
addMatch(const Meta::ComposerPtr & composer)251 QueryMaker* UpnpQueryMaker::addMatch( const Meta::ComposerPtr &composer )
252 {
253 DEBUG_BLOCK
254 debug() << this << "Adding composer match" << composer->name();
255 // NOTE unsupported
256 return this;
257 }
258
addMatch(const Meta::GenrePtr & genre)259 QueryMaker* UpnpQueryMaker::addMatch( const Meta::GenrePtr &genre )
260 {
261 DEBUG_BLOCK
262 debug() << this << "Adding genre match" << genre->name();
263 m_query.addMatch( "( upnp:genre = \"" + genre->name() + "\" )" );
264 return this;
265 }
266
addMatch(const Meta::YearPtr & year)267 QueryMaker* UpnpQueryMaker::addMatch( const Meta::YearPtr &year )
268 {
269 DEBUG_BLOCK
270 debug() << this << "Adding year match" << year->name();
271 // TODO
272 return this;
273 }
274
addMatch(const Meta::LabelPtr & label)275 QueryMaker* UpnpQueryMaker::addMatch( const Meta::LabelPtr &label )
276 {
277 DEBUG_BLOCK
278 debug() << this << "Adding label match" << label->name();
279 // NOTE how?
280 return this;
281 }
282
addFilter(qint64 value,const QString & filter,bool matchBegin,bool matchEnd)283 QueryMaker* UpnpQueryMaker::addFilter( qint64 value, const QString &filter, bool matchBegin, bool matchEnd )
284 {
285 DEBUG_BLOCK
286 debug() << this << "Adding filter" << value << filter << matchBegin << matchEnd;
287
288 // theoretically this should be '=' I think and set to contains below if required
289 QString cmpOp = "contains";
290 //TODO should we add filters ourselves
291 // eg. we always query for audioItems, but how do we decide
292 // whether to add a dc:title filter or others.
293 // for example, for the artist list
294 // our query should be like ( pseudocode )
295 // ( upnp:class = audioItem ) and ( dc:title contains "filter" )
296 // OR
297 // ( upnp:class = audioItem ) and ( upnp:artist contains "filter" );
298 // ...
299 // so who adds the second query?
300 QString property = propertyForValue( value );
301 if( property.isNull() )
302 return this;
303
304 if( matchBegin || matchEnd )
305 cmpOp = "contains";
306
307 QString filterString = "( " + property + " " + cmpOp + " \"" + filter + "\" ) ";
308 m_query.addFilter( filterString );
309 return this;
310 }
311
excludeFilter(qint64 value,const QString & filter,bool matchBegin,bool matchEnd)312 QueryMaker* UpnpQueryMaker::excludeFilter( qint64 value, const QString &filter, bool matchBegin, bool matchEnd )
313 {
314 DEBUG_BLOCK
315 debug() << this << "Excluding filter" << value << filter << matchBegin << matchEnd;
316 QString cmpOp = "!=";
317 QString property = propertyForValue( value );
318 if( property.isNull() )
319 return this;
320
321 if( matchBegin || matchEnd )
322 cmpOp = "doesNotContain";
323
324 QString filterString = "( " + property + " " + cmpOp + " \"" + filter + "\" ) ";
325 m_query.addFilter( filterString );
326 return this;
327 }
328
addNumberFilter(qint64 value,qint64 filter,NumberComparison compare)329 QueryMaker* UpnpQueryMaker::addNumberFilter( qint64 value, qint64 filter, NumberComparison compare )
330 {
331 DEBUG_BLOCK
332 debug() << this << "Adding number filter" << value << filter << compare;
333 NumericFilter f = { value, filter, compare };
334 m_numericFilters << f;
335 return this;
336 }
337
excludeNumberFilter(qint64 value,qint64 filter,NumberComparison compare)338 QueryMaker* UpnpQueryMaker::excludeNumberFilter( qint64 value, qint64 filter, NumberComparison compare )
339 {
340 DEBUG_BLOCK
341 debug() << this << "Excluding number filter" << value << filter << compare;
342 return this;
343 }
344
limitMaxResultSize(int size)345 QueryMaker* UpnpQueryMaker::limitMaxResultSize( int size )
346 {
347 DEBUG_BLOCK
348 debug() << this << "Limit max results to" << size;
349 return this;
350 }
351
setAlbumQueryMode(AlbumQueryMode mode)352 QueryMaker* UpnpQueryMaker::setAlbumQueryMode( AlbumQueryMode mode )
353 {
354 DEBUG_BLOCK
355 debug() << this << "Set album query mode" << mode;
356 m_albumMode = mode;
357 return this;
358 }
359
setLabelQueryMode(LabelQueryMode mode)360 QueryMaker* UpnpQueryMaker::setLabelQueryMode( LabelQueryMode mode )
361 {
362 DEBUG_BLOCK
363 debug() << this << "Set label query mode" << mode;
364 return this;
365 }
366
beginAnd()367 QueryMaker* UpnpQueryMaker::beginAnd()
368 {
369 DEBUG_BLOCK
370 m_query.beginAnd();
371 return this;
372 }
373
beginOr()374 QueryMaker* UpnpQueryMaker::beginOr()
375 {
376 DEBUG_BLOCK
377 m_query.beginOr();
378 return this;
379 }
380
endAndOr()381 QueryMaker* UpnpQueryMaker::endAndOr()
382 {
383 DEBUG_BLOCK
384 debug() << this << "End AND/OR";
385 m_query.endAndOr();
386 return this;
387 }
388
setAutoDelete(bool autoDelete)389 QueryMaker* UpnpQueryMaker::setAutoDelete( bool autoDelete )
390 {
391 DEBUG_BLOCK
392 debug() << this << "Auto delete" << autoDelete;
393 return this;
394 }
395
validFilterMask()396 int UpnpQueryMaker::validFilterMask()
397 {
398 int mask = 0;
399 QStringList caps = m_collection->searchCapabilities();
400 if( caps.contains( "dc:title" ) )
401 mask |= TitleFilter;
402 if( caps.contains( "upnp:album" ) )
403 mask |= AlbumFilter;
404 if( caps.contains( "upnp:artist" ) )
405 mask |= ArtistFilter;
406 if( caps.contains( "upnp:genre" ) )
407 mask |= GenreFilter;
408 return mask;
409 }
410
handleArtists(const Meta::ArtistList & list)411 void UpnpQueryMaker::handleArtists( const Meta::ArtistList &list )
412 {
413 // TODO Post filtering
414 Q_EMIT newArtistsReady( list );
415 }
416
handleAlbums(const Meta::AlbumList & list)417 void UpnpQueryMaker::handleAlbums( const Meta::AlbumList &list )
418 {
419 // TODO Post filtering
420 Q_EMIT newAlbumsReady( list );
421 }
422
handleTracks(const Meta::TrackList & list)423 void UpnpQueryMaker::handleTracks( const Meta::TrackList &list )
424 {
425 // TODO Post filtering
426 Q_EMIT newTracksReady( list );
427 }
428
429 /*
430 void UpnpQueryMaker::handleCustom( const KIO::UDSEntryList& list )
431 {
432 if( m_returnFunction == Count )
433 {
434 {
435 Q_ASSERT( !list.empty() );
436 QString count = list.first().stringValue( KIO::UDSEntry::UDS_NAME );
437 m_collection->setProperty( "numberOfTracks", count.toUInt() );
438 Q_EMIT newResultReady( QStringList( count ) );
439 }
440 default:
441 debug() << "Custom result functions other than \"Count\" are not supported by UpnpQueryMaker";
442 }
443 }
444 */
445
slotDone()446 void UpnpQueryMaker::slotDone()
447 {
448 DEBUG_BLOCK
449 if( m_noResults ) {
450 debug() << "++++++++++++++++++++++++++++++++++++ NO RESULTS ++++++++++++++++++++++++";
451 // TODO proper data types not just DataPtr
452 Meta::DataList ret;
453 Meta::UpnpTrack *fake = new Meta::UpnpTrack( m_collection );
454 fake->setTitle( "No results" );
455 fake->setYear( Meta::UpnpYearPtr( new Meta::UpnpYear( 2010 ) ) );
456 Meta::DataPtr ptr( fake );
457 ret << ptr;
458 //Q_EMIT newResultReady( ret );
459 }
460
461 switch( m_queryType ) {
462 case Artist:
463 {
464 Meta::ArtistList list;
465 foreach( Meta::DataPtr ptr, m_cacheEntries )
466 list << Meta::ArtistPtr::staticCast( ptr );
467 Q_EMIT newArtistsReady( list );
468 break;
469 }
470
471 case Album:
472 {
473 Meta::AlbumList list;
474 foreach( Meta::DataPtr ptr, m_cacheEntries )
475 list << Meta::AlbumPtr::staticCast( ptr );
476 Q_EMIT newAlbumsReady( list );
477 break;
478 }
479
480 case Track:
481 {
482 Meta::TrackList list;
483 foreach( Meta::DataPtr ptr, m_cacheEntries )
484 list << Meta::TrackPtr::staticCast( ptr );
485 Q_EMIT newTracksReady( list );
486 break;
487 }
488 default:
489 {
490 debug() << "Query type not supported by UpnpQueryMaker";
491 }
492 }
493
494 debug() << "ALL JOBS DONE< TERMINATING THIS QM" << this;
495 Q_EMIT queryDone();
496 }
497
propertyForValue(qint64 value)498 QString UpnpQueryMaker::propertyForValue( qint64 value )
499 {
500 switch( value ) {
501 case Meta::valTitle:
502 return "dc:title";
503 case Meta::valArtist:
504 {
505 //if( m_queryType != Artist )
506 return "upnp:artist";
507 }
508 case Meta::valAlbum:
509 {
510 //if( m_queryType != Album )
511 return "upnp:album";
512 }
513 case Meta::valGenre:
514 return "upnp:genre";
515 break;
516 default:
517 debug() << "UNSUPPORTED QUERY TYPE" << value;
518 return QString();
519 }
520 }
521
postFilter(const KIO::UDSEntry & entry)522 bool UpnpQueryMaker::postFilter( const KIO::UDSEntry &entry )
523 {
524 //numeric filters
525 foreach( const NumericFilter &filter, m_numericFilters ) {
526 // should be set by the filter based on filter.type
527 qint64 aValue = 0;
528
529 switch( filter.type ) {
530 case Meta::valCreateDate:
531 {
532 // TODO might use UDSEntry::UDS_CREATION_TIME instead later
533 QString dateString = entry.stringValue( KIO::UPNP_DATE );
534 QDateTime time = QDateTime::fromString( dateString, Qt::ISODate );
535 if( !time.isValid() )
536 return false;
537 aValue = time.toSecsSinceEpoch();
538 debug() << "FILTER BY creation timestamp entry:" << aValue << "query:" << filter.value << "OP:" << filter.compare;
539 break;
540 }
541 }
542
543 if( ( filter.compare == Equals ) && ( filter.value != aValue ) )
544 return false;
545 else if( ( filter.compare == GreaterThan ) && ( filter.value >= aValue ) )
546 return false; // since only allow entries with aValue > filter.value
547 else if( ( filter.compare == LessThan ) && ( filter.value <= aValue ) )
548 return false;
549 }
550 return true;
551 }
552
553 } //namespace Collections
554