1(function (root, factory) { 2 const api = factory(); 3 4 if (typeof module === 'object' && module.exports) { 5 module.exports = api; 6 } 7 8 if (root) { 9 root.S8LiveFootballData = api; 10 } 11})(typeof globalThis !== 'undefined' ? globalThis : this, function () { 12 const SPORTSCORE_ORIGIN = 'https://sportscore.com'; 13 const FIXTURE_PUBLIC_CODE_PATTERN = /^[0-9a-hjkmnp-tv-z]{12}$/i; 14 const FIXTURE_LEGACY_UUID_PATTERN = 15 /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 16 const OVERTIME_SCORE_STATUS_IDS = new Set([5, 6, 7]); 17 const ENDED_STATUS_ID = 8; 18 19 function cleanText(value) { 20 return typeof value === 'string' ? value.trim() : ''; 21 } 22 23 function normalizeFixturePublicId(value) { 24 const normalized = cleanText(String(value || '')).toLowerCase(); 25 return FIXTURE_PUBLIC_CODE_PATTERN.test(normalized) || 26 FIXTURE_LEGACY_UUID_PATTERN.test(normalized) 27 ? normalized 28 : ''; 29 } 30 31 function getFixturePublicId(fixture) { 32 if (!fixture || typeof fixture !== 'object') return ''; 33 34 return ( 35 normalizeFixturePublicId(fixture.public_code) || 36 normalizeFixturePublicId(fixture.public_id) || 37 normalizeFixturePublicId(fixture.id) 38 ); 39 } 40 41 function fixtureHasStreamKey(fixture, streamKey) { 42 const target = cleanText(String(streamKey || '')); 43 if (!target || !fixture || typeof fixture !== 'object') return false; 44 45 const streams = Array.isArray(fixture.streams) 46 ? fixture.streams 47 : Array.isArray(fixture.OBS_stream_key) 48 ? fixture.OBS_stream_key 49 : []; 50 51 return streams.some((stream) => cleanText(String(stream?.stream_key || '')) === target); 52 } 53 54 function resolveFixtureReference(fixtures, fixtureId, streamKey) { 55 const items = Array.isArray(fixtures) ? fixtures : []; 56 const publicId = normalizeFixturePublicId(fixtureId); 57 58 if (publicId) { 59 const exact = items.find((fixture) => getFixturePublicId(fixture) === publicId); 60 if (exact) return exact; 61 } 62 63 // Numeric IDs existed in plugin URLs before public fixture identifiers. 64 // The stream key is immutable and unique per live room, so it can recover 65 // those bookmarks without exposing or re-enabling sequential CMS IDs. 66 return items.find((fixture) => fixtureHasStreamKey(fixture, streamKey)) || null; 67 } 68 69 function removeVietnameseToneMarks(value) { 70 return value 71 .normalize('NFD') 72 .replace(/[\u0300-\u036f]/g, '') 73 .replace(/[\u0111\u0110]/g, 'd'); 74 } 75 76 function sportScoreSlugPart(value) { 77 return removeVietnameseToneMarks(cleanText(value)) 78 .toLowerCase() 79 .replace(/['\u2019]/g, '') 80 .replace(/[^a-z0-9]+/g, '-') 81 .replace(/^-+|-+$/g, '') 82 .replace(/-{2,}/g, '-'); 83 } 84 85 function getExplicitSportScoreSlug(fixture) { 86 return ( 87 cleanText(fixture?.sportscore_slug) || 88 cleanText(fixture?.sportScoreSlug) || 89 cleanText(fixture?.sportscoreSlug) 90 ); 91 } 92 93 function getExplicitSportScoreTrackerUrl(fixture) { 94 const rawUrl = cleanText(fixture?.sportscore_tracker_url); 95 if (!rawUrl) return ''; 96 97 try { 98 const url = new URL(rawUrl); 99 if (url.origin !== SPORTSCORE_ORIGIN) return ''; 100 if (!url.pathname.startsWith('/embed/tracker/football/')) return ''; 101 102 // `src=s8tv` was an internal attribution experiment, not part of the 103 // provider's documented embed contract. Keep existing CMS rows compatible. 104 url.searchParams.delete('src'); 105 return url.toString(); 106 } catch { 107 return ''; 108 } 109 } 110 111 function getFixtureSportScoreSlug(fixture) { 112 // A valid-looking "home-vs-away" guess can point at another match or at 113 // SportScore's unavailable page. Only the worker/CMS mapping is trusted. 114 return sportScoreSlugPart(getExplicitSportScoreSlug(fixture)); 115 } 116 117 function buildSportScoreTrackerUrl(slug, theme) { 118 const safeSlug = sportScoreSlugPart(slug); 119 if (!safeSlug) return ''; 120 121 const url = new URL(`/embed/tracker/football/${safeSlug}/`, SPORTSCORE_ORIGIN); 122 url.searchParams.set('theme', theme === 'light' ? 'light' : 'dark'); 123 return url.toString(); 124 } 125 126 function getFixtureTrackerUrl(fixture, theme) { 127 const explicitUrl = getExplicitSportScoreTrackerUrl(fixture); 128 if (explicitUrl) return explicitUrl; 129 130 return buildSportScoreTrackerUrl(getFixtureSportScoreSlug(fixture), theme); 131 } 132 133 function hasFixtureSimulationData(fixture, theme) {
134 // SportScore owns the upcoming/live/finished presentation. A verified 135 // mapping remains useful even when the score provider status is not live. 136 return Boolean(fixture && getFixtureTrackerUrl(fixture, theme)); 137 } 138 139 function getFixtureScoreReference(fixtureOrReference) { 140 if (!fixtureOrReference || typeof fixtureOrReference !== 'object') return {}; 141 142 const scoreReference = fixtureOrReference.score_reference_id; 143 if (scoreReference && typeof scoreReference === 'object') return scoreReference; 144 145 // An explicit empty score relation means the fixture is unmapped. Never 146 // inherit retired-provider data from another relation in that case. 147 if (Object.prototype.hasOwnProperty.call(fixtureOrReference, 'score_reference_id')) { 148 return {}; 149 } 150 151 return fixtureOrReference; 152 } 153 154 function providerStatusId(fixtureOrReference) { 155 const reference = getFixtureScoreReference(fixtureOrReference); 156 const statusId = Number(reference?.status_id); 157 return Number.isFinite(statusId) ? statusId : 0; 158 } 159 160 function optionalScoreAt(scores, index) { 161 if (!Array.isArray(scores)) return null; 162 const value = scores[index]; 163 if (value === undefined || value === null || value === '') return null; 164 165 const number = Number(value); 166 return Number.isFinite(number) && number >= 0 ? number : null; 167 } 168 169 function scorePairAt(homeScores, awayScores, index) { 170 const left = optionalScoreAt(homeScores, index); 171 const right = optionalScoreAt(awayScores, index); 172 return left === null || right === null ? null : { left, right, index }; 173 } 174 175 function shouldUseOvertimeScore(statusId, regular, overtime) { 176 if (!overtime) return false; 177 178 // API-Football is normalized to the platform score-array contract: 179 // index 0 = regular time, index 5 = 120-minute total. Before extra time 180 // starts, index 5 can be a 0-0 placeholder and must not replace real goals. 181 const looksLikeEmptyPlaceholder = 182 overtime.left === 0 && 183 overtime.right === 0 && 184 regular && 185 (regular.left > 0 || regular.right > 0); 186 if (looksLikeEmptyPlaceholder) return false; 187 188 if (OVERTIME_SCORE_STATUS_IDS.has(statusId)) return true; 189 190 return ( 191 statusId === ENDED_STATUS_ID && 192 regular && 193 (overtime.left !== regular.left || overtime.right !== regular.right) && 194 (overtime.left > 0 || overtime.right > 0) 195 ); 196 } 197 198 function resolveFixtureMainScore(fixtureOrReference) { 199 const reference = getFixtureScoreReference(fixtureOrReference); 200 const homeScores = Array.isArray(reference.home_scores) ? reference.home_scores : []; 201 const awayScores = Array.isArray(reference.away_scores) ? reference.away_scores : []; 202 const regular = scorePairAt(homeScores, awayScores, 0); 203 const overtime = scorePairAt(homeScores, awayScores, 5); 204 205 if (shouldUseOvertimeScore(providerStatusId(reference), regular, overtime)) { 206 return { ...overtime, source: 'overtime' }; 207 } 208 209 return regular ? { ...regular, source: 'regular' } : null; 210 } 211 212 function normalizePreviewElapsed(value) { 213 const rawValue = 214 value && typeof value === 'object' && !Array.isArray(value) 215 ? value.elapsedMs 216 : value; 217 const elapsedMs = Number(rawValue); 218 219 return Number.isFinite(elapsedMs) && elapsedMs > 0 ? elapsedMs : 0; 220 } 221 222 function createPreviewGateStateMachine(options = {}) { 223 const enabled = Boolean(options.enabled); 224 const limitMs = Math.max(1, Number(options.limitMs) || 1); 225 const maxSampleMs = Math.max(250, Number(options.maxSampleMs) || 2000); 226 let elapsedMs = Math.min(limitMs, normalizePreviewElapsed(options.elapsedMs)); 227 let lastTimestamp = null; 228 let state = !enabled ? 'disabled' : elapsedMs >= limitMs ? 'locked' : 'idle'; 229 230 function snapshot(changed = false, lockedNow = false) { 231 return Object.freeze({ 232 state, 233 elapsedMs, 234 remainingMs: Math.max(0, limitMs - elapsedMs), 235 changed, 236 locked: state === 'locked', 237 lockedNow, 238 }); 239 } 240 241 function lock(changed) { 242 const lockedNow = state !== 'locked'; 243 elapsedMs = limitMs; 244 state = 'locked'; 245 lastTimestamp = null; 246 return snapshot(changed || lockedNow, lockedNow); 247 } 248 249 function sample({ playing = false, visible = true, timestamp = 0 } = {}) { 250 if (!enabled || state === 'locked') return snapshot(); 251 252 const nextTimestamp = Number(timestamp); 253 const safeTimestamp = Number.isFinite(nextTimestamp) ? nextTimestamp : 0; 254 const canCount = Boolean(playing && visible); 255 256 if (lastTimestamp === null) { 257 lastTimestamp = safeTimestamp; 258 state = canCount ? 'counting' : 'paused'; 259 return snapshot(); 260 } 261 262 const deltaMs = Math.max(0, safeTimestamp - lastTimestamp); 263 lastTimestamp = safeTimestamp; 264 state = canCount ? 'counting' : 'paused'; 265 266 if (!canCount || deltaMs <= 0) return snapshot(); 267 268 const addedMs = Math.min(deltaMs, maxSampleMs); 269 elapsedMs = Math.min(limitMs, elapsedMs + addedMs); 270 if (elapsedMs >= limitMs) return lock(addedMs > 0); 271
272 return snapshot(addedMs > 0); 273 } 274 275 function sync(value) { 276 if (!enabled || state === 'locked') return snapshot(); 277 278 const nextElapsedMs = Math.min(limitMs, normalizePreviewElapsed(value)); 279 if (nextElapsedMs <= elapsedMs) return snapshot(); 280 281 elapsedMs = nextElapsedMs; 282 if (elapsedMs >= limitMs) return lock(true); 283 284 return snapshot(true); 285 } 286 287 function pause(timestamp = 0) { 288 return sample({ playing: false, visible: false, timestamp }); 289 } 290 291 return Object.freeze({ 292 getSnapshot: () => snapshot(), 293 pause, 294 sample, 295 sync, 296 }); 297 } 298 299 return Object.freeze({ 300 createPreviewGateStateMachine, 301 fixtureHasStreamKey, 302 getExplicitSportScoreTrackerUrl, 303 getFixturePublicId, 304 getFixtureSportScoreSlug, 305 getFixtureTrackerUrl, 306 getFixtureScoreReference, 307 hasFixtureSimulationData, 308 providerStatusId, 309 normalizePreviewElapsed, 310 normalizeFixturePublicId, 311 resolveFixtureReference, 312 resolveFixtureMainScore, 313 }); 314});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.