@churchapps/content-provider-helper 0.0.2 → 0.0.3

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