@churchapps/content-provider-helper 0.0.2 → 0.0.4

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/dist/index.cjs CHANGED
@@ -21,32 +21,48 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  APlayProvider: () => APlayProvider,
24
+ ApiHelper: () => ApiHelper,
24
25
  B1ChurchProvider: () => B1ChurchProvider,
25
26
  BibleProjectProvider: () => BibleProjectProvider,
26
27
  ContentProvider: () => ContentProvider,
28
+ DEFAULT_DURATION_CONFIG: () => DEFAULT_DURATION_CONFIG,
29
+ DeviceFlowHelper: () => DeviceFlowHelper,
27
30
  FormatConverters: () => FormatConverters_exports,
28
31
  FormatResolver: () => FormatResolver,
32
+ HighVoltageKidsProvider: () => HighVoltageKidsProvider,
29
33
  LessonsChurchProvider: () => LessonsChurchProvider,
34
+ OAuthHelper: () => OAuthHelper,
30
35
  PlanningCenterProvider: () => PlanningCenterProvider,
31
36
  SignPresenterProvider: () => SignPresenterProvider,
37
+ TokenHelper: () => TokenHelper,
32
38
  VERSION: () => VERSION,
33
- collapseInstructions: () => collapseInstructions,
39
+ appendToPath: () => appendToPath,
40
+ buildPath: () => buildPath,
41
+ countWords: () => countWords,
42
+ createFile: () => createFile,
43
+ createFolder: () => createFolder,
34
44
  detectMediaType: () => detectMediaType,
45
+ estimateDuration: () => estimateDuration,
46
+ estimateImageDuration: () => estimateImageDuration,
47
+ estimateTextDuration: () => estimateTextDuration,
35
48
  expandedInstructionsToPlaylist: () => expandedInstructionsToPlaylist,
36
49
  expandedInstructionsToPresentations: () => expandedInstructionsToPresentations,
50
+ generatePath: () => generatePath,
37
51
  getAllProviders: () => getAllProviders,
38
52
  getAvailableProviders: () => getAvailableProviders,
39
53
  getProvider: () => getProvider,
40
54
  getProviderConfig: () => getProviderConfig,
55
+ getSegment: () => getSegment,
41
56
  instructionsToPlaylist: () => instructionsToPlaylist,
42
57
  instructionsToPresentations: () => instructionsToPresentations,
43
58
  isContentFile: () => isContentFile,
44
59
  isContentFolder: () => isContentFolder,
60
+ navigateToPath: () => navigateToPath,
61
+ parsePath: () => parsePath,
45
62
  playlistToExpandedInstructions: () => playlistToExpandedInstructions,
46
63
  playlistToInstructions: () => playlistToInstructions,
47
64
  playlistToPresentations: () => playlistToPresentations,
48
65
  presentationsToExpandedInstructions: () => presentationsToExpandedInstructions,
49
- presentationsToInstructions: () => presentationsToInstructions,
50
66
  presentationsToPlaylist: () => presentationsToPlaylist,
51
67
  registerProvider: () => registerProvider
52
68
  });
@@ -66,11 +82,92 @@ function detectMediaType(url, explicitType) {
66
82
  const videoPatterns = [".mp4", ".webm", ".m3u8", ".mov", "stream.mux.com"];
67
83
  return videoPatterns.some((p) => url.includes(p)) ? "video" : "image";
68
84
  }
85
+ function createFolder(id, title, path, image, isLeaf) {
86
+ return { type: "folder", id, title, path, image, isLeaf };
87
+ }
88
+ function createFile(id, title, url, options) {
89
+ return { type: "file", id, title, url, mediaType: options?.mediaType ?? detectMediaType(url), image: options?.image, muxPlaybackId: options?.muxPlaybackId, seconds: options?.seconds, loop: options?.loop, loopVideo: options?.loopVideo, streamUrl: options?.streamUrl };
90
+ }
91
+
92
+ // src/pathUtils.ts
93
+ function parsePath(path) {
94
+ if (!path || path === "/" || path === "") {
95
+ return { segments: [], depth: 0 };
96
+ }
97
+ const segments = path.replace(/^\//, "").split("/").filter(Boolean);
98
+ return { segments, depth: segments.length };
99
+ }
100
+ function getSegment(path, index) {
101
+ const { segments } = parsePath(path);
102
+ return segments[index] ?? null;
103
+ }
104
+ function buildPath(segments) {
105
+ if (segments.length === 0) return "";
106
+ return "/" + segments.join("/");
107
+ }
108
+ function appendToPath(basePath, segment) {
109
+ if (!basePath || basePath === "/" || basePath === "") {
110
+ return "/" + segment;
111
+ }
112
+ const cleanBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
113
+ return cleanBase + "/" + segment;
114
+ }
115
+
116
+ // src/instructionPathUtils.ts
117
+ function navigateToPath(instructions, path) {
118
+ if (!path || !instructions?.items) return null;
119
+ const indices = path.split(".").map(Number);
120
+ if (indices.some(isNaN)) return null;
121
+ let current = instructions.items[indices[0]] || null;
122
+ for (let i = 1; i < indices.length && current; i++) {
123
+ current = current.children?.[indices[i]] || null;
124
+ }
125
+ return current;
126
+ }
127
+ function generatePath(indices) {
128
+ return indices.join(".");
129
+ }
130
+
131
+ // src/durationUtils.ts
132
+ var DEFAULT_DURATION_CONFIG = {
133
+ secondsPerImage: 15,
134
+ wordsPerMinute: 150
135
+ };
136
+ function countWords(text) {
137
+ if (!text || !text.trim()) return 0;
138
+ return text.trim().split(/\s+/).length;
139
+ }
140
+ function estimateImageDuration(config = {}) {
141
+ return config.secondsPerImage ?? DEFAULT_DURATION_CONFIG.secondsPerImage;
142
+ }
143
+ function estimateTextDuration(text, config = {}) {
144
+ const words = countWords(text);
145
+ const wpm = config.wordsPerMinute ?? DEFAULT_DURATION_CONFIG.wordsPerMinute;
146
+ return Math.ceil(words / wpm * 60);
147
+ }
148
+ function estimateDuration(mediaType, options) {
149
+ const config = options?.config ?? {};
150
+ switch (mediaType) {
151
+ case "image":
152
+ return estimateImageDuration(config);
153
+ case "text":
154
+ if (options?.wordCount) {
155
+ const wpm = config.wordsPerMinute ?? DEFAULT_DURATION_CONFIG.wordsPerMinute;
156
+ return Math.ceil(options.wordCount / wpm * 60);
157
+ }
158
+ if (options?.text) {
159
+ return estimateTextDuration(options.text, config);
160
+ }
161
+ return 0;
162
+ case "video":
163
+ default:
164
+ return 0;
165
+ }
166
+ }
69
167
 
70
168
  // src/FormatConverters.ts
71
169
  var FormatConverters_exports = {};
72
170
  __export(FormatConverters_exports, {
73
- collapseInstructions: () => collapseInstructions,
74
171
  expandedInstructionsToPlaylist: () => expandedInstructionsToPlaylist,
75
172
  expandedInstructionsToPresentations: () => expandedInstructionsToPresentations,
76
173
  instructionsToPlaylist: () => instructionsToPlaylist,
@@ -79,7 +176,6 @@ __export(FormatConverters_exports, {
79
176
  playlistToInstructions: () => playlistToInstructions,
80
177
  playlistToPresentations: () => playlistToPresentations,
81
178
  presentationsToExpandedInstructions: () => presentationsToExpandedInstructions,
82
- presentationsToInstructions: () => presentationsToInstructions,
83
179
  presentationsToPlaylist: () => presentationsToPlaylist
84
180
  });
85
181
  function generateId() {
@@ -123,70 +219,15 @@ function presentationsToPlaylist(plan) {
123
219
  }
124
220
  return files;
125
221
  }
126
- function presentationsToInstructions(plan) {
127
- return {
128
- venueName: plan.name,
129
- items: plan.sections.map((section) => ({
130
- id: section.id,
131
- itemType: "section",
132
- label: section.name,
133
- children: section.presentations.map((pres) => {
134
- const totalSeconds = pres.files.reduce(
135
- (sum, f) => sum + (f.providerData?.seconds || 0),
136
- 0
137
- );
138
- return {
139
- id: pres.id,
140
- itemType: mapActionTypeToItemType(pres.actionType),
141
- label: pres.name,
142
- seconds: totalSeconds || void 0,
143
- embedUrl: pres.files[0]?.embedUrl || pres.files[0]?.url
144
- };
145
- })
146
- }))
147
- };
148
- }
149
222
  function presentationsToExpandedInstructions(plan) {
150
- return {
151
- venueName: plan.name,
152
- items: plan.sections.map((section) => ({
153
- id: section.id,
154
- itemType: "section",
155
- label: section.name,
156
- children: section.presentations.map((pres) => ({
157
- id: pres.id,
158
- itemType: mapActionTypeToItemType(pres.actionType),
159
- label: pres.name,
160
- description: pres.actionType !== "other" ? pres.actionType : void 0,
161
- seconds: pres.files.reduce(
162
- (sum, f) => sum + (f.providerData?.seconds || 0),
163
- 0
164
- ) || void 0,
165
- children: pres.files.map((f) => ({
166
- id: f.id,
167
- itemType: "file",
168
- label: f.title,
169
- seconds: f.providerData?.seconds || void 0,
170
- embedUrl: f.embedUrl || f.url
171
- }))
172
- }))
173
- }))
174
- };
223
+ return { venueName: plan.name, items: plan.sections.map((section) => ({ id: section.id, itemType: "section", label: section.name, children: section.presentations.map((pres) => ({ id: pres.id, itemType: mapActionTypeToItemType(pres.actionType), label: pres.name, description: pres.actionType !== "other" ? pres.actionType : void 0, seconds: pres.files.reduce((sum, f) => sum + (f.seconds || 0), 0) || void 0, children: pres.files.map((f) => ({ id: f.id, itemType: "file", label: f.title, seconds: f.seconds, embedUrl: f.embedUrl || f.url })) })) })) };
175
224
  }
176
225
  function instructionsToPlaylist(instructions) {
177
226
  const files = [];
178
227
  function extractFiles(items) {
179
228
  for (const item of items) {
180
229
  if (item.embedUrl && (item.itemType === "file" || !item.children?.length)) {
181
- files.push({
182
- type: "file",
183
- id: item.id || item.relatedId || generateId(),
184
- title: item.label || "Untitled",
185
- mediaType: detectMediaType(item.embedUrl),
186
- url: item.embedUrl,
187
- embedUrl: item.embedUrl,
188
- providerData: item.seconds ? { seconds: item.seconds } : void 0
189
- });
230
+ files.push({ type: "file", id: item.id || item.relatedId || generateId(), title: item.label || "Untitled", mediaType: detectMediaType(item.embedUrl), url: item.embedUrl, embedUrl: item.embedUrl, seconds: item.seconds });
190
231
  }
191
232
  if (item.children) {
192
233
  extractFiles(item.children);
@@ -205,116 +246,30 @@ function instructionsToPresentations(instructions, planId) {
205
246
  if (presItem.children && presItem.children.length > 0) {
206
247
  for (const child of presItem.children) {
207
248
  if (child.embedUrl) {
208
- const file = {
209
- type: "file",
210
- id: child.id || child.relatedId || generateId(),
211
- title: child.label || "Untitled",
212
- mediaType: detectMediaType(child.embedUrl),
213
- url: child.embedUrl,
214
- embedUrl: child.embedUrl,
215
- providerData: child.seconds ? { seconds: child.seconds } : void 0
216
- };
249
+ const file = { type: "file", id: child.id || child.relatedId || generateId(), title: child.label || "Untitled", mediaType: detectMediaType(child.embedUrl), url: child.embedUrl, embedUrl: child.embedUrl, seconds: child.seconds };
217
250
  allFiles.push(file);
218
251
  files.push(file);
219
252
  }
220
253
  }
221
254
  }
222
255
  if (files.length === 0 && presItem.embedUrl) {
223
- const file = {
224
- type: "file",
225
- id: presItem.id || presItem.relatedId || generateId(),
226
- title: presItem.label || "Untitled",
227
- mediaType: detectMediaType(presItem.embedUrl),
228
- url: presItem.embedUrl,
229
- embedUrl: presItem.embedUrl,
230
- providerData: presItem.seconds ? { seconds: presItem.seconds } : void 0
231
- };
256
+ const file = { type: "file", id: presItem.id || presItem.relatedId || generateId(), title: presItem.label || "Untitled", mediaType: detectMediaType(presItem.embedUrl), url: presItem.embedUrl, embedUrl: presItem.embedUrl, seconds: presItem.seconds };
232
257
  allFiles.push(file);
233
258
  files.push(file);
234
259
  }
235
- return {
236
- id: presItem.id || presItem.relatedId || generateId(),
237
- name: presItem.label || "Presentation",
238
- actionType: mapItemTypeToActionType(presItem.itemType),
239
- files
240
- };
260
+ return { id: presItem.id || presItem.relatedId || generateId(), name: presItem.label || "Presentation", actionType: mapItemTypeToActionType(presItem.itemType), files };
241
261
  });
242
- return {
243
- id: sectionItem.id || sectionItem.relatedId || generateId(),
244
- name: sectionItem.label || "Section",
245
- presentations
246
- };
262
+ return { id: sectionItem.id || sectionItem.relatedId || generateId(), name: sectionItem.label || "Section", presentations };
247
263
  });
248
- return {
249
- id: planId || generateId(),
250
- name: instructions.venueName || "Plan",
251
- sections,
252
- allFiles
253
- };
264
+ return { id: planId || generateId(), name: instructions.venueName || "Plan", sections, allFiles };
254
265
  }
255
266
  var expandedInstructionsToPresentations = instructionsToPresentations;
256
- function collapseInstructions(instructions, maxDepth = 2) {
257
- function collapseItem(item, currentDepth) {
258
- if (currentDepth >= maxDepth || !item.children || item.children.length === 0) {
259
- const { children, ...rest } = item;
260
- if (children && children.length > 0) {
261
- const totalSeconds = children.reduce((sum, c) => sum + (c.seconds || 0), 0);
262
- if (totalSeconds > 0) {
263
- rest.seconds = totalSeconds;
264
- }
265
- if (!rest.embedUrl) {
266
- const firstWithUrl = children.find((c) => c.embedUrl);
267
- if (firstWithUrl) {
268
- rest.embedUrl = firstWithUrl.embedUrl;
269
- }
270
- }
271
- }
272
- return rest;
273
- }
274
- return {
275
- ...item,
276
- children: item.children.map((child) => collapseItem(child, currentDepth + 1))
277
- };
278
- }
279
- return {
280
- venueName: instructions.venueName,
281
- items: instructions.items.map((item) => collapseItem(item, 0))
282
- };
283
- }
284
267
  function playlistToPresentations(files, planName = "Playlist", sectionName = "Content") {
285
- const presentations = files.map((file, index) => ({
286
- id: `pres-${index}-${file.id}`,
287
- name: file.title,
288
- actionType: "play",
289
- files: [file]
290
- }));
291
- return {
292
- id: "playlist-plan-" + generateId(),
293
- name: planName,
294
- sections: [{
295
- id: "main-section",
296
- name: sectionName,
297
- presentations
298
- }],
299
- allFiles: [...files]
300
- };
268
+ const presentations = files.map((file, index) => ({ id: `pres-${index}-${file.id}`, name: file.title, actionType: "play", files: [file] }));
269
+ return { id: "playlist-plan-" + generateId(), name: planName, sections: [{ id: "main-section", name: sectionName, presentations }], allFiles: [...files] };
301
270
  }
302
271
  function playlistToInstructions(files, venueName = "Playlist") {
303
- return {
304
- venueName,
305
- items: [{
306
- id: "main-section",
307
- itemType: "section",
308
- label: "Content",
309
- children: files.map((file, index) => ({
310
- id: file.id || `item-${index}`,
311
- itemType: "file",
312
- label: file.title,
313
- seconds: file.providerData?.seconds || void 0,
314
- embedUrl: file.embedUrl || file.url
315
- }))
316
- }]
317
- };
272
+ return { venueName, items: [{ id: "main-section", itemType: "section", label: "Content", children: files.map((file, index) => ({ id: file.id || `item-${index}`, itemType: "file", label: file.title, seconds: file.seconds, embedUrl: file.embedUrl || file.url })) }] };
318
273
  }
319
274
  var playlistToExpandedInstructions = playlistToInstructions;
320
275
 
@@ -322,310 +277,130 @@ var playlistToExpandedInstructions = playlistToInstructions;
322
277
  var FormatResolver = class {
323
278
  constructor(provider, options = {}) {
324
279
  this.provider = provider;
325
- this.options = {
326
- allowLossy: options.allowLossy ?? true
327
- };
280
+ this.options = { allowLossy: options.allowLossy ?? true };
328
281
  }
329
282
  getProvider() {
330
283
  return this.provider;
331
284
  }
332
- async getPlaylist(folder, auth) {
333
- const caps = this.provider.getCapabilities();
334
- if (caps.playlist) {
335
- const result = await this.provider.getPlaylist(folder, auth);
285
+ /** Extract the last segment from a path to use as fallback ID/title */
286
+ getIdFromPath(path) {
287
+ const { segments } = parsePath(path);
288
+ return segments[segments.length - 1] || "content";
289
+ }
290
+ async getPlaylist(path, auth) {
291
+ const caps = this.provider.capabilities;
292
+ if (caps.playlist && this.provider.getPlaylist) {
293
+ const result = await this.provider.getPlaylist(path, auth);
336
294
  if (result && result.length > 0) return result;
337
295
  }
338
296
  if (caps.presentations) {
339
- const plan = await this.provider.getPresentations(folder, auth);
297
+ const plan = await this.provider.getPresentations(path, auth);
340
298
  if (plan) return presentationsToPlaylist(plan);
341
299
  }
342
- if (caps.expandedInstructions) {
343
- const expanded = await this.provider.getExpandedInstructions(folder, auth);
300
+ if (caps.instructions && this.provider.getInstructions) {
301
+ const expanded = await this.provider.getInstructions(path, auth);
344
302
  if (expanded) return instructionsToPlaylist(expanded);
345
303
  }
346
- if (this.options.allowLossy && caps.instructions) {
347
- const instructions = await this.provider.getInstructions(folder, auth);
348
- if (instructions) return instructionsToPlaylist(instructions);
349
- }
350
304
  return null;
351
305
  }
352
- async getPlaylistWithMeta(folder, auth) {
353
- const caps = this.provider.getCapabilities();
354
- if (caps.playlist) {
355
- const result = await this.provider.getPlaylist(folder, auth);
306
+ async getPlaylistWithMeta(path, auth) {
307
+ const caps = this.provider.capabilities;
308
+ if (caps.playlist && this.provider.getPlaylist) {
309
+ const result = await this.provider.getPlaylist(path, auth);
356
310
  if (result && result.length > 0) {
357
311
  return { data: result, meta: { isNative: true, isLossy: false } };
358
312
  }
359
313
  }
360
314
  if (caps.presentations) {
361
- const plan = await this.provider.getPresentations(folder, auth);
362
- if (plan) {
363
- return {
364
- data: presentationsToPlaylist(plan),
365
- meta: { isNative: false, sourceFormat: "presentations", isLossy: false }
366
- };
367
- }
368
- }
369
- if (caps.expandedInstructions) {
370
- const expanded = await this.provider.getExpandedInstructions(folder, auth);
371
- if (expanded) {
372
- return {
373
- data: instructionsToPlaylist(expanded),
374
- meta: { isNative: false, sourceFormat: "expandedInstructions", isLossy: false }
375
- };
376
- }
315
+ const plan = await this.provider.getPresentations(path, auth);
316
+ if (plan) return { data: presentationsToPlaylist(plan), meta: { isNative: false, sourceFormat: "presentations", isLossy: false } };
377
317
  }
378
- if (this.options.allowLossy && caps.instructions) {
379
- const instructions = await this.provider.getInstructions(folder, auth);
380
- if (instructions) {
381
- return {
382
- data: instructionsToPlaylist(instructions),
383
- meta: { isNative: false, sourceFormat: "instructions", isLossy: true }
384
- };
385
- }
318
+ if (caps.instructions && this.provider.getInstructions) {
319
+ const expanded = await this.provider.getInstructions(path, auth);
320
+ if (expanded) return { data: instructionsToPlaylist(expanded), meta: { isNative: false, sourceFormat: "instructions", isLossy: false } };
386
321
  }
387
322
  return { data: null, meta: { isNative: false, isLossy: false } };
388
323
  }
389
- async getPresentations(folder, auth) {
390
- const caps = this.provider.getCapabilities();
324
+ async getPresentations(path, auth) {
325
+ const caps = this.provider.capabilities;
326
+ const fallbackId = this.getIdFromPath(path);
391
327
  if (caps.presentations) {
392
- const result = await this.provider.getPresentations(folder, auth);
328
+ const result = await this.provider.getPresentations(path, auth);
393
329
  if (result) return result;
394
330
  }
395
- if (caps.expandedInstructions) {
396
- const expanded = await this.provider.getExpandedInstructions(folder, auth);
397
- if (expanded) return instructionsToPresentations(expanded, folder.id);
398
- }
399
- if (caps.instructions) {
400
- const instructions = await this.provider.getInstructions(folder, auth);
401
- if (instructions) return instructionsToPresentations(instructions, folder.id);
331
+ if (caps.instructions && this.provider.getInstructions) {
332
+ const expanded = await this.provider.getInstructions(path, auth);
333
+ if (expanded) return instructionsToPresentations(expanded, fallbackId);
402
334
  }
403
- if (this.options.allowLossy && caps.playlist) {
404
- const playlist = await this.provider.getPlaylist(folder, auth);
335
+ if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
336
+ const playlist = await this.provider.getPlaylist(path, auth);
405
337
  if (playlist && playlist.length > 0) {
406
- return playlistToPresentations(playlist, folder.title);
338
+ return playlistToPresentations(playlist, fallbackId);
407
339
  }
408
340
  }
409
341
  return null;
410
342
  }
411
- async getPresentationsWithMeta(folder, auth) {
412
- const caps = this.provider.getCapabilities();
413
- if (caps.presentations) {
414
- const result = await this.provider.getPresentations(folder, auth);
415
- if (result) {
416
- return { data: result, meta: { isNative: true, isLossy: false } };
417
- }
418
- }
419
- if (caps.expandedInstructions) {
420
- const expanded = await this.provider.getExpandedInstructions(folder, auth);
421
- if (expanded) {
422
- return {
423
- data: instructionsToPresentations(expanded, folder.id),
424
- meta: { isNative: false, sourceFormat: "expandedInstructions", isLossy: false }
425
- };
426
- }
427
- }
428
- if (caps.instructions) {
429
- const instructions = await this.provider.getInstructions(folder, auth);
430
- if (instructions) {
431
- return {
432
- data: instructionsToPresentations(instructions, folder.id),
433
- meta: { isNative: false, sourceFormat: "instructions", isLossy: true }
434
- };
435
- }
436
- }
437
- if (this.options.allowLossy && caps.playlist) {
438
- const playlist = await this.provider.getPlaylist(folder, auth);
439
- if (playlist && playlist.length > 0) {
440
- return {
441
- data: playlistToPresentations(playlist, folder.title),
442
- meta: { isNative: false, sourceFormat: "playlist", isLossy: true }
443
- };
444
- }
445
- }
446
- return { data: null, meta: { isNative: false, isLossy: false } };
447
- }
448
- async getInstructions(folder, auth) {
449
- const caps = this.provider.getCapabilities();
450
- if (caps.instructions) {
451
- const result = await this.provider.getInstructions(folder, auth);
452
- if (result) return result;
453
- }
454
- if (caps.expandedInstructions) {
455
- const expanded = await this.provider.getExpandedInstructions(folder, auth);
456
- if (expanded) return collapseInstructions(expanded);
457
- }
343
+ async getPresentationsWithMeta(path, auth) {
344
+ const caps = this.provider.capabilities;
345
+ const fallbackId = this.getIdFromPath(path);
458
346
  if (caps.presentations) {
459
- const plan = await this.provider.getPresentations(folder, auth);
460
- if (plan) return presentationsToInstructions(plan);
461
- }
462
- if (this.options.allowLossy && caps.playlist) {
463
- const playlist = await this.provider.getPlaylist(folder, auth);
464
- if (playlist && playlist.length > 0) {
465
- return playlistToInstructions(playlist, folder.title);
466
- }
467
- }
468
- return null;
469
- }
470
- async getInstructionsWithMeta(folder, auth) {
471
- const caps = this.provider.getCapabilities();
472
- if (caps.instructions) {
473
- const result = await this.provider.getInstructions(folder, auth);
347
+ const result = await this.provider.getPresentations(path, auth);
474
348
  if (result) {
475
349
  return { data: result, meta: { isNative: true, isLossy: false } };
476
350
  }
477
351
  }
478
- if (caps.expandedInstructions) {
479
- const expanded = await this.provider.getExpandedInstructions(folder, auth);
480
- if (expanded) {
481
- return {
482
- data: collapseInstructions(expanded),
483
- meta: { isNative: false, sourceFormat: "expandedInstructions", isLossy: true }
484
- };
485
- }
352
+ if (caps.instructions && this.provider.getInstructions) {
353
+ const expanded = await this.provider.getInstructions(path, auth);
354
+ if (expanded) return { data: instructionsToPresentations(expanded, fallbackId), meta: { isNative: false, sourceFormat: "instructions", isLossy: false } };
486
355
  }
487
- if (caps.presentations) {
488
- const plan = await this.provider.getPresentations(folder, auth);
489
- if (plan) {
490
- return {
491
- data: presentationsToInstructions(plan),
492
- meta: { isNative: false, sourceFormat: "presentations", isLossy: false }
493
- };
494
- }
495
- }
496
- if (this.options.allowLossy && caps.playlist) {
497
- const playlist = await this.provider.getPlaylist(folder, auth);
498
- if (playlist && playlist.length > 0) {
499
- return {
500
- data: playlistToInstructions(playlist, folder.title),
501
- meta: { isNative: false, sourceFormat: "playlist", isLossy: true }
502
- };
503
- }
356
+ if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
357
+ const playlist = await this.provider.getPlaylist(path, auth);
358
+ if (playlist && playlist.length > 0) return { data: playlistToPresentations(playlist, fallbackId), meta: { isNative: false, sourceFormat: "playlist", isLossy: true } };
504
359
  }
505
360
  return { data: null, meta: { isNative: false, isLossy: false } };
506
361
  }
507
- async getExpandedInstructions(folder, auth) {
508
- const caps = this.provider.getCapabilities();
509
- if (caps.expandedInstructions) {
510
- const result = await this.provider.getExpandedInstructions(folder, auth);
362
+ async getInstructions(path, auth) {
363
+ const caps = this.provider.capabilities;
364
+ const fallbackTitle = this.getIdFromPath(path);
365
+ if (caps.instructions && this.provider.getInstructions) {
366
+ const result = await this.provider.getInstructions(path, auth);
511
367
  if (result) return result;
512
368
  }
513
369
  if (caps.presentations) {
514
- const plan = await this.provider.getPresentations(folder, auth);
370
+ const plan = await this.provider.getPresentations(path, auth);
515
371
  if (plan) return presentationsToExpandedInstructions(plan);
516
372
  }
517
- if (caps.instructions) {
518
- const instructions = await this.provider.getInstructions(folder, auth);
519
- if (instructions) return instructions;
520
- }
521
- if (this.options.allowLossy && caps.playlist) {
522
- const playlist = await this.provider.getPlaylist(folder, auth);
373
+ if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
374
+ const playlist = await this.provider.getPlaylist(path, auth);
523
375
  if (playlist && playlist.length > 0) {
524
- return playlistToInstructions(playlist, folder.title);
376
+ return playlistToInstructions(playlist, fallbackTitle);
525
377
  }
526
378
  }
527
379
  return null;
528
380
  }
529
- async getExpandedInstructionsWithMeta(folder, auth) {
530
- const caps = this.provider.getCapabilities();
531
- if (caps.expandedInstructions) {
532
- const result = await this.provider.getExpandedInstructions(folder, auth);
381
+ async getInstructionsWithMeta(path, auth) {
382
+ const caps = this.provider.capabilities;
383
+ const fallbackTitle = this.getIdFromPath(path);
384
+ if (caps.instructions && this.provider.getInstructions) {
385
+ const result = await this.provider.getInstructions(path, auth);
533
386
  if (result) {
534
387
  return { data: result, meta: { isNative: true, isLossy: false } };
535
388
  }
536
389
  }
537
390
  if (caps.presentations) {
538
- const plan = await this.provider.getPresentations(folder, auth);
539
- if (plan) {
540
- return {
541
- data: presentationsToExpandedInstructions(plan),
542
- meta: { isNative: false, sourceFormat: "presentations", isLossy: false }
543
- };
544
- }
391
+ const plan = await this.provider.getPresentations(path, auth);
392
+ if (plan) return { data: presentationsToExpandedInstructions(plan), meta: { isNative: false, sourceFormat: "presentations", isLossy: false } };
545
393
  }
546
- if (caps.instructions) {
547
- const instructions = await this.provider.getInstructions(folder, auth);
548
- if (instructions) {
549
- return {
550
- data: instructions,
551
- meta: { isNative: false, sourceFormat: "instructions", isLossy: true }
552
- };
553
- }
554
- }
555
- if (this.options.allowLossy && caps.playlist) {
556
- const playlist = await this.provider.getPlaylist(folder, auth);
557
- if (playlist && playlist.length > 0) {
558
- return {
559
- data: playlistToInstructions(playlist, folder.title),
560
- meta: { isNative: false, sourceFormat: "playlist", isLossy: true }
561
- };
562
- }
394
+ if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
395
+ const playlist = await this.provider.getPlaylist(path, auth);
396
+ if (playlist && playlist.length > 0) return { data: playlistToInstructions(playlist, fallbackTitle), meta: { isNative: false, sourceFormat: "playlist", isLossy: true } };
563
397
  }
564
398
  return { data: null, meta: { isNative: false, isLossy: false } };
565
399
  }
566
400
  };
567
401
 
568
- // src/ContentProvider.ts
569
- var ContentProvider = class {
570
- async getPlaylist(folder, auth, _resolution) {
571
- const caps = this.getCapabilities();
572
- if (caps.presentations) {
573
- const plan = await this.getPresentations(folder, auth);
574
- if (plan) return presentationsToPlaylist(plan);
575
- }
576
- return null;
577
- }
578
- async getInstructions(folder, auth) {
579
- const caps = this.getCapabilities();
580
- if (caps.expandedInstructions) {
581
- const expanded = await this.getExpandedInstructions(folder, auth);
582
- if (expanded) return collapseInstructions(expanded);
583
- }
584
- if (caps.presentations) {
585
- const plan = await this.getPresentations(folder, auth);
586
- if (plan) return presentationsToInstructions(plan);
587
- }
588
- return null;
589
- }
590
- async getExpandedInstructions(folder, auth) {
591
- const caps = this.getCapabilities();
592
- if (caps.presentations) {
593
- const plan = await this.getPresentations(folder, auth);
594
- if (plan) return presentationsToExpandedInstructions(plan);
595
- }
596
- return null;
597
- }
598
- requiresAuth() {
599
- return !!this.config.clientId;
600
- }
601
- getCapabilities() {
602
- return {
603
- browse: true,
604
- presentations: false,
605
- playlist: false,
606
- instructions: false,
607
- expandedInstructions: false,
608
- mediaLicensing: false
609
- };
610
- }
611
- checkMediaLicense(_mediaId, _auth) {
612
- return Promise.resolve(null);
613
- }
614
- getAuthTypes() {
615
- if (!this.requiresAuth()) return ["none"];
616
- const types = ["oauth_pkce"];
617
- if (this.supportsDeviceFlow()) types.push("device_flow");
618
- return types;
619
- }
620
- isAuthValid(auth) {
621
- if (!auth) return false;
622
- return !this.isTokenExpired(auth);
623
- }
624
- isTokenExpired(auth) {
625
- if (!auth.created_at || !auth.expires_in) return true;
626
- const expiresAt = (auth.created_at + auth.expires_in) * 1e3;
627
- return Date.now() > expiresAt - 5 * 60 * 1e3;
628
- }
402
+ // src/helpers/OAuthHelper.ts
403
+ var OAuthHelper = class {
629
404
  generateCodeVerifier() {
630
405
  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
631
406
  const length = 64;
@@ -649,110 +424,91 @@ var ContentProvider = class {
649
424
  const base64 = btoa(binary);
650
425
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
651
426
  }
652
- async buildAuthUrl(codeVerifier, redirectUri, state) {
427
+ async buildAuthUrl(config, codeVerifier, redirectUri, state) {
653
428
  const codeChallenge = await this.generateCodeChallenge(codeVerifier);
654
429
  const params = new URLSearchParams({
655
430
  response_type: "code",
656
- client_id: this.config.clientId,
431
+ client_id: config.clientId,
657
432
  redirect_uri: redirectUri,
658
- scope: this.config.scopes.join(" "),
433
+ scope: config.scopes.join(" "),
659
434
  code_challenge: codeChallenge,
660
435
  code_challenge_method: "S256",
661
- state: state || this.id
436
+ state: state || ""
662
437
  });
663
- return { url: `${this.config.oauthBase}/authorize?${params.toString()}`, challengeMethod: "S256" };
438
+ return { url: `${config.oauthBase}/authorize?${params.toString()}`, challengeMethod: "S256" };
664
439
  }
665
- async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
440
+ async exchangeCodeForTokens(config, providerId, code, codeVerifier, redirectUri) {
666
441
  try {
667
442
  const params = new URLSearchParams({
668
443
  grant_type: "authorization_code",
669
444
  code,
670
445
  redirect_uri: redirectUri,
671
- client_id: this.config.clientId,
446
+ client_id: config.clientId,
672
447
  code_verifier: codeVerifier
673
448
  });
674
- const tokenUrl = `${this.config.oauthBase}/token`;
675
- console.log(`${this.id} token exchange request to: ${tokenUrl}`);
676
- console.log(` - client_id: ${this.config.clientId}`);
677
- console.log(` - redirect_uri: ${redirectUri}`);
678
- console.log(` - code: ${code.substring(0, 10)}...`);
449
+ const tokenUrl = `${config.oauthBase}/token`;
679
450
  const response = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
680
- console.log(`${this.id} token response status: ${response.status}`);
681
451
  if (!response.ok) {
682
- const errorText = await response.text();
683
- console.error(`${this.id} token exchange failed: ${response.status} - ${errorText}`);
684
452
  return null;
685
453
  }
686
454
  const data = await response.json();
687
- console.log(`${this.id} token exchange successful, got access_token: ${!!data.access_token}`);
688
- return {
689
- access_token: data.access_token,
690
- refresh_token: data.refresh_token,
691
- token_type: data.token_type || "Bearer",
692
- created_at: Math.floor(Date.now() / 1e3),
693
- expires_in: data.expires_in,
694
- scope: data.scope || this.config.scopes.join(" ")
695
- };
696
- } catch (error) {
697
- console.error(`${this.id} token exchange error:`, error);
455
+ return { access_token: data.access_token, refresh_token: data.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || config.scopes.join(" ") };
456
+ } catch {
698
457
  return null;
699
458
  }
700
459
  }
701
- async refreshToken(auth) {
460
+ };
461
+
462
+ // src/helpers/TokenHelper.ts
463
+ var TokenHelper = class {
464
+ isAuthValid(auth) {
465
+ if (!auth) return false;
466
+ return !this.isTokenExpired(auth);
467
+ }
468
+ isTokenExpired(auth) {
469
+ if (!auth.created_at || !auth.expires_in) return true;
470
+ const expiresAt = (auth.created_at + auth.expires_in) * 1e3;
471
+ return Date.now() > expiresAt - 5 * 60 * 1e3;
472
+ }
473
+ async refreshToken(config, auth) {
702
474
  if (!auth.refresh_token) return null;
703
475
  try {
704
476
  const params = new URLSearchParams({
705
477
  grant_type: "refresh_token",
706
478
  refresh_token: auth.refresh_token,
707
- client_id: this.config.clientId
479
+ client_id: config.clientId
708
480
  });
709
- const response = await fetch(`${this.config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
481
+ const response = await fetch(`${config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
710
482
  if (!response.ok) return null;
711
483
  const data = await response.json();
712
- return {
713
- access_token: data.access_token,
714
- refresh_token: data.refresh_token || auth.refresh_token,
715
- token_type: data.token_type || "Bearer",
716
- created_at: Math.floor(Date.now() / 1e3),
717
- expires_in: data.expires_in,
718
- scope: data.scope || auth.scope
719
- };
484
+ return { access_token: data.access_token, refresh_token: data.refresh_token || auth.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || auth.scope };
720
485
  } catch {
721
486
  return null;
722
487
  }
723
488
  }
724
- supportsDeviceFlow() {
725
- return !!this.config.supportsDeviceFlow && !!this.config.deviceAuthEndpoint;
489
+ };
490
+
491
+ // src/helpers/DeviceFlowHelper.ts
492
+ var DeviceFlowHelper = class {
493
+ supportsDeviceFlow(config) {
494
+ return !!config.supportsDeviceFlow && !!config.deviceAuthEndpoint;
726
495
  }
727
- async initiateDeviceFlow() {
728
- if (!this.supportsDeviceFlow()) return null;
496
+ async initiateDeviceFlow(config) {
497
+ if (!this.supportsDeviceFlow(config)) return null;
729
498
  try {
730
- const params = new URLSearchParams({ client_id: this.config.clientId, scope: this.config.scopes.join(" ") });
731
- const response = await fetch(`${this.config.oauthBase}${this.config.deviceAuthEndpoint}`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
499
+ const response = await fetch(`${config.oauthBase}${config.deviceAuthEndpoint}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: config.clientId, scope: config.scopes.join(" ") }) });
732
500
  if (!response.ok) return null;
733
501
  return await response.json();
734
502
  } catch {
735
503
  return null;
736
504
  }
737
505
  }
738
- async pollDeviceFlowToken(deviceCode) {
506
+ async pollDeviceFlowToken(config, deviceCode) {
739
507
  try {
740
- const params = new URLSearchParams({
741
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
742
- device_code: deviceCode,
743
- client_id: this.config.clientId
744
- });
745
- const response = await fetch(`${this.config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
508
+ const response = await fetch(`${config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: deviceCode, client_id: config.clientId }) });
746
509
  if (response.ok) {
747
510
  const data = await response.json();
748
- return {
749
- access_token: data.access_token,
750
- refresh_token: data.refresh_token,
751
- token_type: data.token_type || "Bearer",
752
- created_at: Math.floor(Date.now() / 1e3),
753
- expires_in: data.expires_in,
754
- scope: data.scope || this.config.scopes.join(" ")
755
- };
511
+ return { access_token: data.access_token, refresh_token: data.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || config.scopes.join(" ") };
756
512
  }
757
513
  const errorData = await response.json();
758
514
  switch (errorData.error) {
@@ -774,183 +530,236 @@ var ContentProvider = class {
774
530
  calculatePollDelay(baseInterval = 5, slowDownCount = 0) {
775
531
  return (baseInterval + slowDownCount * 5) * 1e3;
776
532
  }
533
+ };
534
+
535
+ // src/helpers/ApiHelper.ts
536
+ var ApiHelper = class {
777
537
  createAuthHeaders(auth) {
778
538
  if (!auth) return null;
779
539
  return { Authorization: `Bearer ${auth.access_token}`, Accept: "application/json" };
780
540
  }
781
- async apiRequest(path, auth, method = "GET", body) {
541
+ async apiRequest(config, _providerId, path, auth, method = "GET", body) {
782
542
  try {
783
- const url = `${this.config.apiBase}${path}`;
543
+ const url = `${config.apiBase}${path}`;
784
544
  const headers = { Accept: "application/json" };
785
545
  if (auth) headers["Authorization"] = `Bearer ${auth.access_token}`;
786
546
  if (body) headers["Content-Type"] = "application/json";
787
- console.log(`${this.id} API request: ${method} ${url}`);
788
- console.log(`${this.id} API auth present: ${!!auth}`);
789
547
  const options = { method, headers, ...body ? { body: JSON.stringify(body) } : {} };
790
548
  const response = await fetch(url, options);
791
- console.log(`${this.id} API response status: ${response.status}`);
792
549
  if (!response.ok) {
793
- const errorText = await response.text();
794
- console.error(`${this.id} API request failed: ${response.status} - ${errorText}`);
795
550
  return null;
796
551
  }
797
552
  return await response.json();
798
- } catch (error) {
799
- console.error(`${this.id} API request error:`, error);
553
+ } catch {
800
554
  return null;
801
555
  }
802
556
  }
803
- createFolder(id, title, image, providerData) {
804
- return { type: "folder", id, title, image, providerData };
557
+ };
558
+
559
+ // src/ContentProvider.ts
560
+ var ContentProvider = class {
561
+ constructor() {
562
+ this.oauthHelper = new OAuthHelper();
563
+ this.tokenHelper = new TokenHelper();
564
+ this.deviceFlowHelper = new DeviceFlowHelper();
565
+ this.apiHelper = new ApiHelper();
566
+ }
567
+ async getPlaylist(path, auth, _resolution) {
568
+ const caps = this.getCapabilities();
569
+ if (caps.presentations) {
570
+ const plan = await this.getPresentations(path, auth);
571
+ if (plan) return presentationsToPlaylist(plan);
572
+ }
573
+ return null;
574
+ }
575
+ async getInstructions(path, auth) {
576
+ const caps = this.getCapabilities();
577
+ if (caps.presentations) {
578
+ const plan = await this.getPresentations(path, auth);
579
+ if (plan) return presentationsToExpandedInstructions(plan);
580
+ }
581
+ return null;
582
+ }
583
+ requiresAuth() {
584
+ return !!this.config.clientId;
585
+ }
586
+ getCapabilities() {
587
+ return { browse: true, presentations: false, playlist: false, instructions: false, mediaLicensing: false };
588
+ }
589
+ checkMediaLicense(_mediaId, _auth) {
590
+ return Promise.resolve(null);
591
+ }
592
+ getAuthTypes() {
593
+ if (!this.requiresAuth()) return ["none"];
594
+ const types = ["oauth_pkce"];
595
+ if (this.supportsDeviceFlow()) types.push("device_flow");
596
+ return types;
597
+ }
598
+ // Token management - delegated to TokenHelper
599
+ isAuthValid(auth) {
600
+ return this.tokenHelper.isAuthValid(auth);
601
+ }
602
+ isTokenExpired(auth) {
603
+ return this.tokenHelper.isTokenExpired(auth);
604
+ }
605
+ async refreshToken(auth) {
606
+ return this.tokenHelper.refreshToken(this.config, auth);
607
+ }
608
+ // OAuth PKCE - delegated to OAuthHelper
609
+ generateCodeVerifier() {
610
+ return this.oauthHelper.generateCodeVerifier();
611
+ }
612
+ async generateCodeChallenge(verifier) {
613
+ return this.oauthHelper.generateCodeChallenge(verifier);
614
+ }
615
+ async buildAuthUrl(codeVerifier, redirectUri, state) {
616
+ return this.oauthHelper.buildAuthUrl(this.config, codeVerifier, redirectUri, state || this.id);
617
+ }
618
+ async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
619
+ return this.oauthHelper.exchangeCodeForTokens(this.config, this.id, code, codeVerifier, redirectUri);
620
+ }
621
+ // Device flow - delegated to DeviceFlowHelper
622
+ supportsDeviceFlow() {
623
+ return this.deviceFlowHelper.supportsDeviceFlow(this.config);
624
+ }
625
+ async initiateDeviceFlow() {
626
+ return this.deviceFlowHelper.initiateDeviceFlow(this.config);
627
+ }
628
+ async pollDeviceFlowToken(deviceCode) {
629
+ return this.deviceFlowHelper.pollDeviceFlowToken(this.config, deviceCode);
630
+ }
631
+ calculatePollDelay(baseInterval = 5, slowDownCount = 0) {
632
+ return this.deviceFlowHelper.calculatePollDelay(baseInterval, slowDownCount);
633
+ }
634
+ // API requests - delegated to ApiHelper
635
+ createAuthHeaders(auth) {
636
+ return this.apiHelper.createAuthHeaders(auth);
637
+ }
638
+ async apiRequest(path, auth, method = "GET", body) {
639
+ return this.apiHelper.apiRequest(this.config, this.id, path, auth, method, body);
640
+ }
641
+ // Content factories
642
+ createFolder(id, title, path, image, isLeaf) {
643
+ return { type: "folder", id, title, path, image, isLeaf };
805
644
  }
806
645
  createFile(id, title, url, options) {
807
- return {
808
- type: "file",
809
- id,
810
- title,
811
- url,
812
- mediaType: options?.mediaType ?? detectMediaType(url),
813
- image: options?.image,
814
- muxPlaybackId: options?.muxPlaybackId,
815
- providerData: options?.providerData
816
- };
646
+ return { type: "file", id, title, url, mediaType: options?.mediaType ?? detectMediaType(url), image: options?.image, muxPlaybackId: options?.muxPlaybackId, seconds: options?.seconds, loop: options?.loop, loopVideo: options?.loopVideo, streamUrl: options?.streamUrl };
817
647
  }
818
648
  };
819
649
 
820
- // src/providers/APlayProvider.ts
821
- var APlayProvider = class extends ContentProvider {
650
+ // src/providers/aPlay/APlayProvider.ts
651
+ var APlayProvider = class {
822
652
  constructor() {
823
- super(...arguments);
653
+ this.apiHelper = new ApiHelper();
824
654
  this.id = "aplay";
825
655
  this.name = "APlay";
826
- this.logos = {
827
- light: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg",
828
- dark: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg"
829
- };
830
- this.config = {
831
- id: "aplay",
832
- name: "APlay",
833
- apiBase: "https://api-prod.amazingkids.app",
834
- oauthBase: "https://api.joinamazing.com/prod/aims/oauth",
835
- clientId: "xFJFq7yNYuXXXMx0YBiQ",
836
- scopes: ["openid", "profile", "email"],
837
- endpoints: {
838
- modules: "/prod/curriculum/modules",
839
- productLibraries: (productId) => `/prod/curriculum/modules/products/${productId}/libraries`,
840
- libraryMedia: (libraryId) => `/prod/creators/libraries/${libraryId}/media`
841
- }
842
- };
656
+ this.logos = { light: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg", dark: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg" };
657
+ this.config = { id: "aplay", name: "APlay", apiBase: "https://api-prod.amazingkids.app", oauthBase: "https://api.joinamazing.com/prod/aims/oauth", clientId: "xFJFq7yNYuXXXMx0YBiQ", scopes: ["openid", "profile", "email"], endpoints: { modules: "/prod/curriculum/modules", productLibraries: (productId) => `/prod/curriculum/modules/products/${productId}/libraries`, libraryMedia: (libraryId) => `/prod/creators/libraries/${libraryId}/media` } };
658
+ this.requiresAuth = true;
659
+ this.authTypes = ["oauth_pkce"];
660
+ this.capabilities = { browse: true, presentations: true, playlist: true, instructions: true, mediaLicensing: true };
843
661
  }
844
- getCapabilities() {
845
- return {
846
- browse: true,
847
- presentations: true,
848
- playlist: false,
849
- instructions: false,
850
- expandedInstructions: false,
851
- mediaLicensing: true
852
- };
662
+ async apiRequest(path, auth) {
663
+ return this.apiHelper.apiRequest(this.config, this.id, path, auth);
853
664
  }
854
- async browse(folder, auth) {
855
- console.log(`APlay browse called with folder:`, folder ? { id: folder.id, level: folder.providerData?.level } : "null");
856
- console.log(`APlay browse auth present:`, !!auth);
857
- if (!folder) {
858
- console.log(`APlay fetching modules from: ${this.config.endpoints.modules}`);
859
- const response = await this.apiRequest(this.config.endpoints.modules, auth);
860
- console.log(`APlay modules response:`, response ? "received" : "null");
861
- if (!response) return [];
862
- const modules = response.data || response.modules || response;
863
- console.log(`APlay modules count:`, Array.isArray(modules) ? modules.length : "not an array");
864
- if (!Array.isArray(modules)) return [];
865
- const items = [];
866
- for (const m of modules) {
867
- if (m.isLocked) continue;
868
- const allProducts = m.products || [];
869
- const products = allProducts.filter((p) => !p.isHidden);
870
- if (products.length === 0) {
871
- items.push({
872
- type: "folder",
873
- id: m.id || m.moduleId,
874
- title: m.title || m.name,
875
- image: m.image,
876
- providerData: { level: "libraries", productId: m.id || m.moduleId }
877
- });
878
- } else if (products.length === 1) {
879
- const product = products[0];
880
- items.push({
881
- type: "folder",
882
- id: product.productId || product.id,
883
- title: m.title || m.name,
884
- image: m.image || product.image,
885
- providerData: { level: "libraries", productId: product.productId || product.id }
886
- });
887
- } else {
888
- items.push({
889
- type: "folder",
890
- id: m.id || m.moduleId,
891
- title: m.title || m.name,
892
- image: m.image,
893
- providerData: {
894
- level: "products",
895
- products: products.map((p) => ({ id: p.productId || p.id, title: p.title || p.name, image: p.image }))
896
- }
897
- });
898
- }
899
- }
900
- return items;
665
+ async browse(path, auth) {
666
+ const { segments, depth } = parsePath(path);
667
+ if (depth === 0) {
668
+ return [{
669
+ type: "folder",
670
+ id: "modules-root",
671
+ title: "Modules",
672
+ path: "/modules"
673
+ }];
901
674
  }
902
- const level = folder.providerData?.level;
903
- switch (level) {
904
- case "products":
905
- return this.getProductFolders(folder);
906
- case "libraries":
907
- return this.getLibraryFolders(folder, auth);
908
- case "media":
909
- return this.getMediaFiles(folder, auth);
910
- default:
911
- return [];
675
+ const root = segments[0];
676
+ if (root !== "modules") return [];
677
+ if (depth === 1) {
678
+ return this.getModules(auth);
679
+ }
680
+ if (depth === 2) {
681
+ const moduleId = segments[1];
682
+ return this.getModuleContent(moduleId, path, auth);
912
683
  }
684
+ if (depth === 4 && segments[2] === "products") {
685
+ const productId = segments[3];
686
+ return this.getLibraryFolders(productId, path, auth);
687
+ }
688
+ if (depth === 5 && segments[2] === "products") {
689
+ const libraryId = segments[4];
690
+ return this.getMediaFiles(libraryId, auth);
691
+ }
692
+ if (depth === 4 && segments[2] === "libraries") {
693
+ const libraryId = segments[3];
694
+ return this.getMediaFiles(libraryId, auth);
695
+ }
696
+ return [];
913
697
  }
914
- getProductFolders(folder) {
915
- const products = folder.providerData?.products || [];
916
- return products.map((p) => ({
917
- type: "folder",
918
- id: p.id,
919
- title: p.title,
920
- image: p.image,
921
- providerData: { level: "libraries", productId: p.id }
922
- }));
698
+ async getModules(auth) {
699
+ const response = await this.apiRequest(this.config.endpoints.modules, auth);
700
+ if (!response) return [];
701
+ const modules = response.data || response.modules || response;
702
+ if (!Array.isArray(modules)) return [];
703
+ const items = [];
704
+ for (const m of modules) {
705
+ if (m.isLocked) continue;
706
+ const moduleId = m.id || m.moduleId;
707
+ const moduleTitle = m.title || m.name;
708
+ const moduleImage = m.image;
709
+ items.push({
710
+ type: "folder",
711
+ id: moduleId,
712
+ title: moduleTitle,
713
+ image: moduleImage,
714
+ path: `/modules/${moduleId}`
715
+ });
716
+ }
717
+ return items;
923
718
  }
924
- async getLibraryFolders(folder, auth) {
925
- const productId = folder.providerData?.productId;
926
- console.log(`APlay getLibraryFolders called with productId:`, productId);
927
- if (!productId) return [];
719
+ async getModuleContent(moduleId, currentPath, auth) {
720
+ const response = await this.apiRequest(this.config.endpoints.modules, auth);
721
+ if (!response) return [];
722
+ const modules = response.data || response.modules || response;
723
+ if (!Array.isArray(modules)) return [];
724
+ const module2 = modules.find((m) => (m.id || m.moduleId) === moduleId);
725
+ if (!module2) return [];
726
+ const allProducts = module2.products || [];
727
+ const products = allProducts.filter((p) => !p.isHidden);
728
+ if (products.length === 0) {
729
+ return this.getLibraryFolders(moduleId, `${currentPath}/libraries`, auth);
730
+ } else if (products.length === 1) {
731
+ const productId = products[0].productId || products[0].id;
732
+ return this.getLibraryFolders(productId, `${currentPath}/libraries`, auth);
733
+ } else {
734
+ return products.map((p) => ({
735
+ type: "folder",
736
+ id: p.productId || p.id,
737
+ title: p.title || p.name,
738
+ image: p.image,
739
+ path: `${currentPath}/products/${p.productId || p.id}`
740
+ }));
741
+ }
742
+ }
743
+ async getLibraryFolders(productId, currentPath, auth) {
928
744
  const pathFn = this.config.endpoints.productLibraries;
929
- const path = pathFn(productId);
930
- console.log(`APlay fetching libraries from: ${path}`);
931
- const response = await this.apiRequest(path, auth);
932
- console.log(`APlay libraries response:`, response ? "received" : "null");
745
+ const apiPath = pathFn(productId);
746
+ const response = await this.apiRequest(apiPath, auth);
933
747
  if (!response) return [];
934
748
  const libraries = response.data || response.libraries || response;
935
- console.log(`APlay libraries count:`, Array.isArray(libraries) ? libraries.length : "not an array");
936
749
  if (!Array.isArray(libraries)) return [];
937
750
  return libraries.map((l) => ({
938
751
  type: "folder",
939
752
  id: l.libraryId || l.id,
940
753
  title: l.title || l.name,
941
754
  image: l.image,
942
- providerData: { level: "media", libraryId: l.libraryId || l.id }
755
+ isLeaf: true,
756
+ path: `${currentPath}/${l.libraryId || l.id}`
943
757
  }));
944
758
  }
945
- async getMediaFiles(folder, auth) {
946
- const libraryId = folder.providerData?.libraryId;
947
- console.log(`APlay getMediaFiles called with libraryId:`, libraryId);
948
- if (!libraryId) return [];
759
+ async getMediaFiles(libraryId, auth) {
949
760
  const pathFn = this.config.endpoints.libraryMedia;
950
- const path = pathFn(libraryId);
951
- console.log(`APlay fetching media from: ${path}`);
952
- const response = await this.apiRequest(path, auth);
953
- console.log(`APlay media response:`, response ? "received" : "null");
761
+ const apiPath = pathFn(libraryId);
762
+ const response = await this.apiRequest(apiPath, auth);
954
763
  if (!response) return [];
955
764
  const mediaItems = response.data || response.media || response;
956
765
  if (!Array.isArray(mediaItems)) return [];
@@ -980,140 +789,140 @@ var APlayProvider = class extends ContentProvider {
980
789
  if (!url) continue;
981
790
  const detectedMediaType = detectMediaType(url, mediaType);
982
791
  const fileId = item.mediaId || item.id;
983
- files.push({
984
- type: "file",
985
- id: fileId,
986
- title: item.title || item.name || item.fileName || "",
987
- mediaType: detectedMediaType,
988
- image: thumbnail,
989
- url,
990
- muxPlaybackId,
991
- mediaId: fileId
992
- });
792
+ files.push({ type: "file", id: fileId, title: item.title || item.name || item.fileName || "", mediaType: detectedMediaType, image: thumbnail, url, muxPlaybackId, mediaId: fileId });
993
793
  }
994
794
  return files;
995
795
  }
996
- async getPresentations(folder, auth) {
997
- const libraryId = folder.providerData?.libraryId;
998
- if (!libraryId) return null;
999
- const files = await this.getMediaFiles(folder, auth);
796
+ async getPresentations(path, auth) {
797
+ const { segments, depth } = parsePath(path);
798
+ if (depth < 4 || segments[0] !== "modules") return null;
799
+ let libraryId;
800
+ const title = "Library";
801
+ if (segments[2] === "products" && depth === 5) {
802
+ libraryId = segments[4];
803
+ } else if (segments[2] === "libraries" && depth === 4) {
804
+ libraryId = segments[3];
805
+ } else {
806
+ return null;
807
+ }
808
+ const files = await this.getMediaFiles(libraryId, auth);
809
+ if (files.length === 0) return null;
810
+ const presentations = files.map((f) => ({ id: f.id, name: f.title, actionType: "play", files: [f] }));
811
+ return { id: libraryId, name: title, sections: [{ id: `section-${libraryId}`, name: title, presentations }], allFiles: files };
812
+ }
813
+ async getPlaylist(path, auth, _resolution) {
814
+ const { segments, depth } = parsePath(path);
815
+ if (depth < 4 || segments[0] !== "modules") return null;
816
+ let libraryId;
817
+ if (segments[2] === "products" && depth === 5) {
818
+ libraryId = segments[4];
819
+ } else if (segments[2] === "libraries" && depth === 4) {
820
+ libraryId = segments[3];
821
+ } else {
822
+ return null;
823
+ }
824
+ const files = await this.getMediaFiles(libraryId, auth);
825
+ return files.length > 0 ? files : null;
826
+ }
827
+ async getInstructions(path, auth) {
828
+ const { segments, depth } = parsePath(path);
829
+ if (depth < 4 || segments[0] !== "modules") return null;
830
+ let libraryId;
831
+ if (segments[2] === "products" && depth === 5) {
832
+ libraryId = segments[4];
833
+ } else if (segments[2] === "libraries" && depth === 4) {
834
+ libraryId = segments[3];
835
+ } else {
836
+ return null;
837
+ }
838
+ const files = await this.getMediaFiles(libraryId, auth);
1000
839
  if (files.length === 0) return null;
1001
- const presentations = files.map((f) => ({
1002
- id: f.id,
1003
- name: f.title,
1004
- actionType: "play",
1005
- files: [f]
840
+ const fileItems = files.map((file) => ({
841
+ id: file.id,
842
+ itemType: "file",
843
+ label: file.title,
844
+ embedUrl: file.url
1006
845
  }));
1007
846
  return {
1008
- id: libraryId,
1009
- name: folder.title,
1010
- image: folder.image,
1011
- sections: [{
847
+ venueName: "Library",
848
+ items: [{
1012
849
  id: `section-${libraryId}`,
1013
- name: folder.title || "Library",
1014
- presentations
1015
- }],
1016
- allFiles: files
850
+ itemType: "section",
851
+ label: "Content",
852
+ children: fileItems
853
+ }]
1017
854
  };
1018
855
  }
1019
856
  async checkMediaLicense(mediaId, auth) {
1020
857
  if (!auth) return null;
1021
858
  try {
1022
859
  const url = `${this.config.apiBase}/prod/reports/media/license-check`;
1023
- const response = await fetch(url, {
1024
- method: "POST",
1025
- headers: {
1026
- "Authorization": `Bearer ${auth.access_token}`,
1027
- "Content-Type": "application/json",
1028
- "Accept": "application/json"
1029
- },
1030
- body: JSON.stringify({ mediaIds: [mediaId] })
1031
- });
860
+ const response = await fetch(url, { method: "POST", headers: { "Authorization": `Bearer ${auth.access_token}`, "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ mediaIds: [mediaId] }) });
1032
861
  if (!response.ok) return null;
1033
862
  const data = await response.json();
1034
863
  const licenseData = Array.isArray(data) ? data : data.data || [];
1035
864
  const result = licenseData.find((item) => item.mediaId === mediaId);
1036
865
  if (result?.isLicensed) {
1037
- const pingbackUrl = `${this.config.apiBase}/prod/reports/media/${mediaId}/stream-count?source=aplay-pro`;
1038
- return {
1039
- mediaId,
1040
- status: "valid",
1041
- message: "Media is licensed for playback",
1042
- expiresAt: result.expiresAt
1043
- };
866
+ return { mediaId, status: "valid", message: "Media is licensed for playback", expiresAt: result.expiresAt };
1044
867
  }
1045
- return {
1046
- mediaId,
1047
- status: "not_licensed",
1048
- message: "Media is not licensed"
1049
- };
868
+ return { mediaId, status: "not_licensed", message: "Media is not licensed" };
1050
869
  } catch {
1051
- return {
1052
- mediaId,
1053
- status: "unknown",
1054
- message: "Unable to verify license status"
1055
- };
870
+ return { mediaId, status: "unknown", message: "Unable to verify license status" };
1056
871
  }
1057
872
  }
1058
873
  };
1059
874
 
1060
- // src/providers/SignPresenterProvider.ts
1061
- var SignPresenterProvider = class extends ContentProvider {
875
+ // src/providers/signPresenter/SignPresenterProvider.ts
876
+ var SignPresenterProvider = class {
1062
877
  constructor() {
1063
- super(...arguments);
878
+ this.apiHelper = new ApiHelper();
1064
879
  this.id = "signpresenter";
1065
880
  this.name = "SignPresenter";
1066
- this.logos = {
1067
- light: "https://signpresenter.com/files/shared/images/logo.png",
1068
- dark: "https://signpresenter.com/files/shared/images/logo.png"
1069
- };
1070
- this.config = {
1071
- id: "signpresenter",
1072
- name: "SignPresenter",
1073
- apiBase: "https://api.signpresenter.com",
1074
- oauthBase: "https://api.signpresenter.com/oauth",
1075
- clientId: "lessonsscreen-tv",
1076
- scopes: ["openid", "profile", "content"],
1077
- supportsDeviceFlow: true,
1078
- deviceAuthEndpoint: "/device/authorize",
1079
- endpoints: {
1080
- playlists: "/content/playlists",
1081
- messages: (playlistId) => `/content/playlists/${playlistId}/messages`
1082
- }
1083
- };
881
+ this.logos = { light: "https://signpresenter.com/files/shared/images/logo.png", dark: "https://signpresenter.com/files/shared/images/logo.png" };
882
+ this.config = { id: "signpresenter", name: "SignPresenter", apiBase: "https://api.signpresenter.com", oauthBase: "https://api.signpresenter.com/oauth", clientId: "lessonsscreen-tv", scopes: ["openid", "profile", "content"], supportsDeviceFlow: true, deviceAuthEndpoint: "/device/authorize", endpoints: { playlists: "/content/playlists", messages: (playlistId) => `/content/playlists/${playlistId}/messages` } };
883
+ this.requiresAuth = true;
884
+ this.authTypes = ["oauth_pkce", "device_flow"];
885
+ this.capabilities = { browse: true, presentations: true, playlist: true, instructions: true, mediaLicensing: false };
1084
886
  }
1085
- getCapabilities() {
1086
- return {
1087
- browse: true,
1088
- presentations: true,
1089
- playlist: false,
1090
- instructions: false,
1091
- expandedInstructions: false,
1092
- mediaLicensing: false
1093
- };
887
+ async apiRequest(path, auth) {
888
+ return this.apiHelper.apiRequest(this.config, this.id, path, auth);
1094
889
  }
1095
- async browse(folder, auth) {
1096
- if (!folder) {
1097
- const path = this.config.endpoints.playlists;
1098
- const response = await this.apiRequest(path, auth);
1099
- if (!response) return [];
1100
- const playlists = Array.isArray(response) ? response : response.data || response.playlists || [];
1101
- if (!Array.isArray(playlists)) return [];
1102
- return playlists.map((p) => ({
890
+ async browse(path, auth) {
891
+ const { segments, depth } = parsePath(path);
892
+ if (depth === 0) {
893
+ return [{
1103
894
  type: "folder",
1104
- id: p.id,
1105
- title: p.name,
1106
- image: p.image,
1107
- providerData: { level: "messages", playlistId: p.id }
1108
- }));
895
+ id: "playlists-root",
896
+ title: "Playlists",
897
+ path: "/playlists"
898
+ }];
899
+ }
900
+ const root = segments[0];
901
+ if (root !== "playlists") return [];
902
+ if (depth === 1) {
903
+ return this.getPlaylists(auth);
904
+ }
905
+ if (depth === 2) {
906
+ const playlistId = segments[1];
907
+ return this.getMessages(playlistId, auth);
1109
908
  }
1110
- const level = folder.providerData?.level;
1111
- if (level === "messages") return this.getMessages(folder, auth);
1112
909
  return [];
1113
910
  }
1114
- async getMessages(folder, auth) {
1115
- const playlistId = folder.providerData?.playlistId;
1116
- if (!playlistId) return [];
911
+ async getPlaylists(auth) {
912
+ const apiPath = this.config.endpoints.playlists;
913
+ const response = await this.apiRequest(apiPath, auth);
914
+ if (!response) return [];
915
+ const playlists = Array.isArray(response) ? response : response.data || response.playlists || [];
916
+ if (!Array.isArray(playlists)) return [];
917
+ return playlists.map((p) => ({
918
+ type: "folder",
919
+ id: p.id,
920
+ title: p.name,
921
+ image: p.image,
922
+ path: `/playlists/${p.id}`
923
+ }));
924
+ }
925
+ async getMessages(playlistId, auth) {
1117
926
  const pathFn = this.config.endpoints.messages;
1118
927
  const response = await this.apiRequest(pathFn(playlistId), auth);
1119
928
  if (!response) return [];
@@ -1124,93 +933,75 @@ var SignPresenterProvider = class extends ContentProvider {
1124
933
  if (!msg.url) continue;
1125
934
  const url = msg.url;
1126
935
  const seconds = msg.seconds;
1127
- files.push({
1128
- type: "file",
1129
- id: msg.id,
1130
- title: msg.name,
1131
- mediaType: detectMediaType(url, msg.mediaType),
1132
- image: msg.thumbnail || msg.image,
1133
- url,
1134
- // For direct media providers, embedUrl is the media URL itself
1135
- embedUrl: url,
1136
- providerData: seconds !== void 0 ? { seconds } : void 0
1137
- });
936
+ files.push({ type: "file", id: msg.id, title: msg.name, mediaType: detectMediaType(url, msg.mediaType), image: msg.thumbnail || msg.image, url, embedUrl: url, seconds });
1138
937
  }
1139
938
  return files;
1140
939
  }
1141
- async getPresentations(folder, auth) {
1142
- const playlistId = folder.providerData?.playlistId;
1143
- if (!playlistId) return null;
1144
- const files = await this.getMessages(folder, auth);
940
+ async getPresentations(path, auth) {
941
+ const { segments, depth } = parsePath(path);
942
+ if (depth < 2 || segments[0] !== "playlists") return null;
943
+ const playlistId = segments[1];
944
+ const files = await this.getMessages(playlistId, auth);
1145
945
  if (files.length === 0) return null;
1146
- const presentations = files.map((f) => ({
1147
- id: f.id,
1148
- name: f.title,
1149
- actionType: "play",
1150
- files: [f]
946
+ const playlists = await this.getPlaylists(auth);
947
+ const playlist = playlists.find((p) => p.id === playlistId);
948
+ const title = playlist?.title || "Playlist";
949
+ const image = playlist?.image;
950
+ const presentations = files.map((f) => ({ id: f.id, name: f.title, actionType: "play", files: [f] }));
951
+ return { id: playlistId, name: title, image, sections: [{ id: `section-${playlistId}`, name: title, presentations }], allFiles: files };
952
+ }
953
+ async getPlaylist(path, auth, _resolution) {
954
+ const { segments, depth } = parsePath(path);
955
+ if (depth < 2 || segments[0] !== "playlists") return null;
956
+ const playlistId = segments[1];
957
+ const files = await this.getMessages(playlistId, auth);
958
+ return files.length > 0 ? files : null;
959
+ }
960
+ async getInstructions(path, auth) {
961
+ const { segments, depth } = parsePath(path);
962
+ if (depth < 2 || segments[0] !== "playlists") return null;
963
+ const playlistId = segments[1];
964
+ const files = await this.getMessages(playlistId, auth);
965
+ if (files.length === 0) return null;
966
+ const playlists = await this.getPlaylists(auth);
967
+ const playlist = playlists.find((p) => p.id === playlistId);
968
+ const title = playlist?.title || "Playlist";
969
+ const fileItems = files.map((file) => ({
970
+ id: file.id,
971
+ itemType: "file",
972
+ label: file.title,
973
+ seconds: file.seconds,
974
+ embedUrl: file.embedUrl || file.url
1151
975
  }));
1152
976
  return {
1153
- id: playlistId,
1154
- name: folder.title,
1155
- image: folder.image,
1156
- sections: [{
977
+ venueName: title,
978
+ items: [{
1157
979
  id: `section-${playlistId}`,
1158
- name: folder.title || "Playlist",
1159
- presentations
1160
- }],
1161
- allFiles: files
980
+ itemType: "section",
981
+ label: title,
982
+ children: fileItems
983
+ }]
1162
984
  };
1163
985
  }
1164
986
  };
1165
987
 
1166
- // src/providers/LessonsChurchProvider.ts
1167
- var LessonsChurchProvider = class extends ContentProvider {
988
+ // src/providers/lessonsChurch/LessonsChurchProvider.ts
989
+ var LessonsChurchProvider = class {
1168
990
  constructor() {
1169
- super(...arguments);
1170
991
  this.id = "lessonschurch";
1171
992
  this.name = "Lessons.church";
1172
- this.logos = {
1173
- light: "https://lessons.church/images/logo.png",
1174
- dark: "https://lessons.church/images/logo-dark.png"
1175
- };
1176
- this.config = {
1177
- id: "lessonschurch",
1178
- name: "Lessons.church",
1179
- apiBase: "https://api.lessons.church",
1180
- oauthBase: "",
1181
- clientId: "",
1182
- scopes: [],
1183
- endpoints: {
1184
- programs: "/programs/public",
1185
- studies: (programId) => `/studies/public/program/${programId}`,
1186
- lessons: (studyId) => `/lessons/public/study/${studyId}`,
1187
- venues: (lessonId) => `/venues/public/lesson/${lessonId}`,
1188
- playlist: (venueId) => `/venues/playlist/${venueId}`,
1189
- feed: (venueId) => `/venues/public/feed/${venueId}`,
1190
- addOns: "/addOns/public",
1191
- addOnDetail: (id) => `/addOns/public/${id}`
1192
- }
1193
- };
993
+ this.logos = { light: "https://lessons.church/images/logo.png", dark: "https://lessons.church/images/logo-dark.png" };
994
+ this.config = { id: "lessonschurch", name: "Lessons.church", apiBase: "https://api.lessons.church", oauthBase: "", clientId: "", scopes: [], endpoints: { programs: "/programs/public", studies: (programId) => `/studies/public/program/${programId}`, lessons: (studyId) => `/lessons/public/study/${studyId}`, venues: (lessonId) => `/venues/public/lesson/${lessonId}`, playlist: (venueId) => `/venues/playlist/${venueId}`, feed: (venueId) => `/venues/public/feed/${venueId}`, addOns: "/addOns/public", addOnDetail: (id) => `/addOns/public/${id}` } };
995
+ this.requiresAuth = false;
996
+ this.authTypes = ["none"];
997
+ this.capabilities = { browse: true, presentations: true, playlist: true, instructions: true, mediaLicensing: false };
1194
998
  }
1195
- requiresAuth() {
1196
- return false;
1197
- }
1198
- getCapabilities() {
1199
- return {
1200
- browse: true,
1201
- presentations: true,
1202
- playlist: true,
1203
- instructions: true,
1204
- expandedInstructions: true,
1205
- mediaLicensing: false
1206
- };
1207
- }
1208
- async getPlaylist(folder, _auth, resolution) {
1209
- const venueId = folder.providerData?.venueId;
999
+ async getPlaylist(path, _auth, resolution) {
1000
+ const venueId = getSegment(path, 4);
1210
1001
  if (!venueId) return null;
1211
- let path = `/venues/playlist/${venueId}`;
1212
- if (resolution) path += `?resolution=${resolution}`;
1213
- const response = await this.apiRequest(path);
1002
+ let apiPath = `/venues/playlist/${venueId}`;
1003
+ if (resolution) apiPath += `?resolution=${resolution}`;
1004
+ const response = await this.apiRequest(apiPath);
1214
1005
  if (!response) return null;
1215
1006
  const files = [];
1216
1007
  const messages = response.messages || [];
@@ -1222,15 +1013,7 @@ var LessonsChurchProvider = class extends ContentProvider {
1222
1013
  if (!f.url) continue;
1223
1014
  const url = f.url;
1224
1015
  const fileId = f.id || `playlist-${fileIndex++}`;
1225
- files.push({
1226
- type: "file",
1227
- id: fileId,
1228
- title: f.name || msg.name,
1229
- mediaType: detectMediaType(url, f.fileType),
1230
- image: response.lessonImage,
1231
- url,
1232
- providerData: { seconds: f.seconds, loop: f.loop, loopVideo: f.loopVideo }
1233
- });
1016
+ files.push({ type: "file", id: fileId, title: f.name || msg.name, mediaType: detectMediaType(url, f.fileType), image: response.lessonImage, url, seconds: f.seconds, loop: f.loop, loopVideo: f.loopVideo });
1234
1017
  }
1235
1018
  }
1236
1019
  return files;
@@ -1245,48 +1028,31 @@ var LessonsChurchProvider = class extends ContentProvider {
1245
1028
  return null;
1246
1029
  }
1247
1030
  }
1248
- async browse(folder, _auth, resolution) {
1249
- if (!folder) {
1031
+ async browse(path, _auth) {
1032
+ const { segments, depth } = parsePath(path);
1033
+ if (depth === 0) {
1250
1034
  return [
1251
- {
1252
- type: "folder",
1253
- id: "lessons-root",
1254
- title: "Lessons",
1255
- providerData: { level: "programs" }
1256
- },
1257
- {
1258
- type: "folder",
1259
- id: "addons-root",
1260
- title: "Add-Ons",
1261
- providerData: { level: "addOnCategories" }
1262
- }
1035
+ { type: "folder", id: "lessons-root", title: "Lessons", path: "/lessons" },
1036
+ { type: "folder", id: "addons-root", title: "Add-Ons", path: "/addons" }
1263
1037
  ];
1264
1038
  }
1265
- const level = folder.providerData?.level;
1266
- switch (level) {
1267
- // Lessons hierarchy
1268
- case "programs":
1269
- return this.getPrograms();
1270
- case "studies":
1271
- return this.getStudies(folder);
1272
- case "lessons":
1273
- return this.getLessons(folder);
1274
- case "venues":
1275
- return this.getVenues(folder);
1276
- case "playlist":
1277
- return this.getPlaylistFiles(folder, resolution);
1278
- // Add-ons hierarchy
1279
- case "addOnCategories":
1280
- return this.getAddOnCategories();
1281
- case "addOns":
1282
- return this.getAddOnsByCategory(folder);
1283
- default:
1284
- return [];
1285
- }
1039
+ const root = segments[0];
1040
+ if (root === "lessons") return this.browseLessons(path, segments);
1041
+ if (root === "addons") return this.browseAddOns(path, segments);
1042
+ return [];
1043
+ }
1044
+ async browseLessons(currentPath, segments) {
1045
+ const depth = segments.length;
1046
+ if (depth === 1) return this.getPrograms();
1047
+ if (depth === 2) return this.getStudies(segments[1], currentPath);
1048
+ if (depth === 3) return this.getLessons(segments[2], currentPath);
1049
+ if (depth === 4) return this.getVenues(segments[3], currentPath);
1050
+ if (depth === 5) return this.getPlaylistFiles(segments[4]);
1051
+ return [];
1286
1052
  }
1287
1053
  async getPrograms() {
1288
- const path = this.config.endpoints.programs;
1289
- const response = await this.apiRequest(path);
1054
+ const apiPath = this.config.endpoints.programs;
1055
+ const response = await this.apiRequest(apiPath);
1290
1056
  if (!response) return [];
1291
1057
  const programs = Array.isArray(response) ? response : [];
1292
1058
  return programs.map((p) => ({
@@ -1294,12 +1060,10 @@ var LessonsChurchProvider = class extends ContentProvider {
1294
1060
  id: p.id,
1295
1061
  title: p.name,
1296
1062
  image: p.image,
1297
- providerData: { level: "studies", programId: p.id }
1063
+ path: `/lessons/${p.id}`
1298
1064
  }));
1299
1065
  }
1300
- async getStudies(folder) {
1301
- const programId = folder.providerData?.programId;
1302
- if (!programId) return [];
1066
+ async getStudies(programId, currentPath) {
1303
1067
  const pathFn = this.config.endpoints.studies;
1304
1068
  const response = await this.apiRequest(pathFn(programId));
1305
1069
  if (!response) return [];
@@ -1309,12 +1073,10 @@ var LessonsChurchProvider = class extends ContentProvider {
1309
1073
  id: s.id,
1310
1074
  title: s.name,
1311
1075
  image: s.image,
1312
- providerData: { level: "lessons", studyId: s.id }
1076
+ path: `${currentPath}/${s.id}`
1313
1077
  }));
1314
1078
  }
1315
- async getLessons(folder) {
1316
- const studyId = folder.providerData?.studyId;
1317
- if (!studyId) return [];
1079
+ async getLessons(studyId, currentPath) {
1318
1080
  const pathFn = this.config.endpoints.lessons;
1319
1081
  const response = await this.apiRequest(pathFn(studyId));
1320
1082
  if (!response) return [];
@@ -1324,31 +1086,38 @@ var LessonsChurchProvider = class extends ContentProvider {
1324
1086
  id: l.id,
1325
1087
  title: l.name || l.title,
1326
1088
  image: l.image,
1327
- providerData: { level: "venues", lessonId: l.id, lessonImage: l.image }
1089
+ path: `${currentPath}/${l.id}`
1328
1090
  }));
1329
1091
  }
1330
- async getVenues(folder) {
1331
- const lessonId = folder.providerData?.lessonId;
1332
- if (!lessonId) return [];
1092
+ async getVenues(lessonId, currentPath) {
1333
1093
  const pathFn = this.config.endpoints.venues;
1334
1094
  const response = await this.apiRequest(pathFn(lessonId));
1335
1095
  if (!response) return [];
1096
+ const lessonResponse = await this.apiRequest(`/lessons/public/${lessonId}`);
1097
+ const lessonImage = lessonResponse?.image;
1336
1098
  const venues = Array.isArray(response) ? response : [];
1337
1099
  return venues.map((v) => ({
1338
1100
  type: "folder",
1339
1101
  id: v.id,
1340
1102
  title: v.name,
1341
- image: folder.providerData?.lessonImage,
1342
- providerData: { level: "playlist", venueId: v.id }
1103
+ image: lessonImage,
1104
+ isLeaf: true,
1105
+ path: `${currentPath}/${v.id}`
1343
1106
  }));
1344
1107
  }
1345
- async getPlaylistFiles(folder, resolution) {
1346
- const files = await this.getPlaylist(folder, null, resolution);
1108
+ async getPlaylistFiles(venueId) {
1109
+ const files = await this.getPlaylist(`/lessons/_/_/_/${venueId}`, null);
1347
1110
  return files || [];
1348
1111
  }
1112
+ async browseAddOns(_currentPath, segments) {
1113
+ const depth = segments.length;
1114
+ if (depth === 1) return this.getAddOnCategories();
1115
+ if (depth === 2) return this.getAddOnsByCategory(segments[1]);
1116
+ return [];
1117
+ }
1349
1118
  async getAddOnCategories() {
1350
- const path = this.config.endpoints.addOns;
1351
- const response = await this.apiRequest(path);
1119
+ const apiPath = this.config.endpoints.addOns;
1120
+ const response = await this.apiRequest(apiPath);
1352
1121
  if (!response) return [];
1353
1122
  const addOns = Array.isArray(response) ? response : [];
1354
1123
  const categories = Array.from(new Set(addOns.map((a) => a.category).filter(Boolean)));
@@ -1356,17 +1125,16 @@ var LessonsChurchProvider = class extends ContentProvider {
1356
1125
  type: "folder",
1357
1126
  id: `category-${category}`,
1358
1127
  title: category,
1359
- providerData: {
1360
- level: "addOns",
1361
- category,
1362
- allAddOns: addOns
1363
- }
1128
+ path: `/addons/${encodeURIComponent(category)}`
1364
1129
  }));
1365
1130
  }
1366
- async getAddOnsByCategory(folder) {
1367
- const category = folder.providerData?.category;
1368
- const allAddOns = folder.providerData?.allAddOns || [];
1369
- const filtered = allAddOns.filter((a) => a.category === category);
1131
+ async getAddOnsByCategory(category) {
1132
+ const decodedCategory = decodeURIComponent(category);
1133
+ const apiPath = this.config.endpoints.addOns;
1134
+ const response = await this.apiRequest(apiPath);
1135
+ if (!response) return [];
1136
+ const allAddOns = Array.isArray(response) ? response : [];
1137
+ const filtered = allAddOns.filter((a) => a.category === decodedCategory);
1370
1138
  const files = [];
1371
1139
  for (const addOn of filtered) {
1372
1140
  const file = await this.convertAddOnToFile(addOn);
@@ -1376,8 +1144,8 @@ var LessonsChurchProvider = class extends ContentProvider {
1376
1144
  }
1377
1145
  async convertAddOnToFile(addOn) {
1378
1146
  const pathFn = this.config.endpoints.addOnDetail;
1379
- const path = pathFn(addOn.id);
1380
- const detail = await this.apiRequest(path);
1147
+ const apiPath = pathFn(addOn.id);
1148
+ const detail = await this.apiRequest(apiPath);
1381
1149
  if (!detail) return null;
1382
1150
  let url = "";
1383
1151
  let mediaType = "video";
@@ -1394,51 +1162,18 @@ var LessonsChurchProvider = class extends ContentProvider {
1394
1162
  } else {
1395
1163
  return null;
1396
1164
  }
1397
- return {
1398
- type: "file",
1399
- id: addOn.id,
1400
- title: addOn.name,
1401
- mediaType,
1402
- image: addOn.image,
1403
- url,
1404
- embedUrl: `https://lessons.church/embed/addon/${addOn.id}`,
1405
- providerData: {
1406
- seconds,
1407
- loopVideo: video?.loopVideo || false
1408
- }
1409
- };
1165
+ return { type: "file", id: addOn.id, title: addOn.name, mediaType, image: addOn.image, url, embedUrl: `https://lessons.church/embed/addon/${addOn.id}`, seconds, loopVideo: video?.loopVideo || false };
1410
1166
  }
1411
- async getPresentations(folder, _auth, resolution) {
1412
- const venueId = folder.providerData?.venueId;
1167
+ async getPresentations(path, _auth) {
1168
+ const venueId = getSegment(path, 4);
1413
1169
  if (!venueId) return null;
1414
- let path = `/venues/public/feed/${venueId}`;
1415
- if (resolution) path += `?resolution=${resolution}`;
1416
- const venueData = await this.apiRequest(path);
1170
+ const apiPath = `/venues/public/feed/${venueId}`;
1171
+ const venueData = await this.apiRequest(apiPath);
1417
1172
  if (!venueData) return null;
1418
1173
  return this.convertVenueToPlan(venueData);
1419
1174
  }
1420
- async getInstructions(folder, _auth) {
1421
- const venueId = folder.providerData?.venueId;
1422
- if (!venueId) return null;
1423
- const response = await this.apiRequest(`/venues/public/planItems/${venueId}`);
1424
- if (!response) return null;
1425
- const processItem = (item) => ({
1426
- id: item.id,
1427
- itemType: item.itemType,
1428
- relatedId: item.relatedId,
1429
- label: item.label,
1430
- description: item.description,
1431
- seconds: item.seconds,
1432
- children: item.children?.map(processItem),
1433
- embedUrl: this.getEmbedUrl(item.itemType, item.relatedId)
1434
- });
1435
- return {
1436
- venueName: response.venueName,
1437
- items: (response.items || []).map(processItem)
1438
- };
1439
- }
1440
- async getExpandedInstructions(folder, _auth) {
1441
- const venueId = folder.providerData?.venueId;
1175
+ async getInstructions(path, _auth) {
1176
+ const venueId = getSegment(path, 4);
1442
1177
  if (!venueId) return null;
1443
1178
  const [planItemsResponse, actionsResponse] = await Promise.all([
1444
1179
  this.apiRequest(`/venues/public/planItems/${venueId}`),
@@ -1449,66 +1184,48 @@ var LessonsChurchProvider = class extends ContentProvider {
1449
1184
  if (actionsResponse?.sections) {
1450
1185
  for (const section of actionsResponse.sections) {
1451
1186
  if (section.id && section.actions) {
1452
- sectionActionsMap.set(section.id, section.actions.map((action) => ({
1453
- id: action.id,
1454
- itemType: "lessonAction",
1455
- relatedId: action.id,
1456
- label: action.name,
1457
- description: action.actionType,
1458
- seconds: action.seconds,
1459
- embedUrl: this.getEmbedUrl("lessonAction", action.id)
1460
- })));
1187
+ sectionActionsMap.set(section.id, section.actions.map((action) => {
1188
+ const embedUrl = this.getEmbedUrl("action", action.id);
1189
+ const seconds = action.seconds ?? estimateImageDuration();
1190
+ return { id: action.id, itemType: "action", relatedId: action.id, label: action.name, description: action.actionType, seconds, children: [{ id: action.id + "-file", itemType: "file", label: action.name, seconds, embedUrl }] };
1191
+ }));
1461
1192
  }
1462
1193
  }
1463
1194
  }
1464
1195
  const processItem = (item) => {
1465
1196
  const relatedId = item.relatedId;
1466
- const itemType = item.itemType;
1197
+ const itemType = this.normalizeItemType(item.itemType);
1467
1198
  const children = item.children;
1468
1199
  let processedChildren;
1469
1200
  if (children) {
1470
1201
  processedChildren = children.map((child) => {
1471
1202
  const childRelatedId = child.relatedId;
1203
+ const childItemType = this.normalizeItemType(child.itemType);
1472
1204
  if (childRelatedId && sectionActionsMap.has(childRelatedId)) {
1473
- return {
1474
- id: child.id,
1475
- itemType: child.itemType,
1476
- relatedId: childRelatedId,
1477
- label: child.label,
1478
- description: child.description,
1479
- seconds: child.seconds,
1480
- children: sectionActionsMap.get(childRelatedId),
1481
- embedUrl: this.getEmbedUrl(child.itemType, childRelatedId)
1482
- };
1205
+ return { id: child.id, itemType: childItemType, relatedId: childRelatedId, label: child.label, description: child.description, seconds: child.seconds, children: sectionActionsMap.get(childRelatedId), embedUrl: this.getEmbedUrl(childItemType, childRelatedId) };
1483
1206
  }
1484
1207
  return processItem(child);
1485
1208
  });
1486
1209
  }
1487
- return {
1488
- id: item.id,
1489
- itemType,
1490
- relatedId,
1491
- label: item.label,
1492
- description: item.description,
1493
- seconds: item.seconds,
1494
- children: processedChildren,
1495
- embedUrl: this.getEmbedUrl(itemType, relatedId)
1496
- };
1497
- };
1498
- return {
1499
- venueName: planItemsResponse.venueName,
1500
- items: (planItemsResponse.items || []).map(processItem)
1210
+ return { id: item.id, itemType, relatedId, label: item.label, description: item.description, seconds: item.seconds, children: processedChildren, embedUrl: this.getEmbedUrl(itemType, relatedId) };
1501
1211
  };
1212
+ return { venueName: planItemsResponse.venueName, items: (planItemsResponse.items || []).map(processItem) };
1213
+ }
1214
+ normalizeItemType(type) {
1215
+ if (type === "lessonSection") return "section";
1216
+ if (type === "lessonAction") return "action";
1217
+ if (type === "lessonAddOn") return "addon";
1218
+ return type;
1502
1219
  }
1503
1220
  getEmbedUrl(itemType, relatedId) {
1504
1221
  if (!relatedId) return void 0;
1505
1222
  const baseUrl = "https://lessons.church";
1506
1223
  switch (itemType) {
1507
- case "lessonAction":
1224
+ case "action":
1508
1225
  return `${baseUrl}/embed/action/${relatedId}`;
1509
- case "lessonAddOn":
1226
+ case "addon":
1510
1227
  return `${baseUrl}/embed/addon/${relatedId}`;
1511
- case "lessonSection":
1228
+ case "section":
1512
1229
  return `${baseUrl}/embed/section/${relatedId}`;
1513
1230
  default:
1514
1231
  return void 0;
@@ -1526,16 +1243,7 @@ var LessonsChurchProvider = class extends ContentProvider {
1526
1243
  for (const file of action.files || []) {
1527
1244
  if (!file.url) continue;
1528
1245
  const embedUrl = action.id ? `https://lessons.church/embed/action/${action.id}` : void 0;
1529
- const contentFile = {
1530
- type: "file",
1531
- id: file.id || "",
1532
- title: file.name || "",
1533
- mediaType: detectMediaType(file.url, file.fileType),
1534
- image: venue.lessonImage,
1535
- url: file.url,
1536
- embedUrl,
1537
- providerData: { seconds: file.seconds, streamUrl: file.streamUrl }
1538
- };
1246
+ const contentFile = { type: "file", id: file.id || "", title: file.name || "", mediaType: detectMediaType(file.url, file.fileType), image: venue.lessonImage, url: file.url, embedUrl, seconds: file.seconds, streamUrl: file.streamUrl };
1539
1247
  files.push(contentFile);
1540
1248
  allFiles.push(contentFile);
1541
1249
  }
@@ -1547,96 +1255,73 @@ var LessonsChurchProvider = class extends ContentProvider {
1547
1255
  sections.push({ id: section.id || "", name: section.name || "Untitled Section", presentations });
1548
1256
  }
1549
1257
  }
1550
- return {
1551
- id: venue.id || "",
1552
- name: venue.lessonName || venue.name || "Plan",
1553
- description: venue.lessonDescription,
1554
- image: venue.lessonImage,
1555
- sections,
1556
- allFiles
1557
- };
1258
+ return { id: venue.id || "", name: venue.lessonName || venue.name || "Plan", description: venue.lessonDescription, image: venue.lessonImage, sections, allFiles };
1558
1259
  }
1559
1260
  };
1560
1261
 
1561
- // src/providers/b1church/auth.ts
1562
- function buildB1AuthUrl(config, appBase, redirectUri, state) {
1262
+ // src/providers/b1Church/auth.ts
1263
+ async function generateCodeChallenge(verifier) {
1264
+ const encoder = new TextEncoder();
1265
+ const data = encoder.encode(verifier);
1266
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1267
+ const hashArray = new Uint8Array(hashBuffer);
1268
+ let binary = "";
1269
+ for (let i = 0; i < hashArray.length; i++) {
1270
+ binary += String.fromCharCode(hashArray[i]);
1271
+ }
1272
+ const base64 = btoa(binary);
1273
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1274
+ }
1275
+ async function buildB1AuthUrl(config, appBase, redirectUri, codeVerifier, state) {
1276
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
1563
1277
  const oauthParams = new URLSearchParams({
1278
+ response_type: "code",
1564
1279
  client_id: config.clientId,
1565
1280
  redirect_uri: redirectUri,
1566
- response_type: "code",
1567
- scope: config.scopes.join(" ")
1281
+ scope: config.scopes.join(" "),
1282
+ code_challenge: codeChallenge,
1283
+ code_challenge_method: "S256",
1284
+ state: state || ""
1568
1285
  });
1569
- if (state) {
1570
- oauthParams.set("state", state);
1286
+ const url = `${appBase}/oauth?${oauthParams.toString()}`;
1287
+ return { url, challengeMethod: "S256" };
1288
+ }
1289
+ async function exchangeCodeForTokensWithPKCE(config, code, redirectUri, codeVerifier) {
1290
+ try {
1291
+ const params = { grant_type: "authorization_code", code, client_id: config.clientId, code_verifier: codeVerifier, redirect_uri: redirectUri };
1292
+ const tokenUrl = `${config.oauthBase}/token`;
1293
+ const response = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params) });
1294
+ if (!response.ok) {
1295
+ return null;
1296
+ }
1297
+ const data = await response.json();
1298
+ return { access_token: data.access_token, refresh_token: data.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || config.scopes.join(" ") };
1299
+ } catch {
1300
+ return null;
1571
1301
  }
1572
- const returnUrl = `/oauth?${oauthParams.toString()}`;
1573
- const url = `${appBase}/login?returnUrl=${encodeURIComponent(returnUrl)}`;
1574
- return { url, challengeMethod: "none" };
1575
1302
  }
1576
1303
  async function exchangeCodeForTokensWithSecret(config, code, redirectUri, clientSecret) {
1577
1304
  try {
1578
- const params = {
1579
- grant_type: "authorization_code",
1580
- code,
1581
- client_id: config.clientId,
1582
- client_secret: clientSecret,
1583
- redirect_uri: redirectUri
1584
- };
1305
+ const params = { grant_type: "authorization_code", code, client_id: config.clientId, client_secret: clientSecret, redirect_uri: redirectUri };
1585
1306
  const tokenUrl = `${config.oauthBase}/token`;
1586
- console.log(`B1Church token exchange request to: ${tokenUrl}`);
1587
- console.log(` - client_id: ${config.clientId}`);
1588
- console.log(` - redirect_uri: ${redirectUri}`);
1589
- console.log(` - code: ${code.substring(0, 10)}...`);
1590
- const response = await fetch(tokenUrl, {
1591
- method: "POST",
1592
- headers: { "Content-Type": "application/json" },
1593
- body: JSON.stringify(params)
1594
- });
1595
- console.log(`B1Church token response status: ${response.status}`);
1307
+ const response = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params) });
1596
1308
  if (!response.ok) {
1597
- const errorText = await response.text();
1598
- console.error(`B1Church token exchange failed: ${response.status} - ${errorText}`);
1599
1309
  return null;
1600
1310
  }
1601
1311
  const data = await response.json();
1602
- console.log(`B1Church token exchange successful, got access_token: ${!!data.access_token}`);
1603
- return {
1604
- access_token: data.access_token,
1605
- refresh_token: data.refresh_token,
1606
- token_type: data.token_type || "Bearer",
1607
- created_at: Math.floor(Date.now() / 1e3),
1608
- expires_in: data.expires_in,
1609
- scope: data.scope || config.scopes.join(" ")
1610
- };
1611
- } catch (error) {
1612
- console.error("B1Church token exchange error:", error);
1312
+ return { access_token: data.access_token, refresh_token: data.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || config.scopes.join(" ") };
1313
+ } catch {
1613
1314
  return null;
1614
1315
  }
1615
1316
  }
1616
1317
  async function refreshTokenWithSecret(config, auth, clientSecret) {
1617
1318
  if (!auth.refresh_token) return null;
1618
1319
  try {
1619
- const params = {
1620
- grant_type: "refresh_token",
1621
- refresh_token: auth.refresh_token,
1622
- client_id: config.clientId,
1623
- client_secret: clientSecret
1624
- };
1625
- const response = await fetch(`${config.oauthBase}/token`, {
1626
- method: "POST",
1627
- headers: { "Content-Type": "application/json" },
1628
- body: JSON.stringify(params)
1629
- });
1320
+ const params = { grant_type: "refresh_token", refresh_token: auth.refresh_token, client_id: config.clientId, client_secret: clientSecret };
1321
+ const response = await fetch(`${config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params) });
1630
1322
  if (!response.ok) return null;
1631
1323
  const data = await response.json();
1632
- return {
1633
- access_token: data.access_token,
1634
- refresh_token: data.refresh_token || auth.refresh_token,
1635
- token_type: data.token_type || "Bearer",
1636
- created_at: Math.floor(Date.now() / 1e3),
1637
- expires_in: data.expires_in,
1638
- scope: data.scope || auth.scope
1639
- };
1324
+ return { access_token: data.access_token, refresh_token: data.refresh_token || auth.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || auth.scope };
1640
1325
  } catch {
1641
1326
  return null;
1642
1327
  }
@@ -1644,46 +1329,21 @@ async function refreshTokenWithSecret(config, auth, clientSecret) {
1644
1329
  async function initiateDeviceFlow(config) {
1645
1330
  if (!config.supportsDeviceFlow || !config.deviceAuthEndpoint) return null;
1646
1331
  try {
1647
- const response = await fetch(`${config.oauthBase}${config.deviceAuthEndpoint}`, {
1648
- method: "POST",
1649
- headers: { "Content-Type": "application/json" },
1650
- body: JSON.stringify({
1651
- client_id: config.clientId,
1652
- scope: config.scopes.join(" ")
1653
- })
1654
- });
1332
+ const response = await fetch(`${config.oauthBase}${config.deviceAuthEndpoint}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: config.clientId, scope: config.scopes.join(" ") }) });
1655
1333
  if (!response.ok) {
1656
- const errorText = await response.text();
1657
- console.error(`B1Church device authorize failed: ${response.status} - ${errorText}`);
1658
1334
  return null;
1659
1335
  }
1660
1336
  return await response.json();
1661
- } catch (error) {
1662
- console.error("B1Church device flow initiation error:", error);
1337
+ } catch {
1663
1338
  return null;
1664
1339
  }
1665
1340
  }
1666
1341
  async function pollDeviceFlowToken(config, deviceCode) {
1667
- try {
1668
- const response = await fetch(`${config.oauthBase}/token`, {
1669
- method: "POST",
1670
- headers: { "Content-Type": "application/json" },
1671
- body: JSON.stringify({
1672
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1673
- device_code: deviceCode,
1674
- client_id: config.clientId
1675
- })
1676
- });
1342
+ try {
1343
+ const response = await fetch(`${config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: deviceCode, client_id: config.clientId }) });
1677
1344
  if (response.ok) {
1678
1345
  const data = await response.json();
1679
- return {
1680
- access_token: data.access_token,
1681
- refresh_token: data.refresh_token,
1682
- token_type: data.token_type || "Bearer",
1683
- created_at: Math.floor(Date.now() / 1e3),
1684
- expires_in: data.expires_in,
1685
- scope: data.scope || config.scopes.join(" ")
1686
- };
1346
+ return { access_token: data.access_token, refresh_token: data.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || config.scopes.join(" ") };
1687
1347
  }
1688
1348
  const errorData = await response.json();
1689
1349
  switch (errorData.error) {
@@ -1703,7 +1363,7 @@ async function pollDeviceFlowToken(config, deviceCode) {
1703
1363
  }
1704
1364
  }
1705
1365
 
1706
- // src/providers/b1church/api.ts
1366
+ // src/providers/b1Church/api.ts
1707
1367
  var API_BASE = "https://api.churchapps.org";
1708
1368
  var LESSONS_API_BASE = "https://api.lessons.church";
1709
1369
  var CONTENT_API_BASE = "https://contentapi.churchapps.org";
@@ -1735,10 +1395,7 @@ async function fetchPlans(planTypeId, auth) {
1735
1395
  async function fetchVenueFeed(venueId) {
1736
1396
  try {
1737
1397
  const url = `${LESSONS_API_BASE}/venues/public/feed/${venueId}`;
1738
- const response = await fetch(url, {
1739
- method: "GET",
1740
- headers: { Accept: "application/json" }
1741
- });
1398
+ const response = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
1742
1399
  if (!response.ok) return null;
1743
1400
  return await response.json();
1744
1401
  } catch {
@@ -1748,9 +1405,29 @@ async function fetchVenueFeed(venueId) {
1748
1405
  async function fetchArrangementKey(churchId, arrangementId) {
1749
1406
  try {
1750
1407
  const url = `${CONTENT_API_BASE}/arrangementKeys/presenter/${churchId}/${arrangementId}`;
1408
+ const response = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
1409
+ if (!response.ok) return null;
1410
+ return await response.json();
1411
+ } catch {
1412
+ return null;
1413
+ }
1414
+ }
1415
+ async function fetchFromProviderProxy(method, ministryId, providerId, path, authData, resolution) {
1416
+ try {
1417
+ const url = `${API_BASE}/doing/providerProxy/${method}`;
1418
+ const headers = {
1419
+ "Content-Type": "application/json",
1420
+ Accept: "application/json"
1421
+ };
1422
+ if (authData) {
1423
+ headers["Authorization"] = `Bearer ${authData.access_token}`;
1424
+ }
1425
+ const body = { ministryId, providerId, path };
1426
+ if (resolution !== void 0) body.resolution = resolution;
1751
1427
  const response = await fetch(url, {
1752
- method: "GET",
1753
- headers: { Accept: "application/json" }
1428
+ method: "POST",
1429
+ headers,
1430
+ body: JSON.stringify(body)
1754
1431
  });
1755
1432
  if (!response.ok) return null;
1756
1433
  return await response.json();
@@ -1759,107 +1436,39 @@ async function fetchArrangementKey(churchId, arrangementId) {
1759
1436
  }
1760
1437
  }
1761
1438
 
1762
- // src/providers/b1church/converters.ts
1439
+ // src/providers/b1Church/converters.ts
1763
1440
  function ministryToFolder(ministry) {
1764
- return {
1765
- type: "folder",
1766
- id: ministry.id,
1767
- title: ministry.name,
1768
- image: ministry.photoUrl,
1769
- providerData: {
1770
- level: "ministry",
1771
- ministryId: ministry.id,
1772
- churchId: ministry.churchId
1773
- }
1774
- };
1441
+ return { type: "folder", id: ministry.id, title: ministry.name, path: "", image: ministry.photoUrl };
1775
1442
  }
1776
- function planTypeToFolder(planType, ministryId) {
1777
- return {
1778
- type: "folder",
1779
- id: planType.id,
1780
- title: planType.name,
1781
- providerData: {
1782
- level: "planType",
1783
- planTypeId: planType.id,
1784
- ministryId,
1785
- churchId: planType.churchId
1786
- }
1787
- };
1443
+ function planTypeToFolder(planType) {
1444
+ return { type: "folder", id: planType.id, title: planType.name, path: "" };
1788
1445
  }
1789
1446
  function planToFolder(plan) {
1790
- return {
1791
- type: "folder",
1792
- id: plan.id,
1793
- title: plan.name,
1794
- providerData: {
1795
- isLeaf: true,
1796
- level: "plan",
1797
- planId: plan.id,
1798
- planTypeId: plan.planTypeId,
1799
- ministryId: plan.ministryId,
1800
- churchId: plan.churchId,
1801
- serviceDate: plan.serviceDate,
1802
- contentType: plan.contentType,
1803
- contentId: plan.contentId
1804
- }
1805
- };
1447
+ return { type: "folder", id: plan.id, title: plan.name, path: "", isLeaf: true };
1806
1448
  }
1807
1449
  async function planItemToPresentation(item, venueFeed) {
1808
1450
  const itemType = item.itemType;
1809
1451
  if (itemType === "arrangementKey" && item.churchId && item.relatedId) {
1810
1452
  const songData = await fetchArrangementKey(item.churchId, item.relatedId);
1811
- if (songData) {
1812
- return arrangementToPresentation(item, songData);
1813
- }
1453
+ if (songData) return arrangementToPresentation(item, songData);
1814
1454
  }
1815
- if ((itemType === "lessonSection" || itemType === "lessonAction" || itemType === "lessonAddOn") && venueFeed) {
1455
+ if ((itemType === "lessonSection" || itemType === "section" || itemType === "lessonAction" || itemType === "action" || itemType === "lessonAddOn" || itemType === "addon") && venueFeed) {
1816
1456
  const files = getFilesFromVenueFeed(venueFeed, itemType, item.relatedId);
1817
- if (files.length > 0) {
1818
- return {
1819
- id: item.id,
1820
- name: item.label || "Lesson Content",
1821
- actionType: itemType === "lessonAddOn" ? "add-on" : "play",
1822
- files
1823
- };
1824
- }
1457
+ if (files.length > 0) return { id: item.id, name: item.label || "Lesson Content", actionType: itemType === "lessonAddOn" || itemType === "addon" ? "add-on" : "play", files };
1825
1458
  }
1826
1459
  if (itemType === "item" || itemType === "header") {
1827
- return {
1828
- id: item.id,
1829
- name: item.label || "",
1830
- actionType: "other",
1831
- files: [],
1832
- providerData: {
1833
- itemType,
1834
- description: item.description,
1835
- seconds: item.seconds
1836
- }
1837
- };
1460
+ return { id: item.id, name: item.label || "", actionType: "other", files: [], providerData: { itemType, description: item.description, seconds: item.seconds } };
1838
1461
  }
1839
1462
  return null;
1840
1463
  }
1841
1464
  function arrangementToPresentation(item, songData) {
1842
1465
  const title = songData.songDetail?.title || item.label || "Song";
1843
- return {
1844
- id: item.id,
1845
- name: title,
1846
- actionType: "other",
1847
- files: [],
1848
- providerData: {
1849
- itemType: "song",
1850
- title,
1851
- artist: songData.songDetail?.artist,
1852
- lyrics: songData.arrangement?.lyrics,
1853
- keySignature: songData.arrangementKey?.keySignature,
1854
- arrangementName: songData.arrangement?.name,
1855
- seconds: songData.songDetail?.seconds || item.seconds
1856
- }
1857
- };
1466
+ return { id: item.id, name: title, actionType: "other", files: [], providerData: { itemType: "song", title, artist: songData.songDetail?.artist, lyrics: songData.arrangement?.lyrics, keySignature: songData.arrangementKey?.keySignature, arrangementName: songData.arrangement?.name, seconds: songData.songDetail?.seconds || item.seconds } };
1858
1467
  }
1859
1468
  function getFilesFromVenueFeed(venueFeed, itemType, relatedId) {
1860
1469
  const files = [];
1861
1470
  if (!relatedId) return files;
1862
- if (itemType === "lessonSection") {
1471
+ if (itemType === "lessonSection" || itemType === "section") {
1863
1472
  for (const section of venueFeed.sections || []) {
1864
1473
  if (section.id === relatedId) {
1865
1474
  for (const action of section.actions || []) {
@@ -1871,7 +1480,7 @@ function getFilesFromVenueFeed(venueFeed, itemType, relatedId) {
1871
1480
  break;
1872
1481
  }
1873
1482
  }
1874
- } else if (itemType === "lessonAction") {
1483
+ } else if (itemType === "lessonAction" || itemType === "action") {
1875
1484
  for (const section of venueFeed.sections || []) {
1876
1485
  for (const action of section.actions || []) {
1877
1486
  if (action.id === relatedId) {
@@ -1884,68 +1493,51 @@ function getFilesFromVenueFeed(venueFeed, itemType, relatedId) {
1884
1493
  return files;
1885
1494
  }
1886
1495
  function convertFeedFiles(feedFiles, thumbnailImage) {
1887
- return feedFiles.filter((f) => f.url).map((f) => ({
1888
- type: "file",
1889
- id: f.id || "",
1890
- title: f.name || "",
1891
- mediaType: detectMediaType(f.url || "", f.fileType),
1892
- image: thumbnailImage,
1893
- url: f.url || "",
1894
- providerData: { seconds: f.seconds, streamUrl: f.streamUrl }
1895
- }));
1496
+ return feedFiles.filter((f) => f.url).map((f) => ({ type: "file", id: f.id || "", title: f.name || "", mediaType: detectMediaType(f.url || "", f.fileType), image: thumbnailImage, url: f.url || "", seconds: f.seconds, streamUrl: f.streamUrl }));
1896
1497
  }
1897
1498
  function planItemToInstruction(item) {
1898
- return {
1899
- id: item.id,
1900
- itemType: item.itemType,
1901
- relatedId: item.relatedId,
1902
- label: item.label,
1903
- description: item.description,
1904
- seconds: item.seconds,
1905
- children: item.children?.map(planItemToInstruction)
1906
- };
1499
+ let itemType = item.itemType;
1500
+ switch (item.itemType) {
1501
+ case "lessonSection":
1502
+ itemType = "section";
1503
+ break;
1504
+ case "lessonAction":
1505
+ itemType = "action";
1506
+ break;
1507
+ case "lessonAddOn":
1508
+ itemType = "addon";
1509
+ break;
1510
+ }
1511
+ return { id: item.id, itemType, relatedId: item.relatedId, label: item.label, description: item.description, seconds: item.seconds, children: item.children?.map(planItemToInstruction) };
1907
1512
  }
1908
1513
 
1909
- // src/providers/b1church/B1ChurchProvider.ts
1910
- var B1ChurchProvider = class extends ContentProvider {
1514
+ // src/providers/b1Church/B1ChurchProvider.ts
1515
+ function isExternalProviderItem(item) {
1516
+ if (!item.providerId || item.providerId === "b1church") return false;
1517
+ if (item.providerPath) return true;
1518
+ const itemType = item.itemType || "";
1519
+ return itemType.startsWith("provider");
1520
+ }
1521
+ var B1ChurchProvider = class {
1911
1522
  constructor() {
1912
- super(...arguments);
1523
+ this.apiHelper = new ApiHelper();
1913
1524
  this.id = "b1church";
1914
1525
  this.name = "B1.Church";
1915
- this.logos = {
1916
- light: "https://b1.church/b1-church-logo.png",
1917
- dark: "https://b1.church/b1-church-logo.png"
1918
- };
1919
- this.config = {
1920
- id: "b1church",
1921
- name: "B1.Church",
1922
- apiBase: `${API_BASE}/doing`,
1923
- oauthBase: `${API_BASE}/membership/oauth`,
1924
- clientId: "",
1925
- scopes: ["plans"],
1926
- supportsDeviceFlow: true,
1927
- deviceAuthEndpoint: "/device/authorize",
1928
- endpoints: {
1929
- planItems: (churchId, planId) => `/planItems/presenter/${churchId}/${planId}`
1930
- }
1931
- };
1526
+ this.logos = { light: "https://b1.church/b1-church-logo.png", dark: "https://b1.church/b1-church-logo.png" };
1527
+ this.config = { id: "b1church", name: "B1.Church", apiBase: `${API_BASE}/doing`, oauthBase: `${API_BASE}/membership/oauth`, clientId: "nsowldn58dk", scopes: ["plans"], supportsDeviceFlow: true, deviceAuthEndpoint: "/device/authorize", endpoints: { planItems: (churchId, planId) => `/planFeed/presenter/${churchId}/${planId}` } };
1932
1528
  this.appBase = "https://admin.b1.church";
1529
+ this.requiresAuth = true;
1530
+ this.authTypes = ["oauth_pkce", "device_flow"];
1531
+ this.capabilities = { browse: true, presentations: true, playlist: true, instructions: true, mediaLicensing: false };
1933
1532
  }
1934
- requiresAuth() {
1935
- return true;
1533
+ async apiRequest(path, authData) {
1534
+ return this.apiHelper.apiRequest(this.config, this.id, path, authData);
1936
1535
  }
1937
- getCapabilities() {
1938
- return {
1939
- browse: true,
1940
- presentations: true,
1941
- playlist: true,
1942
- instructions: true,
1943
- expandedInstructions: true,
1944
- mediaLicensing: false
1945
- };
1536
+ async buildAuthUrl(codeVerifier, redirectUri, state) {
1537
+ return buildB1AuthUrl(this.config, this.appBase, redirectUri, codeVerifier, state);
1946
1538
  }
1947
- async buildAuthUrl(_codeVerifier, redirectUri, state) {
1948
- return buildB1AuthUrl(this.config, this.appBase, redirectUri, state);
1539
+ async exchangeCodeForTokensWithPKCE(code, redirectUri, codeVerifier) {
1540
+ return exchangeCodeForTokensWithPKCE(this.config, code, redirectUri, codeVerifier);
1949
1541
  }
1950
1542
  async exchangeCodeForTokensWithSecret(code, redirectUri, clientSecret) {
1951
1543
  return exchangeCodeForTokensWithSecret(this.config, code, redirectUri, clientSecret);
@@ -1959,35 +1551,75 @@ var B1ChurchProvider = class extends ContentProvider {
1959
1551
  async pollDeviceFlowToken(deviceCode) {
1960
1552
  return pollDeviceFlowToken(this.config, deviceCode);
1961
1553
  }
1962
- async browse(folder, authData) {
1963
- if (!folder) {
1554
+ async browse(path, authData) {
1555
+ const { segments, depth } = parsePath(path);
1556
+ if (depth === 0) {
1557
+ return [{
1558
+ type: "folder",
1559
+ id: "ministries-root",
1560
+ title: "Ministries",
1561
+ path: "/ministries"
1562
+ }];
1563
+ }
1564
+ const root = segments[0];
1565
+ if (root !== "ministries") return [];
1566
+ if (depth === 1) {
1964
1567
  const ministries = await fetchMinistries(authData);
1965
- return ministries.map(ministryToFolder);
1568
+ return ministries.map((m) => {
1569
+ const folder = ministryToFolder(m);
1570
+ return { ...folder, path: `/ministries/${m.id}` };
1571
+ });
1966
1572
  }
1967
- const level = folder.providerData?.level;
1968
- if (level === "ministry") {
1969
- const ministryId = folder.providerData?.ministryId;
1970
- if (!ministryId) return [];
1573
+ if (depth === 2) {
1574
+ const ministryId = segments[1];
1971
1575
  const planTypes = await fetchPlanTypes(ministryId, authData);
1972
- return planTypes.map((pt) => planTypeToFolder(pt, ministryId));
1576
+ return planTypes.map((pt) => {
1577
+ const folder = planTypeToFolder(pt);
1578
+ return { ...folder, path: `/ministries/${ministryId}/${pt.id}` };
1579
+ });
1973
1580
  }
1974
- if (level === "planType") {
1975
- const planTypeId = folder.providerData?.planTypeId;
1976
- if (!planTypeId) return [];
1581
+ if (depth === 3) {
1582
+ const ministryId = segments[1];
1583
+ const planTypeId = segments[2];
1977
1584
  const plans = await fetchPlans(planTypeId, authData);
1978
- return plans.map(planToFolder);
1585
+ return plans.map((p) => {
1586
+ const folder = planToFolder(p);
1587
+ return {
1588
+ ...folder,
1589
+ isLeaf: true,
1590
+ path: `/ministries/${ministryId}/${planTypeId}/${p.id}`
1591
+ };
1592
+ });
1979
1593
  }
1980
1594
  return [];
1981
1595
  }
1982
- async getPresentations(folder, authData) {
1983
- const level = folder.providerData?.level;
1984
- if (level !== "plan") return null;
1985
- const planId = folder.providerData?.planId;
1986
- const churchId = folder.providerData?.churchId;
1987
- const venueId = folder.providerData?.contentId;
1988
- if (!planId || !churchId) return null;
1596
+ async getPresentations(path, authData) {
1597
+ const { segments, depth } = parsePath(path);
1598
+ if (depth < 4 || segments[0] !== "ministries") return null;
1599
+ const ministryId = segments[1];
1600
+ const planId = segments[3];
1601
+ const planTypeId = segments[2];
1602
+ const plans = await fetchPlans(planTypeId, authData);
1603
+ const planFolder = plans.find((p) => p.id === planId);
1604
+ if (!planFolder) return null;
1605
+ const churchId = planFolder.churchId;
1606
+ const venueId = planFolder.contentId;
1607
+ const planTitle = planFolder.name || "Plan";
1608
+ if (!churchId) return null;
1989
1609
  const pathFn = this.config.endpoints.planItems;
1990
1610
  const planItems = await this.apiRequest(pathFn(churchId, planId), authData);
1611
+ if ((!planItems || planItems.length === 0) && planFolder.providerId && planFolder.providerPlanId) {
1612
+ const externalPlan = await fetchFromProviderProxy(
1613
+ "getPresentations",
1614
+ ministryId,
1615
+ planFolder.providerId,
1616
+ planFolder.providerPlanId,
1617
+ authData
1618
+ );
1619
+ if (externalPlan) {
1620
+ return { id: planId, name: planTitle, sections: externalPlan.sections, allFiles: externalPlan.allFiles };
1621
+ }
1622
+ }
1991
1623
  if (!planItems || !Array.isArray(planItems)) return null;
1992
1624
  const venueFeed = venueId ? await fetchVenueFeed(venueId) : null;
1993
1625
  const sections = [];
@@ -1995,54 +1627,195 @@ var B1ChurchProvider = class extends ContentProvider {
1995
1627
  for (const sectionItem of planItems) {
1996
1628
  const presentations = [];
1997
1629
  for (const child of sectionItem.children || []) {
1998
- const presentation = await planItemToPresentation(child, venueFeed);
1999
- if (presentation) {
2000
- presentations.push(presentation);
2001
- allFiles.push(...presentation.files);
1630
+ if (isExternalProviderItem(child) && child.providerId && child.providerPath) {
1631
+ const externalPlan = await fetchFromProviderProxy(
1632
+ "getPresentations",
1633
+ ministryId,
1634
+ child.providerId,
1635
+ child.providerPath,
1636
+ authData
1637
+ );
1638
+ if (externalPlan) {
1639
+ if (child.providerContentPath) {
1640
+ const externalInstructions = await fetchFromProviderProxy(
1641
+ "getInstructions",
1642
+ ministryId,
1643
+ child.providerId,
1644
+ child.providerPath,
1645
+ authData
1646
+ );
1647
+ const matchingPresentation = this.findPresentationByPath(externalPlan, externalInstructions, child.providerContentPath);
1648
+ if (matchingPresentation) {
1649
+ presentations.push(matchingPresentation);
1650
+ allFiles.push(...matchingPresentation.files);
1651
+ }
1652
+ } else {
1653
+ for (const section of externalPlan.sections) {
1654
+ presentations.push(...section.presentations);
1655
+ }
1656
+ allFiles.push(...externalPlan.allFiles);
1657
+ }
1658
+ }
1659
+ } else {
1660
+ const presentation = await planItemToPresentation(child, venueFeed);
1661
+ if (presentation) {
1662
+ presentations.push(presentation);
1663
+ allFiles.push(...presentation.files);
1664
+ }
2002
1665
  }
2003
1666
  }
2004
1667
  if (presentations.length > 0 || sectionItem.label) {
2005
- sections.push({
2006
- id: sectionItem.id,
2007
- name: sectionItem.label || "Section",
2008
- presentations
2009
- });
1668
+ sections.push({ id: sectionItem.id, name: sectionItem.label || "Section", presentations });
2010
1669
  }
2011
1670
  }
2012
- return { id: planId, name: folder.title, sections, allFiles };
2013
- }
2014
- async getInstructions(folder, authData) {
2015
- const level = folder.providerData?.level;
2016
- if (level !== "plan") return null;
2017
- const planId = folder.providerData?.planId;
2018
- const churchId = folder.providerData?.churchId;
2019
- if (!planId || !churchId) return null;
1671
+ return { id: planId, name: planTitle, sections, allFiles };
1672
+ }
1673
+ async getInstructions(path, authData) {
1674
+ const { segments, depth } = parsePath(path);
1675
+ if (depth < 4 || segments[0] !== "ministries") return null;
1676
+ const ministryId = segments[1];
1677
+ const planId = segments[3];
1678
+ const planTypeId = segments[2];
1679
+ const plans = await fetchPlans(planTypeId, authData);
1680
+ const planFolder = plans.find((p) => p.id === planId);
1681
+ if (!planFolder) return null;
1682
+ const churchId = planFolder.churchId;
1683
+ const planTitle = planFolder.name || "Plan";
1684
+ if (!churchId) return null;
2020
1685
  const pathFn = this.config.endpoints.planItems;
2021
1686
  const planItems = await this.apiRequest(pathFn(churchId, planId), authData);
1687
+ if ((!planItems || planItems.length === 0) && planFolder.providerId && planFolder.providerPlanId) {
1688
+ const externalInstructions = await fetchFromProviderProxy(
1689
+ "getInstructions",
1690
+ ministryId,
1691
+ planFolder.providerId,
1692
+ planFolder.providerPlanId,
1693
+ authData
1694
+ );
1695
+ if (externalInstructions) {
1696
+ return { venueName: planTitle, items: externalInstructions.items };
1697
+ }
1698
+ }
2022
1699
  if (!planItems || !Array.isArray(planItems)) return null;
2023
- return {
2024
- venueName: folder.title,
2025
- items: planItems.map(planItemToInstruction)
2026
- };
1700
+ const processedItems = await this.processInstructionItems(planItems, ministryId, authData);
1701
+ return { venueName: planTitle, items: processedItems };
1702
+ }
1703
+ async processInstructionItems(items, ministryId, authData) {
1704
+ const result = [];
1705
+ for (const item of items) {
1706
+ const instructionItem = planItemToInstruction(item);
1707
+ if (isExternalProviderItem(item) && item.providerId && item.providerPath) {
1708
+ const externalInstructions = await fetchFromProviderProxy(
1709
+ "getInstructions",
1710
+ ministryId,
1711
+ item.providerId,
1712
+ item.providerPath,
1713
+ authData
1714
+ );
1715
+ if (externalInstructions) {
1716
+ if (item.providerContentPath) {
1717
+ const matchingItem = this.findItemByPath(externalInstructions, item.providerContentPath);
1718
+ if (matchingItem?.children) {
1719
+ instructionItem.children = matchingItem.children;
1720
+ }
1721
+ } else {
1722
+ instructionItem.children = externalInstructions.items;
1723
+ }
1724
+ }
1725
+ } else if (item.children && item.children.length > 0) {
1726
+ instructionItem.children = await this.processInstructionItems(item.children, ministryId, authData);
1727
+ }
1728
+ result.push(instructionItem);
1729
+ }
1730
+ return result;
2027
1731
  }
2028
- async getPlaylist(folder, authData) {
2029
- const level = folder.providerData?.level;
2030
- if (level !== "plan") return [];
2031
- const planId = folder.providerData?.planId;
2032
- const churchId = folder.providerData?.churchId;
2033
- const venueId = folder.providerData?.contentId;
2034
- if (!planId || !churchId) return [];
1732
+ findItemByPath(instructions, path) {
1733
+ if (!path || !instructions) return null;
1734
+ return navigateToPath(instructions, path);
1735
+ }
1736
+ findPresentationByPath(plan, instructions, path) {
1737
+ if (!path || !instructions) return null;
1738
+ const item = navigateToPath(instructions, path);
1739
+ if (!item?.relatedId && !item?.id) return null;
1740
+ const presentationId = item.relatedId || item.id;
1741
+ for (const section of plan.sections) {
1742
+ for (const presentation of section.presentations) {
1743
+ if (presentation.id === presentationId) return presentation;
1744
+ }
1745
+ }
1746
+ return null;
1747
+ }
1748
+ async getPlaylist(path, authData, resolution) {
1749
+ const { segments, depth } = parsePath(path);
1750
+ if (depth < 4 || segments[0] !== "ministries") return [];
1751
+ const ministryId = segments[1];
1752
+ const planId = segments[3];
1753
+ const planTypeId = segments[2];
1754
+ const plans = await fetchPlans(planTypeId, authData);
1755
+ const planFolder = plans.find((p) => p.id === planId);
1756
+ if (!planFolder) return [];
1757
+ const churchId = planFolder.churchId;
1758
+ const venueId = planFolder.contentId;
1759
+ if (!churchId) return [];
2035
1760
  const pathFn = this.config.endpoints.planItems;
2036
1761
  const planItems = await this.apiRequest(pathFn(churchId, planId), authData);
1762
+ if ((!planItems || planItems.length === 0) && planFolder.providerId && planFolder.providerPlanId) {
1763
+ const externalFiles = await fetchFromProviderProxy(
1764
+ "getPlaylist",
1765
+ ministryId,
1766
+ planFolder.providerId,
1767
+ planFolder.providerPlanId,
1768
+ authData,
1769
+ resolution
1770
+ );
1771
+ return externalFiles || [];
1772
+ }
2037
1773
  if (!planItems || !Array.isArray(planItems)) return [];
2038
1774
  const venueFeed = venueId ? await fetchVenueFeed(venueId) : null;
2039
1775
  const files = [];
2040
1776
  for (const sectionItem of planItems) {
2041
1777
  for (const child of sectionItem.children || []) {
2042
- const itemType = child.itemType;
2043
- if ((itemType === "lessonSection" || itemType === "lessonAction" || itemType === "lessonAddOn") && venueFeed) {
2044
- const itemFiles = getFilesFromVenueFeed(venueFeed, itemType, child.relatedId);
2045
- files.push(...itemFiles);
1778
+ if (isExternalProviderItem(child) && child.providerId && child.providerPath) {
1779
+ if (child.providerContentPath) {
1780
+ const externalPlan = await fetchFromProviderProxy(
1781
+ "getPresentations",
1782
+ ministryId,
1783
+ child.providerId,
1784
+ child.providerPath,
1785
+ authData
1786
+ );
1787
+ const externalInstructions = await fetchFromProviderProxy(
1788
+ "getInstructions",
1789
+ ministryId,
1790
+ child.providerId,
1791
+ child.providerPath,
1792
+ authData
1793
+ );
1794
+ if (externalPlan) {
1795
+ const matchingPresentation = this.findPresentationByPath(externalPlan, externalInstructions, child.providerContentPath);
1796
+ if (matchingPresentation) {
1797
+ files.push(...matchingPresentation.files);
1798
+ }
1799
+ }
1800
+ } else {
1801
+ const externalFiles = await fetchFromProviderProxy(
1802
+ "getPlaylist",
1803
+ ministryId,
1804
+ child.providerId,
1805
+ child.providerPath,
1806
+ authData,
1807
+ resolution
1808
+ );
1809
+ if (externalFiles) {
1810
+ files.push(...externalFiles);
1811
+ }
1812
+ }
1813
+ } else {
1814
+ const itemType = child.itemType;
1815
+ if ((itemType === "lessonSection" || itemType === "section" || itemType === "lessonAction" || itemType === "action" || itemType === "lessonAddOn" || itemType === "addon") && venueFeed) {
1816
+ const itemFiles = getFilesFromVenueFeed(venueFeed, itemType, child.relatedId);
1817
+ files.push(...itemFiles);
1818
+ }
2046
1819
  }
2047
1820
  }
2048
1821
  }
@@ -2050,81 +1823,62 @@ var B1ChurchProvider = class extends ContentProvider {
2050
1823
  }
2051
1824
  };
2052
1825
 
2053
- // src/providers/PlanningCenterProvider.ts
2054
- var PlanningCenterProvider = class extends ContentProvider {
1826
+ // src/providers/planningCenter/PlanningCenterProvider.ts
1827
+ var PlanningCenterProvider = class {
2055
1828
  constructor() {
2056
- super(...arguments);
1829
+ this.apiHelper = new ApiHelper();
2057
1830
  this.id = "planningcenter";
2058
1831
  this.name = "Planning Center";
2059
- this.logos = {
2060
- light: "https://www.planningcenter.com/icons/icon-512x512.png",
2061
- dark: "https://www.planningcenter.com/icons/icon-512x512.png"
2062
- };
2063
- // Planning Center uses OAuth 2.0 with PKCE (handled by base ContentProvider class)
2064
- this.config = {
2065
- id: "planningcenter",
2066
- name: "Planning Center",
2067
- apiBase: "https://api.planningcenteronline.com",
2068
- oauthBase: "https://api.planningcenteronline.com/oauth",
2069
- clientId: "",
2070
- // Consumer must provide client_id
2071
- scopes: ["services"],
2072
- endpoints: {
2073
- serviceTypes: "/services/v2/service_types",
2074
- plans: (serviceTypeId) => `/services/v2/service_types/${serviceTypeId}/plans`,
2075
- planItems: (serviceTypeId, planId) => `/services/v2/service_types/${serviceTypeId}/plans/${planId}/items`,
2076
- song: (itemId) => `/services/v2/songs/${itemId}`,
2077
- arrangement: (songId, arrangementId) => `/services/v2/songs/${songId}/arrangements/${arrangementId}`,
2078
- arrangementSections: (songId, arrangementId) => `/services/v2/songs/${songId}/arrangements/${arrangementId}/sections`,
2079
- media: (mediaId) => `/services/v2/media/${mediaId}`,
2080
- mediaAttachments: (mediaId) => `/services/v2/media/${mediaId}/attachments`
2081
- }
2082
- };
1832
+ this.logos = { light: "https://www.planningcenter.com/icons/icon-512x512.png", dark: "https://www.planningcenter.com/icons/icon-512x512.png" };
1833
+ this.config = { id: "planningcenter", name: "Planning Center", apiBase: "https://api.planningcenteronline.com", oauthBase: "https://api.planningcenteronline.com/oauth", clientId: "", scopes: ["services"], endpoints: { serviceTypes: "/services/v2/service_types", plans: (serviceTypeId) => `/services/v2/service_types/${serviceTypeId}/plans`, planItems: (serviceTypeId, planId) => `/services/v2/service_types/${serviceTypeId}/plans/${planId}/items`, song: (itemId) => `/services/v2/songs/${itemId}`, arrangement: (songId, arrangementId) => `/services/v2/songs/${songId}/arrangements/${arrangementId}`, arrangementSections: (songId, arrangementId) => `/services/v2/songs/${songId}/arrangements/${arrangementId}/sections`, media: (mediaId) => `/services/v2/media/${mediaId}`, mediaAttachments: (mediaId) => `/services/v2/media/${mediaId}/attachments` } };
2083
1834
  this.ONE_WEEK_MS = 6048e5;
1835
+ this.requiresAuth = true;
1836
+ this.authTypes = ["oauth_pkce"];
1837
+ this.capabilities = { browse: true, presentations: true, playlist: true, instructions: true, mediaLicensing: false };
2084
1838
  }
2085
- requiresAuth() {
2086
- return true;
2087
- }
2088
- getCapabilities() {
2089
- return {
2090
- browse: true,
2091
- presentations: true,
2092
- playlist: false,
2093
- instructions: false,
2094
- expandedInstructions: false,
2095
- mediaLicensing: false
2096
- };
1839
+ async apiRequest(path, auth) {
1840
+ return this.apiHelper.apiRequest(this.config, this.id, path, auth);
2097
1841
  }
2098
- async browse(folder, auth) {
2099
- if (!folder) {
2100
- const response = await this.apiRequest(
2101
- this.config.endpoints.serviceTypes,
2102
- auth
2103
- );
2104
- if (!response?.data) return [];
2105
- return response.data.map((serviceType) => ({
1842
+ async browse(path, auth) {
1843
+ const { segments, depth } = parsePath(path);
1844
+ if (depth === 0) {
1845
+ return [{
2106
1846
  type: "folder",
2107
- id: serviceType.id,
2108
- title: serviceType.attributes.name,
2109
- providerData: {
2110
- level: "serviceType",
2111
- serviceTypeId: serviceType.id
2112
- }
2113
- }));
1847
+ id: "serviceTypes-root",
1848
+ title: "Service Types",
1849
+ path: "/serviceTypes"
1850
+ }];
2114
1851
  }
2115
- const level = folder.providerData?.level;
2116
- switch (level) {
2117
- case "serviceType":
2118
- return this.getPlans(folder, auth);
2119
- case "plan":
2120
- return this.getPlanItems(folder, auth);
2121
- default:
2122
- return [];
1852
+ const root = segments[0];
1853
+ if (root !== "serviceTypes") return [];
1854
+ if (depth === 1) {
1855
+ return this.getServiceTypes(auth);
1856
+ }
1857
+ if (depth === 2) {
1858
+ const serviceTypeId = segments[1];
1859
+ return this.getPlans(serviceTypeId, path, auth);
2123
1860
  }
1861
+ if (depth === 3) {
1862
+ const serviceTypeId = segments[1];
1863
+ const planId = segments[2];
1864
+ return this.getPlanItems(serviceTypeId, planId, auth);
1865
+ }
1866
+ return [];
1867
+ }
1868
+ async getServiceTypes(auth) {
1869
+ const response = await this.apiRequest(
1870
+ this.config.endpoints.serviceTypes,
1871
+ auth
1872
+ );
1873
+ if (!response?.data) return [];
1874
+ return response.data.map((serviceType) => ({
1875
+ type: "folder",
1876
+ id: serviceType.id,
1877
+ title: serviceType.attributes.name,
1878
+ path: `/serviceTypes/${serviceType.id}`
1879
+ }));
2124
1880
  }
2125
- async getPlans(folder, auth) {
2126
- const serviceTypeId = folder.providerData?.serviceTypeId;
2127
- if (!serviceTypeId) return [];
1881
+ async getPlans(serviceTypeId, currentPath, auth) {
2128
1882
  const pathFn = this.config.endpoints.plans;
2129
1883
  const response = await this.apiRequest(
2130
1884
  `${pathFn(serviceTypeId)}?filter=future&order=sort_date`,
@@ -2141,73 +1895,45 @@ var PlanningCenterProvider = class extends ContentProvider {
2141
1895
  type: "folder",
2142
1896
  id: plan.id,
2143
1897
  title: plan.attributes.title || this.formatDate(plan.attributes.sort_date),
2144
- providerData: {
2145
- level: "plan",
2146
- serviceTypeId,
2147
- planId: plan.id,
2148
- sortDate: plan.attributes.sort_date
2149
- }
1898
+ isLeaf: true,
1899
+ path: `${currentPath}/${plan.id}`
2150
1900
  }));
2151
1901
  }
2152
- async getPlanItems(folder, auth) {
2153
- const serviceTypeId = folder.providerData?.serviceTypeId;
2154
- const planId = folder.providerData?.planId;
2155
- if (!serviceTypeId || !planId) return [];
1902
+ async getPlanItems(serviceTypeId, planId, auth) {
2156
1903
  const pathFn = this.config.endpoints.planItems;
2157
1904
  const response = await this.apiRequest(
2158
1905
  `${pathFn(serviceTypeId, planId)}?per_page=100`,
2159
1906
  auth
2160
1907
  );
2161
1908
  if (!response?.data) return [];
2162
- return response.data.map((item) => ({
2163
- type: "file",
2164
- id: item.id,
2165
- title: item.attributes.title || "",
2166
- mediaType: "image",
2167
- url: "",
2168
- providerData: {
2169
- itemType: item.attributes.item_type,
2170
- description: item.attributes.description,
2171
- length: item.attributes.length,
2172
- songId: item.relationships?.song?.data?.id,
2173
- arrangementId: item.relationships?.arrangement?.data?.id
2174
- }
2175
- }));
1909
+ return response.data.map((item) => ({ type: "file", id: item.id, title: item.attributes.title || "", mediaType: "image", url: "" }));
2176
1910
  }
2177
- async getPresentations(folder, auth) {
2178
- const level = folder.providerData?.level;
2179
- if (level !== "plan") return null;
2180
- const serviceTypeId = folder.providerData?.serviceTypeId;
2181
- const planId = folder.providerData?.planId;
2182
- if (!serviceTypeId || !planId) return null;
1911
+ async getPresentations(path, auth) {
1912
+ const { segments, depth } = parsePath(path);
1913
+ if (depth < 3 || segments[0] !== "serviceTypes") return null;
1914
+ const serviceTypeId = segments[1];
1915
+ const planId = segments[2];
2183
1916
  const pathFn = this.config.endpoints.planItems;
2184
1917
  const response = await this.apiRequest(
2185
1918
  `${pathFn(serviceTypeId, planId)}?per_page=100`,
2186
1919
  auth
2187
1920
  );
2188
1921
  if (!response?.data) return null;
1922
+ const plans = await this.getPlans(serviceTypeId, `/serviceTypes/${serviceTypeId}`, auth);
1923
+ const plan = plans.find((p) => p.id === planId);
1924
+ const planTitle = plan?.title || "Plan";
2189
1925
  const sections = [];
2190
1926
  const allFiles = [];
2191
1927
  let currentSection = null;
2192
1928
  for (const item of response.data) {
2193
1929
  const itemType = item.attributes.item_type;
2194
1930
  if (itemType === "header") {
2195
- if (currentSection && currentSection.presentations.length > 0) {
2196
- sections.push(currentSection);
2197
- }
2198
- currentSection = {
2199
- id: item.id,
2200
- name: item.attributes.title || "Section",
2201
- presentations: []
2202
- };
1931
+ if (currentSection && currentSection.presentations.length > 0) sections.push(currentSection);
1932
+ currentSection = { id: item.id, name: item.attributes.title || "Section", presentations: [] };
2203
1933
  continue;
2204
1934
  }
2205
1935
  if (!currentSection) {
2206
- currentSection = {
2207
- id: `default-${planId}`,
2208
- name: "Service",
2209
- presentations: []
2210
- };
1936
+ currentSection = { id: `default-${planId}`, name: "Service", presentations: [] };
2211
1937
  }
2212
1938
  const presentation = await this.convertToPresentation(item, auth);
2213
1939
  if (presentation) {
@@ -2218,12 +1944,7 @@ var PlanningCenterProvider = class extends ContentProvider {
2218
1944
  if (currentSection && currentSection.presentations.length > 0) {
2219
1945
  sections.push(currentSection);
2220
1946
  }
2221
- return {
2222
- id: planId,
2223
- name: folder.title,
2224
- sections,
2225
- allFiles
2226
- };
1947
+ return { id: planId, name: planTitle, sections, allFiles };
2227
1948
  }
2228
1949
  async convertToPresentation(item, auth) {
2229
1950
  const itemType = item.attributes.item_type;
@@ -2234,17 +1955,7 @@ var PlanningCenterProvider = class extends ContentProvider {
2234
1955
  return this.convertMediaToPresentation(item, auth);
2235
1956
  }
2236
1957
  if (itemType === "item") {
2237
- return {
2238
- id: item.id,
2239
- name: item.attributes.title || "",
2240
- actionType: "other",
2241
- files: [],
2242
- providerData: {
2243
- itemType: "item",
2244
- description: item.attributes.description,
2245
- length: item.attributes.length
2246
- }
2247
- };
1958
+ return { id: item.id, name: item.attributes.title || "", actionType: "other", files: [], providerData: { itemType: "item", description: item.attributes.description, length: item.attributes.length } };
2248
1959
  }
2249
1960
  return null;
2250
1961
  }
@@ -2252,13 +1963,7 @@ var PlanningCenterProvider = class extends ContentProvider {
2252
1963
  const songId = item.relationships?.song?.data?.id;
2253
1964
  const arrangementId = item.relationships?.arrangement?.data?.id;
2254
1965
  if (!songId) {
2255
- return {
2256
- id: item.id,
2257
- name: item.attributes.title || "Song",
2258
- actionType: "other",
2259
- files: [],
2260
- providerData: { itemType: "song" }
2261
- };
1966
+ return { id: item.id, name: item.attributes.title || "Song", actionType: "other", files: [], providerData: { itemType: "song" } };
2262
1967
  }
2263
1968
  const songFn = this.config.endpoints.song;
2264
1969
  const songResponse = await this.apiRequest(songFn(songId), auth);
@@ -2280,25 +1985,7 @@ var PlanningCenterProvider = class extends ContentProvider {
2280
1985
  }
2281
1986
  const song = songResponse?.data;
2282
1987
  const title = song?.attributes?.title || item.attributes.title || "Song";
2283
- return {
2284
- id: item.id,
2285
- name: title,
2286
- actionType: "other",
2287
- files: [],
2288
- providerData: {
2289
- itemType: "song",
2290
- title,
2291
- author: song?.attributes?.author,
2292
- copyright: song?.attributes?.copyright,
2293
- ccliNumber: song?.attributes?.ccli_number,
2294
- arrangementName: arrangement?.attributes?.name,
2295
- keySignature: arrangement?.attributes?.chord_chart_key,
2296
- bpm: arrangement?.attributes?.bpm,
2297
- sequence: arrangement?.attributes?.sequence,
2298
- sections: sections.map((s) => ({ label: s.label, lyrics: s.lyrics })),
2299
- length: item.attributes.length
2300
- }
2301
- };
1988
+ return { id: item.id, name: title, actionType: "other", files: [], providerData: { itemType: "song", title, author: song?.attributes?.author, copyright: song?.attributes?.copyright, ccliNumber: song?.attributes?.ccli_number, arrangementName: arrangement?.attributes?.name, keySignature: arrangement?.attributes?.chord_chart_key, bpm: arrangement?.attributes?.bpm, sequence: arrangement?.attributes?.sequence, sections: sections.map((s) => ({ label: s.label, lyrics: s.lyrics })), length: item.attributes.length } };
2302
1989
  }
2303
1990
  async convertMediaToPresentation(item, auth) {
2304
1991
  const files = [];
@@ -2318,33 +2005,55 @@ var PlanningCenterProvider = class extends ContentProvider {
2318
2005
  if (!url) continue;
2319
2006
  const contentType = attachment.attributes.content_type;
2320
2007
  const explicitType = contentType?.startsWith("video/") ? "video" : void 0;
2321
- files.push({
2322
- type: "file",
2323
- id: attachment.id,
2324
- title: attachment.attributes.filename,
2325
- mediaType: detectMediaType(url, explicitType),
2326
- url
2327
- });
2008
+ files.push({ type: "file", id: attachment.id, title: attachment.attributes.filename, mediaType: detectMediaType(url, explicitType), url });
2328
2009
  }
2329
2010
  }
2330
- return {
2331
- id: item.id,
2332
- name: item.attributes.title || "Media",
2333
- actionType: "play",
2334
- files,
2335
- providerData: {
2336
- itemType: "media",
2337
- length: item.attributes.length
2338
- }
2339
- };
2011
+ return { id: item.id, name: item.attributes.title || "Media", actionType: "play", files, providerData: { itemType: "media", length: item.attributes.length } };
2340
2012
  }
2341
2013
  formatDate(dateString) {
2342
2014
  const date = new Date(dateString);
2343
2015
  return date.toISOString().slice(0, 10);
2344
2016
  }
2017
+ async getPlaylist(path, auth, _resolution) {
2018
+ const plan = await this.getPresentations(path, auth);
2019
+ if (!plan) return null;
2020
+ return plan.allFiles.length > 0 ? plan.allFiles : null;
2021
+ }
2022
+ async getInstructions(path, auth) {
2023
+ const plan = await this.getPresentations(path, auth);
2024
+ if (!plan) return null;
2025
+ const sectionItems = plan.sections.map((section) => {
2026
+ const actionItems = section.presentations.map((pres) => {
2027
+ const fileItems = pres.files.map((file) => ({
2028
+ id: file.id,
2029
+ itemType: "file",
2030
+ label: file.title,
2031
+ embedUrl: file.url
2032
+ }));
2033
+ return {
2034
+ id: pres.id,
2035
+ itemType: "action",
2036
+ relatedId: pres.id,
2037
+ label: pres.name,
2038
+ description: pres.actionType,
2039
+ children: fileItems.length > 0 ? fileItems : void 0
2040
+ };
2041
+ });
2042
+ return {
2043
+ id: section.id,
2044
+ itemType: "section",
2045
+ label: section.name,
2046
+ children: actionItems
2047
+ };
2048
+ });
2049
+ return {
2050
+ venueName: plan.name,
2051
+ items: sectionItems
2052
+ };
2053
+ }
2345
2054
  };
2346
2055
 
2347
- // src/providers/bibleproject/data.json
2056
+ // src/providers/bibleProject/data.json
2348
2057
  var data_default = {
2349
2058
  collections: [
2350
2059
  {
@@ -4452,10 +4161,9 @@ var data_default = {
4452
4161
  ]
4453
4162
  };
4454
4163
 
4455
- // src/providers/bibleproject/BibleProjectProvider.ts
4456
- var BibleProjectProvider = class extends ContentProvider {
4164
+ // src/providers/bibleProject/BibleProjectProvider.ts
4165
+ var BibleProjectProvider = class {
4457
4166
  constructor() {
4458
- super(...arguments);
4459
4167
  this.id = "bibleproject";
4460
4168
  this.name = "The Bible Project";
4461
4169
  this.logos = {
@@ -4474,174 +4182,151 @@ var BibleProjectProvider = class extends ContentProvider {
4474
4182
  }
4475
4183
  };
4476
4184
  this.data = data_default;
4477
- }
4478
- requiresAuth() {
4479
- return false;
4480
- }
4481
- getCapabilities() {
4482
- return {
4185
+ this.requiresAuth = false;
4186
+ this.authTypes = ["none"];
4187
+ this.capabilities = {
4483
4188
  browse: true,
4484
4189
  presentations: true,
4485
- // Has collections with videos
4486
4190
  playlist: true,
4487
- // Can return flat list of videos
4488
- instructions: false,
4489
- expandedInstructions: false,
4191
+ instructions: true,
4490
4192
  mediaLicensing: false
4491
4193
  };
4492
4194
  }
4493
- async browse(folder, _auth) {
4494
- if (!folder) {
4495
- return this.data.collections.filter((collection) => collection.videos.length > 0).map((collection) => this.createFolder(
4496
- this.slugify(collection.name),
4497
- collection.name,
4498
- collection.image || void 0,
4499
- { level: "collection", collectionName: collection.name }
4500
- ));
4195
+ async browse(path, _auth) {
4196
+ const { segments, depth } = parsePath(path);
4197
+ if (depth === 0) {
4198
+ return this.getCollections();
4501
4199
  }
4502
- const level = folder.providerData?.level;
4503
- const collectionName = folder.providerData?.collectionName;
4504
- if (level === "collection") {
4505
- return this.getLessonFolders(collectionName);
4200
+ if (depth === 1) {
4201
+ const collectionSlug = segments[0];
4202
+ return this.getLessonFolders(collectionSlug, path);
4506
4203
  }
4507
- if (level === "lesson") {
4508
- const videoData = folder.providerData?.videoData;
4509
- if (videoData) {
4510
- return [this.createFile(
4511
- videoData.id,
4512
- videoData.title,
4513
- videoData.videoUrl,
4514
- {
4515
- mediaType: "video",
4516
- muxPlaybackId: videoData.muxPlaybackId
4517
- }
4518
- )];
4519
- }
4520
- return [];
4204
+ if (depth === 2) {
4205
+ const collectionSlug = segments[0];
4206
+ const videoId = segments[1];
4207
+ return this.getVideoFile(collectionSlug, videoId);
4521
4208
  }
4522
4209
  return [];
4523
4210
  }
4524
- async getPresentations(folder, _auth) {
4525
- const level = folder.providerData?.level;
4526
- if (level === "collection") {
4527
- const collectionName = folder.providerData?.collectionName;
4528
- const collection = this.data.collections.find((c) => c.name === collectionName);
4529
- if (!collection) return null;
4211
+ getCollections() {
4212
+ return this.data.collections.filter((collection) => collection.videos.length > 0).map((collection) => ({
4213
+ type: "folder",
4214
+ id: this.slugify(collection.name),
4215
+ title: collection.name,
4216
+ image: collection.image || void 0,
4217
+ path: `/${this.slugify(collection.name)}`
4218
+ }));
4219
+ }
4220
+ getLessonFolders(collectionSlug, currentPath) {
4221
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
4222
+ if (!collection) return [];
4223
+ return collection.videos.map((video) => ({
4224
+ type: "folder",
4225
+ id: video.id,
4226
+ title: video.title,
4227
+ image: video.thumbnailUrl,
4228
+ isLeaf: true,
4229
+ path: `${currentPath}/${video.id}`
4230
+ }));
4231
+ }
4232
+ getVideoFile(collectionSlug, videoId) {
4233
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
4234
+ if (!collection) return [];
4235
+ const video = collection.videos.find((v) => v.id === videoId);
4236
+ if (!video) return [];
4237
+ return [createFile(video.id, video.title, video.videoUrl, { mediaType: "video", muxPlaybackId: video.muxPlaybackId })];
4238
+ }
4239
+ async getPresentations(path, _auth) {
4240
+ const { segments, depth } = parsePath(path);
4241
+ if (depth < 1) return null;
4242
+ const collectionSlug = segments[0];
4243
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
4244
+ if (!collection) return null;
4245
+ if (depth === 1) {
4530
4246
  const allFiles = [];
4531
4247
  const presentations = collection.videos.map((video) => {
4532
- const file = {
4533
- type: "file",
4534
- id: video.id,
4535
- title: video.title,
4536
- mediaType: "video",
4537
- url: video.videoUrl,
4538
- image: video.thumbnailUrl,
4539
- muxPlaybackId: video.muxPlaybackId
4540
- };
4248
+ const file = { type: "file", id: video.id, title: video.title, mediaType: "video", url: video.videoUrl, image: video.thumbnailUrl, muxPlaybackId: video.muxPlaybackId };
4541
4249
  allFiles.push(file);
4542
- return {
4543
- id: video.id,
4544
- name: video.title,
4545
- actionType: "play",
4546
- files: [file]
4547
- };
4250
+ return { id: video.id, name: video.title, actionType: "play", files: [file] };
4548
4251
  });
4549
- return {
4550
- id: this.slugify(collection.name),
4551
- name: collection.name,
4552
- image: collection.image || void 0,
4553
- sections: [{
4554
- id: "videos",
4555
- name: "Videos",
4556
- presentations
4557
- }],
4558
- allFiles
4559
- };
4252
+ return { id: this.slugify(collection.name), name: collection.name, image: collection.image || void 0, sections: [{ id: "videos", name: "Videos", presentations }], allFiles };
4560
4253
  }
4561
- if (level === "lesson") {
4562
- const videoData = folder.providerData?.videoData;
4563
- if (!videoData) return null;
4564
- const file = {
4565
- type: "file",
4566
- id: videoData.id,
4567
- title: videoData.title,
4568
- mediaType: "video",
4569
- url: videoData.videoUrl,
4570
- image: videoData.thumbnailUrl,
4571
- muxPlaybackId: videoData.muxPlaybackId
4572
- };
4573
- return {
4574
- id: videoData.id,
4575
- name: videoData.title,
4576
- image: videoData.thumbnailUrl,
4577
- sections: [{
4578
- id: "main",
4579
- name: "Content",
4580
- presentations: [{
4581
- id: videoData.id,
4582
- name: videoData.title,
4583
- actionType: "play",
4584
- files: [file]
4585
- }]
4586
- }],
4587
- allFiles: [file]
4588
- };
4254
+ if (depth === 2) {
4255
+ const videoId = segments[1];
4256
+ const video = collection.videos.find((v) => v.id === videoId);
4257
+ if (!video) return null;
4258
+ const file = { type: "file", id: video.id, title: video.title, mediaType: "video", url: video.videoUrl, image: video.thumbnailUrl, muxPlaybackId: video.muxPlaybackId };
4259
+ return { id: video.id, name: video.title, image: video.thumbnailUrl, sections: [{ id: "main", name: "Content", presentations: [{ id: video.id, name: video.title, actionType: "play", files: [file] }] }], allFiles: [file] };
4260
+ }
4261
+ return null;
4262
+ }
4263
+ async getPlaylist(path, _auth, _resolution) {
4264
+ const { segments, depth } = parsePath(path);
4265
+ if (depth < 1) return null;
4266
+ const collectionSlug = segments[0];
4267
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
4268
+ if (!collection) return null;
4269
+ if (depth === 1) {
4270
+ return collection.videos.map((video) => ({ type: "file", id: video.id, title: video.title, mediaType: "video", url: video.videoUrl, image: video.thumbnailUrl, muxPlaybackId: video.muxPlaybackId }));
4271
+ }
4272
+ if (depth === 2) {
4273
+ const videoId = segments[1];
4274
+ const video = collection.videos.find((v) => v.id === videoId);
4275
+ if (!video) return null;
4276
+ return [{ type: "file", id: video.id, title: video.title, mediaType: "video", url: video.videoUrl, image: video.thumbnailUrl, muxPlaybackId: video.muxPlaybackId }];
4589
4277
  }
4590
4278
  return null;
4591
4279
  }
4592
- async getPlaylist(folder, _auth, _resolution) {
4593
- const level = folder.providerData?.level;
4594
- if (level === "collection") {
4595
- const collectionName = folder.providerData?.collectionName;
4596
- const collection = this.data.collections.find((c) => c.name === collectionName);
4597
- if (!collection) return null;
4598
- return collection.videos.map((video) => ({
4599
- type: "file",
4280
+ async getInstructions(path, _auth) {
4281
+ const { segments, depth } = parsePath(path);
4282
+ if (depth < 1) return null;
4283
+ const collectionSlug = segments[0];
4284
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
4285
+ if (!collection) return null;
4286
+ if (depth === 1) {
4287
+ const fileItems = collection.videos.map((video) => ({
4600
4288
  id: video.id,
4601
- title: video.title,
4602
- mediaType: "video",
4603
- url: video.videoUrl,
4604
- image: video.thumbnailUrl,
4605
- muxPlaybackId: video.muxPlaybackId
4289
+ itemType: "file",
4290
+ label: video.title,
4291
+ embedUrl: video.videoUrl
4606
4292
  }));
4293
+ return {
4294
+ venueName: collection.name,
4295
+ items: [{
4296
+ id: this.slugify(collection.name),
4297
+ itemType: "section",
4298
+ label: "Videos",
4299
+ children: fileItems
4300
+ }]
4301
+ };
4607
4302
  }
4608
- if (level === "lesson") {
4609
- const videoData = folder.providerData?.videoData;
4610
- if (!videoData) return null;
4611
- return [{
4612
- type: "file",
4613
- id: videoData.id,
4614
- title: videoData.title,
4615
- mediaType: "video",
4616
- url: videoData.videoUrl,
4617
- image: videoData.thumbnailUrl,
4618
- muxPlaybackId: videoData.muxPlaybackId
4619
- }];
4303
+ if (depth === 2) {
4304
+ const videoId = segments[1];
4305
+ const video = collection.videos.find((v) => v.id === videoId);
4306
+ if (!video) return null;
4307
+ return {
4308
+ venueName: video.title,
4309
+ items: [{
4310
+ id: "main",
4311
+ itemType: "section",
4312
+ label: "Content",
4313
+ children: [{
4314
+ id: video.id,
4315
+ itemType: "file",
4316
+ label: video.title,
4317
+ embedUrl: video.videoUrl
4318
+ }]
4319
+ }]
4320
+ };
4620
4321
  }
4621
4322
  return null;
4622
4323
  }
4623
- getLessonFolders(collectionName) {
4624
- const collection = this.data.collections.find((c) => c.name === collectionName);
4625
- if (!collection) return [];
4626
- return collection.videos.map((video) => this.createFolder(
4627
- video.id,
4628
- video.title,
4629
- video.thumbnailUrl,
4630
- {
4631
- level: "lesson",
4632
- collectionName,
4633
- videoData: video,
4634
- isLeaf: true
4635
- // Mark as leaf so venue choice modal appears
4636
- }
4637
- ));
4638
- }
4639
4324
  slugify(text) {
4640
4325
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4641
4326
  }
4642
4327
  };
4643
4328
 
4644
- // src/providers/highvoltage/data.json
4329
+ // src/providers/highVoltage/data.json
4645
4330
  var data_default2 = {
4646
4331
  collections: [
4647
4332
  {
@@ -12145,10 +11830,9 @@ var data_default2 = {
12145
11830
  ]
12146
11831
  };
12147
11832
 
12148
- // src/providers/HighVoltageKidsProvider.ts
12149
- var HighVoltageKidsProvider = class extends ContentProvider {
11833
+ // src/providers/highVoltage/HighVoltageKidsProvider.ts
11834
+ var HighVoltageKidsProvider = class {
12150
11835
  constructor() {
12151
- super(...arguments);
12152
11836
  this.id = "highvoltagekids";
12153
11837
  this.name = "High Voltage Kids";
12154
11838
  this.logos = {
@@ -12167,195 +11851,215 @@ var HighVoltageKidsProvider = class extends ContentProvider {
12167
11851
  }
12168
11852
  };
12169
11853
  this.data = data_default2;
12170
- }
12171
- requiresAuth() {
12172
- return false;
12173
- }
12174
- getCapabilities() {
12175
- return {
11854
+ this.requiresAuth = false;
11855
+ this.authTypes = ["none"];
11856
+ this.capabilities = {
12176
11857
  browse: true,
12177
11858
  presentations: true,
12178
- // Has hierarchical structure: study -> lessons -> files
12179
11859
  playlist: true,
12180
- // Can return flat list of files for a lesson
12181
- instructions: false,
12182
- expandedInstructions: false,
11860
+ instructions: true,
12183
11861
  mediaLicensing: false
12184
11862
  };
12185
11863
  }
12186
- async browse(folder, _auth) {
12187
- if (!folder) {
12188
- return this.data.collections.filter((collection) => collection.folders.length > 0).map((collection) => this.createFolder(
12189
- this.slugify(collection.name),
12190
- collection.name,
12191
- void 0,
12192
- { level: "collection", collectionName: collection.name }
12193
- ));
11864
+ async browse(path, _auth) {
11865
+ const { segments, depth } = parsePath(path);
11866
+ if (depth === 0) {
11867
+ return this.getCollections();
12194
11868
  }
12195
- const level = folder.providerData?.level;
12196
- const collectionName = folder.providerData?.collectionName;
12197
- if (level === "collection") {
12198
- return this.getStudyFolders(collectionName);
11869
+ if (depth === 1) {
11870
+ const collectionSlug = segments[0];
11871
+ return this.getStudyFolders(collectionSlug, path);
12199
11872
  }
12200
- if (level === "study") {
12201
- const studyData = folder.providerData?.studyData;
12202
- if (studyData) {
12203
- return this.getLessonFolders(studyData);
12204
- }
12205
- return [];
11873
+ if (depth === 2) {
11874
+ const collectionSlug = segments[0];
11875
+ const studyId = segments[1];
11876
+ return this.getLessonFolders(collectionSlug, studyId, path);
12206
11877
  }
12207
- if (level === "lesson") {
12208
- const lessonData = folder.providerData?.lessonData;
12209
- if (lessonData?.files) {
12210
- return lessonData.files.map((file) => this.createFile(
12211
- file.id,
12212
- file.title,
12213
- file.url,
12214
- { mediaType: file.mediaType }
12215
- ));
12216
- }
12217
- return [];
11878
+ if (depth === 3) {
11879
+ const collectionSlug = segments[0];
11880
+ const studyId = segments[1];
11881
+ const lessonId = segments[2];
11882
+ return this.getLessonFiles(collectionSlug, studyId, lessonId);
12218
11883
  }
12219
11884
  return [];
12220
11885
  }
12221
- async getPresentations(folder, _auth) {
12222
- const level = folder.providerData?.level;
12223
- if (level === "study") {
12224
- const studyData = folder.providerData?.studyData;
12225
- if (!studyData) return null;
11886
+ getCollections() {
11887
+ return this.data.collections.filter((collection) => collection.folders.length > 0).map((collection) => ({
11888
+ type: "folder",
11889
+ id: this.slugify(collection.name),
11890
+ title: collection.name,
11891
+ path: `/${this.slugify(collection.name)}`
11892
+ }));
11893
+ }
11894
+ getStudyFolders(collectionSlug, currentPath) {
11895
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
11896
+ if (!collection) return [];
11897
+ return collection.folders.map((study) => ({
11898
+ type: "folder",
11899
+ id: study.id,
11900
+ title: study.name,
11901
+ image: study.image || void 0,
11902
+ path: `${currentPath}/${study.id}`
11903
+ }));
11904
+ }
11905
+ getLessonFolders(collectionSlug, studyId, currentPath) {
11906
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
11907
+ if (!collection) return [];
11908
+ const study = collection.folders.find((s) => s.id === studyId);
11909
+ if (!study) return [];
11910
+ return study.lessons.map((lesson) => ({
11911
+ type: "folder",
11912
+ id: lesson.id,
11913
+ title: lesson.name,
11914
+ image: lesson.image || void 0,
11915
+ isLeaf: true,
11916
+ path: `${currentPath}/${lesson.id}`
11917
+ }));
11918
+ }
11919
+ getLessonFiles(collectionSlug, studyId, lessonId) {
11920
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
11921
+ if (!collection) return [];
11922
+ const study = collection.folders.find((s) => s.id === studyId);
11923
+ if (!study) return [];
11924
+ const lesson = study.lessons.find((l) => l.id === lessonId);
11925
+ if (!lesson?.files) return [];
11926
+ return lesson.files.map((file) => createFile(file.id, file.title, file.url, { mediaType: file.mediaType }));
11927
+ }
11928
+ async getPresentations(path, _auth) {
11929
+ const { segments, depth } = parsePath(path);
11930
+ if (depth < 2) return null;
11931
+ const collectionSlug = segments[0];
11932
+ const studyId = segments[1];
11933
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
11934
+ if (!collection) return null;
11935
+ const study = collection.folders.find((s) => s.id === studyId);
11936
+ if (!study) return null;
11937
+ if (depth === 2) {
12226
11938
  const allFiles = [];
12227
- const sections = studyData.lessons.map((lesson) => {
11939
+ const sections = study.lessons.map((lesson) => {
12228
11940
  const files = lesson.files.map((file) => {
12229
- const contentFile = {
12230
- type: "file",
12231
- id: file.id,
12232
- title: file.title,
12233
- mediaType: file.mediaType,
12234
- url: file.url,
12235
- image: lesson.image
12236
- };
11941
+ const contentFile = { type: "file", id: file.id, title: file.title, mediaType: file.mediaType, url: file.url, image: lesson.image };
12237
11942
  allFiles.push(contentFile);
12238
11943
  return contentFile;
12239
11944
  });
12240
- const presentation = {
12241
- id: lesson.id,
12242
- name: lesson.name,
12243
- actionType: "play",
12244
- files
12245
- };
12246
- return {
12247
- id: lesson.id,
12248
- name: lesson.name,
12249
- presentations: [presentation]
12250
- };
11945
+ const presentation = { id: lesson.id, name: lesson.name, actionType: "play", files };
11946
+ return { id: lesson.id, name: lesson.name, presentations: [presentation] };
12251
11947
  });
12252
- return {
12253
- id: studyData.id,
12254
- name: studyData.name,
12255
- description: studyData.description,
12256
- image: studyData.image,
12257
- sections,
12258
- allFiles
12259
- };
11948
+ return { id: study.id, name: study.name, description: study.description, image: study.image, sections, allFiles };
12260
11949
  }
12261
- if (level === "lesson") {
12262
- const lessonData = folder.providerData?.lessonData;
12263
- if (!lessonData?.files) return null;
12264
- const files = lessonData.files.map((file) => ({
12265
- type: "file",
12266
- id: file.id,
12267
- title: file.title,
12268
- mediaType: file.mediaType,
12269
- url: file.url,
12270
- image: lessonData.image
12271
- }));
12272
- const presentation = {
12273
- id: lessonData.id,
12274
- name: lessonData.name,
12275
- actionType: "play",
12276
- files
12277
- };
12278
- return {
12279
- id: lessonData.id,
12280
- name: lessonData.name,
12281
- image: lessonData.image,
12282
- sections: [{
12283
- id: "main",
12284
- name: "Content",
12285
- presentations: [presentation]
12286
- }],
12287
- allFiles: files
12288
- };
11950
+ if (depth === 3) {
11951
+ const lessonId = segments[2];
11952
+ const lesson = study.lessons.find((l) => l.id === lessonId);
11953
+ if (!lesson?.files) return null;
11954
+ const files = lesson.files.map((file) => ({ type: "file", id: file.id, title: file.title, mediaType: file.mediaType, url: file.url, image: lesson.image }));
11955
+ const presentation = { id: lesson.id, name: lesson.name, actionType: "play", files };
11956
+ return { id: lesson.id, name: lesson.name, image: lesson.image, sections: [{ id: "main", name: "Content", presentations: [presentation] }], allFiles: files };
12289
11957
  }
12290
11958
  return null;
12291
11959
  }
12292
- async getPlaylist(folder, _auth, _resolution) {
12293
- const level = folder.providerData?.level;
12294
- if (level === "lesson") {
12295
- const lessonData = folder.providerData?.lessonData;
12296
- if (!lessonData?.files) return null;
12297
- return lessonData.files.map((file) => ({
12298
- type: "file",
12299
- id: file.id,
12300
- title: file.title,
12301
- mediaType: file.mediaType,
12302
- url: file.url,
12303
- image: lessonData.image
12304
- }));
12305
- }
12306
- if (level === "study") {
12307
- const studyData = folder.providerData?.studyData;
12308
- if (!studyData) return null;
11960
+ async getPlaylist(path, _auth, _resolution) {
11961
+ const { segments, depth } = parsePath(path);
11962
+ if (depth < 2) return null;
11963
+ const collectionSlug = segments[0];
11964
+ const studyId = segments[1];
11965
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
11966
+ if (!collection) return null;
11967
+ const study = collection.folders.find((s) => s.id === studyId);
11968
+ if (!study) return null;
11969
+ if (depth === 2) {
12309
11970
  const allFiles = [];
12310
- for (const lesson of studyData.lessons) {
11971
+ for (const lesson of study.lessons) {
12311
11972
  for (const file of lesson.files) {
12312
- allFiles.push({
12313
- type: "file",
12314
- id: file.id,
12315
- title: file.title,
12316
- mediaType: file.mediaType,
12317
- url: file.url,
12318
- image: lesson.image
12319
- });
11973
+ allFiles.push({ type: "file", id: file.id, title: file.title, mediaType: file.mediaType, url: file.url, image: lesson.image });
12320
11974
  }
12321
11975
  }
12322
11976
  return allFiles;
12323
11977
  }
11978
+ if (depth === 3) {
11979
+ const lessonId = segments[2];
11980
+ const lesson = study.lessons.find((l) => l.id === lessonId);
11981
+ if (!lesson?.files) return null;
11982
+ return lesson.files.map((file) => ({ type: "file", id: file.id, title: file.title, mediaType: file.mediaType, url: file.url, image: lesson.image }));
11983
+ }
12324
11984
  return null;
12325
11985
  }
12326
- getStudyFolders(collectionName) {
12327
- const collection = this.data.collections.find((c) => c.name === collectionName);
12328
- if (!collection) return [];
12329
- return collection.folders.map((study) => this.createFolder(
12330
- study.id,
12331
- study.name,
12332
- study.image || void 0,
12333
- {
12334
- level: "study",
12335
- collectionName,
12336
- studyData: study,
12337
- isLeaf: true
12338
- // Mark as leaf so venue choice modal appears
12339
- }
12340
- ));
12341
- }
12342
- getLessonFolders(study) {
12343
- return study.lessons.map((lesson) => this.createFolder(
12344
- lesson.id,
12345
- lesson.name,
12346
- lesson.image || void 0,
12347
- {
12348
- level: "lesson",
12349
- studyId: study.id,
12350
- lessonData: lesson,
12351
- isLeaf: true
12352
- // Mark as leaf so venue choice modal appears
12353
- }
12354
- ));
11986
+ async getInstructions(path, _auth) {
11987
+ const { segments, depth } = parsePath(path);
11988
+ if (depth < 2) return null;
11989
+ const collectionSlug = segments[0];
11990
+ const studyId = segments[1];
11991
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
11992
+ if (!collection) return null;
11993
+ const study = collection.folders.find((s) => s.id === studyId);
11994
+ if (!study) return null;
11995
+ if (depth === 2) {
11996
+ const lessonItems = study.lessons.map((lesson) => {
11997
+ const fileItems = lesson.files.map((file) => {
11998
+ const seconds = estimateDuration(file.mediaType);
11999
+ return { id: file.id, itemType: "file", label: file.title, seconds, embedUrl: file.url };
12000
+ });
12001
+ return { id: lesson.id, itemType: "action", label: lesson.name, description: "play", children: fileItems };
12002
+ });
12003
+ return { venueName: study.name, items: [{ id: study.id, itemType: "header", label: study.name, children: [{ id: "main", itemType: "section", label: "Content", children: lessonItems }] }] };
12004
+ }
12005
+ if (depth === 3) {
12006
+ const lessonId = segments[2];
12007
+ const lesson = study.lessons.find((l) => l.id === lessonId);
12008
+ if (!lesson?.files) return null;
12009
+ const headerLabel = `${study.name} - ${lesson.name}`;
12010
+ const actionItems = this.groupFilesIntoActions(lesson.files);
12011
+ return { venueName: lesson.name, items: [{ id: lesson.id, itemType: "header", label: headerLabel, children: [{ id: "main", itemType: "section", label: lesson.name, children: actionItems }] }] };
12012
+ }
12013
+ return null;
12355
12014
  }
12356
12015
  slugify(text) {
12357
12016
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
12358
12017
  }
12018
+ groupFilesIntoActions(files) {
12019
+ const actionItems = [];
12020
+ let currentGroup = [];
12021
+ let currentBaseName = null;
12022
+ const flushGroup = () => {
12023
+ if (currentGroup.length === 0) return;
12024
+ const children = currentGroup.map((file) => {
12025
+ const seconds = estimateDuration(file.mediaType);
12026
+ return {
12027
+ id: file.id,
12028
+ itemType: "file",
12029
+ label: file.title,
12030
+ seconds,
12031
+ embedUrl: file.url
12032
+ };
12033
+ });
12034
+ const label = currentGroup.length > 1 && currentBaseName ? currentBaseName : currentGroup[0].title;
12035
+ actionItems.push({
12036
+ id: currentGroup[0].id + "-action",
12037
+ itemType: "action",
12038
+ label,
12039
+ description: "play",
12040
+ children
12041
+ });
12042
+ currentGroup = [];
12043
+ currentBaseName = null;
12044
+ };
12045
+ for (const file of files) {
12046
+ const baseName = this.getBaseName(file.title);
12047
+ const isNumbered = baseName !== file.title;
12048
+ if (isNumbered && baseName === currentBaseName) {
12049
+ currentGroup.push(file);
12050
+ } else {
12051
+ flushGroup();
12052
+ currentGroup = [file];
12053
+ currentBaseName = isNumbered ? baseName : null;
12054
+ }
12055
+ }
12056
+ flushGroup();
12057
+ return actionItems;
12058
+ }
12059
+ getBaseName(title) {
12060
+ const match = title.match(/^(.+?)\s*\d+$/);
12061
+ return match ? match[1].trim() : title;
12062
+ }
12359
12063
  };
12360
12064
 
12361
12065
  // src/providers/index.ts
@@ -12440,15 +12144,15 @@ function getProviderConfig(providerId) {
12440
12144
  const provider = getProvider(providerId);
12441
12145
  return provider?.config || null;
12442
12146
  }
12443
- function getAvailableProviders() {
12147
+ function getAvailableProviders(ids) {
12444
12148
  const implemented = getAllProviders().map((provider) => ({
12445
12149
  id: provider.id,
12446
12150
  name: provider.name,
12447
12151
  logos: provider.logos,
12448
12152
  implemented: true,
12449
- requiresAuth: provider.requiresAuth(),
12450
- authTypes: provider.getAuthTypes(),
12451
- capabilities: provider.getCapabilities()
12153
+ requiresAuth: provider.requiresAuth,
12154
+ authTypes: provider.authTypes,
12155
+ capabilities: provider.capabilities
12452
12156
  }));
12453
12157
  const comingSoon = unimplementedProviders.map((p) => ({
12454
12158
  id: p.id,
@@ -12457,42 +12161,63 @@ function getAvailableProviders() {
12457
12161
  implemented: false,
12458
12162
  requiresAuth: false,
12459
12163
  authTypes: [],
12460
- capabilities: { browse: false, presentations: false, playlist: false, instructions: false, expandedInstructions: false, mediaLicensing: false }
12164
+ capabilities: { browse: false, presentations: false, playlist: false, instructions: false, mediaLicensing: false }
12461
12165
  }));
12462
- return [...implemented, ...comingSoon];
12166
+ const all = [...implemented, ...comingSoon];
12167
+ if (ids && ids.length > 0) {
12168
+ const idSet = new Set(ids);
12169
+ return all.filter((provider) => idSet.has(provider.id));
12170
+ }
12171
+ return all;
12463
12172
  }
12464
12173
 
12465
12174
  // src/index.ts
12466
- var VERSION = "0.0.1";
12175
+ var VERSION = "0.0.4";
12467
12176
  // Annotate the CommonJS export names for ESM import in node:
12468
12177
  0 && (module.exports = {
12469
12178
  APlayProvider,
12179
+ ApiHelper,
12470
12180
  B1ChurchProvider,
12471
12181
  BibleProjectProvider,
12472
12182
  ContentProvider,
12183
+ DEFAULT_DURATION_CONFIG,
12184
+ DeviceFlowHelper,
12473
12185
  FormatConverters,
12474
12186
  FormatResolver,
12187
+ HighVoltageKidsProvider,
12475
12188
  LessonsChurchProvider,
12189
+ OAuthHelper,
12476
12190
  PlanningCenterProvider,
12477
12191
  SignPresenterProvider,
12192
+ TokenHelper,
12478
12193
  VERSION,
12479
- collapseInstructions,
12194
+ appendToPath,
12195
+ buildPath,
12196
+ countWords,
12197
+ createFile,
12198
+ createFolder,
12480
12199
  detectMediaType,
12200
+ estimateDuration,
12201
+ estimateImageDuration,
12202
+ estimateTextDuration,
12481
12203
  expandedInstructionsToPlaylist,
12482
12204
  expandedInstructionsToPresentations,
12205
+ generatePath,
12483
12206
  getAllProviders,
12484
12207
  getAvailableProviders,
12485
12208
  getProvider,
12486
12209
  getProviderConfig,
12210
+ getSegment,
12487
12211
  instructionsToPlaylist,
12488
12212
  instructionsToPresentations,
12489
12213
  isContentFile,
12490
12214
  isContentFolder,
12215
+ navigateToPath,
12216
+ parsePath,
12491
12217
  playlistToExpandedInstructions,
12492
12218
  playlistToInstructions,
12493
12219
  playlistToPresentations,
12494
12220
  presentationsToExpandedInstructions,
12495
- presentationsToInstructions,
12496
12221
  presentationsToPlaylist,
12497
12222
  registerProvider
12498
12223
  });