MythTV  master
upnpcdsmusic.cpp
Go to the documentation of this file.
1 // Program Name: upnpcdsmusic.cpp
3 //
4 // Purpose - uPnp Content Directory Extension for Music
5 //
6 // Created By : David Blain Created On : Jan. 24, 2005
7 // Modified By : Modified On:
8 //
10 
11 #include <climits>
12 
13 #include <QFileInfo>
14 #include <QUrl>
15 #include <QUrlQuery>
16 
17 #include "storagegroup.h"
18 #include "upnpcdsmusic.h"
19 #include "httprequest.h"
20 #include "mythcorecontext.h"
21 #include "upnphelpers.h"
22 
59  : UPnpCDSExtension( "Music", "Music",
60  "object.item.audioItem.musicTrack" )
61 {
62  QString sServerIp = gCoreContext->GetBackendServerIP();
63  int sPort = gCoreContext->GetBackendStatusPort();
64  m_URIBase.setScheme("http");
65  m_URIBase.setHost(sServerIp);
66  m_URIBase.setPort(sPort);
67 
68  // ShortCuts
69  m_shortcuts.insert(UPnPShortcutFeature::MUSIC, "Music");
70  m_shortcuts.insert(UPnPShortcutFeature::MUSIC_ALL, "Music/Track");
71  m_shortcuts.insert(UPnPShortcutFeature::MUSIC_ALBUMS, "Music/Album");
72  m_shortcuts.insert(UPnPShortcutFeature::MUSIC_ARTISTS, "Music/Artist");
73  m_shortcuts.insert(UPnPShortcutFeature::MUSIC_GENRES, "Music/Genre");
74 }
75 
77 //
79 
81 {
82  if (m_pRoot)
83  return;
84 
86  m_sName,
87  "0");
88 
89  CDSObject* pContainer;
90  QString containerId = m_sExtensionId + "/%1";
91 
92  // HACK: I'm not entirely happy with this solution, but it's at least
93  // tidier than passing through half a dozen extra args to Load[Foo]
94  // or having yet more methods just to load the counts
95  UPnpCDSRequest *pRequest = new UPnpCDSRequest();
96  pRequest->m_nRequestedCount = 0; // We don't want to load any results, we just want the TotalCount
98  IDTokenMap tokens;
99  // END HACK
100 
101  // -----------------------------------------------------------------------
102  // All Tracks
103  // -----------------------------------------------------------------------
104  pContainer = CDSObject::CreateContainer ( containerId.arg("Track"),
105  QObject::tr("All Tracks"),
106  m_sExtensionId, // Parent Id
107  nullptr );
108  // HACK
109  LoadTracks(pRequest, pResult, tokens);
110  pContainer->SetChildCount(pResult->m_nTotalMatches);
111  pContainer->SetChildContainerCount(0);
112  // END HACK
113  m_pRoot->AddChild(pContainer);
114 
115  // -----------------------------------------------------------------------
116  // By Artist
117  // -----------------------------------------------------------------------
118  pContainer = CDSObject::CreateContainer ( containerId.arg("Artist"),
119  QObject::tr("Artist"),
120  m_sExtensionId, // Parent Id
121  nullptr );
122  // HACK
123  LoadArtists(pRequest, pResult, tokens);
124  pContainer->SetChildCount(pResult->m_nTotalMatches);
125  pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
126  // END HACK
127  m_pRoot->AddChild(pContainer);
128 
129  // -----------------------------------------------------------------------
130  // By Album
131  // -----------------------------------------------------------------------
132  pContainer = CDSObject::CreateContainer ( containerId.arg("Album"),
133  QObject::tr("Album"),
134  m_sExtensionId, // Parent Id
135  nullptr );
136  // HACK
137  LoadAlbums(pRequest, pResult, tokens);
138  pContainer->SetChildCount(pResult->m_nTotalMatches);
139  pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
140  // END HACK
141  m_pRoot->AddChild(pContainer);
142 
143  // -----------------------------------------------------------------------
144  // By Genre
145  // -----------------------------------------------------------------------
146  pContainer = CDSObject::CreateContainer ( containerId.arg("Genre"),
147  QObject::tr("Genre"),
148  m_sExtensionId, // Parent Id
149  nullptr );
150  // HACK
151  LoadGenres(pRequest, pResult, tokens);
152  pContainer->SetChildCount(pResult->m_nTotalMatches);
153  pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
154  // END HACK
155  m_pRoot->AddChild(pContainer);
156 
157  // -----------------------------------------------------------------------
158  // By Directory
159  // -----------------------------------------------------------------------
160 // pContainer = CDSObject::CreateStorageSystem ( containerId.arg("Directory"),
161 // QObject::tr("Directory"),
162 // m_sExtensionId, // Parent Id
163 // nullptr );
164 // // HACK
165 // LoadDirectories(pRequest, pResult, tokens);
166 // pContainer->SetChildCount(pResult->m_nTotalMatches);
167 // pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
168 // // END HACK
169 // m_pRoot->AddChild(pContainer);
170 
171  // -----------------------------------------------------------------------
172 
173  // HACK
174  delete pRequest;
175  delete pResult;
176  // END HACK
177 }
178 
180 //
182 
184 {
185  // ----------------------------------------------------------------------
186  // See if we need to modify the request for compatibility
187  // ----------------------------------------------------------------------
188 
189  // Xbox360 compatibility code.
190 
191 // if (pRequest->m_eClient == CDS_ClientXBox &&
192 // pRequest->m_sContainerID == "7")
193 // {
194 // pRequest->m_sObjectId = "Music";
195 //
196 // LOG(VB_UPNP, LOG_INFO,
197 // "UPnpCDSMusic::IsBrowseRequestForUs - Yes, ContainerId == 7");
198 //
199 // return true;
200 // }
201 //
202 // if ((pRequest->m_sObjectId.isEmpty()) &&
203 // (!pRequest->m_sContainerID.isEmpty()))
204 // pRequest->m_sObjectId = pRequest->m_sContainerID;
205 
206  LOG(VB_UPNP, LOG_INFO,
207  "UPnpCDSMusic::IsBrowseRequestForUs - Not sure... Calling base class.");
208 
209  return UPnpCDSExtension::IsBrowseRequestForUs( pRequest );
210 }
211 
213 //
215 
217 {
218  // ----------------------------------------------------------------------
219  // See if we need to modify the request for compatibility
220  // ----------------------------------------------------------------------
221 
222  // XBox 360 compatibility code
223 
224 // if (pRequest->m_eClient == CDS_ClientXBox &&
225 // pRequest->m_sContainerID == "7")
226 // {
227 // pRequest->m_sObjectId = "Music/1";
228 // pRequest->m_sSearchCriteria = "object.container.album.musicAlbum";
229 // pRequest->m_sSearchList.append( pRequest->m_sSearchCriteria );
230 //
231 // LOG(VB_UPNP, LOG_INFO, "UPnpCDSMusic::IsSearchRequestForUs... Yes.");
232 //
233 // return true;
234 // }
235 //
236 // if (pRequest->m_sContainerID == "4")
237 // {
238 // pRequest->m_sObjectId = "Music";
239 // pRequest->m_sSearchCriteria = "object.item.audioItem.musicTrack";
240 // pRequest->m_sSearchList.append( pRequest->m_sSearchCriteria );
241 //
242 // LOG(VB_UPNP, LOG_INFO, "UPnpCDSMusic::IsSearchRequestForUs... Yes.");
243 //
244 // return true;
245 // }
246 //
247 // if ((pRequest->m_sObjectId.isEmpty()) &&
248 // (!pRequest->m_sContainerID.isEmpty()))
249 // pRequest->m_sObjectId = pRequest->m_sContainerID;
250 
251  LOG(VB_UPNP, LOG_INFO,
252  "UPnpCDSMusic::IsSearchRequestForUs.. Don't know, calling base class.");
253 
254  return UPnpCDSExtension::IsSearchRequestForUs( pRequest );
255 }
256 
258 //
260 
262  UPnpCDSExtensionResults* pResults,
263  IDTokenMap tokens, QString currentToken)
264 {
265  if (currentToken.isEmpty())
266  {
267  LOG(VB_GENERAL, LOG_ERR, QString("UPnpCDSMusic::LoadMetadata: Final "
268  "token missing from id: %1")
269  .arg(pRequest->m_sParentId));
270  return false;
271  }
272 
273  // Root or Root + 1
274  if (tokens[currentToken].isEmpty())
275  {
276  CDSObject *container = nullptr;
277 
278  if (pRequest->m_sObjectId == m_sExtensionId)
279  container = GetRoot();
280  else
281  container = GetRoot()->GetChild(pRequest->m_sObjectId);
282 
283  if (container)
284  {
285  pResults->Add(container);
286  pResults->m_nTotalMatches = 1;
287  return true;
288  }
289 
290  LOG(VB_GENERAL, LOG_ERR, QString("UPnpCDSMusic::LoadMetadata: Requested "
291  "object cannot be found: %1")
292  .arg(pRequest->m_sObjectId));
293  return false;
294  }
295  if (currentToken == "genre")
296  {
297  // Genre is presently a top tier node, since it doesn't appear
298  // below Artist/Album/etc we don't need to pass through
299  // the ids for filtering
300  return LoadGenres(pRequest, pResults, tokens);
301  }
302  if (currentToken == "artist")
303  {
304  return LoadArtists(pRequest, pResults, tokens);
305  }
306  if (currentToken == "album")
307  {
308  return LoadAlbums(pRequest, pResults, tokens);
309  }
310  if (currentToken == "track")
311  {
312  return LoadTracks(pRequest, pResults, tokens);
313  }
314 
315  LOG(VB_GENERAL, LOG_ERR,
316  QString("UPnpCDSMusic::LoadMetadata(): "
317  "Unhandled metadata request for '%1'.").arg(currentToken));
318  return false;
319 }
320 
322 //
324 
326  UPnpCDSExtensionResults* pResults,
327  IDTokenMap tokens, QString currentToken)
328 {
329  if (currentToken.isEmpty() || currentToken == m_sExtensionId.toLower())
330  {
331  // Root
332  pResults->Add(GetRoot()->GetChildren());
333  pResults->m_nTotalMatches = GetRoot()->GetChildCount();
334  return true;
335  }
336  if (currentToken == "track")
337  {
338  return LoadTracks(pRequest, pResults, tokens);
339  }
340  if (currentToken == "genre")
341  {
342  if (tokens["genre"].toInt() > 0)
343  return LoadArtists(pRequest, pResults, tokens);
344  return LoadGenres(pRequest, pResults, tokens);
345  }
346  if (currentToken == "artist")
347  {
348  if (tokens["artist"].toInt() > 0)
349  return LoadAlbums(pRequest, pResults, tokens);
350  return LoadArtists(pRequest, pResults, tokens);
351  }
352  if (currentToken == "album")
353  {
354  if (tokens["album"].toInt() > 0)
355  return LoadTracks(pRequest, pResults, tokens);
356  return LoadAlbums(pRequest, pResults, tokens);
357  }
358  LOG(VB_GENERAL, LOG_ERR,
359  QString("UPnpCDSMusic::LoadChildren(): "
360  "Unhandled metadata request for '%1'.").arg(currentToken));
361 
362  return false;
363 }
364 
366 //
368 
370 {
371  QUrl artURI = m_URIBase;
372  artURI.setPath("/Content/GetAlbumArt");
373  QUrlQuery artQuery;
374  artQuery.addQueryItem("Id", QString::number(nSongID));
375  artURI.setQuery(artQuery);
376 
377  QList<Property*> propList = pItem->GetProperties("albumArtURI");
378  if (propList.size() >= 4)
379  {
380  // Prefer JPEG over PNG here, although PNG is allowed DLNA requires JPEG
381  // and crucially the filesizes are smaller which speeds up loading times
382  // over the network
383 
384  // We MUST include the thumbnail size, but since some clients may use the
385  // first image they see and the thumbnail is tiny, instead return the
386  // medium first. The large could be very large, which is no good if the
387  // client is pulling images for an entire list at once!
388 
389  // Medium
390  Property *pProp = propList.at(0);
391  if (pProp)
392  {
393  // Must be no more than 1024x768
394  QUrl mediumURI = artURI;
395  QUrlQuery mediumQuery(mediumURI.query());
396  mediumQuery.addQueryItem("Width", "1024");
397  mediumQuery.addQueryItem("Height", "768");
398  mediumURI.setQuery(mediumQuery);
399  pProp->SetValue(mediumURI.toEncoded());
400  pProp->AddAttribute("dlna:profileID", "JPEG_MED");
401  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
402  }
403 
404  // Thumbnail
405  pProp = propList.at(1);
406  if (pProp)
407  {
408  // At least one albumArtURI must be a ThumbNail (TN) no larger
409  // than 160x160, and it must also be a jpeg
410  QUrl thumbURI = artURI;
411  QUrlQuery thumbQuery(thumbURI.query());
412  thumbQuery.addQueryItem("Width", "160");
413  thumbQuery.addQueryItem("Height", "160");
414  thumbURI.setQuery(thumbQuery);
415  pProp->SetValue(thumbURI.toEncoded());
416  pProp->AddAttribute("dlna:profileID", "JPEG_TN");
417  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
418  }
419 
420  // Small
421  pProp = propList.at(2);
422  if (pProp)
423  {
424  // Must be no more than 640x480
425  QUrl smallURI = artURI;
426  QUrlQuery smallQuery(smallURI.query());
427  smallQuery.addQueryItem("Width", "640");
428  smallQuery.addQueryItem("Height", "480");
429  smallURI.setQuery(smallQuery);
430  pProp->SetValue(smallURI.toEncoded());
431  pProp->AddAttribute("dlna:profileID", "JPEG_SM");
432  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
433  }
434 
435  // Large
436  pProp = propList.at(3);
437  if (pProp)
438  {
439  // Must be no more than 4096x4096 - for our purposes, just return
440  // a fullsize image
441  pProp->SetValue(artURI.toEncoded());
442  pProp->AddAttribute("dlna:profileID", "JPEG_LRG");
443  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
444  }
445  }
446  else
447  LOG(VB_GENERAL, LOG_ERR, QString("Unable to designate album artwork "
448  "for '%1' with class '%2' and id '%3'")
449  .arg(pItem->m_sId)
450  .arg(pItem->m_sClass)
451  .arg(nSongID));
452 }
453 
455 //
457 
459  UPnpCDSExtensionResults *pResults,
460  const IDTokenMap& tokens)
461 {
462  QString sRequestId = pRequest->m_sObjectId;
463 
464  uint16_t nCount = pRequest->m_nRequestedCount;
465  uint16_t nOffset = pRequest->m_nStartingIndex;
466 
467  // We must use a dedicated connection to get an acccurate value from
468  // FOUND_ROWS()
470 
471  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
472  "a.album_id, a.album_name, t.artist_name, a.year, "
473  "a.compilation, s.song_id, g.genre, "
474  "COUNT(a.album_id), w.albumart_id "
475  "FROM music_albums a "
476  "LEFT JOIN music_artists t ON a.artist_id=t.artist_id "
477  "LEFT JOIN music_songs s ON a.album_id=s.album_id "
478  "LEFT JOIN music_genres g ON s.genre_id=g.genre_id "
479  "LEFT JOIN music_albumart w ON s.song_id=w.song_id "
480  "%1 " // WHERE clauses
481  "GROUP BY a.album_id "
482  "ORDER BY a.album_name "
483  "LIMIT :OFFSET,:COUNT";
484 
485  QStringList clauses;
486  QString whereString = BuildWhereClause(clauses, tokens);
487 
488  query.prepare(sql.arg(whereString));
489 
490  BindValues(query, tokens);
491 
492  query.bindValue(":OFFSET", nOffset);
493  query.bindValue(":COUNT", nCount);
494 
495  if (!query.exec())
496  return false;
497 
498  while (query.next())
499  {
500  int nAlbumID = query.value(0).toInt();
501  QString sAlbumName = query.value(1).toString();
502  QString sArtist = query.value(2).toString();
503  QString sYear = query.value(3).toString();
504  bool bCompilation = query.value(4).toBool();
505  int nSongId = query.value(5).toInt(); // TODO: Allow artwork lookups by album ID
506  QString sGenre = query.value(6).toString();
507  int nTrackCount = query.value(7).toInt();
508  int nAlbumArtID = query.value(8).toInt();
509 
510  CDSObject* pContainer = CDSObject::CreateMusicAlbum( CreateIDString(sRequestId, "Album", nAlbumID),
511  sAlbumName,
512  pRequest->m_sParentId,
513  nullptr );
514  pContainer->SetPropValue("artist", sArtist);
515  pContainer->SetPropValue("date", sYear);
516  pContainer->SetPropValue("genre", sGenre);
517  pContainer->SetChildCount(nTrackCount);
518  pContainer->SetChildContainerCount(0);
519  if (bCompilation)
520  {
521  // Do nothing for now
522  }
523 
524  // Artwork
525 
526  if (nAlbumArtID > 0)
527  PopulateArtworkURIS(pContainer, nSongId);
528 
529  pResults->Add(pContainer);
530  pContainer->DecrRef();
531  }
532 
533  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
534  // at least the size of this result set
535  if (query.size() > 0)
536  pResults->m_nTotalMatches = query.size();
537 
538  // Fetch the total number of matches ignoring any LIMITs
539  query.prepare("SELECT FOUND_ROWS()");
540  if (query.exec() && query.next())
541  pResults->m_nTotalMatches = query.value(0).toUInt();
542 
543  return true;
544 }
545 
547 //
549 
551  UPnpCDSExtensionResults *pResults,
552  const IDTokenMap& tokens)
553 {
554  QString sRequestId = pRequest->m_sObjectId;
555 
556  uint16_t nCount = pRequest->m_nRequestedCount;
557  uint16_t nOffset = pRequest->m_nStartingIndex;
558 
559  // We must use a dedicated connection to get an accurate value from
560  // FOUND_ROWS()
562 
563  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
564  "t.artist_id, t.artist_name, CONCAT_WS(',', g.genre), "
565  "COUNT(DISTINCT a.album_id) "
566  "FROM music_artists t "
567  "LEFT JOIN music_albums a ON a.artist_id = t.artist_id "
568  "JOIN music_songs s ON t.artist_id = s.artist_id "
569  "LEFT JOIN music_genres g ON s.genre_id = g.genre_id "
570  "%1 " // WHERE clauses
571  "GROUP BY t.artist_id "
572  "ORDER BY t.artist_name "
573  "LIMIT :OFFSET,:COUNT";
574 
575 
576  QStringList clauses;
577  QString whereString = BuildWhereClause(clauses, tokens);
578 
579  query.prepare(sql.arg(whereString));
580 
581  BindValues(query, tokens);
582 
583  query.bindValue(":OFFSET", nOffset);
584  query.bindValue(":COUNT", nCount);
585 
586  if (!query.exec())
587  return false;
588 
589  while (query.next())
590  {
591  int nArtistId = query.value(0).toInt();
592  QString sArtistName = query.value(1).toString();
593  QStringList sGenres = query.value(2).toString().split(',');
594  int nAlbumCount = query.value(3).toInt();
595 
596  CDSObject* pContainer = CDSObject::CreateMusicArtist( CreateIDString(sRequestId, "Artist", nArtistId),
597  sArtistName,
598  pRequest->m_sParentId,
599  nullptr );
600 // TODO: Add SetPropValues option for multi-value properties
601 // QStringList::Iterator it;
602 // for (it = sGenres.begin(); it != sGenres.end(); ++it)
603 // {
604 // pContainer->SetPropValues("genre", sGenres);
605 // }
606  pContainer->SetChildCount(nAlbumCount);
607  pContainer->SetChildContainerCount(nAlbumCount);
608 
609  // Artwork
610  //PopulateArtistURIS(pContainer, nArtistId);
611 
612  pResults->Add(pContainer);
613  pContainer->DecrRef();
614  }
615 
616  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
617  // at least the size of this result set
618  if (query.size() > 0)
619  pResults->m_nTotalMatches = query.size();
620 
621  // Fetch the total number of matches ignoring any LIMITs
622  query.prepare("SELECT FOUND_ROWS()");
623  if (query.exec() && query.next())
624  pResults->m_nTotalMatches = query.value(0).toUInt();
625 
626  return true;
627 }
628 
630 //
632 
634  UPnpCDSExtensionResults *pResults,
635  const IDTokenMap& tokens )
636 {
637  QString sRequestId = pRequest->m_sObjectId;
638 
639  uint16_t nCount = pRequest->m_nRequestedCount;
640  uint16_t nOffset = pRequest->m_nStartingIndex;
641 
642  // We must use a dedicated connection to get an acccurate value from
643  // FOUND_ROWS()
645 
646  QString sql = "SELECT SQL_CALC_FOUND_ROWS g.genre_id, g.genre, "
647  "COUNT( DISTINCT t.artist_id ) "
648  "FROM music_genres g "
649  "LEFT JOIN music_songs s ON g.genre_id = s.genre_id "
650  "LEFT JOIN music_artists t ON t.artist_id = s.artist_id "
651  "%1 " // WHERE clauses
652  "GROUP BY g.genre_id "
653  "ORDER BY g.genre "
654  "LIMIT :OFFSET,:COUNT";
655 
656  QStringList clauses;
657  QString whereString = BuildWhereClause(clauses, tokens);
658 
659  query.prepare(sql.arg(whereString));
660 
661  BindValues(query, tokens);
662 
663  query.bindValue(":OFFSET", nOffset);
664  query.bindValue(":COUNT", nCount);
665 
666  if (!query.exec())
667  return false;
668 
669  while (query.next())
670  {
671  int nGenreId = query.value(0).toInt();
672  QString sGenreName = query.value(1).toString();
673  int nArtistCount = query.value(2).toInt();
674 
675  CDSObject* pContainer = CDSObject::CreateMusicGenre( CreateIDString(sRequestId, "Genre", nGenreId),
676  sGenreName,
677  pRequest->m_sParentId,
678  nullptr );
679  pContainer->SetPropValue("description", sGenreName);
680 
681  pContainer->SetChildCount(nArtistCount);
682  pContainer->SetChildContainerCount(nArtistCount);
683 
684  pResults->Add(pContainer);
685  pContainer->DecrRef();
686  }
687 
688  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
689  // at least the size of this result set
690  if (query.size() > 0)
691  pResults->m_nTotalMatches = query.size();
692 
693  // Fetch the total number of matches ignoring any LIMITs
694  query.prepare("SELECT FOUND_ROWS()");
695  if (query.exec() && query.next())
696  pResults->m_nTotalMatches = query.value(0).toUInt();
697 
698  return true;
699 }
700 
702 //
704 
706  UPnpCDSExtensionResults *pResults,
707  const IDTokenMap& tokens)
708 {
709  QString sRequestId = pRequest->m_sObjectId;
710 
711  uint16_t nCount = pRequest->m_nRequestedCount;
712  uint16_t nOffset = pRequest->m_nStartingIndex;
713 
714  // We must use a dedicated connection to get an acccurate value from
715  // FOUND_ROWS()
717 
718  QString sql = "SELECT SQL_CALC_FOUND_ROWS s.song_id, t.artist_name, "
719  "a.album_name, s.name, "
720  "g.genre, s.year, s.track, "
721  "s.description, s.filename, s.length, s.size, "
722  "s.numplays, s.lastplay, w.albumart_id "
723  "FROM music_songs s "
724  "LEFT JOIN music_artists t ON t.artist_id = s.artist_id "
725  "LEFT JOIN music_albums a ON a.album_id = s.album_id "
726  "LEFT JOIN music_genres g ON g.genre_id = s.genre_id "
727  "LEFT JOIN music_albumart w ON s.song_id = w.song_id "
728  "%1 " // WHERE clauses
729  "GROUP BY s.song_id "
730  "ORDER BY t.artist_name, a.album_name, s.track "
731  "LIMIT :OFFSET,:COUNT";
732 
733  QStringList clauses;
734  QString whereString = BuildWhereClause(clauses, tokens);
735 
736  query.prepare(sql.arg(whereString));
737 
738  BindValues(query, tokens);
739 
740  query.bindValue(":OFFSET", nOffset);
741  query.bindValue(":COUNT", nCount);
742 
743  if (!query.exec())
744  return false;
745 
746  while (query.next())
747  {
748  int nId = query.value( 0).toInt();
749  QString sArtist = query.value( 1).toString();
750  QString sAlbum = query.value( 2).toString();
751  QString sTitle = query.value( 3).toString();
752  QString sGenre = query.value( 4).toString();
753  int nYear = query.value( 5).toInt();
754  int nTrackNum = query.value( 6).toInt();
755  QString sDescription = query.value( 7).toString();
756  QString sFileName = query.value( 8).toString();
757  uint32_t nLengthMS = query.value( 9).toUInt();
758  uint64_t nFileSize = query.value(10).toULongLong();
759 
760  int nPlaybackCount = query.value(11).toInt();
761  QDateTime lastPlayedTime = query.value(12).toDateTime();
762  int nAlbumArtID = query.value(13).toInt();
763 
764  CDSObject* pItem = CDSObject::CreateMusicTrack( CreateIDString(sRequestId, "Track", nId),
765  sTitle,
766  pRequest->m_sParentId,
767  nullptr );
768 
769  // Only add the reference ID for items which are not in the
770  // 'All Tracks' container
771  QString sRefIDBase = QString("%1/Track").arg(m_sExtensionId);
772  if ( pRequest->m_sParentId != sRefIDBase )
773  {
774  QString sRefId = QString( "%1=%2")
775  .arg( sRefIDBase )
776  .arg( nId );
777 
778  pItem->SetPropValue( "refID", sRefId );
779  }
780 
781  pItem->SetPropValue( "genre" , sGenre );
782  pItem->SetPropValue( "description" , sTitle );
783  pItem->SetPropValue( "longDescription" , sDescription);
784 
785  pItem->SetPropValue( "artist" , sArtist );
786  pItem->SetPropValue( "creator" , sArtist );
787  pItem->SetPropValue( "album" , sAlbum );
788  pItem->SetPropValue( "originalTrackNumber" , QString::number(nTrackNum));
789  if (nYear > 0 && nYear < 9999)
790  pItem->SetPropValue( "date", QDate(nYear,1,1).toString(Qt::ISODate));
791 
792  pItem->SetPropValue( "playbackCount" , QString::number(nPlaybackCount));
793  pItem->SetPropValue( "lastPlaybackTime" , UPnPDateTime::DateTimeFormat(lastPlayedTime));
794 
795  // Artwork
796  if (nAlbumArtID > 0)
797  PopulateArtworkURIS(pItem, nId);
798 
799  // ----------------------------------------------------------------------
800  // Add Music Resource Element based on File extension (HTTP)
801  // ----------------------------------------------------------------------
802 
803  QFileInfo fInfo( sFileName );
804 
805  QUrl resURI = m_URIBase;
806  QUrlQuery resQuery;
807  resURI.setPath("/Content/GetMusic");
808  resQuery.addQueryItem("Id", QString::number(nId));
809  resURI.setQuery(resQuery);
810 
811  QString sMimeType = HTTPRequest::GetMimeType( fInfo.suffix() );
812 
813  QString sProtocol = DLNA::ProtocolInfoString(UPNPProtocol::kHTTP,
814  sMimeType);
815 
816  Resource *pResource = pItem->AddResource( sProtocol, resURI.toEncoded() );
817 
818  pResource->AddAttribute( "duration" , UPnPDateTime::resDurationFormat(nLengthMS) );
819  if (nFileSize > 0)
820  pResource->AddAttribute( "size" , QString::number( nFileSize) );
821 
822  pResults->Add(pItem);
823  pItem->DecrRef();
824  }
825 
826  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
827  // at least the size of this result set
828  if (query.size() > 0)
829  pResults->m_nTotalMatches = query.size();
830 
831  // Fetch the total number of matches ignoring any LIMITs
832  query.prepare("SELECT FOUND_ROWS()");
833  if (query.exec() && query.next())
834  pResults->m_nTotalMatches = query.value(0).toUInt();
835 
836  return true;
837 }
838 
840 //
842 
843 QString UPnpCDSMusic::BuildWhereClause( QStringList clauses,
844  IDTokenMap tokens)
845 {
846  if (tokens["track"].toInt() > 0)
847  clauses.append("s.song_id=:TRACK_ID");
848  if (tokens["album"].toInt() > 0)
849  clauses.append("s.album_id=:ALBUM_ID");
850  if (tokens["artist"].toInt() > 0)
851  clauses.append("s.artist_id=:ARTIST_ID");
852  if (tokens["genre"].toInt() > 0)
853  clauses.append("s.genre_id=:GENRE_ID");
854  if (tokens["year"].toInt() > 0)
855  clauses.append("s.year=:YEAR");
856  if (tokens["directory"].toInt() > 0)
857  clauses.append("s.directory_id=:DIRECTORY_ID");
858 
859 // if (tokens["album"].toInt() > 0)
860 // clauses.append("a.album_id=:ALBUM_ID");
861 // if (tokens["artist"].toInt() > 0)
862 // clauses.append("a.artist_id=:ARTIST_ID");
863 
864  QString whereString;
865  if (!clauses.isEmpty())
866  {
867  whereString = " WHERE ";
868  whereString.append(clauses.join(" AND "));
869  }
870 
871  return whereString;
872 }
873 
875 //
877 
879  IDTokenMap tokens)
880 {
881  if (tokens["track"].toInt() > 0)
882  query.bindValue(":TRACK_ID", tokens["track"]);
883  if (tokens["album"].toInt() > 0)
884  query.bindValue(":ALBUM_ID", tokens["album"]);
885  if (tokens["artist"].toInt() > 0)
886  query.bindValue(":ARTIST_ID", tokens["artist"]);
887  if (tokens["genre"].toInt() > 0)
888  query.bindValue(":GENRE_ID", tokens["genre"]);
889  if (tokens["year"].toInt() > 0)
890  query.bindValue(":YEAR", tokens["year"]);
891  if (tokens["directory"].toInt() > 0)
892  query.bindValue(":DIRECTORY_ID", tokens["directory"]);
893 }
894 
895 // vim:ts=4:sw=4:ai:et:si:sts=4
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:782
void SetValue(const QString &value)
Resource * AddResource(const QString &sProtocol, const QString &sURI)
void CreateRoot() override
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:863
QString m_sParentId
Definition: upnpcds.h:83
void BindValues(MSqlQuery &query, IDTokenMap tokens)
virtual CDSObject * GetRoot()
Definition: upnpcds.cpp:1083
static CDSObject * CreateMusicArtist(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
static CDSObject * CreateContainer(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
QString toString(MarkTypes type)
bool LoadChildren(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, IDTokenMap tokens, QString currentToken) override
Fetch the children of the container identified in the request.
CDSObject * AddChild(CDSObject *pChild)
QString GetBackendServerIP(void)
Returns the IP address of the locally defined backend IP.
QString m_sId
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:125
bool IsSearchRequestForUs(UPnpCDSRequest *pRequest) override
int size(void) const
Definition: mythdbcon.h:203
void SetChildCount(uint32_t nCount)
Allows the caller to set childCount without having to load children.
QString resDurationFormat(uint32_t msec)
res@duration Format B.2.1.4 res@duration - UPnP ContentDirectory Service 2008, 2013
Definition: upnphelpers.cpp:86
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
bool LoadMetadata(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, IDTokenMap tokens, QString currentToken) override
Fetch just the metadata for the item identified in the request.
static CDSObject * CreateMusicGenre(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
bool LoadAlbums(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
static QString GetMimeType(const QString &sFileExtension)
bool LoadArtists(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
bool LoadGenres(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
uint16_t m_nTotalMatches
Definition: upnpcds.h:111
QString m_sExtensionId
Definition: upnpcds.h:204
QString ProtocolInfoString(UPNPProtocol::TransferProtocol protocol, const QString &mimeType, const QSize &resolution, double videoFrameRate, const QString &container, const QString &videoCodec, const QString &audioCodec, bool isTranscoded)
Create a properly formatted string for the 4th field of res@protocolInfo.
uint16_t m_nRequestedCount
Definition: upnpcds.h:78
QVariant value(int i) const
Definition: mythdbcon.h:198
bool LoadTracks(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
static CDSObject * CreateMusicTrack(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
void SetChildContainerCount(uint32_t nCount)
Allows the caller to set childContainerCount without having to load children.
QString m_sObjectId
Definition: upnpcds.h:73
bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest) override
QString DateTimeFormat(const QDateTime &dateTime)
Date-Time Format.
Definition: upnphelpers.cpp:48
virtual int DecrRef(void)
Decrements reference count and deletes on 0.
void AddAttribute(const QString &sName, const QString &sValue)
unsigned short uint16_t
Definition: iso6937tables.h:1
QList< Property * > GetProperties(const QString &sName)
static MSqlQueryInfo InitCon(ConnectionReuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:535
void Add(CDSObject *pObject)
Definition: upnpcds.cpp:31
uint32_t GetChildCount(void) const
Return the number of children in this container.
void AddAttribute(const QString &sName, const QString &sValue)
CDSObject * GetChild(const QString &sID)
QString CreateIDString(const QString &RequestId, const QString &Name, int Value)
Definition: upnpcds.cpp:1050
virtual bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:804
QString m_sName
Definition: upnpcds.h:205
int GetBackendStatusPort(void)
Returns the locally defined backend status port.
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:807
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
void SetPropValue(const QString &sName, const QString &sValue, const QString &type="")
uint16_t m_nStartingIndex
Definition: upnpcds.h:77
CDSShortCutList m_shortcuts
Definition: upnpcds.h:208
void PopulateArtworkURIS(CDSObject *pItem, int songID)
QString BuildWhereClause(QStringList clauses, IDTokenMap tokens)
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:603
virtual bool IsSearchRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:891
QString m_sClass
UPnpCDSMusic()
Music Extension for UPnP ContentDirectory Service.
QMap< QString, QString > IDTokenMap
Definition: upnpcds.h:197
Default UTC.
Definition: mythdate.h:14
static CDSObject * CreateMusicAlbum(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
CDSObject * m_pRoot
Definition: upnpcds.h:246