1export const CHANNEL_SHOW_PRELOAD_LEAD_MS = 180_000; 2export const CHANNEL_SHOW_ACTIVE_WINDOW_MS = 15 * 60_000; 3 4const SHOW_KIND_BY_PROGRAM = new Map([ 5 ["hamoach", "quiz"], 6 ["quiz", "quiz"], 7 ["marketplace-price", "marketplace"], 8 ["price", "marketplace"], 9 ["daily-news", "news"], 10 ["news", "news"], 11 ["music-battle", "music"], 12 ["music", "music"], 13 ["songs-quiz", "songs"], 14 ["songs", "songs"], 15 ["film-ghosts", "ghosts"], 16 ["ghosts", "ghosts"], 17]); 18 19const INACTIVE_QUIZ_STATES = new Set(["off_air", "ended"]); 20 21function timestamp(value) { 22 const parsed = Date.parse(String(value ?? "")); 23 return Number.isFinite(parsed) ? parsed : NaN; 24} 25 26function showKind(slot) { 27 return SHOW_KIND_BY_PROGRAM.get( 28 String(slot?.programKey ?? slot?.program_key ?? "").trim().toLowerCase(), 29 ) ?? null; 30} 31 32export function normalizeChannelSchedule(schedule = []) { 33 const unique = new Map(); 34 for (const slot of Array.isArray(schedule) ? schedule : []) { 35 const kind = showKind(slot); 36 const startsAtMs = timestamp(slot?.startsAt ?? slot?.starts_at); 37 if (!kind || !Number.isFinite(startsAtMs)) continue; 38 const declaredEndsAtMs = timestamp(slot?.endsAt ?? slot?.ends_at); 39 const key = `${kind}:${startsAtMs}`; 40 const candidate = { 41 id: String(slot?.id ?? key), 42 kind, 43 programKey: String(slot?.programKey ?? slot?.program_key ?? "") 44 .trim() 45 .toLowerCase(), 46 startsAtMs, 47 declaredEndsAtMs, 48 }; 49 const existing = unique.get(key); 50 if ( 51 !existing || 52 (Number.isFinite(declaredEndsAtMs) && 53 (!Number.isFinite(existing.declaredEndsAtMs) || 54 declaredEndsAtMs < existing.declaredEndsAtMs)) 55 ) { 56 unique.set(key, candidate); 57 } 58 } 59 return [...unique.values()].sort((left, right) => 60 left.startsAtMs - right.startsAtMs || left.kind.localeCompare(right.kind) 61 ); 62} 63 64function activeUntil(slots, index) { 65 const slot = slots[index]; 66 const nextStart = slots.find((candidate, candidateIndex) => 67 candidateIndex > index && candidate.startsAtMs > slot.startsAtMs 68 )?.startsAtMs; 69 const declaredOrFallback = Number.isFinite(slot.declaredEndsAtMs) && 70 slot.declaredEndsAtMs > slot.startsAtMs 71 ? slot.declaredEndsAtMs 72 : slot.startsAtMs + CHANNEL_SHOW_ACTIVE_WINDOW_MS; 73 return Math.min( 74 declaredOrFallback, 75 Number.isFinite(nextStart) ? nextStart : Number.POSITIVE_INFINITY, 76 ); 77} 78 79function authoritativeQuiz(bootstrap, nowMs) { 80 const show = bootstrap?.show ?? null; 81 const state = String(show?.state ?? "").trim().toLowerCase(); 82 if ( 83 !show || 84 !state || 85 INACTIVE_QUIZ_STATES.has(state) || 86 String(show?.runId ?? show?.run_id ?? "") === "off-air" 87 ) return null; 88 89 const startsAtMs = timestamp( 90 show?.startsAt ?? show?.starts_at ?? show?.scheduledAt ?? 91 show?.scheduled_at, 92 ); 93 if ( 94 state === "scheduled" && Number.isFinite(startsAtMs) && startsAtMs > nowMs 95 ) { 96 return null; 97 } 98 return { 99 id: String(show?.runId ?? show?.run_id ?? "live-quiz"), 100 kind: "quiz", 101 programKey: String( 102 show?.programKey ?? show?.program_key ?? show?.publicPayload?.programKey ?? "hamoach", 103 ).trim().toLowerCase(), 104 startsAtMs: Number.isFinite(startsAtMs) ? startsAtMs : nowMs, 105 activeUntilMs: nowMs + CHANNEL_SHOW_ACTIVE_WINDOW_MS, 106 source: "authoritative-live-state", 107 }; 108} 109 110export function buildChannelShowLoadPlan( 111 bootstrap, 112 nowMs = Date.now(), 113 preloadLeadMs = CHANNEL_SHOW_PRELOAD_LEAD_MS, 114) { 115 const slots = normalizeChannelSchedule(bootstrap?.schedule); 116 let active = authoritativeQuiz(bootstrap, nowMs); 117 118 if (!active) { 119 for (let index = slots.length - 1; index >= 0; index -= 1) { 120 const slot = slots[index]; 121 const activeUntilMs = activeUntil(slots, index); 122 if (slot.startsAtMs <= nowMs && nowMs < activeUntilMs) { 123 active = { ...slot, activeUntilMs, source: "schedule" }; 124 break; 125 } 126 } 127 } 128 129 const next = slots.find((slot) => slot.startsAtMs > nowMs) ?? null; 130 const upcoming = next && next.startsAtMs - nowMs <= preloadLeadMs 131 ? { ...next, source: "upcoming" } 132 : null; 133 const immediate = []; 134 if (active) immediate.push({ ...active, phase: "active" }); 135 if ( 136 upcoming && !immediate.some((candidate) => candidate.kind === upcoming.kind) 137 ) { 138 immediate.push({ ...upcoming, phase: "upcoming" }); 139 } 140 141 const reevaluationCandidates = [ 142 active?.activeUntilMs, 143 next && next.startsAtMs - preloadLeadMs > nowMs 144 ? next.startsAtMs - preloadLeadMs 145 : null, 146 upcoming?.startsAtMs, 147 ].filter((value) => Number.isFinite(value) && value > nowMs + 250); 148 149 return { 150 nowMs, 151 active, 152 next, 153 upcoming, 154 immediate, 155 reevaluateAtMs: reevaluationCandidates.length 156 ? Math.min(...reevaluationCandidates) 157 : null, 158 }; 159}
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.