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