MythTV master
upnpcdstv.cpp
Go to the documentation of this file.
1
2// 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
28
29// MythBackend
30#include "upnpcdstv.h"
31
32/*
33 Recordings RecTv
34 - All Programs RecTv/All
35 + <recording 1> RecTv/All/item?ChanId=1004&StartTime=2006-04-06T20:00:00
36 + <recording 2>
37 + <recording 3>
38 - By Title RecTv/title
39 - <title 1> RecTv/title/key=Stargate SG-1
40 + <recording 1> RecTv/title/key=Stargate SG-1/item?ChanId=1004&StartTime=2006-04-06T20:00:00
41 + <recording 2>
42 - By Genre
43 - By Date
44 - By Channel
45 - By Group
46*/
47
48
49// UPnpCDSRootInfo UPnpCDSTv::g_RootNodes[] =
50// {
51// { "All Recordings",
52// "*",
53// "SELECT 0 as key, "
54// "CONCAT( title, ': ', subtitle) as name, "
55// "1 as children "
56// "FROM recorded r "
57// "%1 "
58// "ORDER BY r.starttime DESC",
59// "",
60// "r.starttime DESC",
61// "object.container",
62// "object.item.videoItem" },
63//
64// { "By Title",
65// "r.title",
66// "SELECT r.title as id, "
67// "r.title as name, "
68// "count( r.title ) as children "
69// "FROM recorded r "
70// "%1 "
71// "GROUP BY r.title "
72// "ORDER BY r.title",
73// "WHERE r.title=:KEY",
74// "r.title",
75// "object.container",
76// "object.container" },
77//
78// { "By Genre",
79// "r.category",
80// "SELECT r.category as id, "
81// "r.category as name, "
82// "count( r.category ) as children "
83// "FROM recorded r "
84// "%1 "
85// "GROUP BY r.category "
86// "ORDER BY r.category",
87// "WHERE r.category=:KEY",
88// "r.category",
89// "object.container",
90// "object.container.genre.movieGenre" },
91//
92// { "By Date",
93// "DATE_FORMAT(r.starttime, '%Y-%m-%d')",
94// "SELECT DATE_FORMAT(r.starttime, '%Y-%m-%d') as id, "
95// "DATE_FORMAT(r.starttime, '%Y-%m-%d %W') as name, "
96// "count( DATE_FORMAT(r.starttime, '%Y-%m-%d %W') ) as children "
97// "FROM recorded r "
98// "%1 "
99// "GROUP BY name "
100// "ORDER BY r.starttime DESC",
101// "WHERE DATE_FORMAT(r.starttime, '%Y-%m-%d') =:KEY",
102// "r.starttime DESC",
103// "object.container",
104// "object.container"
105// },
106//
107// { "By Channel",
108// "r.chanid",
109// "SELECT channel.chanid as id, "
110// "CONCAT(channel.channum, ' ', channel.callsign) as name, "
111// "count( channum ) as children "
112// "FROM channel "
113// "INNER JOIN recorded r ON channel.chanid = r.chanid "
114// "%1 "
115// "GROUP BY name "
116// "ORDER BY channel.chanid",
117// "WHERE channel.chanid=:KEY",
118// "",
119// "object.container",
120// "object.container"}, // Cannot be .channelGroup because children of channelGroup must be videoBroadcast items
121//
122// { "By Group",
123// "recgroup",
124// "SELECT recgroup as id, "
125// "recgroup as name, count( recgroup ) as children "
126// "FROM recorded "
127// "%1 "
128// "GROUP BY recgroup "
129// "ORDER BY recgroup",
130// "WHERE recgroup=:KEY",
131// "recgroup",
132// "object.container",
133// "object.container.album" }
134// };
135
137 : UPnpCDSExtension( QObject::tr("Recordings"), "Recordings",
138 "object.item.videoItem" )
139{
140 QString sServerIp = gCoreContext->GetBackendServerIP();
141 int sPort = gCoreContext->GetBackendStatusPort();
142 m_uriBase.setScheme("http");
143 m_uriBase.setHost(sServerIp);
144 m_uriBase.setPort(sPort);
145
146 // ShortCuts
148}
149
151{
152 if (m_pRoot)
153 return;
154
156 m_sName,
157 "0");
158
159 QString containerId = m_sExtensionId + "/%1";
160
161 // HACK: I'm not entirely happy with this solution, but it's at least
162 // tidier than passing through half a dozen extra args to Load[Foo]
163 // or having yet more methods just to load the counts
164 auto *pRequest = new UPnpCDSRequest();
165 pRequest->m_nRequestedCount = 0; // We don't want to load any results, we just want the TotalCount
166 auto *pResult = new UPnpCDSExtensionResults();
167 IDTokenMap tokens;
168 // END HACK
169
170 // -----------------------------------------------------------------------
171 // All Recordings
172 // -----------------------------------------------------------------------
173 CDSObject* pContainer = CDSObject::CreateContainer ( containerId.arg("Recording"),
174 QObject::tr("All Recordings"),
175 m_sExtensionId, // Parent Id
176 nullptr );
177 // HACK
178 LoadRecordings(pRequest, pResult, tokens);
179 pContainer->SetChildCount(pResult->m_nTotalMatches);
180 pContainer->SetChildContainerCount(0);
181 // END HACK
182 m_pRoot->AddChild(pContainer);
183
184 // -----------------------------------------------------------------------
185 // By Film
186 // -----------------------------------------------------------------------
187 pContainer = CDSObject::CreateContainer ( containerId.arg("Movie"),
188 QObject::tr("Movies"),
189 m_sExtensionId, // Parent Id
190 nullptr );
191 // HACK
192 LoadMovies(pRequest, pResult, tokens);
193 pContainer->SetChildCount(pResult->m_nTotalMatches);
194 pContainer->SetChildContainerCount(0);
195 // END HACK
196 m_pRoot->AddChild(pContainer);
197
198 // -----------------------------------------------------------------------
199 // By Title
200 // -----------------------------------------------------------------------
201 pContainer = CDSObject::CreateContainer ( containerId.arg("Title"),
202 QObject::tr("Title"),
203 m_sExtensionId, // Parent Id
204 nullptr );
205 // HACK
206 LoadTitles(pRequest, pResult, tokens);
207 pContainer->SetChildCount(pResult->m_nTotalMatches);
208 // Tricky to calculate ChildContainerCount without loading the full
209 // result set
210 //pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
211 // END HACK
212 m_pRoot->AddChild(pContainer);
213
214 // -----------------------------------------------------------------------
215 // By Date
216 // -----------------------------------------------------------------------
217 pContainer = CDSObject::CreateContainer ( containerId.arg("Date"),
218 QObject::tr("Date"),
219 m_sExtensionId, // Parent Id
220 nullptr );
221 // HACK
222 LoadDates(pRequest, pResult, tokens);
223 pContainer->SetChildCount(pResult->m_nTotalMatches);
224 pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
225 // END HACK
226 m_pRoot->AddChild(pContainer);
227
228 // -----------------------------------------------------------------------
229 // By Genre
230 // -----------------------------------------------------------------------
231 pContainer = CDSObject::CreateContainer ( containerId.arg("Genre"),
232 QObject::tr("Genre"),
233 m_sExtensionId, // Parent Id
234 nullptr );
235 // HACK
236 LoadGenres(pRequest, pResult, tokens);
237 pContainer->SetChildCount(pResult->m_nTotalMatches);
238 pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
239 // END HACK
240 m_pRoot->AddChild(pContainer);
241
242 // -----------------------------------------------------------------------
243 // By Channel
244 // -----------------------------------------------------------------------
245 pContainer = CDSObject::CreateContainer ( containerId.arg("Channel"),
246 QObject::tr("Channel"),
247 m_sExtensionId, // Parent Id
248 nullptr );
249 // HACK
250 LoadChannels(pRequest, pResult, tokens);
251 pContainer->SetChildCount(pResult->m_nTotalMatches);
252 pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
253 // END HACK
254 m_pRoot->AddChild(pContainer);
255
256 // -----------------------------------------------------------------------
257 // By Recording Group
258 // -----------------------------------------------------------------------
259 pContainer = CDSObject::CreateContainer ( containerId.arg("Recgroup"),
260 QObject::tr("Recording Group"),
261 m_sExtensionId, // Parent Id
262 nullptr );
263 // HACK
264 LoadRecGroups(pRequest, pResult, tokens);
265 pContainer->SetChildCount(pResult->m_nTotalMatches);
266 pContainer->SetChildContainerCount(pResult->m_nTotalMatches);
267 // END HACK
268 m_pRoot->AddChild(pContainer);
269
270 // -----------------------------------------------------------------------
271
272 // HACK
273 delete pRequest;
274 delete pResult;
275 // END HACK
276}
277
279//
281
283 UPnpCDSExtensionResults* pResults,
284 const IDTokenMap& tokens, const QString& currentToken)
285{
286 if (currentToken.isEmpty())
287 {
288 LOG(VB_GENERAL, LOG_ERR, QString("UPnpCDSTV::LoadMetadata: Final "
289 "token missing from id: %1")
290 .arg(pRequest->m_sObjectId));
291 return false;
292 }
293
294 // Root or Root + 1
295 if (tokens[currentToken].isEmpty())
296 {
297 CDSObject *container = nullptr;
298
299 if (pRequest->m_sObjectId == m_sExtensionId)
300 container = GetRoot();
301 else
302 container = GetRoot()->GetChild(pRequest->m_sObjectId);
303
304 if (container)
305 {
306 pResults->Add(container);
307 pResults->m_nTotalMatches = 1;
308 return true;
309 }
310 LOG(VB_GENERAL, LOG_ERR, QString("UPnpCDSTV::LoadMetadata: Requested "
311 "object cannot be found: %1")
312 .arg(pRequest->m_sObjectId));
313 }
314 else if (currentToken == "recording")
315 {
316 return LoadRecordings(pRequest, pResults, tokens);
317 }
318 else if (currentToken == "title")
319 {
320 return LoadTitles(pRequest, pResults, tokens);
321 }
322 else if (currentToken == "date")
323 {
324 return LoadDates(pRequest, pResults, tokens);
325 }
326 else if (currentToken == "genre")
327 {
328 return LoadGenres(pRequest, pResults, tokens);
329 }
330 else if (currentToken == "recgroup")
331 {
332 return LoadRecGroups(pRequest, pResults, tokens);
333 }
334 else if (currentToken == "channel")
335 {
336 return LoadChannels(pRequest, pResults, tokens);
337 }
338 else if (currentToken == "movie")
339 {
340 return LoadMovies(pRequest, pResults, tokens);
341 }
342 else
343 {
344 LOG(VB_GENERAL, LOG_ERR,
345 QString("UPnpCDSTV::LoadMetadata(): "
346 "Unhandled metadata request for '%1'.").arg(currentToken));
347 }
348
349 return false;
350}
351
353//
355
357 UPnpCDSExtensionResults* pResults,
358 const IDTokenMap& tokens, const QString& currentToken)
359{
360 if (currentToken.isEmpty() || currentToken == m_sExtensionId.toLower())
361 {
362 // Root
363 pResults->Add(GetRoot()->GetChildren());
364 pResults->m_nTotalMatches = GetRoot()->GetChildCount();
365 return true;
366 }
367 if (currentToken == "title")
368 {
369 if (!tokens["title"].isEmpty())
370 return LoadRecordings(pRequest, pResults, tokens);
371 return LoadTitles(pRequest, pResults, tokens);
372 }
373 if (currentToken == "date")
374 {
375 if (!tokens["date"].isEmpty())
376 return LoadRecordings(pRequest, pResults, tokens);
377 return LoadDates(pRequest, pResults, tokens);
378 }
379 if (currentToken == "genre")
380 {
381 if (!tokens["genre"].isEmpty())
382 return LoadRecordings(pRequest, pResults, tokens);
383 return LoadGenres(pRequest, pResults, tokens);
384 }
385 if (currentToken == "recgroup")
386 {
387 if (!tokens["recgroup"].isEmpty())
388 return LoadRecordings(pRequest, pResults, tokens);
389 return LoadRecGroups(pRequest, pResults, tokens);
390 }
391 if (currentToken == "channel")
392 {
393 if (tokens["channel"].toInt() > 0)
394 return LoadRecordings(pRequest, pResults, tokens);
395 return LoadChannels(pRequest, pResults, tokens);
396 }
397 if (currentToken == "movie")
398 {
399 return LoadMovies(pRequest, pResults, tokens);
400 }
401 if (currentToken == "recording")
402 {
403 return LoadRecordings(pRequest, pResults, tokens);
404 }
405 LOG(VB_GENERAL, LOG_ERR,
406 QString("UPnpCDSTV::LoadChildren(): "
407 "Unhandled metadata request for '%1'.").arg(currentToken));
408
409 return false;
410}
411
413//
415
417{
418 // ----------------------------------------------------------------------
419 // See if we need to modify the request for compatibility
420 // ----------------------------------------------------------------------
421
422 // ----------------------------------------------------------------------
423 // Xbox360 compatibility code.
424 // ----------------------------------------------------------------------
425
426// if (pRequest->m_eClient == CDS_ClientXBox &&
427// pRequest->m_sContainerID == "15" &&
428// gCoreContext->GetSetting("UPnP/WMPSource") != "1")
429// {
430// pRequest->m_sObjectId = "Videos/0";
431//
432// LOG(VB_UPNP, LOG_INFO,
433// "UPnpCDSTv::IsBrowseRequestForUs - Yes ContainerID == 15");
434// return true;
435// }
436
437 // ----------------------------------------------------------------------
438 // WMP11 compatibility code
439 // ----------------------------------------------------------------------
440// if (pRequest->m_eClient == CDS_ClientWMP &&
441// pRequest->m_nClientVersion < 12.0 &&
442// pRequest->m_sContainerID == "13" &&
443// gCoreContext->GetSetting("UPnP/WMPSource") != "1")
444// {
445// pRequest->m_sObjectId = "RecTv/0";
446//
447// LOG(VB_UPNP, LOG_INFO,
448// "UPnpCDSTv::IsBrowseRequestForUs - Yes, ObjectId == 13");
449// return true;
450// }
451
452 LOG(VB_UPNP, LOG_INFO,
453 "UPnpCDSTv::IsBrowseRequestForUs - Not sure... Calling base class.");
454
456}
457
459//
461
463{
464 // ----------------------------------------------------------------------
465 // See if we need to modify the request for compatibility
466 // ----------------------------------------------------------------------
467
468 // ----------------------------------------------------------------------
469 // XBox 360 compatibility code
470 // ----------------------------------------------------------------------
471
472// if (pRequest->m_eClient == CDS_ClientXBox &&
473// pRequest->m_sContainerID == "15" &&
474// gCoreContext->GetSetting("UPnP/WMPSource") != "1")
475// {
476// pRequest->m_sObjectId = "Videos/0";
477//
478// LOG(VB_UPNP, LOG_INFO, "UPnpCDSTv::IsSearchRequestForUs... Yes.");
479//
480// return true;
481// }
482//
483//
484// if ((pRequest->m_sObjectId.isEmpty()) &&
485// (!pRequest->m_sContainerID.isEmpty()))
486// pRequest->m_sObjectId = pRequest->m_sContainerID;
487
488 // ----------------------------------------------------------------------
489
490 bool bOurs = UPnpCDSExtension::IsSearchRequestForUs( pRequest );
491
492 // ----------------------------------------------------------------------
493 // WMP11 compatibility code
494 //
495 // In this mode browsing for "Videos" is forced to either RecordedTV (us)
496 // or Videos (handled by upnpcdsvideo)
497 //
498 // ----------------------------------------------------------------------
499
500// if ( bOurs && pRequest->m_eClient == CDS_ClientWMP &&
501// pRequest->m_nClientVersion < 12.0)
502// {
503// // GetBoolSetting()?
504// if ( gCoreContext->GetSetting("UPnP/WMPSource") != "1")
505// {
506// pRequest->m_sObjectId = "RecTv/0";
507// // -=>TODO: Not sure why this was added
508// pRequest->m_sParentId = '8';
509// }
510// else
511// bOurs = false;
512// }
513
514 return bOurs;
515}
516
518//
520
521 // TODO Load titles where there is more than one, otherwise the recording, but
522 // somehow do so with the minimum number of queries and code duplication
524 UPnpCDSExtensionResults* pResults,
525 const IDTokenMap& tokens)
526{
527 QString sRequestId = pRequest->m_sObjectId;
528
529 uint16_t nCount = pRequest->m_nRequestedCount;
530 uint16_t nOffset = pRequest->m_nStartingIndex;
531
532 // We must use a dedicated connection to get an accurate value from
533 // FOUND_ROWS()
535
536 QString sql = "SELECT SQL_CALC_FOUND_ROWS "
537 "r.title, r.inetref, r.recordedid, COUNT(*) "
538 "FROM recorded r "
539 "LEFT JOIN recgroups g ON r.recgroup=g.recgroup "
540 "%1 " // WHERE clauses
541 "GROUP BY r.title "
542 "ORDER BY r.title "
543 "LIMIT :OFFSET,:COUNT";
544
545 QStringList clauses;
546 QString whereString = BuildWhereClause(clauses, tokens);
547 query.prepare(sql.arg(whereString));
548 BindValues(query, tokens);
549
550 query.bindValue(":OFFSET", nOffset);
551 query.bindValue(":COUNT", nCount);
552
553 if (!query.exec())
554 return false;
555
556 while (query.next())
557 {
558 QString sTitle = query.value(0).toString();
559 QString sInetRef = query.value(1).toString();
560 int nRecordingID = query.value(2).toInt();
561 int nTitleCount = query.value(3).toInt();
562
563 if (nTitleCount > 1)
564 {
565 // TODO Album or plain old container?
566 CDSObject* pContainer = CDSObject::CreateAlbum( CreateIDString(sRequestId, "Title", sTitle),
567 sTitle,
568 pRequest->m_sParentId,
569 nullptr );
570
571
572 pContainer->SetPropValue("description", QObject::tr("%n Episode(s)", "", nTitleCount));
573 pContainer->SetPropValue("longdescription", QObject::tr("%n Episode(s)", "", nTitleCount));
574
575 pContainer->SetChildCount(nTitleCount);
576 pContainer->SetChildContainerCount(0); // Recordings, no containers
577 pContainer->SetPropValue("storageMedium", "HDD");
578
579 // Artwork
580 PopulateArtworkURIS(pContainer, sInetRef, 0, m_uriBase); // No particular season
581
582 pResults->Add(pContainer);
583 pContainer->DecrRef();
584 }
585 else
586 {
587 IDTokenMap newTokens(tokens);
588 newTokens.insert("recording", QString::number(nRecordingID));
589 LoadRecordings(pRequest, pResults, newTokens);
590 }
591 }
592
593 // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
594 // at least the size of this result set
595 if (query.size() > 0)
596 pResults->m_nTotalMatches = query.size();
597
598 // Fetch the total number of matches ignoring any LIMITs
599 query.prepare("SELECT FOUND_ROWS()");
600 if (query.exec() && query.next())
601 pResults->m_nTotalMatches = query.value(0).toUInt();
602
603 return true;
604}
605
607//
609
611 UPnpCDSExtensionResults* pResults,
612 const IDTokenMap& tokens)
613{
614 QString sRequestId = pRequest->m_sObjectId;
615
616 uint16_t nCount = pRequest->m_nRequestedCount;
617 uint16_t nOffset = pRequest->m_nStartingIndex;
618
619 // We must use a dedicated connection to get an accurate value from
620 // FOUND_ROWS()
622
623 QString sql = "SELECT SQL_CALC_FOUND_ROWS "
624 "r.starttime, COUNT(r.recordedid) "
625 "FROM recorded r "
626 "LEFT JOIN recgroups g ON g.recgroup=r.recgroup "
627 "%1 " // WHERE clauses
628 "GROUP BY DATE(CONVERT_TZ(r.starttime, 'UTC', 'SYSTEM')) "
629 "ORDER BY r.starttime DESC "
630 "LIMIT :OFFSET,:COUNT";
631
632 QStringList clauses;
633 QString whereString = BuildWhereClause(clauses, tokens);
634 query.prepare(sql.arg(whereString));
635 BindValues(query, tokens);
636
637 query.bindValue(":OFFSET", nOffset);
638 query.bindValue(":COUNT", nCount);
639
640 if (!query.exec())
641 return false;
642
643 while (query.next())
644 {
645 QDate dtDate = query.value(0).toDate();
646 int nRecCount = query.value(1).toInt();
647
648 // TODO Album or plain old container?
649 CDSObject* pContainer = CDSObject::CreateContainer( CreateIDString(sRequestId, "Date", dtDate.toString(Qt::ISODate)),
651 pRequest->m_sParentId,
652 nullptr );
653 pContainer->SetChildCount(nRecCount);
654 pContainer->SetChildContainerCount(nRecCount);
655
656 pResults->Add(pContainer);
657 pContainer->DecrRef();
658 }
659
660 // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
661 // at least the size of this result set
662 if (query.size() > 0)
663 pResults->m_nTotalMatches = query.size();
664
665 // Fetch the total number of matches ignoring any LIMITs
666 query.prepare("SELECT FOUND_ROWS()");
667 if (query.exec() && query.next())
668 pResults->m_nTotalMatches = query.value(0).toUInt();
669
670 return true;
671}
672
674//
676
678 UPnpCDSExtensionResults* pResults,
679 const IDTokenMap& tokens)
680{
681 QString sRequestId = pRequest->m_sObjectId;
682
683 uint16_t nCount = pRequest->m_nRequestedCount;
684 uint16_t nOffset = pRequest->m_nStartingIndex;
685
686 // We must use a dedicated connection to get an accurate value from
687 // FOUND_ROWS()
689
690 QString sql = "SELECT SQL_CALC_FOUND_ROWS "
691 "r.category, COUNT(r.recordedid) "
692 "FROM recorded r "
693 "LEFT JOIN recgroups g ON g.recgroup=r.recgroup "
694 "%1 " // WHERE clauses
695 "GROUP BY r.category "
696 "ORDER BY r.category "
697 "LIMIT :OFFSET,:COUNT";
698
699 QStringList clauses;
700 QString whereString = BuildWhereClause(clauses, tokens);
701 query.prepare(sql.arg(whereString));
702 BindValues(query, tokens);
703
704 query.bindValue(":OFFSET", nOffset);
705 query.bindValue(":COUNT", nCount);
706
707 if (!query.exec())
708 return false;
709
710 while (query.next())
711 {
712 QString sGenre = query.value(0).toString();
713 int nRecCount = query.value(1).toInt();
714
715 // Handle empty genre strings
716 QString sDisplayGenre = sGenre.isEmpty() ? QObject::tr("No Genre") : sGenre;
717 sGenre = sGenre.isEmpty() ? "MYTH_NO_GENRE" : sGenre;
718
719 // TODO Album or plain old container?
720 CDSObject* pContainer = CDSObject::CreateMovieGenre( CreateIDString(sRequestId, "Genre", sGenre),
721 sDisplayGenre,
722 pRequest->m_sParentId,
723 nullptr );
724 pContainer->SetChildCount(nRecCount);
725 pContainer->SetChildContainerCount(nRecCount);
726
727 pResults->Add(pContainer);
728 pContainer->DecrRef();
729 }
730
731 // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
732 // at least the size of this result set
733 if (query.size() > 0)
734 pResults->m_nTotalMatches = query.size();
735
736 // Fetch the total number of matches ignoring any LIMITs
737 query.prepare("SELECT FOUND_ROWS()");
738 if (query.exec() && query.next())
739 pResults->m_nTotalMatches = query.value(0).toUInt();
740
741 return true;
742}
743
745//
747
749 UPnpCDSExtensionResults* pResults,
750 const IDTokenMap& tokens)
751{
752 QString sRequestId = pRequest->m_sObjectId;
753
754 uint16_t nCount = pRequest->m_nRequestedCount;
755 uint16_t nOffset = pRequest->m_nStartingIndex;
756
757 // We must use a dedicated connection to get an accurate value from
758 // FOUND_ROWS()
760
761 QString sql = "SELECT SQL_CALC_FOUND_ROWS "
762 "r.recgroupid, g.displayname, g.recgroup, COUNT(r.recordedid) "
763 "FROM recorded r "
764 "LEFT JOIN recgroups g ON g.recgroup=r.recgroup " // Use recgroupid in future
765 "%1 " // WHERE clauses
766 "GROUP BY r.recgroup "
767 "ORDER BY g.displayname "
768 "LIMIT :OFFSET,:COUNT";
769
770 QStringList clauses;
771 QString whereString = BuildWhereClause(clauses, tokens);
772
773 query.prepare(sql.arg(whereString));
774
775 BindValues(query, tokens);
776
777 query.bindValue(":OFFSET", nOffset);
778 query.bindValue(":COUNT", nCount);
779
780 if (!query.exec())
781 return false;
782
783 while (query.next())
784 {
785 // Use the string for now until recgroupid support is complete
786// int nRecGroupID = query.value(0).toInt();
787 QString sDisplayName = query.value(1).toString();
788 QString sName = query.value(2).toString();
789 int nRecCount = query.value(3).toInt();
790
791 // TODO Album or plain old container?
792 CDSObject* pContainer = CDSObject::CreateContainer( CreateIDString(sRequestId, "RecGroup", sName),
793 sDisplayName.isEmpty() ? sName : sDisplayName,
794 pRequest->m_sParentId,
795 nullptr );
796 pContainer->SetChildCount(nRecCount);
797 pContainer->SetChildContainerCount(nRecCount);
798
799 pResults->Add(pContainer);
800 pContainer->DecrRef();
801 }
802
803 // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
804 // at least the size of this result set
805 if (query.size() > 0)
806 pResults->m_nTotalMatches = query.size();
807
808 // Fetch the total number of matches ignoring any LIMITs
809 query.prepare("SELECT FOUND_ROWS()");
810 if (query.exec() && query.next())
811 pResults->m_nTotalMatches = query.value(0).toUInt();
812
813 return true;
814}
815
817//
819
821 UPnpCDSExtensionResults* pResults,
822 const IDTokenMap& tokens)
823{
824 QString sRequestId = pRequest->m_sObjectId;
825
826 uint16_t nCount = pRequest->m_nRequestedCount;
827 uint16_t nOffset = pRequest->m_nStartingIndex;
828
829 // We must use a dedicated connection to get an accurate value from
830 // FOUND_ROWS()
832
833 QString sql = "SELECT SQL_CALC_FOUND_ROWS "
834 "r.chanid, c.channum, c.name, COUNT(r.recordedid) "
835 "FROM recorded r "
836 "JOIN channel c ON c.chanid=r.chanid "
837 "LEFT JOIN recgroups g ON g.recgroup=r.recgroup " // Use recgroupid in future
838 "%1 " // WHERE clauses
839 "GROUP BY c.channum "
840 "ORDER BY LPAD(CAST(c.channum AS UNSIGNED), 10, 0), " // Natural sorting including subchannels e.g. 2_4, 1.3
841 " LPAD(c.channum, 10, 0)"
842 "LIMIT :OFFSET,:COUNT";
843
844 QStringList clauses;
845 QString whereString = BuildWhereClause(clauses, tokens);
846
847 query.prepare(sql.arg(whereString));
848
849 BindValues(query, tokens);
850
851 query.bindValue(":OFFSET", nOffset);
852 query.bindValue(":COUNT", nCount);
853
854 if (!query.exec())
855 return false;
856
857 while (query.next())
858 {
859 int nChanID = query.value(0).toInt();
860 QString sChanNum = query.value(1).toString();
861 QString sName = query.value(2).toString();
862 int nRecCount = query.value(3).toInt();
863
864 QString sFullName = QString("%1 %2").arg(sChanNum, sName);
865
866 // TODO Album or plain old container?
867 CDSObject* pContainer = CDSObject::CreateContainer( CreateIDString(sRequestId, "Channel", nChanID),
868 sFullName,
869 pRequest->m_sParentId,
870 nullptr );
871 pContainer->SetChildCount(nRecCount);
872 pContainer->SetChildContainerCount(nRecCount);
873
874 pResults->Add(pContainer);
875 pContainer->DecrRef();
876 }
877
878 // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
879 // at least the size of this result set
880 if (query.size() > 0)
881 pResults->m_nTotalMatches = query.size();
882
883 // Fetch the total number of matches ignoring any LIMITs
884 query.prepare("SELECT FOUND_ROWS()");
885 if (query.exec() && query.next())
886 pResults->m_nTotalMatches = query.value(0).toUInt();
887
888 return true;
889}
890
892//
894
896 UPnpCDSExtensionResults* pResults,
897 IDTokenMap tokens)
898{
899 tokens["category_type"] = "movie";
900 return LoadRecordings(pRequest, pResults, tokens);
901}
902
904//
906
907// TODO Implement this
908// bool UPnpCDSTv::LoadSeasons(const UPnpCDSRequest* pRequest,
909// UPnpCDSExtensionResults* pResults,
910// IDTokenMap tokens)
911// {
912//
913// return false;
914// }
915
917//
919
920// TODO Implement this
921// bool UPnpCDSTv::LoadEpisodes(const UPnpCDSRequest* pRequest,
922// UPnpCDSExtensionResults* pResults,
923// IDTokenMap tokens)
924// {
925// return false;
926// }
927
929//
931
933 UPnpCDSExtensionResults* pResults,
934 IDTokenMap tokens)
935{
936 QString sRequestId = pRequest->m_sObjectId;
937
938 uint16_t nCount = pRequest->m_nRequestedCount;
939 uint16_t nOffset = pRequest->m_nStartingIndex;
940
941 // HACK this is a bit of a hack for loading Recordings in the Title view
942 // where the count/start index from the request aren't applicable
943 if (tokens["recording"].toInt() > 0)
944 {
945 nCount = 1;
946 nOffset = 0;
947 }
948
949 // We must use a dedicated connection to get an accurate value from
950 // FOUND_ROWS()
952
953 QString sql = "SELECT SQL_CALC_FOUND_ROWS "
954 "r.chanid, r.starttime, r.endtime, r.title, "
955 "r.subtitle, r.description, r.category, "
956 "r.hostname, r.recgroup, r.filesize, "
957 "r.basename, r.progstart, r.progend, "
958 "r.storagegroup, r.inetref, "
959 "p.category_type, c.callsign, c.channum, "
960 "p.episode, p.totalepisodes, p.season, "
961 "r.programid, r.seriesid, r.recordid, "
962 "c.default_authority, c.name, "
963 "r.recordedid, r.transcoded, p.videoprop+0, p.audioprop+0, "
964 "f.video_codec, f.audio_codec, f.fps, f.width, f.height, "
965 "f.container "
966 "FROM recorded r "
967 "LEFT JOIN channel c ON r.chanid=c.chanid "
968 "LEFT JOIN recordedprogram p ON p.chanid=r.chanid "
969 " AND p.starttime=r.progstart "
970 "LEFT JOIN recgroups g ON r.recgroup=g.recgroup "
971 "LEFT JOIN recordedfile f ON r.recordedid=f.recordedid "
972 "%1 " // WHERE clauses
973 "%2 " // ORDER BY
974 "LIMIT :OFFSET,:COUNT";
975
976
977 QString orderByString = "ORDER BY r.starttime DESC, r.title";
978
979 if (!tokens["title"].isEmpty())
980 orderByString = "ORDER BY p.season, p.episode, r.starttime ASC"; // In season/episode order, falling back to recorded order
981
982 QStringList clauses;
983 QString whereString = BuildWhereClause(clauses, tokens);
984
985 query.prepare(sql.arg(whereString, orderByString));
986
987 BindValues(query, tokens);
988
989 query.bindValue(":OFFSET", nOffset);
990 query.bindValue(":COUNT", nCount);
991
992 if (!query.exec())
993 return false;
994
995 while (query.next())
996 {
997 int nChanid = query.value( 0).toInt();
998 QDateTime dtStartTime = MythDate::as_utc(query.value(1).toDateTime());
999 QDateTime dtEndTime = MythDate::as_utc(query.value(2).toDateTime());
1000 QString sTitle = query.value( 3).toString();
1001 QString sSubtitle = query.value( 4).toString();
1002 QString sDescription = query.value( 5).toString();
1003 QString sCategory = query.value( 6).toString();
1004 QString sHostName = query.value( 7).toString();
1005// QString sRecGroup = query.value( 8).toString();
1006 uint64_t nFileSize = query.value( 9).toULongLong();
1007 QString sBaseName = query.value(10).toString();
1008
1009 QDateTime dtProgStart =
1010 MythDate::as_utc(query.value(11).toDateTime());
1011 QDateTime dtProgEnd =
1012 MythDate::as_utc(query.value(12).toDateTime());
1013 QString sStorageGrp = query.value(13).toString();
1014
1015 QString sInetRef = query.value(14).toString();
1016 QString sCatType = query.value(15).toString();
1017 QString sCallsign = query.value(16).toString();
1018 QString sChanNum = query.value(17).toString();
1019
1020 int nEpisode = query.value(18).toInt();
1021 int nEpisodeTotal = query.value(19).toInt();
1022 int nSeason = query.value(20).toInt();
1023
1024 QString sProgramId = query.value(21).toString();
1025 QString sSeriesId = query.value(22).toString();
1026 int nRecordId = query.value(23).toInt();
1027
1028 QString sDefaultAuthority = query.value(24).toString();
1029 QString sChanName = query.value(25).toString();
1030
1031 int nRecordedId = query.value(26).toInt();
1032
1033 bool bTranscoded = query.value(27).toBool();
1034 int nVideoProps = query.value(28).toInt();
1035 //int nAudioProps = query.value(29).toInt();
1036
1037 QString sVideoCodec = query.value(30).toString();
1038 QString sAudioCodec = query.value(31).toString();
1039 double dVideoFrameRate = query.value(32).toDouble();
1040 int nVideoWidth = query.value(33).toInt();
1041 int nVideoHeight = query.value(34).toInt();
1042 QString sContainer = query.value(35).toString();
1043
1044 // ----------------------------------------------------------------------
1045 // Cache Host ip Address & Port
1046 // ----------------------------------------------------------------------
1047
1048 if (!m_mapBackendIp.contains( sHostName ))
1049 m_mapBackendIp[ sHostName ] = gCoreContext->GetBackendServerIP(sHostName);
1050
1051 if (!m_mapBackendPort.contains( sHostName ))
1052 m_mapBackendPort[ sHostName ] = gCoreContext->GetBackendStatusPort(sHostName);
1053
1054 // ----------------------------------------------------------------------
1055 // Build Support Strings
1056 // ----------------------------------------------------------------------
1057
1058 QUrl URIBase;
1059 URIBase.setScheme("http");
1060 URIBase.setHost(m_mapBackendIp[sHostName]);
1061 URIBase.setPort(m_mapBackendPort[sHostName]);
1062
1063 CDSObject *pItem = CDSObject::CreateVideoItem( CreateIDString(sRequestId, "Recording", nRecordedId),
1064 sTitle,
1065 pRequest->m_sParentId );
1066
1067 // Only add the reference ID for items which are not in the
1068 // 'All Recordings' container
1069 QString sRefIDBase = QString("%1/Recording").arg(m_sExtensionId);
1070 if ( pRequest->m_sParentId != sRefIDBase )
1071 {
1072 QString sRefId = QString( "%1=%2")
1073 .arg( sRefIDBase )
1074 .arg( nRecordedId );
1075
1076 pItem->SetPropValue( "refID", sRefId );
1077 }
1078
1079 pItem->SetPropValue( "genre", sCategory );
1080
1081 // NOTE There is no max-length on description, no requirement in either UPnP
1082 // or DLNA that the description be a certain size, only that it's 'brief'
1083 //
1084 // The specs only say that the optional longDescription is for longer
1085 // descriptions. Given that clients could easily truncate the description
1086 // themselves this is all very vague.
1087 //
1088 // It's not really correct to stick the subtitle in the description
1089 // field given the existence of the programTitle field. Yet that's what
1090 // we've and what some people have come to expect. There's no easy answer
1091 // but there are wrong answers and whatever we decide, we shouldn't pander
1092 // to devices which don't follow the specs.
1093
1094 if (!sSubtitle.isEmpty())
1095 pItem->SetPropValue( "description" , sSubtitle );
1096 else
1097 pItem->SetPropValue( "description", sDescription.left(128).append(" ..."));
1098 pItem->SetPropValue( "longDescription", sDescription );
1099
1100 pItem->SetPropValue( "channelName" , sChanName );
1101 // TODO Need to detect/switch between DIGITAL/ANALOG
1102 pItem->SetPropValue( "channelID" , sChanNum, "DIGITAL");
1103 pItem->SetPropValue( "callSign" , sCallsign );
1104 // NOTE channelNr must only be used when a DIGITAL or ANALOG channelID is
1105 // given and it MUST be an integer i.e. 2_1 or 2.1 are illegal
1106 int nChanNum = sChanNum.toInt();
1107 if (nChanNum > 0)
1108 pItem->SetPropValue( "channelNr" , QString::number(nChanNum) );
1109
1110 if (sCatType != "movie")
1111 {
1112 pItem->SetPropValue( "seriesTitle" , sTitle);
1113 pItem->SetPropValue( "programTitle" , sSubtitle);
1114 }
1115 else
1116 {
1117 pItem->SetPropValue( "programTitle" , sTitle);
1118 }
1119
1120 if ( nEpisode > 0 || nSeason > 0 ) // There has got to be a better way
1121 {
1122 pItem->SetPropValue( "episodeNumber" , QString::number(nEpisode));
1123 pItem->SetPropValue( "episodeCount" , QString::number(nEpisodeTotal));
1124 }
1125
1126 pItem->SetPropValue( "scheduledStartTime" , UPnPDateTime::DateTimeFormat(dtProgStart));
1127 pItem->SetPropValue( "scheduledEndTime" , UPnPDateTime::DateTimeFormat(dtProgEnd));
1128 auto msecs = std::chrono::milliseconds(dtProgEnd.toMSecsSinceEpoch() - dtProgStart.toMSecsSinceEpoch());
1129 pItem->SetPropValue( "scheduledDuration" , UPnPDateTime::DurationFormat(msecs));
1130 pItem->SetPropValue( "recordedStartDateTime", UPnPDateTime::DateTimeFormat(dtStartTime));
1131 pItem->SetPropValue( "recordedDayOfWeek" , UPnPDateTime::NamedDayFormat(dtStartTime));
1132 pItem->SetPropValue( "srsRecordScheduleID" , QString::number(nRecordId));
1133
1134 if (!sSeriesId.isEmpty())
1135 {
1136 // FIXME: This should be set correctly for EIT data to SI_SERIESID and
1137 // for known sources such as TMS to the correct identifier
1138 QString sIdType = "mythtv.org_XMLTV";
1139 if (sSeriesId.contains(sDefaultAuthority))
1140 sIdType = "mythtv.org_EIT";
1141
1142 pItem->SetPropValue( "seriesID", sSeriesId, sIdType );
1143 }
1144
1145 if (!sProgramId.isEmpty())
1146 {
1147 // FIXME: This should be set correctly for EIT data to SI_PROGRAMID and
1148 // for known sources such as TMS to the correct identifier
1149 QString sIdType = "mythtv.org_XMLTV";
1150 if (sProgramId.contains(sDefaultAuthority))
1151 sIdType = "mythtv.org_EIT";
1152
1153 pItem->SetPropValue( "programID", sProgramId, sIdType );
1154 }
1155
1156 pItem->SetPropValue( "date" , UPnPDateTime::DateTimeFormat(dtStartTime));
1157 pItem->SetPropValue( "creator" , "MythTV" );
1158
1159 // Bookmark support
1160 //pItem->SetPropValue( "lastPlaybackPosition", QString::number());
1161
1162 //pItem->SetPropValue( "producer" , );
1163 //pItem->SetPropValue( "rating" , );
1164 //pItem->SetPropValue( "actor" , );
1165 //pItem->SetPropValue( "director" , );
1166
1167 // ----------------------------------------------------------------------
1168 // Add Video Resource Element based on File contents/extension (HTTP)
1169 // ----------------------------------------------------------------------
1170
1171 StorageGroup sg(sStorageGrp, sHostName);
1172 QString sFilePath = sg.FindFile(sBaseName);
1173 QString sMimeType;
1174
1175 if ( QFile::exists(sFilePath) )
1176 sMimeType = HTTPRequest::TestMimeType( sFilePath );
1177 else
1178 sMimeType = HTTPRequest::TestMimeType( sBaseName );
1179
1180
1181 // If we are dealing with Window Media Player 12 (i.e. Windows 7)
1182 // then fake the Mime type to place the recorded TV in the
1183 // recorded TV section.
1184// if (pRequest->m_eClient == CDS_ClientWMP &&
1185// pRequest->m_nClientVersion >= 12.0)
1186// {
1187// sMimeType = "video/x-ms-dvr";
1188// }
1189
1190 // HACK: If we are dealing with a Sony Blu-ray player then we fake the
1191 // MIME type to force the video to appear
1192// if ( pRequest->m_eClient == CDS_ClientSonyDB )
1193// sMimeType = "video/avi";
1194
1195 std::chrono::milliseconds nDurationMS { 0ms };
1196
1197 // NOTE We intentionally don't use the chanid, recstarttime constructor
1198 // to avoid an unnecessary db query. At least until the time that we're
1199 // creating a RI object throughout
1200 RecordingInfo recInfo = RecordingInfo();
1201 recInfo.SetChanID(nChanid);
1202 recInfo.SetRecordingStartTime(dtStartTime);
1203 // The actual duration may not match the scheduled duration
1204 nDurationMS = recInfo.QueryTotalDuration();
1205 // Older recordings won't have their precise duration stored in
1206 // recordedmarkup
1207 if (nDurationMS == 0ms)
1208 {
1209 auto uiStart = std::chrono::milliseconds(dtStartTime.toMSecsSinceEpoch());
1210 auto uiEnd = std::chrono::milliseconds(dtEndTime.toMSecsSinceEpoch());
1211 nDurationMS = (uiEnd - uiStart);
1212 nDurationMS = std::max(0ms, nDurationMS);
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
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 > 0ms)
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
1324void 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
1442QString 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
static CDSObject * CreateContainer(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
uint32_t GetChildCount(void) const
Return the number of children in this container.
void SetChildCount(uint32_t nCount)
Allows the caller to set childCount without having to load children.
static CDSObject * CreateVideoItem(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
QString m_sClass
Resource * AddResource(const QString &sProtocol, const QString &sURI)
CDSObject * AddChild(CDSObject *pChild)
static CDSObject * CreateAlbum(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
static CDSObject * CreateMovieGenre(const QString &sId, const QString &sTitle, const QString &sParentId, CDSObject *pObject=nullptr)
CDSObject * GetChild(const QString &sID)
void SetPropValue(const QString &sName, const QString &sValue, const QString &type="")
QList< Property * > GetProperties(const QString &sName)
void SetChildContainerCount(uint32_t nCount)
Allows the caller to set childContainerCount without having to load children.
static QString TestMimeType(const QString &sFileName)
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:837
QVariant value(int i) const
Definition: mythdbcon.h:204
int size(void) const
Definition: mythdbcon.h:214
@ kDedicatedConnection
Definition: mythdbcon.h:228
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:618
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:888
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:812
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:550
int GetBackendStatusPort(void)
Returns the locally defined backend status port.
QString GetBackendServerIP(void)
Returns the IP address of the locally defined backend IP.
void SetRecordingStartTime(const QDateTime &dt)
Definition: programinfo.h:530
std::chrono::milliseconds QueryTotalDuration(void) const
If present this loads the total duration in milliseconds of the main video stream from recordedmarkup...
void SetChanID(uint _chanid)
Definition: programinfo.h:527
void AddAttribute(const QString &sName, const QString &sValue)
void SetValue(const QString &value)
Holds information on a TV Program one might wish to record.
Definition: recordinginfo.h:36
static QString GetRecgroupString(uint recGroupID)
Temporary helper during transition from string to ID.
virtual int DecrRef(void)
Decrements reference count and deletes on 0.
void AddAttribute(const QString &sName, const QString &sValue)
QString FindFile(const QString &filename)
void Add(CDSObject *pObject)
Definition: upnpcds.cpp:32
uint16_t m_nTotalMatches
Definition: upnpcds.h:114
QString m_sExtensionId
Definition: upnpcds.h:207
virtual CDSObject * GetRoot()
Definition: upnpcds.cpp:1086
QString m_sName
Definition: upnpcds.h:208
static QString CreateIDString(const QString &RequestId, const QString &Name, int Value)
Definition: upnpcds.cpp:1053
CDSObject * m_pRoot
Definition: upnpcds.h:249
virtual bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:805
virtual bool IsSearchRequestForUs(UPnpCDSRequest *pRequest)
Definition: upnpcds.cpp:892
CDSShortCutList m_shortcuts
Definition: upnpcds.h:211
uint16_t m_nRequestedCount
Definition: upnpcds.h:81
QString m_sObjectId
Definition: upnpcds.h:76
uint16_t m_nStartingIndex
Definition: upnpcds.h:80
QString m_sParentId
Definition: upnpcds.h:86
bool IsSearchRequestForUs(UPnpCDSRequest *pRequest) override
Definition: upnpcdstv.cpp:462
static bool LoadRecGroups(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:748
bool LoadChildren(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens, const QString &currentToken) override
Fetch the children of the container identified in the request.
Definition: upnpcdstv.cpp:356
bool LoadMetadata(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens, const QString &currentToken) override
Fetch just the metadata for the item identified in the request.
Definition: upnpcdstv.cpp:282
static void BindValues(MSqlQuery &query, IDTokenMap tokens)
Definition: upnpcdstv.cpp:1492
void CreateRoot() override
Definition: upnpcdstv.cpp:150
static bool LoadDates(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:610
bool LoadTitles(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:523
QUrl m_uriBase
Definition: upnpcdstv.h:81
static bool LoadGenres(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:677
static bool LoadChannels(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
Definition: upnpcdstv.cpp:820
QStringMap m_mapBackendIp
Definition: upnpcdstv.h:83
static QString BuildWhereClause(QStringList clauses, IDTokenMap tokens)
Definition: upnpcdstv.cpp:1442
QMap< QString, int > m_mapBackendPort
Definition: upnpcdstv.h:84
bool LoadRecordings(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, IDTokenMap tokens)
Definition: upnpcdstv.cpp:932
static void PopulateArtworkURIS(CDSObject *pItem, const QString &sInetRef, int nSeason, const QUrl &URIBase)
Definition: upnpcdstv.cpp:1324
bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest) override
Definition: upnpcdstv.cpp:416
bool LoadMovies(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, IDTokenMap tokens)
Definition: upnpcdstv.cpp:895
unsigned short uint16_t
Definition: iso6937tables.h:3
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
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.
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:28
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:93
@ kSimplify
Do Today/Yesterday/Tomorrow transform.
Definition: mythdate.h:26
@ kDateFull
Default local time.
Definition: mythdate.h:19
@ ISODate
Default UTC.
Definition: mythdate.h:17
@ kAutoYear
Add year only if different from current year.
Definition: mythdate.h:28
QString resDurationFormat(std::chrono::milliseconds msec)
res@duration Format B.2.1.4 res@duration - UPnP ContentDirectory Service 2008, 2013
Definition: upnphelpers.cpp:81
QString NamedDayFormat(const QDateTime &dateTime)
Named-Day Format.
Definition: upnphelpers.cpp:49
QString DurationFormat(std::chrono::milliseconds msec)
Duration Format.
Definition: upnphelpers.cpp:10
QString DateTimeFormat(const QDateTime &dateTime)
Date-Time Format.
Definition: upnphelpers.cpp:43
bool exists(str path)
Definition: xbmcvfs.py:51
QMap< QString, QString > IDTokenMap
Definition: upnpcds.h:200