MythTV  master
upnpcdsvideo.cpp
Go to the documentation of this file.
1 // Program Name: upnpcdsvideo.cpp
2 //
3 // Purpose - UPnP Content Directory Extension for MythVideo Videos
4 //
6 
7 // C++ headers
8 #include <climits>
9 
10 // Qt headers
11 #include <QFileInfo>
12 #include <QUrl>
13 #include <QUrlQuery>
14 
15 // MythTV headers
17 #include "libmythbase/mythdate.h"
21 
22 // MythBackend
23 #include "upnpcdsvideo.h"
24 
25 #define LOC QString("UPnpCDSVideo: ")
26 #define LOC_WARN QString("UPnpCDSVideo, Warning: ")
27 #define LOC_ERR QString("UPnpCDSVideo, Error: ")
28 
30  : UPnpCDSExtension( QObject::tr("Videos"), "Videos",
31  "object.item.videoItem" )
32 {
33  QString sServerIp = gCoreContext->GetBackendServerIP();
34  int sPort = gCoreContext->GetBackendStatusPort();
35  m_uriBase.setScheme("http");
36  m_uriBase.setHost(sServerIp);
37  m_uriBase.setPort(sPort);
38 
39  // ShortCuts
40  m_shortcuts.insert(UPnPShortcutFeature::VIDEOS, "Videos");
41  m_shortcuts.insert(UPnPShortcutFeature::VIDEOS_ALL, "Videos/Video");
42  m_shortcuts.insert(UPnPShortcutFeature::VIDEOS_GENRES, "Videos/Genre");
43 }
44 
46 {
47  if (m_pRoot)
48  return;
49 
51  m_sName,
52  "0");
53 
54  QString containerId = m_sExtensionId + "/%1";
55 
56  // HACK: I'm not entirely happy with this solution, but it's at least
57  // tidier than passing through half a dozen extra args to Load[Foo]
58  // or having yet more methods just to load the counts
59  auto *pRequest = new UPnpCDSRequest();
60  pRequest->m_nRequestedCount = 0; // We don't want to load any results, we just want the TotalCount
61  auto *pResult = new UPnpCDSExtensionResults();
62  IDTokenMap tokens;
63  // END HACK
64 
65  // -----------------------------------------------------------------------
66  // All Videos
67  // -----------------------------------------------------------------------
68  CDSObject* pContainer = CDSObject::CreateContainer ( containerId.arg("Video"),
69  QObject::tr("All Videos"),
70  m_sExtensionId, // Parent Id
71  nullptr );
72  // HACK
73  LoadVideos(pRequest, pResult, tokens);
74  pContainer->SetChildCount(pResult->m_nTotalMatches);
75  pContainer->SetChildContainerCount(0);
76  // END HACK
77  m_pRoot->AddChild(pContainer);
78 
79  // -----------------------------------------------------------------------
80  // Films
81  // -----------------------------------------------------------------------
82  pContainer = CDSObject::CreateContainer ( containerId.arg("Movie"),
83  QObject::tr("Movies"),
84  m_sExtensionId, // Parent Id
85  nullptr );
86  // HACK
87  LoadMovies(pRequest, pResult, tokens);
88  pContainer->SetChildCount(pResult->m_nTotalMatches);
89  pContainer->SetChildContainerCount(0);
90  // END HACK
91  m_pRoot->AddChild(pContainer);
92 
93  // -----------------------------------------------------------------------
94  // Series
95  // -----------------------------------------------------------------------
96  pContainer = CDSObject::CreateContainer ( containerId.arg("Series"),
97  QObject::tr("Series"),
98  m_sExtensionId, // Parent Id
99  nullptr );
100  // HACK
101  LoadSeries(pRequest, pResult, tokens);
102  pContainer->SetChildCount(pResult->m_nTotalMatches);
103  pContainer->SetChildContainerCount(0);
104  // END HACK
105  m_pRoot->AddChild(pContainer);
106 
107  // -----------------------------------------------------------------------
108  // Other (Home videos?)
109  // -----------------------------------------------------------------------
110 // pContainer = CDSObject::CreateContainer ( containerId.arg("Other"),
111 // QObject::tr("Other"),
112 // m_sExtensionId, // Parent Id
113 // nullptr );
114 // m_pRoot->AddChild(pContainer);
115 
116  // -----------------------------------------------------------------------
117  // Genre
118  // -----------------------------------------------------------------------
119  pContainer = CDSObject::CreateContainer ( containerId.arg("Genre"),
120  QObject::tr("Genre"),
121  m_sExtensionId, // Parent Id
122  nullptr );
123  // HACK
124  LoadGenres(pRequest, pResult, tokens);
125  pContainer->SetChildCount(pResult->m_nTotalMatches);
126  pContainer->SetChildContainerCount(0);
127  // END HACK
128  m_pRoot->AddChild(pContainer);
129 
130  // -----------------------------------------------------------------------
131  // By Directory
132  // -----------------------------------------------------------------------
133 // pContainer = CDSObject::CreateStorageSystem ( containerId.arg("Directory"),
134 // QObject::tr("Directory"),
135 // m_sExtensionId, // Parent Id
136 // nullptr );
137 // m_pRoot->AddChild(pContainer);
138 
139  // HACK
140  delete pRequest;
141  delete pResult;
142  // END HACK
143 }
144 
146 //
148 
150 {
151  // ----------------------------------------------------------------------
152  // See if we need to modify the request for compatibility
153  // ----------------------------------------------------------------------
154 
155  // ----------------------------------------------------------------------
156  // Xbox360 compatibility code.
157  // ----------------------------------------------------------------------
158 
159 // if (pRequest->m_eClient == CDS_ClientXBox &&
160 // pRequest->m_sContainerID == "15" &&
161 // gCoreContext->GetSetting("UPnP/WMPSource") == "1")
162 // {
163 // pRequest->m_sObjectId = "Videos/0";
164 //
165 // LOG(VB_UPNP, LOG_INFO,
166 // "UPnpCDSVideo::IsBrowseRequestForUs - Yes ContainerID == 15");
167 // return true;
168 // }
169 //
170 // if ((pRequest->m_sObjectId.isEmpty()) &&
171 // (!pRequest->m_sContainerID.isEmpty()))
172 // pRequest->m_sObjectId = pRequest->m_sContainerID;
173 
174  // ----------------------------------------------------------------------
175  // WMP11 compatibility code
176  //
177  // In this mode browsing for "Videos" is forced to either Videos (us)
178  // or RecordedTV (handled by upnpcdstv)
179  //
180  // ----------------------------------------------------------------------
181 
182 // if (pRequest->m_eClient == CDS_ClientWMP &&
183 // pRequest->m_sContainerID == "13" &&
184 // pRequest->m_nClientVersion < 12.0 &&
185 // gCoreContext->GetSetting("UPnP/WMPSource") == "1")
186 // {
187 // pRequest->m_sObjectId = "Videos/0";
188 //
189 // LOG(VB_UPNP, LOG_INFO,
190 // "UPnpCDSVideo::IsBrowseRequestForUs - Yes ContainerID == 13");
191 // return true;
192 // }
193 
194  LOG(VB_UPNP, LOG_INFO,
195  "UPnpCDSVideo::IsBrowseRequestForUs - Not sure... Calling base class.");
196 
197  return UPnpCDSExtension::IsBrowseRequestForUs( pRequest );
198 }
199 
201 //
203 
205 {
206  // ----------------------------------------------------------------------
207  // See if we need to modify the request for compatibility
208  // ----------------------------------------------------------------------
209 
210  // ----------------------------------------------------------------------
211  // XBox 360 compatibility code
212  // ----------------------------------------------------------------------
213 
214 
215 // if (pRequest->m_eClient == CDS_ClientXBox &&
216 // pRequest->m_sContainerID == "15" &&
217 // gCoreContext->GetSetting("UPnP/WMPSource") == "1")
218 // {
219 // pRequest->m_sObjectId = "Videos/0";
220 //
221 // LOG(VB_UPNP, LOG_INFO, "UPnpCDSVideo::IsSearchRequestForUs... Yes.");
222 //
223 // return true;
224 // }
225 //
226 // if ((pRequest->m_sObjectId.isEmpty()) &&
227 // (!pRequest->m_sContainerID.isEmpty()))
228 // pRequest->m_sObjectId = pRequest->m_sContainerID;
229 
230  // ----------------------------------------------------------------------
231 
232  bool bOurs = UPnpCDSExtension::IsSearchRequestForUs( pRequest );
233 
234  // ----------------------------------------------------------------------
235  // WMP11 compatibility code
236  // ----------------------------------------------------------------------
237 
238 // if ( bOurs && pRequest->m_eClient == CDS_ClientWMP &&
239 // pRequest->m_nClientVersion < 12.0 )
240 // {
241 // if ( gCoreContext->GetSetting("UPnP/WMPSource") == "1")
242 // {
243 // pRequest->m_sObjectId = "Videos/0";
244 // // -=>TODO: Not sure why this was added.
245 // pRequest->m_sParentId = "8";
246 // }
247 // else
248 // bOurs = false;
249 // }
250 
251  return bOurs;
252 }
253 
255 //
257 
259  UPnpCDSExtensionResults* pResults,
260  const IDTokenMap& tokens, const QString& currentToken)
261 {
262  if (currentToken.isEmpty())
263  {
264  LOG(VB_GENERAL, LOG_ERR, QString("UPnpCDSTV::LoadMetadata: Final "
265  "token missing from id: %1")
266  .arg(pRequest->m_sParentId));
267  return false;
268  }
269 
270  // Root or Root + 1
271  if (tokens[currentToken].isEmpty())
272  {
273  CDSObject *container = nullptr;
274 
275  if (pRequest->m_sObjectId == m_sExtensionId)
276  container = GetRoot();
277  else
278  container = GetRoot()->GetChild(pRequest->m_sObjectId);
279 
280  if (container)
281  {
282  pResults->Add(container);
283  pResults->m_nTotalMatches = 1;
284  return true;
285  }
286  LOG(VB_GENERAL, LOG_ERR, QString("UPnpCDSTV::LoadMetadata: Requested "
287  "object cannot be found: %1")
288  .arg(pRequest->m_sObjectId));
289  }
290  else if (currentToken == "series")
291  {
292  return LoadSeries(pRequest, pResults, tokens);
293  }
294  else if (currentToken == "season")
295  {
296  return LoadSeasons(pRequest, pResults, tokens);
297  }
298  else if (currentToken == "genre")
299  {
300  return LoadGenres(pRequest, pResults, tokens);
301  }
302  else if (currentToken == "movie")
303  {
304  return LoadMovies(pRequest, pResults, tokens);
305  }
306  else if (currentToken == "video")
307  {
308  return LoadVideos(pRequest, pResults, tokens);
309  }
310  else
311  {
312  LOG(VB_GENERAL, LOG_ERR,
313  QString("UPnpCDSVideo::LoadMetadata(): "
314  "Unhandled metadata request for '%1'.").arg(currentToken));
315  }
316 
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 == "series")
336  {
337  if (!tokens["series"].isEmpty())
338  return LoadSeasons(pRequest, pResults, tokens);
339  return LoadSeries(pRequest, pResults, tokens);
340  }
341  if (currentToken == "season")
342  {
343  if (!tokens["season"].isEmpty() && tokens["season"].toInt() >= 0) // Season 0 is valid
344  return LoadVideos(pRequest, pResults, tokens);
345  return LoadSeasons(pRequest, pResults, tokens);
346  }
347  if (currentToken == "genre")
348  {
349  if (!tokens["genre"].isEmpty())
350  return LoadVideos(pRequest, pResults, tokens);
351  return LoadGenres(pRequest, pResults, tokens);
352  }
353  if (currentToken == "movie")
354  {
355  return LoadMovies(pRequest, pResults, tokens);
356  }
357  if (currentToken == "video")
358  {
359  return LoadVideos(pRequest, pResults, tokens);
360  }
361  LOG(VB_GENERAL, LOG_ERR,
362  QString("UPnpCDSVideo::LoadChildren(): "
363  "Unhandled metadata request for '%1'.").arg(currentToken));
364 
365  return false;
366 }
367 
369 //
371 
373  UPnpCDSExtensionResults* pResults,
374  const IDTokenMap& tokens)
375 {
376  QString sRequestId = pRequest->m_sObjectId;
377 
378  uint16_t nCount = pRequest->m_nRequestedCount;
379  uint16_t nOffset = pRequest->m_nStartingIndex;
380 
381  // We must use a dedicated connection to get an acccurate value from
382  // FOUND_ROWS()
384 
385  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
386  "v.title, COUNT(DISTINCT v.season), v.intid "
387  "FROM videometadata v "
388  "%1 " // whereString
389  "GROUP BY v.title "
390  "ORDER BY v.title "
391  "LIMIT :OFFSET,:COUNT ";
392 
393  QStringList clauses;
394  clauses.append("contenttype='TELEVISION'");
395  QString whereString = BuildWhereClause(clauses, tokens);
396 
397  query.prepare(sql.arg(whereString));
398 
399  BindValues(query, tokens);
400 
401  query.bindValue(":OFFSET", nOffset);
402  query.bindValue(":COUNT", nCount);
403 
404  if (!query.exec())
405  return false;
406 
407  while (query.next())
408  {
409  QString sTitle = query.value(0).toString();
410  int nSeasonCount = query.value(1).toInt();
411  int nVidID = query.value(2).toInt();
412 
413  // TODO Album or plain old container?
414  CDSObject* pContainer = CDSObject::CreateAlbum( CreateIDString(sRequestId, "Series", sTitle),
415  sTitle,
416  pRequest->m_sParentId,
417  nullptr );
418  pContainer->SetPropValue("description", QObject::tr("%n Seasons", "", nSeasonCount));
419  pContainer->SetPropValue("longdescription", QObject::tr("%n Seasons", "", nSeasonCount));
420  pContainer->SetPropValue("storageMedium", "HDD");
421 
422  pContainer->SetChildCount(nSeasonCount);
423  pContainer->SetChildContainerCount(nSeasonCount);
424 
425  PopulateArtworkURIS(pContainer, nVidID, m_uriBase);
426 
427  pResults->Add(pContainer);
428  pContainer->DecrRef();
429  }
430 
431  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
432  // at least the size of this result set
433  if (query.size() >= 0)
434  pResults->m_nTotalMatches = query.size();
435 
436  // Fetch the total number of matches ignoring any LIMITs
437  query.prepare("SELECT FOUND_ROWS()");
438  if (query.exec() && query.next())
439  pResults->m_nTotalMatches = query.value(0).toUInt();
440 
441  return true;
442 }
443 
445 //
447 
449  UPnpCDSExtensionResults* pResults,
450  const IDTokenMap& tokens)
451 {
452  QString sRequestId = pRequest->m_sObjectId;
453 
454  uint16_t nCount = pRequest->m_nRequestedCount;
455  uint16_t nOffset = pRequest->m_nStartingIndex;
456 
457  // We must use a dedicated connection to get an acccurate value from
458  // FOUND_ROWS()
460 
461  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
462  "v.season, COUNT(DISTINCT v.intid), v.intid "
463  "FROM videometadata v "
464  "%1 " // whereString
465  "GROUP BY v.season "
466  "ORDER BY v.season "
467  "LIMIT :OFFSET,:COUNT ";
468 
469  QStringList clauses;
470  QString whereString = BuildWhereClause(clauses, tokens);
471 
472  query.prepare(sql.arg(whereString));
473 
474  BindValues(query, tokens);
475 
476  query.bindValue(":OFFSET", nOffset);
477  query.bindValue(":COUNT", nCount);
478 
479  if (!query.exec())
480  return false;
481 
482  while (query.next())
483  {
484  int nSeason = query.value(0).toInt();
485  int nVideoCount = query.value(1).toInt();
486  int nVidID = query.value(2).toInt();
487 
488  QString sTitle = QObject::tr("Season %1").arg(nSeason);
489 
490  // TODO Album or plain old container?
491  CDSObject* pContainer = CDSObject::CreateAlbum( CreateIDString(sRequestId, "Season", nSeason),
492  sTitle,
493  pRequest->m_sParentId,
494  nullptr );
495  pContainer->SetPropValue("description", QObject::tr("%n Episode(s)", "", nVideoCount));
496  pContainer->SetPropValue("longdescription", QObject::tr("%n Episode(s)", "", nVideoCount));
497  pContainer->SetPropValue("storageMedium", "HDD");
498 
499  pContainer->SetChildCount(nVideoCount);
500  pContainer->SetChildContainerCount(0);
501 
502  PopulateArtworkURIS(pContainer, nVidID, m_uriBase);
503 
504  pResults->Add(pContainer);
505  pContainer->DecrRef();
506  }
507 
508  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
509  // at least the size of this result set
510  if (query.size() >= 0)
511  pResults->m_nTotalMatches = query.size();
512 
513  // Fetch the total number of matches ignoring any LIMITs
514  query.prepare("SELECT FOUND_ROWS()");
515  if (query.exec() && query.next())
516  pResults->m_nTotalMatches = query.value(0).toUInt();
517 
518  return true;
519 }
520 
522 //
524 
526  UPnpCDSExtensionResults* pResults,
527  IDTokenMap tokens)
528 {
529  tokens["type"] = "MOVIE";
530  //LoadGenres(pRequest, pResults, tokens);
531  return LoadVideos(pRequest, pResults, tokens);
532 }
533 
535 //
537 
539  UPnpCDSExtensionResults* pResults,
540  const IDTokenMap& tokens)
541 {
542  QString sRequestId = pRequest->m_sObjectId;
543 
544  uint16_t nCount = pRequest->m_nRequestedCount;
545  uint16_t nOffset = pRequest->m_nStartingIndex;
546 
547  // We must use a dedicated connection to get an acccurate value from
548  // FOUND_ROWS()
550 
551  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
552  "v.category, g.genre, COUNT(DISTINCT v.intid) "
553  "FROM videometadata v "
554  "LEFT JOIN videogenre g ON g.intid=v.category "
555  "%1 " // whereString
556  "GROUP BY g.intid "
557  "ORDER BY g.genre "
558  "LIMIT :OFFSET,:COUNT ";
559 
560  QStringList clauses;
561  clauses.append("v.category != 0");
562  QString whereString = BuildWhereClause(clauses, tokens);
563 
564  query.prepare(sql.arg(whereString));
565 
566  BindValues(query, tokens);
567 
568  query.bindValue(":OFFSET", nOffset);
569  query.bindValue(":COUNT", nCount);
570 
571  if (!query.exec())
572  return false;
573 
574  while (query.next())
575  {
576  int nGenreID = query.value(0).toInt();
577  QString sName = query.value(1).toString();
578  int nVideoCount = query.value(2).toInt();
579 
580  // TODO Album or plain old container?
581  CDSObject* pContainer = CDSObject::CreateMovieGenre( CreateIDString(sRequestId, "Genre", nGenreID),
582  sName,
583  pRequest->m_sParentId,
584  nullptr );
585 
586  pContainer->SetChildCount(nVideoCount);
587  pContainer->SetChildContainerCount(0);
588 
589  pResults->Add(pContainer);
590  pContainer->DecrRef();
591  }
592 
593  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
594  // at least the size of this result set
595  if (query.size() >= 0)
596  pResults->m_nTotalMatches = query.size();
597 
598  // Fetch the total number of matches ignoring any LIMITs
599  query.prepare("SELECT FOUND_ROWS()");
600  if (query.exec() && query.next())
601  pResults->m_nTotalMatches = query.value(0).toUInt();
602 
603  return true;
604 }
605 
607 //
609 
611  UPnpCDSExtensionResults* pResults,
612  const IDTokenMap& tokens)
613 {
614  QString sRequestId = pRequest->m_sObjectId;
615 
616  uint16_t nCount = pRequest->m_nRequestedCount;
617  uint16_t nOffset = pRequest->m_nStartingIndex;
618 
619  // We must use a dedicated connection to get an acccurate value from
620  // FOUND_ROWS()
622 
623  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
624  "v.intid, title, subtitle, filename, director, plot, "
625  "rating, year, userrating, length, "
626  "season, episode, coverfile, insertdate, host, "
627  "g.genre, studio, collectionref, contenttype "
628  "FROM videometadata v "
629  "LEFT JOIN videogenre g ON g.intid=v.category "
630  "%1 " //
631  "ORDER BY title, season, episode "
632  "LIMIT :OFFSET,:COUNT ";
633 
634  QStringList clauses;
635  QString whereString = BuildWhereClause(clauses, tokens);
636 
637  query.prepare(sql.arg(whereString));
638 
639  BindValues(query, tokens);
640 
641  query.bindValue(":OFFSET", nOffset);
642  query.bindValue(":COUNT", nCount);
643 
644  if (!query.exec())
645  return false;
646 
647  while (query.next())
648  {
649 
650  int nVidID = query.value( 0).toInt();
651  QString sTitle = query.value( 1).toString();
652  QString sSubtitle = query.value( 2).toString();
653  QString sFilePath = query.value( 3).toString();
654  QString sDirector = query.value( 4).toString();
655  QString sPlot = query.value( 5).toString();
656  // QString sRating = query.value( 6).toString();
657  int nYear = query.value( 7).toInt();
658  // int nUserRating = query.value( 8).toInt();
659 
660  auto nLength = std::chrono::minutes(query.value( 9).toUInt());
661 
662  int nSeason = query.value(10).toInt();
663  int nEpisode = query.value(11).toInt();
664  QString sCoverArt = query.value(12).toString();
665  QDateTime dtInsertDate =
666  MythDate::as_utc(query.value(13).toDateTime());
667  QString sHostName = query.value(14).toString();
668  QString sGenre = query.value(15).toString();
669  // QString sStudio = query.value(16).toString();
670  // QString sCollectionRef = query.value(17).toString();
671  QString sContentType = query.value(18).toString();
672 
673  // ----------------------------------------------------------------------
674  // Cache Host ip Address & Port
675  // ----------------------------------------------------------------------
676 
677  // If the host-name is empty then we assume it is our local host
678  // otherwise, we look up the host's IP address and port. When the
679  // client then trys to play the video it will be directed to the
680  // host which actually has the content.
681  if (!m_mapBackendIp.contains( sHostName ))
682  {
683  if (sHostName.isEmpty())
684  {
685  m_mapBackendIp[sHostName] =
687  }
688  else
689  {
690  m_mapBackendIp[sHostName] =
691  gCoreContext->GetBackendServerIP(sHostName);
692  }
693  }
694 
695  if (!m_mapBackendPort.contains( sHostName ))
696  {
697  if (sHostName.isEmpty())
698  {
699  m_mapBackendPort[sHostName] =
701  }
702  else
703  {
704  m_mapBackendPort[sHostName] =
706  }
707  }
708 
709 
710  // ----------------------------------------------------------------------
711  // Build Support Strings
712  // ----------------------------------------------------------------------
713 
714  QString sName = sTitle;
715  if( !sSubtitle.isEmpty() )
716  {
717  sName += " - " + sSubtitle;
718  }
719 
720  QUrl URIBase;
721  URIBase.setScheme("http");
722  URIBase.setHost(m_mapBackendIp[sHostName]);
723  URIBase.setPort(m_mapBackendPort[sHostName]);
724 
725  CDSObject *pItem = nullptr;
726  if (sContentType == "MOVIE")
727  {
728  pItem = CDSObject::CreateMovie( CreateIDString(sRequestId, "Video", nVidID),
729  sTitle,
730  pRequest->m_sParentId );
731  }
732  else
733  {
734  pItem = CDSObject::CreateVideoItem( CreateIDString(sRequestId, "Video", nVidID),
735  sName,
736  pRequest->m_sParentId );
737  }
738 
739  if (!sSubtitle.isEmpty())
740  pItem->SetPropValue( "description", sSubtitle );
741  else
742  pItem->SetPropValue( "description", sPlot.left(128).append(" ..."));
743  pItem->SetPropValue( "longDescription", sPlot );
744  pItem->SetPropValue( "director" , sDirector );
745 
746  if (nEpisode > 0 || nSeason > 0) // There has got to be a better way
747  {
748  pItem->SetPropValue( "seriesTitle" , sTitle );
749  pItem->SetPropValue( "programTitle" , sSubtitle );
750  pItem->SetPropValue( "episodeNumber" , QString::number(nEpisode));
751  //pItem->SetPropValue( "episodeCount" , nEpisodeCount);
752  }
753 
754  pItem->SetPropValue( "genre" , sGenre );
755  if (nYear > 1830 && nYear < 9999)
756  pItem->SetPropValue( "date", QDate(nYear,1,1).toString(Qt::ISODate));
757  else
758  pItem->SetPropValue( "date", UPnPDateTime::DateTimeFormat(dtInsertDate) );
759 
760  // HACK: Windows Media Centre Compat (Not a UPnP or DLNA requirement, should only be done for WMC)
761 // pItem->SetPropValue( "genre" , "[Unknown Genre]" );
762 // pItem->SetPropValue( "actor" , "[Unknown Author]" );
763 // pItem->SetPropValue( "creator" , "[Unknown Creator]" );
764 // pItem->SetPropValue( "album" , "[Unknown Album]" );
766 
767  //pItem->SetPropValue( "producer" , );
768  //pItem->SetPropValue( "rating" , );
769  //pItem->SetPropValue( "actor" , );
770  //pItem->SetPropValue( "publisher" , );
771  //pItem->SetPropValue( "language" , );
772  //pItem->SetPropValue( "relation" , );
773  //pItem->SetPropValue( "region" , );
774 
775  // Only add the reference ID for items which are not in the
776  // 'All Videos' container
777  QString sRefIDBase = QString("%1/Video").arg(m_sExtensionId);
778  if ( pRequest->m_sParentId != sRefIDBase )
779  {
780  QString sRefId = QString( "%1=%2")
781  .arg( sRefIDBase )
782  .arg( nVidID );
783 
784  pItem->SetPropValue( "refID", sRefId );
785  }
786 
787  // FIXME - If the slave or storage hosting this video is offline we
788  // won't find it. We probably shouldn't list it, but better
789  // still would be storing the filesize in the database so we
790  // don't waste time re-checking it constantly
791  QString sFullFileName = sFilePath;
792  if (!QFile::exists( sFullFileName ))
793  {
794  StorageGroup sgroup("Videos");
795  sFullFileName = sgroup.FindFile( sFullFileName );
796  }
797  QFileInfo fInfo( sFullFileName );
798 
799  // ----------------------------------------------------------------------
800  // Add Video Resource Element based on File extension (HTTP)
801  // ----------------------------------------------------------------------
802 
803  QString sMimeType = HTTPRequest::GetMimeType( QFileInfo(sFilePath).suffix() );
804 
805  // HACK: If we are dealing with a Sony Blu-ray player then we fake the
806  // MIME type to force the video to appear
807 // if ( pRequest->m_eClient == CDS_ClientSonyDB )
808 // {
809 // sMimeType = "video/avi";
810 // }
811 
812  QUrl resURI = URIBase;
813  QUrlQuery resQuery;
814  resURI.setPath("/Content/GetVideo");
815  resQuery.addQueryItem("Id", QString::number(nVidID));
816  resURI.setQuery(resQuery);
817 
818  // DLNA requires a mimetype of video/mp2p for TS files, it's not the
819  // correct mimetype, but then DLNA doesn't seem to care about such
820  // things
821  if (sMimeType == "video/mp2t" || sMimeType == "video/mp2p")
822  sMimeType = "video/mpeg";
823 
824  QString sProtocol = DLNA::ProtocolInfoString(UPNPProtocol::kHTTP,
825  sMimeType);
826 
827  Resource *pRes = pItem->AddResource( sProtocol, resURI.toEncoded() );
828  pRes->AddAttribute( "size" , QString("%1").arg(fInfo.size()) );
829  pRes->AddAttribute( "duration", UPnPDateTime::resDurationFormat(nLength) );
830 
831  // ----------------------------------------------------------------------
832  // Add Artwork
833  // ----------------------------------------------------------------------
834  if (!sCoverArt.isEmpty() && (sCoverArt != "No Cover"))
835  {
836  PopulateArtworkURIS(pItem, nVidID, URIBase);
837  }
838 
839  pResults->Add( pItem );
840  pItem->DecrRef();
841  }
842 
843  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
844  // at least the size of this result set
845  if (query.size() >= 0)
846  pResults->m_nTotalMatches = query.size();
847 
848  // Fetch the total number of matches ignoring any LIMITs
849  query.prepare("SELECT FOUND_ROWS()");
850  if (query.exec() && query.next())
851  pResults->m_nTotalMatches = query.value(0).toUInt();
852 
853  return true;
854 }
855 
857  const QUrl& URIBase)
858 {
859  QUrl artURI = URIBase;
860  artURI.setPath("/Content/GetVideoArtwork");
861  QUrlQuery artQuery;
862  artQuery.addQueryItem("Id", QString::number(nVidID));
863  artURI.setQuery(artQuery);
864 
865  // Prefer JPEG over PNG here, although PNG is allowed JPEG probably
866  // has wider device support and crucially the filesizes are smaller
867  // which speeds up loading times over the network
868 
869  // We MUST include the thumbnail size, but since some clients may use the
870  // first image they see and the thumbnail is tiny, instead return the
871  // medium first. The large could be very large, which is no good if the
872  // client is pulling images for an entire list at once!
873 
874  // Thumbnail
875  // At least one albumArtURI must be a ThumbNail (TN) no larger
876  // than 160x160, and it must also be a jpeg
877  QUrl thumbURI = artURI;
878  QUrlQuery thumbQuery(thumbURI.query());
879  if (pItem->m_sClass == "object.item.videoItem") // Show screenshot for TV, coverart for movies
880  thumbQuery.addQueryItem("Type", "screenshot");
881  else
882  thumbQuery.addQueryItem("Type", "coverart");
883  thumbQuery.addQueryItem("Width", "160");
884  thumbQuery.addQueryItem("Height", "160");
885  thumbURI.setQuery(thumbQuery);
886 
887  // Small
888  // Must be no more than 640x480
889  QUrl smallURI = artURI;
890  QUrlQuery smallQuery(smallURI.query());
891  smallQuery.addQueryItem("Type", "coverart");
892  smallQuery.addQueryItem("Width", "640");
893  smallQuery.addQueryItem("Height", "480");
894  smallURI.setQuery(smallQuery);
895 
896  // Medium
897  // Must be no more than 1024x768
898  QUrl mediumURI = artURI;
899  QUrlQuery mediumQuery(mediumURI.query());
900  mediumQuery.addQueryItem("Type", "coverart");
901  mediumQuery.addQueryItem("Width", "1024");
902  mediumQuery.addQueryItem("Height", "768");
903  mediumURI.setQuery(mediumQuery);
904 
905  // Large
906  // Must be no more than 4096x4096 - for our purposes, just return
907  // a fullsize image
908  QUrl largeURI = artURI;
909  QUrlQuery largeQuery(largeURI.query());
910  largeQuery.addQueryItem("Type", "fanart");
911  largeURI.setQuery(largeQuery);
912 
913  QList<Property*> propList = pItem->GetProperties("albumArtURI");
914  if (propList.size() >= 4)
915  {
916  Property *pProp = propList.at(0);
917  if (pProp)
918  {
919  pProp->SetValue(mediumURI.toEncoded());
920  pProp->AddAttribute("dlna:profileID", "JPEG_MED");
921  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
922  }
923 
924  pProp = propList.at(1);
925  if (pProp)
926  {
927 
928  pProp->SetValue(thumbURI.toEncoded());
929  pProp->AddAttribute("dlna:profileID", "JPEG_TN");
930  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
931  }
932 
933  pProp = propList.at(2);
934  if (pProp)
935  {
936  pProp->SetValue(smallURI.toEncoded());
937  pProp->AddAttribute("dlna:profileID", "JPEG_SM");
938  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
939  }
940 
941  pProp = propList.at(3);
942  if (pProp)
943  {
944  pProp->SetValue(largeURI.toEncoded());
945  pProp->AddAttribute("dlna:profileID", "JPEG_LRG");
946  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
947  }
948  }
949 
950  if (pItem->m_sClass.startsWith("object.item.videoItem"))
951  {
952  QString sProtocol;
953 
955  "image/jpeg", QSize(1024, 768));
956  pItem->AddResource( sProtocol, mediumURI.toEncoded());
957 
959  "image/jpeg", QSize(160, 160));
960  pItem->AddResource( sProtocol, thumbURI.toEncoded());
961 
963  "image/jpeg", QSize(640, 480));
964  pItem->AddResource( sProtocol, smallURI.toEncoded());
965 
967  "image/jpeg", QSize(1920, 1080)); // Not the actual res, we don't know that
968  pItem->AddResource( sProtocol, largeURI.toEncoded());
969  }
970 }
971 
972 QString UPnpCDSVideo::BuildWhereClause(QStringList clauses, IDTokenMap tokens)
973 {
974  if (tokens["video"].toInt() > 0)
975  clauses.append("v.intid=:VIDEO_ID");
976  if (!tokens["series"].isEmpty())
977  clauses.append("v.title=:TITLE");
978  if (!tokens["season"].isEmpty() && tokens["season"].toInt() >= 0) // Season 0 is valid
979  clauses.append("v.season=:SEASON");
980  if (!tokens["type"].isEmpty())
981  clauses.append("v.contenttype=:TYPE");
982  if (tokens["genre"].toInt() > 0)
983  clauses.append("v.category=:GENRE_ID");
984 
985  QString whereString;
986  if (!clauses.isEmpty())
987  {
988  whereString = " WHERE ";
989  whereString.append(clauses.join(" AND "));
990  }
991 
992  return whereString;
993 }
994 
996 {
997  if (tokens["video"].toInt() > 0)
998  query.bindValue(":VIDEO_ID", tokens["video"]);
999  if (!tokens["series"].isEmpty())
1000  query.bindValue(":TITLE", tokens["series"]);
1001  if (!tokens["season"].isEmpty() && tokens["season"].toInt() >= 0) // Season 0 is valid
1002  query.bindValue(":SEASON", tokens["season"]);
1003  if (!tokens["type"].isEmpty())
1004  query.bindValue(":TYPE", tokens["type"]);
1005  if (tokens["genre"].toInt() > 0)
1006  query.bindValue(":GENRE_ID", tokens["genre"]);
1007 }
CDSObject::GetProperties
QList< Property * > GetProperties(const QString &sName)
Definition: upnpcdsobjects.cpp:105
MSqlQuery::next
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:807
MSqlQuery
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
UPnpCDSRequest::m_nRequestedCount
uint16_t m_nRequestedCount
Definition: upnpcds.h:81
MythDate::toString
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:84
MSqlQuery::size
int size(void) const
Definition: mythdbcon.h:215
UPnpCDSExtension
Definition: upnpcds.h:203
ReferenceCounter::DecrRef
virtual int DecrRef(void)
Decrements reference count and deletes on 0.
Definition: referencecounter.cpp:125
UPNPProtocol::kHTTP
@ kHTTP
Definition: upnphelpers.h:133
CDSObject
Definition: upnpcdsobjects.h:184
MythDate::as_utc
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:27
UPnpCDSVideo::LoadSeries
bool LoadSeries(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdsvideo.cpp:372
UPnpCDSRequest::m_sParentId
QString m_sParentId
Definition: upnpcds.h:86
upnpcdsvideo.h
UPnPDateTime::DateTimeFormat
QString DateTimeFormat(const QDateTime &dateTime)
Date-Time Format.
Definition: upnphelpers.cpp:43
StorageGroup::FindFile
QString FindFile(const QString &filename)
Definition: storagegroup.cpp:602
MythCoreContext::GetBackendStatusPort
int GetBackendStatusPort(void)
Returns the locally defined backend status port.
Definition: mythcorecontext.cpp:1092
Resource
Definition: upnpcdsobjects.h:103
UPnpCDSExtension::CreateIDString
static QString CreateIDString(const QString &RequestId, const QString &Name, int Value)
Definition: upnpcds.cpp:1059
MSqlQuery::value
QVariant value(int i) const
Definition: mythdbcon.h:205
UPnpCDSVideo::CreateRoot
void CreateRoot() override
Definition: upnpcdsvideo.cpp:45
UPnpCDSExtension::m_sName
QString m_sName
Definition: upnpcds.h:208
UPnpCDSRequest::m_nStartingIndex
uint16_t m_nStartingIndex
Definition: upnpcds.h:80
MSqlQuery::exec
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:608
UPnpCDSExtension::IsSearchRequestForUs
virtual bool IsSearchRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:899
Resource::AddAttribute
void AddAttribute(const QString &sName, const QString &sValue)
Definition: upnpcdsobjects.h:121
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
CDSObject::CreateMovie
static CDSObject * CreateMovie(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:671
DLNA::ProtocolInfoString
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.
Definition: upnphelpers.cpp:315
UPnpCDSVideo::IsBrowseRequestForUs
bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest) override
Definition: upnpcdsvideo.cpp:149
UPnpCDSVideo::LoadVideos
bool LoadVideos(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdsvideo.cpp:610
UPnpCDSVideo::PopulateArtworkURIS
static void PopulateArtworkURIS(CDSObject *pItem, int nVidID, const QUrl &URIBase)
Definition: upnpcdsvideo.cpp:856
CDSObject::SetChildContainerCount
void SetChildContainerCount(uint32_t nCount)
Allows the caller to set childContainerCount without having to load children.
Definition: upnpcdsobjects.cpp:261
UPnPShortcutFeature::VIDEOS_ALL
@ VIDEOS_ALL
Definition: upnpcds.h:182
Property
Definition: upnpcdsobjects.h:45
Property::AddAttribute
void AddAttribute(const QString &sName, const QString &sValue)
Definition: upnpcdsobjects.h:86
UPnpCDSExtension::m_shortcuts
CDSShortCutList m_shortcuts
Definition: upnpcds.h:211
UPnpCDSVideo::m_uriBase
QUrl m_uriBase
Definition: upnpcdsvideo.h:79
mythdate.h
UPnPShortcutFeature::VIDEOS
@ VIDEOS
Definition: upnpcds.h:174
MSqlQuery::kDedicatedConnection
@ kDedicatedConnection
Definition: mythdbcon.h:229
Property::SetValue
void SetValue(const QString &value)
Definition: upnpcdsobjects.h:71
HTTPRequest::GetMimeType
static QString GetMimeType(const QString &sFileExtension)
Definition: httprequest.cpp:978
UPnpCDSExtensionResults::m_nTotalMatches
uint16_t m_nTotalMatches
Definition: upnpcds.h:114
UPnPShortcutFeature::VIDEOS_GENRES
@ VIDEOS_GENRES
Definition: upnpcds.h:175
MSqlQuery::InitCon
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:540
UPnpCDSVideo::LoadMovies
bool LoadMovies(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, IDTokenMap tokens)
Definition: upnpcdsvideo.cpp:525
UPnpCDSVideo::UPnpCDSVideo
UPnpCDSVideo()
Definition: upnpcdsvideo.cpp:29
UPnpCDSExtensionResults::Add
void Add(CDSObject *pObject)
Definition: upnpcds.cpp:33
CDSObject::SetPropValue
void SetPropValue(const QString &sName, const QString &sValue, const QString &type="")
Definition: upnpcdsobjects.cpp:123
MythCoreContext::GetBackendServerIP
QString GetBackendServerIP(void)
Returns the IP address of the locally defined backend IP.
Definition: mythcorecontext.cpp:1004
UPnpCDSExtension::m_pRoot
CDSObject * m_pRoot
Definition: upnpcds.h:249
UPnpCDSVideo::IsSearchRequestForUs
bool IsSearchRequestForUs(UPnpCDSRequest *pRequest) override
Definition: upnpcdsvideo.cpp:204
storagegroup.h
CDSObject::SetChildCount
void SetChildCount(uint32_t nCount)
Allows the caller to set childCount without having to load children.
Definition: upnpcdsobjects.cpp:238
UPnpCDSVideo::LoadSeasons
bool LoadSeasons(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdsvideo.cpp:448
UPnpCDSVideo::LoadGenres
static bool LoadGenres(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdsvideo.cpp:538
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:55
CDSObject::AddChild
CDSObject * AddChild(CDSObject *pChild)
Definition: upnpcdsobjects.cpp:171
UPnpCDSVideo::m_mapBackendPort
QMap< QString, int > m_mapBackendPort
Definition: upnpcdsvideo.h:77
UPnpCDSRequest::m_sObjectId
QString m_sObjectId
Definition: upnpcds.h:76
upnphelpers.h
UPnpCDSExtension::GetRoot
virtual CDSObject * GetRoot()
Definition: upnpcds.cpp:1092
mythcorecontext.h
CDSObject::AddResource
Resource * AddResource(const QString &sProtocol, const QString &sURI)
Definition: upnpcdsobjects.cpp:211
MSqlQuery::bindValue
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:883
MythDate::ISODate
@ ISODate
Default UTC.
Definition: mythdate.h:17
IDTokenMap
QMap< QString, QString > IDTokenMap
Definition: upnpcds.h:200
UPnpCDSVideo::LoadMetadata
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.
Definition: upnpcdsvideo.cpp:258
UPnpCDSVideo::BindValues
static void BindValues(MSqlQuery &query, IDTokenMap tokens)
Definition: upnpcdsvideo.cpp:995
CDSObject::CreateVideoItem
static CDSObject * CreateVideoItem(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:613
CDSObject::CreateContainer
static CDSObject * CreateContainer(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:478
StorageGroup
Definition: storagegroup.h:11
CDSObject::m_sClass
QString m_sClass
Definition: upnpcdsobjects.h:196
CDSObject::CreateAlbum
static CDSObject * CreateAlbum(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:818
uint16_t
unsigned short uint16_t
Definition: iso6937tables.h:3
UPnpCDSExtensionResults
Definition: upnpcds.h:106
UPnpCDSVideo::LoadChildren
bool LoadChildren(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens, const QString &currentToken) override
Fetch the children of the container identified in the request.
Definition: upnpcdsvideo.cpp:324
CDSObject::GetChildCount
uint32_t GetChildCount(void) const
Return the number of children in this container.
Definition: upnpcdsobjects.cpp:225
CDSObject::GetChild
CDSObject * GetChild(const QString &sID)
Definition: upnpcdsobjects.cpp:190
httprequest.h
CDSObject::CreateMovieGenre
static CDSObject * CreateMovieGenre(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:910
UPnpCDSVideo::m_mapBackendIp
QStringMap m_mapBackendIp
Definition: upnpcdsvideo.h:76
UPnPDateTime::resDurationFormat
QString resDurationFormat(std::chrono::milliseconds msec)
res@duration Format B.2.1.4 res@duration - UPnP ContentDirectory Service 2008, 2013
Definition: upnphelpers.cpp:81
UPnpCDSRequest
Definition: upnpcds.h:72
UPnpCDSVideo::BuildWhereClause
static QString BuildWhereClause(QStringList clauses, IDTokenMap tokens)
Definition: upnpcdsvideo.cpp:972
MSqlQuery::prepare
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:832
UPnpCDSExtension::m_sExtensionId
QString m_sExtensionId
Definition: upnpcds.h:207
UPnpCDSExtension::IsBrowseRequestForUs
virtual bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:812