MythTV  master
upnpcdstv.cpp
Go to the documentation of this file.
1 // Program Name: upnpcdstv.cpp
3 //
4 // Purpose - uPnp Content Directory Extension for Recorded TV
5 //
6 // Created By : David Blain Created On : Jan. 24, 2005
7 // Modified By : Modified On:
8 //
10 
11 // C++ headers
12 #include <climits>
13 #include <cstdint>
14 
15 // Qt headers
16 #include <QSize>
17 #include <QUrl>
18 #include <QUrlQuery>
19 
20 // MythTV headers
22 #include "libmythbase/mythdate.h"
27 
28 // MythBackend
29 #include "upnpcdstv.h"
30 
31 /*
32  Recordings RecTv
33  - All Programs RecTv/All
34  + <recording 1> RecTv/All/item?ChanId=1004&StartTime=2006-04-06T20:00:00
35  + <recording 2>
36  + <recording 3>
37  - By Title RecTv/title
38  - <title 1> RecTv/title/key=Stargate SG-1
39  + <recording 1> RecTv/title/key=Stargate SG-1/item?ChanId=1004&StartTime=2006-04-06T20:00:00
40  + <recording 2>
41  - By Genre
42  - By Date
43  - By Channel
44  - By Group
45 */
46 
47 
48 // UPnpCDSRootInfo UPnpCDSTv::g_RootNodes[] =
49 // {
50 // { "All Recordings",
51 // "*",
52 // "SELECT 0 as key, "
53 // "CONCAT( title, ': ', subtitle) as name, "
54 // "1 as children "
55 // "FROM recorded r "
56 // "%1 "
57 // "ORDER BY r.starttime DESC",
58 // "",
59 // "r.starttime DESC",
60 // "object.container",
61 // "object.item.videoItem" },
62 //
63 // { "By Title",
64 // "r.title",
65 // "SELECT r.title as id, "
66 // "r.title as name, "
67 // "count( r.title ) as children "
68 // "FROM recorded r "
69 // "%1 "
70 // "GROUP BY r.title "
71 // "ORDER BY r.title",
72 // "WHERE r.title=:KEY",
73 // "r.title",
74 // "object.container",
75 // "object.container" },
76 //
77 // { "By Genre",
78 // "r.category",
79 // "SELECT r.category as id, "
80 // "r.category as name, "
81 // "count( r.category ) as children "
82 // "FROM recorded r "
83 // "%1 "
84 // "GROUP BY r.category "
85 // "ORDER BY r.category",
86 // "WHERE r.category=:KEY",
87 // "r.category",
88 // "object.container",
89 // "object.container.genre.movieGenre" },
90 //
91 // { "By Date",
92 // "DATE_FORMAT(r.starttime, '%Y-%m-%d')",
93 // "SELECT DATE_FORMAT(r.starttime, '%Y-%m-%d') as id, "
94 // "DATE_FORMAT(r.starttime, '%Y-%m-%d %W') as name, "
95 // "count( DATE_FORMAT(r.starttime, '%Y-%m-%d %W') ) as children "
96 // "FROM recorded r "
97 // "%1 "
98 // "GROUP BY name "
99 // "ORDER BY r.starttime DESC",
100 // "WHERE DATE_FORMAT(r.starttime, '%Y-%m-%d') =:KEY",
101 // "r.starttime DESC",
102 // "object.container",
103 // "object.container"
104 // },
105 //
106 // { "By Channel",
107 // "r.chanid",
108 // "SELECT channel.chanid as id, "
109 // "CONCAT(channel.channum, ' ', channel.callsign) as name, "
110 // "count( channum ) as children "
111 // "FROM channel "
112 // "INNER JOIN recorded r ON channel.chanid = r.chanid "
113 // "%1 "
114 // "GROUP BY name "
115 // "ORDER BY channel.chanid",
116 // "WHERE channel.chanid=:KEY",
117 // "",
118 // "object.container",
119 // "object.container"}, // Cannot be .channelGroup because children of channelGroup must be videoBroadcast items
120 //
121 // { "By Group",
122 // "recgroup",
123 // "SELECT recgroup as id, "
124 // "recgroup as name, count( recgroup ) as children "
125 // "FROM recorded "
126 // "%1 "
127 // "GROUP BY recgroup "
128 // "ORDER BY recgroup",
129 // "WHERE recgroup=:KEY",
130 // "recgroup",
131 // "object.container",
132 // "object.container.album" }
133 // };
134 
136  : UPnpCDSExtension( QObject::tr("Recordings"), "Recordings",
137  "object.item.videoItem" )
138 {
139  QString sServerIp = gCoreContext->GetBackendServerIP();
140  int sPort = gCoreContext->GetBackendStatusPort();
141  m_uriBase.setScheme("http");
142  m_uriBase.setHost(sServerIp);
143  m_uriBase.setPort(sPort);
144 
145  // ShortCuts
147 }
148 
150 {
151  if (m_pRoot)
152  return;
153 
155  m_sName,
156  "0");
157 
158  QString containerId = m_sExtensionId + "/%1";
159 
160  // HACK: I'm not entirely happy with this solution, but it's at least
161  // tidier than passing through half a dozen extra args to Load[Foo]
162  // or having yet more methods just to load the counts
163  auto *pRequest = new UPnpCDSRequest();
164  pRequest->m_nRequestedCount = 0; // We don't want to load any results, we just want the TotalCount
165  auto *pResult = new UPnpCDSExtensionResults();
166  IDTokenMap tokens;
167  // END HACK
168 
169  // -----------------------------------------------------------------------
170  // All Recordings
171  // -----------------------------------------------------------------------
172  CDSObject* pContainer = CDSObject::CreateContainer ( containerId.arg("Recording"),
173  QObject::tr("All Recordings"),
174  m_sExtensionId, // Parent Id
175  nullptr );
176  // HACK
177  LoadRecordings(pRequest, pResult, tokens);
178  pContainer->SetChildCount(pResult->m_nTotalMatches);
179  pContainer->SetChildContainerCount(0);
180  // END HACK
181  m_pRoot->AddChild(pContainer);
182 
183  // -----------------------------------------------------------------------
184  // By Film
185  // -----------------------------------------------------------------------
186  pContainer = CDSObject::CreateContainer ( containerId.arg("Movie"),
187  QObject::tr("Movies"),
188  m_sExtensionId, // Parent Id
189  nullptr );
190  // HACK
191  LoadMovies(pRequest, pResult, tokens);
192  pContainer->SetChildCount(pResult->m_nTotalMatches);
193  pContainer->SetChildContainerCount(0);
194  // END HACK
195  m_pRoot->AddChild(pContainer);
196 
197  // -----------------------------------------------------------------------
198  // By Title
199  // -----------------------------------------------------------------------
200  pContainer = CDSObject::CreateContainer ( containerId.arg("Title"),
201  QObject::tr("Title"),
202  m_sExtensionId, // Parent Id
203  nullptr );
204  // HACK
205  LoadTitles(pRequest, pResult, tokens);
206  pContainer->SetChildCount(pResult->m_nTotalMatches);
207  // Tricky to calculate ChildContainerCount without loading the full
208  // result set
209  //pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
210  // END HACK
211  m_pRoot->AddChild(pContainer);
212 
213  // -----------------------------------------------------------------------
214  // By Date
215  // -----------------------------------------------------------------------
216  pContainer = CDSObject::CreateContainer ( containerId.arg("Date"),
217  QObject::tr("Date"),
218  m_sExtensionId, // Parent Id
219  nullptr );
220  // HACK
221  LoadDates(pRequest, pResult, tokens);
222  pContainer->SetChildCount(pResult->m_nTotalMatches);
223  pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
224  // END HACK
225  m_pRoot->AddChild(pContainer);
226 
227  // -----------------------------------------------------------------------
228  // By Genre
229  // -----------------------------------------------------------------------
230  pContainer = CDSObject::CreateContainer ( containerId.arg("Genre"),
231  QObject::tr("Genre"),
232  m_sExtensionId, // Parent Id
233  nullptr );
234  // HACK
235  LoadGenres(pRequest, pResult, tokens);
236  pContainer->SetChildCount(pResult->m_nTotalMatches);
237  pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
238  // END HACK
239  m_pRoot->AddChild(pContainer);
240 
241  // -----------------------------------------------------------------------
242  // By Channel
243  // -----------------------------------------------------------------------
244  pContainer = CDSObject::CreateContainer ( containerId.arg("Channel"),
245  QObject::tr("Channel"),
246  m_sExtensionId, // Parent Id
247  nullptr );
248  // HACK
249  LoadChannels(pRequest, pResult, tokens);
250  pContainer->SetChildCount(pResult->m_nTotalMatches);
251  pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
252  // END HACK
253  m_pRoot->AddChild(pContainer);
254 
255  // -----------------------------------------------------------------------
256  // By Recording Group
257  // -----------------------------------------------------------------------
258  pContainer = CDSObject::CreateContainer ( containerId.arg("Recgroup"),
259  QObject::tr("Recording Group"),
260  m_sExtensionId, // Parent Id
261  nullptr );
262  // HACK
263  LoadRecGroups(pRequest, pResult, tokens);
264  pContainer->SetChildCount(pResult->m_nTotalMatches);
265  pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
266  // END HACK
267  m_pRoot->AddChild(pContainer);
268 
269  // -----------------------------------------------------------------------
270 
271  // HACK
272  delete pRequest;
273  delete pResult;
274  // END HACK
275 }
276 
278 //
280 
282  UPnpCDSExtensionResults* pResults,
283  const IDTokenMap& tokens, const QString& currentToken)
284 {
285  if (currentToken.isEmpty())
286  {
287  LOG(VB_GENERAL, LOG_ERR, QString("UPnpCDSTV::LoadMetadata: Final "
288  "token missing from id: %1")
289  .arg(pRequest->m_sObjectId));
290  return false;
291  }
292 
293  // Root or Root + 1
294  if (tokens[currentToken].isEmpty())
295  {
296  CDSObject *container = nullptr;
297 
298  if (pRequest->m_sObjectId == m_sExtensionId)
299  container = GetRoot();
300  else
301  container = GetRoot()->GetChild(pRequest->m_sObjectId);
302 
303  if (container)
304  {
305  pResults->Add(container);
306  pResults->m_nTotalMatches = 1;
307  return true;
308  }
309  LOG(VB_GENERAL, LOG_ERR, QString("UPnpCDSTV::LoadMetadata: Requested "
310  "object cannot be found: %1")
311  .arg(pRequest->m_sObjectId));
312  }
313  else if (currentToken == "recording")
314  {
315  return LoadRecordings(pRequest, pResults, tokens);
316  }
317  else if (currentToken == "title")
318  {
319  return LoadTitles(pRequest, pResults, tokens);
320  }
321  else if (currentToken == "date")
322  {
323  return LoadDates(pRequest, pResults, tokens);
324  }
325  else if (currentToken == "genre")
326  {
327  return LoadGenres(pRequest, pResults, tokens);
328  }
329  else if (currentToken == "recgroup")
330  {
331  return LoadRecGroups(pRequest, pResults, tokens);
332  }
333  else if (currentToken == "channel")
334  {
335  return LoadChannels(pRequest, pResults, tokens);
336  }
337  else if (currentToken == "movie")
338  {
339  return LoadMovies(pRequest, pResults, tokens);
340  }
341  else
342  {
343  LOG(VB_GENERAL, LOG_ERR,
344  QString("UPnpCDSTV::LoadMetadata(): "
345  "Unhandled metadata request for '%1'.").arg(currentToken));
346  }
347 
348  return false;
349 }
350 
352 //
354 
356  UPnpCDSExtensionResults* pResults,
357  const IDTokenMap& tokens, const QString& currentToken)
358 {
359  if (currentToken.isEmpty() || currentToken == m_sExtensionId.toLower())
360  {
361  // Root
362  pResults->Add(GetRoot()->GetChildren());
363  pResults->m_nTotalMatches = GetRoot()->GetChildCount();
364  return true;
365  }
366  if (currentToken == "title")
367  {
368  if (!tokens["title"].isEmpty())
369  return LoadRecordings(pRequest, pResults, tokens);
370  return LoadTitles(pRequest, pResults, tokens);
371  }
372  if (currentToken == "date")
373  {
374  if (!tokens["date"].isEmpty())
375  return LoadRecordings(pRequest, pResults, tokens);
376  return LoadDates(pRequest, pResults, tokens);
377  }
378  if (currentToken == "genre")
379  {
380  if (!tokens["genre"].isEmpty())
381  return LoadRecordings(pRequest, pResults, tokens);
382  return LoadGenres(pRequest, pResults, tokens);
383  }
384  if (currentToken == "recgroup")
385  {
386  if (!tokens["recgroup"].isEmpty())
387  return LoadRecordings(pRequest, pResults, tokens);
388  return LoadRecGroups(pRequest, pResults, tokens);
389  }
390  if (currentToken == "channel")
391  {
392  if (tokens["channel"].toInt() > 0)
393  return LoadRecordings(pRequest, pResults, tokens);
394  return LoadChannels(pRequest, pResults, tokens);
395  }
396  if (currentToken == "movie")
397  {
398  return LoadMovies(pRequest, pResults, tokens);
399  }
400  if (currentToken == "recording")
401  {
402  return LoadRecordings(pRequest, pResults, tokens);
403  }
404  LOG(VB_GENERAL, LOG_ERR,
405  QString("UPnpCDSTV::LoadChildren(): "
406  "Unhandled metadata request for '%1'.").arg(currentToken));
407 
408  return false;
409 }
410 
412 //
414 
416 {
417  // ----------------------------------------------------------------------
418  // See if we need to modify the request for compatibility
419  // ----------------------------------------------------------------------
420 
421  // ----------------------------------------------------------------------
422  // Xbox360 compatibility code.
423  // ----------------------------------------------------------------------
424 
425 // if (pRequest->m_eClient == CDS_ClientXBox &&
426 // pRequest->m_sContainerID == "15" &&
427 // gCoreContext->GetSetting("UPnP/WMPSource") != "1")
428 // {
429 // pRequest->m_sObjectId = "Videos/0";
430 //
431 // LOG(VB_UPNP, LOG_INFO,
432 // "UPnpCDSTv::IsBrowseRequestForUs - Yes ContainerID == 15");
433 // return true;
434 // }
435 
436  // ----------------------------------------------------------------------
437  // WMP11 compatibility code
438  // ----------------------------------------------------------------------
439 // if (pRequest->m_eClient == CDS_ClientWMP &&
440 // pRequest->m_nClientVersion < 12.0 &&
441 // pRequest->m_sContainerID == "13" &&
442 // gCoreContext->GetSetting("UPnP/WMPSource") != "1")
443 // {
444 // pRequest->m_sObjectId = "RecTv/0";
445 //
446 // LOG(VB_UPNP, LOG_INFO,
447 // "UPnpCDSTv::IsBrowseRequestForUs - Yes, ObjectId == 13");
448 // return true;
449 // }
450 
451  LOG(VB_UPNP, LOG_INFO,
452  "UPnpCDSTv::IsBrowseRequestForUs - Not sure... Calling base class.");
453 
454  return UPnpCDSExtension::IsBrowseRequestForUs( pRequest );
455 }
456 
458 //
460 
462 {
463  // ----------------------------------------------------------------------
464  // See if we need to modify the request for compatibility
465  // ----------------------------------------------------------------------
466 
467  // ----------------------------------------------------------------------
468  // XBox 360 compatibility code
469  // ----------------------------------------------------------------------
470 
471 // if (pRequest->m_eClient == CDS_ClientXBox &&
472 // pRequest->m_sContainerID == "15" &&
473 // gCoreContext->GetSetting("UPnP/WMPSource") != "1")
474 // {
475 // pRequest->m_sObjectId = "Videos/0";
476 //
477 // LOG(VB_UPNP, LOG_INFO, "UPnpCDSTv::IsSearchRequestForUs... Yes.");
478 //
479 // return true;
480 // }
481 //
482 //
483 // if ((pRequest->m_sObjectId.isEmpty()) &&
484 // (!pRequest->m_sContainerID.isEmpty()))
485 // pRequest->m_sObjectId = pRequest->m_sContainerID;
486 
487  // ----------------------------------------------------------------------
488 
489  bool bOurs = UPnpCDSExtension::IsSearchRequestForUs( pRequest );
490 
491  // ----------------------------------------------------------------------
492  // WMP11 compatibility code
493  //
494  // In this mode browsing for "Videos" is forced to either RecordedTV (us)
495  // or Videos (handled by upnpcdsvideo)
496  //
497  // ----------------------------------------------------------------------
498 
499 // if ( bOurs && pRequest->m_eClient == CDS_ClientWMP &&
500 // pRequest->m_nClientVersion < 12.0)
501 // {
502 // // GetBoolSetting()?
503 // if ( gCoreContext->GetSetting("UPnP/WMPSource") != "1")
504 // {
505 // pRequest->m_sObjectId = "RecTv/0";
506 // // -=>TODO: Not sure why this was added
507 // pRequest->m_sParentId = '8';
508 // }
509 // else
510 // bOurs = false;
511 // }
512 
513  return bOurs;
514 }
515 
517 //
519 
520  // TODO Load titles where there is more than one, otherwise the recording, but
521  // somehow do so with the minimum number of queries and code duplication
523  UPnpCDSExtensionResults* pResults,
524  const IDTokenMap& tokens)
525 {
526  QString sRequestId = pRequest->m_sObjectId;
527 
528  uint16_t nCount = pRequest->m_nRequestedCount;
529  uint16_t nOffset = pRequest->m_nStartingIndex;
530 
531  // We must use a dedicated connection to get an accurate value from
532  // FOUND_ROWS()
534 
535  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
536  "r.title, r.inetref, r.recordedid, COUNT(*) "
537  "FROM recorded r "
538  "LEFT JOIN recgroups g ON r.recgroup=g.recgroup "
539  "%1 " // WHERE clauses
540  "GROUP BY r.title "
541  "ORDER BY r.title "
542  "LIMIT :OFFSET,:COUNT";
543 
544  QStringList clauses;
545  QString whereString = BuildWhereClause(clauses, tokens);
546  query.prepare(sql.arg(whereString));
547  BindValues(query, tokens);
548 
549  query.bindValue(":OFFSET", nOffset);
550  query.bindValue(":COUNT", nCount);
551 
552  if (!query.exec())
553  return false;
554 
555  while (query.next())
556  {
557  QString sTitle = query.value(0).toString();
558  QString sInetRef = query.value(1).toString();
559  int nRecordingID = query.value(2).toInt();
560  int nTitleCount = query.value(3).toInt();
561 
562  if (nTitleCount > 1)
563  {
564  // TODO Album or plain old container?
565  CDSObject* pContainer = CDSObject::CreateAlbum( CreateIDString(sRequestId, "Title", sTitle),
566  sTitle,
567  pRequest->m_sParentId,
568  nullptr );
569 
570 
571  pContainer->SetPropValue("description", QObject::tr("%n Episode(s)", "", nTitleCount));
572  pContainer->SetPropValue("longdescription", QObject::tr("%n Episode(s)", "", nTitleCount));
573 
574  pContainer->SetChildCount(nTitleCount);
575  pContainer->SetChildContainerCount(0); // Recordings, no containers
576  pContainer->SetPropValue("storageMedium", "HDD");
577 
578  // Artwork
579  PopulateArtworkURIS(pContainer, sInetRef, 0, m_uriBase); // No particular season
580 
581  pResults->Add(pContainer);
582  pContainer->DecrRef();
583  }
584  else
585  {
586  IDTokenMap newTokens(tokens);
587  newTokens.insert("recording", QString::number(nRecordingID));
588  LoadRecordings(pRequest, pResults, newTokens);
589  }
590  }
591 
592  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
593  // at least the size of this result set
594  if (query.size() > 0)
595  pResults->m_nTotalMatches = query.size();
596 
597  // Fetch the total number of matches ignoring any LIMITs
598  query.prepare("SELECT FOUND_ROWS()");
599  if (query.exec() && query.next())
600  pResults->m_nTotalMatches = query.value(0).toUInt();
601 
602  return true;
603 }
604 
606 //
608 
610  UPnpCDSExtensionResults* pResults,
611  const IDTokenMap& tokens)
612 {
613  QString sRequestId = pRequest->m_sObjectId;
614 
615  uint16_t nCount = pRequest->m_nRequestedCount;
616  uint16_t nOffset = pRequest->m_nStartingIndex;
617 
618  // We must use a dedicated connection to get an accurate value from
619  // FOUND_ROWS()
621 
622  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
623  "r.starttime, COUNT(r.recordedid) "
624  "FROM recorded r "
625  "LEFT JOIN recgroups g ON g.recgroup=r.recgroup "
626  "%1 " // WHERE clauses
627  "GROUP BY DATE(CONVERT_TZ(r.starttime, 'UTC', 'SYSTEM')) "
628  "ORDER BY r.starttime DESC "
629  "LIMIT :OFFSET,:COUNT";
630 
631  QStringList clauses;
632  QString whereString = BuildWhereClause(clauses, tokens);
633  query.prepare(sql.arg(whereString));
634  BindValues(query, tokens);
635 
636  query.bindValue(":OFFSET", nOffset);
637  query.bindValue(":COUNT", nCount);
638 
639  if (!query.exec())
640  return false;
641 
642  while (query.next())
643  {
644  QDate dtDate = query.value(0).toDate();
645  int nRecCount = query.value(1).toInt();
646 
647  // TODO Album or plain old container?
648  CDSObject* pContainer = CDSObject::CreateContainer( CreateIDString(sRequestId, "Date", dtDate.toString(Qt::ISODate)),
650  pRequest->m_sParentId,
651  nullptr );
652  pContainer->SetChildCount(nRecCount);
653  pContainer->SetChildContainerCount(nRecCount);
654 
655  pResults->Add(pContainer);
656  pContainer->DecrRef();
657  }
658 
659  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
660  // at least the size of this result set
661  if (query.size() > 0)
662  pResults->m_nTotalMatches = query.size();
663 
664  // Fetch the total number of matches ignoring any LIMITs
665  query.prepare("SELECT FOUND_ROWS()");
666  if (query.exec() && query.next())
667  pResults->m_nTotalMatches = query.value(0).toUInt();
668 
669  return true;
670 }
671 
673 //
675 
676 bool UPnpCDSTv::LoadGenres( const UPnpCDSRequest* pRequest,
677  UPnpCDSExtensionResults* pResults,
678  const IDTokenMap& tokens)
679 {
680  QString sRequestId = pRequest->m_sObjectId;
681 
682  uint16_t nCount = pRequest->m_nRequestedCount;
683  uint16_t nOffset = pRequest->m_nStartingIndex;
684 
685  // We must use a dedicated connection to get an accurate value from
686  // FOUND_ROWS()
688 
689  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
690  "r.category, COUNT(r.recordedid) "
691  "FROM recorded r "
692  "LEFT JOIN recgroups g ON g.recgroup=r.recgroup "
693  "%1 " // WHERE clauses
694  "GROUP BY r.category "
695  "ORDER BY r.category "
696  "LIMIT :OFFSET,:COUNT";
697 
698  QStringList clauses;
699  QString whereString = BuildWhereClause(clauses, tokens);
700  query.prepare(sql.arg(whereString));
701  BindValues(query, tokens);
702 
703  query.bindValue(":OFFSET", nOffset);
704  query.bindValue(":COUNT", nCount);
705 
706  if (!query.exec())
707  return false;
708 
709  while (query.next())
710  {
711  QString sGenre = query.value(0).toString();
712  int nRecCount = query.value(1).toInt();
713 
714  // Handle empty genre strings
715  QString sDisplayGenre = sGenre.isEmpty() ? QObject::tr("No Genre") : sGenre;
716  sGenre = sGenre.isEmpty() ? "MYTH_NO_GENRE" : sGenre;
717 
718  // TODO Album or plain old container?
719  CDSObject* pContainer = CDSObject::CreateMovieGenre( CreateIDString(sRequestId, "Genre", sGenre),
720  sDisplayGenre,
721  pRequest->m_sParentId,
722  nullptr );
723  pContainer->SetChildCount(nRecCount);
724  pContainer->SetChildContainerCount(nRecCount);
725 
726  pResults->Add(pContainer);
727  pContainer->DecrRef();
728  }
729 
730  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
731  // at least the size of this result set
732  if (query.size() > 0)
733  pResults->m_nTotalMatches = query.size();
734 
735  // Fetch the total number of matches ignoring any LIMITs
736  query.prepare("SELECT FOUND_ROWS()");
737  if (query.exec() && query.next())
738  pResults->m_nTotalMatches = query.value(0).toUInt();
739 
740  return true;
741 }
742 
744 //
746 
748  UPnpCDSExtensionResults* pResults,
749  const IDTokenMap& tokens)
750 {
751  QString sRequestId = pRequest->m_sObjectId;
752 
753  uint16_t nCount = pRequest->m_nRequestedCount;
754  uint16_t nOffset = pRequest->m_nStartingIndex;
755 
756  // We must use a dedicated connection to get an accurate value from
757  // FOUND_ROWS()
759 
760  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
761  "r.recgroupid, g.displayname, g.recgroup, COUNT(r.recordedid) "
762  "FROM recorded r "
763  "LEFT JOIN recgroups g ON g.recgroup=r.recgroup " // Use recgroupid in future
764  "%1 " // WHERE clauses
765  "GROUP BY r.recgroup "
766  "ORDER BY g.displayname "
767  "LIMIT :OFFSET,:COUNT";
768 
769  QStringList clauses;
770  QString whereString = BuildWhereClause(clauses, tokens);
771 
772  query.prepare(sql.arg(whereString));
773 
774  BindValues(query, tokens);
775 
776  query.bindValue(":OFFSET", nOffset);
777  query.bindValue(":COUNT", nCount);
778 
779  if (!query.exec())
780  return false;
781 
782  while (query.next())
783  {
784  // Use the string for now until recgroupid support is complete
785 // int nRecGroupID = query.value(0).toInt();
786  QString sDisplayName = query.value(1).toString();
787  QString sName = query.value(2).toString();
788  int nRecCount = query.value(3).toInt();
789 
790  // TODO Album or plain old container?
791  CDSObject* pContainer = CDSObject::CreateContainer( CreateIDString(sRequestId, "RecGroup", sName),
792  sDisplayName.isEmpty() ? sName : sDisplayName,
793  pRequest->m_sParentId,
794  nullptr );
795  pContainer->SetChildCount(nRecCount);
796  pContainer->SetChildContainerCount(nRecCount);
797 
798  pResults->Add(pContainer);
799  pContainer->DecrRef();
800  }
801 
802  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
803  // at least the size of this result set
804  if (query.size() > 0)
805  pResults->m_nTotalMatches = query.size();
806 
807  // Fetch the total number of matches ignoring any LIMITs
808  query.prepare("SELECT FOUND_ROWS()");
809  if (query.exec() && query.next())
810  pResults->m_nTotalMatches = query.value(0).toUInt();
811 
812  return true;
813 }
814 
816 //
818 
820  UPnpCDSExtensionResults* pResults,
821  const IDTokenMap& tokens)
822 {
823  QString sRequestId = pRequest->m_sObjectId;
824 
825  uint16_t nCount = pRequest->m_nRequestedCount;
826  uint16_t nOffset = pRequest->m_nStartingIndex;
827 
828  // We must use a dedicated connection to get an accurate value from
829  // FOUND_ROWS()
831 
832  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
833  "r.chanid, c.channum, c.name, COUNT(r.recordedid) "
834  "FROM recorded r "
835  "JOIN channel c ON c.chanid=r.chanid "
836  "LEFT JOIN recgroups g ON g.recgroup=r.recgroup " // Use recgroupid in future
837  "%1 " // WHERE clauses
838  "GROUP BY c.channum "
839  "ORDER BY LPAD(CAST(c.channum AS UNSIGNED), 10, 0), " // Natural sorting including subchannels e.g. 2_4, 1.3
840  " LPAD(c.channum, 10, 0)"
841  "LIMIT :OFFSET,:COUNT";
842 
843  QStringList clauses;
844  QString whereString = BuildWhereClause(clauses, tokens);
845 
846  query.prepare(sql.arg(whereString));
847 
848  BindValues(query, tokens);
849 
850  query.bindValue(":OFFSET", nOffset);
851  query.bindValue(":COUNT", nCount);
852 
853  if (!query.exec())
854  return false;
855 
856  while (query.next())
857  {
858  int nChanID = query.value(0).toInt();
859  QString sChanNum = query.value(1).toString();
860  QString sName = query.value(2).toString();
861  int nRecCount = query.value(3).toInt();
862 
863  QString sFullName = QString("%1 %2").arg(sChanNum, sName);
864 
865  // TODO Album or plain old container?
866  CDSObject* pContainer = CDSObject::CreateContainer( CreateIDString(sRequestId, "Channel", nChanID),
867  sFullName,
868  pRequest->m_sParentId,
869  nullptr );
870  pContainer->SetChildCount(nRecCount);
871  pContainer->SetChildContainerCount(nRecCount);
872 
873  pResults->Add(pContainer);
874  pContainer->DecrRef();
875  }
876 
877  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
878  // at least the size of this result set
879  if (query.size() > 0)
880  pResults->m_nTotalMatches = query.size();
881 
882  // Fetch the total number of matches ignoring any LIMITs
883  query.prepare("SELECT FOUND_ROWS()");
884  if (query.exec() && query.next())
885  pResults->m_nTotalMatches = query.value(0).toUInt();
886 
887  return true;
888 }
889 
891 //
893 
895  UPnpCDSExtensionResults* pResults,
896  IDTokenMap tokens)
897 {
898  tokens["category_type"] = "movie";
899  return LoadRecordings(pRequest, pResults, tokens);
900 }
901 
903 //
905 
906 // TODO Implement this
907 // bool UPnpCDSTv::LoadSeasons(const UPnpCDSRequest* pRequest,
908 // UPnpCDSExtensionResults* pResults,
909 // IDTokenMap tokens)
910 // {
911 //
912 // return false;
913 // }
914 
916 //
918 
919 // TODO Implement this
920 // bool UPnpCDSTv::LoadEpisodes(const UPnpCDSRequest* pRequest,
921 // UPnpCDSExtensionResults* pResults,
922 // IDTokenMap tokens)
923 // {
924 // return false;
925 // }
926 
928 //
930 
932  UPnpCDSExtensionResults* pResults,
933  IDTokenMap tokens)
934 {
935  QString sRequestId = pRequest->m_sObjectId;
936 
937  uint16_t nCount = pRequest->m_nRequestedCount;
938  uint16_t nOffset = pRequest->m_nStartingIndex;
939 
940  // HACK this is a bit of a hack for loading Recordings in the Title view
941  // where the count/start index from the request aren't applicable
942  if (tokens["recording"].toInt() > 0)
943  {
944  nCount = 1;
945  nOffset = 0;
946  }
947 
948  // We must use a dedicated connection to get an accurate value from
949  // FOUND_ROWS()
951 
952  QString sql = "SELECT SQL_CALC_FOUND_ROWS "
953  "r.chanid, r.starttime, r.endtime, r.title, "
954  "r.subtitle, r.description, r.category, "
955  "r.hostname, r.recgroup, r.filesize, "
956  "r.basename, r.progstart, r.progend, "
957  "r.storagegroup, r.inetref, "
958  "p.category_type, c.callsign, c.channum, "
959  "p.episode, p.totalepisodes, p.season, "
960  "r.programid, r.seriesid, r.recordid, "
961  "c.default_authority, c.name, "
962  "r.recordedid, r.transcoded, p.videoprop+0, p.audioprop+0, "
963  "f.video_codec, f.audio_codec, f.fps, f.width, f.height, "
964  "f.container "
965  "FROM recorded r "
966  "LEFT JOIN channel c ON r.chanid=c.chanid "
967  "LEFT JOIN recordedprogram p ON p.chanid=r.chanid "
968  " AND p.starttime=r.progstart "
969  "LEFT JOIN recgroups g ON r.recgroup=g.recgroup "
970  "LEFT JOIN recordedfile f ON r.recordedid=f.recordedid "
971  "%1 " // WHERE clauses
972  "%2 " // ORDER BY
973  "LIMIT :OFFSET,:COUNT";
974 
975 
976  QString orderByString = "ORDER BY r.starttime DESC, r.title";
977 
978  if (!tokens["title"].isEmpty())
979  orderByString = "ORDER BY p.season, p.episode, r.starttime ASC"; // In season/episode order, falling back to recorded order
980 
981  QStringList clauses;
982  QString whereString = BuildWhereClause(clauses, tokens);
983 
984  query.prepare(sql.arg(whereString, orderByString));
985 
986  BindValues(query, tokens);
987 
988  query.bindValue(":OFFSET", nOffset);
989  query.bindValue(":COUNT", nCount);
990 
991  if (!query.exec())
992  return false;
993 
994  while (query.next())
995  {
996  int nChanid = query.value( 0).toInt();
997  QDateTime dtStartTime = MythDate::as_utc(query.value(1).toDateTime());
998  QDateTime dtEndTime = MythDate::as_utc(query.value(2).toDateTime());
999  QString sTitle = query.value( 3).toString();
1000  QString sSubtitle = query.value( 4).toString();
1001  QString sDescription = query.value( 5).toString();
1002  QString sCategory = query.value( 6).toString();
1003  QString sHostName = query.value( 7).toString();
1004 // QString sRecGroup = query.value( 8).toString();
1005  uint64_t nFileSize = query.value( 9).toULongLong();
1006  QString sBaseName = query.value(10).toString();
1007 
1008  QDateTime dtProgStart =
1009  MythDate::as_utc(query.value(11).toDateTime());
1010  QDateTime dtProgEnd =
1011  MythDate::as_utc(query.value(12).toDateTime());
1012  QString sStorageGrp = query.value(13).toString();
1013 
1014  QString sInetRef = query.value(14).toString();
1015  QString sCatType = query.value(15).toString();
1016  QString sCallsign = query.value(16).toString();
1017  QString sChanNum = query.value(17).toString();
1018 
1019  int nEpisode = query.value(18).toInt();
1020  int nEpisodeTotal = query.value(19).toInt();
1021  int nSeason = query.value(20).toInt();
1022 
1023  QString sProgramId = query.value(21).toString();
1024  QString sSeriesId = query.value(22).toString();
1025  int nRecordId = query.value(23).toInt();
1026 
1027  QString sDefaultAuthority = query.value(24).toString();
1028  QString sChanName = query.value(25).toString();
1029 
1030  int nRecordedId = query.value(26).toInt();
1031 
1032  bool bTranscoded = query.value(27).toBool();
1033  int nVideoProps = query.value(28).toInt();
1034  //int nAudioProps = query.value(29).toInt();
1035 
1036  QString sVideoCodec = query.value(30).toString();
1037  QString sAudioCodec = query.value(31).toString();
1038  double dVideoFrameRate = query.value(32).toDouble();
1039  int nVideoWidth = query.value(33).toInt();
1040  int nVideoHeight = query.value(34).toInt();
1041  QString sContainer = query.value(35).toString();
1042 
1043  // ----------------------------------------------------------------------
1044  // Cache Host ip Address & Port
1045  // ----------------------------------------------------------------------
1046 
1047  if (!m_mapBackendIp.contains( sHostName ))
1048  m_mapBackendIp[ sHostName ] = gCoreContext->GetBackendServerIP(sHostName);
1049 
1050  if (!m_mapBackendPort.contains( sHostName ))
1051  m_mapBackendPort[ sHostName ] = gCoreContext->GetBackendStatusPort(sHostName);
1052 
1053  // ----------------------------------------------------------------------
1054  // Build Support Strings
1055  // ----------------------------------------------------------------------
1056 
1057  QUrl URIBase;
1058  URIBase.setScheme("http");
1059  URIBase.setHost(m_mapBackendIp[sHostName]);
1060  URIBase.setPort(m_mapBackendPort[sHostName]);
1061 
1062  CDSObject *pItem = CDSObject::CreateVideoItem( CreateIDString(sRequestId, "Recording", nRecordedId),
1063  sTitle,
1064  pRequest->m_sParentId );
1065 
1066  // Only add the reference ID for items which are not in the
1067  // 'All Recordings' container
1068  QString sRefIDBase = QString("%1/Recording").arg(m_sExtensionId);
1069  if ( pRequest->m_sParentId != sRefIDBase )
1070  {
1071  QString sRefId = QString( "%1=%2")
1072  .arg( sRefIDBase )
1073  .arg( nRecordedId );
1074 
1075  pItem->SetPropValue( "refID", sRefId );
1076  }
1077 
1078  pItem->SetPropValue( "genre", sCategory );
1079 
1080  // NOTE There is no max-length on description, no requirement in either UPnP
1081  // or DLNA that the description be a certain size, only that it's 'brief'
1082  //
1083  // The specs only say that the optional longDescription is for longer
1084  // descriptions. Given that clients could easily truncate the description
1085  // themselves this is all very vague.
1086  //
1087  // It's not really correct to stick the subtitle in the description
1088  // field given the existence of the programTitle field. Yet that's what
1089  // we've and what some people have come to expect. There's no easy answer
1090  // but there are wrong answers and whatever we decide, we shouldn't pander
1091  // to devices which don't follow the specs.
1092 
1093  if (!sSubtitle.isEmpty())
1094  pItem->SetPropValue( "description" , sSubtitle );
1095  else
1096  pItem->SetPropValue( "description", sDescription.left(128).append(" ..."));
1097  pItem->SetPropValue( "longDescription", sDescription );
1098 
1099  pItem->SetPropValue( "channelName" , sChanName );
1100  // TODO Need to detect/switch between DIGITAL/ANALOG
1101  pItem->SetPropValue( "channelID" , sChanNum, "DIGITAL");
1102  pItem->SetPropValue( "callSign" , sCallsign );
1103  // NOTE channelNr must only be used when a DIGITAL or ANALOG channelID is
1104  // given and it MUST be an integer i.e. 2_1 or 2.1 are illegal
1105  int nChanNum = sChanNum.toInt();
1106  if (nChanNum > 0)
1107  pItem->SetPropValue( "channelNr" , QString::number(nChanNum) );
1108 
1109  if (sCatType != "movie")
1110  {
1111  pItem->SetPropValue( "seriesTitle" , sTitle);
1112  pItem->SetPropValue( "programTitle" , sSubtitle);
1113  }
1114  else
1115  pItem->SetPropValue( "programTitle" , sTitle);
1116 
1117  if ( nEpisode > 0 || nSeason > 0 ) // There has got to be a better way
1118  {
1119  pItem->SetPropValue( "episodeNumber" , QString::number(nEpisode));
1120  pItem->SetPropValue( "episodeCount" , QString::number(nEpisodeTotal));
1121  }
1122 
1123  pItem->SetPropValue( "scheduledStartTime" , UPnPDateTime::DateTimeFormat(dtProgStart));
1124  pItem->SetPropValue( "scheduledEndTime" , UPnPDateTime::DateTimeFormat(dtProgEnd));
1125  auto msecs = std::chrono::milliseconds(dtProgEnd.toMSecsSinceEpoch() - dtProgStart.toMSecsSinceEpoch());
1126  pItem->SetPropValue( "scheduledDuration" , UPnPDateTime::DurationFormat(msecs));
1127  pItem->SetPropValue( "recordedStartDateTime", UPnPDateTime::DateTimeFormat(dtStartTime));
1128  pItem->SetPropValue( "recordedDayOfWeek" , UPnPDateTime::NamedDayFormat(dtStartTime));
1129  pItem->SetPropValue( "srsRecordScheduleID" , QString::number(nRecordId));
1130 
1131  if (!sSeriesId.isEmpty())
1132  {
1133  // FIXME: This should be set correctly for EIT data to SI_SERIESID and
1134  // for known sources such as TMS to the correct identifier
1135  QString sIdType = "mythtv.org_XMLTV";
1136  if (sSeriesId.contains(sDefaultAuthority))
1137  sIdType = "mythtv.org_EIT";
1138 
1139  pItem->SetPropValue( "seriesID", sSeriesId, sIdType );
1140  }
1141 
1142  if (!sProgramId.isEmpty())
1143  {
1144  // FIXME: This should be set correctly for EIT data to SI_PROGRAMID and
1145  // for known sources such as TMS to the correct identifier
1146  QString sIdType = "mythtv.org_XMLTV";
1147  if (sProgramId.contains(sDefaultAuthority))
1148  sIdType = "mythtv.org_EIT";
1149 
1150  pItem->SetPropValue( "programID", sProgramId, sIdType );
1151  }
1152 
1153  pItem->SetPropValue( "date" , UPnPDateTime::DateTimeFormat(dtStartTime));
1154  pItem->SetPropValue( "creator" , "MythTV" );
1155 
1156  // Bookmark support
1157  //pItem->SetPropValue( "lastPlaybackPosition", QString::number());
1158 
1159  //pItem->SetPropValue( "producer" , );
1160  //pItem->SetPropValue( "rating" , );
1161  //pItem->SetPropValue( "actor" , );
1162  //pItem->SetPropValue( "director" , );
1163 
1164  // ----------------------------------------------------------------------
1165  // Add Video Resource Element based on File contents/extension (HTTP)
1166  // ----------------------------------------------------------------------
1167 
1168  StorageGroup sg(sStorageGrp, sHostName);
1169  QString sFilePath = sg.FindFile(sBaseName);
1170  QString sMimeType;
1171 
1172  if ( QFile::exists(sFilePath) )
1173  sMimeType = HTTPRequest::TestMimeType( sFilePath );
1174  else
1175  sMimeType = HTTPRequest::TestMimeType( sBaseName );
1176 
1177 
1178  // If we are dealing with Window Media Player 12 (i.e. Windows 7)
1179  // then fake the Mime type to place the recorded TV in the
1180  // recorded TV section.
1181 // if (pRequest->m_eClient == CDS_ClientWMP &&
1182 // pRequest->m_nClientVersion >= 12.0)
1183 // {
1184 // sMimeType = "video/x-ms-dvr";
1185 // }
1186 
1187  // HACK: If we are dealing with a Sony Blu-ray player then we fake the
1188  // MIME type to force the video to appear
1189 // if ( pRequest->m_eClient == CDS_ClientSonyDB )
1190 // sMimeType = "video/avi";
1191 
1192  std::chrono::milliseconds nDurationMS { 0ms };
1193 
1194  // NOTE We intentionally don't use the chanid, recstarttime constructor
1195  // to avoid an unnecessary db query. At least until the time that we're
1196  // creating a RI object throughout
1197  RecordingInfo recInfo = RecordingInfo();
1198  recInfo.SetChanID(nChanid);
1199  recInfo.SetRecordingStartTime(dtStartTime);
1200  // The actual duration may not match the scheduled duration
1201  nDurationMS = recInfo.QueryTotalDuration();
1202  // Older recordings won't have their precise duration stored in
1203  // recordedmarkup
1204  if (nDurationMS == 0ms)
1205  {
1206  auto uiStart = std::chrono::milliseconds(dtStartTime.toMSecsSinceEpoch());
1207  auto uiEnd = std::chrono::milliseconds(dtEndTime.toMSecsSinceEpoch());
1208  nDurationMS = (uiEnd - uiStart);
1209  nDurationMS = std::max(0ms, nDurationMS);
1210  }
1211 
1212  pItem->SetPropValue( "recordedDuration", UPnPDateTime::DurationFormat(nDurationMS));
1213 
1214 
1215  QSize resolution = QSize(nVideoWidth, nVideoHeight);
1216 
1217  // Attempt to guess the container if the information is missing from
1218  // the database
1219  if (sContainer.isEmpty())
1220  {
1221  sContainer = "NUV";
1222  if (sMimeType == "video/mp2p")
1223  {
1224  if (bTranscoded) // Transcoded mpeg will probably be in a PS container
1225  sContainer = "MPEG2-PS";
1226  else // For temporary backwards compatibility with old file naming
1227  sContainer = "MPEG2-TS"; // 99% of recordings will be in MPEG-2 TS containers before transcoding
1228  }
1229  else if (sMimeType == "video/mp2t")
1230  {
1231  sMimeType = "video/mp2p";
1232  sContainer = "MPEG2-TS";
1233  }
1234  }
1235  // Make an educated guess at the video codec if the information is
1236  // missing from the database
1237  if (sVideoCodec.isEmpty())
1238  {
1239  if (sMimeType == "video/mp2p" || sMimeType == "video/mp2t")
1240  sVideoCodec = (nVideoProps & VID_AVC) ? "H264" : "MPEG2VIDEO";
1241  else if (sMimeType == "video/mp4")
1242  sVideoCodec = "MPEG4";
1243  }
1244 
1245  // DLNA requires a mimetype of video/mp2p for TS files, it's not the
1246  // correct mimetype, but then DLNA doesn't seem to care about such
1247  // things
1248  if (sMimeType == "video/mp2t" || sMimeType == "video/mp2p")
1249  sMimeType = "video/mpeg";
1250 
1251  QUrl resURI = URIBase;
1252  QUrlQuery resQuery;
1253  resURI.setPath("/Content/GetRecording");
1254  resQuery.addQueryItem("RecordedId", QString::number(nRecordedId));
1255  resURI.setQuery(resQuery);
1256 
1257  QString sProtocol = DLNA::ProtocolInfoString(UPNPProtocol::kHTTP,
1258  sMimeType,
1259  resolution,
1260  dVideoFrameRate,
1261  sContainer,
1262  sVideoCodec,
1263  sAudioCodec,
1264  bTranscoded);
1265 
1266  Resource *pRes = pItem->AddResource( sProtocol, resURI.toEncoded() );
1267  // Must be the duration of the entire video not the scheduled programme duration
1268  // Appendix B.2.1.4 - res@duration
1269  if (nDurationMS > 0ms)
1270  pRes->AddAttribute ( "duration" , UPnPDateTime::resDurationFormat(nDurationMS) );
1271  if (nVideoHeight > 0 && nVideoWidth > 0)
1272  pRes->AddAttribute ( "resolution" , QString("%1x%2").arg(nVideoWidth).arg(nVideoHeight) );
1273  pRes->AddAttribute ( "size" , QString::number( nFileSize) );
1274 
1275  // ----------------------------------------------------------------------
1276  // Add Preview URI as <res>
1277  // MUST be _TN and 160px
1278  // ----------------------------------------------------------------------
1279 
1280  QUrl previewURI = URIBase;
1281  QUrlQuery previewQuery;
1282  previewURI.setPath("/Content/GetPreviewImage");
1283  previewQuery.addQueryItem("RecordedId", QString::number(nRecordedId));
1284  previewQuery.addQueryItem("Width", "160");
1285  previewQuery.addQueryItem("Format", "JPG");
1286  previewURI.setQuery(previewQuery);
1287 
1288  sProtocol = DLNA::ProtocolInfoString(UPNPProtocol::kHTTP, "image/jpeg",
1289  QSize(160, 160));
1290  pItem->AddResource( sProtocol, previewURI.toEncoded());
1291 
1292  // ----------------------------------------------------------------------
1293  // Add Artwork
1294  // ----------------------------------------------------------------------
1295  if (!sInetRef.isEmpty())
1296  {
1297  PopulateArtworkURIS(pItem, sInetRef, nSeason, URIBase);
1298  }
1299 
1300  pResults->Add( pItem );
1301  pItem->DecrRef();
1302  }
1303 
1304  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
1305  // at least the size of this result set
1306  if (query.size() > 0)
1307  pResults->m_nTotalMatches = query.size();
1308 
1309  // Fetch the total number of matches ignoring any LIMITs
1310  query.prepare("SELECT FOUND_ROWS()");
1311  if (query.exec() && query.next())
1312  pResults->m_nTotalMatches = query.value(0).toUInt();
1313 
1314  return true;
1315 }
1316 
1318 //
1320 
1321 void UPnpCDSTv::PopulateArtworkURIS(CDSObject* pItem, const QString &sInetRef,
1322  int nSeason, const QUrl& URIBase)
1323 {
1324  QUrl artURI = URIBase;
1325  artURI.setPath("/Content/GetRecordingArtwork");
1326  QUrlQuery artQuery(artURI.query());
1327  artQuery.addQueryItem("Inetref", sInetRef);
1328  artQuery.addQueryItem("Season", QString::number(nSeason));
1329  artURI.setQuery(artQuery);
1330 
1331  // Prefer JPEG over PNG here, although PNG is allowed JPEG probably
1332  // has wider device support and crucially the filesizes are smaller
1333  // which speeds up loading times over the network
1334 
1335  // We MUST include the thumbnail size, but since some clients may use the
1336  // first image they see and the thumbnail is tiny, instead return the
1337  // medium first. The large could be very large, which is no good if the
1338  // client is pulling images for an entire list at once!
1339 
1340  // Thumbnail
1341  // At least one albumArtURI must be a ThumbNail (TN) no larger
1342  // than 160x160, and it must also be a jpeg
1343  QUrl thumbURI = artURI;
1344  QUrlQuery thumbQuery(thumbURI.query());
1345  thumbQuery.addQueryItem("Type", "screenshot");
1346  thumbQuery.addQueryItem("Width", "160");
1347  thumbQuery.addQueryItem("Height", "160");
1348  thumbURI.setQuery(thumbQuery);
1349 
1350  // Small
1351  // Must be no more than 640x480
1352  QUrl smallURI = artURI;
1353  QUrlQuery smallQuery(smallURI.query());
1354  smallQuery.addQueryItem("Type", "coverart");
1355  smallQuery.addQueryItem("Width", "640");
1356  smallQuery.addQueryItem("Height", "480");
1357  smallURI.setQuery(smallQuery);
1358 
1359  // Medium
1360  // Must be no more than 1024x768
1361  QUrl mediumURI = artURI;
1362  QUrlQuery mediumQuery(mediumURI.query());
1363  mediumQuery.addQueryItem("Type", "coverart");
1364  mediumQuery.addQueryItem("Width", "1024");
1365  mediumQuery.addQueryItem("Height", "768");
1366  mediumURI.setQuery(mediumQuery);
1367 
1368  // Large
1369  // Must be no more than 4096x4096 - for our purposes, just return
1370  // a fullsize image
1371  QUrl largeURI = artURI;
1372  QUrlQuery largeQuery(largeURI.query());
1373  largeQuery.addQueryItem("Type", "fanart");
1374  largeURI.setQuery(largeQuery);
1375 
1376  QList<Property*> propList = pItem->GetProperties("albumArtURI");
1377  if (propList.size() >= 4)
1378  {
1379  Property *pProp = propList.at(0);
1380  if (pProp)
1381  {
1382  pProp->SetValue(mediumURI.toEncoded());
1383  pProp->AddAttribute("dlna:profileID", "JPEG_MED");
1384  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
1385  }
1386 
1387  pProp = propList.at(1);
1388  if (pProp)
1389  {
1390  pProp->SetValue(thumbURI.toEncoded());
1391  pProp->AddAttribute("dlna:profileID", "JPEG_TN");
1392  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
1393  }
1394 
1395  pProp = propList.at(2);
1396  if (pProp)
1397  {
1398  pProp->SetValue(smallURI.toEncoded());
1399  pProp->AddAttribute("dlna:profileID", "JPEG_SM");
1400  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
1401  }
1402 
1403  pProp = propList.at(3);
1404  if (pProp)
1405  {
1406  pProp->SetValue(largeURI.toEncoded());
1407  pProp->AddAttribute("dlna:profileID", "JPEG_LRG");
1408  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
1409  }
1410  }
1411 
1412  if (pItem->m_sClass.startsWith("object.item.videoItem"))
1413  {
1414  QString sProtocol;
1415 
1417  "image/jpeg", QSize(1024, 768));
1418  pItem->AddResource( sProtocol, mediumURI.toEncoded());
1419 
1420  // We already include a thumbnail
1421  //sProtocol = DLNA::ProtocolInfoString(UPNPProtocol::kHTTP,
1422  // "image/jpeg", QSize(160, 160));
1423  //pItem->AddResource( sProtocol, thumbURI.toEncoded());
1424 
1426  "image/jpeg", QSize(640, 480));
1427  pItem->AddResource( sProtocol, smallURI.toEncoded());
1428 
1430  "image/jpeg", QSize(1920, 1080)); // Not the actual res, we don't know that
1431  pItem->AddResource( sProtocol, largeURI.toEncoded());
1432  }
1433 }
1434 
1436 //
1438 
1439 QString UPnpCDSTv::BuildWhereClause( QStringList clauses,
1440  IDTokenMap tokens)
1441 {
1442  // We ignore protected recgroups, UPnP offers no mechanism to provide
1443  // restricted access to containers and there's no point in having
1444  // password protected groups if that protection can be easily circumvented
1445  // by children just by pointing a phone, tablet or other computer at the
1446  // advertised UPnP server.
1447  //
1448  // In short, don't use password protected recording groups if you want to
1449  // be able to access those recordings via upnp
1450  clauses.append("g.password=''");
1451  // Ignore recordings in the LiveTV and Deleted recgroups
1452  // We cannot currently prevent LiveTV recordings from being expired while
1453  // being streamed to a upnp device, so there's no point in listing them.
1455  clauses.append(QString("g.recgroup != '%1'").arg(liveTVGroup));
1457  clauses.append(QString("g.recgroup != '%1'").arg(deletedGroup));
1458 
1459  if (tokens["recording"].toInt() > 0)
1460  clauses.append("r.recordedid=:RECORDED_ID");
1461  if (!tokens["date"].isEmpty())
1462  clauses.append("DATE(CONVERT_TZ(r.starttime, 'UTC', 'SYSTEM'))=:DATE");
1463  if (!tokens["genre"].isEmpty())
1464  clauses.append("r.category=:GENRE");
1465  if (!tokens["recgroup"].isEmpty())
1466  clauses.append("r.recgroup=:RECGROUP");
1467  if (!tokens["title"].isEmpty())
1468  clauses.append("r.title=:TITLE");
1469  if (!tokens["channel"].isEmpty())
1470  clauses.append("r.chanid=:CHANNEL");
1471  // Special token
1472  if (!tokens["category_type"].isEmpty())
1473  clauses.append("p.category_type=:CATTYPE");
1474 
1475  QString whereString;
1476  if (!clauses.isEmpty())
1477  {
1478  whereString = " WHERE ";
1479  whereString.append(clauses.join(" AND "));
1480  }
1481 
1482  return whereString;
1483 }
1484 
1486 //
1488 
1490  IDTokenMap tokens)
1491 {
1492  if (tokens["recording"].toInt() > 0)
1493  query.bindValue(":RECORDED_ID", tokens["recording"]);
1494  if (!tokens["date"].isEmpty())
1495  query.bindValue(":DATE", tokens["date"]);
1496  if (!tokens["genre"].isEmpty())
1497  query.bindValue(":GENRE", tokens["genre"] == "MYTH_NO_GENRE" ? "" : tokens["genre"]);
1498  if (!tokens["recgroup"].isEmpty())
1499  query.bindValue(":RECGROUP", tokens["recgroup"]);
1500  if (!tokens["title"].isEmpty())
1501  query.bindValue(":TITLE", tokens["title"]);
1502  if (tokens["channel"].toInt() > 0)
1503  query.bindValue(":CHANNEL", tokens["channel"]);
1504  if (!tokens["category_type"].isEmpty())
1505  query.bindValue(":CATTYPE", tokens["category_type"]);
1506 }
1507 
1508 
1509 // vim:ts=4:sw=4:ai:et:si:sts=4
CDSObject::GetProperties
QList< Property * > GetProperties(const QString &sName)
Definition: upnpcdsobjects.cpp:105
MSqlQuery::next
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:811
MSqlQuery
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
UPnpCDSRequest::m_nRequestedCount
uint16_t m_nRequestedCount
Definition: upnpcds.h:81
MythDate::toString
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:84
MSqlQuery::size
int size(void) const
Definition: mythdbcon.h:215
UPnpCDSExtension
Definition: upnpcds.h:203
ReferenceCounter::DecrRef
virtual int DecrRef(void)
Decrements reference count and deletes on 0.
Definition: referencecounter.cpp:125
UPNPProtocol::kHTTP
@ kHTTP
Definition: upnphelpers.h:133
CDSObject
Definition: upnpcdsobjects.h:184
MythDate::as_utc
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:27
UPnpCDSRequest::m_sParentId
QString m_sParentId
Definition: upnpcds.h:86
RecordingInfo
Holds information on a TV Program one might wish to record.
Definition: recordinginfo.h:35
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:1091
Resource
Definition: upnpcdsobjects.h:103
UPnpCDSTv::UPnpCDSTv
UPnpCDSTv()
Definition: upnpcdstv.cpp:135
RecordingInfo::kDeletedRecGroup
@ kDeletedRecGroup
Definition: recordinginfo.h:193
UPnPShortcutFeature::VIDEOS_RECORDINGS
@ VIDEOS_RECORDINGS
Definition: upnpcds.h:181
UPnpCDSExtension::CreateIDString
static QString CreateIDString(const QString &RequestId, const QString &Name, int Value)
Definition: upnpcds.cpp:1058
MSqlQuery::value
QVariant value(int i) const
Definition: mythdbcon.h:205
UPnpCDSExtension::m_sName
QString m_sName
Definition: upnpcds.h:208
UPnpCDSRequest::m_nStartingIndex
uint16_t m_nStartingIndex
Definition: upnpcds.h:80
MSqlQuery::exec
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:617
UPnpCDSExtension::IsSearchRequestForUs
virtual bool IsSearchRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:898
Resource::AddAttribute
void AddAttribute(const QString &sName, const QString &sValue)
Definition: upnpcdsobjects.h:121
UPnpCDSTv::CreateRoot
void CreateRoot() override
Definition: upnpcdstv.cpp:149
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
RecordingInfo::kLiveTVRecGroup
@ kLiveTVRecGroup
Definition: recordinginfo.h:192
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
CDSObject::SetChildContainerCount
void SetChildContainerCount(uint32_t nCount)
Allows the caller to set childContainerCount without having to load children.
Definition: upnpcdsobjects.cpp:261
HTTPRequest::TestMimeType
static QString TestMimeType(const QString &sFileName)
Definition: httprequest.cpp:1014
UPnpCDSTv::IsSearchRequestForUs
bool IsSearchRequestForUs(UPnpCDSRequest *pRequest) override
Definition: upnpcdstv.cpp:461
Property
Definition: upnpcdsobjects.h:45
UPnpCDSTv::LoadChannels
static bool LoadChannels(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:819
Property::AddAttribute
void AddAttribute(const QString &sName, const QString &sValue)
Definition: upnpcdsobjects.h:86
UPnpCDSTv::m_mapBackendPort
QMap< QString, int > m_mapBackendPort
Definition: upnpcdstv.h:84
UPnpCDSExtension::m_shortcuts
CDSShortCutList m_shortcuts
Definition: upnpcds.h:211
UPnpCDSTv::LoadRecordings
bool LoadRecordings(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, IDTokenMap tokens)
Definition: upnpcdstv.cpp:931
upnpcdstv.h
mythdate.h
MSqlQuery::kDedicatedConnection
@ kDedicatedConnection
Definition: mythdbcon.h:229
Property::SetValue
void SetValue(const QString &value)
Definition: upnpcdsobjects.h:71
UPnpCDSExtensionResults::m_nTotalMatches
uint16_t m_nTotalMatches
Definition: upnpcds.h:114
UPnpCDSTv::m_uriBase
QUrl m_uriBase
Definition: upnpcdstv.h:81
RecordingInfo::GetRecgroupString
static QString GetRecgroupString(uint recGroupID)
Temporary helper during transition from string to ID.
Definition: recordinginfo.cpp:1705
MSqlQuery::InitCon
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:549
UPnpCDSExtensionResults::Add
void Add(CDSObject *pObject)
Definition: upnpcds.cpp:32
CDSObject::SetPropValue
void SetPropValue(const QString &sName, const QString &sValue, const QString &type="")
Definition: upnpcdsobjects.cpp:123
MythCoreContext::GetBackendServerIP
QString GetBackendServerIP(void)
Returns the IP address of the locally defined backend IP.
Definition: mythcorecontext.cpp:1003
UPnpCDSTv::LoadDates
static bool LoadDates(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:609
UPnpCDSTv::LoadMovies
bool LoadMovies(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, IDTokenMap tokens)
Definition: upnpcdstv.cpp:894
MythDate::kAutoYear
@ kAutoYear
Add year only if different from current year.
Definition: mythdate.h:28
UPnpCDSTv::IsBrowseRequestForUs
bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest) override
Definition: upnpcdstv.cpp:415
UPnpCDSExtension::m_pRoot
CDSObject * m_pRoot
Definition: upnpcds.h:249
UPnpCDSTv::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: upnpcdstv.cpp:355
storagegroup.h
CDSObject::SetChildCount
void SetChildCount(uint32_t nCount)
Allows the caller to set childCount without having to load children.
Definition: upnpcdsobjects.cpp:238
UPnpCDSTv::m_mapBackendIp
QStringMap m_mapBackendIp
Definition: upnpcdstv.h:83
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:55
CDSObject::AddChild
CDSObject * AddChild(CDSObject *pChild)
Definition: upnpcdsobjects.cpp:171
UPnpCDSTv::PopulateArtworkURIS
static void PopulateArtworkURIS(CDSObject *pItem, const QString &sInetRef, int nSeason, const QUrl &URIBase)
Definition: upnpcdstv.cpp:1321
UPnpCDSRequest::m_sObjectId
QString m_sObjectId
Definition: upnpcds.h:76
MythDate::kSimplify
@ kSimplify
Do Today/Yesterday/Tomorrow transform.
Definition: mythdate.h:26
upnphelpers.h
ProgramInfo::SetChanID
void SetChanID(uint _chanid)
Definition: programinfo.h:522
recordinginfo.h
UPnPDateTime::DurationFormat
QString DurationFormat(std::chrono::milliseconds msec)
Duration Format.
Definition: upnphelpers.cpp:10
UPnpCDSTv::LoadGenres
static bool LoadGenres(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:676
UPnpCDSExtension::GetRoot
virtual CDSObject * GetRoot()
Definition: upnpcds.cpp:1091
mythcorecontext.h
CDSObject::AddResource
Resource * AddResource(const QString &sProtocol, const QString &sURI)
Definition: upnpcdsobjects.cpp:211
UPnpCDSTv::BindValues
static void BindValues(MSqlQuery &query, IDTokenMap tokens)
Definition: upnpcdstv.cpp:1489
MSqlQuery::bindValue
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:887
UPnpCDSTv::LoadRecGroups
static bool LoadRecGroups(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:747
MythDate::ISODate
@ ISODate
Default UTC.
Definition: mythdate.h:17
IDTokenMap
QMap< QString, QString > IDTokenMap
Definition: upnpcds.h:200
UPnpCDSTv::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: upnpcdstv.cpp:281
ProgramInfo::QueryTotalDuration
std::chrono::milliseconds QueryTotalDuration(void) const
If present this loads the total duration in milliseconds of the main video stream from recordedmarkup...
Definition: programinfo.cpp:4539
CDSObject::CreateVideoItem
static CDSObject * CreateVideoItem(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:613
CDSObject::CreateContainer
static CDSObject * CreateContainer(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:478
StorageGroup
Definition: storagegroup.h:11
CDSObject::m_sClass
QString m_sClass
Definition: upnpcdsobjects.h:196
CDSObject::CreateAlbum
static CDSObject * CreateAlbum(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:818
UPnpCDSTv::BuildWhereClause
static QString BuildWhereClause(QStringList clauses, IDTokenMap tokens)
Definition: upnpcdstv.cpp:1439
MythDate::kDateFull
@ kDateFull
Default local time.
Definition: mythdate.h:19
uint16_t
unsigned short uint16_t
Definition: iso6937tables.h:3
UPnpCDSExtensionResults
Definition: upnpcds.h:106
UPnPDateTime::NamedDayFormat
QString NamedDayFormat(const QDateTime &dateTime)
Named-Day Format.
Definition: upnphelpers.cpp:49
CDSObject::GetChildCount
uint32_t GetChildCount(void) const
Return the number of children in this container.
Definition: upnpcdsobjects.cpp:225
CDSObject::GetChild
CDSObject * GetChild(const QString &sID)
Definition: upnpcdsobjects.cpp:190
httprequest.h
UPnpCDSTv::LoadTitles
bool LoadTitles(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:522
CDSObject::CreateMovieGenre
static CDSObject * CreateMovieGenre(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:910
UPnPDateTime::resDurationFormat
QString resDurationFormat(std::chrono::milliseconds msec)
res@duration Format B.2.1.4 res@duration - UPnP ContentDirectory Service 2008, 2013
Definition: upnphelpers.cpp:81
UPnpCDSRequest
Definition: upnpcds.h:72
MSqlQuery::prepare
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:836
UPnpCDSExtension::m_sExtensionId
QString m_sExtensionId
Definition: upnpcds.h:207
UPnpCDSExtension::IsBrowseRequestForUs
virtual bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:811
ProgramInfo::SetRecordingStartTime
void SetRecordingStartTime(const QDateTime &dt)
Definition: programinfo.h:525