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
34#include "libmythbase/mythdb.h"
38
39// MythBackend
40#include "recordingextender.h"
41#include "scheduler.h"
42
43#define LOC QString("RecExt: ")
44
46static constexpr int64_t kLookBackTime { 3LL * 60 * 60 };
47static constexpr int64_t kLookForwardTime { 1LL * 60 * 60 };
48
49static constexpr std::chrono::minutes kExtensionTime {10};
50static constexpr int kExtensionTimeInSec {
51 (duration_cast<std::chrono::seconds>(kExtensionTime).count()) };
52static const QRegularExpression kVersusPattern {R"(\s(at|@|vs\.?)\s)"};
53static const QRegularExpression kSentencePattern {R"(:|\.+\s)"};
54
60static 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
74{
75 LOG(VB_GENERAL, LOG_DEBUG, LOC +
76 QString("setInfoUrl(%1)").arg(url.url()));
77 m_infoUrl = std::move(url);
78}
79
86{
87 LOG(VB_GENERAL, LOG_DEBUG, LOC +
88 QString("setGameUrl(%1)").arg(url.url()));
89 m_gameUrl = std::move(url);
90}
91
101bool 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
137bool 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
159QJsonObject 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
210bool 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
236bool RecExtDataPage::getJsonInt(const QJsonObject& object, const QString& key, int& value)
237{
238 QStringList list = key.split('/');
239 return getJsonInt(object, list, value);
240}
241
248bool 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
274bool RecExtDataPage::getJsonString(const QJsonObject& object, const QString& key, QString& value)
275{
276 QStringList list = key.split('/');
277 return getJsonString(object, list, value);
278}
279
286bool 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
312bool RecExtDataPage::getJsonObject(const QJsonObject& object, const QString& key, QJsonObject& value)
313{
314 QStringList list = key.split('/');
315 return getJsonObject(object, list, value);
316}
317
324bool 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
335QHash<QString,QJsonDocument> RecExtDataSource::s_downloadedJson {};
336
339{
340 s_downloadedJson.clear();
341}
342
350static 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
400static const QString espnInfoUrlFmt {"http://site.api.espn.com/apis/site/v2/sports/%1/%2/scoreboard"};
401static const QString espnGameUrlFmt {"http://sports.core.api.espn.com/v2/sports/%1/leagues/%2/events/%3/competitions/%3/status"};
402
404const 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
565RecExtEspnDataSource::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
629QUrl 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
649QUrl 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
762bool 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
939RecExtMlbDataSource::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
993QUrl 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
1017QUrl 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{
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;
1130 return new RecExtEspnDataSource(this);
1132 return new RecExtMlbDataSource(this);
1133 }
1134}
1135
1155bool 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
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 {
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".
1219static 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
1250bool 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
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
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
1352void 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())
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;
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
1697
1698 RunEpilog();
1699 quit();
1700}
void setTeams(QString team1, QString team2)
QString m_team1Normalized
void setGameUrl(QUrl url)
Set the game status information URL.
void setTeamsNorm(QString team1, QString team2)
QUrl getGameUrl() const
QDateTime getStartTime() const
QString getTitle() const
void setInfo(const SportInfo &info)
QString getTeam2() const
void setAbbrevs(QStringList abbrevs)
QString getAbbrev2() const
QString getStartTimeAsString() const
bool teamsMatch(const QStringList &names, const QStringList &abbrevs) const
Do the supplied team names/abbrevs match this game.
QString m_team2Normalized
int getRecordedId() const
QString getTeam1() const
void setInfoUrl(QUrl url)
Set the game scheduling information URL.
QString getAbbrev1() const
void setStartTime(const QDateTime &time)
SportInfo getInfo() const
QUrl getInfoUrl() const
void setTextState(QString text)
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:837
QVariant value(int i) const
Definition: mythdbcon.h:204
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:618
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:888
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:812
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:550
void RunProlog(void)
Sets up a thread, call this if you reimplement run().
Definition: mthread.cpp:196
static void usleep(std::chrono::microseconds time)
Definition: mthread.cpp:335
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:283
void quit(void)
calls exit(0)
Definition: mthread.cpp:295
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
Definition: mthread.cpp:209
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
void dispatch(const MythEvent &event)
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:226
bool download(const QString &url, const QString &dest, bool reload=false)
Downloads a URL to a file in blocking mode.
This class is used as a container for messages.
Definition: mythevent.h:17
uint GetChanID(void) const
This is the unique key used in the database to locate tuning information.
Definition: programinfo.h:373
uint GetRecordingID(void) const
Definition: programinfo.h:450
QString GetDescription(void) const
Definition: programinfo.h:366
QString GetTitle(void) const
Definition: programinfo.h:362
QDateTime GetRecordingStartTime(void) const
Approximate time the recording started.
Definition: programinfo.h:405
QDateTime GetScheduledStartTime(void) const
The scheduled start time of program.
Definition: programinfo.h:391
RecStatus::Type GetRecordingStatus(void) const
Definition: programinfo.h:451
QDateTime GetRecordingEndTime(void) const
Approximate time the recording should have ended, did end, or is intended to end.
Definition: programinfo.h:413
QString GetSubtitle(void) const
Definition: programinfo.h:364
virtual QDateTime getNow()
Get the current time. Overridden by the testing code.
virtual bool timeIsClose(const QDateTime &eventStart)
Base Classes ///.
static bool getJsonObject(const QJsonObject &object, QStringList &path, QJsonObject &value)
Retrieve a specific object from another json object.
static bool getJsonArray(const QJsonObject &object, const QString &key, QJsonArray &value)
Retrieve the specified array from a json object.
static QJsonObject walkJsonPath(QJsonObject &object, const QStringList &path)
Iterate through a json object and return the specified object.
static bool getJsonInt(const QJsonObject &object, QStringList &path, int &value)
Retrieve the specified integer from a json object.
virtual bool findGameInfo(ActiveGame &game)=0
static bool getJsonString(const QJsonObject &object, QStringList &path, QString &value)
Retrieve the specified string from a json object.
QJsonDocument m_doc
RecExtDataSource * getSource()
static void clearCache()
Clear the downloaded document cache.
static QHash< QString, QJsonDocument > s_downloadedJson
A cache of downloaded documents.
GameState findGameScore(ActiveGame &game) override
Parse the previously downloaded data page for a given game.
static const QList< GameStatus > kFinalStatuses
A list of the ESPN status that mean the game is over.
bool findGameInfo(ActiveGame &game) override
Parse a previously downloaded data page for a given sport.
RecExtDataPage * loadPage(const ActiveGame &game, const QUrl &_url) override
Download the data page for a game, and do some minimal validation.
QUrl findInfoUrl(ActiveGame &game, SportInfo &info) override
Find the right URL for a specific recording.
RecExtDataPage * newPage(const QJsonDocument &doc) override
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 ...
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.
GameState findGameScore(ActiveGame &game) override
Parse the previously downloaded data page for a given game.
bool parseGameObject(const QJsonObject &gameObject, ActiveGame &game)
MLB ///.
bool findGameInfo(ActiveGame &game) override
Parse a previously downloaded data page for a given sport.
QUrl findInfoUrl(ActiveGame &game, SportInfo &info) override
Find the right URL for a specific recording.
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.
RecExtDataPage * newPage(const QJsonDocument &doc) override
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...
RecExtDataPage * loadPage(const ActiveGame &game, const QUrl &_url) override
Download the data page for a game, and do some minimal validation.
static QString toString(RecStatus::Type recstatus, uint id)
Converts "recstatus" into a short (unreadable) string.
virtual RecExtDataSource * createDataSource(AutoExtendType type)
Create a RecExtDataSource object for the specified service.
static void clearDownloadedInfo()
Clear all downloaded info.
void processNewRecordings()
Process the list of newly started sports recordings.
QMutex m_newRecordingsLock
New recordings are added by the scheduler process and removed by this process.
uint m_forcedYearforTesting
Testing data.
bool findKnownSport(const QString &_title, AutoExtendType type, SportInfoList &info) const
Retrieve the db record for a sporting event on a specific provider.
bool m_running
Whether the RecordingExtender process is running.
void addNewRecording(int recordedID)
Add an item to the list of new recordings.
static QString ruleIdAsString(const RecordingRule *rr)
Quick helper function for printing recording rule numbers.
void expireOverrides()
Delete the list of the override rules that have been created by this instance of RecordingExtender.
static void unchangedRecording(const RecordingInfo *ri, RecordingRule *rr, const ActiveGame &game)
Log that this recording hasn't changed.
static void nameCleanup(const SportInfo &info, QString &name1, QString &name2)
Clean up two team names for comparison against the ESPN API.
QList< int > m_overrideRules
Recordings that have had an override rule creates.
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...
void processActiveRecordings()
Process the currently active sports recordings.
Scheduler * m_scheduler
Pointer to the scheduler.
static QMutex s_createLock
Interlock the scheduler thread crating this process, and this process determining whether it should c...
static bool parseProgramInfo(const QString &subtitle, const QString &description, QString &team1, QString &team2)
Parse a RecordingInfo to find the team names.
QList< int > m_newRecordings
Newly started recordings to process.
void run(void) override
The main execution loop for the Recording Extender.
QList< ActiveGame > m_activeGames
Currently ongoing games to track.
static void finishRecording(const RecordingInfo *ri, RecordingRule *rr, const ActiveGame &game)
Stop the current recording early.
void extendRecording(const RecordingInfo *ri, RecordingRule *rr, const ActiveGame &game)
Extend the current recording by XX minutes.
void checkDone()
Is there any remaining work? Check for both newly created recording and for active recordings.
static RecordingExtender * s_singleton
The single instance of a RecordingExtender.
Holds information on a TV Program one might wish to record.
Definition: recordinginfo.h:36
RecordingRule * GetRecordingRule(void)
Returns the "record" field, creating it if necessary.
Internal representation of a recording rule, mirrors the record table.
Definition: recordingrule.h:30
RecordingType m_type
int m_recordID
Unique Recording Rule ID.
Definition: recordingrule.h:71
bool MakeOverride(void)
bool Save(bool sendSig=true)
QString m_subtitle
Definition: recordingrule.h:80
AutoExtendType m_autoExtend
QMap< QString, ProgramInfo * > GetRecording(void) const override
Definition: scheduler.cpp:1785
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:93
@ ISODate
Default UTC.
Definition: mythdate.h:17
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:39
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
dictionary info
Definition: azlyrics.py:7
def error(message)
Definition: smolt.py:409
bool exists(str path)
Definition: xbmcvfs.py:51
#define LOC
static const QString espnInfoUrlFmt
ESPN ///.
static bool ValidRecordingStatus(RecStatus::Type recstatus)
Does this recording status indicate that the recording is still ongoing.
static constexpr int kExtensionTimeInSec
static QString normalizeString(const QString &s)
Remove all diacritical marks, etc., etc., from a string leaving just the base characters.
static const QString espnGameUrlFmt
static const QRegularExpression kSentencePattern
static constexpr std::chrono::minutes kExtensionTime
static bool parseProgramString(const QString &string, qsizetype limit, QString &team1, QString &team2)
static constexpr int64_t kLookForwardTime
static constexpr int64_t kLookBackTime
Does the specified time fall within -3/+1 hour from now?
static const QRegularExpression kVersusPattern
QList< SportInfo > SportInfoList
AutoExtendType
@ kOverrideRecord