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