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  {
310  LOG(VB_GENERAL, LOG_ERR,
311  QString("UPnpCDSVideo::LoadMetadata(): "
312  "Unhandled metadata request for '%1'.").arg(currentToken));
313  }
314 
315  return false;
316 }
317 
319 //
321 
323  UPnpCDSExtensionResults* pResults,
324  const IDTokenMap& tokens, const QString& currentToken)
325 {
326  if (currentToken.isEmpty() || currentToken == m_sExtensionId.toLower())
327  {
328  // Root
329  pResults->Add(GetRoot()->GetChildren());
330  pResults->m_nTotalMatches = GetRoot()->GetChildCount();
331  return true;
332  }
333  if (currentToken == "series")
334  {
335  if (!tokens["series"].isEmpty())
336  return LoadSeasons(pRequest, pResults, tokens);
337  return LoadSeries(pRequest, pResults, tokens);
338  }
339  if (currentToken == "season")
340  {
341  if (!tokens["season"].isEmpty() && tokens["season"].toInt() >= 0) // Season 0 is valid
342  return LoadVideos(pRequest, pResults, tokens);
343  return LoadSeasons(pRequest, pResults, tokens);
344  }
345  if (currentToken == "genre")
346  {
347  if (!tokens["genre"].isEmpty())
348  return LoadVideos(pRequest, pResults, tokens);
349  return LoadGenres(pRequest, pResults, tokens);
350  }
351  if (currentToken == "movie")
352  {
353  return LoadMovies(pRequest, pResults, tokens);
354  }
355  if (currentToken == "video")
356  {
357  return LoadVideos(pRequest, pResults, tokens);
358  }
359  LOG(VB_GENERAL, LOG_ERR,
360  QString("UPnpCDSVideo::LoadChildren(): "
361  "Unhandled metadata request for '%1'.").arg(currentToken));
362 
363  return false;
364 }
365 
367 //
369 
371  UPnpCDSExtensionResults* pResults,
372  const IDTokenMap& tokens)
373 {
374  QString sRequestId = pRequest->m_sObjectId;
375 
376  uint16_t nCount = pRequest->m_nRequestedCount;
377  uint16_t nOffset = pRequest->m_nStartingIndex;
378 
379  // We must use a dedicated connection to get an acccurate value from
380  // FOUND_ROWS()
382 
383  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
384  "v.title, COUNT(DISTINCT v.season), v.intid "
385  "FROM videometadata v "
386  "%1 " // whereString
387  "GROUP BY v.title "
388  "ORDER BY v.title "
389  "LIMIT :OFFSET,:COUNT ";
390 
391  QStringList clauses;
392  clauses.append("contenttype='TELEVISION'");
393  QString whereString = BuildWhereClause(clauses, tokens);
394 
395  query.prepare(sql.arg(whereString));
396 
397  BindValues(query, tokens);
398 
399  query.bindValue(":OFFSET", nOffset);
400  query.bindValue(":COUNT", nCount);
401 
402  if (!query.exec())
403  return false;
404 
405  while (query.next())
406  {
407  QString sTitle = query.value(0).toString();
408  int nSeasonCount = query.value(1).toInt();
409  int nVidID = query.value(2).toInt();
410 
411  // TODO Album or plain old container?
412  CDSObject* pContainer = CDSObject::CreateAlbum( CreateIDString(sRequestId, "Series", sTitle),
413  sTitle,
414  pRequest->m_sParentId,
415  nullptr );
416  pContainer->SetPropValue("description", QObject::tr("%n Seasons", "", nSeasonCount));
417  pContainer->SetPropValue("longdescription", QObject::tr("%n Seasons", "", nSeasonCount));
418  pContainer->SetPropValue("storageMedium", "HDD");
419 
420  pContainer->SetChildCount(nSeasonCount);
421  pContainer->SetChildContainerCount(nSeasonCount);
422 
423  PopulateArtworkURIS(pContainer, nVidID, m_uriBase);
424 
425  pResults->Add(pContainer);
426  pContainer->DecrRef();
427  }
428 
429  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
430  // at least the size of this result set
431  if (query.size() >= 0)
432  pResults->m_nTotalMatches = query.size();
433 
434  // Fetch the total number of matches ignoring any LIMITs
435  query.prepare("SELECT FOUND_ROWS()");
436  if (query.exec() && query.next())
437  pResults->m_nTotalMatches = query.value(0).toUInt();
438 
439  return true;
440 }
441 
443 //
445 
447  UPnpCDSExtensionResults* pResults,
448  const IDTokenMap& tokens)
449 {
450  QString sRequestId = pRequest->m_sObjectId;
451 
452  uint16_t nCount = pRequest->m_nRequestedCount;
453  uint16_t nOffset = pRequest->m_nStartingIndex;
454 
455  // We must use a dedicated connection to get an acccurate value from
456  // FOUND_ROWS()
458 
459  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
460  "v.season, COUNT(DISTINCT v.intid), v.intid "
461  "FROM videometadata v "
462  "%1 " // whereString
463  "GROUP BY v.season "
464  "ORDER BY v.season "
465  "LIMIT :OFFSET,:COUNT ";
466 
467  QStringList clauses;
468  QString whereString = BuildWhereClause(clauses, tokens);
469 
470  query.prepare(sql.arg(whereString));
471 
472  BindValues(query, tokens);
473 
474  query.bindValue(":OFFSET", nOffset);
475  query.bindValue(":COUNT", nCount);
476 
477  if (!query.exec())
478  return false;
479 
480  while (query.next())
481  {
482  int nSeason = query.value(0).toInt();
483  int nVideoCount = query.value(1).toInt();
484  int nVidID = query.value(2).toInt();
485 
486  QString sTitle = QObject::tr("Season %1").arg(nSeason);
487 
488  // TODO Album or plain old container?
489  CDSObject* pContainer = CDSObject::CreateAlbum( CreateIDString(sRequestId, "Season", nSeason),
490  sTitle,
491  pRequest->m_sParentId,
492  nullptr );
493  pContainer->SetPropValue("description", QObject::tr("%n Episode(s)", "", nVideoCount));
494  pContainer->SetPropValue("longdescription", QObject::tr("%n Episode(s)", "", nVideoCount));
495  pContainer->SetPropValue("storageMedium", "HDD");
496 
497  pContainer->SetChildCount(nVideoCount);
498  pContainer->SetChildContainerCount(0);
499 
500  PopulateArtworkURIS(pContainer, nVidID, m_uriBase);
501 
502  pResults->Add(pContainer);
503  pContainer->DecrRef();
504  }
505 
506  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
507  // at least the size of this result set
508  if (query.size() >= 0)
509  pResults->m_nTotalMatches = query.size();
510 
511  // Fetch the total number of matches ignoring any LIMITs
512  query.prepare("SELECT FOUND_ROWS()");
513  if (query.exec() && query.next())
514  pResults->m_nTotalMatches = query.value(0).toUInt();
515 
516  return true;
517 }
518 
520 //
522 
524  UPnpCDSExtensionResults* pResults,
525  IDTokenMap tokens)
526 {
527  tokens["type"] = "MOVIE";
528  //LoadGenres(pRequest, pResults, tokens);
529  return LoadVideos(pRequest, pResults, tokens);
530 }
531 
533 //
535 
537  UPnpCDSExtensionResults* pResults,
538  const IDTokenMap& tokens)
539 {
540  QString sRequestId = pRequest->m_sObjectId;
541 
542  uint16_t nCount = pRequest->m_nRequestedCount;
543  uint16_t nOffset = pRequest->m_nStartingIndex;
544 
545  // We must use a dedicated connection to get an acccurate value from
546  // FOUND_ROWS()
548 
549  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
550  "v.category, g.genre, COUNT(DISTINCT v.intid) "
551  "FROM videometadata v "
552  "LEFT JOIN videogenre g ON g.intid=v.category "
553  "%1 " // whereString
554  "GROUP BY g.intid "
555  "ORDER BY g.genre "
556  "LIMIT :OFFSET,:COUNT ";
557 
558  QStringList clauses;
559  clauses.append("v.category != 0");
560  QString whereString = BuildWhereClause(clauses, tokens);
561 
562  query.prepare(sql.arg(whereString));
563 
564  BindValues(query, tokens);
565 
566  query.bindValue(":OFFSET", nOffset);
567  query.bindValue(":COUNT", nCount);
568 
569  if (!query.exec())
570  return false;
571 
572  while (query.next())
573  {
574  int nGenreID = query.value(0).toInt();
575  QString sName = query.value(1).toString();
576  int nVideoCount = query.value(2).toInt();
577 
578  // TODO Album or plain old container?
579  CDSObject* pContainer = CDSObject::CreateMovieGenre( CreateIDString(sRequestId, "Genre", nGenreID),
580  sName,
581  pRequest->m_sParentId,
582  nullptr );
583 
584  pContainer->SetChildCount(nVideoCount);
585  pContainer->SetChildContainerCount(0);
586 
587  pResults->Add(pContainer);
588  pContainer->DecrRef();
589  }
590 
591  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
592  // at least the size of this result set
593  if (query.size() >= 0)
594  pResults->m_nTotalMatches = query.size();
595 
596  // Fetch the total number of matches ignoring any LIMITs
597  query.prepare("SELECT FOUND_ROWS()");
598  if (query.exec() && query.next())
599  pResults->m_nTotalMatches = query.value(0).toUInt();
600 
601  return true;
602 }
603 
605 //
607 
609  UPnpCDSExtensionResults* pResults,
610  const IDTokenMap& tokens)
611 {
612  QString sRequestId = pRequest->m_sObjectId;
613 
614  uint16_t nCount = pRequest->m_nRequestedCount;
615  uint16_t nOffset = pRequest->m_nStartingIndex;
616 
617  // We must use a dedicated connection to get an acccurate value from
618  // FOUND_ROWS()
620 
621  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
622  "v.intid, title, subtitle, filename, director, plot, "
623  "rating, year, userrating, length, "
624  "season, episode, coverfile, insertdate, host, "
625  "g.genre, studio, collectionref, contenttype "
626  "FROM videometadata v "
627  "LEFT JOIN videogenre g ON g.intid=v.category "
628  "%1 " //
629  "ORDER BY title, season, episode "
630  "LIMIT :OFFSET,:COUNT ";
631 
632  QStringList clauses;
633  QString whereString = BuildWhereClause(clauses, tokens);
634 
635  query.prepare(sql.arg(whereString));
636 
637  BindValues(query, tokens);
638 
639  query.bindValue(":OFFSET", nOffset);
640  query.bindValue(":COUNT", nCount);
641 
642  if (!query.exec())
643  return false;
644 
645  while (query.next())
646  {
647 
648  int nVidID = query.value( 0).toInt();
649  QString sTitle = query.value( 1).toString();
650  QString sSubtitle = query.value( 2).toString();
651  QString sFilePath = query.value( 3).toString();
652  QString sDirector = query.value( 4).toString();
653  QString sPlot = query.value( 5).toString();
654  // QString sRating = query.value( 6).toString();
655  int nYear = query.value( 7).toInt();
656  // int nUserRating = query.value( 8).toInt();
657 
658  uint32_t nLength = query.value( 9).toUInt();
659  // Convert from minutes to milliseconds
660  nLength = (nLength * 60 *1000);
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:99
MSqlQuery::next
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:783
MSqlQuery
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:126
UPnpCDSRequest::m_nRequestedCount
uint16_t m_nRequestedCount
Definition: upnpcds.h:80
MSqlQuery::size
int size(void) const
Definition: mythdbcon.h:203
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
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:129
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:23
UPnpCDSVideo::LoadSeries
bool LoadSeries(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdsvideo.cpp:370
UPnpCDSRequest::m_sParentId
QString m_sParentId
Definition: upnpcds.h:85
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:1097
Resource
Definition: upnpcdsobjects.h:103
UPnpCDSExtension::CreateIDString
static QString CreateIDString(const QString &RequestId, const QString &Name, int Value)
Definition: upnpcds.cpp:1057
MSqlQuery::value
QVariant value(int i) const
Definition: mythdbcon.h:198
arg
arg(title).arg(filename).arg(doDelete))
UPnpCDSVideo::CreateRoot
void CreateRoot() override
Definition: upnpcdsvideo.cpp:43
UPnpCDSExtension::m_sName
QString m_sName
Definition: upnpcds.h:207
UPnpCDSRequest::m_nStartingIndex
uint16_t m_nStartingIndex
Definition: upnpcds.h:79
MSqlQuery::exec
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:603
UPnpCDSExtension::IsSearchRequestForUs
virtual bool IsSearchRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:896
Resource::AddAttribute
void AddAttribute(const QString &sName, const QString &sValue)
Definition: upnpcdsobjects.h:120
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:23
CDSObject::CreateMovie
static CDSObject * CreateMovie(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:661
UPnpCDSVideo::IsBrowseRequestForUs
bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest) override
Definition: upnpcdsvideo.cpp:147
UPnpCDSVideo::LoadVideos
bool LoadVideos(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdsvideo.cpp:608
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:255
UPnPShortcutFeature::VIDEOS_ALL
@ VIDEOS_ALL
Definition: upnpcds.h:181
Property
Definition: upnpcdsobjects.h:45
Property::AddAttribute
void AddAttribute(const QString &sName, const QString &sValue)
Definition: upnpcdsobjects.h:85
UPnpCDSExtension::m_shortcuts
CDSShortCutList m_shortcuts
Definition: upnpcds.h:210
UPnpCDSVideo::m_uriBase
QUrl m_uriBase
Definition: upnpcdsvideo.h:79
toString
QString toString(MarkTypes type)
Definition: programtypes.cpp:26
mythdate.h
UPnPShortcutFeature::VIDEOS
@ VIDEOS
Definition: upnpcds.h:173
MSqlQuery::kDedicatedConnection
@ kDedicatedConnection
Definition: mythdbcon.h:217
Property::SetValue
void SetValue(const QString &value)
Definition: upnpcdsobjects.h:70
HTTPRequest::GetMimeType
static QString GetMimeType(const QString &sFileExtension)
Definition: httprequest.cpp:1041
UPnpCDSExtensionResults::m_nTotalMatches
uint16_t m_nTotalMatches
Definition: upnpcds.h:113
UPnPShortcutFeature::VIDEOS_GENRES
@ VIDEOS_GENRES
Definition: upnpcds.h:174
MSqlQuery::InitCon
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:535
UPnpCDSVideo::LoadMovies
bool LoadMovies(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, IDTokenMap tokens)
Definition: upnpcdsvideo.cpp:523
UPnpCDSVideo::UPnpCDSVideo
UPnpCDSVideo()
Definition: upnpcdsvideo.cpp:27
UPnpCDSExtensionResults::Add
void Add(CDSObject *pObject)
Definition: upnpcds.cpp:30
CDSObject::SetPropValue
void SetPropValue(const QString &sName, const QString &sValue, const QString &type="")
Definition: upnpcdsobjects.cpp:117
MythCoreContext::GetBackendServerIP
QString GetBackendServerIP(void)
Returns the IP address of the locally defined backend IP.
Definition: mythcorecontext.cpp:1021
UPnpCDSExtension::m_pRoot
CDSObject * m_pRoot
Definition: upnpcds.h:248
UPnpCDSVideo::IsSearchRequestForUs
bool IsSearchRequestForUs(UPnpCDSRequest *pRequest) override
Definition: upnpcdsvideo.cpp:202
storagegroup.h
CDSObject::SetChildCount
void SetChildCount(uint32_t nCount)
Allows the caller to set childCount without having to load children.
Definition: upnpcdsobjects.cpp:232
UPnpCDSVideo::LoadSeasons
bool LoadSeasons(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdsvideo.cpp:446
UPnpCDSVideo::LoadGenres
static bool LoadGenres(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdsvideo.cpp:536
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:56
CDSObject::AddChild
CDSObject * AddChild(CDSObject *pChild)
Definition: upnpcdsobjects.cpp:165
UPnpCDSVideo::m_mapBackendPort
QMap< QString, int > m_mapBackendPort
Definition: upnpcdsvideo.h:77
UPnpCDSRequest::m_sObjectId
QString m_sObjectId
Definition: upnpcds.h:75
upnphelpers.h
UPnpCDSExtension::GetRoot
virtual CDSObject * GetRoot()
Definition: upnpcds.cpp:1090
mythcorecontext.h
CDSObject::AddResource
Resource * AddResource(const QString &sProtocol, const QString &sURI)
Definition: upnpcdsobjects.cpp:205
MSqlQuery::bindValue
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:864
MythDate::ISODate
@ ISODate
Default UTC.
Definition: mythdate.h:14
IDTokenMap
QMap< QString, QString > IDTokenMap
Definition: upnpcds.h:199
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:256
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:603
CDSObject::CreateContainer
static CDSObject * CreateContainer(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:468
StorageGroup
Definition: storagegroup.h:12
CDSObject::m_sClass
QString m_sClass
Definition: upnpcdsobjects.h:195
CDSObject::CreateAlbum
static CDSObject * CreateAlbum(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:808
uint16_t
unsigned short uint16_t
Definition: iso6937tables.h:3
UPnpCDSExtensionResults
Definition: upnpcds.h:106
UPnPDateTime::resDurationFormat
QString resDurationFormat(uint32_t msec)
res@duration Format B.2.1.4 res@duration - UPnP ContentDirectory Service 2008, 2013
Definition: upnphelpers.cpp:81
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:322
CDSObject::GetChildCount
uint32_t GetChildCount(void) const
Return the number of children in this container.
Definition: upnpcdsobjects.cpp:219
CDSObject::GetChild
CDSObject * GetChild(const QString &sID)
Definition: upnpcdsobjects.cpp:184
httprequest.h
query
MSqlQuery query(MSqlQuery::InitCon())
CDSObject::CreateMovieGenre
static CDSObject * CreateMovieGenre(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:900
UPnpCDSVideo::m_mapBackendIp
QStringMap m_mapBackendIp
Definition: upnpcdsvideo.h:76
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:808
UPnpCDSExtension::m_sExtensionId
QString m_sExtensionId
Definition: upnpcds.h:206
UPnpCDSExtension::IsBrowseRequestForUs
virtual bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:809