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