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