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  LOG(VB_GENERAL, LOG_ERR, QString("UPnpCDSTV::LoadMetadata: Requested "
286  "object cannot be found: %1")
287  .arg(pRequest->m_sObjectId));
288  }
289  else if (currentToken == "series")
290  {
291  return LoadSeries(pRequest, pResults, tokens);
292  }
293  else if (currentToken == "season")
294  {
295  return LoadSeasons(pRequest, pResults, tokens);
296  }
297  else if (currentToken == "genre")
298  {
299  return LoadGenres(pRequest, pResults, tokens);
300  }
301  else if (currentToken == "movie")
302  {
303  return LoadMovies(pRequest, pResults, tokens);
304  }
305  else if (currentToken == "video")
306  {
307  return LoadVideos(pRequest, pResults, tokens);
308  }
309  else
310  LOG(VB_GENERAL, LOG_ERR,
311  QString("UPnpCDSVideo::LoadMetadata(): "
312  "Unhandled metadata request for '%1'.").arg(currentToken));
313 
314  return false;
315 }
316 
318 //
320 
322  UPnpCDSExtensionResults* pResults,
323  IDTokenMap tokens, QString currentToken)
324 {
325  if (currentToken.isEmpty() || currentToken == m_sExtensionId.toLower())
326  {
327  // Root
328  pResults->Add(GetRoot()->GetChildren());
329  pResults->m_nTotalMatches = GetRoot()->GetChildCount();
330  return true;
331  }
332  if (currentToken == "series")
333  {
334  if (!tokens["series"].isEmpty())
335  return LoadSeasons(pRequest, pResults, tokens);
336  return LoadSeries(pRequest, pResults, tokens);
337  }
338  if (currentToken == "season")
339  {
340  if (!tokens["season"].isEmpty() && tokens["season"].toInt() >= 0) // Season 0 is valid
341  return LoadVideos(pRequest, pResults, tokens);
342  return LoadSeasons(pRequest, pResults, tokens);
343  }
344  if (currentToken == "genre")
345  {
346  if (!tokens["genre"].isEmpty())
347  return LoadVideos(pRequest, pResults, tokens);
348  return LoadGenres(pRequest, pResults, tokens);
349  }
350  if (currentToken == "movie")
351  {
352  return LoadMovies(pRequest, pResults, tokens);
353  }
354  if (currentToken == "video")
355  {
356  return LoadVideos(pRequest, pResults, tokens);
357  }
358  LOG(VB_GENERAL, LOG_ERR,
359  QString("UPnpCDSVideo::LoadChildren(): "
360  "Unhandled metadata request for '%1'.").arg(currentToken));
361 
362  return false;
363 }
364 
366 //
368 
370  UPnpCDSExtensionResults* pResults,
371  const IDTokenMap& tokens)
372 {
373  QString sRequestId = pRequest->m_sObjectId;
374 
375  uint16_t nCount = pRequest->m_nRequestedCount;
376  uint16_t nOffset = pRequest->m_nStartingIndex;
377 
378  // We must use a dedicated connection to get an acccurate value from
379  // FOUND_ROWS()
381 
382  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
383  "v.title, COUNT(DISTINCT v.season), v.intid "
384  "FROM videometadata v "
385  "%1 " // whereString
386  "GROUP BY v.title "
387  "ORDER BY v.title "
388  "LIMIT :OFFSET,:COUNT ";
389 
390  QStringList clauses;
391  clauses.append("contenttype='TELEVISION'");
392  QString whereString = BuildWhereClause(clauses, tokens);
393 
394  query.prepare(sql.arg(whereString));
395 
396  BindValues(query, tokens);
397 
398  query.bindValue(":OFFSET", nOffset);
399  query.bindValue(":COUNT", nCount);
400 
401  if (!query.exec())
402  return false;
403 
404  while (query.next())
405  {
406  QString sTitle = query.value(0).toString();
407  int nSeasonCount = query.value(1).toInt();
408  int nVidID = query.value(2).toInt();
409 
410  // TODO Album or plain old container?
411  CDSObject* pContainer = CDSObject::CreateAlbum( CreateIDString(sRequestId, "Series", sTitle),
412  sTitle,
413  pRequest->m_sParentId,
414  nullptr );
415  pContainer->SetPropValue("description", QObject::tr("%n Seasons", "", nSeasonCount));
416  pContainer->SetPropValue("longdescription", QObject::tr("%n Seasons", "", nSeasonCount));
417  pContainer->SetPropValue("storageMedium", "HDD");
418 
419  pContainer->SetChildCount(nSeasonCount);
420  pContainer->SetChildContainerCount(nSeasonCount);
421 
422  PopulateArtworkURIS(pContainer, nVidID, m_URIBase);
423 
424  pResults->Add(pContainer);
425  pContainer->DecrRef();
426  }
427 
428  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
429  // at least the size of this result set
430  if (query.size() >= 0)
431  pResults->m_nTotalMatches = query.size();
432 
433  // Fetch the total number of matches ignoring any LIMITs
434  query.prepare("SELECT FOUND_ROWS()");
435  if (query.exec() && query.next())
436  pResults->m_nTotalMatches = query.value(0).toUInt();
437 
438  return true;
439 }
440 
442 //
444 
446  UPnpCDSExtensionResults* pResults,
447  const IDTokenMap& tokens)
448 {
449  QString sRequestId = pRequest->m_sObjectId;
450 
451  uint16_t nCount = pRequest->m_nRequestedCount;
452  uint16_t nOffset = pRequest->m_nStartingIndex;
453 
454  // We must use a dedicated connection to get an acccurate value from
455  // FOUND_ROWS()
457 
458  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
459  "v.season, COUNT(DISTINCT v.intid), v.intid "
460  "FROM videometadata v "
461  "%1 " // whereString
462  "GROUP BY v.season "
463  "ORDER BY v.season "
464  "LIMIT :OFFSET,:COUNT ";
465 
466  QStringList clauses;
467  QString whereString = BuildWhereClause(clauses, tokens);
468 
469  query.prepare(sql.arg(whereString));
470 
471  BindValues(query, tokens);
472 
473  query.bindValue(":OFFSET", nOffset);
474  query.bindValue(":COUNT", nCount);
475 
476  if (!query.exec())
477  return false;
478 
479  while (query.next())
480  {
481  int nSeason = query.value(0).toInt();
482  int nVideoCount = query.value(1).toInt();
483  int nVidID = query.value(2).toInt();
484 
485  QString sTitle = QObject::tr("Season %1").arg(nSeason);
486 
487  // TODO Album or plain old container?
488  CDSObject* pContainer = CDSObject::CreateAlbum( CreateIDString(sRequestId, "Season", nSeason),
489  sTitle,
490  pRequest->m_sParentId,
491  nullptr );
492  pContainer->SetPropValue("description", QObject::tr("%n Episode(s)", "", nVideoCount));
493  pContainer->SetPropValue("longdescription", QObject::tr("%n Episode(s)", "", nVideoCount));
494  pContainer->SetPropValue("storageMedium", "HDD");
495 
496  pContainer->SetChildCount(nVideoCount);
497  pContainer->SetChildContainerCount(0);
498 
499  PopulateArtworkURIS(pContainer, nVidID, m_URIBase);
500 
501  pResults->Add(pContainer);
502  pContainer->DecrRef();
503  }
504 
505  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
506  // at least the size of this result set
507  if (query.size() >= 0)
508  pResults->m_nTotalMatches = query.size();
509 
510  // Fetch the total number of matches ignoring any LIMITs
511  query.prepare("SELECT FOUND_ROWS()");
512  if (query.exec() && query.next())
513  pResults->m_nTotalMatches = query.value(0).toUInt();
514 
515  return true;
516 }
517 
519 //
521 
523  UPnpCDSExtensionResults* pResults,
524  IDTokenMap tokens)
525 {
526  tokens["type"] = "MOVIE";
527  //LoadGenres(pRequest, pResults, tokens);
528  return LoadVideos(pRequest, pResults, tokens);
529 }
530 
532 //
534 
536  UPnpCDSExtensionResults* pResults,
537  const IDTokenMap& tokens)
538 {
539  QString sRequestId = pRequest->m_sObjectId;
540 
541  uint16_t nCount = pRequest->m_nRequestedCount;
542  uint16_t nOffset = pRequest->m_nStartingIndex;
543 
544  // We must use a dedicated connection to get an acccurate value from
545  // FOUND_ROWS()
547 
548  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
549  "v.category, g.genre, COUNT(DISTINCT v.intid) "
550  "FROM videometadata v "
551  "LEFT JOIN videogenre g ON g.intid=v.category "
552  "%1 " // whereString
553  "GROUP BY g.intid "
554  "ORDER BY g.genre "
555  "LIMIT :OFFSET,:COUNT ";
556 
557  QStringList clauses;
558  clauses.append("v.category != 0");
559  QString whereString = BuildWhereClause(clauses, tokens);
560 
561  query.prepare(sql.arg(whereString));
562 
563  BindValues(query, tokens);
564 
565  query.bindValue(":OFFSET", nOffset);
566  query.bindValue(":COUNT", nCount);
567 
568  if (!query.exec())
569  return false;
570 
571  while (query.next())
572  {
573  int nGenreID = query.value(0).toInt();
574  QString sName = query.value(1).toString();
575  int nVideoCount = query.value(2).toInt();
576 
577  // TODO Album or plain old container?
578  CDSObject* pContainer = CDSObject::CreateMovieGenre( CreateIDString(sRequestId, "Genre", nGenreID),
579  sName,
580  pRequest->m_sParentId,
581  nullptr );
582 
583  pContainer->SetChildCount(nVideoCount);
584  pContainer->SetChildContainerCount(0);
585 
586  pResults->Add(pContainer);
587  pContainer->DecrRef();
588  }
589 
590  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
591  // at least the size of this result set
592  if (query.size() >= 0)
593  pResults->m_nTotalMatches = query.size();
594 
595  // Fetch the total number of matches ignoring any LIMITs
596  query.prepare("SELECT FOUND_ROWS()");
597  if (query.exec() && query.next())
598  pResults->m_nTotalMatches = query.value(0).toUInt();
599 
600  return true;
601 }
602 
604 //
606 
608  UPnpCDSExtensionResults* pResults,
609  const IDTokenMap& tokens)
610 {
611  QString sRequestId = pRequest->m_sObjectId;
612 
613  uint16_t nCount = pRequest->m_nRequestedCount;
614  uint16_t nOffset = pRequest->m_nStartingIndex;
615 
616  // We must use a dedicated connection to get an acccurate value from
617  // FOUND_ROWS()
619 
620  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
621  "v.intid, title, subtitle, filename, director, plot, "
622  "rating, year, userrating, length, "
623  "season, episode, coverfile, insertdate, host, "
624  "g.genre, studio, collectionref, contenttype "
625  "FROM videometadata v "
626  "LEFT JOIN videogenre g ON g.intid=v.category "
627  "%1 " //
628  "ORDER BY title, season, episode "
629  "LIMIT :OFFSET,:COUNT ";
630 
631  QStringList clauses;
632  QString whereString = BuildWhereClause(clauses, tokens);
633 
634  query.prepare(sql.arg(whereString));
635 
636  BindValues(query, tokens);
637 
638  query.bindValue(":OFFSET", nOffset);
639  query.bindValue(":COUNT", nCount);
640 
641  if (!query.exec())
642  return false;
643 
644  while (query.next())
645  {
646 
647  int nVidID = query.value( 0).toInt();
648  QString sTitle = query.value( 1).toString();
649  QString sSubtitle = query.value( 2).toString();
650  QString sFilePath = query.value( 3).toString();
651  QString sDirector = query.value( 4).toString();
652  QString sPlot = query.value( 5).toString();
653  // QString sRating = query.value( 6).toString();
654  int nYear = query.value( 7).toInt();
655  // int nUserRating = query.value( 8).toInt();
656 
657  uint32_t nLength = query.value( 9).toUInt();
658  // Convert from minutes to milliseconds
659  nLength = (nLength * 60 *1000);
660 
661  int nSeason = query.value(10).toInt();
662  int nEpisode = query.value(11).toInt();
663  QString sCoverArt = query.value(12).toString();
664  QDateTime dtInsertDate =
665  MythDate::as_utc(query.value(13).toDateTime());
666  QString sHostName = query.value(14).toString();
667  QString sGenre = query.value(15).toString();
668  // QString sStudio = query.value(16).toString();
669  // QString sCollectionRef = query.value(17).toString();
670  QString sContentType = query.value(18).toString();
671 
672  // ----------------------------------------------------------------------
673  // Cache Host ip Address & Port
674  // ----------------------------------------------------------------------
675 
676  // If the host-name is empty then we assume it is our local host
677  // otherwise, we look up the host's IP address and port. When the
678  // client then trys to play the video it will be directed to the
679  // host which actually has the content.
680  if (!m_mapBackendIp.contains( sHostName ))
681  {
682  if (sHostName.isEmpty())
683  {
684  m_mapBackendIp[sHostName] =
686  }
687  else
688  {
689  m_mapBackendIp[sHostName] =
690  gCoreContext->GetBackendServerIP(sHostName);
691  }
692  }
693 
694  if (!m_mapBackendPort.contains( sHostName ))
695  {
696  if (sHostName.isEmpty())
697  {
698  m_mapBackendPort[sHostName] =
700  }
701  else
702  {
703  m_mapBackendPort[sHostName] =
705  }
706  }
707 
708 
709  // ----------------------------------------------------------------------
710  // Build Support Strings
711  // ----------------------------------------------------------------------
712 
713  QString sName = sTitle;
714  if( !sSubtitle.isEmpty() )
715  {
716  sName += " - " + sSubtitle;
717  }
718 
719  QUrl URIBase;
720  URIBase.setScheme("http");
721  URIBase.setHost(m_mapBackendIp[sHostName]);
722  URIBase.setPort(m_mapBackendPort[sHostName]);
723 
724  CDSObject *pItem;
725  if (sContentType == "MOVIE")
726  {
727  pItem = CDSObject::CreateMovie( CreateIDString(sRequestId, "Video", nVidID),
728  sTitle,
729  pRequest->m_sParentId );
730  }
731  else
732  {
733  pItem = CDSObject::CreateVideoItem( CreateIDString(sRequestId, "Video", nVidID),
734  sName,
735  pRequest->m_sParentId );
736  }
737 
738  if (!sSubtitle.isEmpty())
739  pItem->SetPropValue( "description", sSubtitle );
740  else
741  pItem->SetPropValue( "description", sPlot.left(128).append(" ..."));
742  pItem->SetPropValue( "longDescription", sPlot );
743  pItem->SetPropValue( "director" , sDirector );
744 
745  if (nEpisode > 0 || nSeason > 0) // There has got to be a better way
746  {
747  pItem->SetPropValue( "seriesTitle" , sTitle );
748  pItem->SetPropValue( "programTitle" , sSubtitle );
749  pItem->SetPropValue( "episodeNumber" , QString::number(nEpisode));
750  //pItem->SetPropValue( "episodeCount" , nEpisodeCount);
751  }
752 
753  pItem->SetPropValue( "genre" , sGenre );
754  if (nYear > 1830 && nYear < 9999)
755  pItem->SetPropValue( "date", QDate(nYear,1,1).toString(Qt::ISODate));
756  else
757  pItem->SetPropValue( "date", UPnPDateTime::DateTimeFormat(dtInsertDate) );
758 
759  // HACK: Windows Media Centre Compat (Not a UPnP or DLNA requirement, should only be done for WMC)
760 // pItem->SetPropValue( "genre" , "[Unknown Genre]" );
761 // pItem->SetPropValue( "actor" , "[Unknown Author]" );
762 // pItem->SetPropValue( "creator" , "[Unknown Creator]" );
763 // pItem->SetPropValue( "album" , "[Unknown Album]" );
765 
766  //pItem->SetPropValue( "producer" , );
767  //pItem->SetPropValue( "rating" , );
768  //pItem->SetPropValue( "actor" , );
769  //pItem->SetPropValue( "publisher" , );
770  //pItem->SetPropValue( "language" , );
771  //pItem->SetPropValue( "relation" , );
772  //pItem->SetPropValue( "region" , );
773 
774  // Only add the reference ID for items which are not in the
775  // 'All Videos' container
776  QString sRefIDBase = QString("%1/Video").arg(m_sExtensionId);
777  if ( pRequest->m_sParentId != sRefIDBase )
778  {
779  QString sRefId = QString( "%1=%2")
780  .arg( sRefIDBase )
781  .arg( nVidID );
782 
783  pItem->SetPropValue( "refID", sRefId );
784  }
785 
786  // FIXME - If the slave or storage hosting this video is offline we
787  // won't find it. We probably shouldn't list it, but better
788  // still would be storing the filesize in the database so we
789  // don't waste time re-checking it constantly
790  QString sFullFileName = sFilePath;
791  if (!QFile::exists( sFullFileName ))
792  {
793  StorageGroup sgroup("Videos");
794  sFullFileName = sgroup.FindFile( sFullFileName );
795  }
796  QFileInfo fInfo( sFullFileName );
797 
798  // ----------------------------------------------------------------------
799  // Add Video Resource Element based on File extension (HTTP)
800  // ----------------------------------------------------------------------
801 
802  QString sMimeType = HTTPRequest::GetMimeType( QFileInfo(sFilePath).suffix() );
803 
804  // HACK: If we are dealing with a Sony Blu-ray player then we fake the
805  // MIME type to force the video to appear
806 // if ( pRequest->m_eClient == CDS_ClientSonyDB )
807 // {
808 // sMimeType = "video/avi";
809 // }
810 
811  QUrl resURI = URIBase;
812  QUrlQuery resQuery;
813  resURI.setPath("/Content/GetVideo");
814  resQuery.addQueryItem("Id", QString::number(nVidID));
815  resURI.setQuery(resQuery);
816 
817  // DLNA requires a mimetype of video/mp2p for TS files, it's not the
818  // correct mimetype, but then DLNA doesn't seem to care about such
819  // things
820  if (sMimeType == "video/mp2t" || sMimeType == "video/mp2p")
821  sMimeType = "video/mpeg";
822 
823  QString sProtocol = DLNA::ProtocolInfoString(UPNPProtocol::kHTTP,
824  sMimeType);
825 
826  Resource *pRes = pItem->AddResource( sProtocol, resURI.toEncoded() );
827  pRes->AddAttribute( "size" , QString("%1").arg(fInfo.size()) );
828  pRes->AddAttribute( "duration", UPnPDateTime::resDurationFormat(nLength) );
829 
830  // ----------------------------------------------------------------------
831  // Add Artwork
832  // ----------------------------------------------------------------------
833  if (!sCoverArt.isEmpty() && (sCoverArt != "No Cover"))
834  {
835  PopulateArtworkURIS(pItem, nVidID, URIBase);
836  }
837 
838  pResults->Add( pItem );
839  pItem->DecrRef();
840  }
841 
842  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
843  // at least the size of this result set
844  if (query.size() >= 0)
845  pResults->m_nTotalMatches = query.size();
846 
847  // Fetch the total number of matches ignoring any LIMITs
848  query.prepare("SELECT FOUND_ROWS()");
849  if (query.exec() && query.next())
850  pResults->m_nTotalMatches = query.value(0).toUInt();
851 
852  return true;
853 }
854 
856  const QUrl& URIBase)
857 {
858  QUrl artURI = URIBase;
859  artURI.setPath("/Content/GetVideoArtwork");
860  QUrlQuery artQuery;
861  artQuery.addQueryItem("Id", QString::number(nVidID));
862  artURI.setQuery(artQuery);
863 
864  // Prefer JPEG over PNG here, although PNG is allowed JPEG probably
865  // has wider device support and crucially the filesizes are smaller
866  // which speeds up loading times over the network
867 
868  // We MUST include the thumbnail size, but since some clients may use the
869  // first image they see and the thumbnail is tiny, instead return the
870  // medium first. The large could be very large, which is no good if the
871  // client is pulling images for an entire list at once!
872 
873  // Thumbnail
874  // At least one albumArtURI must be a ThumbNail (TN) no larger
875  // than 160x160, and it must also be a jpeg
876  QUrl thumbURI = artURI;
877  QUrlQuery thumbQuery(thumbURI.query());
878  if (pItem->m_sClass == "object.item.videoItem") // Show screenshot for TV, coverart for movies
879  thumbQuery.addQueryItem("Type", "screenshot");
880  else
881  thumbQuery.addQueryItem("Type", "coverart");
882  thumbQuery.addQueryItem("Width", "160");
883  thumbQuery.addQueryItem("Height", "160");
884  thumbURI.setQuery(thumbQuery);
885 
886  // Small
887  // Must be no more than 640x480
888  QUrl smallURI = artURI;
889  QUrlQuery smallQuery(smallURI.query());
890  smallQuery.addQueryItem("Type", "coverart");
891  smallQuery.addQueryItem("Width", "640");
892  smallQuery.addQueryItem("Height", "480");
893  smallURI.setQuery(smallQuery);
894 
895  // Medium
896  // Must be no more than 1024x768
897  QUrl mediumURI = artURI;
898  QUrlQuery mediumQuery(mediumURI.query());
899  mediumQuery.addQueryItem("Type", "coverart");
900  mediumQuery.addQueryItem("Width", "1024");
901  mediumQuery.addQueryItem("Height", "768");
902  mediumURI.setQuery(mediumQuery);
903 
904  // Large
905  // Must be no more than 4096x4096 - for our purposes, just return
906  // a fullsize image
907  QUrl largeURI = artURI;
908  QUrlQuery largeQuery(largeURI.query());
909  largeQuery.addQueryItem("Type", "fanart");
910  largeURI.setQuery(largeQuery);
911 
912  QList<Property*> propList = pItem->GetProperties("albumArtURI");
913  if (propList.size() >= 4)
914  {
915  Property *pProp = propList.at(0);
916  if (pProp)
917  {
918  pProp->SetValue(mediumURI.toEncoded());
919  pProp->AddAttribute("dlna:profileID", "JPEG_MED");
920  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
921  }
922 
923  pProp = propList.at(1);
924  if (pProp)
925  {
926 
927  pProp->SetValue(thumbURI.toEncoded());
928  pProp->AddAttribute("dlna:profileID", "JPEG_TN");
929  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
930  }
931 
932  pProp = propList.at(2);
933  if (pProp)
934  {
935  pProp->SetValue(smallURI.toEncoded());
936  pProp->AddAttribute("dlna:profileID", "JPEG_SM");
937  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
938  }
939 
940  pProp = propList.at(3);
941  if (pProp)
942  {
943  pProp->SetValue(largeURI.toEncoded());
944  pProp->AddAttribute("dlna:profileID", "JPEG_LRG");
945  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
946  }
947  }
948 
949  if (pItem->m_sClass.startsWith("object.item.videoItem"))
950  {
951  QString sProtocol;
952 
954  "image/jpeg", QSize(1024, 768));
955  pItem->AddResource( sProtocol, mediumURI.toEncoded());
956 
958  "image/jpeg", QSize(160, 160));
959  pItem->AddResource( sProtocol, thumbURI.toEncoded());
960 
962  "image/jpeg", QSize(640, 480));
963  pItem->AddResource( sProtocol, smallURI.toEncoded());
964 
966  "image/jpeg", QSize(1920, 1080)); // Not the actual res, we don't know that
967  pItem->AddResource( sProtocol, largeURI.toEncoded());
968  }
969 }
970 
971 QString UPnpCDSVideo::BuildWhereClause(QStringList clauses, IDTokenMap tokens)
972 {
973  if (tokens["video"].toInt() > 0)
974  clauses.append("v.intid=:VIDEO_ID");
975  if (!tokens["series"].isEmpty())
976  clauses.append("v.title=:TITLE");
977  if (!tokens["season"].isEmpty() && tokens["season"].toInt() >= 0) // Season 0 is valid
978  clauses.append("v.season=:SEASON");
979  if (!tokens["type"].isEmpty())
980  clauses.append("v.contenttype=:TYPE");
981  if (tokens["genre"].toInt() > 0)
982  clauses.append("v.category=:GENRE_ID");
983 
984  QString whereString;
985  if (!clauses.isEmpty())
986  {
987  whereString = " WHERE ";
988  whereString.append(clauses.join(" AND "));
989  }
990 
991  return whereString;
992 }
993 
995 {
996  if (tokens["video"].toInt() > 0)
997  query.bindValue(":VIDEO_ID", tokens["video"]);
998  if (!tokens["series"].isEmpty())
999  query.bindValue(":TITLE", tokens["series"]);
1000  if (!tokens["season"].isEmpty() && tokens["season"].toInt() >= 0) // Season 0 is valid
1001  query.bindValue(":SEASON", tokens["season"]);
1002  if (!tokens["type"].isEmpty())
1003  query.bindValue(":TYPE", tokens["type"]);
1004  if (tokens["genre"].toInt() > 0)
1005  query.bindValue(":GENRE_ID", tokens["genre"]);
1006 }
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
void PopulateArtworkURIS(CDSObject *pItem, int nVidID, const QUrl &URIBase)
QString m_sParentId
Definition: upnpcds.h:83
virtual CDSObject * GetRoot()
Definition: upnpcds.cpp:1083
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)
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:203
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:111
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:204
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:198
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
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)
QString CreateIDString(const QString &RequestId, const QString &Name, int Value)
Definition: upnpcds.cpp:1050
virtual bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:804
QString m_sName
Definition: upnpcds.h:205
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)
#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)
uint16_t m_nStartingIndex
Definition: upnpcds.h:77
QString BuildWhereClause(QStringList clauses, IDTokenMap tokens)
bool LoadSeasons(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
bool LoadGenres(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
CDSShortCutList m_shortcuts
Definition: upnpcds.h:208
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
QMap< QString, QString > IDTokenMap
Definition: upnpcds.h:197
Default UTC.
Definition: mythdate.h:14
CDSObject * m_pRoot
Definition: upnpcds.h:246
void BindValues(MSqlQuery &query, IDTokenMap tokens)