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