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