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.
- package/README.md +8 -5
- package/dist/cli.js +982 -166
- 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
|
-
|
|
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=${
|
|
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: "\
|
|
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: "\
|
|
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)
|
|
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)
|
|
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 \
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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 \
|
|
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)
|
|
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)
|
|
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)
|
|
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 \
|
|
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)
|
|
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 \
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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 \
|
|
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: "\
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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 \
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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: "\
|
|
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: "\
|
|
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)
|
|
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)
|
|
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: "
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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 \
|
|
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 \
|
|
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: "
|
|
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: "
|
|
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: "\
|
|
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)
|
|
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)
|
|
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 \
|
|
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)
|
|
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: "
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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: "
|
|
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: "
|
|
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)
|
|
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: "
|
|
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: "
|
|
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: "
|
|
1919
|
-
text: "
|
|
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)
|
|
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: "
|
|
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)
|
|
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)
|
|
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: "
|
|
1986
|
-
text: "
|
|
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)
|
|
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: "
|
|
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: "
|
|
2024
|
-
text: "
|
|
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)
|
|
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: "\
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2453
|
-
|
|
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
|
|
2665
|
-
rows: () => out.rows
|
|
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
|
|
2675
|
-
|
|
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
|
|
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
|
|
3117
|
-
|
|
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(...
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3145
|
-
this.
|
|
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(
|
|
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 === "
|
|
3184
|
-
|
|
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
|
|
3228
|
-
const
|
|
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
|
-
|
|
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
|
-
]
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
4183
|
+
const near = nearWindow(matches);
|
|
4184
|
+
const live = near.filter((m) => isLivePhase(m.phase));
|
|
3393
4185
|
const pickable2 = renderList(
|
|
3394
|
-
live.length > 0 ? live :
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
3443
|
-
|
|
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
|
|
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;
|