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( QObject::tr("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  qint64 uiStart = dtStartTime.toMSecsSinceEpoch();
1205  qint64 uiEnd = dtEndTime.toMSecsSinceEpoch();
1206  nDurationMS = (uiEnd - uiStart);
1207  nDurationMS = (nDurationMS > 0) ? nDurationMS : 0;
1208  }
1209 
1210  pItem->SetPropValue( "recordedDuration", UPnPDateTime::DurationFormat(nDurationMS));
1211 
1212 
1213  QSize resolution = QSize(nVideoWidth, nVideoHeight);
1214 
1215  // Attempt to guess the container if the information is missing from
1216  // the database
1217  if (sContainer.isEmpty())
1218  {
1219  sContainer = "NUV";
1220  if (sMimeType == "video/mp2p")
1221  {
1222  if (bTranscoded) // Transcoded mpeg will probably be in a PS container
1223  sContainer = "MPEG2-PS";
1224  else // For temporary backwards compatibility with old file naming
1225  sContainer = "MPEG2-TS"; // 99% of recordings will be in MPEG-2 TS containers before transcoding
1226  }
1227  else if (sMimeType == "video/mp2t")
1228  {
1229  sMimeType = "video/mp2p";
1230  sContainer = "MPEG2-TS";
1231  }
1232  }
1233  // Make an educated guess at the video codec if the information is
1234  // missing from the database
1235  if (sVideoCodec.isEmpty())
1236  {
1237  if (sMimeType == "video/mp2p" || sMimeType == "video/mp2t")
1238  sVideoCodec = (nVideoProps & VID_AVC) ? "H264" : "MPEG2VIDEO";
1239  else if (sMimeType == "video/mp4")
1240  sVideoCodec = "MPEG4";
1241  }
1242 
1243  // DLNA requires a mimetype of video/mp2p for TS files, it's not the
1244  // correct mimetype, but then DLNA doesn't seem to care about such
1245  // things
1246  if (sMimeType == "video/mp2t" || sMimeType == "video/mp2p")
1247  sMimeType = "video/mpeg";
1248 
1249  QUrl resURI = URIBase;
1250  QUrlQuery resQuery;
1251  resURI.setPath("/Content/GetRecording");
1252  resQuery.addQueryItem("RecordedId", QString::number(nRecordedId));
1253  resURI.setQuery(resQuery);
1254 
1255  QString sProtocol = DLNA::ProtocolInfoString(UPNPProtocol::kHTTP,
1256  sMimeType,
1257  resolution,
1258  dVideoFrameRate,
1259  sContainer,
1260  sVideoCodec,
1261  sAudioCodec,
1262  bTranscoded);
1263 
1264  Resource *pRes = pItem->AddResource( sProtocol, resURI.toEncoded() );
1265  // Must be the duration of the entire video not the scheduled programme duration
1266  // Appendix B.2.1.4 - res@duration
1267  if (nDurationMS > 0)
1268  pRes->AddAttribute ( "duration" , UPnPDateTime::resDurationFormat(nDurationMS) );
1269  if (nVideoHeight > 0 && nVideoWidth > 0)
1270  pRes->AddAttribute ( "resolution" , QString("%1x%2").arg(nVideoWidth).arg(nVideoHeight) );
1271  pRes->AddAttribute ( "size" , QString::number( nFileSize) );
1272 
1273  // ----------------------------------------------------------------------
1274  // Add Preview URI as <res>
1275  // MUST be _TN and 160px
1276  // ----------------------------------------------------------------------
1277 
1278  QUrl previewURI = URIBase;
1279  QUrlQuery previewQuery;
1280  previewURI.setPath("/Content/GetPreviewImage");
1281  previewQuery.addQueryItem("RecordedId", QString::number(nRecordedId));
1282  previewQuery.addQueryItem("Width", "160");
1283  previewQuery.addQueryItem("Format", "JPG");
1284  previewURI.setQuery(previewQuery);
1285 
1286  sProtocol = DLNA::ProtocolInfoString(UPNPProtocol::kHTTP, "image/jpeg",
1287  QSize(160, 160));
1288  pItem->AddResource( sProtocol, previewURI.toEncoded());
1289 
1290  // ----------------------------------------------------------------------
1291  // Add Artwork
1292  // ----------------------------------------------------------------------
1293  if (!sInetRef.isEmpty())
1294  {
1295  PopulateArtworkURIS(pItem, sInetRef, nSeason, URIBase);
1296  }
1297 
1298  pResults->Add( pItem );
1299  pItem->DecrRef();
1300  }
1301 
1302  // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
1303  // at least the size of this result set
1304  if (query.size() > 0)
1305  pResults->m_nTotalMatches = query.size();
1306 
1307  // Fetch the total number of matches ignoring any LIMITs
1308  query.prepare("SELECT FOUND_ROWS()");
1309  if (query.exec() && query.next())
1310  pResults->m_nTotalMatches = query.value(0).toUInt();
1311 
1312  return true;
1313 }
1314 
1316 //
1318 
1319 void UPnpCDSTv::PopulateArtworkURIS(CDSObject* pItem, const QString &sInetRef,
1320  int nSeason, const QUrl& URIBase)
1321 {
1322  QUrl artURI = URIBase;
1323  artURI.setPath("/Content/GetRecordingArtwork");
1324  QUrlQuery artQuery(artURI.query());
1325  artQuery.addQueryItem("Inetref", sInetRef);
1326  artQuery.addQueryItem("Season", QString::number(nSeason));
1327  artURI.setQuery(artQuery);
1328 
1329  // Prefer JPEG over PNG here, although PNG is allowed JPEG probably
1330  // has wider device support and crucially the filesizes are smaller
1331  // which speeds up loading times over the network
1332 
1333  // We MUST include the thumbnail size, but since some clients may use the
1334  // first image they see and the thumbnail is tiny, instead return the
1335  // medium first. The large could be very large, which is no good if the
1336  // client is pulling images for an entire list at once!
1337 
1338  // Thumbnail
1339  // At least one albumArtURI must be a ThumbNail (TN) no larger
1340  // than 160x160, and it must also be a jpeg
1341  QUrl thumbURI = artURI;
1342  QUrlQuery thumbQuery(thumbURI.query());
1343  thumbQuery.addQueryItem("Type", "screenshot");
1344  thumbQuery.addQueryItem("Width", "160");
1345  thumbQuery.addQueryItem("Height", "160");
1346  thumbURI.setQuery(thumbQuery);
1347 
1348  // Small
1349  // Must be no more than 640x480
1350  QUrl smallURI = artURI;
1351  QUrlQuery smallQuery(smallURI.query());
1352  smallQuery.addQueryItem("Type", "coverart");
1353  smallQuery.addQueryItem("Width", "640");
1354  smallQuery.addQueryItem("Height", "480");
1355  smallURI.setQuery(smallQuery);
1356 
1357  // Medium
1358  // Must be no more than 1024x768
1359  QUrl mediumURI = artURI;
1360  QUrlQuery mediumQuery(mediumURI.query());
1361  mediumQuery.addQueryItem("Type", "coverart");
1362  mediumQuery.addQueryItem("Width", "1024");
1363  mediumQuery.addQueryItem("Height", "768");
1364  mediumURI.setQuery(mediumQuery);
1365 
1366  // Large
1367  // Must be no more than 4096x4096 - for our purposes, just return
1368  // a fullsize image
1369  QUrl largeURI = artURI;
1370  QUrlQuery largeQuery(largeURI.query());
1371  largeQuery.addQueryItem("Type", "fanart");
1372  largeURI.setQuery(largeQuery);
1373 
1374  QList<Property*> propList = pItem->GetProperties("albumArtURI");
1375  if (propList.size() >= 4)
1376  {
1377  Property *pProp = propList.at(0);
1378  if (pProp)
1379  {
1380  pProp->SetValue(mediumURI.toEncoded());
1381  pProp->AddAttribute("dlna:profileID", "JPEG_MED");
1382  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
1383  }
1384 
1385  pProp = propList.at(1);
1386  if (pProp)
1387  {
1388  pProp->SetValue(thumbURI.toEncoded());
1389  pProp->AddAttribute("dlna:profileID", "JPEG_TN");
1390  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
1391  }
1392 
1393  pProp = propList.at(2);
1394  if (pProp)
1395  {
1396  pProp->SetValue(smallURI.toEncoded());
1397  pProp->AddAttribute("dlna:profileID", "JPEG_SM");
1398  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
1399  }
1400 
1401  pProp = propList.at(3);
1402  if (pProp)
1403  {
1404  pProp->SetValue(largeURI.toEncoded());
1405  pProp->AddAttribute("dlna:profileID", "JPEG_LRG");
1406  pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
1407  }
1408  }
1409 
1410  if (pItem->m_sClass.startsWith("object.item.videoItem"))
1411  {
1412  QString sProtocol;
1413 
1415  "image/jpeg", QSize(1024, 768));
1416  pItem->AddResource( sProtocol, mediumURI.toEncoded());
1417 
1418  // We already include a thumbnail
1419  //sProtocol = DLNA::ProtocolInfoString(UPNPProtocol::kHTTP,
1420  // "image/jpeg", QSize(160, 160));
1421  //pItem->AddResource( sProtocol, thumbURI.toEncoded());
1422 
1424  "image/jpeg", QSize(640, 480));
1425  pItem->AddResource( sProtocol, smallURI.toEncoded());
1426 
1428  "image/jpeg", QSize(1920, 1080)); // Not the actual res, we don't know that
1429  pItem->AddResource( sProtocol, largeURI.toEncoded());
1430  }
1431 }
1432 
1434 //
1436 
1437 QString UPnpCDSTv::BuildWhereClause( QStringList clauses,
1438  IDTokenMap tokens)
1439 {
1440  // We ignore protected recgroups, UPnP offers no mechanism to provide
1441  // restricted access to containers and there's no point in having
1442  // password protected groups if that protection can be easily circumvented
1443  // by children just by pointing a phone, tablet or other computer at the
1444  // advertised UPnP server.
1445  //
1446  // In short, don't use password protected recording groups if you want to
1447  // be able to access those recordings via upnp
1448  clauses.append("g.password=''");
1449  // Ignore recordings in the LiveTV and Deleted recgroups
1450  // We cannot currently prevent LiveTV recordings from being expired while
1451  // being streamed to a upnp device, so there's no point in listing them.
1453  clauses.append(QString("g.recgroup != '%1'").arg(liveTVGroup));
1455  clauses.append(QString("g.recgroup != '%1'").arg(deletedGroup));
1456 
1457  if (tokens["recording"].toInt() > 0)
1458  clauses.append("r.recordedid=:RECORDED_ID");
1459  if (!tokens["date"].isEmpty())
1460  clauses.append("DATE(CONVERT_TZ(r.starttime, 'UTC', 'SYSTEM'))=:DATE");
1461  if (!tokens["genre"].isEmpty())
1462  clauses.append("r.category=:GENRE");
1463  if (!tokens["recgroup"].isEmpty())
1464  clauses.append("r.recgroup=:RECGROUP");
1465  if (!tokens["title"].isEmpty())
1466  clauses.append("r.title=:TITLE");
1467  if (!tokens["channel"].isEmpty())
1468  clauses.append("r.chanid=:CHANNEL");
1469  // Special token
1470  if (!tokens["category_type"].isEmpty())
1471  clauses.append("p.category_type=:CATTYPE");
1472 
1473  QString whereString;
1474  if (!clauses.isEmpty())
1475  {
1476  whereString = " WHERE ";
1477  whereString.append(clauses.join(" AND "));
1478  }
1479 
1480  return whereString;
1481 }
1482 
1484 //
1486 
1488  IDTokenMap tokens)
1489 {
1490  if (tokens["recording"].toInt() > 0)
1491  query.bindValue(":RECORDED_ID", tokens["recording"]);
1492  if (!tokens["date"].isEmpty())
1493  query.bindValue(":DATE", tokens["date"]);
1494  if (!tokens["genre"].isEmpty())
1495  query.bindValue(":GENRE", tokens["genre"] == "MYTH_NO_GENRE" ? "" : tokens["genre"]);
1496  if (!tokens["recgroup"].isEmpty())
1497  query.bindValue(":RECGROUP", tokens["recgroup"]);
1498  if (!tokens["title"].isEmpty())
1499  query.bindValue(":TITLE", tokens["title"]);
1500  if (tokens["channel"].toInt() > 0)
1501  query.bindValue(":CHANNEL", tokens["channel"]);
1502  if (!tokens["category_type"].isEmpty())
1503  query.bindValue(":CATTYPE", tokens["category_type"]);
1504 }
1505 
1506 
1507 // vim:ts=4:sw=4:ai:et:si:sts=4
CDSObject::GetProperties
QList< Property * > GetProperties(const QString &sName)
Definition: upnpcdsobjects.cpp:99
MSqlQuery::next
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:783
MSqlQuery
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:126
UPnpCDSRequest::m_nRequestedCount
uint16_t m_nRequestedCount
Definition: upnpcds.h:80
MythDate::toString
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:80
MSqlQuery::size
int size(void) const
Definition: mythdbcon.h:203
UPnpCDSExtension
Definition: upnpcds.h:203
ReferenceCounter::DecrRef
virtual int DecrRef(void)
Decrements reference count and deletes on 0.
Definition: referencecounter.cpp:125
UPNPProtocol::kHTTP
@ kHTTP
Definition: upnphelpers.h:129
CDSObject
Definition: upnpcdsobjects.h:184
MythDate::as_utc
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:23
UPnpCDSRequest::m_sParentId
QString m_sParentId
Definition: upnpcds.h:85
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:1101
Resource
Definition: upnpcdsobjects.h:103
UPnpCDSTv::UPnpCDSTv
UPnpCDSTv()
Definition: upnpcdstv.cpp:133
RecordingInfo::kDeletedRecGroup
@ kDeletedRecGroup
Definition: recordinginfo.h:192
UPnPShortcutFeature::VIDEOS_RECORDINGS
@ VIDEOS_RECORDINGS
Definition: upnpcds.h:180
UPnpCDSExtension::CreateIDString
static QString CreateIDString(const QString &RequestId, const QString &Name, int Value)
Definition: upnpcds.cpp:1057
MSqlQuery::value
QVariant value(int i) const
Definition: mythdbcon.h:198
arg
arg(title).arg(filename).arg(doDelete))
UPnpCDSExtension::m_sName
QString m_sName
Definition: upnpcds.h:207
UPnpCDSRequest::m_nStartingIndex
uint16_t m_nStartingIndex
Definition: upnpcds.h:79
MSqlQuery::exec
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:603
UPnpCDSExtension::IsSearchRequestForUs
virtual bool IsSearchRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:896
Resource::AddAttribute
void AddAttribute(const QString &sName, const QString &sValue)
Definition: upnpcdsobjects.h:120
UPnpCDSTv::CreateRoot
void CreateRoot() override
Definition: upnpcdstv.cpp:147
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:23
RecordingInfo::kLiveTVRecGroup
@ kLiveTVRecGroup
Definition: recordinginfo.h:191
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
ProgramInfo::QueryTotalDuration
uint32_t QueryTotalDuration(void) const
If present this loads the total duration in milliseconds of the main video stream from recordedmarkup...
Definition: programinfo.cpp:4361
CDSObject::SetChildContainerCount
void SetChildContainerCount(uint32_t nCount)
Allows the caller to set childContainerCount without having to load children.
Definition: upnpcdsobjects.cpp:255
HTTPRequest::TestMimeType
static QString TestMimeType(const QString &sFileName)
Definition: httprequest.cpp:1081
UPnpCDSTv::IsSearchRequestForUs
bool IsSearchRequestForUs(UPnpCDSRequest *pRequest) override
Definition: upnpcdstv.cpp:459
Property
Definition: upnpcdsobjects.h:45
UPnpCDSTv::LoadChannels
static bool LoadChannels(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:817
Property::AddAttribute
void AddAttribute(const QString &sName, const QString &sValue)
Definition: upnpcdsobjects.h:85
UPnpCDSTv::m_mapBackendPort
QMap< QString, int > m_mapBackendPort
Definition: upnpcdstv.h:84
UPnpCDSExtension::m_shortcuts
CDSShortCutList m_shortcuts
Definition: upnpcds.h:210
UPnpCDSTv::LoadRecordings
bool LoadRecordings(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, IDTokenMap tokens)
Definition: upnpcdstv.cpp:929
upnpcdstv.h
mythdate.h
MSqlQuery::kDedicatedConnection
@ kDedicatedConnection
Definition: mythdbcon.h:217
Property::SetValue
void SetValue(const QString &value)
Definition: upnpcdsobjects.h:70
UPnpCDSExtensionResults::m_nTotalMatches
uint16_t m_nTotalMatches
Definition: upnpcds.h:113
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:1624
MSqlQuery::InitCon
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:535
UPnpCDSExtensionResults::Add
void Add(CDSObject *pObject)
Definition: upnpcds.cpp:30
CDSObject::SetPropValue
void SetPropValue(const QString &sName, const QString &sValue, const QString &type="")
Definition: upnpcdsobjects.cpp:117
MythCoreContext::GetBackendServerIP
QString GetBackendServerIP(void)
Returns the IP address of the locally defined backend IP.
Definition: mythcorecontext.cpp:1025
UPnpCDSTv::LoadDates
static bool LoadDates(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:607
UPnpCDSTv::LoadMovies
bool LoadMovies(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, IDTokenMap tokens)
Definition: upnpcdstv.cpp:892
MythDate::kAutoYear
@ kAutoYear
Add year only if different from current year.
Definition: mythdate.h:25
UPnpCDSTv::IsBrowseRequestForUs
bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest) override
Definition: upnpcdstv.cpp:413
UPnpCDSExtension::m_pRoot
CDSObject * m_pRoot
Definition: upnpcds.h:248
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:353
storagegroup.h
CDSObject::SetChildCount
void SetChildCount(uint32_t nCount)
Allows the caller to set childCount without having to load children.
Definition: upnpcdsobjects.cpp:232
VID_AVC
@ VID_AVC
Definition: programtypes.h:190
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:60
CDSObject::AddChild
CDSObject * AddChild(CDSObject *pChild)
Definition: upnpcdsobjects.cpp:165
UPnpCDSTv::PopulateArtworkURIS
static void PopulateArtworkURIS(CDSObject *pItem, const QString &sInetRef, int nSeason, const QUrl &URIBase)
Definition: upnpcdstv.cpp:1319
UPnpCDSRequest::m_sObjectId
QString m_sObjectId
Definition: upnpcds.h:75
MythDate::kSimplify
@ kSimplify
Do Today/Yesterday/Tomorrow transform.
Definition: mythdate.h:23
upnphelpers.h
ProgramInfo::SetChanID
void SetChanID(uint _chanid)
Definition: programinfo.h:513
recordinginfo.h
UPnpCDSTv::LoadGenres
static bool LoadGenres(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:674
UPnpCDSExtension::GetRoot
virtual CDSObject * GetRoot()
Definition: upnpcds.cpp:1090
mythcorecontext.h
UPnPDateTime::DurationFormat
QString DurationFormat(uint32_t msec)
Duration Format.
Definition: upnphelpers.cpp:10
CDSObject::AddResource
Resource * AddResource(const QString &sProtocol, const QString &sURI)
Definition: upnpcdsobjects.cpp:205
UPnpCDSTv::BindValues
static void BindValues(MSqlQuery &query, IDTokenMap tokens)
Definition: upnpcdstv.cpp:1487
MSqlQuery::bindValue
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:864
UPnpCDSTv::LoadRecGroups
static bool LoadRecGroups(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:745
MythDate::ISODate
@ ISODate
Default UTC.
Definition: mythdate.h:14
IDTokenMap
QMap< QString, QString > IDTokenMap
Definition: upnpcds.h:199
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:279
CDSObject::CreateVideoItem
static CDSObject * CreateVideoItem(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:603
CDSObject::CreateContainer
static CDSObject * CreateContainer(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:468
StorageGroup
Definition: storagegroup.h:12
CDSObject::m_sClass
QString m_sClass
Definition: upnpcdsobjects.h:195
CDSObject::CreateAlbum
static CDSObject * CreateAlbum(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:808
UPnpCDSTv::BuildWhereClause
static QString BuildWhereClause(QStringList clauses, IDTokenMap tokens)
Definition: upnpcdstv.cpp:1437
MythDate::kDateFull
@ kDateFull
Default local time.
Definition: mythdate.h:16
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
UPnPDateTime::resDurationFormat
QString resDurationFormat(uint32_t msec)
res@duration Format B.2.1.4 res@duration - UPnP ContentDirectory Service 2008, 2013
Definition: upnphelpers.cpp:81
CDSObject::GetChildCount
uint32_t GetChildCount(void) const
Return the number of children in this container.
Definition: upnpcdsobjects.cpp:219
CDSObject::GetChild
CDSObject * GetChild(const QString &sID)
Definition: upnpcdsobjects.cpp:184
httprequest.h
query
MSqlQuery query(MSqlQuery::InitCon())
UPnpCDSTv::LoadTitles
bool LoadTitles(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:520
CDSObject::CreateMovieGenre
static CDSObject * CreateMovieGenre(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
Definition: upnpcdsobjects.cpp:900
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:808
UPnpCDSExtension::m_sExtensionId
QString m_sExtensionId
Definition: upnpcds.h:206
UPnpCDSExtension::IsBrowseRequestForUs
virtual bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:809
ProgramInfo::SetRecordingStartTime
void SetRecordingStartTime(const QDateTime &dt)
Definition: programinfo.h:516