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 // Qt
25 #include <QFile>
26 #include <QJsonArray>
27 #include <QJsonObject>
28 #include <QUrlQuery>
29 
30 // MythTV
31 #include "libmythbase/mythchrono.h"
33 #include "libmythbase/mythdate.h"
34 #include "libmythbase/mythdb.h"
36 #include "libmythbase/mythevent.h"
38 
39 // MythBackend
40 #include "recordingextender.h"
41 #include "scheduler.h"
42 
43 #define LOC QString("RecExt: ")
44 
46 static constexpr int64_t kLookBackTime { 3LL * 60 * 60 };
47 static constexpr int64_t kLookForwardTime { 1LL * 60 * 60 };
48 
49 static constexpr std::chrono::minutes kExtensionTime {10};
50 static constexpr int kExtensionTimeInSec {
51  (duration_cast<std::chrono::seconds>(kExtensionTime).count()) };
52 static const QRegularExpression kVersusPattern {R"(\s(at|@|vs\.?)\s)"};
53 static const QRegularExpression kSentencePattern {R"(:|\.+\s)"};
54 
60 static inline bool ValidRecordingStatus(RecStatus::Type recstatus)
61 {
62  return (recstatus == RecStatus::Recording ||
63  recstatus == RecStatus::Tuning ||
64  recstatus == RecStatus::WillRecord ||
65  recstatus == RecStatus::Pending);
66 }
67 
73 void ActiveGame::setInfoUrl(QUrl url)
74 {
75  LOG(VB_GENERAL, LOG_DEBUG, LOC +
76  QString("setInfoUrl(%1)").arg(url.url()));
77  m_infoUrl = std::move(url);
78 }
79 
85 void ActiveGame::setGameUrl(QUrl url)
86 {
87  LOG(VB_GENERAL, LOG_DEBUG, LOC +
88  QString("setGameUrl(%1)").arg(url.url()));
89  m_gameUrl = std::move(url);
90 }
91 
101 bool ActiveGame::teamsMatch(const QStringList& names, const QStringList& abbrevs) const
102 {
103  // Exact name matches
104  if ((m_team1Normalized == names[0]) &&
105  (m_team2Normalized == names[1]))
106  return true;
107  if ((m_team1Normalized == names[1]) &&
108  (m_team2Normalized == names[0]))
109  return true;
110 
111  // One name or the other is shortened
112  if (((m_team1Normalized.contains(names[0])) ||
113  (names[0].contains(m_team1Normalized))) &&
114  ((m_team2Normalized.contains(names[1])) ||
115  names[1].contains(m_team2Normalized)))
116  return true;
117  if (((m_team1Normalized.contains(names[1])) ||
118  (names[1].contains(m_team1Normalized))) &&
119  ((m_team2Normalized.contains(names[0])) ||
120  names[0].contains(m_team2Normalized)))
121  return true;
122 
123  // Check abbrevs
124  if ((m_team1 == abbrevs[0]) && (m_team2 == abbrevs[1]))
125  return true;
126  return ((m_team1 == abbrevs[1]) && (m_team2 == abbrevs[0]));
127 }
128 
132 
137 bool RecExtDataPage::timeIsClose(const QDateTime& eventStart)
138 {
139  QDateTime now = getNow();
140  QDateTime past = now.addSecs(-kLookBackTime);
141  QDateTime future = now.addSecs( kLookForwardTime);
142 #if 0
143  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("past: %1.").arg(past.toString(Qt::ISODate)));
144  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("eventStart: %1.").arg(eventStart.toString(Qt::ISODate)));
145  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("future: %1.").arg(future.toString(Qt::ISODate)));
146  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("result is %1.")
147  .arg(((past < eventStart) && (eventStart < future)) ? "true" : "false"));
148 #endif
149  return ((past < eventStart) && (eventStart < future));
150 }
151 
159 QJsonObject RecExtDataPage::walkJsonPath(QJsonObject& object, const QStringList& path)
160 {
161  static QRegularExpression re { R"((\w+)\[(\d+)\])" };
162  QRegularExpressionMatch match;
163 
164  for (const QString& step : path)
165  {
166  if (step.contains(re, &match))
167  {
168  QString name = match.captured(1);
169  int index = match.captured(2).toInt();
170  if (!object.contains(name) || !object[name].isArray())
171  {
172  LOG(VB_GENERAL, LOG_ERR, LOC +
173  QString("Invalid json at %1 in path %2 (not an array)")
174  .arg(name, path.join('/')));
175  return {};
176  }
177  QJsonArray array = object[name].toArray();
178  if ((array.size() < index) || !array[index].isObject())
179  {
180  LOG(VB_GENERAL, LOG_ERR, LOC +
181  QString("Invalid json at %1[%2] in path %3 (invalid array)")
182  .arg(name).arg(index).arg(path.join('/')));
183  return {};
184  }
185  object = array[index].toObject();
186  }
187  else
188  {
189  if (!object.contains(step) || !object[step].isObject())
190  {
191  LOG(VB_GENERAL, LOG_ERR, LOC +
192  QString("Invalid json at %1 in path %2 (not an object)")
193  .arg(step, path.join('/')));
194  return {};
195  }
196  object = object[step].toObject();
197  }
198  }
199  return object;
200 }
201 
210 bool RecExtDataPage::getJsonInt(const QJsonObject& _object, QStringList& path, int& value)
211 {
212  if (path.empty())
213  return false;
214  QString key = path.takeLast();
215  QJsonObject object = _object;
216  if (!path.empty())
217  object = walkJsonPath(object, path);
218  if (object.isEmpty() || !object.contains(key) || !object[key].isDouble())
219  {
220  LOG(VB_GENERAL, LOG_DEBUG, LOC +
221  QString("invalid key: %1.").arg(path.join('/')));
222  return false;
223  }
224  value = object[key].toDouble();
225  return true;
226 }
227 
236 bool RecExtDataPage::getJsonInt(const QJsonObject& object, const QString& key, int& value)
237 {
238  QStringList list = key.split('/');
239  return getJsonInt(object, list, value);
240 }
241 
248 bool RecExtDataPage::getJsonString(const QJsonObject& _object, QStringList& path, QString& value)
249 {
250  if (path.empty())
251  return false;
252  QString key = path.takeLast();
253  QJsonObject object = _object;
254  if (!path.empty())
255  object = walkJsonPath(object, path);
256  if (object.isEmpty() || !object.contains(key) || !object[key].isString())
257  {
258  LOG(VB_GENERAL, LOG_DEBUG, LOC +
259  QString("invalid key: %1.").arg(path.join('/')));
260  return false;
261  }
262  value = object[key].toString();
263  return true;
264 }
265 
274 bool RecExtDataPage::getJsonString(const QJsonObject& object, const QString& key, QString& value)
275 {
276  QStringList list = key.split('/');
277  return getJsonString(object, list, value);
278 }
279 
286 bool RecExtDataPage::getJsonObject(const QJsonObject& _object, QStringList& path, QJsonObject& value)
287 {
288  if (path.empty())
289  return false;
290  QString key = path.takeLast();
291  QJsonObject object = _object;
292  if (!path.empty())
293  object = walkJsonPath(object, path);
294  if (object.isEmpty() || !object.contains(key) || !object[key].isObject())
295  {
296  LOG(VB_GENERAL, LOG_DEBUG, LOC +
297  QString("invalid key: %1.").arg(path.join('/')));
298  return false;
299  }
300  value = object[key].toObject();
301  return true;
302 }
303 
312 bool RecExtDataPage::getJsonObject(const QJsonObject& object, const QString& key, QJsonObject& value)
313 {
314  QStringList list = key.split('/');
315  return getJsonObject(object, list, value);
316 }
317 
324 bool RecExtDataPage::getJsonArray(const QJsonObject& object, const QString& key, QJsonArray& value)
325 {
326  if (!object.contains(key) || !object[key].isArray())
327  return false;
328  value = object[key].toArray();
329  return true;
330 }
331 
333 
335 QHash<QString,QJsonDocument> RecExtDataSource::s_downloadedJson {};
336 
339 {
340  s_downloadedJson.clear();
341 }
342 
350 static QString normalizeString(const QString& s)
351 {
352  QString result;
353 
354  QString norm = s.normalized(QString::NormalizationForm_D);
355  for (QChar c : std::as_const(norm))
356  {
357  switch (c.category())
358  {
359  case QChar::Mark_NonSpacing:
360  case QChar::Mark_SpacingCombining:
361  case QChar::Mark_Enclosing:
362  continue;
363  default:
364  result += c;
365  }
366  }
367 
368  // Possibly needed? Haven't seen a team name with a German eszett
369  // to know how they are handled by the api providers.
370  //result = result.replace("ß","ss");
371  return result.simplified();
372 }
373 
377 
378 // The URL to get the names of all the leagues in a given sport:
379 // https://site.api.espn.com/apis/site/v2/leagues/dropdown?sport=${sport}&limit=100
380 //
381 // The URL to get the names of all the teams in a league:
382 // http://site.api.espn.com/apis/site/v2/sports/${sport}/${league}/teams
383 
384 // The URL to retrieve schedules and scores.
385 // http://site.api.espn.com/apis/site/v2/sports/${sport}/${league}/scoreboard
386 // http://site.api.espn.com/apis/site/v2/sports/${sport}/${league}/scoreboard?dates=20180901
387 // http://sports.core.api.espn.com/v2/sports/${sport}/leagues/${league}/events/${eventId}/competitions/${eventId}/status
388 //
389 // Mens College Basketball (Group 50)
390 //
391 // All teams:
392 // http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams?groups=50&limit=500
393 //
394 // This only shows teams in the top 25:
395 // http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?date=20220126
396 //
397 // This shows all the scheduled games.
398 // http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?date=20220126&groups=50&limit=500
399 
400 static const QString espnInfoUrlFmt {"http://site.api.espn.com/apis/site/v2/sports/%1/%2/scoreboard"};
401 static const QString espnGameUrlFmt {"http://sports.core.api.espn.com/v2/sports/%1/leagues/%2/events/%3/competitions/%3/status"};
402 
404 const QList<RecExtEspnDataPage::GameStatus> RecExtEspnDataPage::kFinalStatuses {
405  FINAL, FORFEIT, CANCELLED, POSTPONED, SUSPENDED,
406  FORFEIT_HOME_TEAM, FORFEIT_AWAY_TEAM, ABANDONED, FULL_TIME,
407  PLAY_COMPLETE, OFFICIAL_EVENT_SHORTENED, RETIRED,
408  BYE, ESPNVOID, FINAL_SCORE_AFTER_EXTRA_TIME, FINAL_SCORE_AFTER_GOLDEN_GOAL,
409  FINAL_SCORE_AFTER_PENALTIES, END_EXTRA_TIME, FINAL_SCORE_ABANDONED,
410 };
411 
421 {
422  LOG(VB_GENERAL, LOG_DEBUG, LOC +
423  QString("Looking for match of %1/%2")
424  .arg(game.getTeam1(), game.getTeam2()));
425 
426  QJsonObject json = m_doc.object();
427  if (json.isEmpty())
428  return false;
429  if (!json.contains("events") || !json["events"].isArray())
430  {
431  LOG(VB_GENERAL, LOG_INFO, LOC +
432  QString("malformed json document, step %1.").arg(1));
433  return false;
434  }
435 
436  // Process the games
437  QJsonArray eventArray = json["events"].toArray();
438  for (const auto& eventValue : std::as_const(eventArray))
439  {
440  // Process info at the game level
441  if (!eventValue.isObject())
442  {
443  LOG(VB_GENERAL, LOG_INFO, LOC +
444  QString("malformed json document, step %1.").arg(2));
445  continue;
446  }
447  QJsonObject event = eventValue.toObject();
448 
449  // Top level info for a game
450  QString idStr {};
451  QString dateStr {};
452  QString gameTitle {};
453  QString gameShortTitle {};
454  if (!getJsonString(event, "id", idStr) ||
455  !getJsonString(event, "date", dateStr) ||
456  !getJsonString(event, "name", gameTitle) ||
457  !getJsonString(event, "shortName", gameShortTitle))
458  {
459  LOG(VB_GENERAL, LOG_INFO, LOC +
460  QString("malformed json document, step %1.").arg(3));
461  continue;
462  }
463  QStringList teamNames = gameTitle.split(kVersusPattern);
464  QStringList teamAbbrevs = gameShortTitle.split(kVersusPattern);
465  if ((teamNames.size() != 2) || (teamAbbrevs.size() != 2))
466  {
467  LOG(VB_GENERAL, LOG_INFO, LOC +
468  QString("malformed json document, step %1.").arg(4));
469  continue;
470  }
471  RecordingExtender::nameCleanup(game.getInfo(), teamNames[0], teamNames[1]);
472  if (!game.teamsMatch(teamNames, teamAbbrevs))
473  {
474  LOG(VB_GENERAL, LOG_DEBUG, LOC +
475  QString("Found %1 at %2 (%3 @ %4). Teams don't match.")
476  .arg(teamNames[0], teamNames[1],
477  teamAbbrevs[0], teamAbbrevs[1]));
478  continue;
479  }
480  QDateTime startTime = QDateTime::fromString(dateStr, Qt::ISODate);
481  if (!timeIsClose(startTime))
482  {
483  LOG(VB_GENERAL, LOG_INFO, LOC +
484  QString("Found '%1 vs %2' starting time %3 wrong")
485  .arg(game.getTeam1(), game.getTeam2(),
486  game.getStartTimeAsString()));
487  continue;
488  }
489 
490  // Found everthing we need.
491  game.setAbbrevs(teamAbbrevs);
492  game.setStartTime(startTime);
493  game.setGameUrl(getSource()->makeGameUrl(game, idStr));
494 
495  LOG(VB_GENERAL, LOG_DEBUG, LOC +
496  QString("Match: %1 at %2 (%3 @ %4), start %5.")
497  .arg(game.getTeam1(), game.getTeam2(),
498  game.getAbbrev1(), game.getAbbrev2(),
499  game.getStartTimeAsString()));
500  return true;
501  }
502  return false;
503 }
504 
517 {
518  LOG(VB_GENERAL, LOG_DEBUG, LOC +
519  QString("Parsing game score for %1/%2")
520  .arg(game.getTeam1(), game.getTeam2()));
521 
522  QJsonObject json = m_doc.object();
523  if (json.isEmpty())
524  return {};
525 
526  int period {-1};
527  QString typeId;
528  QString detail;
529  QString description;
530  if (!getJsonInt(json, "period", period) ||
531  !getJsonString(json, "type/id", typeId) ||
532  !getJsonString(json, "type/description", description) ||
533  !getJsonString(json, "type/detail", detail))
534  {
535  LOG(VB_GENERAL, LOG_INFO, LOC +
536  QString("malformed json document, step %1.").arg(5));
537  return {};
538  }
539  auto stateId = static_cast<GameStatus>(typeId.toInt());
540  bool gameOver = kFinalStatuses.contains(stateId);
541 
542  GameState state(game, period, gameOver);
543  QString extra;
544  if ((description == detail) || (description == "In Progress"))
545  extra = detail;
546  else
547  extra = QString ("%1: %2").arg(description, detail);
548  state.setTextState(extra);
549  LOG(VB_GENERAL, LOG_INFO, LOC +
550  QString("%1 at %2 (%3 @ %4), %5.")
551  .arg(game.getTeam1(), game.getTeam2(),
552  game.getAbbrev1(), game.getAbbrev2(), extra));
553  return state;
554 }
555 
565 RecExtEspnDataSource::loadPage(const ActiveGame& game, const QUrl& _url)
566 {
567  QString url = _url.url();
568 
569  // Return cached document
570  if (s_downloadedJson.contains(url))
571  {
572  LOG(VB_GENERAL, LOG_DEBUG, LOC +
573  QString("Using cached document for %1.").arg(url));
574  return newPage(s_downloadedJson[url]);
575  }
576 
577  QByteArray data;
578  bool ok {false};
579  QString scheme = _url.scheme();
580  if (scheme == QStringLiteral(u"file"))
581  {
582  QFile file(_url.path(QUrl::FullyDecoded));
583  ok = file.open(QIODevice::ReadOnly);
584  if (ok)
585  data = file.readAll();
586  }
587  else if ((scheme == QStringLiteral(u"http")) ||
588  (scheme == QStringLiteral(u"https")))
589  {
590  ok = GetMythDownloadManager()->download(url, &data);
591  }
592  if (!ok)
593  {
594  LOG(VB_GENERAL, LOG_INFO, LOC +
595  QString("\"%1\" couldn't download %2.")
596  .arg(game.getTitle(), url));
597  return nullptr;
598  }
599 
600  QJsonParseError error {};
601  QJsonDocument doc = QJsonDocument::fromJson(data, &error);
602  if (error.error != QJsonParseError::NoError)
603  {
604  LOG(VB_GENERAL, LOG_ERR, LOC +
605  QString("Error parsing %1 at offset %2: %3")
606  .arg(url).arg(error.offset).arg(error.errorString()));
607  return nullptr;
608  }
609 
610  QJsonObject json = doc.object();
611  if (json.contains("code") && json["code"].isDouble() &&
612  json.contains("detail") && json["detail"].isString())
613  {
614  LOG(VB_GENERAL, LOG_INFO, LOC +
615  QString("error downloading json document, code %1, detail %2.")
616  .arg(json["code"].toInt()).arg(json["detail"].toString()));
617  return nullptr;
618  }
619  s_downloadedJson[url] = doc;
620  return newPage(doc);
621 }
622 
629 QUrl RecExtEspnDataSource::makeInfoUrl (const SportInfo& info, const QDateTime& dt)
630 {
631  QUrl url {QString(espnInfoUrlFmt).arg(info.sport, info.league)};
632  QUrlQuery query;
633  query.addQueryItem("limit", "500");
634  // Add this to get all games, otherwise only top-25 games are returned.
635  if (info.league.endsWith("college-basketball"))
636  query.addQueryItem("group", "50");
637  if (dt.isValid())
638  query.addQueryItem("dates", dt.toString("yyyyMMdd"));
639  url.setQuery(query);
640  return url;
641 }
642 
649 QUrl RecExtEspnDataSource::makeGameUrl(const ActiveGame& game, const QString& str)
650 {
651  SportInfo info = game.getInfo();
652  QUrl gameUrl = QUrl(espnGameUrlFmt.arg(info.sport, info.league, str));
653  return gameUrl;
654 }
655 
671 {
672  // Find game with today's date (in UTC)
673  // Is the starting time close to now?
674  QDateTime now = MythDate::current();
675  game.setInfoUrl(makeInfoUrl(info, now));
676  RecExtDataPage* page = loadPage(game, game.getInfoUrl());
677  if (!page)
678  {
679  LOG(VB_GENERAL, LOG_INFO, LOC +
680  QString("Couldn't load %1").arg(game.getInfoUrl().url()));
681  return {};
682  }
683  if (page->findGameInfo(game))
684  {
685  LOG(VB_GENERAL, LOG_INFO, LOC +
686  QString("Found game '%1 vs %2' at %3")
687  .arg(game.getTeam1(), game.getTeam2(), game.getStartTimeAsString()));
688  return game.getInfoUrl();
689  }
690 
691  // Find game with yesterdays's date (in UTC)
692  // Handles evening games that start after 00:00 UTC. E.G. an 8pm EST football game.
693  // Is the starting time close to now?
694  game.setInfoUrl(makeInfoUrl(info, now.addDays(-1)));
695  page = loadPage(game, game.getInfoUrl());
696  if (!page)
697  {
698  LOG(VB_GENERAL, LOG_INFO, LOC +
699  QString("Couldn't load %1").arg(game.getInfoUrl().url()));
700  return {};
701  }
702  if (page->findGameInfo(game))
703  {
704  LOG(VB_GENERAL, LOG_INFO, LOC +
705  QString("Found game '%1 vs %2' at %3")
706  .arg(game.getTeam1(), game.getTeam2(), game.getStartTimeAsString()));
707  return game.getInfoUrl();
708  }
709 
710  // Find game with tomorrow's date (in UTC)
711  // E.G. Handles
712  // Is the starting time close to now?
713  game.setInfoUrl(makeInfoUrl(info, now.addDays(1)));
714  page = loadPage(game, game.getInfoUrl());
715  if (!page)
716  {
717  LOG(VB_GENERAL, LOG_INFO, LOC +
718  QString("Couldn't load %1").arg(game.getInfoUrl().url()));
719  return {};
720  }
721  if (page->findGameInfo(game))
722  {
723  LOG(VB_GENERAL, LOG_INFO, LOC +
724  QString("Found game '%1 vs %2' at %3")
725  .arg(game.getTeam1(), game.getTeam2(), game.getStartTimeAsString()));
726  return game.getInfoUrl();
727  }
728  return {};
729 }
730 
734 
735 // The MLB API is free for individual, non-commercial use. See
736 // http://gdx.mlb.com/components/copyright.txt
737 //
738 // Working queryable version of the API:
739 // https://beta-statsapi.mlb.com/docs/
740 //
741 // For schedule information:
742 // https://statsapi.mlb.com/api/v1/schedule?sportId=1&date=2021-09-18
743 // https://statsapi.mlb.com/api/v1/schedule?sportId=1&startDate=2021-09-18&endDate=2021-09-20"
744 //
745 // For game information:
746 // https://statsapi.mlb.com/api/v1.1/game/{game_pk}/feed/live
747 // where the game_pk comes from the schedule data.
748 //
749 // You can request extra data be returned by asking for "hydrations"
750 // (additional data) to be added to the response. The list of
751 // available hydrations for any API can be retrieved by adding
752 // "hydrate=hydrations" to the URL. For example:
753 // https://statsapi.mlb.com/api/v1/schedule?sportId=1&hydrate=hydrations&date=2021-09-18"
754 // https://statsapi.mlb.com/api/v1/schedule?sportId=1&hydrate=team&startDate=2021-09-18&endDate=2021-09-20"
755 
762 bool RecExtMlbDataPage::parseGameObject(const QJsonObject& gameObject,
763  ActiveGame& game)
764 {
765  QString dateStr {};
766  QString gameLink {};
767  QStringList teamNames {"", ""};
768  QStringList teamAbbrevs { "", ""};
769  if (!getJsonString(gameObject, "gameDate", dateStr) ||
770  !getJsonString(gameObject, "link", gameLink) ||
771  !getJsonString(gameObject, "teams/home/team/name", teamNames[0]) ||
772  !getJsonString(gameObject, "teams/away/team/name", teamNames[1]) ||
773  !getJsonString(gameObject, "teams/home/team/abbreviation", teamAbbrevs[0]) ||
774  !getJsonString(gameObject, "teams/away/team/abbreviation", teamAbbrevs[1]))
775  {
776  LOG(VB_GENERAL, LOG_INFO, LOC +
777  QString("malformed json document, step %1.").arg(1));
778  return false;
779  }
780 
781  RecordingExtender::nameCleanup(game.getInfo(), teamNames[0], teamNames[1]);
782  bool success = game.teamsMatch(teamNames, teamAbbrevs);
783  LOG(VB_GENERAL, LOG_DEBUG, LOC +
784  QString("Found: %1 at %2 (%3 @ %4), starting %5. (%6)")
785  .arg(teamNames[0], teamNames[1],
786  teamAbbrevs[0], teamAbbrevs[1], dateStr,
787  success ? "Success" : "Teams don't match"));
788  if (!success)
789  return false;
790 
791  // Found everthing we need.
792  game.setAbbrevs(teamAbbrevs);
793  game.setGameUrl(getSource()->makeGameUrl(game, gameLink));
795  return true;
796 }
797 
807 {
808  LOG(VB_GENERAL, LOG_DEBUG, LOC +
809  QString("Looking for match of %1/%2")
810  .arg(game.getTeam1(), game.getTeam2()));
811 
812  QJsonObject json = m_doc.object();
813  if (json.isEmpty())
814  return false;
815 
816  QJsonArray datesArray;
817  if (!getJsonArray(json, "dates", datesArray))
818  {
819  LOG(VB_GENERAL, LOG_INFO, LOC +
820  QString("malformed json document, step %1.").arg(1));
821  return false;
822  }
823 
824  // Process each of the three dates
825  for (const auto& dateValue : std::as_const(datesArray))
826  {
827  if (!dateValue.isObject())
828  {
829  LOG(VB_GENERAL, LOG_INFO, LOC +
830  QString("malformed json document, step %1.").arg(2));
831  continue;
832  }
833  QJsonObject dateObject = dateValue.toObject();
834 
835  QJsonArray gamesArray;
836  if (!getJsonArray(dateObject, "games", gamesArray))
837  {
838  LOG(VB_GENERAL, LOG_INFO, LOC +
839  QString("malformed json document, step %1.").arg(3));
840  continue;
841  }
842 
843  // Process each game on a given date
844  for (const auto& gameValue : std::as_const(gamesArray))
845  {
846  if (!gameValue.isObject())
847  {
848  LOG(VB_GENERAL, LOG_INFO, LOC +
849  QString("malformed json document, step %1.").arg(4));
850  continue;
851  }
852  QJsonObject gameObject = gameValue.toObject();
853 
854  if (!parseGameObject(gameObject, game))
855  continue;
856  bool match = timeIsClose(game.getStartTime());
857  LOG(VB_GENERAL, LOG_INFO, LOC +
858  QString("Found '%1 vs %2' starting %3 (%4)")
859  .arg(game.getTeam1(), game.getTeam2(), game.getStartTimeAsString(),
860  match ? "match" : "keep looking"));
861  if (!match)
862  continue;
863  return true;
864  }
865  }
866  return false;
867 }
868 
876 {
877  LOG(VB_GENERAL, LOG_DEBUG, LOC +
878  QString("Parsing game score for %1/%2")
879  .arg(game.getTeam1(), game.getTeam2()));
880 
881  QJsonObject json = m_doc.object();
882  if (json.isEmpty())
883  return {};
884 
885  int period {-1};
886  QString abstractGameState;
887  QString detailedGameState;
888  QString inningState;
889  QString inningOrdinal;
890  if (!getJsonString(json, "gameData/status/abstractGameState", abstractGameState) ||
891  !getJsonString(json, "gameData/status/detailedState", detailedGameState))
892  {
893  LOG(VB_GENERAL, LOG_INFO, LOC +
894  QString("malformed json document (%d)").arg(1));
895  return {};
896  }
897 
898  if (detailedGameState != "Scheduled")
899  {
900  if (!getJsonInt( json, "liveData/linescore/currentInning", period) ||
901  !getJsonString(json, "liveData/linescore/currentInningOrdinal", inningOrdinal) ||
902  !getJsonString(json, "liveData/linescore/inningState", inningState))
903  {
904  LOG(VB_GENERAL, LOG_INFO, LOC +
905  QString("malformed json document (%d)").arg(2));
906  return {};
907  }
908  }
909 
910  bool gameOver = (abstractGameState == "Final") ||
911  detailedGameState.contains("Suspended");
912  GameState state = GameState(game.getTeam1(), game.getTeam2(),
913  game.getAbbrev1(), game.getAbbrev2(),
914  period, gameOver);
915  QString extra;
916  if (gameOver)
917  extra = "game over";
918  else if (detailedGameState == "In Progress")
919  extra = QString("%1 %2").arg(inningState, inningOrdinal);
920  else
921  extra = detailedGameState;
922  state.setTextState(extra);
923  LOG(VB_GENERAL, LOG_DEBUG, LOC +
924  QString("%1 at %2 (%3 @ %4), %5.")
925  .arg(game.getTeam1(), game.getTeam2(),
926  game.getAbbrev1(), game.getAbbrev2(), extra));
927  return state;
928 }
929 
939 RecExtMlbDataSource::loadPage(const ActiveGame& game, const QUrl& _url)
940 {
941  QString url = _url.url();
942 
943  // Return cached document
944  if (s_downloadedJson.contains(url))
945  {
946  LOG(VB_GENERAL, LOG_DEBUG, LOC +
947  QString("Using cached document for %1.").arg(url));
948  return newPage(s_downloadedJson[url]);
949  }
950 
951  QByteArray data;
952  bool ok {false};
953  QString scheme = _url.scheme();
954  if (scheme == QStringLiteral(u"file"))
955  {
956  QFile file(_url.path(QUrl::FullyDecoded));
957  ok = file.open(QIODevice::ReadOnly);
958  if (ok)
959  data = file.readAll();
960  }
961  else if ((scheme == QStringLiteral(u"http")) ||
962  (scheme == QStringLiteral(u"https")))
963  {
964  ok = GetMythDownloadManager()->download(url, &data);
965  }
966  if (!ok)
967  {
968  LOG(VB_GENERAL, LOG_INFO, LOC +
969  QString("\"%1\" couldn't download %2.")
970  .arg(game.getTitle(), url));
971  return nullptr;
972  }
973 
974  QJsonParseError error {};
975  QJsonDocument doc = QJsonDocument::fromJson(data, &error);
976  if (error.error != QJsonParseError::NoError)
977  {
978  LOG(VB_GENERAL, LOG_ERR, LOC +
979  QString("Error parsing %1 at offset %2: %3")
980  .arg(url).arg(error.offset).arg(error.errorString()));
981  return nullptr;
982  }
983  s_downloadedJson[url] = doc;
984  return newPage(doc);
985 }
986 
993 QUrl RecExtMlbDataSource::makeInfoUrl ([[maybe_unused]] const SportInfo& info,
994  const QDateTime& dt)
995 {
996  if (!dt.isValid())
997  return {};
998 
999  QDateTime yesterday = dt.addDays(-1);
1000  QDateTime tomorrow = dt.addDays(+1);
1001  QUrl url {"https://statsapi.mlb.com/api/v1/schedule"};
1002  QUrlQuery query;
1003  query.addQueryItem("sportId", "1");
1004  query.addQueryItem("hydrate", "team");
1005  query.addQueryItem("startDate", QString("%1").arg(yesterday.toString("yyyy-MM-dd")));
1006  query.addQueryItem("endDate", QString("%1").arg(tomorrow.toString("yyyy-MM-dd")));
1007  url.setQuery(query);
1008  return url;
1009 }
1010 
1017 QUrl RecExtMlbDataSource::makeGameUrl (const ActiveGame& game, const QString& str)
1018 {
1019  QUrl gameUrl = game.getInfoUrl();
1020  gameUrl.setPath(str);
1021  gameUrl.setQuery(QString());
1022  return gameUrl;
1023 }
1024 
1038 {
1039  // Find game with today's date (in UTC)
1040  // Is the starting time close to now?
1041  QDateTime now = MythDate::current();
1042  game.setInfoUrl(makeInfoUrl(info, now));
1043  RecExtDataPage* page = loadPage(game, game.getInfoUrl());
1044  if (!page)
1045  {
1046  LOG(VB_GENERAL, LOG_INFO, LOC +
1047  QString("Couldn't load %1").arg(game.getInfoUrl().url()));
1048  return {};
1049  }
1050  LOG(VB_GENERAL, LOG_INFO, LOC +
1051  QString("Loaded page %1").arg(game.getInfoUrl().url()));
1052  if (page->findGameInfo(game))
1053  return game.getGameUrl();
1054 
1055  return {};
1056 }
1057 
1061 
1064 
1066 {
1068 }
1069 
1081 {
1082  RecordingRule *rr = ri.GetRecordingRule();
1084  {
1085  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1086  QString("Recording of %1 at %2 not marked for auto extend.")
1088  return;
1089  }
1090  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1091  QString("Adding %1 at %2 to new recordings list.")
1093 
1094  QMutexLocker lock(&s_createLock);
1095  if (!s_singleton)
1096  {
1098  s_singleton->m_scheduler = scheduler;
1099  s_singleton->start();
1100  s_singleton->moveToThread(s_singleton->qthread());
1101  }
1103 };
1104 
1110 {
1111  QMutexLocker lock(&m_newRecordingsLock);
1112  m_newRecordings.append(recordedID);
1113 }
1114 
1124 {
1125  switch (type)
1126  {
1127  default:
1128  return nullptr;
1129  case AutoExtendType::ESPN:
1130  return new RecExtEspnDataSource(this);
1131  case AutoExtendType::MLB:
1132  return new RecExtMlbDataSource(this);
1133  }
1134 }
1135 
1155 bool RecordingExtender::findKnownSport(const QString& _title,
1157  SportInfoList& infoList) const
1158 {
1159  static const QRegularExpression year {R"(\d{4})"};
1160  QRegularExpressionMatch match;
1161  QString title = _title;
1162  if (title.contains(year, &match))
1163  {
1164  bool ok {false};
1165  int matchYear = match.captured().toInt(&ok);
1166  int thisYear = m_forcedYearforTesting
1168  : QDateTime::currentDateTimeUtc().date().year();
1169  // FIFA Qualifiers can be in the year before the tournament.
1170  if (!ok || ((matchYear != thisYear) && (matchYear != thisYear+1)))
1171  return false;
1172  title = title.remove(match.capturedStart(), match.capturedLength());
1173  }
1174  title = title.simplified();
1175  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1176  QString("Looking for %1 title '%2")
1177  .arg(toString(type), title));
1178 
1179  MSqlQuery query(MSqlQuery::InitCon());
1180  query.prepare(
1181  "SELECT sl.title, api.provider, api.key1, api.key2" \
1182  " FROM sportslisting sl " \
1183  " INNER JOIN sportsapi api ON sl.api = api.id" \
1184  " WHERE api.provider = :PROVIDER AND :TITLE REGEXP sl.title");
1185  query.bindValue(":PROVIDER", static_cast<uint8_t>(type));
1186  query.bindValue(":TITLE", title);
1187  if (!query.exec())
1188  {
1189  MythDB::DBError("sportsapi() -- findKnownSport", query);
1190  return false;
1191  }
1192  while (query.next())
1193  {
1194  SportInfo info;
1195 
1196  info.showTitle = query.value(0).toString();
1197  info.dataProvider = type;
1198  info.sport = query.value(2).toString();
1199  info.league = query.value(3).toString();
1200  infoList.append(info);
1201 
1202  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1203  QString("Info: '%1' matches '%2' '%3' '%4' '%5'")
1204  .arg(title, toString(info.dataProvider), info.showTitle,
1205  info.sport, info.league));
1206  }
1207  return !infoList.isEmpty();
1208 }
1209 
1212 {
1214 }
1215 
1216 // Parse a single string. First split it into parts on a semi-colon or
1217 // 'period space', and then selectively check those parts for the
1218 // pattern "A vs B".
1219 static bool parseProgramString (const QString& string,
1220 #if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1221  int limit,
1222 #else
1223  qsizetype limit,
1224 #endif
1225  QString& team1, QString& team2)
1226 {
1227  QString lString = string;
1228  QStringList parts = lString.replace("vs.", "vs").split(kSentencePattern);
1229  for (int i = 0; i < std::min(limit,parts.size()); i++)
1230  {
1231  QStringList words = parts[i].split(kVersusPattern);
1232  if (words.size() == 2)
1233  {
1234  team1 = words[0].simplified();
1235  team2 = words[1].simplified();
1236  return true;
1237  }
1238  }
1239  return false;
1240 }
1241 
1250 bool RecordingExtender::parseProgramInfo (const QString& subtitle, const QString& description,
1251  QString& team1, QString& team2)
1252 {
1253  if (parseProgramString(subtitle, 2, team1, team2))
1254  return true;
1255  if (parseProgramString(description, 1, team1, team2))
1256  return true;
1257  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1258  QString("can't find team names in subtitle or description '%1'").arg(description));
1259  return false;
1260 }
1261 
1269 {
1270  if (rr->m_parentRecID)
1271  return QString("%1->%2").arg(rr->m_parentRecID).arg(rr->m_recordID);
1272  return QString::number(rr->m_recordID);
1273 }
1274 
1284 void RecordingExtender::nameCleanup(const SportInfo& info, QString& name)
1285 {
1286  name = normalizeString(name);
1287  name = name.simplified();
1288 
1289  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Start: %1").arg(name));
1290 
1291  // Ask the database for all the applicable cleanups
1292  MSqlQuery query(MSqlQuery::InitCon());
1293  query.prepare(
1294  "SELECT sc.name, sc.pattern, sc.nth, sc.replacement" \
1295  " FROM sportscleanup sc " \
1296  " WHERE (provider=0 or provider=:PROVIDER) " \
1297  " AND (key1='all' or key1=:SPORT) " \
1298  " AND (:NAME REGEXP pattern)" \
1299  " ORDER BY sc.weight");
1300  query.bindValue(":PROVIDER", static_cast<uint8_t>(info.dataProvider));
1301  query.bindValue(":SPORT", info.sport);
1302  query.bindValue(":NAME", name);
1303  if (!query.exec())
1304  {
1305  MythDB::DBError("sportscleanup() -- main query", query);
1306  return;
1307  }
1308 
1309  // Now apply each cleanup.
1310  while (query.next())
1311  {
1312  QString patternName = query.value(0).toString();
1313  QString patternStr = query.value(1).toString();
1314  int patternField = query.value(2).toInt();
1315  QString replacement = query.value(3).toString();
1316 
1317  QString original = name;
1318  QString tag {"no match"};
1319  QRegularExpressionMatch match;
1320  // Should always be true....
1321  if (name.contains(QRegularExpression(patternStr), &match) &&
1322  match.hasMatch())
1323  {
1324  QString capturedText = match.captured(patternField);
1325  name = name.replace(match.capturedStart(patternField),
1326  match.capturedLength(patternField),
1327  replacement);
1328  name = name.simplified();
1329  if (name.isEmpty())
1330  {
1331  name = original;
1332  tag = "fail";
1333  }
1334  else
1335  {
1336  tag = QString("matched '%1'").arg(capturedText);
1337  }
1338  }
1339  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1340  QString("pattern '%1', %2, now '%3'")
1341  .arg(patternName, tag, name));
1342  }
1343 }
1344 
1352 void RecordingExtender::nameCleanup(const SportInfo& info, QString& name1, QString& name2)
1353 {
1354  nameCleanup(info, name1);
1355  if (!name2.isEmpty())
1356  nameCleanup(info, name2);
1357 }
1358 
1370  const RecordingInfo *ri, RecordingRule *rr, ActiveGame const& game)
1371 {
1372  LOG(VB_GENERAL, LOG_INFO, LOC +
1373  QString("Recording %1 rule %2 for '%3 @ %4' has finished. Stop recording.")
1374  .arg(ri->GetRecordingID())
1375  .arg(ruleIdAsString(rr), game.getTeam1(), game.getTeam2()));
1376 
1377  MythEvent me(QString("STOP_RECORDING %1 %2")
1378  .arg(ri->GetChanID())
1380  gCoreContext->dispatch(me);
1381 }
1382 
1392  const RecordingInfo *ri, RecordingRule *rr, const ActiveGame& game)
1393 {
1394  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1395  QString("Recording %1 rule %2 for '%3 @ %4' scheduled to end soon. Extending recording.")
1396  .arg(ri->GetRecordingID())
1397  .arg(ruleIdAsString(rr), game.getTeam1(), game.getTeam2()));
1398 
1399  // Create an override to make it easy to clean up later.
1400  if (rr->m_type != kOverrideRecord)
1401  {
1402  rr->MakeOverride();
1403  rr->m_type = kOverrideRecord;
1404  }
1405  static const QString ae {"(Auto Extend)"};
1406  rr->m_subtitle = rr->m_subtitle.startsWith(ae)
1407  ? rr->m_subtitle
1408  : ae + ' ' + rr->m_subtitle;
1409 
1410  // Update the recording end time. The m_endOffset field is the
1411  // one that is used by the scheduler for timing. The others are
1412  // only updated for consistency.
1413  rr->m_endOffset += kExtensionTime.count();
1414  QDateTime oldDt = ri->GetRecordingEndTime();
1415  QDateTime newDt = oldDt.addSecs(kExtensionTimeInSec);
1416  rr->m_enddate = newDt.date();
1417  rr->m_endtime = newDt.time();
1418 
1419  // Update the RecordingRule and Save/Apply it.
1420  if (!rr->Save(true))
1421  {
1422  // Oops. Maybe the backend crashed and there's an old override
1423  // recording already in the table?
1424  LOG(VB_GENERAL, LOG_ERR, LOC +
1425  QString("Recording %1, couldn't save override rule for '%2 @ %3'.")
1426  .arg(ri->GetRecordingID()).arg(game.getTeam1(), game.getTeam2()));
1427  return;
1428  }
1429 
1430  // Debugging
1431  bool exists = m_overrideRules.contains(rr->m_recordID);
1432  LOG(VB_GENERAL, LOG_INFO, LOC +
1433  QString("Recording %1, %2 override rule %3 for '%4 @ %5' ending %6 -> %7.")
1434  .arg(ri->GetRecordingID())
1435  .arg(exists ? "updated" : "created",
1436  ruleIdAsString(rr), game.getTeam1(), game.getTeam2(),
1437  oldDt.toString(Qt::ISODate), newDt.toString(Qt::ISODate)));
1438 
1439  // Remember the new rule number for later cleanup.
1440  if (!exists)
1441  m_overrideRules.append(rr->m_recordID);
1442 }
1443 
1452  const RecordingInfo *ri, RecordingRule *rr, const ActiveGame& game)
1453 {
1454  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1455  QString("Recording %1 rule %2 for '%3 @ %4' ends %5.")
1456  .arg(ri->GetRecordingID())
1457  .arg(ruleIdAsString(rr), game.getTeam1(), game.getTeam2(),
1458  ri->GetRecordingEndTime().toString(Qt::ISODate)));
1459 }
1460 
1465 {
1466  m_overrideRules.clear();
1467 }
1468 
1473 {
1474  while (true)
1475  {
1476  QMutexLocker locker (&m_newRecordingsLock);
1477  if (m_newRecordings.isEmpty())
1478  break;
1479  int recordedID = m_newRecordings.takeFirst();
1480  locker.unlock();
1481 
1482  // Have to get this from the scheduler, otherwise we never see
1483  // the actual recording state.
1484  // WE OWN THIS POINTER.
1485  RecordingInfo *ri = m_scheduler->GetRecording(recordedID);
1486  if (nullptr == ri)
1487  {
1488  LOG(VB_GENERAL, LOG_INFO, LOC +
1489  QString("Couldn't get recording %1 from scheduler")
1490  .arg(recordedID));
1491  continue;
1492  }
1493 
1495  {
1496  LOG(VB_GENERAL, LOG_INFO, LOC +
1497  QString("Invalid status for '%1 : %2', status %3.")
1498  .arg(ri->GetTitle(), ri->GetSubtitle(),
1500  delete ri;
1501  continue;
1502  }
1503  RecordingRule *rr = ri->GetRecordingRule(); // owned by ri
1504 
1505  SportInfoList infoList;
1506  if (!findKnownSport(ri->GetTitle(), rr->m_autoExtend, infoList))
1507  {
1508  LOG(VB_GENERAL, LOG_INFO, LOC +
1509  QString("Unknown sport '%1' for provider %2")
1510  .arg(ri->GetTitle(), toString(rr->m_autoExtend)));
1511  delete ri;
1512  continue;
1513  }
1514 
1515  auto* source = createDataSource(rr->m_autoExtend);
1516  if (!source)
1517  {
1518  LOG(VB_GENERAL, LOG_INFO, LOC +
1519  QString("unable to create data source of type %1.")
1520  .arg(toString(rr->m_autoExtend)));
1521  delete ri;
1522  continue;
1523  }
1524 
1525  // Build the game data structure
1526  ActiveGame game(recordedID, ri->GetTitle());
1527  QString team1;
1528  QString team2;
1529  if (!parseProgramInfo(ri->GetSubtitle(), ri->GetDescription(),
1530  team1, team2))
1531  {
1532  LOG(VB_GENERAL, LOG_INFO, LOC +
1533  QString("Unable to find '%1 : %2', provider %3")
1534  .arg(ri->GetTitle(), ri->GetSubtitle(),
1535  toString(rr->m_autoExtend)));
1536  delete source;
1537  delete ri;
1538  continue;
1539  }
1540  game.setTeams(team1, team2);
1541 
1542  // Now try each of the returned sport APIs
1543  bool found {false};
1544  for (auto it = infoList.begin(); !found && it != infoList.end(); it++)
1545  {
1546  SportInfo info = *it;
1547  game.setInfo(info);
1548  nameCleanup(info, team1, team2);
1549  game.setTeamsNorm(team1, team2);
1550 
1551  source->findInfoUrl(game, info);
1552  if (game.getGameUrl().isEmpty())
1553  {
1554  LOG(VB_GENERAL, LOG_INFO, LOC +
1555  QString("unable to find data page for recording '%1 : %2'.")
1556  .arg(ri->GetTitle(), ri->GetSubtitle()));
1557  continue;
1558  }
1559  found = true;
1560  }
1561 
1562  if (found)
1563  m_activeGames.append(game);
1564  delete source;
1565  delete ri;
1566  }
1567 }
1568 
1574 {
1575  for (auto it = m_activeGames.begin(); it != m_activeGames.end(); )
1576  {
1577  ActiveGame game = *it;
1578  // Have to get this from the scheduler, otherwise we never see
1579  // the change from original to override recording rule.
1580  // WE OWN THIS POINTER.
1582  if (nullptr == _ri)
1583  {
1584  LOG(VB_GENERAL, LOG_INFO, LOC +
1585  QString("Couldn't get recording %1 from scheduler")
1586  .arg(game.getRecordedId()));
1587  it = m_activeGames.erase(it);
1588  continue;
1589  }
1590 
1591  // Simplify memory management
1592  auto ri = std::make_unique<RecordingInfo>(*_ri);
1593  delete _ri;
1594 
1595  if (!ValidRecordingStatus(ri->GetRecordingStatus()))
1596  {
1597  LOG(VB_GENERAL, LOG_INFO, LOC +
1598  QString("Invalid status for '%1 : %2', status %3.")
1599  .arg(ri->GetTitle(), ri->GetSubtitle(),
1600  RecStatus::toString(ri->GetRecordingStatus())));
1601  it = m_activeGames.erase(it);
1602  continue;
1603  }
1604 
1605  RecordingRule *rr = ri->GetRecordingRule(); // owned by ri
1606  auto* source = createDataSource(rr->m_autoExtend);
1607  if (nullptr == source)
1608  {
1609  LOG(VB_GENERAL, LOG_INFO, LOC +
1610  QString("Couldn't create source of type %1")
1611  .arg(toString(rr->m_autoExtend)));
1612  it++;
1613  delete source;
1614  continue;
1615  }
1616  auto* page = source->loadPage(game, game.getGameUrl());
1617  if (nullptr == page)
1618  {
1619  LOG(VB_GENERAL, LOG_INFO, LOC +
1620  QString("Couldn't load source %1, teams %2 and %3, url %4")
1621  .arg(toString(rr->m_autoExtend), game.getTeam1(), game.getTeam2(),
1622  game.getGameUrl().url()));
1623  it++;
1624  delete source;
1625  continue;
1626  }
1627  auto gameState = page->findGameScore(game);
1628  if (!gameState.isValid())
1629  {
1630  LOG(VB_GENERAL, LOG_INFO, LOC +
1631  QString("Game state for source %1, teams %2 and %3 is invalid")
1632  .arg(toString(rr->m_autoExtend), game.getTeam1(), game.getTeam2()));
1633  it++;
1634  delete source;
1635  continue;
1636  }
1637  if (gameState.isFinished())
1638  {
1639  finishRecording(ri.get(), rr, game);
1640  it = m_activeGames.erase(it);
1641  delete source;
1642  continue;
1643  }
1644  if (ri->GetScheduledEndTime() <
1646  {
1647  extendRecording(ri.get(), rr, game);
1648  it++;
1649  delete source;
1650  continue;
1651  }
1652  unchangedRecording(ri.get(), rr, game);
1653  it++;
1654  delete source;
1655  }
1656 }
1657 
1661 {
1662  QMutexLocker lock1(&s_createLock);
1663  QMutexLocker lock2(&m_newRecordingsLock);
1664 
1665  if (m_newRecordings.empty() && m_activeGames.empty())
1666  {
1667  m_running = false;
1668  s_singleton = nullptr;
1669  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1670  QString("Nothing left to do. Exiting."));
1671  return;
1672  }
1673 
1674  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1675  QString("%1 new recordings, %2 active recordings, %3 overrides.")
1676  .arg(m_newRecordings.size()).arg(m_activeGames.size())
1677  .arg(m_overrideRules.size()));
1678 }
1679 
1682 {
1683  RunProlog();
1684 
1685  while (m_running)
1686  {
1687  usleep(kExtensionTime); // cppcheck-suppress usleepCalled
1688 
1692 
1693  checkDone();
1694  }
1695 
1696  expireOverrides();
1697 
1698  RunEpilog();
1699  quit();
1700 }
Scheduler
Definition: scheduler.h:45
MSqlQuery::next
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:812
RecStatus::Type
Type
Definition: recordingstatus.h:16
RecordingExtender::clearDownloadedInfo
static void clearDownloadedInfo()
Clear all downloaded info.
Definition: recordingextender.cpp:1211
MSqlQuery
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:127
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:806
MythDate::toString
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:93
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:46
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:60
ActiveGame::setAbbrevs
void setAbbrevs(QStringList abbrevs)
Definition: recordingextender.h:79
RecExtDataSource::clearCache
static void clearCache()
Clear the downloaded document cache.
Definition: recordingextender.cpp:338
RecExtMlbDataPage::parseGameObject
bool parseGameObject(const QJsonObject &gameObject, ActiveGame &game)
MLB ///.
Definition: recordingextender.cpp:762
error
static void error(const char *str,...)
Definition: vbi.cpp:37
RecordingExtender::addNewRecording
void addNewRecording(int recordedID)
Add an item to the list of new recordings.
Definition: recordingextender.cpp:1109
RecExtMlbDataSource::newPage
RecExtDataPage * newPage(const QJsonDocument &doc) override
Definition: recordingextender.h:296
mythdb.h
RecordingRule::Save
bool Save(bool sendSig=true)
Definition: recordingrule.cpp:392
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:1017
ActiveGame::getInfo
SportInfo getInfo() const
Definition: recordingextender.h:59
ActiveGame::teamsMatch
bool teamsMatch(const QStringList &names, const QStringList &abbrevs) const
Do the supplied team names/abbrevs match this game.
Definition: recordingextender.cpp:101
RecordingExtender::RecordingExtender
RecordingExtender()
Definition: recordingextender.h:319
RecStatus::Tuning
@ Tuning
Definition: recordingstatus.h:22
LOC
#define LOC
Definition: recordingextender.cpp:43
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
RecExtEspnDataPage::GameStatus
GameStatus
Definition: recordingextender.h:195
ActiveGame::getTitle
QString getTitle() const
Definition: recordingextender.h:58
ProgramInfo::GetRecordingID
uint GetRecordingID(void) const
Definition: programinfo.h:450
RecExtEspnDataPage::findGameInfo
bool findGameInfo(ActiveGame &game) override
Parse a previously downloaded data page for a given sport.
Definition: recordingextender.cpp:420
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
xbmcvfs.exists
bool exists(str path)
Definition: xbmcvfs.py:51
MSqlQuery::value
QVariant value(int i) const
Definition: mythdbcon.h:204
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:1573
ActiveGame::setGameUrl
void setGameUrl(QUrl url)
Set the game status information URL.
Definition: recordingextender.cpp:85
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:159
MSqlQuery::exec
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:618
parseProgramString
static bool parseProgramString(const QString &string, qsizetype limit, QString &team1, QString &team2)
Definition: recordingextender.cpp:1219
RecordingExtender::processNewRecordings
void processNewRecordings()
Process the list of newly started sports recordings.
Definition: recordingextender.cpp:1472
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:92
ActiveGame::setInfoUrl
void setInfoUrl(QUrl url)
Set the game scheduling information URL.
Definition: recordingextender.cpp:73
ProgramInfo::GetRecordingEndTime
QDateTime GetRecordingEndTime(void) const
Approximate time the recording should have ended, did end, or is intended to end.
Definition: programinfo.h:413
RecordingExtender
Definition: recordingextender.h:307
scheduler.h
MythDate::current
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
ProgramInfo::GetRecordingStartTime
QDateTime GetRecordingStartTime(void) const
Approximate time the recording started.
Definition: programinfo.h:405
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:1155
RecordingExtender::run
void run(void) override
The main execution loop for the Recording Extender.
Definition: recordingextender.cpp:1681
RecExtMlbDataSource::findInfoUrl
QUrl findInfoUrl(ActiveGame &game, SportInfo &info) override
Find the right URL for a specific recording.
Definition: recordingextender.cpp:1037
RecStatus::WillRecord
@ WillRecord
Definition: recordingstatus.h:31
kLookForwardTime
static constexpr int64_t kLookForwardTime
Definition: recordingextender.cpp:47
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:248
SportInfoList
QList< SportInfo > SportInfoList
Definition: recordingextender.h:47
ProgramInfo::GetScheduledStartTime
QDateTime GetScheduledStartTime(void) const
The scheduled start time of program.
Definition: programinfo.h:391
mythlogging.h
ProgramInfo::GetRecordingStatus
RecStatus::Type GetRecordingStatus(void) const
Definition: programinfo.h:451
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:1268
RecExtDataPage::getJsonInt
static bool getJsonInt(const QJsonObject &object, QStringList &path, int &value)
Retrieve the specified integer from a json object.
Definition: recordingextender.cpp:210
RecExtEspnDataSource::findInfoUrl
QUrl findInfoUrl(ActiveGame &game, SportInfo &info) override
Find the right URL for a specific recording.
Definition: recordingextender.cpp:670
kSentencePattern
static const QRegularExpression kSentencePattern
Definition: recordingextender.cpp:53
espnInfoUrlFmt
static const QString espnInfoUrlFmt
ESPN ///.
Definition: recordingextender.cpp:400
recordingextender.h
RecordingExtender::createDataSource
virtual RecExtDataSource * createDataSource(AutoExtendType type)
Create a RecExtDataSource object for the specified service.
Definition: recordingextender.cpp:1123
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:550
ProgramInfo::GetTitle
QString GetTitle(void) const
Definition: programinfo.h:362
ProgramInfo::GetDescription
QString GetDescription(void) const
Definition: programinfo.h:366
MythDB::DBError
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:225
RecExtDataPage::getJsonArray
static bool getJsonArray(const QJsonObject &object, const QString &key, QJsonArray &value)
Retrieve the specified array from a json object.
Definition: recordingextender.cpp:324
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:1250
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:431
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:1784
RecordingExtender::checkDone
void checkDone()
Is there any remaining work? Check for both newly created recording and for active recordings.
Definition: recordingextender.cpp:1660
kOverrideRecord
@ kOverrideRecord
Definition: recordingtypes.h:28
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:350
ActiveGame::m_gameUrl
QUrl m_gameUrl
Definition: recordingextender.h:105
espnGameUrlFmt
static const QString espnGameUrlFmt
Definition: recordingextender.cpp:401
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:55
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:565
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:39
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:373
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:649
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:1080
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
MSqlQuery::bindValue
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:888
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:1352
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:137
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:875
RecordingRule::MakeOverride
bool MakeOverride(void)
Definition: recordingrule.cpp:370
RecExtDataPage::getJsonObject
static bool getJsonObject(const QJsonObject &object, QStringList &path, QJsonObject &value)
Retrieve a specific object from another json object.
Definition: recordingextender.cpp:286
azlyrics.info
dictionary info
Definition: azlyrics.py:7
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:1369
RecordingInfo::GetRecordingRule
RecordingRule * GetRecordingRule(void)
Returns the "record" field, creating it if necessary.
Definition: recordinginfo.cpp:930
kExtensionTimeInSec
static constexpr int kExtensionTimeInSec
Definition: recordingextender.cpp:50
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:993
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:1464
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:629
RecordingExtender::extendRecording
void extendRecording(const RecordingInfo *ri, RecordingRule *rr, const ActiveGame &game)
Extend the current recording by XX minutes.
Definition: recordingextender.cpp:1391
MythCoreContext::dispatch
void dispatch(const MythEvent &event)
Definition: mythcorecontext.cpp:1727
RecExtEspnDataPage::findGameScore
GameState findGameScore(ActiveGame &game) override
Parse the previously downloaded data page for a given game.
Definition: recordingextender.cpp:516
kExtensionTime
static constexpr std::chrono::minutes kExtensionTime
Definition: recordingextender.cpp:49
RecordingExtender::~RecordingExtender
~RecordingExtender() override
Definition: recordingextender.cpp:1065
kVersusPattern
static const QRegularExpression kVersusPattern
Definition: recordingextender.cpp:52
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:939
GetMythDownloadManager
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
Definition: mythdownloadmanager.cpp:146
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:837
ProgramInfo::GetSubtitle
QString GetSubtitle(void) const
Definition: programinfo.h:364
RecordingExtender::unchangedRecording
static void unchangedRecording(const RecordingInfo *ri, RecordingRule *rr, const ActiveGame &game)
Log that this recording hasn't changed.
Definition: recordingextender.cpp:1451