MythTV  master
guide.cpp
Go to the documentation of this file.
1 // Program Name: guide.cpp
3 // Created : Mar. 7, 2011
4 //
5 // Copyright (c) 2011 David Blain <dblain@mythtv.org>
6 //
7 // This program is free software; you can redistribute it and/or modify
8 // it under the terms of the GNU General Public License as published by
9 // the Free Software Foundation; either version 2 of the License, or
10 // (at your option) any later version.
11 //
12 // This program is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 // GNU General Public License for more details.
16 //
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 //
21 // You should have received a copy of the GNU General Public License
22 // along with this program. If not, see <http://www.gnu.org/licenses/>.
23 //
25 
26 #include <cmath>
27 
28 #include "guide.h"
29 
30 #include "compat.h"
31 #include "mythversion.h"
32 #include "mythcorecontext.h"
33 #include "scheduler.h"
34 #include "autoexpire.h"
35 #include "channelutil.h"
36 #include "channelgroup.h"
37 #include "storagegroup.h"
38 
39 #include "mythlogging.h"
40 
41 extern AutoExpire *expirer;
42 extern Scheduler *sched;
43 
45 //
47 
48 DTC::ProgramGuide *Guide::GetProgramGuide( const QDateTime &rawStartTime,
49  const QDateTime &rawEndTime,
50  bool bDetails,
51  int nChannelGroupId,
52  int nStartIndex,
53  int nCount,
54  bool bWithInvisible)
55 {
56  if (!rawStartTime.isValid())
57  throw QString( "StartTime is invalid" );
58 
59  if (!rawEndTime.isValid())
60  throw QString( "EndTime is invalid" );
61 
62  QDateTime dtStartTime = rawStartTime.toUTC();
63  QDateTime dtEndTime = rawEndTime.toUTC();
64 
65  if (dtEndTime < dtStartTime)
66  throw QString( "EndTime is before StartTime");
67 
68  if (nStartIndex <= 0)
69  nStartIndex = 0;
70 
71  if (nCount <= 0)
72  nCount = 20000;
73 
74  // ----------------------------------------------------------------------
75  // Load the channel list
76  // ----------------------------------------------------------------------
77 
78  uint nTotalAvailable = 0;
79  ChannelInfoList chanList = ChannelUtil::LoadChannels(nStartIndex, nCount,
80  nTotalAvailable,
81  !bWithInvisible,
84  0,
85  nChannelGroupId);
86 
87  // ----------------------------------------------------------------------
88  // Build SQL statement for Program Listing
89  // ----------------------------------------------------------------------
90 
91  ProgramList schedList;
92  MSqlBindings bindings;
93 
94  QString sWhere = "program.chanid = :CHANID "
95  "AND program.endtime >= :STARTDATE "
96  "AND program.starttime < :ENDDATE "
97  "AND program.starttime >= :STARTDATELIMIT "
98  "AND program.manualid = 0"; // Omit 'manual' recordings scheds
99 
100 #if 0
101  QString sGroupBy = "program.starttime, channel.channum,"
102  "channel.callsign, program.title";
103 #endif
104 
105  QString sOrderBy = "program.starttime";
106 
107  bindings[":STARTDATE" ] = dtStartTime;
108  bindings[":STARTDATELIMIT"] = dtStartTime.addDays(-1);
109  bindings[":ENDDATE" ] = dtEndTime;
110 
111  // ----------------------------------------------------------------------
112  // Get all Pending Scheduled Programs
113  // ----------------------------------------------------------------------
114 
115  // NOTE: Fetching this information directly from the schedule is
116  // significantly faster than using ProgramInfo::LoadFromScheduler()
117  auto *scheduler = dynamic_cast<Scheduler*>(gCoreContext->GetScheduler());
118  if (scheduler)
119  scheduler->GetAllPending(schedList);
120 
121  // ----------------------------------------------------------------------
122  // Build Response
123  // ----------------------------------------------------------------------
124 
125  auto *pGuide = new DTC::ProgramGuide();
126 
127  ChannelInfoList::iterator chan_it;
128  for (chan_it = chanList.begin(); chan_it != chanList.end(); ++chan_it)
129  {
130  // Create ChannelInfo Object
131  DTC::ChannelInfo *pChannel = pGuide->AddNewChannel();
132  FillChannelInfo( pChannel, (*chan_it), bDetails );
133 
134  // Load the list of programmes for this channel
135  ProgramList progList;
136  bindings[":CHANID"] = (*chan_it).m_chanid;
137  LoadFromProgram( progList, sWhere, sOrderBy, sOrderBy, bindings,
138  schedList );
139 
140  // Create Program objects and add them to the channel object
141  ProgramList::iterator progIt;
142  for( progIt = progList.begin(); progIt != progList.end(); ++progIt)
143  {
144  DTC::Program *pProgram = pChannel->AddNewProgram();
145  FillProgramInfo( pProgram, *progIt, false, bDetails, false ); // No cast info
146  }
147  }
148 
149  // ----------------------------------------------------------------------
150 
151  pGuide->setStartTime ( dtStartTime );
152  pGuide->setEndTime ( dtEndTime );
153  pGuide->setDetails ( bDetails );
154 
155  pGuide->setStartIndex ( nStartIndex );
156  pGuide->setCount ( chanList.size() );
157  pGuide->setTotalAvailable( nTotalAvailable );
158  pGuide->setAsOf ( MythDate::current() );
159 
160  pGuide->setVersion ( MYTH_BINARY_VERSION );
161  pGuide->setProtoVer ( MYTH_PROTO_VERSION );
162 
163  return pGuide;
164 }
165 
167 //
169 
171  int nCount,
172  const QDateTime& rawStartTime,
173  const QDateTime& rawEndTime,
174  int nChanId,
175  const QString& sTitleFilter,
176  const QString& sCategoryFilter,
177  const QString& sPersonFilter,
178  const QString& sKeywordFilter,
179  bool bOnlyNew,
180  bool bDetails,
181  const QString &sSort,
182  bool bDescending,
183  bool bWithInvisible)
184 {
185  if (!rawStartTime.isNull() && !rawStartTime.isValid())
186  throw QString( "StartTime is invalid" );
187 
188  if (!rawEndTime.isNull() && !rawEndTime.isValid())
189  throw QString( "EndTime is invalid" );
190 
191  QDateTime dtStartTime = rawStartTime;
192  const QDateTime& dtEndTime = rawEndTime;
193 
194  if (!rawEndTime.isNull() && dtEndTime < dtStartTime)
195  throw QString( "EndTime is before StartTime");
196 
197  MSqlQuery query(MSqlQuery::InitCon());
198 
199 
200  // ----------------------------------------------------------------------
201  // Build SQL statement for Program Listing
202  // ----------------------------------------------------------------------
203 
204  ProgramList progList;
205  ProgramList schedList;
206  MSqlBindings bindings;
207 
208  QString sSQL;
209 
210  if (!sPersonFilter.isEmpty())
211  {
212  sSQL = ", people, credits " // LEFT JOIN
213  "WHERE people.name LIKE :PersonFilter "
214  "AND credits.person = people.person "
215  "AND program.chanid = credits.chanid "
216  "AND program.starttime = credits.starttime AND ";
217  bindings[":PersonFilter"] = QString("%%1%").arg(sPersonFilter);
218  }
219  else
220  sSQL = "WHERE ";
221 
222  if (bOnlyNew)
223  sSQL = "LEFT JOIN oldprogram ON oldprogram.oldtitle = program.title "
224  + sSQL
225  + "oldprogram.oldtitle IS NULL AND ";
226 
227  sSQL += "deleted IS NULL AND ";
228 
229  if (!bWithInvisible)
230  sSQL += "visible != 0 AND ";
231 
232  sSQL += "program.manualid = 0 "; // Exclude programmes created purely for 'manual' recording schedules
233 
234  if (nChanId < 0)
235  nChanId = 0;
236 
237  if (nChanId > 0)
238  {
239  sSQL += "AND program.chanid = :ChanId ";
240  bindings[":ChanId"] = nChanId;
241  }
242 
243  if (dtStartTime.isNull())
244  dtStartTime = QDateTime::currentDateTimeUtc();
245 
246  sSQL += " AND program.endtime >= :StartDate ";
247  bindings[":StartDate"] = dtStartTime;
248 
249  if (!dtEndTime.isNull())
250  {
251  sSQL += "AND program.starttime <= :EndDate ";
252  bindings[":EndDate"] = dtEndTime;
253  }
254 
255  if (!sTitleFilter.isEmpty())
256  {
257  sSQL += "AND program.title LIKE :Title ";
258  bindings[":Title"] = QString("%%1%").arg(sTitleFilter);
259  }
260 
261  if (!sCategoryFilter.isEmpty())
262  {
263  sSQL += "AND program.category LIKE :Category ";
264  bindings[":Category"] = sCategoryFilter;
265  }
266 
267  if (!sKeywordFilter.isEmpty())
268  {
269  sSQL += "AND (program.title LIKE :Keyword1 "
270  "OR program.subtitle LIKE :Keyword2 "
271  "OR program.description LIKE :Keyword3) ";
272 
273  QString filter = QString("%%1%").arg(sKeywordFilter);
274  bindings[":Keyword1"] = filter;
275  bindings[":Keyword2"] = filter;
276  bindings[":Keyword3"] = filter;
277  }
278 
279  if (sSort == "starttime")
280  sSQL += "ORDER BY program.starttime "; // NOLINT(bugprone-branch-clone)
281  else if (sSort == "title")
282  sSQL += "ORDER BY program.title ";
283  else if (sSort == "channel")
284  sSQL += "ORDER BY channel.channum ";
285  else if (sSort == "duration")
286  sSQL += "ORDER BY (program.endtime - program.starttime) ";
287  else
288  sSQL += "ORDER BY program.starttime ";
289 
290  if (bDescending)
291  sSQL += "DESC ";
292  else
293  sSQL += "ASC ";
294 
295  // ----------------------------------------------------------------------
296  // Get all Pending Scheduled Programs
297  // ----------------------------------------------------------------------
298 
299  // NOTE: Fetching this information directly from the schedule is
300  // significantly faster than using ProgramInfo::LoadFromScheduler()
301  auto *scheduler = dynamic_cast<Scheduler*>(gCoreContext->GetScheduler());
302  if (scheduler)
303  scheduler->GetAllPending(schedList);
304 
305  // ----------------------------------------------------------------------
306 
307  uint nTotalAvailable = 0;
308  LoadFromProgram( progList, sSQL, bindings, schedList,
309  (uint)nStartIndex, (uint)nCount, nTotalAvailable);
310 
311  // ----------------------------------------------------------------------
312  // Build Response
313  // ----------------------------------------------------------------------
314 
315  auto *pPrograms = new DTC::ProgramList();
316 
317  nCount = (int)progList.size();
318  int nEndIndex = (int)progList.size();
319 
320  for( int n = 0; n < nEndIndex; n++)
321  {
322  ProgramInfo *pInfo = progList[ n ];
323 
324  DTC::Program *pProgram = pPrograms->AddNewProgram();
325 
326  FillProgramInfo( pProgram, pInfo, true, bDetails, false ); // No cast info, loading this takes far too long
327  }
328 
329  // ----------------------------------------------------------------------
330 
331  pPrograms->setStartIndex ( nStartIndex );
332  pPrograms->setCount ( nCount );
333  pPrograms->setTotalAvailable( nTotalAvailable );
334  pPrograms->setAsOf ( MythDate::current() );
335  pPrograms->setVersion ( MYTH_BINARY_VERSION );
336  pPrograms->setProtoVer ( MYTH_PROTO_VERSION );
337 
338  return pPrograms;
339 }
340 
342 //
344 
346  const QDateTime &rawStartTime )
347 
348 {
349  if (!(nChanId > 0))
350  throw QString( "Channel ID is invalid" );
351  if (!rawStartTime.isValid())
352  throw QString( "StartTime is invalid" );
353 
354  QDateTime dtStartTime = rawStartTime.toUTC();
355 
356  // ----------------------------------------------------------------------
357  // -=>TODO: Add support for getting Recorded Program Info
358  // ----------------------------------------------------------------------
359 
360  // Build Response
361 
362  auto *pProgram = new DTC::Program();
363  ProgramInfo *pInfo = LoadProgramFromProgram(nChanId, dtStartTime);
364 
365  FillProgramInfo( pProgram, pInfo, true, true, true );
366 
367  delete pInfo;
368 
369  return pProgram;
370 }
371 
373 //
375 
376 QFileInfo Guide::GetChannelIcon( int nChanId,
377  int nWidth /* = 0 */,
378  int nHeight /* = 0 */ )
379 {
380  // Get Icon file path
381 
382  QString sFileName = ChannelUtil::GetIcon( nChanId );
383 
384  if (sFileName.isEmpty())
385  {
386  LOG(VB_UPNP, LOG_ERR,
387  QString("GetImageFile - ChanId %1 doesn't exist or isn't visible")
388  .arg(nChanId));
389  return QFileInfo();
390  }
391 
392  // ------------------------------------------------------------------
393  // Search for the filename
394  // ------------------------------------------------------------------
395 
396  StorageGroup storage( "ChannelIcons" );
397  QString sFullFileName = storage.FindFile( sFileName );
398 
399  if (sFullFileName.isEmpty())
400  {
401  LOG(VB_UPNP, LOG_ERR,
402  QString("GetImageFile - Unable to find %1.").arg(sFileName));
403 
404  return QFileInfo();
405  }
406 
407  // ----------------------------------------------------------------------
408  // check to see if the file (still) exists
409  // ----------------------------------------------------------------------
410 
411  if ((nWidth == 0) && (nHeight == 0))
412  {
413  if (QFile::exists( sFullFileName ))
414  {
415  return QFileInfo( sFullFileName );
416  }
417 
418  LOG(VB_UPNP, LOG_ERR,
419  QString("GetImageFile - File Does not exist %1.").arg(sFullFileName));
420 
421  return QFileInfo();
422  }
423  // -------------------------------------------------------------------
424 
425  QString sNewFileName = QString( "%1.%2x%3.png" )
426  .arg( sFullFileName )
427  .arg( nWidth )
428  .arg( nHeight );
429 
430  // ----------------------------------------------------------------------
431  // check to see if image is already created.
432  // ----------------------------------------------------------------------
433 
434  if (QFile::exists( sNewFileName ))
435  return QFileInfo( sNewFileName );
436 
437  // ----------------------------------------------------------------------
438  // We need to create it...
439  // ----------------------------------------------------------------------
440 
441  QString sChannelsDirectory = QFileInfo( sNewFileName ).absolutePath();
442 
443  if (!QFileInfo( sChannelsDirectory ).isWritable())
444  {
445  LOG(VB_UPNP, LOG_ERR, QString("GetImageFile - no write access to: %1")
446  .arg( sChannelsDirectory ));
447  return QFileInfo();
448  }
449 
450  auto *pImage = new QImage( sFullFileName );
451 
452  if (!pImage)
453  {
454  LOG(VB_UPNP, LOG_ERR, QString("GetImageFile - can't create image: %1")
455  .arg( sFullFileName ));
456  return QFileInfo();
457  }
458 
459  float fAspect = (float)(pImage->width()) / pImage->height();
460  if (fAspect == 0)
461  {
462  LOG(VB_UPNP, LOG_ERR, QString("GetImageFile - zero aspect"));
463  delete pImage;
464  return QFileInfo();
465  }
466 
467  if ( nWidth == 0 )
468  nWidth = (int)rint(nHeight * fAspect);
469 
470  if ( nHeight == 0 )
471  nHeight = (int)rint(nWidth / fAspect);
472 
473  QImage img = pImage->scaled( nWidth, nHeight, Qt::IgnoreAspectRatio,
474  Qt::SmoothTransformation);
475 
476  if (img.isNull())
477  {
478  LOG(VB_UPNP, LOG_ERR, QString("SaveImageFile - unable to scale. "
479  "See if %1 is really an image.").arg( sFullFileName ));
480  delete pImage;
481  return QFileInfo();
482  }
483 
484  if (!img.save( sNewFileName, "PNG" ))
485  {
486  LOG(VB_UPNP, LOG_ERR, QString("SaveImageFile - failed, %1")
487  .arg( sNewFileName ));
488  delete pImage;
489  return QFileInfo();
490  }
491 
492  delete pImage;
493 
494  return QFileInfo( sNewFileName );
495 }
496 
498 //
500 
502 {
503  ChannelGroupList list = ChannelGroup::GetChannelGroups(bIncludeEmpty);
504  auto *pGroupList = new DTC::ChannelGroupList();
505 
506  ChannelGroupList::iterator it;
507  for (it = list.begin(); it < list.end(); ++it)
508  {
509  DTC::ChannelGroup *pGroup = pGroupList->AddNewChannelGroup();
510  FillChannelGroup(pGroup, (*it));
511  }
512 
513  return pGroupList;
514 }
515 
517 //
519 
520 QStringList Guide::GetCategoryList( ) //int nStartIndex, int nCount)
521 {
522  QStringList catList;
523  MSqlQuery query(MSqlQuery::InitCon());
524 
525  query.prepare("SELECT DISTINCT category FROM program WHERE category != '' "
526  "ORDER BY category");
527 
528  if (!query.exec())
529  return catList;
530 
531  while (query.next())
532  {
533  catList << query.value(0).toString();
534  }
535 
536  return catList;
537 }
538 
540 //
542 
543 QStringList Guide::GetStoredSearches( const QString& sType )
544 {
545  QStringList keywordList;
546  MSqlQuery query(MSqlQuery::InitCon());
547 
548  RecSearchType iType = searchTypeFromString(sType);
549 
550  if (iType == kNoSearch)
551  {
552  //throw QString( "Invalid Type" );
553  return keywordList;
554  }
555 
556  query.prepare("SELECT DISTINCT phrase FROM keyword "
557  "WHERE searchtype = :TYPE "
558  "ORDER BY phrase");
559  query.bindValue(":TYPE", static_cast<int>(iType));
560 
561  if (!query.exec())
562  return keywordList;
563 
564  while (query.next())
565  {
566  keywordList << query.value(0).toString();
567  }
568 
569  return keywordList;
570 }
571 
573 //
575 
576 bool Guide::AddToChannelGroup ( int nChannelGroupId,
577  int nChanId )
578 {
579  bool bResult = false;
580 
581  if (!(nChanId > 0))
582  throw QString( "Channel ID is invalid" );
583 
584  bResult = ChannelGroup::AddChannel(nChanId, nChannelGroupId);
585 
586  return bResult;
587 }
588 
590 //
592 
593 bool Guide::RemoveFromChannelGroup ( int nChannelGroupId,
594  int nChanId )
595 {
596  bool bResult = false;
597 
598  if (!(nChanId > 0))
599  throw QString( "Channel ID is invalid" );
600 
601  bResult = ChannelGroup::DeleteChannel(nChanId, nChannelGroupId);
602 
603  return bResult;
604 }
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:781
MythScheduler * GetScheduler(void)
void FillChannelGroup(DTC::ChannelGroup *pGroup, const ChannelGroupItem &pGroupItem)
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:862
static ChannelInfoList LoadChannels(uint startIndex, uint count, uint &totalAvailable, bool ignoreHidden=true, OrderBy orderBy=kChanOrderByChanNum, GroupBy groupBy=kChanGroupByChanid, uint sourceID=0, uint channelGroupID=0, bool liveTVOnly=false, const QString &callsign="", const QString &channum="", bool ignoreUntunable=true)
Load channels from database into a list of ChannelInfo objects.
ProgramInfo * LoadProgramFromProgram(const uint chanid, const QDateTime &starttime)
DTC::ChannelGroupList * GetChannelGroupList(bool IncludeEmpty) override
Definition: guide.cpp:501
void FillProgramInfo(DTC::Program *pProgram, ProgramInfo *pInfo, bool bIncChannel, bool bDetails, bool bIncCast)
Definition: serviceUtil.cpp:44
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:125
AutoExpire * expirer
bool RemoveFromChannelGroup(int ChannelGroupId, int ChanId) override
Definition: guide.cpp:593
RecSearchType searchTypeFromString(const QString &type)
AutoDeleteDeque< ProgramInfo * > ProgramList
Definition: programinfo.h:30
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
size_t size(void) const
bool AddToChannelGroup(int ChannelGroupId, int ChanId) override
Definition: guide.cpp:576
iterator begin(void)
typename List::iterator iterator
QVariant value(int i) const
Definition: mythdbcon.h:198
bool FillChannelInfo(DTC::ChannelInfo *pChannel, uint nChanID, bool bDetails)
Holds information on recordings and videos.
Definition: programinfo.h:66
QMap< QString, QVariant > MSqlBindings
typedef for a map of string -> string bindings for generic queries.
Definition: mythdbcon.h:98
DTC::ProgramList * GetProgramList(int StartIndex, int Count, const QDateTime &StartTime, const QDateTime &EndTime, int ChanId, const QString &TitleFilter, const QString &CategoryFilter, const QString &PersonFilter, const QString &KeywordFilter, bool OnlyNew, bool Details, const QString &Sort, bool Descending, bool WithInvisible) override
Definition: guide.cpp:170
static bool DeleteChannel(uint chanid, int changrpid)
static ChannelGroupList GetChannelGroups(bool includeEmpty=true)
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:10
QStringList GetCategoryList() override
Definition: guide.cpp:520
#define MYTH_PROTO_VERSION
Increment this whenever the MythTV network protocol changes.
Definition: mythversion.h:48
static QString GetIcon(uint chanid)
RecSearchType
unsigned int uint
Definition: compat.h:140
static MSqlQueryInfo InitCon(ConnectionReuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:534
vector< ChannelGroupItem > ChannelGroupList
Definition: channelgroup.h:32
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:806
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
DTC::ProgramGuide * GetProgramGuide(const QDateTime &StartTime, const QDateTime &EndTime, bool Details, int ChannelGroupId, int StartIndex, int Count, bool WithInvisible) override
Definition: guide.cpp:48
bool LoadFromProgram(ProgramList &destination, const QString &where, const QString &groupBy, const QString &orderBy, const MSqlBindings &bindings, const ProgramList &schedList)
QString FindFile(const QString &filename)
Used to expire recordings to make space for new recordings.
Definition: autoexpire.h:61
iterator end(void)
static bool AddChannel(uint chanid, int changrpid)
#define MYTH_BINARY_VERSION
Update this whenever the plug-in ABI changes.
Definition: mythversion.h:16
DTC::Program * GetProgramDetails(int ChanId, const QDateTime &StartTime) override
Definition: guide.cpp:345
Scheduler * sched
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:602
Program * AddNewProgram()
vector< ChannelInfo > ChannelInfoList
Definition: channelinfo.h:121
QStringList GetStoredSearches(const QString &Type) override
Definition: guide.cpp:543
QFileInfo GetChannelIcon(int ChanId, int Width, int Height) override
Definition: guide.cpp:376