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