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