Skip to content

Commit 0e6a30d

Browse files
committed
Properly get speakers and schedule
1 parent 8c17d58 commit 0e6a30d

File tree

2 files changed

+173
-14
lines changed

2 files changed

+173
-14
lines changed

scripts/sync-sched/sync.ts

Lines changed: 170 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
import assert from "node:assert"
44
import { parseArgs } from "node:util"
5+
import { readFileSync, existsSync } from "node:fs"
6+
import { join } from "node:path"
57

68
import { getSchedule, getSpeakers } from "@/app/conf/_api/sched-client"
9+
import { SchedSpeaker, ScheduleSession } from "@/app/conf/_api/sched-types"
10+
import { writeFile } from "node:fs/promises"
711

812
// Sched API rate limit is 30 requests per minute per token.
913
// This scripts fires:
@@ -23,7 +27,7 @@ const options = {
2327
},
2428
}
2529

26-
function main() {
30+
async function main() {
2731
try {
2832
const { values } = parseArgs({ options })
2933

@@ -39,8 +43,7 @@ function main() {
3943
const token = process.env[`SCHED_ACCESS_TOKEN_${year}`]
4044
assert(token, `SCHED_ACCESS_TOKEN_${year} is not set`)
4145

42-
// TODO: Implement sync logic here
43-
// You can now use the `year` variable in your sync logic
46+
await sync(year, token)
4447
} catch (error) {
4548
if (error instanceof Error && error.message.includes("Unknown option")) {
4649
console.error(`Error: ${error.message}`)
@@ -52,7 +55,20 @@ function main() {
5255
}
5356

5457
if (require.main === module) {
55-
main()
58+
void main()
59+
}
60+
61+
function readJsonFile<T>(filePath: string, defaultValue: T): T {
62+
if (!existsSync(filePath)) {
63+
return defaultValue
64+
}
65+
66+
try {
67+
const content = readFileSync(filePath, "utf-8")
68+
return JSON.parse(content)
69+
} catch {
70+
return defaultValue
71+
}
5672
}
5773

5874
async function sync(year: number, token: string) {
@@ -66,18 +82,159 @@ async function sync(year: number, token: string) {
6682

6783
const ctx = { apiUrl, token }
6884

85+
const scriptDir = __dirname
86+
const speakersFilePath = join(scriptDir, "speakers.json")
87+
const scheduleFilePath = join(scriptDir, `schedule-${year}.json`)
88+
89+
const existingSpeakers = readJsonFile<SchedSpeaker[]>(speakersFilePath, [])
90+
const existingSchedule = readJsonFile<ScheduleSession[]>(scheduleFilePath, [])
91+
6992
console.log("Getting schedule and speakers list...")
70-
const [schedule, speakers] = await Promise.all([
71-
getSchedule(ctx),
72-
getSpeakers(ctx),
73-
])
74-
75-
// console.log("Getting speaker details...")
76-
// const speakerDetails = await Promise.all(
77-
// speakers.map(speaker => getSpeakerDetails(ctx, speaker.username)),
78-
// )
93+
94+
const schedule = getSchedule(ctx)
95+
const speakers = getSpeakers(ctx)
96+
97+
const scheduleComparison = compare(existingSchedule, await schedule, "id")
98+
printComparison(scheduleComparison, "sessions", "id")
99+
100+
const writeSchedule = writeFile(
101+
scheduleFilePath,
102+
JSON.stringify(await schedule, null, 2),
103+
)
104+
105+
const speakerComparison = compare(
106+
existingSpeakers,
107+
await speakers,
108+
"username",
109+
)
110+
printComparison(speakerComparison, "speakers", "username")
111+
112+
const updatedSpeakers = [
113+
...speakerComparison.removed,
114+
...speakerComparison.unchanged,
115+
...speakerComparison.changed.map(change => change.new),
116+
...speakerComparison.added.map(speaker => ({
117+
...speaker,
118+
["~syncedAt"]: -1,
119+
})),
120+
].sort((a, b) => a.username.localeCompare(b.username))
121+
122+
const writeSpeakers = writeFile(
123+
speakersFilePath,
124+
JSON.stringify(updatedSpeakers, null, 2),
125+
)
126+
127+
writeSpeakers.then(() => {
128+
console.log(
129+
`Updated speakers data: ${updatedSpeakers.length} total speakers`,
130+
)
131+
})
132+
133+
await writeSchedule
134+
await writeSpeakers
79135
}
80136

81137
function help() {
82138
return console.log("Usage: tsx sync.ts --year <year>")
83139
}
140+
141+
// #region utility
142+
143+
type Change<T> = { old: T; new: T }
144+
type Comparison<T> = {
145+
added: T[]
146+
removed: T[]
147+
changed: Change<T>[]
148+
unchanged: T[]
149+
}
150+
151+
function compare<T extends object>(olds: T[], news: T[], key: keyof T) {
152+
const oldMap = new Map(olds.map(o => [o[key], o]))
153+
const newMap = new Map(news.map(n => [n[key], n]))
154+
155+
const added: T[] = []
156+
const removed: T[] = []
157+
const changed: Change<T>[] = []
158+
const unchanged: T[] = []
159+
160+
for (const newItem of news) {
161+
const oldItem = oldMap.get(newItem[key])
162+
if (oldItem) {
163+
if (JSON.stringify(oldItem) === JSON.stringify(newItem)) {
164+
unchanged.push(oldItem)
165+
} else {
166+
changed.push({ old: oldItem, new: newItem })
167+
}
168+
} else {
169+
added.push(newItem)
170+
}
171+
}
172+
173+
for (const oldItem of olds) {
174+
if (!newMap.has(oldItem[key])) {
175+
removed.push(oldItem)
176+
}
177+
}
178+
179+
return { added, removed, changed, unchanged }
180+
}
181+
182+
function printComparison<T extends object>(
183+
comparison: Comparison<T>,
184+
name: string,
185+
key: keyof T,
186+
) {
187+
console.log(`Removed: ${comparison.removed.length}`)
188+
console.log(`Changed: ${comparison.changed.length}`)
189+
console.log(`Unchanged: ${comparison.unchanged.length}`)
190+
191+
console.log(`Added ${comparison.added.length} ${name}`)
192+
for (const item of comparison.added) {
193+
console.log(`+ ${item}`)
194+
}
195+
196+
console.log(`Removed ${comparison.removed.length} ${name}`)
197+
for (const item of comparison.removed) {
198+
console.log(`- ${item}`)
199+
}
200+
201+
console.log(`Unchanged ${comparison.unchanged.length} ${name}`)
202+
for (const item of comparison.unchanged) {
203+
console.log(item)
204+
}
205+
206+
console.log(`Changed ${comparison.changed.length} ${name}`)
207+
for (const change of comparison.changed) {
208+
console.log(change.new[key], objectDiff(change))
209+
}
210+
}
211+
212+
function objectDiff<T extends object>(change: Change<T>): string {
213+
const allKeys = [
214+
...new Set([
215+
...(Object.keys(change.old) as Array<keyof T>),
216+
...(Object.keys(change.new) as Array<keyof T>),
217+
]),
218+
].sort()
219+
220+
const diff = allKeys
221+
.map(key => {
222+
const oldValue = change.old[key]
223+
const newValue = change.new[key]
224+
225+
if (oldValue === newValue) {
226+
return null
227+
}
228+
229+
return { key, oldValue, newValue }
230+
})
231+
.filter(x => !!x)
232+
233+
return diff
234+
.map(diff => {
235+
return `\x1b[33m${String(diff.key)}\x1b[0m: ${diff.oldValue} -> ${diff.newValue}`
236+
})
237+
.join("\n")
238+
}
239+
240+
// #endregion utility

src/app/conf/_api/sched-types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ export type SchedSpeaker = {
2323
role: string
2424
location?: string
2525
socialurls: { service: string; url: string }[]
26-
year: "2025" | "2024" | "2023"
26+
year?: "2025" | "2024" | "2023"
27+
/* unix timestamp */
28+
["~syncedAt"]?: number
2729
}

0 commit comments

Comments
 (0)