termfc 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -5
  2. package/dist/cli.js +982 -166
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -39,7 +39,19 @@ var LABELS = {
39
39
  speed: "\uBC30\uC18D",
40
40
  recorded: "\uB179\uD654\uB428",
41
41
  loading: "\uBD88\uB7EC\uC624\uB294 \uC911...",
42
- noData: "\uB370\uC774\uD130\uB97C \uAC00\uC838\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4"
42
+ noData: "\uB370\uC774\uD130\uB97C \uAC00\uC838\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4",
43
+ groupStage: "\uC870\uBCC4\uB9AC\uADF8",
44
+ roundR32: "32\uAC15",
45
+ roundR16: "16\uAC15",
46
+ roundQF: "8\uAC15",
47
+ roundSF: "\uC900\uACB0\uC2B9",
48
+ roundThird: "3\uC704 \uACB0\uC815\uC804",
49
+ roundFinal: "\uACB0\uC2B9",
50
+ noBracketData: "\uB300\uC9C4\uD45C\uB97C \uAD6C\uC131\uD560 \uB300\uD68C \uC804\uCCB4 \uB370\uC774\uD130\uB97C \uAC00\uC838\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4",
51
+ standingsCols: ["\uACBD\uAE30", "\uC2B9", "\uBB34", "\uD328", "\uB4DD\uC2E4", "\uC2B9\uC810"],
52
+ kickoffIn: "\uD0A5\uC624\uD504\uAE4C\uC9C0",
53
+ waitingKickoff: "\uD0A5\uC624\uD504 \uB300\uAE30 \uC911 \u2014 \uC2DC\uC791\uD558\uBA74 \uC790\uB3D9\uC73C\uB85C \uC911\uACC4\uAC00 \uC2DC\uC791\uB429\uB2C8\uB2E4",
54
+ foldedNotice: "{n}\uAC1C\uC758 \uC77C\uBC18 \uC774\uBCA4\uD2B8 \uC811\uD798"
43
55
  },
44
56
  en: {
45
57
  live: "LIVE",
@@ -74,12 +86,42 @@ var LABELS = {
74
86
  speed: "speed",
75
87
  recorded: "recorded",
76
88
  loading: "Loading...",
77
- noData: "Failed to fetch data"
89
+ noData: "Failed to fetch data",
90
+ groupStage: "Group stage",
91
+ roundR32: "Round of 32",
92
+ roundR16: "Round of 16",
93
+ roundQF: "Quarter-finals",
94
+ roundSF: "Semi-finals",
95
+ roundThird: "Third place",
96
+ roundFinal: "Final",
97
+ noBracketData: "Couldn't fetch full-tournament data for the bracket",
98
+ standingsCols: ["P", "W", "D", "L", "GD", "Pts"],
99
+ kickoffIn: "Kick-off in",
100
+ waitingKickoff: "Waiting for kick-off \u2014 commentary starts automatically",
101
+ foldedNotice: "{n} routine events folded"
78
102
  }
79
103
  };
80
104
  function labels(lang) {
81
105
  return LABELS[lang];
82
106
  }
107
+ function roundLabel(kind, l) {
108
+ switch (kind) {
109
+ case "GROUP":
110
+ return l.groupStage;
111
+ case "R32":
112
+ return l.roundR32;
113
+ case "R16":
114
+ return l.roundR16;
115
+ case "QF":
116
+ return l.roundQF;
117
+ case "SF":
118
+ return l.roundSF;
119
+ case "THIRD":
120
+ return l.roundThird;
121
+ case "FINAL":
122
+ return l.roundFinal;
123
+ }
124
+ }
83
125
  function eventSentence(e, lang, ctx = {}) {
84
126
  const who = ctx.player;
85
127
  const team = ctx.team;
@@ -127,6 +169,8 @@ function eventSentence(e, lang, ctx = {}) {
127
169
  return "\uACBD\uAE30 \uC77C\uC2DC \uC911\uB2E8";
128
170
  case "RESUMED":
129
171
  return "\uACBD\uAE30 \uC7AC\uAC1C";
172
+ case "ADDED_TIME":
173
+ return "\uCD94\uAC00\uC2DC\uAC04 \uD45C\uC2DC";
130
174
  default:
131
175
  return "\uACBD\uAE30 \uC0C1\uD669";
132
176
  }
@@ -172,6 +216,8 @@ function eventSentence(e, lang, ctx = {}) {
172
216
  return "Match paused";
173
217
  case "RESUMED":
174
218
  return "Match resumed";
219
+ case "ADDED_TIME":
220
+ return "Added time signalled";
175
221
  default:
176
222
  return "Match event";
177
223
  }
@@ -197,6 +243,7 @@ var EVENT_ICON = {
197
243
  COIN_TOSS: "\u{1FA99}",
198
244
  BREAK: "\u{1F4A7}",
199
245
  RESUMED: "\u25B6\uFE0F ",
246
+ ADDED_TIME: "\u23F1 ",
200
247
  UNKNOWN: "\xB7"
201
248
  };
202
249
 
@@ -520,6 +567,20 @@ var ESPN_WC_SLUG = "fifa.world";
520
567
  function fmtDate(d) {
521
568
  return d.toISOString().slice(0, 10).replaceAll("-", "");
522
569
  }
570
+ function parseEspnStage(...texts) {
571
+ const s = texts.filter(Boolean).join(" ").toLowerCase();
572
+ if (!s) return {};
573
+ const grp = /group\s+([a-l])\b/.exec(s);
574
+ if (grp) return { stageKind: "GROUP", group: grp[1].toUpperCase() };
575
+ if (/round[\s-]of[\s-]32/.test(s)) return { stageKind: "R32" };
576
+ if (/round[\s-]of[\s-]16/.test(s)) return { stageKind: "R16" };
577
+ if (s.includes("quarter")) return { stageKind: "QF" };
578
+ if (s.includes("semi")) return { stageKind: "SF" };
579
+ if (s.includes("third")) return { stageKind: "THIRD" };
580
+ if (s.includes("final")) return { stageKind: "FINAL" };
581
+ if (s.includes("group")) return { stageKind: "GROUP" };
582
+ return {};
583
+ }
523
584
  var EspnProvider = class {
524
585
  constructor(league = ESPN_WC_SLUG, base = BASE) {
525
586
  this.league = league;
@@ -534,7 +595,19 @@ var EspnProvider = class {
534
595
  return `${this.base}/${this.league}/scoreboard?dates=${from}-${to}`;
535
596
  }
536
597
  async fetchSchedule(_lang) {
537
- const data = await getJson(this.scoreboardUrl());
598
+ return this.fetchScoreboard(this.scoreboardUrl());
599
+ }
600
+ /**
601
+ * One-day scoreboard around a specific match — used to lazily link an
602
+ * ESPN ref at watch time, when both team codes are finally known.
603
+ */
604
+ async fetchDaySchedule(day, _lang) {
605
+ return this.fetchScoreboard(
606
+ `${this.base}/${this.league}/scoreboard?dates=${fmtDate(day)}`
607
+ );
608
+ }
609
+ async fetchScoreboard(url) {
610
+ const data = await getJson(url);
538
611
  if (!Array.isArray(data?.events))
539
612
  throw new SchemaError("espn", "scoreboard events is not an array");
540
613
  return data.events.map((e) => this.normalizeEvent(e)).filter((m) => m !== null).sort((a, b) => a.kickoff.localeCompare(b.kickoff));
@@ -559,6 +632,10 @@ var EspnProvider = class {
559
632
  flag: flagEmoji(code)
560
633
  };
561
634
  };
635
+ const { stageKind, group } = parseEspnStage(
636
+ e.season?.slug,
637
+ comp.altGameNote
638
+ );
562
639
  return {
563
640
  id: String(e.id),
564
641
  stage: e.season?.slug ?? comp.altGameNote ?? "",
@@ -571,9 +648,25 @@ var EspnProvider = class {
571
648
  },
572
649
  phase,
573
650
  matchTime: status.displayClock,
651
+ ...stageKind ? { stageKind } : {},
652
+ ...group ? { group } : {},
574
653
  sourceRefs: { espn: { eventId: String(e.id) } }
575
654
  };
576
655
  }
656
+ async fetchLiveMatchIds(_lang) {
657
+ const today = fmtDate(/* @__PURE__ */ new Date());
658
+ const data = await getJson(
659
+ `${this.base}/${this.league}/scoreboard?dates=${today}`
660
+ );
661
+ if (!Array.isArray(data?.events))
662
+ throw new SchemaError("espn", "scoreboard events is not an array");
663
+ const ids = /* @__PURE__ */ new Set();
664
+ for (const e of data.events) {
665
+ const status = e.competitions?.[0]?.status ?? e.status ?? {};
666
+ if (status.type?.state === "in") ids.add(String(e.id));
667
+ }
668
+ return ids;
669
+ }
577
670
  async fetchMatchState(match, _lang) {
578
671
  const ref = match.sourceRefs.espn;
579
672
  if (!ref) return null;
@@ -690,6 +783,9 @@ var FailoverProvider = class {
690
783
  fetchTimeline(match, lang) {
691
784
  return this.call((p) => p.fetchTimeline(match, lang));
692
785
  }
786
+ fetchLiveMatchIds(lang) {
787
+ return this.call((p) => p.fetchLiveMatchIds(lang));
788
+ }
693
789
  async call(fn) {
694
790
  try {
695
791
  const result = await fn(this.active);
@@ -762,6 +858,8 @@ var fifa_event_map_default = {
762
858
  // src/data/fifa.ts
763
859
  var BASE2 = "https://api.fifa.com/api/v3";
764
860
  var WORLD_CUP_2026 = { idCompetition: "17", idSeason: "285023" };
861
+ var TOURNAMENT_FROM = "2026-06-09T00:00:00Z";
862
+ var TOURNAMENT_TO = "2026-07-21T00:00:00Z";
765
863
  function apiLang(lang) {
766
864
  return lang === "ko" ? "ko" : "en";
767
865
  }
@@ -770,6 +868,33 @@ function isoFloorHour(ms) {
770
868
  d.setUTCMinutes(0, 0, 0);
771
869
  return d.toISOString().replace(/\.\d{3}Z$/, "Z");
772
870
  }
871
+ var STAGE_KIND_BY_ID = {
872
+ "289273": "GROUP",
873
+ "289287": "R32",
874
+ "289288": "R16",
875
+ "289289": "QF",
876
+ "289290": "SF",
877
+ "289291": "THIRD",
878
+ "289292": "FINAL"
879
+ };
880
+ function stageKindOf(idStage, matchNumber) {
881
+ const byId = STAGE_KIND_BY_ID[String(idStage)];
882
+ if (byId) return byId;
883
+ if (matchNumber === void 0) return void 0;
884
+ if (matchNumber >= 1 && matchNumber <= 72) return "GROUP";
885
+ if (matchNumber <= 88) return "R32";
886
+ if (matchNumber <= 96) return "R16";
887
+ if (matchNumber <= 100) return "QF";
888
+ if (matchNumber <= 102) return "SF";
889
+ if (matchNumber === 103) return "THIRD";
890
+ if (matchNumber === 104) return "FINAL";
891
+ return void 0;
892
+ }
893
+ function groupLetter(desc) {
894
+ if (!desc) return void 0;
895
+ const m = /Group\s+([A-L])\b/i.exec(desc) ?? /([A-L])\s*조/.exec(desc);
896
+ return m ? m[1].toUpperCase() : void 0;
897
+ }
773
898
  function toTeam(raw, placeholder) {
774
899
  if (!raw) return { code: "TBD", name: placeholder || "TBD", flag: "\u{1F3F3}\uFE0F" };
775
900
  const code = raw.Abbreviation ?? "TBD";
@@ -781,6 +906,9 @@ function normalizeMatch(m) {
781
906
  const phase = mapFifaPhase(m.MatchStatus, m.Period, minute);
782
907
  const home = toTeam(m.Home, m.PlaceHolderA ?? "TBD");
783
908
  const away = toTeam(m.Away, m.PlaceHolderB ?? "TBD");
909
+ const matchNumber = typeof m.MatchNumber === "number" ? m.MatchNumber : void 0;
910
+ const stageKind = stageKindOf(m.IdStage, matchNumber);
911
+ const group = groupLetter(m.GroupName?.[0]?.Description);
784
912
  const match = {
785
913
  id: String(m.IdMatch),
786
914
  stage: m.StageName?.[0]?.Description ?? "",
@@ -797,6 +925,9 @@ function normalizeMatch(m) {
797
925
  },
798
926
  phase,
799
927
  matchTime: m.MatchTime || void 0,
928
+ ...matchNumber !== void 0 ? { matchNumber } : {},
929
+ ...stageKind ? { stageKind } : {},
930
+ ...group ? { group } : {},
800
931
  sourceRefs: {
801
932
  fifa: {
802
933
  idCompetition: String(m.IdCompetition),
@@ -835,11 +966,8 @@ var FifaProvider = class {
835
966
  base;
836
967
  name = "fifa";
837
968
  async fetchSchedule(lang) {
838
- const now = Date.now();
839
- const from = isoFloorHour(now - 36 * 36e5);
840
- const to = isoFloorHour(now + 8 * 864e5 + 36e5);
841
969
  const data = await getJson(
842
- `${this.base}/calendar/matches?from=${from}&to=${to}&idCompetition=${this.season.idCompetition}&idSeason=${this.season.idSeason}&count=200&language=${apiLang(lang)}`
970
+ `${this.base}/calendar/matches?from=${TOURNAMENT_FROM}&to=${TOURNAMENT_TO}&idCompetition=${this.season.idCompetition}&idSeason=${this.season.idSeason}&count=200&language=${apiLang(lang)}`
843
971
  );
844
972
  if (!Array.isArray(data?.Results))
845
973
  throw new SchemaError("fifa", "calendar Results is not an array");
@@ -847,6 +975,14 @@ var FifaProvider = class {
847
975
  (a, b) => a.kickoff.localeCompare(b.kickoff)
848
976
  );
849
977
  }
978
+ async fetchLiveMatchIds(lang) {
979
+ const live = await getJson(
980
+ `${this.base}/live/football/now?language=${apiLang(lang)}`
981
+ );
982
+ if (!Array.isArray(live?.Results))
983
+ throw new SchemaError("fifa", "live/now Results is not an array");
984
+ return new Set(live.Results.map((r) => String(r.IdMatch)));
985
+ }
850
986
  async fetchMatchState(match, lang) {
851
987
  const ref = match.sourceRefs.fifa;
852
988
  if (!ref) return null;
@@ -1045,7 +1181,7 @@ var demo_match_default = {
1045
1181
  period: 2,
1046
1182
  teamSide: "away",
1047
1183
  teamCode: "ESP",
1048
- text: "\uC2A4\uD398\uC778 \uD300\uC774 \uB3D9\uC804 \uB358\uC9C0\uAE30\uC5D0\uC11C \uC2B9\uB9AC\uD574 \uC120\uCD95\uC744 \uC120\uD0DD\uD588\uB2E4",
1184
+ text: "\uB3D9\uC804 \uB358\uC9C0\uAE30 \u2014 \uC2A4\uD398\uC778 \uC120\uCD95",
1049
1185
  source: "fifa",
1050
1186
  seq: 0
1051
1187
  },
@@ -1054,7 +1190,7 @@ var demo_match_default = {
1054
1190
  type: "PERIOD_START",
1055
1191
  minute: 0,
1056
1192
  period: 3,
1057
- text: "\uC8FC\uC2EC\uC774 \uC804\uBC18\uC804 \uC2DC\uC791 \uD718\uC2AC\uC744 \uBD88\uC5C8\uC2B5\uB2C8\uB2E4.",
1193
+ text: "\uC804\uBC18 \uC2DC\uC791",
1058
1194
  source: "fifa",
1059
1195
  seq: 1
1060
1196
  },
@@ -1066,7 +1202,7 @@ var demo_match_default = {
1066
1202
  teamSide: "away",
1067
1203
  teamCode: "ESP",
1068
1204
  player: "Lamine YAMAL",
1069
- text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1205
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1070
1206
  source: "fifa",
1071
1207
  seq: 2
1072
1208
  },
@@ -1078,7 +1214,7 @@ var demo_match_default = {
1078
1214
  teamSide: "away",
1079
1215
  teamCode: "ESP",
1080
1216
  player: "Mikel OYARZABAL",
1081
- text: "Mikel OYARZABAL (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1217
+ text: "Mikel OYARZABAL (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1082
1218
  source: "fifa",
1083
1219
  seq: 3
1084
1220
  },
@@ -1089,7 +1225,7 @@ var demo_match_default = {
1089
1225
  period: 3,
1090
1226
  teamSide: "home",
1091
1227
  teamCode: "POR",
1092
- text: "\uD3EC\uB974\uD22C\uAC08 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1228
+ text: "\uD3EC\uB974\uD22C\uAC08 \uACE8\uD0A4\uD37C \uC120\uBC29",
1093
1229
  source: "fifa",
1094
1230
  seq: 4
1095
1231
  },
@@ -1101,7 +1237,7 @@ var demo_match_default = {
1101
1237
  teamSide: "home",
1102
1238
  teamCode: "POR",
1103
1239
  player: "JOAO CANCELO",
1104
- text: "JOAO CANCELO (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1240
+ text: "JOAO CANCELO (\uD3EC\uB974\uD22C\uAC08) \uC288\uD305 \uC2DC\uB3C4",
1105
1241
  source: "fifa",
1106
1242
  seq: 5
1107
1243
  },
@@ -1113,7 +1249,7 @@ var demo_match_default = {
1113
1249
  teamSide: "away",
1114
1250
  teamCode: "ESP",
1115
1251
  player: "Mikel OYARZABAL",
1116
- text: "Mikel OYARZABAL (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1252
+ text: "Mikel OYARZABAL (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1117
1253
  source: "fifa",
1118
1254
  seq: 6
1119
1255
  },
@@ -1124,7 +1260,7 @@ var demo_match_default = {
1124
1260
  period: 3,
1125
1261
  teamSide: "home",
1126
1262
  teamCode: "POR",
1127
- text: "BRUNO FERNANDES (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1263
+ text: "BRUNO FERNANDES (\uD3EC\uB974\uD22C\uAC08) \uCF54\uB108\uD0A5",
1128
1264
  source: "fifa",
1129
1265
  seq: 7
1130
1266
  },
@@ -1136,7 +1272,7 @@ var demo_match_default = {
1136
1272
  teamSide: "home",
1137
1273
  teamCode: "POR",
1138
1274
  player: "PEDRO NETO",
1139
- text: "PEDRO NETO (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1275
+ text: "PEDRO NETO (\uD3EC\uB974\uD22C\uAC08) \uC288\uD305 \uC2DC\uB3C4",
1140
1276
  source: "fifa",
1141
1277
  seq: 8
1142
1278
  },
@@ -1148,7 +1284,7 @@ var demo_match_default = {
1148
1284
  teamSide: "home",
1149
1285
  teamCode: "POR",
1150
1286
  player: "JOAO FELIX",
1151
- text: "JOAO FELIX (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1287
+ text: "JOAO FELIX (\uD3EC\uB974\uD22C\uAC08) \uD30C\uC6B8",
1152
1288
  source: "fifa",
1153
1289
  seq: 9
1154
1290
  },
@@ -1160,7 +1296,7 @@ var demo_match_default = {
1160
1296
  teamSide: "home",
1161
1297
  teamCode: "POR",
1162
1298
  player: "CRISTIANO RONALDO",
1163
- text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1299
+ text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08) \uC288\uD305 \uC2DC\uB3C4",
1164
1300
  source: "fifa",
1165
1301
  seq: 10
1166
1302
  },
@@ -1171,7 +1307,7 @@ var demo_match_default = {
1171
1307
  period: 3,
1172
1308
  teamSide: "away",
1173
1309
  teamCode: "ESP",
1174
- text: "\uC2A4\uD398\uC778 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1310
+ text: "\uC2A4\uD398\uC778 \uACE8\uD0A4\uD37C \uC120\uBC29",
1175
1311
  source: "fifa",
1176
1312
  seq: 11
1177
1313
  },
@@ -1182,7 +1318,7 @@ var demo_match_default = {
1182
1318
  period: 3,
1183
1319
  teamSide: "home",
1184
1320
  teamCode: "POR",
1185
- text: "NUNO MENDES (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1321
+ text: "NUNO MENDES (\uD3EC\uB974\uD22C\uAC08) \uCF54\uB108\uD0A5",
1186
1322
  source: "fifa",
1187
1323
  seq: 12
1188
1324
  },
@@ -1194,7 +1330,7 @@ var demo_match_default = {
1194
1330
  teamSide: "away",
1195
1331
  teamCode: "ESP",
1196
1332
  player: "Alex BAENA",
1197
- text: "Alex BAENA (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1333
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1198
1334
  source: "fifa",
1199
1335
  seq: 13
1200
1336
  },
@@ -1206,7 +1342,7 @@ var demo_match_default = {
1206
1342
  teamSide: "away",
1207
1343
  teamCode: "ESP",
1208
1344
  player: "Lamine YAMAL",
1209
- text: "Lamine YAMAL (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1345
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1210
1346
  source: "fifa",
1211
1347
  seq: 14
1212
1348
  },
@@ -1217,7 +1353,7 @@ var demo_match_default = {
1217
1353
  period: 3,
1218
1354
  teamSide: "home",
1219
1355
  teamCode: "POR",
1220
- text: "\uD3EC\uB974\uD22C\uAC08 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1356
+ text: "\uD3EC\uB974\uD22C\uAC08 \uACE8\uD0A4\uD37C \uC120\uBC29",
1221
1357
  source: "fifa",
1222
1358
  seq: 15
1223
1359
  },
@@ -1229,7 +1365,7 @@ var demo_match_default = {
1229
1365
  teamSide: "away",
1230
1366
  teamCode: "ESP",
1231
1367
  player: "Alex BAENA",
1232
- text: "Alex BAENA (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1368
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1233
1369
  source: "fifa",
1234
1370
  seq: 16
1235
1371
  },
@@ -1240,7 +1376,7 @@ var demo_match_default = {
1240
1376
  period: 3,
1241
1377
  teamSide: "home",
1242
1378
  teamCode: "POR",
1243
- text: "\uD3EC\uB974\uD22C\uAC08 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1379
+ text: "\uD3EC\uB974\uD22C\uAC08 \uACE8\uD0A4\uD37C \uC120\uBC29",
1244
1380
  source: "fifa",
1245
1381
  seq: 17
1246
1382
  },
@@ -1251,7 +1387,7 @@ var demo_match_default = {
1251
1387
  period: 3,
1252
1388
  teamSide: "away",
1253
1389
  teamCode: "ESP",
1254
- text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1390
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uCF54\uB108\uD0A5",
1255
1391
  source: "fifa",
1256
1392
  seq: 18
1257
1393
  },
@@ -1263,7 +1399,7 @@ var demo_match_default = {
1263
1399
  teamSide: "away",
1264
1400
  teamCode: "ESP",
1265
1401
  player: "Alex BAENA",
1266
- text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1402
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1267
1403
  source: "fifa",
1268
1404
  seq: 19
1269
1405
  },
@@ -1275,7 +1411,7 @@ var demo_match_default = {
1275
1411
  teamSide: "home",
1276
1412
  teamCode: "POR",
1277
1413
  player: "BRUNO FERNANDES",
1278
- text: "BRUNO FERNANDES (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218, \uC624\uD504\uC0AC\uC774\uB4DC\uB85C \uD310\uC815\uB429\uB2C8\uB2E4.",
1414
+ text: "BRUNO FERNANDES (\uD3EC\uB974\uD22C\uAC08) \uC624\uD504\uC0AC\uC774\uB4DC",
1279
1415
  source: "fifa",
1280
1416
  seq: 20
1281
1417
  },
@@ -1287,7 +1423,7 @@ var demo_match_default = {
1287
1423
  teamSide: "home",
1288
1424
  teamCode: "POR",
1289
1425
  player: "JOAO CANCELO",
1290
- text: "JOAO CANCELO (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1426
+ text: "JOAO CANCELO (\uD3EC\uB974\uD22C\uAC08) \uD30C\uC6B8",
1291
1427
  source: "fifa",
1292
1428
  seq: 21
1293
1429
  },
@@ -1299,7 +1435,7 @@ var demo_match_default = {
1299
1435
  teamSide: "away",
1300
1436
  teamCode: "ESP",
1301
1437
  player: "Pedro PORRO",
1302
- text: "Pedro PORRO (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1438
+ text: "Pedro PORRO (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1303
1439
  source: "fifa",
1304
1440
  seq: 22
1305
1441
  },
@@ -1308,7 +1444,7 @@ var demo_match_default = {
1308
1444
  type: "BREAK",
1309
1445
  minute: 23,
1310
1446
  period: 3,
1311
- text: "\uC218\uBD84 \uBCF4\uCDA9\uC744 \uC704\uD55C \uD734\uC2DD\uC73C\uB85C \uACBD\uAE30 \uC911\uB2E8",
1447
+ text: "\uC218\uBD84 \uC12D\uCDE8 \uD734\uC2DD \u2014 \uACBD\uAE30 \uC77C\uC2DC \uC911\uB2E8",
1312
1448
  source: "fifa",
1313
1449
  seq: 23
1314
1450
  },
@@ -1317,7 +1453,7 @@ var demo_match_default = {
1317
1453
  type: "RESUMED",
1318
1454
  minute: 26,
1319
1455
  period: 3,
1320
- text: "\uC911\uB2E8 \uD6C4 \uACBD\uAE30 \uC7AC\uAC1C",
1456
+ text: "\uACBD\uAE30 \uC7AC\uAC1C",
1321
1457
  source: "fifa",
1322
1458
  seq: 24
1323
1459
  },
@@ -1329,7 +1465,7 @@ var demo_match_default = {
1329
1465
  teamSide: "home",
1330
1466
  teamCode: "POR",
1331
1467
  player: "PEDRO NETO",
1332
- text: "PEDRO NETO (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1468
+ text: "PEDRO NETO (\uD3EC\uB974\uD22C\uAC08) \uD30C\uC6B8",
1333
1469
  source: "fifa",
1334
1470
  seq: 25
1335
1471
  },
@@ -1340,7 +1476,7 @@ var demo_match_default = {
1340
1476
  period: 3,
1341
1477
  teamSide: "away",
1342
1478
  teamCode: "ESP",
1343
- text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1479
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uCF54\uB108\uD0A5",
1344
1480
  source: "fifa",
1345
1481
  seq: 26
1346
1482
  },
@@ -1352,7 +1488,7 @@ var demo_match_default = {
1352
1488
  teamSide: "away",
1353
1489
  teamCode: "ESP",
1354
1490
  player: "Dani OLMO",
1355
- text: "Dani OLMO (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1491
+ text: "Dani OLMO (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1356
1492
  source: "fifa",
1357
1493
  seq: 27
1358
1494
  },
@@ -1364,7 +1500,7 @@ var demo_match_default = {
1364
1500
  teamSide: "away",
1365
1501
  teamCode: "ESP",
1366
1502
  player: "Aymeric LAPORTE",
1367
- text: "Aymeric LAPORTE (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1503
+ text: "Aymeric LAPORTE (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1368
1504
  source: "fifa",
1369
1505
  seq: 28
1370
1506
  },
@@ -1376,7 +1512,7 @@ var demo_match_default = {
1376
1512
  teamSide: "away",
1377
1513
  teamCode: "ESP",
1378
1514
  player: "Lamine YAMAL",
1379
- text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1515
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1380
1516
  source: "fifa",
1381
1517
  seq: 29
1382
1518
  },
@@ -1388,7 +1524,7 @@ var demo_match_default = {
1388
1524
  teamSide: "home",
1389
1525
  teamCode: "POR",
1390
1526
  player: "CRISTIANO RONALDO",
1391
- text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1527
+ text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08) \uC288\uD305 \uC2DC\uB3C4",
1392
1528
  source: "fifa",
1393
1529
  seq: 30
1394
1530
  },
@@ -1399,7 +1535,7 @@ var demo_match_default = {
1399
1535
  period: 3,
1400
1536
  teamSide: "away",
1401
1537
  teamCode: "ESP",
1402
- text: "\uC2A4\uD398\uC778 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1538
+ text: "\uC2A4\uD398\uC778 \uACE8\uD0A4\uD37C \uC120\uBC29",
1403
1539
  source: "fifa",
1404
1540
  seq: 31
1405
1541
  },
@@ -1411,7 +1547,7 @@ var demo_match_default = {
1411
1547
  teamSide: "away",
1412
1548
  teamCode: "ESP",
1413
1549
  player: "Pedro PORRO",
1414
- text: "Pedro PORRO (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1550
+ text: "Pedro PORRO (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1415
1551
  source: "fifa",
1416
1552
  seq: 32
1417
1553
  },
@@ -1422,7 +1558,7 @@ var demo_match_default = {
1422
1558
  period: 3,
1423
1559
  teamSide: "home",
1424
1560
  teamCode: "POR",
1425
- text: "NUNO MENDES (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1561
+ text: "NUNO MENDES (\uD3EC\uB974\uD22C\uAC08) \uCF54\uB108\uD0A5",
1426
1562
  source: "fifa",
1427
1563
  seq: 33
1428
1564
  },
@@ -1434,7 +1570,7 @@ var demo_match_default = {
1434
1570
  teamSide: "home",
1435
1571
  teamCode: "POR",
1436
1572
  player: "NUNO MENDES",
1437
- text: "NUNO MENDES (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1573
+ text: "NUNO MENDES (\uD3EC\uB974\uD22C\uAC08) \uC288\uD305 \uC2DC\uB3C4",
1438
1574
  source: "fifa",
1439
1575
  seq: 34
1440
1576
  },
@@ -1446,7 +1582,7 @@ var demo_match_default = {
1446
1582
  teamSide: "away",
1447
1583
  teamCode: "ESP",
1448
1584
  player: "RODRI",
1449
- text: "RODRI (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1585
+ text: "RODRI (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1450
1586
  source: "fifa",
1451
1587
  seq: 35
1452
1588
  },
@@ -1459,7 +1595,7 @@ var demo_match_default = {
1459
1595
  teamSide: "away",
1460
1596
  teamCode: "ESP",
1461
1597
  player: "Lamine YAMAL",
1462
- text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218, \uC624\uD504\uC0AC\uC774\uB4DC\uB85C \uD310\uC815\uB429\uB2C8\uB2E4.",
1598
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC624\uD504\uC0AC\uC774\uB4DC",
1463
1599
  source: "fifa",
1464
1600
  seq: 36
1465
1601
  },
@@ -1472,7 +1608,7 @@ var demo_match_default = {
1472
1608
  teamSide: "home",
1473
1609
  teamCode: "POR",
1474
1610
  player: "RUBEN DIAS",
1475
- text: "RUBEN DIAS (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1611
+ text: "RUBEN DIAS (\uD3EC\uB974\uD22C\uAC08) \uD30C\uC6B8",
1476
1612
  source: "fifa",
1477
1613
  seq: 37
1478
1614
  },
@@ -1485,7 +1621,7 @@ var demo_match_default = {
1485
1621
  teamSide: "away",
1486
1622
  teamCode: "ESP",
1487
1623
  player: "Pau CUBARSI",
1488
- text: "Pau CUBARSI (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1624
+ text: "Pau CUBARSI (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1489
1625
  source: "fifa",
1490
1626
  seq: 38
1491
1627
  },
@@ -1498,7 +1634,7 @@ var demo_match_default = {
1498
1634
  teamSide: "home",
1499
1635
  teamCode: "POR",
1500
1636
  player: "JOAO FELIX",
1501
- text: "JOAO FELIX (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1637
+ text: "JOAO FELIX (\uD3EC\uB974\uD22C\uAC08) \uD30C\uC6B8",
1502
1638
  source: "fifa",
1503
1639
  seq: 39
1504
1640
  },
@@ -1508,7 +1644,7 @@ var demo_match_default = {
1508
1644
  minute: 45,
1509
1645
  injury: 7,
1510
1646
  period: 3,
1511
- text: "\uC8FC\uC2EC\uC774 \uC804\uBC18\uC804 \uC885\uB8CC\uB97C \uC120\uC5B8\uD569\uB2C8\uB2E4.",
1647
+ text: "\uC804\uBC18 \uC885\uB8CC",
1512
1648
  source: "fifa",
1513
1649
  seq: 40
1514
1650
  },
@@ -1517,7 +1653,7 @@ var demo_match_default = {
1517
1653
  type: "PERIOD_START",
1518
1654
  minute: 45,
1519
1655
  period: 5,
1520
- text: "\uC8FC\uC2EC\uC774 \uD6C4\uBC18\uC804 \uC2DC\uC791 \uD718\uC2AC\uC744 \uBD88\uC5C8\uC2B5\uB2C8\uB2E4.",
1656
+ text: "\uD6C4\uBC18 \uC2DC\uC791",
1521
1657
  source: "fifa",
1522
1658
  seq: 41
1523
1659
  },
@@ -1529,7 +1665,7 @@ var demo_match_default = {
1529
1665
  teamSide: "away",
1530
1666
  teamCode: "ESP",
1531
1667
  player: "Dani OLMO",
1532
- text: "Dani OLMO (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1668
+ text: "Dani OLMO (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1533
1669
  source: "fifa",
1534
1670
  seq: 42
1535
1671
  },
@@ -1541,7 +1677,7 @@ var demo_match_default = {
1541
1677
  teamSide: "away",
1542
1678
  teamCode: "ESP",
1543
1679
  player: "Lamine YAMAL",
1544
- text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1680
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1545
1681
  source: "fifa",
1546
1682
  seq: 43
1547
1683
  },
@@ -1552,7 +1688,7 @@ var demo_match_default = {
1552
1688
  period: 5,
1553
1689
  teamSide: "home",
1554
1690
  teamCode: "POR",
1555
- text: "NELSON SEMEDO (in) \uC120\uC218\uAC00 NUNO MENDES(\uAD50\uCCB4)(\uD3EC\uB974\uD22C\uAC08) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1691
+ text: "\uC120\uC218 \uAD50\uCCB4 (\uD3EC\uB974\uD22C\uAC08): NELSON SEMEDO IN, NUNO MENDES OUT",
1556
1692
  source: "fifa",
1557
1693
  seq: 44
1558
1694
  },
@@ -1564,7 +1700,7 @@ var demo_match_default = {
1564
1700
  teamSide: "home",
1565
1701
  teamCode: "POR",
1566
1702
  player: "CRISTIANO RONALDO",
1567
- text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218, \uC624\uD504\uC0AC\uC774\uB4DC\uB85C \uD310\uC815\uB429\uB2C8\uB2E4.",
1703
+ text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08) \uC624\uD504\uC0AC\uC774\uB4DC",
1568
1704
  source: "fifa",
1569
1705
  seq: 45
1570
1706
  },
@@ -1576,7 +1712,7 @@ var demo_match_default = {
1576
1712
  teamSide: "home",
1577
1713
  teamCode: "POR",
1578
1714
  player: "CRISTIANO RONALDO",
1579
- text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1715
+ text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08) \uC288\uD305 \uC2DC\uB3C4",
1580
1716
  source: "fifa",
1581
1717
  seq: 46
1582
1718
  },
@@ -1588,7 +1724,7 @@ var demo_match_default = {
1588
1724
  teamSide: "away",
1589
1725
  teamCode: "ESP",
1590
1726
  player: "Lamine YAMAL",
1591
- text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1727
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1592
1728
  source: "fifa",
1593
1729
  seq: 47
1594
1730
  },
@@ -1600,7 +1736,7 @@ var demo_match_default = {
1600
1736
  teamSide: "away",
1601
1737
  teamCode: "ESP",
1602
1738
  player: "PEDRI",
1603
- text: "PEDRI (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1739
+ text: "PEDRI (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1604
1740
  source: "fifa",
1605
1741
  seq: 48
1606
1742
  },
@@ -1611,7 +1747,7 @@ var demo_match_default = {
1611
1747
  period: 5,
1612
1748
  teamSide: "away",
1613
1749
  teamCode: "ESP",
1614
- text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1750
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uCF54\uB108\uD0A5",
1615
1751
  source: "fifa",
1616
1752
  seq: 49
1617
1753
  },
@@ -1623,7 +1759,7 @@ var demo_match_default = {
1623
1759
  teamSide: "away",
1624
1760
  teamCode: "ESP",
1625
1761
  player: "Lamine YAMAL",
1626
- text: "Lamine YAMAL (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1762
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1627
1763
  source: "fifa",
1628
1764
  seq: 50
1629
1765
  },
@@ -1634,7 +1770,7 @@ var demo_match_default = {
1634
1770
  period: 5,
1635
1771
  teamSide: "away",
1636
1772
  teamCode: "ESP",
1637
- text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1773
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uCF54\uB108\uD0A5",
1638
1774
  source: "fifa",
1639
1775
  seq: 51
1640
1776
  },
@@ -1646,7 +1782,7 @@ var demo_match_default = {
1646
1782
  teamSide: "away",
1647
1783
  teamCode: "ESP",
1648
1784
  player: "PEDRI",
1649
- text: "PEDRI (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1785
+ text: "PEDRI (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1650
1786
  source: "fifa",
1651
1787
  seq: 52
1652
1788
  },
@@ -1658,7 +1794,7 @@ var demo_match_default = {
1658
1794
  teamSide: "away",
1659
1795
  teamCode: "ESP",
1660
1796
  player: "Alex BAENA",
1661
- text: "Alex BAENA (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1797
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1662
1798
  source: "fifa",
1663
1799
  seq: 53
1664
1800
  },
@@ -1669,7 +1805,7 @@ var demo_match_default = {
1669
1805
  period: 5,
1670
1806
  teamSide: "home",
1671
1807
  teamCode: "POR",
1672
- text: "\uD3EC\uB974\uD22C\uAC08 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1808
+ text: "\uD3EC\uB974\uD22C\uAC08 \uACE8\uD0A4\uD37C \uC120\uBC29",
1673
1809
  source: "fifa",
1674
1810
  seq: 54
1675
1811
  },
@@ -1678,7 +1814,7 @@ var demo_match_default = {
1678
1814
  type: "BREAK",
1679
1815
  minute: 68,
1680
1816
  period: 5,
1681
- text: "\uC218\uBD84 \uBCF4\uCDA9\uC744 \uC704\uD55C \uD734\uC2DD\uC73C\uB85C \uACBD\uAE30 \uC911\uB2E8",
1817
+ text: "\uC218\uBD84 \uC12D\uCDE8 \uD734\uC2DD \u2014 \uACBD\uAE30 \uC77C\uC2DC \uC911\uB2E8",
1682
1818
  source: "fifa",
1683
1819
  seq: 55
1684
1820
  },
@@ -1689,7 +1825,7 @@ var demo_match_default = {
1689
1825
  period: 5,
1690
1826
  teamSide: "home",
1691
1827
  teamCode: "POR",
1692
- text: "RAFAEL LEAO (in) \uC120\uC218\uAC00 JOAO FELIX(\uAD50\uCCB4)(\uD3EC\uB974\uD22C\uAC08) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1828
+ text: "\uC120\uC218 \uAD50\uCCB4 (\uD3EC\uB974\uD22C\uAC08): RAFAEL LEAO IN, JOAO FELIX OUT",
1693
1829
  source: "fifa",
1694
1830
  seq: 56
1695
1831
  },
@@ -1700,7 +1836,7 @@ var demo_match_default = {
1700
1836
  period: 5,
1701
1837
  teamSide: "home",
1702
1838
  teamCode: "POR",
1703
- text: "DIOGO DALOT (in) \uC120\uC218\uAC00 JOAO CANCELO(\uAD50\uCCB4)(\uD3EC\uB974\uD22C\uAC08) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1839
+ text: "\uC120\uC218 \uAD50\uCCB4 (\uD3EC\uB974\uD22C\uAC08): DIOGO DALOT IN, JOAO CANCELO OUT",
1704
1840
  source: "fifa",
1705
1841
  seq: 57
1706
1842
  },
@@ -1709,7 +1845,7 @@ var demo_match_default = {
1709
1845
  type: "RESUMED",
1710
1846
  minute: 72,
1711
1847
  period: 5,
1712
- text: "\uC911\uB2E8 \uD6C4 \uACBD\uAE30 \uC7AC\uAC1C",
1848
+ text: "\uACBD\uAE30 \uC7AC\uAC1C",
1713
1849
  source: "fifa",
1714
1850
  seq: 58
1715
1851
  },
@@ -1721,7 +1857,7 @@ var demo_match_default = {
1721
1857
  teamSide: "home",
1722
1858
  teamCode: "POR",
1723
1859
  player: "NELSON SEMEDO",
1724
- text: "NELSON SEMEDO (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1860
+ text: "NELSON SEMEDO (\uD3EC\uB974\uD22C\uAC08) \uD30C\uC6B8",
1725
1861
  source: "fifa",
1726
1862
  seq: 59
1727
1863
  },
@@ -1733,7 +1869,7 @@ var demo_match_default = {
1733
1869
  teamSide: "away",
1734
1870
  teamCode: "ESP",
1735
1871
  player: "Lamine YAMAL",
1736
- text: "Lamine YAMAL (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1872
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1737
1873
  source: "fifa",
1738
1874
  seq: 60
1739
1875
  },
@@ -1744,7 +1880,7 @@ var demo_match_default = {
1744
1880
  period: 5,
1745
1881
  teamSide: "home",
1746
1882
  teamCode: "POR",
1747
- text: "\uD3EC\uB974\uD22C\uAC08 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1883
+ text: "\uD3EC\uB974\uD22C\uAC08 \uACE8\uD0A4\uD37C \uC120\uBC29",
1748
1884
  source: "fifa",
1749
1885
  seq: 61
1750
1886
  },
@@ -1755,7 +1891,7 @@ var demo_match_default = {
1755
1891
  period: 5,
1756
1892
  teamSide: "away",
1757
1893
  teamCode: "ESP",
1758
- text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1894
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uCF54\uB108\uD0A5",
1759
1895
  source: "fifa",
1760
1896
  seq: 62
1761
1897
  },
@@ -1766,7 +1902,7 @@ var demo_match_default = {
1766
1902
  period: 5,
1767
1903
  teamSide: "away",
1768
1904
  teamCode: "ESP",
1769
- text: "Ferran TORRES (in) \uC120\uC218\uAC00 Alex BAENA(\uAD50\uCCB4)(\uC2A4\uD398\uC778) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1905
+ text: "\uC120\uC218 \uAD50\uCCB4 (\uC2A4\uD398\uC778): Ferran TORRES IN, Alex BAENA OUT",
1770
1906
  source: "fifa",
1771
1907
  seq: 63
1772
1908
  },
@@ -1778,7 +1914,7 @@ var demo_match_default = {
1778
1914
  teamSide: "home",
1779
1915
  teamCode: "POR",
1780
1916
  player: "VITINHA",
1781
- text: "VITINHA (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1917
+ text: "VITINHA (\uD3EC\uB974\uD22C\uAC08) \uC288\uD305 \uC2DC\uB3C4",
1782
1918
  source: "fifa",
1783
1919
  seq: 64
1784
1920
  },
@@ -1790,7 +1926,7 @@ var demo_match_default = {
1790
1926
  teamSide: "home",
1791
1927
  teamCode: "POR",
1792
1928
  player: "BRUNO FERNANDES",
1793
- text: "BRUNO FERNANDES (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1929
+ text: "BRUNO FERNANDES (\uD3EC\uB974\uD22C\uAC08) \uC288\uD305 \uC2DC\uB3C4",
1794
1930
  source: "fifa",
1795
1931
  seq: 65
1796
1932
  },
@@ -1802,7 +1938,7 @@ var demo_match_default = {
1802
1938
  teamSide: "away",
1803
1939
  teamCode: "ESP",
1804
1940
  player: "Ferran TORRES",
1805
- text: "Ferran TORRES (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1941
+ text: "Ferran TORRES (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1806
1942
  source: "fifa",
1807
1943
  seq: 66
1808
1944
  },
@@ -1814,7 +1950,7 @@ var demo_match_default = {
1814
1950
  teamSide: "away",
1815
1951
  teamCode: "ESP",
1816
1952
  player: "Dani OLMO",
1817
- text: "Dani OLMO (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1953
+ text: "Dani OLMO (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1818
1954
  source: "fifa",
1819
1955
  seq: 67
1820
1956
  },
@@ -1825,7 +1961,7 @@ var demo_match_default = {
1825
1961
  period: 5,
1826
1962
  teamSide: "away",
1827
1963
  teamCode: "ESP",
1828
- text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1964
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uCF54\uB108\uD0A5",
1829
1965
  source: "fifa",
1830
1966
  seq: 68
1831
1967
  },
@@ -1836,7 +1972,7 @@ var demo_match_default = {
1836
1972
  period: 5,
1837
1973
  teamSide: "away",
1838
1974
  teamCode: "ESP",
1839
- text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1975
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uCF54\uB108\uD0A5",
1840
1976
  source: "fifa",
1841
1977
  seq: 69
1842
1978
  },
@@ -1848,7 +1984,7 @@ var demo_match_default = {
1848
1984
  teamSide: "away",
1849
1985
  teamCode: "ESP",
1850
1986
  player: "RODRI",
1851
- text: "RODRI (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1987
+ text: "RODRI (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1852
1988
  source: "fifa",
1853
1989
  seq: 70
1854
1990
  },
@@ -1859,7 +1995,7 @@ var demo_match_default = {
1859
1995
  period: 5,
1860
1996
  teamSide: "home",
1861
1997
  teamCode: "POR",
1862
- text: "BERNARDO SILVA (in) \uC120\uC218\uAC00 VITINHA(\uAD50\uCCB4)(\uD3EC\uB974\uD22C\uAC08) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1998
+ text: "\uC120\uC218 \uAD50\uCCB4 (\uD3EC\uB974\uD22C\uAC08): BERNARDO SILVA IN, VITINHA OUT",
1863
1999
  source: "fifa",
1864
2000
  seq: 71
1865
2001
  },
@@ -1870,7 +2006,7 @@ var demo_match_default = {
1870
2006
  period: 5,
1871
2007
  teamSide: "home",
1872
2008
  teamCode: "POR",
1873
- text: "FRANCISCO CONCEICAO (in) \uC120\uC218\uAC00 PEDRO NETO(\uAD50\uCCB4)(\uD3EC\uB974\uD22C\uAC08) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
2009
+ text: "\uC120\uC218 \uAD50\uCCB4 (\uD3EC\uB974\uD22C\uAC08): FRANCISCO CONCEICAO IN, PEDRO NETO OUT",
1874
2010
  source: "fifa",
1875
2011
  seq: 72
1876
2012
  },
@@ -1882,7 +2018,7 @@ var demo_match_default = {
1882
2018
  teamSide: "away",
1883
2019
  teamCode: "ESP",
1884
2020
  player: "Lamine YAMAL",
1885
- text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
2021
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uD30C\uC6B8",
1886
2022
  source: "fifa",
1887
2023
  seq: 73
1888
2024
  },
@@ -1893,7 +2029,7 @@ var demo_match_default = {
1893
2029
  period: 5,
1894
2030
  teamSide: "away",
1895
2031
  teamCode: "ESP",
1896
- text: "Mikel MERINO (in) \uC120\uC218\uAC00 Dani OLMO(\uAD50\uCCB4)(\uC2A4\uD398\uC778) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
2032
+ text: "\uC120\uC218 \uAD50\uCCB4 (\uC2A4\uD398\uC778): Mikel MERINO IN, Dani OLMO OUT",
1897
2033
  source: "fifa",
1898
2034
  seq: 74
1899
2035
  },
@@ -1904,7 +2040,7 @@ var demo_match_default = {
1904
2040
  period: 5,
1905
2041
  teamSide: "away",
1906
2042
  teamCode: "ESP",
1907
- text: "Fabian RUIZ (in) \uC120\uC218\uAC00 PEDRI(\uAD50\uCCB4)(\uC2A4\uD398\uC778) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
2043
+ text: "\uC120\uC218 \uAD50\uCCB4 (\uC2A4\uD398\uC778): Fabian RUIZ IN, PEDRI OUT",
1908
2044
  source: "fifa",
1909
2045
  seq: 75
1910
2046
  },
@@ -1915,8 +2051,8 @@ var demo_match_default = {
1915
2051
  period: 5,
1916
2052
  teamSide: "home",
1917
2053
  teamCode: "POR",
1918
- player: "\uC8FC\uC2EC\uC774 BERNARDO SILVA",
1919
- text: "\uC8FC\uC2EC\uC774 BERNARDO SILVA (\uD3EC\uB974\uD22C\uAC08) \uC5D0\uAC8C \uACBD\uACE0 \uC870\uCE58\uB97C \uD569\uB2C8\uB2E4.",
2054
+ player: "BERNARDO SILVA",
2055
+ text: "BERNARDO SILVA (\uD3EC\uB974\uD22C\uAC08) \uACBD\uACE0 (\uC610\uB85C\uCE74\uB4DC)",
1920
2056
  source: "fifa",
1921
2057
  seq: 76
1922
2058
  },
@@ -1928,7 +2064,7 @@ var demo_match_default = {
1928
2064
  teamSide: "home",
1929
2065
  teamCode: "POR",
1930
2066
  player: "BERNARDO SILVA",
1931
- text: "BERNARDO SILVA (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
2067
+ text: "BERNARDO SILVA (\uD3EC\uB974\uD22C\uAC08) \uD30C\uC6B8",
1932
2068
  source: "fifa",
1933
2069
  seq: 77
1934
2070
  },
@@ -1940,7 +2076,7 @@ var demo_match_default = {
1940
2076
  period: 5,
1941
2077
  teamSide: "away",
1942
2078
  teamCode: "ESP",
1943
- text: "Ferran TORRES \uC120\uC218\uC758 \uC5B4\uC2DC\uC2A4\uD2B8\uC785\uB2C8\uB2E4.",
2079
+ text: "\uC5B4\uC2DC\uC2A4\uD2B8: Ferran TORRES",
1944
2080
  source: "fifa",
1945
2081
  seq: 78
1946
2082
  },
@@ -1953,7 +2089,7 @@ var demo_match_default = {
1953
2089
  teamSide: "away",
1954
2090
  teamCode: "ESP",
1955
2091
  player: "Mikel MERINO",
1956
- text: "Mikel MERINO (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
2092
+ text: "Mikel MERINO (\uC2A4\uD398\uC778) \uC288\uD305 \uC2DC\uB3C4",
1957
2093
  source: "fifa",
1958
2094
  seq: 79
1959
2095
  },
@@ -1966,7 +2102,7 @@ var demo_match_default = {
1966
2102
  teamSide: "away",
1967
2103
  teamCode: "ESP",
1968
2104
  player: "Mikel MERINO",
1969
- text: "Mikel MERINO (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uB4DD\uC810\uD569\uB2C8\uB2E4!!",
2105
+ text: "Mikel MERINO (\uC2A4\uD398\uC778) \uB4DD\uC810!",
1970
2106
  scoreAfter: {
1971
2107
  home: 0,
1972
2108
  away: 1
@@ -1982,8 +2118,8 @@ var demo_match_default = {
1982
2118
  period: 5,
1983
2119
  teamSide: "home",
1984
2120
  teamCode: "POR",
1985
- player: "\uC8FC\uC2EC\uC774 RENATO VEIGA",
1986
- text: "\uC8FC\uC2EC\uC774 RENATO VEIGA (\uD3EC\uB974\uD22C\uAC08) \uC5D0\uAC8C \uACBD\uACE0 \uC870\uCE58\uB97C \uD569\uB2C8\uB2E4.",
2121
+ player: "RENATO VEIGA",
2122
+ text: "RENATO VEIGA (\uD3EC\uB974\uD22C\uAC08) \uACBD\uACE0 (\uC610\uB85C\uCE74\uB4DC)",
1987
2123
  source: "fifa",
1988
2124
  seq: 81
1989
2125
  },
@@ -1996,7 +2132,7 @@ var demo_match_default = {
1996
2132
  teamSide: "home",
1997
2133
  teamCode: "POR",
1998
2134
  player: "BERNARDO SILVA",
1999
- text: "BERNARDO SILVA (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
2135
+ text: "BERNARDO SILVA (\uD3EC\uB974\uD22C\uAC08) \uC288\uD305 \uC2DC\uB3C4",
2000
2136
  source: "fifa",
2001
2137
  seq: 82
2002
2138
  },
@@ -2008,7 +2144,7 @@ var demo_match_default = {
2008
2144
  period: 5,
2009
2145
  teamSide: "away",
2010
2146
  teamCode: "ESP",
2011
- text: "Borja IGLESIAS (in) \uC120\uC218\uAC00 Mikel OYARZABAL(\uAD50\uCCB4)(\uC2A4\uD398\uC778) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
2147
+ text: "\uC120\uC218 \uAD50\uCCB4 (\uC2A4\uD398\uC778): Borja IGLESIAS IN, Mikel OYARZABAL OUT",
2012
2148
  source: "fifa",
2013
2149
  seq: 83
2014
2150
  },
@@ -2020,8 +2156,8 @@ var demo_match_default = {
2020
2156
  period: 5,
2021
2157
  teamSide: "away",
2022
2158
  teamCode: "ESP",
2023
- player: "\uC8FC\uC2EC\uC774 Ferran TORRES",
2024
- text: "\uC8FC\uC2EC\uC774 Ferran TORRES (\uC2A4\uD398\uC778) \uC5D0\uAC8C \uACBD\uACE0 \uC870\uCE58\uB97C \uD569\uB2C8\uB2E4.",
2159
+ player: "Ferran TORRES",
2160
+ text: "Ferran TORRES (\uC2A4\uD398\uC778) \uACBD\uACE0 (\uC610\uB85C\uCE74\uB4DC)",
2025
2161
  source: "fifa",
2026
2162
  seq: 84
2027
2163
  },
@@ -2034,7 +2170,7 @@ var demo_match_default = {
2034
2170
  teamSide: "home",
2035
2171
  teamCode: "POR",
2036
2172
  player: "JOAO NEVES",
2037
- text: "JOAO NEVES (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
2173
+ text: "JOAO NEVES (\uD3EC\uB974\uD22C\uAC08) \uC288\uD305 \uC2DC\uB3C4",
2038
2174
  source: "fifa",
2039
2175
  seq: 85
2040
2176
  },
@@ -2044,7 +2180,7 @@ var demo_match_default = {
2044
2180
  minute: 90,
2045
2181
  injury: 9,
2046
2182
  period: 5,
2047
- text: "\uC8FC\uC2EC\uC774 \uD6C4\uBC18\uC804 \uC885\uB8CC\uB97C \uC120\uC5B8\uD569\uB2C8\uB2E4.",
2183
+ text: "\uD6C4\uBC18 \uC885\uB8CC",
2048
2184
  source: "fifa",
2049
2185
  seq: 86
2050
2186
  },
@@ -2053,7 +2189,7 @@ var demo_match_default = {
2053
2189
  type: "FULLTIME",
2054
2190
  minute: 99,
2055
2191
  period: 10,
2056
- text: "\uACBD\uAE30 \uC885\uB8CC \uD718\uC2AC\uC774 \uC6B8\uB9BD\uB2C8\uB2E4.",
2192
+ text: "\uACBD\uAE30 \uC885\uB8CC",
2057
2193
  source: "fifa",
2058
2194
  seq: 87
2059
2195
  }
@@ -2119,7 +2255,105 @@ var EventDiffer = class {
2119
2255
  }
2120
2256
  };
2121
2257
 
2258
+ // src/core/digest.ts
2259
+ var FOLD_SURVIVORS = /* @__PURE__ */ new Set([
2260
+ "GOAL",
2261
+ "OWN_GOAL",
2262
+ "PENALTY_GOAL",
2263
+ "PENALTY_MISS",
2264
+ "YELLOW",
2265
+ "RED",
2266
+ "SUBSTITUTION",
2267
+ "VAR",
2268
+ "BREAK",
2269
+ "RESUMED",
2270
+ "ADDED_TIME"
2271
+ ]);
2272
+ var FOLD_AFTER_MINUTES = 15;
2273
+ function foldTimeline(lines, nowMinute, eventOf) {
2274
+ const horizon = nowMinute - FOLD_AFTER_MINUTES;
2275
+ const visible = [];
2276
+ let foldedCount = 0;
2277
+ for (const line of lines) {
2278
+ const e = eventOf(line);
2279
+ if (e && !FOLD_SURVIVORS.has(e.type) && e.minute < horizon) {
2280
+ foldedCount++;
2281
+ continue;
2282
+ }
2283
+ visible.push(line);
2284
+ }
2285
+ return { visible, foldedCount };
2286
+ }
2287
+ function fifaPeriodOf(phase) {
2288
+ switch (phase) {
2289
+ case "FIRST_HALF":
2290
+ return 3;
2291
+ case "HALFTIME":
2292
+ return 4;
2293
+ case "SECOND_HALF":
2294
+ return 5;
2295
+ case "ET_FIRST":
2296
+ return 7;
2297
+ case "ET_SECOND":
2298
+ return 9;
2299
+ case "PENALTIES":
2300
+ return 11;
2301
+ default:
2302
+ return void 0;
2303
+ }
2304
+ }
2305
+ var BOUNDARY_PHASES = /* @__PURE__ */ new Set([
2306
+ "HALFTIME",
2307
+ "ET_BREAK",
2308
+ "FINISHED"
2309
+ ]);
2310
+ function boundaryPhaseOf(e) {
2311
+ if (e.type === "FULLTIME") return "FINISHED";
2312
+ if (e.type === "PERIOD_END" && e.period === 3) return "HALFTIME";
2313
+ return null;
2314
+ }
2315
+ var HIGHLIGHT_TYPES = /* @__PURE__ */ new Set([
2316
+ "GOAL",
2317
+ "OWN_GOAL",
2318
+ "PENALTY_GOAL",
2319
+ "RED",
2320
+ "YELLOW"
2321
+ ]);
2322
+ function deriveHighlights(events, cancelledIds) {
2323
+ const seen = /* @__PURE__ */ new Set();
2324
+ const out = [];
2325
+ for (const e of events) {
2326
+ if (!HIGHLIGHT_TYPES.has(e.type)) continue;
2327
+ if (cancelledIds.has(e.id) || seen.has(e.id)) continue;
2328
+ seen.add(e.id);
2329
+ out.push(e);
2330
+ }
2331
+ return out.sort(compareEvents);
2332
+ }
2333
+ function synthesizeAddedTime(prev, next, opts) {
2334
+ if (!next.injury || next.injury <= 0) return null;
2335
+ const samePhaseAlreadyAnnounced = prev !== null && prev.phase === next.phase && !!prev.injury;
2336
+ if (samePhaseAlreadyAnnounced) return null;
2337
+ const text = opts.lang === "ko" ? `\uCD94\uAC00\uC2DC\uAC04 +${next.injury}\uBD84` : `+${next.injury} minutes added`;
2338
+ return {
2339
+ id: `local:added-time-${next.phase}`,
2340
+ type: "ADDED_TIME",
2341
+ minute: next.minute,
2342
+ period: fifaPeriodOf(next.phase),
2343
+ text,
2344
+ source: opts.source,
2345
+ seq: 0
2346
+ };
2347
+ }
2348
+
2122
2349
  // src/engine/polling.ts
2350
+ function nextPollDelay(msToKickoff) {
2351
+ if (msToKickoff > 60 * 6e4) return 5 * 6e4;
2352
+ if (msToKickoff > 0) return 6e4;
2353
+ return 15e3;
2354
+ }
2355
+ var REVERIFY_AFTER_MS = 10 * 6e4;
2356
+ var REVERIFY_EVERY_MS = 10 * 6e4;
2123
2357
  var PollingEngine = class extends EventEmitter {
2124
2358
  constructor(provider, match, lang, opts = {}) {
2125
2359
  super();
@@ -2128,6 +2362,7 @@ var PollingEngine = class extends EventEmitter {
2128
2362
  this.lang = lang;
2129
2363
  this.intervalMs = opts.intervalMs ?? 1e4;
2130
2364
  this.jitterMs = opts.jitterMs ?? 2e3;
2365
+ this.waiting = match.phase === "SCHEDULED";
2131
2366
  }
2132
2367
  provider;
2133
2368
  match;
@@ -2140,6 +2375,9 @@ var PollingEngine = class extends EventEmitter {
2140
2375
  errorStreak = 0;
2141
2376
  lastOkAt;
2142
2377
  finishedPolls = 0;
2378
+ waiting;
2379
+ lastReverifyAt = 0;
2380
+ prevState = null;
2143
2381
  start() {
2144
2382
  void this.tick();
2145
2383
  }
@@ -2151,8 +2389,65 @@ var PollingEngine = class extends EventEmitter {
2151
2389
  if (this.stopped) return;
2152
2390
  this.timer = setTimeout(() => void this.tick(), delayMs);
2153
2391
  }
2392
+ /**
2393
+ * Waiting mode: probe the cheap live-ids endpoint only — never state or
2394
+ * timeline (their SCHEDULED fallback hits the calendar every tick). The
2395
+ * match appearing in the live list IS the kickoff signal; long past
2396
+ * kickoff with no appearance, do a bounded full re-check to catch
2397
+ * postponements/abandonment.
2398
+ */
2399
+ async waitTick() {
2400
+ try {
2401
+ const ids = await this.provider.fetchLiveMatchIds(this.lang);
2402
+ if (this.errorStreak > 0) {
2403
+ this.emit("net", { down: false, lastOkAt: Date.now() });
2404
+ }
2405
+ this.errorStreak = 0;
2406
+ this.lastOkAt = Date.now();
2407
+ if (this.isOurs(ids)) {
2408
+ this.waiting = false;
2409
+ void this.tick();
2410
+ return;
2411
+ }
2412
+ const msToKickoff = new Date(this.match.kickoff).getTime() - Date.now();
2413
+ if (msToKickoff < -REVERIFY_AFTER_MS && Date.now() - this.lastReverifyAt >= REVERIFY_EVERY_MS) {
2414
+ this.lastReverifyAt = Date.now();
2415
+ const state = await this.provider.fetchMatchState(
2416
+ this.match,
2417
+ this.lang
2418
+ );
2419
+ if (state && state.phase !== "SCHEDULED") {
2420
+ this.waiting = false;
2421
+ void this.tick();
2422
+ return;
2423
+ }
2424
+ }
2425
+ this.schedule(nextPollDelay(msToKickoff) + Math.random() * this.jitterMs);
2426
+ } catch {
2427
+ this.errorStreak++;
2428
+ const backoff = Math.min(
2429
+ this.intervalMs * 2 ** Math.min(this.errorStreak - 1, 3),
2430
+ 6e4
2431
+ );
2432
+ const msToKickoff = new Date(this.match.kickoff).getTime() - Date.now();
2433
+ const delay = Math.max(nextPollDelay(msToKickoff), backoff);
2434
+ this.emit("net", {
2435
+ down: true,
2436
+ retryInMs: delay,
2437
+ lastOkAt: this.lastOkAt
2438
+ });
2439
+ this.schedule(delay + Math.random() * this.jitterMs);
2440
+ }
2441
+ }
2442
+ /** The probe returns active-source native ids; match against every ref. */
2443
+ isOurs(ids) {
2444
+ const fifaId = this.match.sourceRefs.fifa?.idMatch;
2445
+ const espnId = this.match.sourceRefs.espn?.eventId;
2446
+ return ids.has(this.match.id) || fifaId !== void 0 && ids.has(fifaId) || espnId !== void 0 && ids.has(espnId);
2447
+ }
2154
2448
  async tick() {
2155
2449
  if (this.stopped) return;
2450
+ if (this.waiting) return this.waitTick();
2156
2451
  try {
2157
2452
  const [state, timeline] = await Promise.all([
2158
2453
  this.provider.fetchMatchState(this.match, this.lang),
@@ -2166,7 +2461,13 @@ var PollingEngine = class extends EventEmitter {
2166
2461
  if (state) {
2167
2462
  this.match.score = state.score;
2168
2463
  this.match.phase = state.phase;
2464
+ const addedTime = synthesizeAddedTime(this.prevState, state, {
2465
+ lang: this.lang,
2466
+ source: this.provider.name
2467
+ });
2468
+ this.prevState = state;
2169
2469
  this.emit("state", state);
2470
+ if (addedTime) this.emit("events", [addedTime]);
2170
2471
  }
2171
2472
  const { fresh, cancelled, sourceSwitched } = this.differ.reconcile(timeline);
2172
2473
  if (sourceSwitched) this.emit("sourceSwitched");
@@ -2374,8 +2675,10 @@ var CONFIG_FILE = join(paths.config, "config.json");
2374
2675
  var CACHE_DIR = paths.cache;
2375
2676
  var REPLAY_DIR = join(paths.cache, "replays");
2376
2677
  var SCHEDULE_CACHE_FILE = join(CACHE_DIR, "schedule-17-285023.json");
2377
- var SCHEMA_VERSION = 1;
2678
+ var SCHEMA_VERSION = 2;
2378
2679
  var SCHEDULE_TTL_MS = 30 * 6e4;
2680
+ var LIVE_WINDOW_BEFORE_MS = 5 * 6e4;
2681
+ var LIVE_WINDOW_AFTER_MS = 3.5 * 36e5;
2379
2682
  function atomicWrite(file, data) {
2380
2683
  mkdirSync(dirname(file), { recursive: true });
2381
2684
  const tmp = `${file}.tmp-${process.pid}`;
@@ -2394,32 +2697,60 @@ function readConfig() {
2394
2697
  function writeConfig(cfg) {
2395
2698
  atomicWrite(CONFIG_FILE, JSON.stringify(cfg, null, 2));
2396
2699
  }
2397
- function readScheduleCache(lang) {
2700
+ function parseScheduleCache(raw, lang) {
2701
+ if (raw?.schemaVersion !== SCHEMA_VERSION || raw?.lang !== lang) return null;
2702
+ if (!Array.isArray(raw.matches) || typeof raw.fetchedAt !== "number")
2703
+ return null;
2704
+ if (raw.coverage !== "full" && raw.coverage !== "window") return null;
2705
+ return {
2706
+ fetchedAt: raw.fetchedAt,
2707
+ lang: raw.lang,
2708
+ source: raw.source === "espn" ? "espn" : "fifa",
2709
+ coverage: raw.coverage,
2710
+ matches: raw.matches
2711
+ };
2712
+ }
2713
+ function readRawScheduleCache() {
2398
2714
  try {
2399
2715
  if (!existsSync(SCHEDULE_CACHE_FILE)) return null;
2400
- const raw = JSON.parse(readFileSync(SCHEDULE_CACHE_FILE, "utf8"));
2401
- if (raw?.schemaVersion !== SCHEMA_VERSION || raw?.lang !== lang)
2402
- return null;
2403
- if (!Array.isArray(raw.matches) || typeof raw.fetchedAt !== "number")
2404
- return null;
2405
- return { fetchedAt: raw.fetchedAt, lang: raw.lang, matches: raw.matches };
2716
+ return JSON.parse(readFileSync(SCHEDULE_CACHE_FILE, "utf8"));
2406
2717
  } catch {
2407
2718
  return null;
2408
2719
  }
2409
2720
  }
2410
- function writeScheduleCache(matches, lang) {
2721
+ function readScheduleCache(lang) {
2722
+ const raw = readRawScheduleCache();
2723
+ return raw ? parseScheduleCache(raw, lang) : null;
2724
+ }
2725
+ function shouldReplaceCache(existingCoverage, incoming) {
2726
+ return !(incoming === "window" && existingCoverage === "full");
2727
+ }
2728
+ function writeScheduleCache(matches, lang, source, coverage) {
2729
+ const raw = readRawScheduleCache();
2730
+ const existingCoverage = raw?.schemaVersion === SCHEMA_VERSION && (raw?.coverage === "full" || raw?.coverage === "window") ? raw.coverage : null;
2731
+ if (!shouldReplaceCache(existingCoverage, coverage)) return;
2411
2732
  atomicWrite(
2412
2733
  SCHEDULE_CACHE_FILE,
2413
2734
  JSON.stringify({
2414
2735
  schemaVersion: SCHEMA_VERSION,
2415
2736
  fetchedAt: Date.now(),
2416
2737
  lang,
2738
+ source,
2739
+ coverage,
2417
2740
  matches
2418
2741
  })
2419
2742
  );
2420
2743
  }
2421
- function isScheduleFresh(cache) {
2422
- return Date.now() - cache.fetchedAt < SCHEDULE_TTL_MS;
2744
+ function isScheduleFresh(cache, now = Date.now()) {
2745
+ if (now - cache.fetchedAt >= SCHEDULE_TTL_MS) return false;
2746
+ for (const m of cache.matches) {
2747
+ if (isLivePhase(m.phase)) return false;
2748
+ if (m.phase === "FINISHED" || m.phase === "ABANDONED") continue;
2749
+ const k = new Date(m.kickoff).getTime();
2750
+ if (now >= k - LIVE_WINDOW_BEFORE_MS && now <= k + LIVE_WINDOW_AFTER_MS)
2751
+ return false;
2752
+ }
2753
+ return true;
2423
2754
  }
2424
2755
 
2425
2756
  // src/replay/recorder.ts
@@ -2427,31 +2758,42 @@ var REPLAY_FORMAT_VERSION = 1;
2427
2758
  var ReplayRecorder = class {
2428
2759
  constructor(match, dir = REPLAY_DIR) {
2429
2760
  this.match = match;
2430
- mkdirSync2(dir, { recursive: true });
2761
+ this.dir = dir;
2431
2762
  const slug = `${match.id}-${match.home.code}-${match.away.code}`.replace(
2432
2763
  /[^\w.-]/g,
2433
2764
  "_"
2434
2765
  );
2435
2766
  this.file = join2(dir, `${slug}.jsonl`);
2436
- const empty = !existsSync2(this.file) || statSync(this.file).size === 0;
2437
- if (empty) {
2438
- const header = {
2439
- v: REPLAY_FORMAT_VERSION,
2440
- kind: "termfc-replay",
2441
- recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
2442
- match
2443
- };
2444
- appendFileSync(this.file, `${JSON.stringify(header)}
2445
- `);
2446
- }
2447
2767
  }
2448
2768
  match;
2769
+ dir;
2449
2770
  file;
2771
+ started = false;
2772
+ broken = false;
2450
2773
  append(events) {
2451
- if (events.length === 0) return;
2452
- const lines = events.map((e) => JSON.stringify(e)).join("\n");
2453
- appendFileSync(this.file, `${lines}
2774
+ if (events.length === 0 || this.broken) return;
2775
+ try {
2776
+ if (!this.started) {
2777
+ mkdirSync2(this.dir, { recursive: true });
2778
+ const empty = !existsSync2(this.file) || statSync(this.file).size === 0;
2779
+ if (empty) {
2780
+ const header = {
2781
+ v: REPLAY_FORMAT_VERSION,
2782
+ kind: "termfc-replay",
2783
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
2784
+ match: this.match
2785
+ };
2786
+ appendFileSync(this.file, `${JSON.stringify(header)}
2787
+ `);
2788
+ }
2789
+ this.started = true;
2790
+ }
2791
+ const lines = events.map((e) => JSON.stringify(e)).join("\n");
2792
+ appendFileSync(this.file, `${lines}
2454
2793
  `);
2794
+ } catch {
2795
+ this.broken = true;
2796
+ }
2455
2797
  }
2456
2798
  };
2457
2799
 
@@ -2653,6 +2995,10 @@ function truncate(s, max) {
2653
2995
  }
2654
2996
  return out;
2655
2997
  }
2998
+ function padEndVisual(s, len) {
2999
+ const w = width(s);
3000
+ return w >= len ? s : s + " ".repeat(len - w);
3001
+ }
2656
3002
  function center(s, cols) {
2657
3003
  const w = width(s);
2658
3004
  if (w >= cols) return s;
@@ -2661,8 +3007,8 @@ function center(s, cols) {
2661
3007
  function term(out = process.stdout) {
2662
3008
  return {
2663
3009
  out,
2664
- cols: () => out.columns ?? 80,
2665
- rows: () => out.rows ?? 24
3010
+ cols: () => out.columns || 80,
3011
+ rows: () => out.rows || 24
2666
3012
  };
2667
3013
  }
2668
3014
  function enterAltScreen(out) {
@@ -2671,8 +3017,15 @@ function enterAltScreen(out) {
2671
3017
  function leaveAltScreen(out) {
2672
3018
  if (out.isTTY) out.write("\x1B[?25h\x1B[?1049l");
2673
3019
  }
2674
- function clearScreen(out) {
2675
- out.write(out.isTTY ? "\x1B[2J\x1B[H" : "\n");
3020
+ function paintFrame(out, lines) {
3021
+ if (!out.isTTY) {
3022
+ out.write(`${lines.join("\n")}
3023
+ `);
3024
+ return;
3025
+ }
3026
+ const body = lines.map((l) => `${l}\x1B[K`).join("\r\n");
3027
+ out.write(`\x1B[?2026h\x1B[H${body}\r
3028
+ \x1B[0J\x1B[?2026l`);
2676
3029
  }
2677
3030
 
2678
3031
  // src/ui/banner.ts
@@ -2698,6 +3051,144 @@ function renderBanner(version) {
2698
3051
  ${colored}${info}`;
2699
3052
  }
2700
3053
 
3054
+ // src/core/bracket.ts
3055
+ var KNOCKOUT_ORDER = [
3056
+ "R32",
3057
+ "R16",
3058
+ "QF",
3059
+ "SF",
3060
+ "THIRD",
3061
+ "FINAL"
3062
+ ];
3063
+ function buildBracket(matches) {
3064
+ const byKind = /* @__PURE__ */ new Map();
3065
+ for (const m of matches) {
3066
+ if (!m.stageKind || m.stageKind === "GROUP") continue;
3067
+ const list = byKind.get(m.stageKind) ?? [];
3068
+ list.push(m);
3069
+ byKind.set(m.stageKind, list);
3070
+ }
3071
+ const rounds = [];
3072
+ for (const kind of KNOCKOUT_ORDER) {
3073
+ const list = byKind.get(kind);
3074
+ if (!list) continue;
3075
+ list.sort(
3076
+ (a, b) => (a.matchNumber ?? Number.MAX_SAFE_INTEGER) - (b.matchNumber ?? Number.MAX_SAFE_INTEGER) || a.kickoff.localeCompare(b.kickoff)
3077
+ );
3078
+ rounds.push({ kind, matches: list });
3079
+ }
3080
+ return rounds;
3081
+ }
3082
+ function placeholderLabel(raw, lang) {
3083
+ let m = /^W(\d+)$/.exec(raw);
3084
+ if (m) return lang === "ko" ? `${m[1]}\uBC88 \uC2B9\uC790` : `Winner M${m[1]}`;
3085
+ m = /^RU(\d+)$/.exec(raw);
3086
+ if (m) return lang === "ko" ? `${m[1]}\uBC88 \uD328\uC790` : `Loser M${m[1]}`;
3087
+ m = /^([12])([A-L])$/.exec(raw);
3088
+ if (m)
3089
+ return lang === "ko" ? `${m[2]}\uC870 ${m[1]}\uC704` : `Group ${m[2]} #${m[1]}`;
3090
+ m = /^3([A-L]{2,})$/.exec(raw);
3091
+ if (m) {
3092
+ const pool = m[1].split("").join("/");
3093
+ return lang === "ko" ? `${pool}\uC870 \uC911 3\uC704` : `3rd of ${pool}`;
3094
+ }
3095
+ return raw;
3096
+ }
3097
+
3098
+ // src/core/standings.ts
3099
+ function tally(into, team, gf, ga) {
3100
+ const agg = into.get(team.code) ?? {
3101
+ team,
3102
+ played: 0,
3103
+ won: 0,
3104
+ drawn: 0,
3105
+ lost: 0,
3106
+ gf: 0,
3107
+ ga: 0
3108
+ };
3109
+ agg.played++;
3110
+ agg.gf += gf;
3111
+ agg.ga += ga;
3112
+ if (gf > ga) agg.won++;
3113
+ else if (gf === ga) agg.drawn++;
3114
+ else agg.lost++;
3115
+ into.set(team.code, agg);
3116
+ }
3117
+ var points = (a) => a.won * 3 + a.drawn;
3118
+ var gd = (a) => a.gf - a.ga;
3119
+ function compareAgg(a, b) {
3120
+ return points(b) - points(a) || gd(b) - gd(a) || b.gf - a.gf;
3121
+ }
3122
+ function zeroAgg(team) {
3123
+ return { team, played: 0, won: 0, drawn: 0, lost: 0, gf: 0, ga: 0 };
3124
+ }
3125
+ function aggregate(matches, teams) {
3126
+ const map = /* @__PURE__ */ new Map();
3127
+ for (const m of matches) {
3128
+ if (teams && !(teams.has(m.home.code) && teams.has(m.away.code))) continue;
3129
+ for (const t of [m.home, m.away])
3130
+ if (!map.has(t.code)) map.set(t.code, zeroAgg(t));
3131
+ if (m.phase !== "FINISHED") continue;
3132
+ tally(map, m.home, m.score.home, m.score.away);
3133
+ tally(map, m.away, m.score.away, m.score.home);
3134
+ }
3135
+ return map;
3136
+ }
3137
+ function computeGroupStandings(matches) {
3138
+ const byGroup = /* @__PURE__ */ new Map();
3139
+ for (const m of matches) {
3140
+ if (m.stageKind !== "GROUP" || !m.group) continue;
3141
+ const list = byGroup.get(m.group) ?? [];
3142
+ list.push(m);
3143
+ byGroup.set(m.group, list);
3144
+ }
3145
+ const tables = [];
3146
+ for (const group of [...byGroup.keys()].sort()) {
3147
+ const groupMatches = byGroup.get(group);
3148
+ const overall = [...aggregate(groupMatches).values()].sort(compareAgg);
3149
+ const ordered = [];
3150
+ for (let i = 0; i < overall.length; ) {
3151
+ let j = i + 1;
3152
+ while (j < overall.length && compareAgg(overall[i], overall[j]) === 0)
3153
+ j++;
3154
+ const cluster = overall.slice(i, j);
3155
+ if (cluster.length === 1) {
3156
+ ordered.push({ agg: cluster[0], tiedWithNext: false });
3157
+ } else {
3158
+ const codes = new Set(cluster.map((a) => a.team.code));
3159
+ const mini = aggregate(groupMatches, codes);
3160
+ const h2h = (a) => mini.get(a.team.code) ?? zeroAgg(a.team);
3161
+ cluster.sort((a, b) => compareAgg(h2h(a), h2h(b)) || compareAgg(a, b));
3162
+ for (let k = 0; k < cluster.length; k++) {
3163
+ const next = cluster[k + 1];
3164
+ ordered.push({
3165
+ agg: cluster[k],
3166
+ tiedWithNext: next !== void 0 && compareAgg(h2h(cluster[k]), h2h(next)) === 0
3167
+ });
3168
+ }
3169
+ }
3170
+ i = j;
3171
+ }
3172
+ tables.push({
3173
+ group,
3174
+ rows: ordered.map(({ agg, tiedWithNext }, idx) => ({
3175
+ team: agg.team,
3176
+ played: agg.played,
3177
+ won: agg.won,
3178
+ drawn: agg.drawn,
3179
+ lost: agg.lost,
3180
+ goalsFor: agg.gf,
3181
+ goalsAgainst: agg.ga,
3182
+ goalDiff: gd(agg),
3183
+ points: points(agg),
3184
+ rank: idx + 1,
3185
+ tiedWithNext
3186
+ }))
3187
+ });
3188
+ }
3189
+ return tables;
3190
+ }
3191
+
2701
3192
  // src/ui/listScreen.ts
2702
3193
  import * as readline from "readline/promises";
2703
3194
  function kickoffLabel(iso, lang) {
@@ -2784,6 +3275,50 @@ function renderList(matches, opts) {
2784
3275
  }
2785
3276
  return pickable;
2786
3277
  }
3278
+ var FULL_ORDER = [
3279
+ "GROUP",
3280
+ "R32",
3281
+ "R16",
3282
+ "QF",
3283
+ "SF",
3284
+ "THIRD",
3285
+ "FINAL",
3286
+ "OTHER"
3287
+ ];
3288
+ function renderFullSchedule(matches, opts) {
3289
+ const l = labels(opts.lang);
3290
+ const out = process.stdout;
3291
+ const pickable = [];
3292
+ if (opts.staleAt) {
3293
+ const at = new Date(opts.staleAt).toTimeString().slice(0, 5);
3294
+ out.write(yellow(` \u26A0 ${l.offline} \xB7 ${at} ${l.staleData}
3295
+
3296
+ `));
3297
+ }
3298
+ const byStage = /* @__PURE__ */ new Map();
3299
+ for (const m of matches) {
3300
+ const key = m.stageKind ?? "OTHER";
3301
+ const list = byStage.get(key) ?? [];
3302
+ list.push(m);
3303
+ byStage.set(key, list);
3304
+ }
3305
+ for (const kind of FULL_ORDER) {
3306
+ const list = byStage.get(kind);
3307
+ if (!list?.length) continue;
3308
+ list.sort((a, b) => a.kickoff.localeCompare(b.kickoff));
3309
+ const title = kind === "OTHER" ? l.schedule : roundLabel(kind, l);
3310
+ out.write(` ${bold(cyan(title))}
3311
+ `);
3312
+ for (const m of list) {
3313
+ pickable.push(m);
3314
+ const no = String(pickable.length).padStart(3);
3315
+ out.write(` ${dim(no + ".")} ${matchLine(m, opts.lang)}
3316
+ `);
3317
+ }
3318
+ out.write("\n");
3319
+ }
3320
+ return pickable;
3321
+ }
2787
3322
  async function pickMatch(pickable, lang) {
2788
3323
  if (pickable.length === 0) return null;
2789
3324
  const l = labels(lang);
@@ -2805,6 +3340,80 @@ async function pickMatch(pickable, lang) {
2805
3340
  }
2806
3341
  }
2807
3342
 
3343
+ // src/ui/bracketScreen.ts
3344
+ function teamLabel(t, lang) {
3345
+ return t.code === "TBD" ? dim(placeholderLabel(t.name, lang)) : `${t.flag} ${t.name}`;
3346
+ }
3347
+ function slotLine(m, lang, l) {
3348
+ const num = m.matchNumber !== void 0 ? dim(`M${m.matchNumber} `) : "";
3349
+ const home = teamLabel(m.home, lang);
3350
+ const away = teamLabel(m.away, lang);
3351
+ if (isLivePhase(m.phase)) {
3352
+ const t = m.matchTime ? ` ${bold(m.matchTime)}` : "";
3353
+ return `${num}${red("\u25CF")}${t} ${home} ${bold(`${m.score.home} : ${m.score.away}`)} ${away} ${dim(phaseLabel(m.phase, l))}`;
3354
+ }
3355
+ if (m.phase === "FINISHED") {
3356
+ const pens = m.score.penHome !== void 0 ? ` (PSO ${m.score.penHome}-${m.score.penAway})` : "";
3357
+ return `${num}${gray(`FT ${m.home.name} ${m.score.home} : ${m.score.away}${pens} ${m.away.name}`)}`;
3358
+ }
3359
+ return `${num}${dim(kickoffLabel(m.kickoff, lang))} ${home} ${dim("vs")} ${away}`;
3360
+ }
3361
+ var NAME_COL = 22;
3362
+ var NUM_COLS = [5, 3, 3, 3, 5, 4];
3363
+ function renderBracket(matches, opts) {
3364
+ const { lang } = opts;
3365
+ const l = labels(lang);
3366
+ const out = process.stdout;
3367
+ if (opts.staleAt) {
3368
+ const at = new Date(opts.staleAt).toTimeString().slice(0, 5);
3369
+ out.write(yellow(` \u26A0 ${l.offline} \xB7 ${at} ${l.staleData}
3370
+
3371
+ `));
3372
+ }
3373
+ const tables = computeGroupStandings(matches);
3374
+ if (tables.length > 0) out.write(` ${bold(cyan(l.groupStage))}
3375
+
3376
+ `);
3377
+ for (const table of tables) {
3378
+ const title = lang === "ko" ? `${table.group}\uC870` : `Group ${table.group}`;
3379
+ const header = [
3380
+ padEndVisual(` ${bold(title)}`, 5 + NAME_COL),
3381
+ ...l.standingsCols.map((c, i) => padEndVisual(dim(c), NUM_COLS[i]))
3382
+ ].join("");
3383
+ out.write(`${header}
3384
+ `);
3385
+ for (const r of table.rows) {
3386
+ const tied = r.tiedWithNext ? "=" : " ";
3387
+ const cells = [
3388
+ String(r.played),
3389
+ String(r.won),
3390
+ String(r.drawn),
3391
+ String(r.lost),
3392
+ r.goalDiff > 0 ? `+${r.goalDiff}` : String(r.goalDiff),
3393
+ String(r.points)
3394
+ ];
3395
+ const name = truncate(`${r.team.flag} ${r.team.name}`, NAME_COL - 1);
3396
+ out.write(
3397
+ ` ${dim(String(r.rank))}${tied} ${padEndVisual(name, NAME_COL)}` + cells.map((c, i) => padEndVisual(i === 5 ? bold(c) : c, NUM_COLS[i])).join("") + "\n"
3398
+ );
3399
+ }
3400
+ out.write("\n");
3401
+ }
3402
+ const pickable = [];
3403
+ for (const round of buildBracket(matches)) {
3404
+ out.write(` ${bold(cyan(roundLabel(round.kind, l)))}
3405
+ `);
3406
+ for (const m of round.matches) {
3407
+ pickable.push(m);
3408
+ const no = String(pickable.length).padStart(2);
3409
+ out.write(` ${dim(no + ".")} ${slotLine(m, lang, l)}
3410
+ `);
3411
+ }
3412
+ out.write("\n");
3413
+ }
3414
+ return pickable;
3415
+ }
3416
+
2808
3417
  // src/ui/matchScreen.ts
2809
3418
  import * as readline2 from "readline";
2810
3419
 
@@ -2907,6 +3516,27 @@ function formatClock(d) {
2907
3516
  return `${mm}:${ss}`;
2908
3517
  }
2909
3518
 
3519
+ // src/core/text.ts
3520
+ var ALL_CAPS_WORD = /^[A-Z]{2,}$/;
3521
+ function titleCaseWord(w) {
3522
+ return w.toLowerCase().replace(/(^|[-'’])([a-z])/g, (_, sep, ch) => sep + ch.toUpperCase());
3523
+ }
3524
+ function normalizeNamesInText(s) {
3525
+ return s.replace(
3526
+ /[A-Za-z][A-Za-z'’-]*(?: [A-Za-z][A-Za-z'’-]*)+/g,
3527
+ (run) => run.split(" ").map((w) => ALL_CAPS_WORD.test(w.replace(/['’-]/g, "")) ? titleCaseWord(w) : w).join(" ")
3528
+ );
3529
+ }
3530
+ function normalizeEventText(s) {
3531
+ let t = s.replace(/\s{2,}/g, " ").trim();
3532
+ t = t.replace(/(\S) (의|에게) /g, "$1$2 ");
3533
+ t = normalizeNamesInText(t);
3534
+ return t.replace(/(?<![A-Za-z'’-])[A-Z]{4,}(?![A-Za-z'’-])/g, titleCaseWord);
3535
+ }
3536
+ function normalizePlayerName(s) {
3537
+ return s.replace(/\s{2,}/g, " ").trim().split(" ").map((w) => ALL_CAPS_WORD.test(w.replace(/['’-]/g, "")) ? titleCaseWord(w) : w).join(" ");
3538
+ }
3539
+
2910
3540
  // src/ui/animations.ts
2911
3541
  function sleep2(ms) {
2912
3542
  return new Promise((r) => setTimeout(r, ms));
@@ -3028,6 +3658,14 @@ function entranceFrames(match, cols) {
3028
3658
  }
3029
3659
 
3030
3660
  // src/ui/matchScreen.ts
3661
+ var SYSTEM_TYPES = /* @__PURE__ */ new Set([
3662
+ "PERIOD_START",
3663
+ "PERIOD_END",
3664
+ "FULLTIME",
3665
+ "BREAK",
3666
+ "RESUMED",
3667
+ "ADDED_TIME"
3668
+ ]);
3031
3669
  var MatchScreen = class {
3032
3670
  constructor(match, feed, opts) {
3033
3671
  this.match = match;
@@ -3036,9 +3674,8 @@ var MatchScreen = class {
3036
3674
  this.l = labels(opts.lang);
3037
3675
  this.clock = new ClockInterpolator(void 0, opts.clockRate ?? 1);
3038
3676
  this.controller = new AnimationController((frame) => {
3039
- clearScreen(this.t.out);
3040
3677
  const pad = Math.max(0, Math.floor((this.t.rows() - frame.length) / 2));
3041
- this.t.out.write("\n".repeat(pad) + frame.join("\n") + "\n");
3678
+ paintFrame(this.t.out, [...Array(pad).fill(""), ...frame]);
3042
3679
  });
3043
3680
  if (match.phase !== "SCHEDULED") {
3044
3681
  this.clock.update({ minute: 0, phase: match.phase });
@@ -3060,6 +3697,10 @@ var MatchScreen = class {
3060
3697
  flashText = "";
3061
3698
  finished = false;
3062
3699
  sourceSwitchedLabel = false;
3700
+ keyEvents = [];
3701
+ cancelledIds = /* @__PURE__ */ new Set();
3702
+ boundariesMarked = /* @__PURE__ */ new Set();
3703
+ lastPhase = null;
3063
3704
  async run() {
3064
3705
  const { out } = this.t;
3065
3706
  enterAltScreen(out);
@@ -3110,26 +3751,48 @@ var MatchScreen = class {
3110
3751
  injury: s.injury,
3111
3752
  phase: s.phase
3112
3753
  });
3754
+ if (this.lastPhase !== null && s.phase !== this.lastPhase) {
3755
+ this.markBoundary(s.phase, s.score);
3756
+ }
3757
+ this.lastPhase = s.phase;
3113
3758
  });
3114
3759
  this.feed.on("events", (events) => {
3115
3760
  this.opts.onEvents?.(events);
3116
- const formatted = events.filter((e) => e.type !== "ASSIST" || e.text).map((e) => this.formatEvent(e));
3117
- if (this.controller.mode === "ANIMATION") this.pending.push(...formatted);
3761
+ const entries = [];
3762
+ for (const e of events) {
3763
+ if (e.type === "ASSIST" && !e.text) continue;
3764
+ const isGoal = e.type === "GOAL" || e.type === "PENALTY_GOAL" || e.type === "OWN_GOAL";
3765
+ if (isGoal) entries.push({ kind: "sep" });
3766
+ entries.push({ kind: "event", e });
3767
+ if (isGoal) entries.push({ kind: "sep" });
3768
+ }
3769
+ this.keyEvents.push(...events);
3770
+ if (this.controller.mode === "ANIMATION") this.pending.push(...entries);
3118
3771
  else {
3119
- this.lines.push(...formatted);
3772
+ this.lines.push(...entries);
3120
3773
  this.render();
3121
3774
  }
3775
+ for (const e of events) {
3776
+ const boundary = boundaryPhaseOf(e);
3777
+ if (boundary) {
3778
+ this.markBoundary(
3779
+ boundary,
3780
+ e.scoreAfter ?? this.state?.score ?? this.match.score
3781
+ );
3782
+ }
3783
+ }
3122
3784
  });
3123
3785
  this.feed.on("goal", (e) => {
3124
3786
  const side = e.teamSide;
3125
3787
  const teamName = side ? this.match[side].name : void 0;
3126
3788
  const flag = side ? this.match[side].flag : "\u26BD";
3127
3789
  this.flashUntil = Date.now() + 6e3;
3128
- this.flashText = e.player ? `GOAL! ${e.player}` : "GOAL!";
3790
+ const player = e.player ? normalizePlayerName(e.player) : void 0;
3791
+ this.flashText = player ? `GOAL! ${player}` : "GOAL!";
3129
3792
  if (this.opts.animations) {
3130
3793
  void this.controller.enqueue(
3131
3794
  goalFrames({
3132
- player: e.player,
3795
+ player,
3133
3796
  teamName,
3134
3797
  flag,
3135
3798
  cols: this.t.cols()
@@ -3141,12 +3804,17 @@ var MatchScreen = class {
3141
3804
  }
3142
3805
  });
3143
3806
  this.feed.on("cancelled", (e) => {
3144
- const line = `${dim(formatEventClock(e).padStart(7))} ${red(`\u{1F4FA} ${this.l.goalCancelled} (${this.l.corrected})`)} ${dim(e.text ?? "")}`;
3145
- this.pushLine(line);
3807
+ this.cancelledIds.add(e.id);
3808
+ const line = `${dim(formatEventClock(e).padStart(7))} ${red(`\u{1F4FA} ${this.l.goalCancelled} (${this.l.corrected})`)} ${dim(e.text ? normalizeEventText(e.text) : "")}`;
3809
+ this.pushLine({ kind: "text", text: line });
3146
3810
  });
3147
3811
  this.feed.on("sourceSwitched", () => {
3148
3812
  this.sourceSwitchedLabel = true;
3149
- this.pushLine(yellow(`\u2500\u2500 ${this.l.sourceSwitched} \u2500\u2500`));
3813
+ this.pushLine({
3814
+ kind: "text",
3815
+ text: yellow(`\u2500\u2500 ${this.l.sourceSwitched} \u2500\u2500`),
3816
+ align: "center"
3817
+ });
3150
3818
  });
3151
3819
  this.feed.on("net", (info) => {
3152
3820
  this.net = info;
@@ -3164,24 +3832,95 @@ var MatchScreen = class {
3164
3832
  this.render();
3165
3833
  }
3166
3834
  }
3835
+ /** Insert a period-boundary score block, once per boundary phase. */
3836
+ markBoundary(phase, score) {
3837
+ if (!BOUNDARY_PHASES.has(phase) || this.boundariesMarked.has(phase))
3838
+ return;
3839
+ this.boundariesMarked.add(phase);
3840
+ this.pushLine({ kind: "boundary", phase, score: { ...score } });
3841
+ }
3167
3842
  flushPending() {
3168
3843
  if (this.pending.length > 0) {
3169
3844
  this.lines.push(...this.pending);
3170
3845
  this.pending = [];
3171
3846
  }
3172
3847
  }
3173
- formatEvent(e) {
3174
- const clock = dim(formatEventClock(e).padStart(7));
3848
+ formatEvent(e, cols) {
3175
3849
  const icon = EVENT_ICON[e.type] ?? "\xB7";
3176
- let text = e.text ?? "";
3850
+ let text = e.text ? normalizeEventText(e.text) : "";
3177
3851
  if (e.type === "GOAL" || e.type === "PENALTY_GOAL" || e.type === "OWN_GOAL")
3178
3852
  text = bold(yellow(text));
3179
3853
  else if (e.type === "RED") text = bold(red(text));
3180
3854
  else if (e.type === "YELLOW") text = yellow(text);
3181
3855
  else if (e.type === "PERIOD_START" || e.type === "PERIOD_END" || e.type === "FULLTIME")
3182
3856
  text = cyan(text);
3183
- else if (e.type === "UNKNOWN") text = dim(text);
3184
- return `${clock} ${icon} ${text}`;
3857
+ else if (e.type === "FOUL" || e.type === "OFFSIDE" || e.type === "UNKNOWN")
3858
+ text = dim(text);
3859
+ if (SYSTEM_TYPES.has(e.type))
3860
+ return center(`${dim(formatEventClock(e))} ${icon} ${text}`, cols);
3861
+ const line = `${dim(formatEventClock(e).padStart(7))} ${icon} ${text}`;
3862
+ if (e.teamSide === "away") {
3863
+ const pad = cols - 1 - width(line);
3864
+ if (pad > 0) return " ".repeat(pad) + line;
3865
+ }
3866
+ return line;
3867
+ }
3868
+ renderLine(l, cols) {
3869
+ if (l.kind === "sep")
3870
+ return center(dim("\u2500".repeat(Math.min(24, Math.max(8, cols - 8)))), cols);
3871
+ if (l.kind === "boundary") {
3872
+ const label = phaseLabel(l.phase, this.l);
3873
+ const pens = l.score.penHome !== void 0 ? ` (PSO ${l.score.penHome}-${l.score.penAway})` : "";
3874
+ return center(
3875
+ cyan(
3876
+ `\u2501\u2501 ${label} \xB7 ${l.score.home} : ${l.score.away}${pens} \u2501\u2501`
3877
+ ),
3878
+ cols
3879
+ );
3880
+ }
3881
+ if (l.kind === "text") return l.align === "center" ? center(l.text, cols) : l.text;
3882
+ return this.formatEvent(l.e, cols);
3883
+ }
3884
+ /**
3885
+ * One-line pinned summary of goals/cards under the header. Kept to one
3886
+ * row: when it overflows, the oldest items drop first (newest stay).
3887
+ */
3888
+ highlightStrip(cols) {
3889
+ const items = deriveHighlights(this.keyEvents, this.cancelledIds);
3890
+ if (items.length === 0) return "";
3891
+ const parts = items.map((e) => {
3892
+ const icon = EVENT_ICON[e.type] ?? "";
3893
+ const who = e.player ? normalizePlayerName(e.player).split(" ").pop() ?? "" : e.teamCode ?? "";
3894
+ return `${icon}${formatEventClock(e)} ${who}`.trim();
3895
+ });
3896
+ const joined = () => parts.join(" \xB7 ");
3897
+ while (parts.length > 1 && width(joined()) > cols - 4) parts.shift();
3898
+ return center(dim(joined()), cols);
3899
+ }
3900
+ /** Shown in the body before any commentary has arrived. */
3901
+ waitingCard(bodyRows, cols) {
3902
+ const kickoff = new Intl.DateTimeFormat(
3903
+ this.opts.lang === "ko" ? "ko-KR" : "en-US",
3904
+ { month: "short", day: "numeric", weekday: "short", hour: "2-digit", minute: "2-digit" }
3905
+ ).format(new Date(this.match.kickoff));
3906
+ const phase = this.state?.phase ?? this.match.phase;
3907
+ let status = dim(this.l.loading);
3908
+ if (phase === "SCHEDULED" && this.opts.mode === "live") {
3909
+ const ms = new Date(this.match.kickoff).getTime() - Date.now();
3910
+ status = ms > 0 ? `${dim(this.l.kickoffIn)} ${bold(green(fmtCountdown(ms)))}` : dim(this.l.waitingKickoff);
3911
+ }
3912
+ const card = [
3913
+ this.match.stage ? center(dim(this.match.stage), cols) : "",
3914
+ center(
3915
+ `${this.match.home.flag} ${bold(this.match.home.name)} ${dim("vs")} ${bold(this.match.away.name)} ${this.match.away.flag}`,
3916
+ cols
3917
+ ),
3918
+ center(dim(`\u23F1 ${kickoff}`), cols),
3919
+ "",
3920
+ center(status, cols)
3921
+ ];
3922
+ const pad = Math.max(0, Math.floor((bodyRows - card.length) / 2));
3923
+ return [...Array(pad).fill(""), ...card].slice(0, bodyRows);
3185
3924
  }
3186
3925
  winprob() {
3187
3926
  if (this.opts.lambdas === null) return null;
@@ -3224,20 +3963,40 @@ var MatchScreen = class {
3224
3963
  }
3225
3964
  const header2 = center(line2, cols);
3226
3965
  const sep = dim("\u2500".repeat(Math.max(4, cols - 2)));
3227
- const bodyRows = Math.max(3, rows - 6);
3228
- const body = this.lines.slice(-bodyRows).map((l) => truncate(l, cols - 1));
3966
+ const strip = this.highlightStrip(cols);
3967
+ const bodyRows = Math.max(3, rows - 7);
3968
+ let body;
3969
+ if (this.lines.length === 0) {
3970
+ body = this.waitingCard(bodyRows, cols);
3971
+ } else {
3972
+ const nowMinute = Math.floor(this.clock.display().totalSeconds / 60);
3973
+ const { visible, foldedCount } = foldTimeline(
3974
+ this.lines,
3975
+ nowMinute,
3976
+ (l) => l.kind === "event" ? l.e : null
3977
+ );
3978
+ const rendered = foldedCount > 0 ? [
3979
+ center(
3980
+ dim(
3981
+ `\u22EF ${this.l.foldedNotice.replace("{n}", String(foldedCount))} \u22EF`
3982
+ ),
3983
+ cols
3984
+ ),
3985
+ ...visible.slice(-(bodyRows - 1)).map((l) => this.renderLine(l, cols))
3986
+ ] : visible.slice(-bodyRows).map((l) => this.renderLine(l, cols));
3987
+ body = rendered.map((l) => truncate(l, cols - 1));
3988
+ }
3229
3989
  const footer = this.footerLine();
3230
- const frame = [
3990
+ paintFrame(out, [
3231
3991
  truncate(header1, cols),
3232
3992
  truncate(header2, cols),
3993
+ truncate(strip, cols),
3233
3994
  sep,
3234
3995
  ...body,
3235
3996
  ...Array(Math.max(0, bodyRows - body.length)).fill(""),
3236
3997
  sep,
3237
3998
  truncate(footer, cols)
3238
- ].join("\n");
3239
- clearScreen(out);
3240
- out.write(frame);
3999
+ ]);
3241
4000
  }
3242
4001
  footerLine() {
3243
4002
  const l = this.l;
@@ -3258,7 +4017,7 @@ var MatchScreen = class {
3258
4017
  // package.json
3259
4018
  var package_default = {
3260
4019
  name: "termfc",
3261
- version: "0.2.0",
4020
+ version: "0.3.0",
3262
4021
  description: "\u26BD Live football text commentary in your terminal \u2014 FIFA World Cup 2026",
3263
4022
  type: "module",
3264
4023
  bin: {
@@ -3338,7 +4097,8 @@ var HELP = `
3338
4097
  ${bold("usage")}
3339
4098
  termfc ${dim("live + upcoming matches, pick one to join")}
3340
4099
  termfc live ${dim("matches in progress")}
3341
- termfc schedule ${dim("full schedule window")}
4100
+ termfc schedule ${dim("full tournament schedule, pick one to join")}
4101
+ termfc bracket ${dim("group tables + knockout bracket")}
3342
4102
  termfc watch <team|id> ${dim("join a match (e.g. termfc watch KOR)")}
3343
4103
  termfc replay [file] ${dim("replay a recorded match (no arg: list recordings)")}
3344
4104
  termfc demo ${dim("bundled demo match (no network needed)")}
@@ -3377,26 +4137,58 @@ async function main() {
3377
4137
  await replayCommand(args[0], lang, flags);
3378
4138
  return;
3379
4139
  }
3380
- const { matches, staleAt } = await loadSchedule(provider, espn, lang);
4140
+ const { matches, staleAt, coverage } = await loadSchedule(provider, espn, lang);
3381
4141
  if (matches.length === 0) {
3382
4142
  console.log(red(` ${l.noData}
3383
4143
  `));
3384
4144
  process.exitCode = 1;
3385
4145
  return;
3386
4146
  }
4147
+ if (cmd === "bracket") {
4148
+ let data = { matches, staleAt, coverage };
4149
+ if (data.coverage !== "full") {
4150
+ const cachedFull = readScheduleCache(lang);
4151
+ if (cachedFull?.coverage === "full") {
4152
+ data = {
4153
+ matches: cachedFull.matches,
4154
+ staleAt: cachedFull.fetchedAt,
4155
+ coverage: "full"
4156
+ };
4157
+ } else {
4158
+ console.log(red(` ${l.noBracketData}
4159
+ `));
4160
+ process.exitCode = 1;
4161
+ return;
4162
+ }
4163
+ }
4164
+ const pickable2 = renderBracket(data.matches, {
4165
+ lang,
4166
+ staleAt: data.staleAt
4167
+ });
4168
+ if (process.stdin.isTTY && pickable2.length > 0) {
4169
+ const choice2 = await pickMatch(pickable2, lang);
4170
+ if (choice2) await enterMatch(choice2, provider, espn, lang, flags);
4171
+ }
4172
+ return;
4173
+ }
3387
4174
  if (cmd === "schedule") {
3388
- renderList(matches, { lang, staleAt });
4175
+ const pickable2 = renderFullSchedule(matches, { lang, staleAt });
4176
+ if (process.stdin.isTTY && pickable2.length > 0) {
4177
+ const choice2 = await pickMatch(pickable2, lang);
4178
+ if (choice2) await enterMatch(choice2, provider, espn, lang, flags);
4179
+ }
3389
4180
  return;
3390
4181
  }
3391
4182
  if (cmd === "live") {
3392
- const live = matches.filter((m) => isLivePhase(m.phase));
4183
+ const near = nearWindow(matches);
4184
+ const live = near.filter((m) => isLivePhase(m.phase));
3393
4185
  const pickable2 = renderList(
3394
- live.length > 0 ? live : matches.filter((m) => m.phase === "SCHEDULED").slice(0, 5),
4186
+ live.length > 0 ? live : near.filter((m) => m.phase === "SCHEDULED").slice(0, 5),
3395
4187
  { lang, staleAt }
3396
4188
  );
3397
4189
  if (live.length === 0) return;
3398
4190
  const choice2 = await pickMatch(pickable2, lang);
3399
- if (choice2) await watch(choice2, provider, lang, flags);
4191
+ if (choice2) await enterMatch(choice2, provider, espn, lang, flags);
3400
4192
  return;
3401
4193
  }
3402
4194
  if (cmd === "watch") {
@@ -3412,37 +4204,52 @@ async function main() {
3412
4204
  process.exitCode = 1;
3413
4205
  return;
3414
4206
  }
3415
- await watch(match, provider, lang, flags);
4207
+ await enterMatch(match, provider, espn, lang, flags);
3416
4208
  return;
3417
4209
  }
3418
4210
  if (cmd !== void 0) {
3419
4211
  console.log(HELP);
3420
4212
  return;
3421
4213
  }
3422
- const pickable = renderList(matches, { lang, staleAt });
4214
+ const pickable = renderList(nearWindow(matches), { lang, staleAt });
3423
4215
  const choice = await pickMatch(pickable, lang);
3424
- if (choice) await watch(choice, provider, lang, flags);
4216
+ if (choice) await enterMatch(choice, provider, espn, lang, flags);
3425
4217
  }
3426
4218
  async function loadSchedule(provider, espn, lang) {
3427
4219
  const cached = readScheduleCache(lang);
3428
4220
  if (cached && isScheduleFresh(cached)) {
3429
- return { matches: cached.matches, staleAt: null };
4221
+ return { matches: cached.matches, staleAt: null, coverage: cached.coverage };
3430
4222
  }
3431
4223
  try {
3432
4224
  const matches = await provider.fetchSchedule(lang);
3433
- if (provider.activeSource === "fifa") {
4225
+ const source = provider.activeSource;
4226
+ const coverage = source === "fifa" ? "full" : "window";
4227
+ if (source === "fifa") {
3434
4228
  try {
3435
4229
  linkSourceRefs(matches, await espn.fetchSchedule(lang));
3436
4230
  } catch {
3437
4231
  }
3438
4232
  }
3439
- writeScheduleCache(matches, lang);
3440
- return { matches, staleAt: null };
4233
+ writeScheduleCache(matches, lang, source, coverage);
4234
+ return { matches, staleAt: null, coverage };
3441
4235
  } catch {
3442
- if (cached) return { matches: cached.matches, staleAt: cached.fetchedAt };
3443
- return { matches: [], staleAt: null };
4236
+ if (cached)
4237
+ return {
4238
+ matches: cached.matches,
4239
+ staleAt: cached.fetchedAt,
4240
+ coverage: cached.coverage
4241
+ };
4242
+ return { matches: [], staleAt: null, coverage: "window" };
3444
4243
  }
3445
4244
  }
4245
+ function nearWindow(matches, now = Date.now()) {
4246
+ const from = now - 36 * 36e5;
4247
+ const to = now + 8 * 864e5;
4248
+ return matches.filter((m) => {
4249
+ const k = new Date(m.kickoff).getTime();
4250
+ return k >= from && k <= to;
4251
+ });
4252
+ }
3446
4253
  function findMatch(matches, ident) {
3447
4254
  const byId = matches.find((m) => m.id === ident);
3448
4255
  if (byId) return byId;
@@ -3453,7 +4260,16 @@ function findMatch(matches, ident) {
3453
4260
  if (hits.length === 0) return null;
3454
4261
  return hits.find((m) => isLivePhase(m.phase)) ?? hits.find((m) => m.phase === "SCHEDULED") ?? hits[hits.length - 1] ?? null;
3455
4262
  }
3456
- async function watch(match, provider, lang, flags) {
4263
+ async function enterMatch(match, provider, espn, lang, flags) {
4264
+ if (!match.sourceRefs.espn && match.home.code !== "TBD") {
4265
+ try {
4266
+ linkSourceRefs(
4267
+ [match],
4268
+ await espn.fetchDaySchedule(new Date(match.kickoff), lang)
4269
+ );
4270
+ } catch {
4271
+ }
4272
+ }
3457
4273
  const lambdas = await fetchPreMatchLambdas(match);
3458
4274
  const engine = new PollingEngine(provider, match, lang);
3459
4275
  let recorder = null;