MythTV  master
recordingextender.cpp
Go to the documentation of this file.
1 /*
2  * Class RecordingExtender
3  *
4  * Copyright (c) David Hampton 2021
5  *
6  * Based on the ideas in the standalone Myth Recording PHP code from
7  * Derek Battams <derek@battams.ca>.
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22  */
23 
24 // MythTV
25 #include "libmythbase/mythchrono.h"
27 #include "libmythbase/mythdate.h"
28 #include "libmythbase/mythdb.h"
30 #include "libmythbase/mythevent.h"
32 
33 // MythBackend
34 #include "recordingextender.h"
35 #include "scheduler.h"
36 
37 #define LOC QString("RecExt: ")
38 
40 static constexpr int64_t kLookBackTime { 3LL * 60 * 60 };
41 static constexpr int64_t kLookForwardTime { 1LL * 60 * 60 };
42 
43 static constexpr std::chrono::minutes kExtensionTime {10};
44 static constexpr int kExtensionTimeInSec {
45  (duration_cast<std::chrono::seconds>(kExtensionTime).count()) };
46 static const QRegularExpression kVersusPattern {R"(\s(at|@|vs\.?)\s)"};
47 static const QRegularExpression kSentencePattern {R"(:|\.+\s)"};
48 
54 static inline bool ValidRecordingStatus(RecStatus::Type recstatus)
55 {
56  return (recstatus == RecStatus::Recording ||
57  recstatus == RecStatus::Tuning ||
58  recstatus == RecStatus::WillRecord ||
59  recstatus == RecStatus::Pending);
60 }
61 
67 void ActiveGame::setInfoUrl(QUrl url)
68 {
69  LOG(VB_GENERAL, LOG_DEBUG, LOC +
70  QString("setInfoUrl(%1)").arg(url.url()));
71  m_infoUrl = std::move(url);
72 }
73 
79 void ActiveGame::setGameUrl(QUrl url)
80 {
81  LOG(VB_GENERAL, LOG_DEBUG, LOC +
82  QString("setGameUrl(%1)").arg(url.url()));
83  m_gameUrl = std::move(url);
84 }
85 
95 bool ActiveGame::teamsMatch(const QStringList& names, const QStringList& abbrevs) const
96 {
97  // Exact name matches
98  if ((m_team1Normalized == names[0]) &&
99  (m_team2Normalized == names[1]))
100  return true;
101  if ((m_team1Normalized == names[1]) &&
102  (m_team2Normalized == names[0]))
103  return true;
104 
105  // One name or the other is shortened
106  if (((m_team1Normalized.contains(names[0])) ||
107  (names[0].contains(m_team1Normalized))) &&
108  ((m_team2Normalized.contains(names[1])) ||
109  names[1].contains(m_team2Normalized)))
110  return true;
111  if (((m_team1Normalized.contains(names[1])) ||
112  (names[1].contains(m_team1Normalized))) &&
113  ((m_team2Normalized.contains(names[0])) ||
114  names[0].contains(m_team2Normalized)))
115  return true;
116 
117  // Check abbrevs
118  if ((m_team1 == abbrevs[0]) && (m_team2 == abbrevs[1]))
119  return true;
120  return ((m_team1 == abbrevs[1]) && (m_team2 == abbrevs[0]));
121 }
122 
126 
131 bool RecExtDataPage::timeIsClose(const QDateTime& eventStart)
132 {
133  QDateTime now = getNow();
134  QDateTime past = now.addSecs(-kLookBackTime);
135  QDateTime future = now.addSecs( kLookForwardTime);
136 #if 0
137  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("past: %1.").arg(past.toString(Qt::ISODate)));
138  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("eventStart: %1.").arg(eventStart.toString(Qt::ISODate)));
139  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("future: %1.").arg(future.toString(Qt::ISODate)));
140  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("result is %1.")
141  .arg(((past < eventStart) && (eventStart < future)) ? "true" : "false"));
142 #endif
143  return ((past < eventStart) && (eventStart < future));
144 }
145 
153 QJsonObject RecExtDataPage::walkJsonPath(QJsonObject& object, const QStringList& path)
154 {
155  static QRegularExpression re { R"((\w+)\[(\d+)\])" };
156  QRegularExpressionMatch match;
157 
158  for (const QString& step : path)
159  {
160  if (step.contains(re, &match))
161  {
162  QString name = match.captured(1);
163  int index = match.captured(2).toInt();
164  if (!object.contains(name) || !object[name].isArray())
165  {
166  LOG(VB_GENERAL, LOG_ERR, LOC +
167  QString("Invalid json at %1 in path %2 (not an array)")
168  .arg(name, path.join('/')));
169  return {};
170  }
171  QJsonArray array = object[name].toArray();
172  if ((array.size() < index) || !array[index].isObject())
173  {
174  LOG(VB_GENERAL, LOG_ERR, LOC +
175  QString("Invalid json at %1[%2] in path %3 (invalid array)")
176  .arg(name).arg(index).arg(path.join('/')));
177  return {};
178  }
179  object = array[index].toObject();
180  }
181  else
182  {
183  if (!object.contains(step) || !object[step].isObject())
184  {
185  LOG(VB_GENERAL, LOG_ERR, LOC +
186  QString("Invalid json at %1 in path %2 (not an object)")
187  .arg(step, path.join('/')));
188  return {};
189  }
190  object = object[step].toObject();
191  }
192  }
193  return object;
194 }
195 
204 bool RecExtDataPage::getJsonInt(const QJsonObject& _object, QStringList& path, int& value)
205 {
206  if (path.empty())
207  return false;
208  QString key = path.takeLast();
209  QJsonObject object = _object;
210  if (!path.empty())
211  object = walkJsonPath(object, path);
212  if (object.isEmpty() || !object.contains(key) || !object[key].isDouble())
213  {
214  LOG(VB_GENERAL, LOG_DEBUG, LOC +
215  QString("invalid key: %1.").arg(path.join('/')));
216  return false;
217  }
218  value = object[key].toDouble();
219  return true;
220 }
221 
230 bool RecExtDataPage::getJsonInt(const QJsonObject& object, const QString& key, int& value)
231 {
232  QStringList list = key.split('/');
233  return getJsonInt(object, list, value);
234 }
235 
242 bool RecExtDataPage::getJsonString(const QJsonObject& _object, QStringList& path, QString& value)
243 {
244  if (path.empty())
245  return false;
246  QString key = path.takeLast();
247  QJsonObject object = _object;
248  if (!path.empty())
249  object = walkJsonPath(object, path);
250  if (object.isEmpty() || !object.contains(key) || !object[key].isString())
251  {
252  LOG(VB_GENERAL, LOG_DEBUG, LOC +
253  QString("invalid key: %1.").arg(path.join('/')));
254  return false;
255  }
256  value = object[key].toString();
257  return true;
258 }
259 
268 bool RecExtDataPage::getJsonString(const QJsonObject& object, const QString& key, QString& value)
269 {
270  QStringList list = key.split('/');
271  return getJsonString(object, list, value);
272 }
273 
280 bool RecExtDataPage::getJsonObject(const QJsonObject& _object, QStringList& path, QJsonObject& value)
281 {
282  if (path.empty())
283  return false;
284  QString key = path.takeLast();
285  QJsonObject object = _object;
286  if (!path.empty())
287  object = walkJsonPath(object, path);
288  if (object.isEmpty() || !object.contains(key) || !object[key].isObject())
289  {
290  LOG(VB_GENERAL, LOG_DEBUG, LOC +
291  QString("invalid key: %1.").arg(path.join('/')));
292  return false;
293  }
294  value = object[key].toObject();
295  return true;
296 }
297 
306 bool RecExtDataPage::getJsonObject(const QJsonObject& object, const QString& key, QJsonObject& value)
307 {
308  QStringList list = key.split('/');
309  return getJsonObject(object, list, value);
310 }
311 
318 bool RecExtDataPage::getJsonArray(const QJsonObject& object, const QString& key, QJsonArray& value)
319 {
320  if (!object.contains(key) || !object[key].isArray())
321  return false;
322  value = object[key].toArray();
323  return true;
324 }
325 
327 
329 QHash<QString,QJsonDocument> RecExtDataSource::s_downloadedJson {};
330 
333 {
334  s_downloadedJson.clear();
335 }
336 
344 static QString normalizeString(const QString& s)
345 {
346  QString result;
347 
348  QString norm = s.normalized(QString::NormalizationForm_D);
349  for (QChar c : qAsConst(norm))
350  {
351  switch (c.category())
352  {
353  case QChar::Mark_NonSpacing:
354  case QChar::Mark_SpacingCombining:
355  case QChar::Mark_Enclosing:
356  continue;
357  default:
358  result += c;
359  }
360  }
361 
362  // Possibly needed? Haven't seen a team name with a German eszett
363  // to know how they are handled by the api providers.
364  //result = result.replace("ß","ss");
365  return result.simplified();
366 }
367 
371 
372 // The URL to get the names of all the leagues in a given sport:
373 // https://site.api.espn.com/apis/site/v2/leagues/dropdown?sport=${sport}&limit=100
374 //
375 // The URL to get the names of all the teams in a league:
376 // http://site.api.espn.com/apis/site/v2/sports/${sport}/${league}/teams
377 
378 // The URL to retrieve schedules and scores.
379 // http://site.api.espn.com/apis/site/v2/sports/${sport}/${league}/scoreboard
380 // http://site.api.espn.com/apis/site/v2/sports/${sport}/${league}/scoreboard?dates=20180901
381 // http://sports.core.api.espn.com/v2/sports/${sport}/leagues/${league}/events/${eventId}/competitions/${eventId}/status
382 //
383 // Mens College Basketball (Group 50)
384 //
385 // All teams:
386 // http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams?groups=50&limit=500
387 //
388 // This only shows teams in the top 25:
389 // http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?date=20220126
390 //
391 // This shows all the scheduled games.
392 // http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?date=20220126&groups=50&limit=500
393 
394 static const QString espnInfoUrlFmt {"http://site.api.espn.com/apis/site/v2/sports/%1/%2/scoreboard"};
395 static const QString espnGameUrlFmt {"http://sports.core.api.espn.com/v2/sports/%1/leagues/%2/events/%3/competitions/%3/status"};
396 
398 const QList<RecExtEspnDataPage::GameStatus> RecExtEspnDataPage::kFinalStatuses {
399  FINAL, FORFEIT, CANCELLED, POSTPONED, SUSPENDED,
400  FORFEIT_HOME_TEAM, FORFEIT_AWAY_TEAM, ABANDONED, FULL_TIME,
401  PLAY_COMPLETE, OFFICIAL_EVENT_SHORTENED, RETIRED,
402  BYE, ESPNVOID, FINAL_SCORE_AFTER_EXTRA_TIME, FINAL_SCORE_AFTER_GOLDEN_GOAL,
403  FINAL_SCORE_AFTER_PENALTIES, END_EXTRA_TIME, FINAL_SCORE_ABANDONED,
404 };
405 
415 {
416  LOG(VB_GENERAL, LOG_DEBUG, LOC +
417  QString("Looking for match of %1/%2")
418  .arg(game.getTeam1(), game.getTeam2()));
419 
420  QJsonObject json = m_doc.object();
421  if (json.isEmpty())
422  return false;
423  if (!json.contains("events") || !json["events"].isArray())
424  {
425  LOG(VB_GENERAL, LOG_INFO, LOC +
426  QString("malformed json document, step %1.").arg(1));
427  return false;
428  }
429 
430  // Process the games
431  QJsonArray eventArray = json["events"].toArray();
432  for (const auto& eventValue : qAsConst(eventArray))
433  {
434  // Process info at the game level
435  if (!eventValue.isObject())
436  {
437  LOG(VB_GENERAL, LOG_INFO, LOC +
438  QString("malformed json document, step %1.").arg(2));
439  continue;
440  }
441  QJsonObject event = eventValue.toObject();
442 
443  // Top level info for a game
444  QString idStr {};
445  QString dateStr {};
446  QString gameTitle {};
447  QString gameShortTitle {};
448  if (!getJsonString(event, "id", idStr) ||
449  !getJsonString(event, "date", dateStr) ||
450  !getJsonString(event, "name", gameTitle) ||
451  !getJsonString(event, "shortName", gameShortTitle))
452  {
453  LOG(VB_GENERAL, LOG_INFO, LOC +
454  QString("malformed json document, step %1.").arg(3));
455  continue;
456  }
457  QStringList teamNames = gameTitle.split(kVersusPattern);
458  QStringList teamAbbrevs = gameShortTitle.split(kVersusPattern);
459  if ((teamNames.size() != 2) || (teamAbbrevs.size() != 2))
460  {
461  LOG(VB_GENERAL, LOG_INFO, LOC +
462  QString("malformed json document, step %1.").arg(4));
463  continue;
464  }
465  RecordingExtender::nameCleanup(game.getInfo(), teamNames[0], teamNames[1]);
466  if (!game.teamsMatch(teamNames, teamAbbrevs))
467  {
468  LOG(VB_GENERAL, LOG_DEBUG, LOC +
469  QString("Found %1 at %2 (%3 @ %4). Teams don't match.")
470  .arg(teamNames[0], teamNames[1],
471  teamAbbrevs[0], teamAbbrevs[1]));
472  continue;
473  }
474  QDateTime startTime = QDateTime::fromString(dateStr, Qt::ISODate);
475  if (!timeIsClose(startTime))
476  {
477  LOG(VB_GENERAL, LOG_INFO, LOC +
478  QString("Found '%1 vs %2' starting time %3 wrong")
479  .arg(game.getTeam1(), game.getTeam2(),
480  game.getStartTimeAsString()));
481  continue;
482  }
483 
484  // Found everthing we need.
485  game.setAbbrevs(teamAbbrevs);
486  game.setStartTime(startTime);
487  game.setGameUrl(getSource()->makeGameUrl(game, idStr));
488 
489  LOG(VB_GENERAL, LOG_DEBUG, LOC +
490  QString("Match: %1 at %2 (%3 @ %4), start %5.")
491  .arg(game.getTeam1(), game.getTeam2(),
492  game.getAbbrev1(), game.getAbbrev2(),
493  game.getStartTimeAsString()));
494  return true;
495  }
496  return false;
497 }
498 
511 {
512  LOG(VB_GENERAL, LOG_DEBUG, LOC +
513  QString("Parsing game score for %1/%2")
514  .arg(game.getTeam1(), game.getTeam2()));
515 
516  QJsonObject json = m_doc.object();
517  if (json.isEmpty())
518  return {};
519 
520  int period {-1};
521  QString typeId;
522  QString detail;
523  QString description;
524  if (!getJsonInt(json, "period", period) ||
525  !getJsonString(json, "type/id", typeId) ||
526  !getJsonString(json, "type/description", description) ||
527  !getJsonString(json, "type/detail", detail))
528  {
529  LOG(VB_GENERAL, LOG_INFO, LOC +
530  QString("malformed json document, step %1.").arg(5));
531  return {};
532  }
533  auto stateId = static_cast<GameStatus>(typeId.toInt());
534  bool gameOver = kFinalStatuses.contains(stateId);
535 
536  GameState state(game, period, gameOver);
537  QString extra;
538  if ((description == detail) || (description == "In Progress"))
539  extra = detail;
540  else
541  extra = QString ("%1: %2").arg(description, detail);
542  state.setTextState(extra);
543  LOG(VB_GENERAL, LOG_INFO, LOC +
544  QString("%1 at %2 (%3 @ %4), %5.")
545  .arg(game.getTeam1(), game.getTeam2(),
546  game.getAbbrev1(), game.getAbbrev2(), extra));
547  return state;
548 }
549 
559 RecExtEspnDataSource::loadPage(const ActiveGame& game, const QUrl& _url)
560 {
561  QString url = _url.url();
562 
563  // Return cached document
564  if (s_downloadedJson.contains(url))
565  {
566  LOG(VB_GENERAL, LOG_DEBUG, LOC +
567  QString("Using cached document for %1.").arg(url));
568  return newPage(s_downloadedJson[url]);
569  }
570 
571  QByteArray data;
572  bool ok {false};
573  QString scheme = _url.scheme();
574  if (scheme == QStringLiteral(u"file"))
575  {
576  QFile file(_url.path(QUrl::FullyDecoded));
577  ok = file.open(QIODevice::ReadOnly);
578  if (ok)
579  data = file.readAll();
580  }
581  else if ((scheme == QStringLiteral(u"http")) ||
582  (scheme == QStringLiteral(u"https")))
583  {
584  ok = GetMythDownloadManager()->download(url, &data);
585  }
586  if (!ok)
587  {
588  LOG(VB_GENERAL, LOG_INFO, LOC +
589  QString("\"%1\" couldn't download %2.")
590  .arg(game.getTitle(), url));
591  return nullptr;
592  }
593 
594  QJsonParseError error {};
595  QJsonDocument doc = QJsonDocument::fromJson(data, &error);
596  if (error.error != QJsonParseError::NoError)
597  {
598  LOG(VB_GENERAL, LOG_ERR, LOC +
599  QString("Error parsing %1 at offset %2: %3")
600  .arg(url).arg(error.offset).arg(error.errorString()));
601  return nullptr;
602  }
603 
604  QJsonObject json = doc.object();
605  if (json.contains("code") && json["code"].isDouble() &&
606  json.contains("detail") && json["detail"].isString())
607  {
608  LOG(VB_GENERAL, LOG_INFO, LOC +
609  QString("error downloading json document, code %1, detail %2.")
610  .arg(json["code"].toInt()).arg(json["detail"].toString()));
611  return nullptr;
612  }
613  s_downloadedJson[url] = doc;
614  return newPage(doc);
615 }
616 
623 QUrl RecExtEspnDataSource::makeInfoUrl (const SportInfo& info, const QDateTime& dt)
624 {
625  QUrl url {QString(espnInfoUrlFmt).arg(info.sport, info.league)};
626  QUrlQuery query;
627  query.addQueryItem("limit", "500");
628  // Add this to get all games, otherwise only top-25 games are returned.
629  if (info.league.endsWith("college-basketball"))
630  query.addQueryItem("group", "50");
631  if (dt.isValid())
632  query.addQueryItem("dates", dt.toString("yyyyMMdd"));
633  url.setQuery(query);
634  return url;
635 }
636 
643 QUrl RecExtEspnDataSource::makeGameUrl(const ActiveGame& game, const QString& str)
644 {
645  SportInfo info = game.getInfo();
646  QUrl gameUrl = QUrl(espnGameUrlFmt.arg(info.sport, info.league, str));
647  return gameUrl;
648 }
649 
665 {
666  // Find game with today's date (in UTC)
667  // Is the starting time close to now?
668  QDateTime now = MythDate::current();
669  game.setInfoUrl(makeInfoUrl(info, now));
670  RecExtDataPage* page = loadPage(game, game.getInfoUrl());
671  if (!page)
672  {
673  LOG(VB_GENERAL, LOG_INFO, LOC +
674  QString("Couldn't load %1").arg(game.getInfoUrl().url()));
675  return {};
676  }
677  if (page->findGameInfo(game))
678  {
679  LOG(VB_GENERAL, LOG_INFO, LOC +
680  QString("Found game '%1 vs %2' at %3")
681  .arg(game.getTeam1(), game.getTeam2(), game.getStartTimeAsString()));
682  return game.getInfoUrl();
683  }
684 
685  // Find game with yesterdays's date (in UTC)
686  // Handles evening games that start after 00:00 UTC. E.G. an 8pm EST football game.
687  // Is the starting time close to now?
688  game.setInfoUrl(makeInfoUrl(info, now.addDays(-1)));
689  page = loadPage(game, game.getInfoUrl());
690  if (!page)
691  {
692  LOG(VB_GENERAL, LOG_INFO, LOC +
693  QString("Couldn't load %1").arg(game.getInfoUrl().url()));
694  return {};
695  }
696  if (page->findGameInfo(game))
697  {
698  LOG(VB_GENERAL, LOG_INFO, LOC +
699  QString("Found game '%1 vs %2' at %3")
700  .arg(game.getTeam1(), game.getTeam2(), game.getStartTimeAsString()));
701  return game.getInfoUrl();
702  }
703 
704  // Find game with tomorrow's date (in UTC)
705  // E.G. Handles
706  // Is the starting time close to now?
707  game.setInfoUrl(makeInfoUrl(info, now.addDays(1)));
708  page = loadPage(game, game.getInfoUrl());
709  if (!page)
710  {
711  LOG(VB_GENERAL, LOG_INFO, LOC +
712  QString("Couldn't load %1").arg(game.getInfoUrl().url()));
713  return {};
714  }
715  if (page->findGameInfo(game))
716  {
717  LOG(VB_GENERAL, LOG_INFO, LOC +
718  QString("Found game '%1 vs %2' at %3")
719  .arg(game.getTeam1(), game.getTeam2(), game.getStartTimeAsString()));
720  return game.getInfoUrl();
721  }
722  return {};
723 }
724 
728 
729 // The MLB API is free for individual, non-commercial use. See
730 // http://gdx.mlb.com/components/copyright.txt
731 //
732 // Working queryable version of the API:
733 // https://beta-statsapi.mlb.com/docs/
734 //
735 // For schedule information:
736 // https://statsapi.mlb.com/api/v1/schedule?sportId=1&date=2021-09-18
737 // https://statsapi.mlb.com/api/v1/schedule?sportId=1&startDate=2021-09-18&endDate=2021-09-20"
738 //
739 // For game information:
740 // https://statsapi.mlb.com/api/v1.1/game/{game_pk}/feed/live
741 // where the game_pk comes from the schedule data.
742 //
743 // You can request extra data be returned by asking for "hydrations"
744 // (additional data) to be added to the response. The list of
745 // available hydrations for any API can be retrieved by adding
746 // "hydrate=hydrations" to the URL. For example:
747 // https://statsapi.mlb.com/api/v1/schedule?sportId=1&hydrate=hydrations&date=2021-09-18"
748 // https://statsapi.mlb.com/api/v1/schedule?sportId=1&hydrate=team&startDate=2021-09-18&endDate=2021-09-20"
749 
756 bool RecExtMlbDataPage::parseGameObject(const QJsonObject& gameObject,
757  ActiveGame& game)
758 {
759  QString dateStr {};
760  QString gameLink {};
761  QStringList teamNames {"", ""};
762  QStringList teamAbbrevs { "", ""};
763  if (!getJsonString(gameObject, "gameDate", dateStr) ||
764  !getJsonString(gameObject, "link", gameLink) ||
765  !getJsonString(gameObject, "teams/home/team/name", teamNames[0]) ||
766  !getJsonString(gameObject, "teams/away/team/name", teamNames[1]) ||
767  !getJsonString(gameObject, "teams/home/team/abbreviation", teamAbbrevs[0]) ||
768  !getJsonString(gameObject, "teams/away/team/abbreviation", teamAbbrevs[1]))
769  {
770  LOG(VB_GENERAL, LOG_INFO, LOC +
771  QString("malformed json document, step %1.").arg(1));
772  return false;
773  }
774 
775  RecordingExtender::nameCleanup(game.getInfo(), teamNames[0], teamNames[1]);
776  bool success = game.teamsMatch(teamNames, teamAbbrevs);
777  LOG(VB_GENERAL, LOG_DEBUG, LOC +
778  QString("Found: %1 at %2 (%3 @ %4), starting %5. (%6)")
779  .arg(teamNames[0], teamNames[1],
780  teamAbbrevs[0], teamAbbrevs[1], dateStr,
781  success ? "Success" : "Teams don't match"));
782  if (!success)
783  return false;
784 
785  // Found everthing we need.
786  game.setAbbrevs(teamAbbrevs);
787  game.setGameUrl(getSource()->makeGameUrl(game, gameLink));
789  return true;
790 }
791 
801 {
802  LOG(VB_GENERAL, LOG_DEBUG, LOC +
803  QString("Looking for match of %1/%2")
804  .arg(game.getTeam1(), game.getTeam2()));
805 
806  QJsonObject json = m_doc.object();
807  if (json.isEmpty())
808  return false;
809 
810  QJsonArray datesArray;
811  if (!getJsonArray(json, "dates", datesArray))
812  {
813  LOG(VB_GENERAL, LOG_INFO, LOC +
814  QString("malformed json document, step %1.").arg(1));
815  return false;
816  }
817 
818  // Process each of the three dates
819  for (const auto& dateValue : qAsConst(datesArray))
820  {
821  if (!dateValue.isObject())
822  {
823  LOG(VB_GENERAL, LOG_INFO, LOC +
824  QString("malformed json document, step %1.").arg(2));
825  continue;
826  }
827  QJsonObject dateObject = dateValue.toObject();
828 
829  QJsonArray gamesArray;
830  if (!getJsonArray(dateObject, "games", gamesArray))
831  {
832  LOG(VB_GENERAL, LOG_INFO, LOC +
833  QString("malformed json document, step %1.").arg(3));
834  continue;
835  }
836 
837  // Process each game on a given date
838  for (const auto& gameValue : qAsConst(gamesArray))
839  {
840  if (!gameValue.isObject())
841  {
842  LOG(VB_GENERAL, LOG_INFO, LOC +
843  QString("malformed json document, step %1.").arg(4));
844  continue;
845  }
846  QJsonObject gameObject = gameValue.toObject();
847 
848  if (!parseGameObject(gameObject, game))
849  continue;
850  bool match = timeIsClose(game.getStartTime());
851  LOG(VB_GENERAL, LOG_INFO, LOC +
852  QString("Found '%1 vs %2' starting %3 (%4)")
853  .arg(game.getTeam1(), game.getTeam2(), game.getStartTimeAsString(),
854  match ? "match" : "keep looking"));
855  if (!match)
856  continue;
857  return true;
858  }
859  }
860  return false;
861 }
862 
870 {
871  LOG(VB_GENERAL, LOG_DEBUG, LOC +
872  QString("Parsing game score for %1/%2")
873  .arg(game.getTeam1(), game.getTeam2()));
874 
875  QJsonObject json = m_doc.object();
876  if (json.isEmpty())
877  return {};
878 
879  int period {-1};
880  QString abstractGameState;
881  QString detailedGameState;
882  QString inningState;
883  QString inningOrdinal;
884  if (!getJsonString(json, "gameData/status/abstractGameState", abstractGameState) ||
885  !getJsonString(json, "gameData/status/detailedState", detailedGameState))
886  {
887  LOG(VB_GENERAL, LOG_INFO, LOC +
888  QString("malformed json document (%d)").arg(1));
889  return {};
890  }
891 
892  if (detailedGameState != "Scheduled")
893  {
894  if (!getJsonInt( json, "liveData/linescore/currentInning", period) ||
895  !getJsonString(json, "liveData/linescore/currentInningOrdinal", inningOrdinal) ||
896  !getJsonString(json, "liveData/linescore/inningState", inningState))
897  {
898  LOG(VB_GENERAL, LOG_INFO, LOC +
899  QString("malformed json document (%d)").arg(2));
900  return {};
901  }
902  }
903 
904  bool gameOver = (abstractGameState == "Final") ||
905  detailedGameState.contains("Suspended");
906  GameState state = GameState(game.getTeam1(), game.getTeam2(),
907  game.getAbbrev1(), game.getAbbrev2(),
908  period, gameOver);
909  QString extra;
910  if (gameOver)
911  extra = "game over";
912  else if (detailedGameState == "In Progress")
913  extra = QString("%1 %2").arg(inningState, inningOrdinal);
914  else
915  extra = detailedGameState;
916  state.setTextState(extra);
917  LOG(VB_GENERAL, LOG_DEBUG, LOC +
918  QString("%1 at %2 (%3 @ %4), %5.")
919  .arg(game.getTeam1(), game.getTeam2(),
920  game.getAbbrev1(), game.getAbbrev2(), extra));
921  return state;
922 }
923 
933 RecExtMlbDataSource::loadPage(const ActiveGame& game, const QUrl& _url)
934 {
935  QString url = _url.url();
936 
937  // Return cached document
938  if (s_downloadedJson.contains(url))
939  {
940  LOG(VB_GENERAL, LOG_DEBUG, LOC +
941  QString("Using cached document for %1.").arg(url));
942  return newPage(s_downloadedJson[url]);
943  }
944 
945  QByteArray data;
946  bool ok {false};
947  QString scheme = _url.scheme();
948  if (scheme == QStringLiteral(u"file"))
949  {
950  QFile file(_url.path(QUrl::FullyDecoded));
951  ok = file.open(QIODevice::ReadOnly);
952  if (ok)
953  data = file.readAll();
954  }
955  else if ((scheme == QStringLiteral(u"http")) ||
956  (scheme == QStringLiteral(u"https")))
957  {
958  ok = GetMythDownloadManager()->download(url, &data);
959  }
960  if (!ok)
961  {
962  LOG(VB_GENERAL, LOG_INFO, LOC +
963  QString("\"%1\" couldn't download %2.")
964  .arg(game.getTitle(), url));
965  return nullptr;
966  }
967 
968  QJsonParseError error {};
969  QJsonDocument doc = QJsonDocument::fromJson(data, &error);
970  if (error.error != QJsonParseError::NoError)
971  {
972  LOG(VB_GENERAL, LOG_ERR, LOC +
973  QString("Error parsing %1 at offset %2: %3")
974  .arg(url).arg(error.offset).arg(error.errorString()));
975  return nullptr;
976  }
977  s_downloadedJson[url] = doc;
978  return newPage(doc);
979 }
980 
987 QUrl RecExtMlbDataSource::makeInfoUrl (const SportInfo& info, const QDateTime& dt)
988 {
989  Q_UNUSED(info);
990 
991  if (!dt.isValid())
992  return {};
993 
994  QDateTime yesterday = dt.addDays(-1);
995  QDateTime tomorrow = dt.addDays(+1);
996  QUrl url {"https://statsapi.mlb.com/api/v1/schedule"};
997  QUrlQuery query;
998  query.addQueryItem("sportId", "1");
999  query.addQueryItem("hydrate", "team");
1000  query.addQueryItem("startDate", QString("%1").arg(yesterday.toString("yyyy-MM-dd")));
1001  query.addQueryItem("endDate", QString("%1").arg(tomorrow.toString("yyyy-MM-dd")));
1002  url.setQuery(query);
1003  return url;
1004 }
1005 
1012 QUrl RecExtMlbDataSource::makeGameUrl (const ActiveGame& game, const QString& str)
1013 {
1014  QUrl gameUrl = game.getInfoUrl();
1015  gameUrl.setPath(str);
1016  gameUrl.setQuery(QString());
1017  return gameUrl;
1018 }
1019 
1033 {
1034  // Find game with today's date (in UTC)
1035  // Is the starting time close to now?
1036  QDateTime now = MythDate::current();
1037  game.setInfoUrl(makeInfoUrl(info, now));
1038  RecExtDataPage* page = loadPage(game, game.getInfoUrl());
1039  if (!page)
1040  {
1041  LOG(VB_GENERAL, LOG_INFO, LOC +
1042  QString("Couldn't load %1").arg(game.getInfoUrl().url()));
1043  return {};
1044  }
1045  LOG(VB_GENERAL, LOG_INFO, LOC +
1046  QString("Loaded page %1").arg(game.getInfoUrl().url()));
1047  if (page->findGameInfo(game))
1048  return game.getGameUrl();
1049 
1050  return {};
1051 }
1052 
1056 
1059 
1061 {
1063 }
1064 
1076 {
1077  RecordingRule *rr = ri.GetRecordingRule();
1079  {
1080  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1081  QString("Recording of %1 at %2 not marked for auto extend.")
1083  return;
1084  }
1085  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1086  QString("Adding %1 at %2 to new recordings list.")
1088 
1089  QMutexLocker lock(&s_createLock);
1090  if (!s_singleton)
1091  {
1093  s_singleton->m_scheduler = scheduler;
1094  s_singleton->start();
1095  s_singleton->moveToThread(s_singleton->qthread());
1096  }
1098 };
1099 
1105 {
1106  QMutexLocker lock(&m_newRecordingsLock);
1107  m_newRecordings.append(recordedID);
1108 }
1109 
1119 {
1120  switch (type)
1121  {
1122  default:
1123  return nullptr;
1124  case AutoExtendType::ESPN:
1125  return new RecExtEspnDataSource(this);
1126  case AutoExtendType::MLB:
1127  return new RecExtMlbDataSource(this);
1128  }
1129 }
1130 
1150 bool RecordingExtender::findKnownSport(const QString& _title,
1152  SportInfoList& infoList) const
1153 {
1154  static const QRegularExpression year {R"(\d{4})"};
1155  QRegularExpressionMatch match;
1156  QString title = _title;
1157  if (title.contains(year, &match))
1158  {
1159  bool ok {false};
1160  int matchYear = match.captured().toInt(&ok);
1161  int thisYear = m_forcedYearforTesting
1163  : QDateTime::currentDateTimeUtc().date().year();
1164  // FIFA Qualifiers can be in the year before the tournament.
1165  if (!ok || ((matchYear != thisYear) && (matchYear != thisYear+1)))
1166  return false;
1167  title = title.remove(match.capturedStart(), match.capturedLength());
1168  }
1169  title = title.simplified();
1170  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1171  QString("Looking for %1 title '%2")
1172  .arg(toString(type), title));
1173 
1174  MSqlQuery query(MSqlQuery::InitCon());
1175  query.prepare(
1176  "SELECT sl.title, api.provider, api.key1, api.key2" \
1177  " FROM sportslisting sl " \
1178  " INNER JOIN sportsapi api ON sl.api = api.id" \
1179  " WHERE api.provider = :PROVIDER AND :TITLE REGEXP sl.title");
1180  query.bindValue(":PROVIDER", static_cast<uint8_t>(type));
1181  query.bindValue(":TITLE", title);
1182  if (!query.exec())
1183  {
1184  MythDB::DBError("sportsapi() -- findKnownSport", query);
1185  return false;
1186  }
1187  while (query.next())
1188  {
1189  SportInfo info;
1190 
1191  info.showTitle = query.value(0).toString();
1192  info.dataProvider = type;
1193  info.sport = query.value(2).toString();
1194  info.league = query.value(3).toString();
1195  infoList.append(info);
1196 
1197  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1198  QString("Info: '%1' matches '%2' '%3' '%4' '%5'")
1199  .arg(title, toString(info.dataProvider), info.showTitle,
1200  info.sport, info.league));
1201  }
1202  return !infoList.isEmpty();
1203 }
1204 
1207 {
1209 }
1210 
1211 // Parse a single string. First split it into parts on a semi-colon or
1212 // 'period space', and then selectively check those parts for the
1213 // pattern "A vs B".
1214 static bool parseProgramString (const QString& string, int limit,
1215  QString& team1, QString& team2)
1216 {
1217  QString lString = string;
1218  QStringList parts = lString.replace("vs.", "vs").split(kSentencePattern);
1219  for (int i = 0; i < std::min(limit,static_cast<int>(parts.size())); i++)
1220  {
1221  QStringList words = parts[i].split(kVersusPattern);
1222  if (words.size() == 2)
1223  {
1224  team1 = words[0].simplified();
1225  team2 = words[1].simplified();
1226  return true;
1227  }
1228  }
1229  return false;
1230 }
1231 
1240 bool RecordingExtender::parseProgramInfo (const QString& subtitle, const QString& description,
1241  QString& team1, QString& team2)
1242 {
1243  if (parseProgramString(subtitle, 2, team1, team2))
1244  return true;
1245  if (parseProgramString(description, 1, team1, team2))
1246  return true;
1247  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1248  QString("can't find team names in subtitle or description '%1'").arg(description));
1249  return false;
1250 }
1251 
1259 {
1260  if (rr->m_parentRecID)
1261  return QString("%1->%2").arg(rr->m_parentRecID).arg(rr->m_recordID);
1262  return QString::number(rr->m_recordID);
1263 }
1264 
1274 void RecordingExtender::nameCleanup(const SportInfo& info, QString& name)
1275 {
1276  name = normalizeString(name);
1277  name = name.simplified();
1278 
1279  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Start: %1").arg(name));
1280 
1281  // Ask the database for all the applicable cleanups
1282  MSqlQuery query(MSqlQuery::InitCon());
1283  query.prepare(
1284  "SELECT sc.name, sc.pattern, sc.nth, sc.replacement" \
1285  " FROM sportscleanup sc " \
1286  " WHERE (provider=0 or provider=:PROVIDER) " \
1287  " AND (key1='all' or key1=:SPORT) " \
1288  " AND (:NAME REGEXP pattern)" \
1289  " ORDER BY sc.weight");
1290  query.bindValue(":PROVIDER", static_cast<uint8_t>(info.dataProvider));
1291  query.bindValue(":SPORT", info.sport);
1292  query.bindValue(":NAME", name);
1293  if (!query.exec())
1294  {
1295  MythDB::DBError("sportscleanup() -- main query", query);
1296  return;
1297  }
1298 
1299  // Now apply each cleanup.
1300  while (query.next())
1301  {
1302  QString patternName = query.value(0).toString();
1303  QString patternStr = query.value(1).toString();
1304  int patternField = query.value(2).toInt();
1305  QString replacement = query.value(3).toString();
1306 
1307  QString original = name;
1308  QString tag {"no match"};
1309  QRegularExpressionMatch match;
1310  // Should always be true....
1311  if (name.contains(QRegularExpression(patternStr), &match) &&
1312  match.hasMatch())
1313  {
1314  QString capturedText = match.captured(patternField);
1315  name = name.replace(match.capturedStart(patternField),
1316  match.capturedLength(patternField),
1317  replacement);
1318  name = name.simplified();
1319  if (name.isEmpty())
1320  {
1321  name = original;
1322  tag = "fail";
1323  }
1324  else
1325  {
1326  tag = QString("matched '%1'").arg(capturedText);
1327  }
1328  }
1329  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1330  QString("pattern '%1', %2, now '%3'")
1331  .arg(patternName, tag, name));
1332  }
1333 }
1334 
1342 void RecordingExtender::nameCleanup(const SportInfo& info, QString& name1, QString& name2)
1343 {
1344  nameCleanup(info, name1);
1345  if (!name2.isEmpty())
1346  nameCleanup(info, name2);
1347 }
1348 
1360  const RecordingInfo *ri, RecordingRule *rr, ActiveGame const& game)
1361 {
1362  LOG(VB_GENERAL, LOG_INFO, LOC +
1363  QString("Recording %1 rule %2 for '%3 @ %4' has finished. Stop recording.")
1364  .arg(ri->GetRecordingID())
1365  .arg(ruleIdAsString(rr), game.getTeam1(), game.getTeam2()));
1366 
1367  MythEvent me(QString("STOP_RECORDING %1 %2")
1368  .arg(ri->GetChanID())
1370  gCoreContext->dispatch(me);
1371 }
1372 
1382  const RecordingInfo *ri, RecordingRule *rr, const ActiveGame& game)
1383 {
1384  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1385  QString("Recording %1 rule %2 for '%3 @ %4' scheduled to end soon. Extending recording.")
1386  .arg(ri->GetRecordingID())
1387  .arg(ruleIdAsString(rr), game.getTeam1(), game.getTeam2()));
1388 
1389  // Create an override to make it easy to clean up later.
1390  if (rr->m_type != kOverrideRecord)
1391  {
1392  rr->MakeOverride();
1393  rr->m_type = kOverrideRecord;
1394  }
1395  static const QString ae {"(Auto Extend)"};
1396  rr->m_subtitle = rr->m_subtitle.startsWith(ae)
1397  ? rr->m_subtitle
1398  : ae + ' ' + rr->m_subtitle;
1399 
1400  // Update the recording end time. The m_endOffset field is the
1401  // one that is used by the scheduler for timing. The others are
1402  // only updated for consistency.
1403  rr->m_endOffset += kExtensionTime.count();
1404  QDateTime oldDt = ri->GetRecordingEndTime();
1405  QDateTime newDt = oldDt.addSecs(kExtensionTimeInSec);
1406  rr->m_enddate = newDt.date();
1407  rr->m_endtime = newDt.time();
1408 
1409  // Update the RecordingRule and Save/Apply it.
1410  if (!rr->Save(true))
1411  {
1412  // Oops. Maybe the backend crashed and there's an old override
1413  // recording already in the table?
1414  LOG(VB_GENERAL, LOG_ERR, LOC +
1415  QString("Recording %1, couldn't save override rule for '%2 @ %3'.")
1416  .arg(ri->GetRecordingID()).arg(game.getTeam1(), game.getTeam2()));
1417  return;
1418  }
1419 
1420  // Debugging
1421  bool exists = m_overrideRules.contains(rr->m_recordID);
1422  LOG(VB_GENERAL, LOG_INFO, LOC +
1423  QString("Recording %1, %2 override rule %3 for '%4 @ %5' ending %6 -> %7.")
1424  .arg(ri->GetRecordingID())
1425  .arg(exists ? "updated" : "created",
1426  ruleIdAsString(rr), game.getTeam1(), game.getTeam2(),
1427  oldDt.toString(Qt::ISODate), newDt.toString(Qt::ISODate)));
1428 
1429  // Remember the new rule number for later cleanup.
1430  if (!exists)
1431  m_overrideRules.append(rr->m_recordID);
1432 }
1433 
1442  const RecordingInfo *ri, RecordingRule *rr, const ActiveGame& game)
1443 {
1444  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1445  QString("Recording %1 rule %2 for '%3 @ %4' ends %5.")
1446  .arg(ri->GetRecordingID())
1447  .arg(ruleIdAsString(rr), game.getTeam1(), game.getTeam2(),
1448  ri->GetRecordingEndTime().toString(Qt::ISODate)));
1449 }
1450 
1455 {
1456  m_overrideRules.clear();
1457 }
1458 
1463 {
1464  while (true)
1465  {
1466  QMutexLocker locker (&m_newRecordingsLock);
1467  if (m_newRecordings.isEmpty())
1468  break;
1469  int recordedID = m_newRecordings.takeFirst();
1470  locker.unlock();
1471 
1472  // Have to get this from the scheduler, otherwise we never see
1473  // the actual recording state.
1474  // WE OWN THIS POINTER.
1475  RecordingInfo *ri = m_scheduler->GetRecording(recordedID);
1476  if (nullptr == ri)
1477  {
1478  LOG(VB_GENERAL, LOG_INFO, LOC +
1479  QString("Couldn't get recording %1 from scheduler")
1480  .arg(recordedID));
1481  continue;
1482  }
1483 
1485  {
1486  LOG(VB_GENERAL, LOG_INFO, LOC +
1487  QString("Invalid status for '%1 : %2', status %3.")
1488  .arg(ri->GetTitle(), ri->GetSubtitle(),
1490  delete ri;
1491  continue;
1492  }
1493  RecordingRule *rr = ri->GetRecordingRule(); // owned by ri
1494 
1495  SportInfoList infoList;
1496  if (!findKnownSport(ri->GetTitle(), rr->m_autoExtend, infoList))
1497  {
1498  LOG(VB_GENERAL, LOG_INFO, LOC +
1499  QString("Unknown sport '%1' for provider %2")
1500  .arg(ri->GetTitle(), toString(rr->m_autoExtend)));
1501  delete ri;
1502  continue;
1503  }
1504 
1505  auto* source = createDataSource(rr->m_autoExtend);
1506  if (!source)
1507  {
1508  LOG(VB_GENERAL, LOG_INFO, LOC +
1509  QString("unable to create data source of type %1.")
1510  .arg(toString(rr->m_autoExtend)));
1511  delete ri;
1512  continue;
1513  }
1514 
1515  // Build the game data structure
1516  ActiveGame game(recordedID, ri->GetTitle());
1517  QString team1;
1518  QString team2;
1519  if (!parseProgramInfo(ri->GetSubtitle(), ri->GetDescription(),
1520  team1, team2))
1521  {
1522  LOG(VB_GENERAL, LOG_INFO, LOC +
1523  QString("Unable to find '%1 : %2', provider %3")
1524  .arg(ri->GetTitle(), ri->GetSubtitle(),
1525  toString(rr->m_autoExtend)));
1526  delete source;
1527  delete ri;
1528  continue;
1529  }
1530  game.setTeams(team1, team2);
1531 
1532  // Now try each of the returned sport APIs
1533  bool found {false};
1534  for (auto it = infoList.begin(); !found && it != infoList.end(); it++)
1535  {
1536  SportInfo info = *it;
1537  game.setInfo(info);
1538  nameCleanup(info, team1, team2);
1539  game.setTeamsNorm(team1, team2);
1540 
1541  source->findInfoUrl(game, info);
1542  if (game.getGameUrl().isEmpty())
1543  {
1544  LOG(VB_GENERAL, LOG_INFO, LOC +
1545  QString("unable to find data page for recording '%1 : %2'.")
1546  .arg(ri->GetTitle(), ri->GetSubtitle()));
1547  continue;
1548  }
1549  found = true;
1550  }
1551 
1552  if (found)
1553  m_activeGames.append(game);
1554  delete source;
1555  delete ri;
1556  }
1557 }
1558 
1564 {
1565  for (auto it = m_activeGames.begin(); it != m_activeGames.end(); )
1566  {
1567  ActiveGame game = *it;
1568  // Have to get this from the scheduler, otherwise we never see
1569  // the change from original to override recording rule.
1570  // WE OWN THIS POINTER.
1572  if (nullptr == _ri)
1573  {
1574  LOG(VB_GENERAL, LOG_INFO, LOC +
1575  QString("Couldn't get recording %1 from scheduler")
1576  .arg(game.getRecordedId()));
1577  it = m_activeGames.erase(it);
1578  continue;
1579  }
1580 
1581  // Simplify memory management
1582  auto ri = std::make_unique<RecordingInfo>(*_ri);
1583  delete _ri;
1584 
1585  if (!ValidRecordingStatus(ri->GetRecordingStatus()))
1586  {
1587  LOG(VB_GENERAL, LOG_INFO, LOC +
1588  QString("Invalid status for '%1 : %2', status %3.")
1589  .arg(ri->GetTitle(), ri->GetSubtitle(),
1590  RecStatus::toString(ri->GetRecordingStatus())));
1591  it = m_activeGames.erase(it);
1592  continue;
1593  }
1594 
1595  RecordingRule *rr = ri->GetRecordingRule(); // owned by ri
1596  auto* source = createDataSource(rr->m_autoExtend);
1597  if (nullptr == source)
1598  {
1599  LOG(VB_GENERAL, LOG_INFO, LOC +
1600  QString("Couldn't create source of type %1")
1601  .arg(toString(rr->m_autoExtend)));
1602  it++;
1603  delete source;
1604  continue;
1605  }
1606  auto* page = source->loadPage(game, game.getGameUrl());
1607  if (nullptr == page)
1608  {
1609  LOG(VB_GENERAL, LOG_INFO, LOC +
1610  QString("Couldn't load source %1, teams %2 and %3, url %4")
1611  .arg(toString(rr->m_autoExtend), game.getTeam1(), game.getTeam2(),
1612  game.getGameUrl().url()));
1613  it++;
1614  delete source;
1615  continue;
1616  }
1617  auto gameState = page->findGameScore(game);
1618  if (!gameState.isValid())
1619  {
1620  LOG(VB_GENERAL, LOG_INFO, LOC +
1621  QString("Game state for source %1, teams %2 and %3 is invalid")
1622  .arg(toString(rr->m_autoExtend), game.getTeam1(), game.getTeam2()));
1623  it++;
1624  delete source;
1625  continue;
1626  }
1627  if (gameState.isFinished())
1628  {
1629  finishRecording(ri.get(), rr, game);
1630  it = m_activeGames.erase(it);
1631  delete source;
1632  continue;
1633  }
1634  if (ri->GetScheduledEndTime() <
1636  {
1637  extendRecording(ri.get(), rr, game);
1638  it++;
1639  delete source;
1640  continue;
1641  }
1642  unchangedRecording(ri.get(), rr, game);
1643  it++;
1644  delete source;
1645  }
1646 }
1647 
1651 {
1652  QMutexLocker lock1(&s_createLock);
1653  QMutexLocker lock2(&m_newRecordingsLock);
1654 
1655  if (m_newRecordings.empty() && m_activeGames.empty())
1656  {
1657  m_running = false;
1658  s_singleton = nullptr;
1659  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1660  QString("Nothing left to do. Exiting."));
1661  return;
1662  }
1663 
1664  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1665  QString("%1 new recordings, %2 active recordings, %3 overrides.")
1666  .arg(m_newRecordings.size()).arg(m_activeGames.size())
1667  .arg(m_overrideRules.size()));
1668 }
1669 
1672 {
1673  RunProlog();
1674 
1675  while (m_running)
1676  {
1677  usleep(kExtensionTime); // cppcheck-suppress usleepCalled
1678 
1682 
1683  checkDone();
1684  }
1685 
1686  expireOverrides();
1687 
1688  RunEpilog();
1689  quit();
1690 }
Scheduler
Definition: scheduler.h:45
MSqlQuery::next
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:811
RecStatus::Type
Type
Definition: recordingstatus.h:16
RecordingExtender::clearDownloadedInfo
static void clearDownloadedInfo()
Clear all downloaded info.
Definition: recordingextender.cpp:1206
MSqlQuery
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
ActiveGame::m_team1Normalized
QString m_team1Normalized
Definition: recordingextender.h:100
ActiveGame::setTeamsNorm
void setTeamsNorm(QString team1, QString team2)
Definition: recordingextender.h:75
mythevent.h
RecordingRule::m_enddate
QDate m_enddate
Definition: recordingrule.h:90
RecExtMlbDataPage::findGameInfo
bool findGameInfo(ActiveGame &game) override
Parse a previously downloaded data page for a given sport.
Definition: recordingextender.cpp:800
MythDate::toString
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:84
RecordingRule::m_parentRecID
int m_parentRecID
Definition: recordingrule.h:71
MThread::start
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:283
kLookBackTime
static constexpr int64_t kLookBackTime
Does the specified time fall within -3/+1 hour from now?
Definition: recordingextender.cpp:40
MThread::quit
void quit(void)
calls exit(0)
Definition: mthread.cpp:295
ValidRecordingStatus
static bool ValidRecordingStatus(RecStatus::Type recstatus)
Does this recording status indicate that the recording is still ongoing.
Definition: recordingextender.cpp:54
ActiveGame::setAbbrevs
void setAbbrevs(QStringList abbrevs)
Definition: recordingextender.h:79
RecExtDataSource::clearCache
static void clearCache()
Clear the downloaded document cache.
Definition: recordingextender.cpp:332
RecExtMlbDataPage::parseGameObject
bool parseGameObject(const QJsonObject &gameObject, ActiveGame &game)
MLB ///.
Definition: recordingextender.cpp:756
error
static void error(const char *str,...)
Definition: vbi.cpp:36
RecordingExtender::addNewRecording
void addNewRecording(int recordedID)
Add an item to the list of new recordings.
Definition: recordingextender.cpp:1104
RecExtMlbDataSource::newPage
RecExtDataPage * newPage(const QJsonDocument &doc) override
Definition: recordingextender.h:296
mythdb.h
RecordingRule::Save
bool Save(bool sendSig=true)
Definition: recordingrule.cpp:388
ActiveGame::getInfoUrl
QUrl getInfoUrl() const
Definition: recordingextender.h:66
ActiveGame::setTeams
void setTeams(QString team1, QString team2)
Definition: recordingextender.h:73
ActiveGame::getGameUrl
QUrl getGameUrl() const
Definition: recordingextender.h:67
RecExtMlbDataSource::makeGameUrl
QUrl makeGameUrl(const ActiveGame &game, const QString &str) override
Create a URL for one specific game in the MLB API that is built from the various known bits of data a...
Definition: recordingextender.cpp:1012
ActiveGame::getInfo
SportInfo getInfo() const
Definition: recordingextender.h:59
SportInfo::showTitle
QString showTitle
Definition: recordingextender.h:42
ActiveGame::teamsMatch
bool teamsMatch(const QStringList &names, const QStringList &abbrevs) const
Do the supplied team names/abbrevs match this game.
Definition: recordingextender.cpp:95
RecordingExtender::RecordingExtender
RecordingExtender()
Definition: recordingextender.h:319
RecStatus::Tuning
@ Tuning
Definition: recordingstatus.h:22
LOC
#define LOC
Definition: recordingextender.cpp:37
RecordingInfo
Holds information on a TV Program one might wish to record.
Definition: recordinginfo.h:35
RecordingExtender::s_createLock
static QMutex s_createLock
Interlock the scheduler thread crating this process, and this process determining whether it should c...
Definition: recordingextender.h:349
RecordingExtender::m_forcedYearforTesting
uint m_forcedYearforTesting
Testing data.
Definition: recordingextender.h:366
ActiveGame::getTitle
QString getTitle() const
Definition: recordingextender.h:58
ProgramInfo::GetRecordingID
uint GetRecordingID(void) const
Definition: programinfo.h:446
RecExtEspnDataPage::findGameInfo
bool findGameInfo(ActiveGame &game) override
Parse a previously downloaded data page for a given sport.
Definition: recordingextender.cpp:414
MythEvent
This class is used as a container for messages.
Definition: mythevent.h:16
MThread::usleep
static void usleep(std::chrono::microseconds time)
Definition: mthread.cpp:335
MSqlQuery::value
QVariant value(int i) const
Definition: mythdbcon.h:205
RecordingRule
Internal representation of a recording rule, mirrors the record table.
Definition: recordingrule.h:28
GameState::setTextState
void setTextState(QString text)
Definition: recordingextender.h:132
RecordingRule::m_endOffset
int m_endOffset
Definition: recordingrule.h:110
RecordingExtender::processActiveRecordings
void processActiveRecordings()
Process the currently active sports recordings.
Definition: recordingextender.cpp:1563
ActiveGame::setGameUrl
void setGameUrl(QUrl url)
Set the game status information URL.
Definition: recordingextender.cpp:79
ActiveGame::setStartTime
void setStartTime(const QDateTime &time)
Definition: recordingextender.h:83
RecExtDataPage::walkJsonPath
static QJsonObject walkJsonPath(QJsonObject &object, const QStringList &path)
Iterate through a json object and return the specified object.
Definition: recordingextender.cpp:153
MSqlQuery::exec
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:617
RecordingExtender::processNewRecordings
void processNewRecordings()
Process the list of newly started sports recordings.
Definition: recordingextender.cpp:1462
ActiveGame::getTeam1
QString getTeam1() const
Definition: recordingextender.h:60
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MThread::RunProlog
void RunProlog(void)
Sets up a thread, call this if you reimplement run().
Definition: mthread.cpp:196
ActiveGame::m_team1
QString m_team1
Definition: recordingextender.h:98
ActiveGame::getAbbrev2
QString getAbbrev2() const
Definition: recordingextender.h:65
build_compdb.file
file
Definition: build_compdb.py:55
AutoExtendType
AutoExtendType
Definition: recordingtypes.h:93
ActiveGame::setInfoUrl
void setInfoUrl(QUrl url)
Set the game scheduling information URL.
Definition: recordingextender.cpp:67
ProgramInfo::GetRecordingEndTime
QDateTime GetRecordingEndTime(void) const
Approximate time the recording should have ended, did end, or is intended to end.
Definition: programinfo.h:412
RecordingExtender
Definition: recordingextender.h:307
scheduler.h
MythDate::current
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:14
ProgramInfo::GetRecordingStartTime
QDateTime GetRecordingStartTime(void) const
Approximate time the recording started.
Definition: programinfo.h:404
parseProgramString
static bool parseProgramString(const QString &string, int limit, QString &team1, QString &team2)
Definition: recordingextender.cpp:1214
RecordingExtender::m_activeGames
QList< ActiveGame > m_activeGames
Currently ongoing games to track.
Definition: recordingextender.h:361
AutoExtendType::None
@ None
ActiveGame::m_infoUrl
QUrl m_infoUrl
Definition: recordingextender.h:104
RecStatus::toString
static QString toString(RecStatus::Type recstatus, uint id)
Converts "recstatus" into a short (unreadable) string.
Definition: recordingstatus.cpp:40
RecordingExtender::findKnownSport
bool findKnownSport(const QString &_title, AutoExtendType type, SportInfoList &info) const
Retrieve the db record for a sporting event on a specific provider.
Definition: recordingextender.cpp:1150
RecordingExtender::run
void run(void) override
The main execution loop for the Recording Extender.
Definition: recordingextender.cpp:1671
RecExtMlbDataSource::findInfoUrl
QUrl findInfoUrl(ActiveGame &game, SportInfo &info) override
Find the right URL for a specific recording.
Definition: recordingextender.cpp:1032
RecStatus::WillRecord
@ WillRecord
Definition: recordingstatus.h:31
kLookForwardTime
static constexpr int64_t kLookForwardTime
Definition: recordingextender.cpp:41
ActiveGame::setInfo
void setInfo(const SportInfo &info)
Definition: recordingextender.h:72
mythdate.h
RecExtDataPage::getJsonString
static bool getJsonString(const QJsonObject &object, QStringList &path, QString &value)
Retrieve the specified string from a json object.
Definition: recordingextender.cpp:242
SportInfoList
QList< SportInfo > SportInfoList
Definition: recordingextender.h:47
ProgramInfo::GetScheduledStartTime
QDateTime GetScheduledStartTime(void) const
The scheduled start time of program.
Definition: programinfo.h:390
mythlogging.h
ProgramInfo::GetRecordingStatus
RecStatus::Type GetRecordingStatus(void) const
Definition: programinfo.h:447
RecordingExtender::m_running
bool m_running
Whether the RecordingExtender process is running.
Definition: recordingextender.h:351
ActiveGame::getStartTimeAsString
QString getStartTimeAsString() const
Definition: recordingextender.h:69
RecordingExtender::ruleIdAsString
static QString ruleIdAsString(const RecordingRule *rr)
Quick helper function for printing recording rule numbers.
Definition: recordingextender.cpp:1258
RecExtDataPage::getJsonInt
static bool getJsonInt(const QJsonObject &object, QStringList &path, int &value)
Retrieve the specified integer from a json object.
Definition: recordingextender.cpp:204
RecExtEspnDataSource::findInfoUrl
QUrl findInfoUrl(ActiveGame &game, SportInfo &info) override
Find the right URL for a specific recording.
Definition: recordingextender.cpp:664
kSentencePattern
static const QRegularExpression kSentencePattern
Definition: recordingextender.cpp:47
espnInfoUrlFmt
static const QString espnInfoUrlFmt
ESPN ///.
Definition: recordingextender.cpp:394
recordingextender.h
SportInfo::dataProvider
AutoExtendType dataProvider
Definition: recordingextender.h:43
RecordingExtender::createDataSource
virtual RecExtDataSource * createDataSource(AutoExtendType type)
Create a RecExtDataSource object for the specified service.
Definition: recordingextender.cpp:1118
RecExtDataPage::findGameInfo
virtual bool findGameInfo(ActiveGame &game)=0
RecordingExtender::m_newRecordings
QList< int > m_newRecordings
Newly started recordings to process.
Definition: recordingextender.h:359
MSqlQuery::InitCon
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:549
ProgramInfo::GetTitle
QString GetTitle(void) const
Definition: programinfo.h:361
ProgramInfo::GetDescription
QString GetDescription(void) const
Definition: programinfo.h:365
MythDB::DBError
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:227
RecExtDataPage::getJsonArray
static bool getJsonArray(const QJsonObject &object, const QString &key, QJsonArray &value)
Retrieve the specified array from a json object.
Definition: recordingextender.cpp:318
ActiveGame::m_team2Normalized
QString m_team2Normalized
Definition: recordingextender.h:101
RecordingExtender::parseProgramInfo
static bool parseProgramInfo(const QString &subtitle, const QString &description, QString &team1, QString &team2)
Parse a RecordingInfo to find the team names.
Definition: recordingextender.cpp:1240
MThread::qthread
QThread * qthread(void)
Returns the thread, this will always return the same pointer no matter how often you restart the thre...
Definition: mthread.cpp:233
RecordingRule::m_type
RecordingType m_type
Definition: recordingrule.h:111
RecExtEspnDataSource::newPage
RecExtDataPage * newPage(const QJsonDocument &doc) override
Definition: recordingextender.h:284
MThread::RunEpilog
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
Definition: mthread.cpp:209
MythDownloadManager::download
bool download(const QString &url, const QString &dest, bool reload=false)
Downloads a URL to a file in blocking mode.
Definition: mythdownloadmanager.cpp:430
RecordingRule::m_subtitle
QString m_subtitle
Definition: recordingrule.h:79
RecordingExtender::m_newRecordingsLock
QMutex m_newRecordingsLock
New recordings are added by the scheduler process and removed by this process.
Definition: recordingextender.h:357
RecExtDataPage::getSource
RecExtDataSource * getSource()
Definition: recordingextender.h:165
Scheduler::GetRecording
QMap< QString, ProgramInfo * > GetRecording(void) const override
Definition: scheduler.cpp:1780
RecordingExtender::checkDone
void checkDone()
Is there any remaining work? Check for both newly created recording and for active recordings.
Definition: recordingextender.cpp:1650
RecExtEspnDataPage::GameStatus
GameStatus
Definition: recordingextender.h:195
SportInfo
Definition: recordingextender.h:40
ActiveGame
Definition: recordingextender.h:49
RecExtEspnDataPage::kFinalStatuses
static const QList< GameStatus > kFinalStatuses
A list of the ESPN status that mean the game is over.
Definition: recordingextender.h:240
normalizeString
static QString normalizeString(const QString &s)
Remove all diacritical marks, etc., etc., from a string leaving just the base characters.
Definition: recordingextender.cpp:344
ActiveGame::m_gameUrl
QUrl m_gameUrl
Definition: recordingextender.h:105
espnGameUrlFmt
static const QString espnGameUrlFmt
Definition: recordingextender.cpp:395
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:54
RecExtDataPage
Definition: recordingextender.h:161
RecExtEspnDataSource::loadPage
RecExtDataPage * loadPage(const ActiveGame &game, const QUrl &_url) override
Download the data page for a game, and do some minimal validation.
Definition: recordingextender.cpp:559
RecExtDataSource::s_downloadedJson
static QHash< QString, QJsonDocument > s_downloadedJson
A cache of downloaded documents.
Definition: recordingextender.h:277
AutoExtendType::MLB
@ MLB
MythDate::fromString
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:34
RecStatus::Pending
@ Pending
Definition: recordingstatus.h:17
RecordingRule::m_endtime
QTime m_endtime
Definition: recordingrule.h:92
GameState
Definition: recordingextender.h:112
ProgramInfo::GetChanID
uint GetChanID(void) const
This is the unique key used in the database to locate tuning information.
Definition: programinfo.h:372
RecExtEspnDataSource::makeGameUrl
QUrl makeGameUrl(const ActiveGame &game, const QString &str) override
Create a URL for one specific game in the ESPN API that is built from the various known bits of data ...
Definition: recordingextender.cpp:643
RecordingRule::m_recordID
int m_recordID
Unique Recording Rule ID.
Definition: recordingrule.h:70
RecStatus::Recording
@ Recording
Definition: recordingstatus.h:30
RecordingExtender::m_scheduler
Scheduler * m_scheduler
Pointer to the scheduler.
Definition: recordingextender.h:354
RecordingExtender::create
static void create(Scheduler *scheduler, RecordingInfo &ri)
Create an instance of the RecordingExtender if necessary, and add this recording to the list of new r...
Definition: recordingextender.cpp:1075
AutoExtendType::ESPN
@ ESPN
mythcorecontext.h
ActiveGame::getStartTime
QDateTime getStartTime() const
Definition: recordingextender.h:68
RecExtEspnDataSource
Definition: recordingextender.h:280
ActiveGame::m_team2
QString m_team2
Definition: recordingextender.h:99
kOverrideRecord
@ kOverrideRecord
Definition: recordingtypes.h:29
SportInfo::sport
QString sport
Definition: recordingextender.h:44
MSqlQuery::bindValue
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:887
MythDate::ISODate
@ ISODate
Default UTC.
Definition: mythdate.h:17
ActiveGame::getTeam2
QString getTeam2() const
Definition: recordingextender.h:61
ActiveGame::getAbbrev1
QString getAbbrev1() const
Definition: recordingextender.h:64
RecordingExtender::nameCleanup
static void nameCleanup(const SportInfo &info, QString &name1, QString &name2)
Clean up two team names for comparison against the ESPN API.
Definition: recordingextender.cpp:1342
RecExtMlbDataSource
Definition: recordingextender.h:292
RecExtDataPage::m_doc
QJsonDocument m_doc
Definition: recordingextender.h:187
RecordingRule::m_autoExtend
AutoExtendType m_autoExtend
Definition: recordingrule.h:116
mythchrono.h
RecExtDataPage::timeIsClose
virtual bool timeIsClose(const QDateTime &eventStart)
Base Classes ///.
Definition: recordingextender.cpp:131
ActiveGame::getRecordedId
int getRecordedId() const
Definition: recordingextender.h:57
RecExtMlbDataPage::findGameScore
GameState findGameScore(ActiveGame &game) override
Parse the previously downloaded data page for a given game.
Definition: recordingextender.cpp:869
RecordingRule::MakeOverride
bool MakeOverride(void)
Definition: recordingrule.cpp:366
RecExtDataPage::getJsonObject
static bool getJsonObject(const QJsonObject &object, QStringList &path, QJsonObject &value)
Retrieve a specific object from another json object.
Definition: recordingextender.cpp:280
RecExtDataPage::getNow
virtual QDateTime getNow()
Get the current time. Overridden by the testing code.
Definition: recordingextender.h:168
RecordingExtender::s_singleton
static RecordingExtender * s_singleton
The single instance of a RecordingExtender.
Definition: recordingextender.h:346
RecordingExtender::finishRecording
static void finishRecording(const RecordingInfo *ri, RecordingRule *rr, const ActiveGame &game)
Stop the current recording early.
Definition: recordingextender.cpp:1359
RecordingInfo::GetRecordingRule
RecordingRule * GetRecordingRule(void)
Returns the "record" field, creating it if necessary.
Definition: recordinginfo.cpp:926
kExtensionTimeInSec
static constexpr int kExtensionTimeInSec
Definition: recordingextender.cpp:44
RecExtMlbDataSource::makeInfoUrl
QUrl makeInfoUrl(const SportInfo &info, const QDateTime &dt) override
Create a URL for the MLB API that is built from the various known bits of data accumulated so far.
Definition: recordingextender.cpp:987
mythdownloadmanager.h
RecordingExtender::expireOverrides
void expireOverrides()
Delete the list of the override rules that have been created by this instance of RecordingExtender.
Definition: recordingextender.cpp:1454
RecordingExtender::m_overrideRules
QList< int > m_overrideRules
Recordings that have had an override rule creates.
Definition: recordingextender.h:363
RecExtEspnDataSource::makeInfoUrl
QUrl makeInfoUrl(const SportInfo &info, const QDateTime &dt) override
Create a URL for the ESPN API that is built from the various known bits of data accumulated so far.
Definition: recordingextender.cpp:623
RecordingExtender::extendRecording
void extendRecording(const RecordingInfo *ri, RecordingRule *rr, const ActiveGame &game)
Extend the current recording by XX minutes.
Definition: recordingextender.cpp:1381
MythCoreContext::dispatch
void dispatch(const MythEvent &event)
Definition: mythcorecontext.cpp:1723
RecExtEspnDataPage::findGameScore
GameState findGameScore(ActiveGame &game) override
Parse the previously downloaded data page for a given game.
Definition: recordingextender.cpp:510
kExtensionTime
static constexpr std::chrono::minutes kExtensionTime
Definition: recordingextender.cpp:43
SportInfo::league
QString league
Definition: recordingextender.h:45
RecordingExtender::~RecordingExtender
~RecordingExtender() override
Definition: recordingextender.cpp:1060
kVersusPattern
static const QRegularExpression kVersusPattern
Definition: recordingextender.cpp:46
RecExtMlbDataSource::loadPage
RecExtDataPage * loadPage(const ActiveGame &game, const QUrl &_url) override
Download the data page for a game, and do some minimal validation.
Definition: recordingextender.cpp:933
GetMythDownloadManager
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
Definition: mythdownloadmanager.cpp:145
RecExtDataSource
Definition: recordingextender.h:259
MSqlQuery::prepare
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:836
ProgramInfo::GetSubtitle
QString GetSubtitle(void) const
Definition: programinfo.h:363
RecordingExtender::unchangedRecording
static void unchangedRecording(const RecordingInfo *ri, RecordingRule *rr, const ActiveGame &game)
Log that this recording hasn't changed.
Definition: recordingextender.cpp:1441