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