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