MythTV master
upnpcdsvideo.cpp
Go to the documentation of this file.
1// Program Name: upnpcdsvideo.cpp
2//
3// Purpose - UPnP Content Directory Extension for MythVideo Videos
4//
6
7// C++ headers
8#include <climits>
9
10// Qt headers
11#include <QFileInfo>
12#include <QUrl>
13#include <QUrlQuery>
14
15// MythTV headers
22
23// MythBackend
24#include "upnpcdsvideo.h"
25
26#define LOC QString("UPnpCDSVideo: ")
27#define LOC_WARN QString("UPnpCDSVideo, Warning: ")
28#define LOC_ERR QString("UPnpCDSVideo, Error: ")
29
31 : UPnpCDSExtension( QObject::tr("Videos"), "Videos",
32 "object.item.videoItem" )
33{
34 QString sServerIp = gCoreContext->GetBackendServerIP();
36 m_uriBase.setScheme("http");
37 m_uriBase.setHost(sServerIp);
38 m_uriBase.setPort(sPort);
39
40 // ShortCuts
42 m_shortcuts.insert(UPnPShortcutFeature::VIDEOS_ALL, "Videos/Video");
43 m_shortcuts.insert(UPnPShortcutFeature::VIDEOS_GENRES, "Videos/Genre");
44}
45
47{
48 if (m_pRoot)
49 return;
50
52 m_sName,
53 "0");
54
55 QString containerId = m_sExtensionId + "/%1";
56
57 // HACK: I'm not entirely happy with this solution, but it's at least
58 // tidier than passing through half a dozen extra args to Load[Foo]
59 // or having yet more methods just to load the counts
60 auto *pRequest = new UPnpCDSRequest();
61 pRequest->m_nRequestedCount = 0; // We don't want to load any results, we just want the TotalCount
62 auto *pResult = new UPnpCDSExtensionResults();
63 IDTokenMap tokens;
64 // END HACK
65
66 // -----------------------------------------------------------------------
67 // All Videos
68 // -----------------------------------------------------------------------
69 CDSObject* pContainer = CDSObject::CreateContainer ( containerId.arg("Video"),
70 QObject::tr("All Videos"),
71 m_sExtensionId, // Parent Id
72 nullptr );
73 // HACK
74 LoadVideos(pRequest, pResult, tokens);
75 pContainer->SetChildCount(pResult->m_nTotalMatches);
76 pContainer->SetChildContainerCount(0);
77 // END HACK
78 m_pRoot->AddChild(pContainer);
79
80 // -----------------------------------------------------------------------
81 // Films
82 // -----------------------------------------------------------------------
83 pContainer = CDSObject::CreateContainer ( containerId.arg("Movie"),
84 QObject::tr("Movies"),
85 m_sExtensionId, // Parent Id
86 nullptr );
87 // HACK
88 LoadMovies(pRequest, pResult, tokens);
89 pContainer->SetChildCount(pResult->m_nTotalMatches);
90 pContainer->SetChildContainerCount(0);
91 // END HACK
92 m_pRoot->AddChild(pContainer);
93
94 // -----------------------------------------------------------------------
95 // Series
96 // -----------------------------------------------------------------------
97 pContainer = CDSObject::CreateContainer ( containerId.arg("Series"),
98 QObject::tr("Series"),
99 m_sExtensionId, // Parent Id
100 nullptr );
101 // HACK
102 LoadSeries(pRequest, pResult, tokens);
103 pContainer->SetChildCount(pResult->m_nTotalMatches);
104 pContainer->SetChildContainerCount(0);
105 // END HACK
106 m_pRoot->AddChild(pContainer);
107
108 // -----------------------------------------------------------------------
109 // Other (Home videos?)
110 // -----------------------------------------------------------------------
111// pContainer = CDSObject::CreateContainer ( containerId.arg("Other"),
112// QObject::tr("Other"),
113// m_sExtensionId, // Parent Id
114// nullptr );
115// m_pRoot->AddChild(pContainer);
116
117 // -----------------------------------------------------------------------
118 // Genre
119 // -----------------------------------------------------------------------
120 pContainer = CDSObject::CreateContainer ( containerId.arg("Genre"),
121 QObject::tr("Genre"),
122 m_sExtensionId, // Parent Id
123 nullptr );
124 // HACK
125 LoadGenres(pRequest, pResult, tokens);
126 pContainer->SetChildCount(pResult->m_nTotalMatches);
127 pContainer->SetChildContainerCount(0);
128 // END HACK
129 m_pRoot->AddChild(pContainer);
130
131 // -----------------------------------------------------------------------
132 // By Directory
133 // -----------------------------------------------------------------------
134// pContainer = CDSObject::CreateStorageSystem ( containerId.arg("Directory"),
135// QObject::tr("Directory"),
136// m_sExtensionId, // Parent Id
137// nullptr );
138// m_pRoot->AddChild(pContainer);
139
140 // HACK
141 delete pRequest;
142 delete pResult;
143 // END HACK
144}
145
147//
149
151{
152 // ----------------------------------------------------------------------
153 // See if we need to modify the request for compatibility
154 // ----------------------------------------------------------------------
155
156 // ----------------------------------------------------------------------
157 // Xbox360 compatibility code.
158 // ----------------------------------------------------------------------
159
160// if (pRequest->m_eClient == CDS_ClientXBox &&
161// pRequest->m_sContainerID == "15" &&
162// gCoreContext->GetSetting("UPnP/WMPSource") == "1")
163// {
164// pRequest->m_sObjectId = "Videos/0";
165//
166// LOG(VB_UPNP, LOG_INFO,
167// "UPnpCDSVideo::IsBrowseRequestForUs - Yes ContainerID == 15");
168// return true;
169// }
170//
171// if ((pRequest->m_sObjectId.isEmpty()) &&
172// (!pRequest->m_sContainerID.isEmpty()))
173// pRequest->m_sObjectId = pRequest->m_sContainerID;
174
175 // ----------------------------------------------------------------------
176 // WMP11 compatibility code
177 //
178 // In this mode browsing for "Videos" is forced to either Videos (us)
179 // or RecordedTV (handled by upnpcdstv)
180 //
181 // ----------------------------------------------------------------------
182
183// if (pRequest->m_eClient == CDS_ClientWMP &&
184// pRequest->m_sContainerID == "13" &&
185// pRequest->m_nClientVersion < 12.0 &&
186// gCoreContext->GetSetting("UPnP/WMPSource") == "1")
187// {
188// pRequest->m_sObjectId = "Videos/0";
189//
190// LOG(VB_UPNP, LOG_INFO,
191// "UPnpCDSVideo::IsBrowseRequestForUs - Yes ContainerID == 13");
192// return true;
193// }
194
195 LOG(VB_UPNP, LOG_INFO,
196 "UPnpCDSVideo::IsBrowseRequestForUs - Not sure... Calling base class.");
197
199}
200
202//
204
206{
207 // ----------------------------------------------------------------------
208 // See if we need to modify the request for compatibility
209 // ----------------------------------------------------------------------
210
211 // ----------------------------------------------------------------------
212 // XBox 360 compatibility code
213 // ----------------------------------------------------------------------
214
215
216// if (pRequest->m_eClient == CDS_ClientXBox &&
217// pRequest->m_sContainerID == "15" &&
218// gCoreContext->GetSetting("UPnP/WMPSource") == "1")
219// {
220// pRequest->m_sObjectId = "Videos/0";
221//
222// LOG(VB_UPNP, LOG_INFO, "UPnpCDSVideo::IsSearchRequestForUs... Yes.");
223//
224// return true;
225// }
226//
227// if ((pRequest->m_sObjectId.isEmpty()) &&
228// (!pRequest->m_sContainerID.isEmpty()))
229// pRequest->m_sObjectId = pRequest->m_sContainerID;
230
231 // ----------------------------------------------------------------------
232
233 bool bOurs = UPnpCDSExtension::IsSearchRequestForUs( pRequest );
234
235 // ----------------------------------------------------------------------
236 // WMP11 compatibility code
237 // ----------------------------------------------------------------------
238
239// if ( bOurs && pRequest->m_eClient == CDS_ClientWMP &&
240// pRequest->m_nClientVersion < 12.0 )
241// {
242// if ( gCoreContext->GetSetting("UPnP/WMPSource") == "1")
243// {
244// pRequest->m_sObjectId = "Videos/0";
245// // -=>TODO: Not sure why this was added.
246// pRequest->m_sParentId = "8";
247// }
248// else
249// bOurs = false;
250// }
251
252 return bOurs;
253}
254
256//
258
260 UPnpCDSExtensionResults* pResults,
261 const IDTokenMap& tokens, const QString& currentToken)
262{
263 if (currentToken.isEmpty())
264 {
265 LOG(VB_GENERAL, LOG_ERR, QString("UPnpCDSTV::LoadMetadata: Final "
266 "token missing from id: %1")
267 .arg(pRequest->m_sParentId));
268 return false;
269 }
270
271 // Root or Root + 1
272 if (tokens[currentToken].isEmpty())
273 {
274 CDSObject *container = nullptr;
275
276 if (pRequest->m_sObjectId == m_sExtensionId)
277 container = GetRoot();
278 else
279 container = GetRoot()->GetChild(pRequest->m_sObjectId);
280
281 if (container)
282 {
283 pResults->Add(container);
284 pResults->m_nTotalMatches = 1;
285 return true;
286 }
287 LOG(VB_GENERAL, LOG_ERR, QString("UPnpCDSTV::LoadMetadata: Requested "
288 "object cannot be found: %1")
289 .arg(pRequest->m_sObjectId));
290 }
291 else if (currentToken == "series")
292 {
293 return LoadSeries(pRequest, pResults, tokens);
294 }
295 else if (currentToken == "season")
296 {
297 return LoadSeasons(pRequest, pResults, tokens);
298 }
299 else if (currentToken == "genre")
300 {
301 return LoadGenres(pRequest, pResults, tokens);
302 }
303 else if (currentToken == "movie")
304 {
305 return LoadMovies(pRequest, pResults, tokens);
306 }
307 else if (currentToken == "video")
308 {
309 return LoadVideos(pRequest, pResults, tokens);
310 }
311 else
312 {
313 LOG(VB_GENERAL, LOG_ERR,
314 QString("UPnpCDSVideo::LoadMetadata(): "
315 "Unhandled metadata request for '%1'.").arg(currentToken));
316 }
317
318 return false;
319}
320
322//
324
326 UPnpCDSExtensionResults* pResults,
327 const IDTokenMap& tokens, const QString& currentToken)
328{
329 if (currentToken.isEmpty() || currentToken == m_sExtensionId.toLower())
330 {
331 // Root
332 pResults->Add(GetRoot()->GetChildren());
333 pResults->m_nTotalMatches = GetRoot()->GetChildCount();
334 return true;
335 }
336 if (currentToken == "series")
337 {
338 if (!tokens["series"].isEmpty())
339 return LoadSeasons(pRequest, pResults, tokens);
340 return LoadSeries(pRequest, pResults, tokens);
341 }
342 if (currentToken == "season")
343 {
344 if (!tokens["season"].isEmpty() && tokens["season"].toInt() >= 0) // Season 0 is valid
345 return LoadVideos(pRequest, pResults, tokens);
346 return LoadSeasons(pRequest, pResults, tokens);
347 }
348 if (currentToken == "genre")
349 {
350 if (!tokens["genre"].isEmpty())
351 return LoadVideos(pRequest, pResults, tokens);
352 return LoadGenres(pRequest, pResults, tokens);
353 }
354 if (currentToken == "movie")
355 {
356 return LoadMovies(pRequest, pResults, tokens);
357 }
358 if (currentToken == "video")
359 {
360 return LoadVideos(pRequest, pResults, tokens);
361 }
362 LOG(VB_GENERAL, LOG_ERR,
363 QString("UPnpCDSVideo::LoadChildren(): "
364 "Unhandled metadata request for '%1'.").arg(currentToken));
365
366 return false;
367}
368
370//
372
374 UPnpCDSExtensionResults* pResults,
375 const IDTokenMap& tokens)
376{
377 QString sRequestId = pRequest->m_sObjectId;
378
379 uint16_t nCount = pRequest->m_nRequestedCount;
380 uint16_t nOffset = pRequest->m_nStartingIndex;
381
382 // We must use a dedicated connection to get an acccurate value from
383 // FOUND_ROWS()
385
386 QString sql = "SELECT SQL_CALC_FOUND_ROWS "
387 "v.title, COUNT(DISTINCT v.season), v.intid "
388 "FROM videometadata v "
389 "%1 " // whereString
390 "GROUP BY v.title "
391 "ORDER BY v.title "
392 "LIMIT :OFFSET,:COUNT ";
393
394 QStringList clauses;
395 clauses.append("contenttype='TELEVISION'");
396 QString whereString = BuildWhereClause(clauses, tokens);
397
398 query.prepare(sql.arg(whereString));
399
400 BindValues(query, tokens);
401
402 query.bindValue(":OFFSET", nOffset);
403 query.bindValue(":COUNT", nCount);
404
405 if (!query.exec())
406 return false;
407
408 while (query.next())
409 {
410 QString sTitle = query.value(0).toString();
411 int nSeasonCount = query.value(1).toInt();
412 int nVidID = query.value(2).toInt();
413
414 // TODO Album or plain old container?
415 CDSObject* pContainer = CDSObject::CreateAlbum( CreateIDString(sRequestId, "Series", sTitle),
416 sTitle,
417 pRequest->m_sParentId,
418 nullptr );
419 pContainer->SetPropValue("description", QObject::tr("%n Seasons", "", nSeasonCount));
420 pContainer->SetPropValue("longdescription", QObject::tr("%n Seasons", "", nSeasonCount));
421 pContainer->SetPropValue("storageMedium", "HDD");
422
423 pContainer->SetChildCount(nSeasonCount);
424 pContainer->SetChildContainerCount(nSeasonCount);
425
426 PopulateArtworkURIS(pContainer, nVidID, m_uriBase);
427
428 pResults->Add(pContainer);
429 pContainer->DecrRef();
430 }
431
432 // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
433 // at least the size of this result set
434 if (query.size() >= 0)
435 pResults->m_nTotalMatches = query.size();
436
437 // Fetch the total number of matches ignoring any LIMITs
438 query.prepare("SELECT FOUND_ROWS()");
439 if (query.exec() && query.next())
440 pResults->m_nTotalMatches = query.value(0).toUInt();
441
442 return true;
443}
444
446//
448
450 UPnpCDSExtensionResults* pResults,
451 const IDTokenMap& tokens)
452{
453 QString sRequestId = pRequest->m_sObjectId;
454
455 uint16_t nCount = pRequest->m_nRequestedCount;
456 uint16_t nOffset = pRequest->m_nStartingIndex;
457
458 // We must use a dedicated connection to get an acccurate value from
459 // FOUND_ROWS()
461
462 QString sql = "SELECT SQL_CALC_FOUND_ROWS "
463 "v.season, COUNT(DISTINCT v.intid), v.intid "
464 "FROM videometadata v "
465 "%1 " // whereString
466 "GROUP BY v.season "
467 "ORDER BY v.season "
468 "LIMIT :OFFSET,:COUNT ";
469
470 QStringList clauses;
471 QString whereString = BuildWhereClause(clauses, tokens);
472
473 query.prepare(sql.arg(whereString));
474
475 BindValues(query, tokens);
476
477 query.bindValue(":OFFSET", nOffset);
478 query.bindValue(":COUNT", nCount);
479
480 if (!query.exec())
481 return false;
482
483 while (query.next())
484 {
485 int nSeason = query.value(0).toInt();
486 int nVideoCount = query.value(1).toInt();
487 int nVidID = query.value(2).toInt();
488
489 QString sTitle = QObject::tr("Season %1").arg(nSeason);
490
491 // TODO Album or plain old container?
492 CDSObject* pContainer = CDSObject::CreateAlbum( CreateIDString(sRequestId, "Season", nSeason),
493 sTitle,
494 pRequest->m_sParentId,
495 nullptr );
496 pContainer->SetPropValue("description", QObject::tr("%n Episode(s)", "", nVideoCount));
497 pContainer->SetPropValue("longdescription", QObject::tr("%n Episode(s)", "", nVideoCount));
498 pContainer->SetPropValue("storageMedium", "HDD");
499
500 pContainer->SetChildCount(nVideoCount);
501 pContainer->SetChildContainerCount(0);
502
503 PopulateArtworkURIS(pContainer, nVidID, m_uriBase);
504
505 pResults->Add(pContainer);
506 pContainer->DecrRef();
507 }
508
509 // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
510 // at least the size of this result set
511 if (query.size() >= 0)
512 pResults->m_nTotalMatches = query.size();
513
514 // Fetch the total number of matches ignoring any LIMITs
515 query.prepare("SELECT FOUND_ROWS()");
516 if (query.exec() && query.next())
517 pResults->m_nTotalMatches = query.value(0).toUInt();
518
519 return true;
520}
521
523//
525
527 UPnpCDSExtensionResults* pResults,
528 IDTokenMap tokens)
529{
530 tokens["type"] = "MOVIE";
531 //LoadGenres(pRequest, pResults, tokens);
532 return LoadVideos(pRequest, pResults, tokens);
533}
534
536//
538
540 UPnpCDSExtensionResults* pResults,
541 const IDTokenMap& tokens)
542{
543 QString sRequestId = pRequest->m_sObjectId;
544
545 uint16_t nCount = pRequest->m_nRequestedCount;
546 uint16_t nOffset = pRequest->m_nStartingIndex;
547
548 // We must use a dedicated connection to get an acccurate value from
549 // FOUND_ROWS()
551
552 QString sql = "SELECT SQL_CALC_FOUND_ROWS "
553 "v.category, g.genre, COUNT(DISTINCT v.intid) "
554 "FROM videometadata v "
555 "LEFT JOIN videogenre g ON g.intid=v.category "
556 "%1 " // whereString
557 "GROUP BY g.intid "
558 "ORDER BY g.genre "
559 "LIMIT :OFFSET,:COUNT ";
560
561 QStringList clauses;
562 clauses.append("v.category != 0");
563 QString whereString = BuildWhereClause(clauses, tokens);
564
565 query.prepare(sql.arg(whereString));
566
567 BindValues(query, tokens);
568
569 query.bindValue(":OFFSET", nOffset);
570 query.bindValue(":COUNT", nCount);
571
572 if (!query.exec())
573 return false;
574
575 while (query.next())
576 {
577 int nGenreID = query.value(0).toInt();
578 QString sName = query.value(1).toString();
579 int nVideoCount = query.value(2).toInt();
580
581 // TODO Album or plain old container?
582 CDSObject* pContainer = CDSObject::CreateMovieGenre( CreateIDString(sRequestId, "Genre", nGenreID),
583 sName,
584 pRequest->m_sParentId,
585 nullptr );
586
587 pContainer->SetChildCount(nVideoCount);
588 pContainer->SetChildContainerCount(0);
589
590 pResults->Add(pContainer);
591 pContainer->DecrRef();
592 }
593
594 // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
595 // at least the size of this result set
596 if (query.size() >= 0)
597 pResults->m_nTotalMatches = query.size();
598
599 // Fetch the total number of matches ignoring any LIMITs
600 query.prepare("SELECT FOUND_ROWS()");
601 if (query.exec() && query.next())
602 pResults->m_nTotalMatches = query.value(0).toUInt();
603
604 return true;
605}
606
608//
610
612 UPnpCDSExtensionResults* pResults,
613 const IDTokenMap& tokens)
614{
615 QString sRequestId = pRequest->m_sObjectId;
616
617 uint16_t nCount = pRequest->m_nRequestedCount;
618 uint16_t nOffset = pRequest->m_nStartingIndex;
619
620 // We must use a dedicated connection to get an acccurate value from
621 // FOUND_ROWS()
623
624 QString sql = "SELECT SQL_CALC_FOUND_ROWS "
625 "v.intid, title, subtitle, filename, director, plot, "
626 "rating, year, userrating, length, "
627 "season, episode, coverfile, insertdate, host, "
628 "g.genre, studio, collectionref, contenttype "
629 "FROM videometadata v "
630 "LEFT JOIN videogenre g ON g.intid=v.category "
631 "%1 " //
632 "ORDER BY title, season, episode "
633 "LIMIT :OFFSET,:COUNT ";
634
635 QStringList clauses;
636 QString whereString = BuildWhereClause(clauses, tokens);
637
638 query.prepare(sql.arg(whereString));
639
640 BindValues(query, tokens);
641
642 query.bindValue(":OFFSET", nOffset);
643 query.bindValue(":COUNT", nCount);
644
645 if (!query.exec())
646 return false;
647
648 while (query.next())
649 {
650
651 int nVidID = query.value( 0).toInt();
652 QString sTitle = query.value( 1).toString();
653 QString sSubtitle = query.value( 2).toString();
654 QString sFilePath = query.value( 3).toString();
655 QString sDirector = query.value( 4).toString();
656 QString sPlot = query.value( 5).toString();
657 // QString sRating = query.value( 6).toString();
658 int nYear = query.value( 7).toInt();
659 // int nUserRating = query.value( 8).toInt();
660
661 auto nLength = std::chrono::minutes(query.value( 9).toUInt());
662
663 int nSeason = query.value(10).toInt();
664 int nEpisode = query.value(11).toInt();
665 QString sCoverArt = query.value(12).toString();
666 QDateTime dtInsertDate =
667 MythDate::as_utc(query.value(13).toDateTime());
668 QString sHostName = query.value(14).toString();
669 QString sGenre = query.value(15).toString();
670 // QString sStudio = query.value(16).toString();
671 // QString sCollectionRef = query.value(17).toString();
672 QString sContentType = query.value(18).toString();
673
674 // ----------------------------------------------------------------------
675 // Cache Host ip Address & Port
676 // ----------------------------------------------------------------------
677
678 // If the host-name is empty then we assume it is our local host
679 // otherwise, we look up the host's IP address and port. When the
680 // client then trys to play the video it will be directed to the
681 // host which actually has the content.
682 if (!m_mapBackendIp.contains( sHostName ))
683 {
684 if (sHostName.isEmpty())
685 {
686 m_mapBackendIp[sHostName] =
688 }
689 else
690 {
691 m_mapBackendIp[sHostName] =
693 }
694 }
695
696 if (!m_mapBackendPort.contains( sHostName ))
697 {
698 if (sHostName.isEmpty())
699 {
700 m_mapBackendPort[sHostName] =
702 }
703 else
704 {
705 m_mapBackendPort[sHostName] =
707 }
708 }
709
710
711 // ----------------------------------------------------------------------
712 // Build Support Strings
713 // ----------------------------------------------------------------------
714
715 QString sName = sTitle;
716 if( !sSubtitle.isEmpty() )
717 {
718 sName += " - " + sSubtitle;
719 }
720
721 QUrl URIBase;
722 URIBase.setScheme("http");
723 URIBase.setHost(m_mapBackendIp[sHostName]);
724 URIBase.setPort(m_mapBackendPort[sHostName]);
725
726 CDSObject *pItem = nullptr;
727 if (sContentType == "MOVIE")
728 {
729 pItem = CDSObject::CreateMovie( CreateIDString(sRequestId, "Video", nVidID),
730 sTitle,
731 pRequest->m_sParentId );
732 }
733 else
734 {
735 pItem = CDSObject::CreateVideoItem( CreateIDString(sRequestId, "Video", nVidID),
736 sName,
737 pRequest->m_sParentId );
738 }
739
740 if (!sSubtitle.isEmpty())
741 pItem->SetPropValue( "description", sSubtitle );
742 else
743 pItem->SetPropValue( "description", sPlot.left(128).append(" ..."));
744 pItem->SetPropValue( "longDescription", sPlot );
745 pItem->SetPropValue( "director" , sDirector );
746
747 if (nEpisode > 0 || nSeason > 0) // There has got to be a better way
748 {
749 pItem->SetPropValue( "seriesTitle" , sTitle );
750 pItem->SetPropValue( "programTitle" , sSubtitle );
751 pItem->SetPropValue( "episodeNumber" , QString::number(nEpisode));
752 //pItem->SetPropValue( "episodeCount" , nEpisodeCount);
753 }
754
755 pItem->SetPropValue( "genre" , sGenre );
756 if (nYear > 1830 && nYear < 9999)
757 pItem->SetPropValue( "date", QDate(nYear,1,1).toString(Qt::ISODate));
758 else
759 pItem->SetPropValue( "date", UPnPDateTime::DateTimeFormat(dtInsertDate) );
760
761 // HACK: Windows Media Centre Compat (Not a UPnP or DLNA requirement, should only be done for WMC)
762// pItem->SetPropValue( "genre" , "[Unknown Genre]" );
763// pItem->SetPropValue( "actor" , "[Unknown Author]" );
764// pItem->SetPropValue( "creator" , "[Unknown Creator]" );
765// pItem->SetPropValue( "album" , "[Unknown Album]" );
767
768 //pItem->SetPropValue( "producer" , );
769 //pItem->SetPropValue( "rating" , );
770 //pItem->SetPropValue( "actor" , );
771 //pItem->SetPropValue( "publisher" , );
772 //pItem->SetPropValue( "language" , );
773 //pItem->SetPropValue( "relation" , );
774 //pItem->SetPropValue( "region" , );
775
776 // Only add the reference ID for items which are not in the
777 // 'All Videos' container
778 QString sRefIDBase = QString("%1/Video").arg(m_sExtensionId);
779 if ( pRequest->m_sParentId != sRefIDBase )
780 {
781 QString sRefId = QString( "%1=%2")
782 .arg( sRefIDBase )
783 .arg( nVidID );
784
785 pItem->SetPropValue( "refID", sRefId );
786 }
787
788 // FIXME - If the slave or storage hosting this video is offline we
789 // won't find it. We probably shouldn't list it, but better
790 // still would be storing the filesize in the database so we
791 // don't waste time re-checking it constantly
792 QString sFullFileName = sFilePath;
793 if (!QFile::exists( sFullFileName ))
794 {
795 StorageGroup sgroup("Videos");
796 sFullFileName = sgroup.FindFile( sFullFileName );
797 }
798 QFileInfo fInfo( sFullFileName );
799
800 // ----------------------------------------------------------------------
801 // Add Video Resource Element based on File extension (HTTP)
802 // ----------------------------------------------------------------------
803
804 QString sMimeType = HTTPRequest::GetMimeType( QFileInfo(sFilePath).suffix() );
805
806 // HACK: If we are dealing with a Sony Blu-ray player then we fake the
807 // MIME type to force the video to appear
808// if ( pRequest->m_eClient == CDS_ClientSonyDB )
809// {
810// sMimeType = "video/avi";
811// }
812
813 QUrl resURI = URIBase;
814 QUrlQuery resQuery;
815 resURI.setPath("/Content/GetVideo");
816 resQuery.addQueryItem("Id", QString::number(nVidID));
817 resURI.setQuery(resQuery);
818
819 // DLNA requires a mimetype of video/mp2p for TS files, it's not the
820 // correct mimetype, but then DLNA doesn't seem to care about such
821 // things
822 if (sMimeType == "video/mp2t" || sMimeType == "video/mp2p")
823 sMimeType = "video/mpeg";
824
826 sMimeType);
827
828 Resource *pRes = pItem->AddResource( sProtocol, resURI.toEncoded() );
829 pRes->AddAttribute( "size" , QString("%1").arg(fInfo.size()) );
830 pRes->AddAttribute( "duration", UPnPDateTime::resDurationFormat(nLength) );
831
832 // ----------------------------------------------------------------------
833 // Add Artwork
834 // ----------------------------------------------------------------------
835 if (!sCoverArt.isEmpty() && (sCoverArt != "No Cover"))
836 {
837 PopulateArtworkURIS(pItem, nVidID, URIBase);
838 }
839
840 pResults->Add( pItem );
841 pItem->DecrRef();
842 }
843
844 // Just in case FOUND_ROWS() should fail, ensure m_nTotalMatches contains
845 // at least the size of this result set
846 if (query.size() >= 0)
847 pResults->m_nTotalMatches = query.size();
848
849 // Fetch the total number of matches ignoring any LIMITs
850 query.prepare("SELECT FOUND_ROWS()");
851 if (query.exec() && query.next())
852 pResults->m_nTotalMatches = query.value(0).toUInt();
853
854 return true;
855}
856
858 const QUrl& URIBase)
859{
860 QUrl artURI = URIBase;
861 artURI.setPath("/Content/GetVideoArtwork");
862 QUrlQuery artQuery;
863 artQuery.addQueryItem("Id", QString::number(nVidID));
864 artURI.setQuery(artQuery);
865
866 // Prefer JPEG over PNG here, although PNG is allowed JPEG probably
867 // has wider device support and crucially the filesizes are smaller
868 // which speeds up loading times over the network
869
870 // We MUST include the thumbnail size, but since some clients may use the
871 // first image they see and the thumbnail is tiny, instead return the
872 // medium first. The large could be very large, which is no good if the
873 // client is pulling images for an entire list at once!
874
875 // Thumbnail
876 // At least one albumArtURI must be a ThumbNail (TN) no larger
877 // than 160x160, and it must also be a jpeg
878 QUrl thumbURI = artURI;
879 QUrlQuery thumbQuery(thumbURI.query());
880 if (pItem->m_sClass == "object.item.videoItem") // Show screenshot for TV, coverart for movies
881 thumbQuery.addQueryItem("Type", "screenshot");
882 else
883 thumbQuery.addQueryItem("Type", "coverart");
884 thumbQuery.addQueryItem("Width", "160");
885 thumbQuery.addQueryItem("Height", "160");
886 thumbURI.setQuery(thumbQuery);
887
888 // Small
889 // Must be no more than 640x480
890 QUrl smallURI = artURI;
891 QUrlQuery smallQuery(smallURI.query());
892 smallQuery.addQueryItem("Type", "coverart");
893 smallQuery.addQueryItem("Width", "640");
894 smallQuery.addQueryItem("Height", "480");
895 smallURI.setQuery(smallQuery);
896
897 // Medium
898 // Must be no more than 1024x768
899 QUrl mediumURI = artURI;
900 QUrlQuery mediumQuery(mediumURI.query());
901 mediumQuery.addQueryItem("Type", "coverart");
902 mediumQuery.addQueryItem("Width", "1024");
903 mediumQuery.addQueryItem("Height", "768");
904 mediumURI.setQuery(mediumQuery);
905
906 // Large
907 // Must be no more than 4096x4096 - for our purposes, just return
908 // a fullsize image
909 QUrl largeURI = artURI;
910 QUrlQuery largeQuery(largeURI.query());
911 largeQuery.addQueryItem("Type", "fanart");
912 largeURI.setQuery(largeQuery);
913
914 QList<Property*> propList = pItem->GetProperties("albumArtURI");
915 if (propList.size() >= 4)
916 {
917 Property *pProp = propList.at(0);
918 if (pProp)
919 {
920 pProp->SetValue(mediumURI.toEncoded());
921 pProp->AddAttribute("dlna:profileID", "JPEG_MED");
922 pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
923 }
924
925 pProp = propList.at(1);
926 if (pProp)
927 {
928
929 pProp->SetValue(thumbURI.toEncoded());
930 pProp->AddAttribute("dlna:profileID", "JPEG_TN");
931 pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
932 }
933
934 pProp = propList.at(2);
935 if (pProp)
936 {
937 pProp->SetValue(smallURI.toEncoded());
938 pProp->AddAttribute("dlna:profileID", "JPEG_SM");
939 pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
940 }
941
942 pProp = propList.at(3);
943 if (pProp)
944 {
945 pProp->SetValue(largeURI.toEncoded());
946 pProp->AddAttribute("dlna:profileID", "JPEG_LRG");
947 pProp->AddAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0");
948 }
949 }
950
951 if (pItem->m_sClass.startsWith("object.item.videoItem"))
952 {
953 QString sProtocol;
954
956 "image/jpeg", QSize(1024, 768));
957 pItem->AddResource( sProtocol, mediumURI.toEncoded());
958
960 "image/jpeg", QSize(160, 160));
961 pItem->AddResource( sProtocol, thumbURI.toEncoded());
962
964 "image/jpeg", QSize(640, 480));
965 pItem->AddResource( sProtocol, smallURI.toEncoded());
966
968 "image/jpeg", QSize(1920, 1080)); // Not the actual res, we don't know that
969 pItem->AddResource( sProtocol, largeURI.toEncoded());
970 }
971}
972
973QString UPnpCDSVideo::BuildWhereClause(QStringList clauses, IDTokenMap tokens)
974{
975 if (tokens["video"].toInt() > 0)
976 clauses.append("v.intid=:VIDEO_ID");
977 if (!tokens["series"].isEmpty())
978 clauses.append("v.title=:TITLE");
979 if (!tokens["season"].isEmpty() && tokens["season"].toInt() >= 0) // Season 0 is valid
980 clauses.append("v.season=:SEASON");
981 if (!tokens["type"].isEmpty())
982 clauses.append("v.contenttype=:TYPE");
983 if (tokens["genre"].toInt() > 0)
984 clauses.append("v.category=:GENRE_ID");
985
986 QString whereString;
987 if (!clauses.isEmpty())
988 {
989 whereString = " WHERE ";
990 whereString.append(clauses.join(" AND "));
991 }
992
993 return whereString;
994}
995
997{
998 if (tokens["video"].toInt() > 0)
999 query.bindValue(":VIDEO_ID", tokens["video"]);
1000 if (!tokens["series"].isEmpty())
1001 query.bindValue(":TITLE", tokens["series"]);
1002 if (!tokens["season"].isEmpty() && tokens["season"].toInt() >= 0) // Season 0 is valid
1003 query.bindValue(":SEASON", tokens["season"]);
1004 if (!tokens["type"].isEmpty())
1005 query.bindValue(":TYPE", tokens["type"]);
1006 if (tokens["genre"].toInt() > 0)
1007 query.bindValue(":GENRE_ID", tokens["genre"]);
1008}
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)
static CDSObject * CreateMovie(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 GetMimeType(const QString &sFileExtension)
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 AddAttribute(const QString &sName, const QString &sValue)
void SetValue(const QString &value)
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
void CreateRoot() override
QMap< QString, int > m_mapBackendPort
Definition: upnpcdsvideo.h:77
bool LoadSeries(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
bool LoadChildren(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens, const QString &currentToken) override
Fetch the children of the container identified in the request.
static bool LoadGenres(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
bool LoadMovies(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, IDTokenMap tokens)
static QString BuildWhereClause(QStringList clauses, IDTokenMap tokens)
bool IsSearchRequestForUs(UPnpCDSRequest *pRequest) override
bool IsBrowseRequestForUs(UPnpCDSRequest *pRequest) override
static void BindValues(MSqlQuery &query, IDTokenMap tokens)
bool LoadSeasons(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
bool LoadVideos(const UPnpCDSRequest *pRequest, UPnpCDSExtensionResults *pResults, const IDTokenMap &tokens)
QStringMap m_mapBackendIp
Definition: upnpcdsvideo.h:76
static void PopulateArtworkURIS(CDSObject *pItem, int nVidID, const QUrl &URIBase)
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.
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
@ ISODate
Default UTC.
Definition: mythdate.h:17
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 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