44 * - Accepted のみ残す
55 * - スピーカー情報をセッションへ埋め込む
66 * - categoryItems から type / difficulty / duration を解決する(欠けていればエラー)
7+ * - 登壇資料 URL は埋め込み形式へ変換し slidesUrl として残す
8+ * (想定外ドメインは元 URL。変換不可は needsManual。既存の URL は上書きしない)
79 * - 部屋・開始時刻は Sessionize に含まれないため、ここでは扱わない
810 * (sessionGrid.ts / schedule.ts で手動紐づけする)
911 *
1315import fs from "node:fs" ;
1416import path from "node:path" ;
1517import { fileURLToPath } from "node:url" ;
18+ import {
19+ isPreservableSlidesUrl ,
20+ parseSlideEmbed ,
21+ SLIDES_URL_NEEDS_MANUAL ,
22+ toSlidesUrl ,
23+ } from "../src/components/timetable/parseSlideEmbed.ts" ;
1624
1725const rootDir = path . resolve (
1826 path . dirname ( fileURLToPath ( import . meta. url ) ) ,
@@ -59,6 +67,7 @@ type ShapedSession = {
5967 speaker : Speaker ;
6068 description : string ;
6169 duration ?: WorkshopDuration ;
70+ slidesUrl ?: string ;
6271} ;
6372
6473type ShapedData = {
@@ -80,15 +89,23 @@ type RawSpeaker = {
8089 links ?: RawSpeakerLink [ ] ;
8190} ;
8291
92+ type RawQuestionAnswer = {
93+ questionId : number ;
94+ answerValue ?: string | null ;
95+ } ;
96+
8397type RawSession = {
8498 id : string ;
8599 title : string ;
86100 description ?: string ;
87101 speakers ?: string [ ] ;
88102 categoryItems ?: number [ ] ;
103+ questionAnswers ?: RawQuestionAnswer [ ] ;
89104 status : string ;
90105} ;
91106
107+ const SLIDES_QUESTION_ID = 138931 ;
108+
92109type RawData = {
93110 sessions ?: RawSession [ ] ;
94111 speakers ?: RawSpeaker [ ] ;
@@ -111,6 +128,53 @@ function normalizeDescription(description: string): string {
111128 return description . replace ( / \r \n / g, "\n" ) ;
112129}
113130
131+ function extractSlidesUrl ( session : RawSession ) : string | undefined {
132+ const answer = ( session . questionAnswers ?? [ ] ) . find (
133+ ( qa ) => qa . questionId === SLIDES_QUESTION_ID ,
134+ ) ;
135+ const value = answer ?. answerValue ?. trim ( ) ;
136+ if ( ! value ) {
137+ return undefined ;
138+ }
139+
140+ return toSlidesUrl ( parseSlideEmbed ( value ) ) ;
141+ }
142+
143+ function resolveSlidesUrl (
144+ session : RawSession ,
145+ existingSlidesUrls : Map < string , string > ,
146+ ) : string | undefined {
147+ const parsed = extractSlidesUrl ( session ) ;
148+ const existing = existingSlidesUrls . get ( session . id ) ;
149+
150+ if (
151+ existing &&
152+ isPreservableSlidesUrl ( existing ) &&
153+ ( ! parsed || parsed === SLIDES_URL_NEEDS_MANUAL )
154+ ) {
155+ return existing ;
156+ }
157+
158+ return parsed ;
159+ }
160+
161+ function loadExistingSlidesUrls ( filePath : string ) : Map < string , string > {
162+ const map = new Map < string , string > ( ) ;
163+ if ( ! fs . existsSync ( filePath ) ) {
164+ return map ;
165+ }
166+
167+ const existing = JSON . parse ( fs . readFileSync ( filePath , "utf8" ) ) as {
168+ sessions ?: Array < { id ?: string ; slidesUrl ?: string } > ;
169+ } ;
170+ for ( const session of existing . sessions ?? [ ] ) {
171+ if ( session . id && session . slidesUrl ) {
172+ map . set ( session . id , session . slidesUrl ) ;
173+ }
174+ }
175+ return map ;
176+ }
177+
114178function requireExactlyOne < T > (
115179 categoryItems : number [ ] ,
116180 mapping : ReadonlyArray < readonly [ number , T ] > ,
@@ -153,6 +217,7 @@ function parseSpeaker(rawSpeaker: RawSpeaker, sessionId: string): Speaker {
153217function transformSession (
154218 session : RawSession ,
155219 speakerMap : Map < string , RawSpeaker > ,
220+ existingSlidesUrls : Map < string , string > ,
156221) : ShapedSession {
157222 const categoryItems = session . categoryItems ;
158223 if ( ! categoryItems || categoryItems . length === 0 ) {
@@ -208,10 +273,18 @@ function transformSession(
208273 shaped . duration = proposal . duration ;
209274 }
210275
276+ const slidesUrl = resolveSlidesUrl ( session , existingSlidesUrls ) ;
277+ if ( slidesUrl ) {
278+ shaped . slidesUrl = slidesUrl ;
279+ }
280+
211281 return shaped ;
212282}
213283
214- function transform ( raw : RawData ) : ShapedData {
284+ function transform (
285+ raw : RawData ,
286+ existingSlidesUrls : Map < string , string > ,
287+ ) : ShapedData {
215288 if ( ! raw . sessions ) {
216289 throw new Error ( "rawData.json に sessions がありません" ) ;
217290 }
@@ -225,7 +298,9 @@ function transform(raw: RawData): ShapedData {
225298
226299 const sessions = raw . sessions
227300 . filter ( ( session ) => session . status === "Accepted" )
228- . map ( ( session ) => transformSession ( session , speakerMap ) ) ;
301+ . map ( ( session ) =>
302+ transformSession ( session , speakerMap , existingSlidesUrls ) ,
303+ ) ;
229304
230305 return { sessions } ;
231306}
@@ -240,7 +315,8 @@ function main(): void {
240315 }
241316
242317 const raw = JSON . parse ( fs . readFileSync ( inputPath , "utf8" ) ) as RawData ;
243- const shaped = transform ( raw ) ;
318+ const existingSlidesUrls = loadExistingSlidesUrls ( outputPath ) ;
319+ const shaped = transform ( raw , existingSlidesUrls ) ;
244320 fs . writeFileSync ( outputPath , `${ JSON . stringify ( shaped , null , 2 ) } \n` , "utf8" ) ;
245321
246322 console . log ( `Wrote ${ outputPath } (${ shaped . sessions . length } sessions)` ) ;
0 commit comments