@churchapps/content-provider-helper 0.0.1 → 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
@@ -1,3 +1,9 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
1
7
  // src/interfaces.ts
2
8
  function isContentFolder(item) {
3
9
  return item.type === "folder";
@@ -12,105 +18,379 @@ function detectMediaType(url, explicitType) {
12
18
  const videoPatterns = [".mp4", ".webm", ".m3u8", ".mov", "stream.mux.com"];
13
19
  return videoPatterns.some((p) => url.includes(p)) ? "video" : "image";
14
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
+ }
15
27
 
16
- // src/ContentProvider.ts
17
- var ContentProvider = class {
18
- /**
19
- * Get a flat list of media files (playlist) for a folder.
20
- * Override in subclass if the provider supports playlists.
21
- * @param _folder - The folder to get playlist for (typically a venue or playlist folder)
22
- * @param _auth - Optional authentication data
23
- * @param _resolution - Optional resolution hint for video quality
24
- * @returns Array of ContentFile objects, or null if not supported
25
- */
26
- getPlaylist(_folder, _auth, _resolution) {
27
- return Promise.resolve(null);
28
+ // src/pathUtils.ts
29
+ function parsePath(path) {
30
+ if (!path || path === "/" || path === "") {
31
+ return { segments: [], depth: 0 };
28
32
  }
29
- /**
30
- * Get instruction/run sheet data for a folder.
31
- * Override in subclass if the provider supports instructions.
32
- * @param _folder - The folder to get instructions for
33
- * @param _auth - Optional authentication data
34
- * @returns Instructions object, or null if not supported
35
- */
36
- getInstructions(_folder, _auth) {
37
- return Promise.resolve(null);
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;
38
47
  }
39
- /**
40
- * Get expanded instruction data with actions for a folder.
41
- * Override in subclass if the provider supports expanded instructions.
42
- * @param _folder - The folder to get expanded instructions for
43
- * @param _auth - Optional authentication data
44
- * @returns Instructions object with expanded action data, or null if not supported
45
- */
46
- getExpandedInstructions(_folder, _auth) {
47
- return Promise.resolve(null);
48
+ const cleanBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
49
+ return cleanBase + "/" + segment;
50
+ }
51
+
52
+ // src/FormatConverters.ts
53
+ var FormatConverters_exports = {};
54
+ __export(FormatConverters_exports, {
55
+ collapseInstructions: () => collapseInstructions,
56
+ expandedInstructionsToPlaylist: () => expandedInstructionsToPlaylist,
57
+ expandedInstructionsToPresentations: () => expandedInstructionsToPresentations,
58
+ instructionsToPlaylist: () => instructionsToPlaylist,
59
+ instructionsToPresentations: () => instructionsToPresentations,
60
+ playlistToExpandedInstructions: () => playlistToExpandedInstructions,
61
+ playlistToInstructions: () => playlistToInstructions,
62
+ playlistToPresentations: () => playlistToPresentations,
63
+ presentationsToExpandedInstructions: () => presentationsToExpandedInstructions,
64
+ presentationsToInstructions: () => presentationsToInstructions,
65
+ presentationsToPlaylist: () => presentationsToPlaylist
66
+ });
67
+ function generateId() {
68
+ return "gen-" + Math.random().toString(36).substring(2, 11);
69
+ }
70
+ function mapItemTypeToActionType(itemType) {
71
+ switch (itemType) {
72
+ case "action":
73
+ case "lessonAction":
74
+ case "providerPresentation":
75
+ case "play":
76
+ return "play";
77
+ case "addon":
78
+ case "add-on":
79
+ case "lessonAddOn":
80
+ case "providerFile":
81
+ return "add-on";
82
+ default:
83
+ return "other";
48
84
  }
49
- /**
50
- * Check if this provider requires authentication.
51
- * @returns true if authentication is required
52
- */
53
- requiresAuth() {
54
- return !!this.config.clientId;
85
+ }
86
+ function mapActionTypeToItemType(actionType) {
87
+ switch (actionType) {
88
+ case "play":
89
+ return "action";
90
+ case "add-on":
91
+ return "addon";
92
+ default:
93
+ return "item";
55
94
  }
56
- /**
57
- * Get the capabilities supported by this provider.
58
- * Override in subclass to indicate supported features.
59
- * @returns ProviderCapabilities object
60
- */
61
- getCapabilities() {
95
+ }
96
+ function presentationsToPlaylist(plan) {
97
+ if (plan.allFiles && plan.allFiles.length > 0) {
98
+ return [...plan.allFiles];
99
+ }
100
+ const files = [];
101
+ for (const section of plan.sections) {
102
+ for (const presentation of section.presentations) {
103
+ files.push(...presentation.files);
104
+ }
105
+ }
106
+ return files;
107
+ }
108
+ function presentationsToInstructions(plan) {
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
+ }) })) };
113
+ }
114
+ function presentationsToExpandedInstructions(plan) {
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 })) })) })) };
116
+ }
117
+ function instructionsToPlaylist(instructions) {
118
+ const files = [];
119
+ function extractFiles(items) {
120
+ for (const item of items) {
121
+ if (item.embedUrl && (item.itemType === "file" || !item.children?.length)) {
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 });
123
+ }
124
+ if (item.children) {
125
+ extractFiles(item.children);
126
+ }
127
+ }
128
+ }
129
+ extractFiles(instructions.items);
130
+ return files;
131
+ }
132
+ var expandedInstructionsToPlaylist = instructionsToPlaylist;
133
+ function instructionsToPresentations(instructions, planId) {
134
+ const allFiles = [];
135
+ const sections = instructions.items.filter((item) => item.children && item.children.length > 0).map((sectionItem) => {
136
+ const presentations = (sectionItem.children || []).map((presItem) => {
137
+ const files = [];
138
+ if (presItem.children && presItem.children.length > 0) {
139
+ for (const child of presItem.children) {
140
+ if (child.embedUrl) {
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 };
142
+ allFiles.push(file);
143
+ files.push(file);
144
+ }
145
+ }
146
+ }
147
+ if (files.length === 0 && presItem.embedUrl) {
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 };
149
+ allFiles.push(file);
150
+ files.push(file);
151
+ }
152
+ return { id: presItem.id || presItem.relatedId || generateId(), name: presItem.label || "Presentation", actionType: mapItemTypeToActionType(presItem.itemType), files };
153
+ });
154
+ return { id: sectionItem.id || sectionItem.relatedId || generateId(), name: sectionItem.label || "Section", presentations };
155
+ });
156
+ return { id: planId || generateId(), name: instructions.venueName || "Plan", sections, allFiles };
157
+ }
158
+ var expandedInstructionsToPresentations = instructionsToPresentations;
159
+ function collapseInstructions(instructions, maxDepth = 2) {
160
+ function collapseItem(item, currentDepth) {
161
+ if (currentDepth >= maxDepth || !item.children || item.children.length === 0) {
162
+ const { children, ...rest } = item;
163
+ if (children && children.length > 0) {
164
+ const totalSeconds = children.reduce((sum, c) => sum + (c.seconds || 0), 0);
165
+ if (totalSeconds > 0) {
166
+ rest.seconds = totalSeconds;
167
+ }
168
+ if (!rest.embedUrl) {
169
+ const firstWithUrl = children.find((c) => c.embedUrl);
170
+ if (firstWithUrl) {
171
+ rest.embedUrl = firstWithUrl.embedUrl;
172
+ }
173
+ }
174
+ }
175
+ return rest;
176
+ }
62
177
  return {
63
- browse: true,
64
- presentations: false,
65
- playlist: false,
66
- instructions: false,
67
- expandedInstructions: false,
68
- mediaLicensing: false
178
+ ...item,
179
+ children: item.children.map((child) => collapseItem(child, currentDepth + 1))
69
180
  };
70
181
  }
71
- /**
72
- * Check the license status for a specific media item.
73
- * Override in subclass if the provider requires license validation.
74
- * @param _mediaId - The media ID to check
75
- * @param _auth - Optional authentication data
76
- * @returns MediaLicenseResult object, or null if not supported
77
- */
78
- checkMediaLicense(_mediaId, _auth) {
79
- return Promise.resolve(null);
182
+ return {
183
+ venueName: instructions.venueName,
184
+ items: instructions.items.map((item) => collapseItem(item, 0))
185
+ };
186
+ }
187
+ function playlistToPresentations(files, planName = "Playlist", sectionName = "Content") {
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] };
190
+ }
191
+ function playlistToInstructions(files, venueName = "Playlist") {
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 })) }] };
193
+ }
194
+ var playlistToExpandedInstructions = playlistToInstructions;
195
+
196
+ // src/FormatResolver.ts
197
+ var FormatResolver = class {
198
+ constructor(provider, options = {}) {
199
+ this.provider = provider;
200
+ this.options = { allowLossy: options.allowLossy ?? true };
80
201
  }
81
- /**
82
- * Get the authentication types supported by this provider.
83
- * @returns Array of supported AuthType values ('none', 'oauth_pkce', 'device_flow')
84
- */
85
- getAuthTypes() {
86
- if (!this.requiresAuth()) return ["none"];
87
- const types = ["oauth_pkce"];
88
- if (this.supportsDeviceFlow()) types.push("device_flow");
89
- return types;
202
+ getProvider() {
203
+ return this.provider;
90
204
  }
91
- /**
92
- * Check if the provided auth data is still valid (not expired).
93
- * @param auth - Authentication data to validate
94
- * @returns true if auth is valid and not expired
95
- */
96
- isAuthValid(auth) {
97
- if (!auth) return false;
98
- return !this.isTokenExpired(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";
99
209
  }
100
- /**
101
- * Check if a token is expired (with 5-minute buffer).
102
- * @param auth - Authentication data to check
103
- * @returns true if token is expired or will expire within 5 minutes
104
- */
105
- isTokenExpired(auth) {
106
- if (!auth.created_at || !auth.expires_in) return true;
107
- const expiresAt = (auth.created_at + auth.expires_in) * 1e3;
108
- return Date.now() > expiresAt - 5 * 60 * 1e3;
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);
214
+ if (result && result.length > 0) return result;
215
+ }
216
+ if (caps.presentations) {
217
+ const plan = await this.provider.getPresentations(path, auth);
218
+ if (plan) return presentationsToPlaylist(plan);
219
+ }
220
+ if (caps.expandedInstructions && this.provider.getExpandedInstructions) {
221
+ const expanded = await this.provider.getExpandedInstructions(path, auth);
222
+ if (expanded) return instructionsToPlaylist(expanded);
223
+ }
224
+ if (this.options.allowLossy && caps.instructions && this.provider.getInstructions) {
225
+ const instructions = await this.provider.getInstructions(path, auth);
226
+ if (instructions) return instructionsToPlaylist(instructions);
227
+ }
228
+ return null;
109
229
  }
110
- /**
111
- * Generate a random code verifier for PKCE.
112
- * @returns A 64-character random string
113
- */
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);
234
+ if (result && result.length > 0) {
235
+ return { data: result, meta: { isNative: true, isLossy: false } };
236
+ }
237
+ }
238
+ if (caps.presentations) {
239
+ const plan = await this.provider.getPresentations(path, auth);
240
+ if (plan) return { data: presentationsToPlaylist(plan), meta: { isNative: false, sourceFormat: "presentations", isLossy: false } };
241
+ }
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 } };
245
+ }
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 } };
249
+ }
250
+ return { data: null, meta: { isNative: false, isLossy: false } };
251
+ }
252
+ async getPresentations(path, auth) {
253
+ const caps = this.provider.capabilities;
254
+ const fallbackId = this.getIdFromPath(path);
255
+ if (caps.presentations) {
256
+ const result = await this.provider.getPresentations(path, auth);
257
+ if (result) return result;
258
+ }
259
+ if (caps.expandedInstructions && this.provider.getExpandedInstructions) {
260
+ const expanded = await this.provider.getExpandedInstructions(path, auth);
261
+ if (expanded) return instructionsToPresentations(expanded, fallbackId);
262
+ }
263
+ if (caps.instructions && this.provider.getInstructions) {
264
+ const instructions = await this.provider.getInstructions(path, auth);
265
+ if (instructions) return instructionsToPresentations(instructions, fallbackId);
266
+ }
267
+ if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
268
+ const playlist = await this.provider.getPlaylist(path, auth);
269
+ if (playlist && playlist.length > 0) {
270
+ return playlistToPresentations(playlist, fallbackId);
271
+ }
272
+ }
273
+ return null;
274
+ }
275
+ async getPresentationsWithMeta(path, auth) {
276
+ const caps = this.provider.capabilities;
277
+ const fallbackId = this.getIdFromPath(path);
278
+ if (caps.presentations) {
279
+ const result = await this.provider.getPresentations(path, auth);
280
+ if (result) {
281
+ return { data: result, meta: { isNative: true, isLossy: false } };
282
+ }
283
+ }
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 } };
287
+ }
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 } };
291
+ }
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 } };
295
+ }
296
+ return { data: null, meta: { isNative: false, isLossy: false } };
297
+ }
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);
303
+ if (result) return result;
304
+ }
305
+ if (caps.expandedInstructions && this.provider.getExpandedInstructions) {
306
+ const expanded = await this.provider.getExpandedInstructions(path, auth);
307
+ if (expanded) return collapseInstructions(expanded);
308
+ }
309
+ if (caps.presentations) {
310
+ const plan = await this.provider.getPresentations(path, auth);
311
+ if (plan) return presentationsToInstructions(plan);
312
+ }
313
+ if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
314
+ const playlist = await this.provider.getPlaylist(path, auth);
315
+ if (playlist && playlist.length > 0) {
316
+ return playlistToInstructions(playlist, fallbackTitle);
317
+ }
318
+ }
319
+ return null;
320
+ }
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);
326
+ if (result) {
327
+ return { data: result, meta: { isNative: true, isLossy: false } };
328
+ }
329
+ }
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 } };
333
+ }
334
+ if (caps.presentations) {
335
+ const plan = await this.provider.getPresentations(path, auth);
336
+ if (plan) return { data: presentationsToInstructions(plan), meta: { isNative: false, sourceFormat: "presentations", isLossy: false } };
337
+ }
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 } };
341
+ }
342
+ return { data: null, meta: { isNative: false, isLossy: false } };
343
+ }
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);
349
+ if (result) return result;
350
+ }
351
+ if (caps.presentations) {
352
+ const plan = await this.provider.getPresentations(path, auth);
353
+ if (plan) return presentationsToExpandedInstructions(plan);
354
+ }
355
+ if (caps.instructions && this.provider.getInstructions) {
356
+ const instructions = await this.provider.getInstructions(path, auth);
357
+ if (instructions) return instructions;
358
+ }
359
+ if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
360
+ const playlist = await this.provider.getPlaylist(path, auth);
361
+ if (playlist && playlist.length > 0) {
362
+ return playlistToInstructions(playlist, fallbackTitle);
363
+ }
364
+ }
365
+ return null;
366
+ }
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);
372
+ if (result) {
373
+ return { data: result, meta: { isNative: true, isLossy: false } };
374
+ }
375
+ }
376
+ if (caps.presentations) {
377
+ const plan = await this.provider.getPresentations(path, auth);
378
+ if (plan) return { data: presentationsToExpandedInstructions(plan), meta: { isNative: false, sourceFormat: "presentations", isLossy: false } };
379
+ }
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 } };
383
+ }
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 } };
387
+ }
388
+ return { data: null, meta: { isNative: false, isLossy: false } };
389
+ }
390
+ };
391
+
392
+ // src/helpers/OAuthHelper.ts
393
+ var OAuthHelper = class {
114
394
  generateCodeVerifier() {
115
395
  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
116
396
  const length = 64;
@@ -122,11 +402,6 @@ var ContentProvider = class {
122
402
  }
123
403
  return result;
124
404
  }
125
- /**
126
- * Generate a code challenge from a code verifier using SHA-256.
127
- * @param verifier - The code verifier string
128
- * @returns Base64url-encoded SHA-256 hash of the verifier
129
- */
130
405
  async generateCodeChallenge(verifier) {
131
406
  const encoder = new TextEncoder();
132
407
  const data = encoder.encode(verifier);
@@ -139,142 +414,100 @@ var ContentProvider = class {
139
414
  const base64 = btoa(binary);
140
415
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
141
416
  }
142
- /**
143
- * Build the OAuth authorization URL for PKCE flow.
144
- * @param codeVerifier - The code verifier (store this for token exchange)
145
- * @param redirectUri - The redirect URI to return to after authorization
146
- * @param state - Optional state parameter for CSRF protection (defaults to provider ID)
147
- * @returns Object with authorization URL and challenge method
148
- */
149
- async buildAuthUrl(codeVerifier, redirectUri, state) {
417
+ async buildAuthUrl(config, codeVerifier, redirectUri, state) {
150
418
  const codeChallenge = await this.generateCodeChallenge(codeVerifier);
151
419
  const params = new URLSearchParams({
152
420
  response_type: "code",
153
- client_id: this.config.clientId,
421
+ client_id: config.clientId,
154
422
  redirect_uri: redirectUri,
155
- scope: this.config.scopes.join(" "),
423
+ scope: config.scopes.join(" "),
156
424
  code_challenge: codeChallenge,
157
425
  code_challenge_method: "S256",
158
- state: state || this.id
426
+ state: state || ""
159
427
  });
160
- return { url: `${this.config.oauthBase}/authorize?${params.toString()}`, challengeMethod: "S256" };
161
- }
162
- /**
163
- * Exchange an authorization code for access and refresh tokens.
164
- * @param code - The authorization code from the callback
165
- * @param codeVerifier - The original code verifier used to generate the challenge
166
- * @param redirectUri - The redirect URI (must match the one used in buildAuthUrl)
167
- * @returns Authentication data, or null if exchange failed
168
- */
169
- async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
428
+ return { url: `${config.oauthBase}/authorize?${params.toString()}`, challengeMethod: "S256" };
429
+ }
430
+ async exchangeCodeForTokens(config, providerId, code, codeVerifier, redirectUri) {
170
431
  try {
171
432
  const params = new URLSearchParams({
172
433
  grant_type: "authorization_code",
173
434
  code,
174
435
  redirect_uri: redirectUri,
175
- client_id: this.config.clientId,
436
+ client_id: config.clientId,
176
437
  code_verifier: codeVerifier
177
438
  });
178
- const tokenUrl = `${this.config.oauthBase}/token`;
179
- console.log(`${this.id} token exchange request to: ${tokenUrl}`);
180
- 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}`);
181
442
  console.log(` - redirect_uri: ${redirectUri}`);
182
443
  console.log(` - code: ${code.substring(0, 10)}...`);
183
444
  const response = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
184
- console.log(`${this.id} token response status: ${response.status}`);
445
+ console.log(`${providerId} token response status: ${response.status}`);
185
446
  if (!response.ok) {
186
447
  const errorText = await response.text();
187
- console.error(`${this.id} token exchange failed: ${response.status} - ${errorText}`);
448
+ console.error(`${providerId} token exchange failed: ${response.status} - ${errorText}`);
188
449
  return null;
189
450
  }
190
451
  const data = await response.json();
191
- console.log(`${this.id} token exchange successful, got access_token: ${!!data.access_token}`);
192
- return {
193
- access_token: data.access_token,
194
- refresh_token: data.refresh_token,
195
- token_type: data.token_type || "Bearer",
196
- created_at: Math.floor(Date.now() / 1e3),
197
- expires_in: data.expires_in,
198
- scope: data.scope || this.config.scopes.join(" ")
199
- };
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(" ") };
200
454
  } catch (error) {
201
- console.error(`${this.id} token exchange error:`, error);
455
+ console.error(`${providerId} token exchange error:`, error);
202
456
  return null;
203
457
  }
204
458
  }
205
- /**
206
- * Refresh an expired access token using the refresh token.
207
- * @param auth - The current authentication data (must include refresh_token)
208
- * @returns New authentication data, or null if refresh failed
209
- */
210
- 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) {
211
473
  if (!auth.refresh_token) return null;
212
474
  try {
213
475
  const params = new URLSearchParams({
214
476
  grant_type: "refresh_token",
215
477
  refresh_token: auth.refresh_token,
216
- client_id: this.config.clientId
478
+ client_id: config.clientId
217
479
  });
218
- 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() });
219
481
  if (!response.ok) return null;
220
482
  const data = await response.json();
221
- return {
222
- access_token: data.access_token,
223
- refresh_token: data.refresh_token || auth.refresh_token,
224
- token_type: data.token_type || "Bearer",
225
- created_at: Math.floor(Date.now() / 1e3),
226
- expires_in: data.expires_in,
227
- scope: data.scope || auth.scope
228
- };
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 };
229
484
  } catch {
230
485
  return null;
231
486
  }
232
487
  }
233
- /**
234
- * Check if this provider supports device flow authentication.
235
- * @returns true if device flow is supported
236
- */
237
- supportsDeviceFlow() {
238
- 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;
239
494
  }
240
- /**
241
- * Initiate the device authorization flow (RFC 8628).
242
- * @returns Device authorization response with user_code and verification_uri, or null if not supported
243
- */
244
- async initiateDeviceFlow() {
245
- if (!this.supportsDeviceFlow()) return null;
495
+ async initiateDeviceFlow(config) {
496
+ if (!this.supportsDeviceFlow(config)) return null;
246
497
  try {
247
- const params = new URLSearchParams({ client_id: this.config.clientId, scope: this.config.scopes.join(" ") });
248
- 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(" ") }) });
249
499
  if (!response.ok) return null;
250
500
  return await response.json();
251
501
  } catch {
252
502
  return null;
253
503
  }
254
504
  }
255
- /**
256
- * Poll for a token after user has authorized the device.
257
- * @param deviceCode - The device_code from initiateDeviceFlow response
258
- * @returns Auth data if successful, error object if pending/slow_down, or null if failed/expired
259
- */
260
- async pollDeviceFlowToken(deviceCode) {
505
+ async pollDeviceFlowToken(config, deviceCode) {
261
506
  try {
262
- const params = new URLSearchParams({
263
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
264
- device_code: deviceCode,
265
- client_id: this.config.clientId
266
- });
267
- 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 }) });
268
508
  if (response.ok) {
269
509
  const data = await response.json();
270
- return {
271
- access_token: data.access_token,
272
- refresh_token: data.refresh_token,
273
- token_type: data.token_type || "Bearer",
274
- created_at: Math.floor(Date.now() / 1e3),
275
- expires_in: data.expires_in,
276
- scope: data.scope || this.config.scopes.join(" ")
277
- };
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(" ") };
278
511
  }
279
512
  const errorData = await response.json();
280
513
  switch (errorData.error) {
@@ -293,221 +526,295 @@ var ContentProvider = class {
293
526
  return { error: "network_error" };
294
527
  }
295
528
  }
296
- /**
297
- * Calculate the delay between device flow poll attempts.
298
- * @param baseInterval - Base interval in seconds (default: 5)
299
- * @param slowDownCount - Number of slow_down responses received
300
- * @returns Delay in milliseconds
301
- */
302
529
  calculatePollDelay(baseInterval = 5, slowDownCount = 0) {
303
530
  return (baseInterval + slowDownCount * 5) * 1e3;
304
531
  }
305
- /**
306
- * Create authorization headers for API requests.
307
- * @param auth - Authentication data
308
- * @returns Headers object with Authorization header, or null if no auth
309
- */
532
+ };
533
+
534
+ // src/helpers/ApiHelper.ts
535
+ var ApiHelper = class {
310
536
  createAuthHeaders(auth) {
311
537
  if (!auth) return null;
312
538
  return { Authorization: `Bearer ${auth.access_token}`, Accept: "application/json" };
313
539
  }
314
- /**
315
- * Make an authenticated API request.
316
- * @param path - API endpoint path (appended to config.apiBase)
317
- * @param auth - Optional authentication data
318
- * @param method - HTTP method (default: 'GET')
319
- * @param body - Optional request body (for POST requests)
320
- * @returns Parsed JSON response, or null if request failed
321
- */
322
- async apiRequest(path, auth, method = "GET", body) {
540
+ async apiRequest(config, providerId, path, auth, method = "GET", body) {
323
541
  try {
324
- const url = `${this.config.apiBase}${path}`;
542
+ const url = `${config.apiBase}${path}`;
325
543
  const headers = { Accept: "application/json" };
326
544
  if (auth) headers["Authorization"] = `Bearer ${auth.access_token}`;
327
545
  if (body) headers["Content-Type"] = "application/json";
328
- console.log(`${this.id} API request: ${method} ${url}`);
329
- console.log(`${this.id} API auth present: ${!!auth}`);
546
+ console.log(`${providerId} API request: ${method} ${url}`);
547
+ console.log(`${providerId} API auth present: ${!!auth}`);
330
548
  const options = { method, headers, ...body ? { body: JSON.stringify(body) } : {} };
331
549
  const response = await fetch(url, options);
332
- console.log(`${this.id} API response status: ${response.status}`);
550
+ console.log(`${providerId} API response status: ${response.status}`);
333
551
  if (!response.ok) {
334
552
  const errorText = await response.text();
335
- console.error(`${this.id} API request failed: ${response.status} - ${errorText}`);
553
+ console.error(`${providerId} API request failed: ${response.status} - ${errorText}`);
336
554
  return null;
337
555
  }
338
556
  return await response.json();
339
557
  } catch (error) {
340
- console.error(`${this.id} API request error:`, error);
558
+ console.error(`${providerId} API request error:`, error);
341
559
  return null;
342
560
  }
343
561
  }
344
- /**
345
- * Helper to create a ContentFolder object.
346
- * @param id - Unique identifier
347
- * @param title - Display title
348
- * @param image - Optional image URL
349
- * @param providerData - Optional provider-specific data
350
- * @returns ContentFolder object
351
- */
352
- createFolder(id, title, image, providerData) {
353
- return { type: "folder", id, title, image, providerData };
354
- }
355
- /**
356
- * Helper to create a ContentFile object with automatic media type detection.
357
- * @param id - Unique identifier
358
- * @param title - Display title
359
- * @param url - Media URL
360
- * @param options - Optional properties (mediaType, image, muxPlaybackId, providerData)
361
- * @returns ContentFile object
362
- */
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 };
661
+ }
363
662
  createFile(id, title, url, options) {
364
- return {
365
- type: "file",
366
- id,
367
- title,
368
- url,
369
- mediaType: options?.mediaType ?? detectMediaType(url),
370
- image: options?.image,
371
- muxPlaybackId: options?.muxPlaybackId,
372
- providerData: options?.providerData
373
- };
663
+ return { type: "file", id, title, url, mediaType: options?.mediaType ?? detectMediaType(url), image: options?.image, muxPlaybackId: options?.muxPlaybackId, providerData: options?.providerData };
374
664
  }
375
665
  };
376
666
 
377
- // src/providers/APlayProvider.ts
378
- var APlayProvider = class extends ContentProvider {
667
+ // src/providers/aPlay/APlayProvider.ts
668
+ var APlayProvider = class {
379
669
  constructor() {
380
- super(...arguments);
670
+ this.apiHelper = new ApiHelper();
381
671
  this.id = "aplay";
382
672
  this.name = "APlay";
383
- this.logos = {
384
- light: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg",
385
- dark: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg"
386
- };
387
- this.config = {
388
- id: "aplay",
389
- name: "APlay",
390
- apiBase: "https://api-prod.amazingkids.app",
391
- oauthBase: "https://api.joinamazing.com/prod/aims/oauth",
392
- clientId: "xFJFq7yNYuXXXMx0YBiQ",
393
- scopes: ["openid", "profile", "email"],
394
- endpoints: {
395
- modules: "/prod/curriculum/modules",
396
- productLibraries: (productId) => `/prod/curriculum/modules/products/${productId}/libraries`,
397
- libraryMedia: (libraryId) => `/prod/creators/libraries/${libraryId}/media`
398
- }
399
- };
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 };
400
678
  }
401
- getCapabilities() {
402
- return {
403
- browse: true,
404
- presentations: true,
405
- playlist: false,
406
- instructions: false,
407
- expandedInstructions: false,
408
- mediaLicensing: true
409
- };
679
+ async apiRequest(path, auth) {
680
+ return this.apiHelper.apiRequest(this.config, this.id, path, auth);
410
681
  }
411
- async browse(folder, auth) {
412
- console.log(`APlay browse called with folder:`, folder ? { id: folder.id, level: folder.providerData?.level } : "null");
413
- console.log(`APlay browse auth present:`, !!auth);
414
- if (!folder) {
415
- console.log(`APlay fetching modules from: ${this.config.endpoints.modules}`);
416
- const response = await this.apiRequest(this.config.endpoints.modules, auth);
417
- console.log(`APlay modules response:`, response ? "received" : "null");
418
- if (!response) return [];
419
- const modules = response.data || response.modules || response;
420
- console.log(`APlay modules count:`, Array.isArray(modules) ? modules.length : "not an array");
421
- if (!Array.isArray(modules)) return [];
422
- const items = [];
423
- for (const m of modules) {
424
- if (m.isLocked) continue;
425
- const allProducts = m.products || [];
426
- const products = allProducts.filter((p) => !p.isHidden);
427
- if (products.length === 0) {
428
- items.push({
429
- type: "folder",
430
- id: m.id || m.moduleId,
431
- title: m.title || m.name,
432
- image: m.image,
433
- providerData: { level: "libraries", productId: m.id || m.moduleId }
434
- });
435
- } else if (products.length === 1) {
436
- const product = products[0];
437
- items.push({
438
- type: "folder",
439
- id: product.productId || product.id,
440
- title: m.title || m.name,
441
- image: m.image || product.image,
442
- providerData: { level: "libraries", productId: product.productId || product.id }
443
- });
444
- } else {
445
- items.push({
446
- type: "folder",
447
- id: m.id || m.moduleId,
448
- title: m.title || m.name,
449
- image: m.image,
450
- providerData: {
451
- level: "products",
452
- products: products.map((p) => ({ id: p.productId || p.id, title: p.title || p.name, image: p.image }))
453
- }
454
- });
455
- }
456
- }
457
- 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
+ }];
458
692
  }
459
- const level = folder.providerData?.level;
460
- switch (level) {
461
- case "products":
462
- return this.getProductFolders(folder);
463
- case "libraries":
464
- return this.getLibraryFolders(folder, auth);
465
- case "media":
466
- return this.getMediaFiles(folder, auth);
467
- default:
468
- return [];
693
+ const root = segments[0];
694
+ if (root !== "modules") return [];
695
+ if (depth === 1) {
696
+ return this.getModules(auth);
469
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);
709
+ }
710
+ if (depth === 4 && segments[2] === "libraries") {
711
+ const libraryId = segments[3];
712
+ return this.getMediaFiles(libraryId, auth);
713
+ }
714
+ return [];
470
715
  }
471
- getProductFolders(folder) {
472
- const products = folder.providerData?.products || [];
473
- return products.map((p) => ({
474
- type: "folder",
475
- id: p.id,
476
- title: p.title,
477
- image: p.image,
478
- providerData: { level: "libraries", productId: p.id }
479
- }));
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;
480
770
  }
481
- async getLibraryFolders(folder, auth) {
482
- const productId = folder.providerData?.productId;
483
- console.log(`APlay getLibraryFolders called with productId:`, productId);
484
- 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);
485
793
  const pathFn = this.config.endpoints.productLibraries;
486
- const path = pathFn(productId);
487
- console.log(`APlay fetching libraries from: ${path}`);
488
- const response = await this.apiRequest(path, auth);
489
- 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");
490
798
  if (!response) return [];
491
799
  const libraries = response.data || response.libraries || response;
492
- 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");
493
801
  if (!Array.isArray(libraries)) return [];
494
802
  return libraries.map((l) => ({
495
803
  type: "folder",
496
804
  id: l.libraryId || l.id,
497
805
  title: l.title || l.name,
498
806
  image: l.image,
499
- providerData: { level: "media", libraryId: l.libraryId || l.id }
807
+ isLeaf: true,
808
+ path: `${currentPath}/${l.libraryId || l.id}`
500
809
  }));
501
810
  }
502
- async getMediaFiles(folder, auth) {
503
- const libraryId = folder.providerData?.libraryId;
504
- console.log(`APlay getMediaFiles called with libraryId:`, libraryId);
505
- if (!libraryId) return [];
811
+ async getMediaFiles(libraryId, auth) {
812
+ console.log("APlay getMediaFiles called with libraryId:", libraryId);
506
813
  const pathFn = this.config.endpoints.libraryMedia;
507
- const path = pathFn(libraryId);
508
- console.log(`APlay fetching media from: ${path}`);
509
- const response = await this.apiRequest(path, auth);
510
- 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");
511
818
  if (!response) return [];
512
819
  const mediaItems = response.data || response.media || response;
513
820
  if (!Array.isArray(mediaItems)) return [];
@@ -537,144 +844,97 @@ var APlayProvider = class extends ContentProvider {
537
844
  if (!url) continue;
538
845
  const detectedMediaType = detectMediaType(url, mediaType);
539
846
  const fileId = item.mediaId || item.id;
540
- files.push({
541
- type: "file",
542
- id: fileId,
543
- title: item.title || item.name || item.fileName || "",
544
- mediaType: detectedMediaType,
545
- image: thumbnail,
546
- url,
547
- muxPlaybackId,
548
- mediaId: fileId
549
- });
847
+ files.push({ type: "file", id: fileId, title: item.title || item.name || item.fileName || "", mediaType: detectedMediaType, image: thumbnail, url, muxPlaybackId, mediaId: fileId });
550
848
  }
551
849
  return files;
552
850
  }
553
- async getPresentations(folder, auth) {
554
- const libraryId = folder.providerData?.libraryId;
555
- if (!libraryId) return null;
556
- 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);
557
864
  if (files.length === 0) return null;
558
- const presentations = files.map((f) => ({
559
- id: f.id,
560
- name: f.title,
561
- actionType: "play",
562
- files: [f]
563
- }));
564
- return {
565
- id: libraryId,
566
- name: folder.title,
567
- image: folder.image,
568
- sections: [{
569
- id: `section-${libraryId}`,
570
- name: folder.title || "Library",
571
- presentations
572
- }],
573
- allFiles: files
574
- };
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 };
575
867
  }
576
- /**
577
- * Check the license status for a specific media item.
578
- * Returns license information including pingback URL if licensed.
579
- */
580
868
  async checkMediaLicense(mediaId, auth) {
581
869
  if (!auth) return null;
582
870
  try {
583
871
  const url = `${this.config.apiBase}/prod/reports/media/license-check`;
584
- const response = await fetch(url, {
585
- method: "POST",
586
- headers: {
587
- "Authorization": `Bearer ${auth.access_token}`,
588
- "Content-Type": "application/json",
589
- "Accept": "application/json"
590
- },
591
- body: JSON.stringify({ mediaIds: [mediaId] })
592
- });
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] }) });
593
873
  if (!response.ok) return null;
594
874
  const data = await response.json();
595
875
  const licenseData = Array.isArray(data) ? data : data.data || [];
596
876
  const result = licenseData.find((item) => item.mediaId === mediaId);
597
877
  if (result?.isLicensed) {
598
- const pingbackUrl = `${this.config.apiBase}/prod/reports/media/${mediaId}/stream-count?source=aplay-pro`;
599
- return {
600
- mediaId,
601
- status: "valid",
602
- message: "Media is licensed for playback",
603
- expiresAt: result.expiresAt
604
- };
878
+ return { mediaId, status: "valid", message: "Media is licensed for playback", expiresAt: result.expiresAt };
605
879
  }
606
- return {
607
- mediaId,
608
- status: "not_licensed",
609
- message: "Media is not licensed"
610
- };
880
+ return { mediaId, status: "not_licensed", message: "Media is not licensed" };
611
881
  } catch {
612
- return {
613
- mediaId,
614
- status: "unknown",
615
- message: "Unable to verify license status"
616
- };
882
+ return { mediaId, status: "unknown", message: "Unable to verify license status" };
617
883
  }
618
884
  }
619
885
  };
620
886
 
621
- // src/providers/SignPresenterProvider.ts
622
- var SignPresenterProvider = class extends ContentProvider {
887
+ // src/providers/signPresenter/SignPresenterProvider.ts
888
+ var SignPresenterProvider = class {
623
889
  constructor() {
624
- super(...arguments);
890
+ this.apiHelper = new ApiHelper();
625
891
  this.id = "signpresenter";
626
892
  this.name = "SignPresenter";
627
- this.logos = {
628
- light: "https://signpresenter.com/files/shared/images/logo.png",
629
- dark: "https://signpresenter.com/files/shared/images/logo.png"
630
- };
631
- this.config = {
632
- id: "signpresenter",
633
- name: "SignPresenter",
634
- apiBase: "https://api.signpresenter.com",
635
- oauthBase: "https://api.signpresenter.com/oauth",
636
- clientId: "lessonsscreen-tv",
637
- scopes: ["openid", "profile", "content"],
638
- supportsDeviceFlow: true,
639
- deviceAuthEndpoint: "/device/authorize",
640
- endpoints: {
641
- playlists: "/content/playlists",
642
- messages: (playlistId) => `/content/playlists/${playlistId}/messages`
643
- }
644
- };
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 };
645
898
  }
646
- getCapabilities() {
647
- return {
648
- browse: true,
649
- presentations: true,
650
- playlist: false,
651
- instructions: false,
652
- expandedInstructions: false,
653
- mediaLicensing: false
654
- };
899
+ async apiRequest(path, auth) {
900
+ return this.apiHelper.apiRequest(this.config, this.id, path, auth);
655
901
  }
656
- async browse(folder, auth) {
657
- if (!folder) {
658
- const path = this.config.endpoints.playlists;
659
- const response = await this.apiRequest(path, auth);
660
- if (!response) return [];
661
- const playlists = Array.isArray(response) ? response : response.data || response.playlists || [];
662
- if (!Array.isArray(playlists)) return [];
663
- return playlists.map((p) => ({
902
+ async browse(path, auth) {
903
+ const { segments, depth } = parsePath(path);
904
+ if (depth === 0) {
905
+ return [{
664
906
  type: "folder",
665
- id: p.id,
666
- title: p.name,
667
- image: p.image,
668
- providerData: { level: "messages", playlistId: p.id }
669
- }));
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);
670
920
  }
671
- const level = folder.providerData?.level;
672
- if (level === "messages") return this.getMessages(folder, auth);
673
921
  return [];
674
922
  }
675
- async getMessages(folder, auth) {
676
- const playlistId = folder.providerData?.playlistId;
677
- 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) {
678
938
  const pathFn = this.config.endpoints.messages;
679
939
  const response = await this.apiRequest(pathFn(playlistId), auth);
680
940
  if (!response) return [];
@@ -685,93 +945,42 @@ var SignPresenterProvider = class extends ContentProvider {
685
945
  if (!msg.url) continue;
686
946
  const url = msg.url;
687
947
  const seconds = msg.seconds;
688
- files.push({
689
- type: "file",
690
- id: msg.id,
691
- title: msg.name,
692
- mediaType: detectMediaType(url, msg.mediaType),
693
- image: msg.thumbnail || msg.image,
694
- url,
695
- // For direct media providers, embedUrl is the media URL itself
696
- embedUrl: url,
697
- providerData: seconds !== void 0 ? { seconds } : void 0
698
- });
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 });
699
949
  }
700
950
  return files;
701
951
  }
702
- async getPresentations(folder, auth) {
703
- const playlistId = folder.providerData?.playlistId;
704
- if (!playlistId) return null;
705
- 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);
706
957
  if (files.length === 0) return null;
707
- const presentations = files.map((f) => ({
708
- id: f.id,
709
- name: f.title,
710
- actionType: "play",
711
- files: [f]
712
- }));
713
- return {
714
- id: playlistId,
715
- name: folder.title,
716
- image: folder.image,
717
- sections: [{
718
- id: `section-${playlistId}`,
719
- name: folder.title || "Playlist",
720
- presentations
721
- }],
722
- allFiles: files
723
- };
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 };
724
964
  }
725
965
  };
726
966
 
727
- // src/providers/LessonsChurchProvider.ts
728
- var LessonsChurchProvider = class extends ContentProvider {
967
+ // src/providers/lessonsChurch/LessonsChurchProvider.ts
968
+ var LessonsChurchProvider = class {
729
969
  constructor() {
730
- super(...arguments);
731
970
  this.id = "lessonschurch";
732
971
  this.name = "Lessons.church";
733
- this.logos = {
734
- light: "https://lessons.church/images/logo.png",
735
- dark: "https://lessons.church/images/logo-dark.png"
736
- };
737
- this.config = {
738
- id: "lessonschurch",
739
- name: "Lessons.church",
740
- apiBase: "https://api.lessons.church",
741
- oauthBase: "",
742
- clientId: "",
743
- scopes: [],
744
- endpoints: {
745
- programs: "/programs/public",
746
- studies: (programId) => `/studies/public/program/${programId}`,
747
- lessons: (studyId) => `/lessons/public/study/${studyId}`,
748
- venues: (lessonId) => `/venues/public/lesson/${lessonId}`,
749
- playlist: (venueId) => `/venues/playlist/${venueId}`,
750
- feed: (venueId) => `/venues/public/feed/${venueId}`,
751
- addOns: "/addOns/public",
752
- addOnDetail: (id) => `/addOns/public/${id}`
753
- }
754
- };
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 };
755
977
  }
756
- requiresAuth() {
757
- return false;
758
- }
759
- getCapabilities() {
760
- return {
761
- browse: true,
762
- presentations: true,
763
- playlist: true,
764
- instructions: true,
765
- expandedInstructions: true,
766
- mediaLicensing: false
767
- };
768
- }
769
- async getPlaylist(folder, _auth, resolution) {
770
- const venueId = folder.providerData?.venueId;
978
+ async getPlaylist(path, _auth, resolution) {
979
+ const venueId = getSegment(path, 4);
771
980
  if (!venueId) return null;
772
- let path = `/venues/playlist/${venueId}`;
773
- if (resolution) path += `?resolution=${resolution}`;
774
- 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);
775
984
  if (!response) return null;
776
985
  const files = [];
777
986
  const messages = response.messages || [];
@@ -783,15 +992,7 @@ var LessonsChurchProvider = class extends ContentProvider {
783
992
  if (!f.url) continue;
784
993
  const url = f.url;
785
994
  const fileId = f.id || `playlist-${fileIndex++}`;
786
- files.push({
787
- type: "file",
788
- id: fileId,
789
- title: f.name || msg.name,
790
- mediaType: detectMediaType(url, f.fileType),
791
- image: response.lessonImage,
792
- url,
793
- providerData: { seconds: f.seconds, loop: f.loop, loopVideo: f.loopVideo }
794
- });
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 } });
795
996
  }
796
997
  }
797
998
  return files;
@@ -806,48 +1007,32 @@ var LessonsChurchProvider = class extends ContentProvider {
806
1007
  return null;
807
1008
  }
808
1009
  }
809
- async browse(folder, _auth, resolution) {
810
- 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) {
811
1014
  return [
812
- {
813
- type: "folder",
814
- id: "lessons-root",
815
- title: "Lessons",
816
- providerData: { level: "programs" }
817
- },
818
- {
819
- type: "folder",
820
- id: "addons-root",
821
- title: "Add-Ons",
822
- providerData: { level: "addOnCategories" }
823
- }
1015
+ { type: "folder", id: "lessons-root", title: "Lessons", path: "/lessons" },
1016
+ { type: "folder", id: "addons-root", title: "Add-Ons", path: "/addons" }
824
1017
  ];
825
1018
  }
826
- const level = folder.providerData?.level;
827
- switch (level) {
828
- // Lessons hierarchy
829
- case "programs":
830
- return this.getPrograms();
831
- case "studies":
832
- return this.getStudies(folder);
833
- case "lessons":
834
- return this.getLessons(folder);
835
- case "venues":
836
- return this.getVenues(folder);
837
- case "playlist":
838
- return this.getPlaylistFiles(folder, resolution);
839
- // Add-ons hierarchy
840
- case "addOnCategories":
841
- return this.getAddOnCategories();
842
- case "addOns":
843
- return this.getAddOnsByCategory(folder);
844
- default:
845
- return [];
846
- }
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 [];
847
1032
  }
848
1033
  async getPrograms() {
849
- const path = this.config.endpoints.programs;
850
- const response = await this.apiRequest(path);
1034
+ const apiPath = this.config.endpoints.programs;
1035
+ const response = await this.apiRequest(apiPath);
851
1036
  if (!response) return [];
852
1037
  const programs = Array.isArray(response) ? response : [];
853
1038
  return programs.map((p) => ({
@@ -855,12 +1040,10 @@ var LessonsChurchProvider = class extends ContentProvider {
855
1040
  id: p.id,
856
1041
  title: p.name,
857
1042
  image: p.image,
858
- providerData: { level: "studies", programId: p.id }
1043
+ path: `/lessons/${p.id}`
859
1044
  }));
860
1045
  }
861
- async getStudies(folder) {
862
- const programId = folder.providerData?.programId;
863
- if (!programId) return [];
1046
+ async getStudies(programId, currentPath) {
864
1047
  const pathFn = this.config.endpoints.studies;
865
1048
  const response = await this.apiRequest(pathFn(programId));
866
1049
  if (!response) return [];
@@ -870,12 +1053,10 @@ var LessonsChurchProvider = class extends ContentProvider {
870
1053
  id: s.id,
871
1054
  title: s.name,
872
1055
  image: s.image,
873
- providerData: { level: "lessons", studyId: s.id }
1056
+ path: `${currentPath}/${s.id}`
874
1057
  }));
875
1058
  }
876
- async getLessons(folder) {
877
- const studyId = folder.providerData?.studyId;
878
- if (!studyId) return [];
1059
+ async getLessons(studyId, currentPath) {
879
1060
  const pathFn = this.config.endpoints.lessons;
880
1061
  const response = await this.apiRequest(pathFn(studyId));
881
1062
  if (!response) return [];
@@ -885,31 +1066,42 @@ var LessonsChurchProvider = class extends ContentProvider {
885
1066
  id: l.id,
886
1067
  title: l.name || l.title,
887
1068
  image: l.image,
888
- 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
889
1072
  }));
890
1073
  }
891
- async getVenues(folder) {
892
- const lessonId = folder.providerData?.lessonId;
893
- if (!lessonId) return [];
1074
+ async getVenues(lessonId, currentPath) {
894
1075
  const pathFn = this.config.endpoints.venues;
895
1076
  const response = await this.apiRequest(pathFn(lessonId));
896
1077
  if (!response) return [];
1078
+ const lessonResponse = await this.apiRequest(`/lessons/public/${lessonId}`);
1079
+ const lessonImage = lessonResponse?.image;
897
1080
  const venues = Array.isArray(response) ? response : [];
898
- return venues.map((v) => ({
1081
+ const result = venues.map((v) => ({
899
1082
  type: "folder",
900
1083
  id: v.id,
901
1084
  title: v.name,
902
- image: folder.providerData?.lessonImage,
903
- providerData: { level: "playlist", venueId: v.id }
1085
+ image: lessonImage,
1086
+ isLeaf: true,
1087
+ path: `${currentPath}/${v.id}`
904
1088
  }));
1089
+ console.log("[LessonsChurchProvider.getVenues] returning:", result.map((r) => ({ id: r.id, title: r.title, isLeaf: r.isLeaf })));
1090
+ return result;
905
1091
  }
906
- async getPlaylistFiles(folder, resolution) {
907
- const files = await this.getPlaylist(folder, null, resolution);
1092
+ async getPlaylistFiles(venueId) {
1093
+ const files = await this.getPlaylist(`/lessons/_/_/_/${venueId}`, null);
908
1094
  return files || [];
909
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
+ }
910
1102
  async getAddOnCategories() {
911
- const path = this.config.endpoints.addOns;
912
- const response = await this.apiRequest(path);
1103
+ const apiPath = this.config.endpoints.addOns;
1104
+ const response = await this.apiRequest(apiPath);
913
1105
  if (!response) return [];
914
1106
  const addOns = Array.isArray(response) ? response : [];
915
1107
  const categories = Array.from(new Set(addOns.map((a) => a.category).filter(Boolean)));
@@ -917,17 +1109,16 @@ var LessonsChurchProvider = class extends ContentProvider {
917
1109
  type: "folder",
918
1110
  id: `category-${category}`,
919
1111
  title: category,
920
- providerData: {
921
- level: "addOns",
922
- category,
923
- allAddOns: addOns
924
- }
1112
+ path: `/addons/${encodeURIComponent(category)}`
925
1113
  }));
926
1114
  }
927
- async getAddOnsByCategory(folder) {
928
- const category = folder.providerData?.category;
929
- const allAddOns = folder.providerData?.allAddOns || [];
930
- 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);
931
1122
  const files = [];
932
1123
  for (const addOn of filtered) {
933
1124
  const file = await this.convertAddOnToFile(addOn);
@@ -937,8 +1128,8 @@ var LessonsChurchProvider = class extends ContentProvider {
937
1128
  }
938
1129
  async convertAddOnToFile(addOn) {
939
1130
  const pathFn = this.config.endpoints.addOnDetail;
940
- const path = pathFn(addOn.id);
941
- const detail = await this.apiRequest(path);
1131
+ const apiPath = pathFn(addOn.id);
1132
+ const detail = await this.apiRequest(apiPath);
942
1133
  if (!detail) return null;
943
1134
  let url = "";
944
1135
  let mediaType = "video";
@@ -953,53 +1144,32 @@ var LessonsChurchProvider = class extends ContentProvider {
953
1144
  const fileType = file.fileType;
954
1145
  mediaType = fileType?.startsWith("video/") ? "video" : "image";
955
1146
  } else {
956
- return null;
957
- }
958
- return {
959
- type: "file",
960
- id: addOn.id,
961
- title: addOn.name,
962
- mediaType,
963
- image: addOn.image,
964
- url,
965
- embedUrl: `https://lessons.church/embed/addon/${addOn.id}`,
966
- providerData: {
967
- seconds,
968
- loopVideo: video?.loopVideo || false
969
- }
970
- };
1147
+ return null;
1148
+ }
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 } };
971
1150
  }
972
- async getPresentations(folder, _auth, resolution) {
973
- const venueId = folder.providerData?.venueId;
1151
+ async getPresentations(path, _auth) {
1152
+ const venueId = getSegment(path, 4);
974
1153
  if (!venueId) return null;
975
- let path = `/venues/public/feed/${venueId}`;
976
- if (resolution) path += `?resolution=${resolution}`;
977
- const venueData = await this.apiRequest(path);
1154
+ const apiPath = `/venues/public/feed/${venueId}`;
1155
+ const venueData = await this.apiRequest(apiPath);
978
1156
  if (!venueData) return null;
979
1157
  return this.convertVenueToPlan(venueData);
980
1158
  }
981
- async getInstructions(folder, _auth) {
982
- const venueId = folder.providerData?.venueId;
1159
+ async getInstructions(path, _auth) {
1160
+ const venueId = getSegment(path, 4);
983
1161
  if (!venueId) return null;
984
1162
  const response = await this.apiRequest(`/venues/public/planItems/${venueId}`);
985
1163
  if (!response) return null;
986
- const processItem = (item) => ({
987
- id: item.id,
988
- itemType: item.itemType,
989
- relatedId: item.relatedId,
990
- label: item.label,
991
- description: item.description,
992
- seconds: item.seconds,
993
- children: item.children?.map(processItem),
994
- embedUrl: this.getEmbedUrl(item.itemType, item.relatedId)
995
- });
996
- return {
997
- venueName: response.venueName,
998
- 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) };
999
1168
  };
1169
+ return { venueName: response.venueName, items: (response.items || []).map(processItem) };
1000
1170
  }
1001
- async getExpandedInstructions(folder, _auth) {
1002
- const venueId = folder.providerData?.venueId;
1171
+ async getExpandedInstructions(path, _auth) {
1172
+ const venueId = getSegment(path, 4);
1003
1173
  if (!venueId) return null;
1004
1174
  const [planItemsResponse, actionsResponse] = await Promise.all([
1005
1175
  this.apiRequest(`/venues/public/planItems/${venueId}`),
@@ -1010,66 +1180,47 @@ var LessonsChurchProvider = class extends ContentProvider {
1010
1180
  if (actionsResponse?.sections) {
1011
1181
  for (const section of actionsResponse.sections) {
1012
1182
  if (section.id && section.actions) {
1013
- sectionActionsMap.set(section.id, section.actions.map((action) => ({
1014
- id: action.id,
1015
- itemType: "lessonAction",
1016
- relatedId: action.id,
1017
- label: action.name,
1018
- description: action.actionType,
1019
- seconds: action.seconds,
1020
- embedUrl: this.getEmbedUrl("lessonAction", action.id)
1021
- })));
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
+ }));
1022
1187
  }
1023
1188
  }
1024
1189
  }
1025
1190
  const processItem = (item) => {
1026
1191
  const relatedId = item.relatedId;
1027
- const itemType = item.itemType;
1192
+ const itemType = this.normalizeItemType(item.itemType);
1028
1193
  const children = item.children;
1029
1194
  let processedChildren;
1030
1195
  if (children) {
1031
1196
  processedChildren = children.map((child) => {
1032
1197
  const childRelatedId = child.relatedId;
1198
+ const childItemType = this.normalizeItemType(child.itemType);
1033
1199
  if (childRelatedId && sectionActionsMap.has(childRelatedId)) {
1034
- return {
1035
- id: child.id,
1036
- itemType: child.itemType,
1037
- relatedId: childRelatedId,
1038
- label: child.label,
1039
- description: child.description,
1040
- seconds: child.seconds,
1041
- children: sectionActionsMap.get(childRelatedId),
1042
- embedUrl: this.getEmbedUrl(child.itemType, childRelatedId)
1043
- };
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) };
1044
1201
  }
1045
1202
  return processItem(child);
1046
1203
  });
1047
1204
  }
1048
- return {
1049
- id: item.id,
1050
- itemType,
1051
- relatedId,
1052
- label: item.label,
1053
- description: item.description,
1054
- seconds: item.seconds,
1055
- children: processedChildren,
1056
- embedUrl: this.getEmbedUrl(itemType, relatedId)
1057
- };
1058
- };
1059
- return {
1060
- venueName: planItemsResponse.venueName,
1061
- 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) };
1062
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;
1063
1214
  }
1064
1215
  getEmbedUrl(itemType, relatedId) {
1065
1216
  if (!relatedId) return void 0;
1066
1217
  const baseUrl = "https://lessons.church";
1067
1218
  switch (itemType) {
1068
- case "lessonAction":
1219
+ case "action":
1069
1220
  return `${baseUrl}/embed/action/${relatedId}`;
1070
- case "lessonAddOn":
1221
+ case "addon":
1071
1222
  return `${baseUrl}/embed/addon/${relatedId}`;
1072
- case "lessonSection":
1223
+ case "section":
1073
1224
  return `${baseUrl}/embed/section/${relatedId}`;
1074
1225
  default:
1075
1226
  return void 0;
@@ -1087,16 +1238,7 @@ var LessonsChurchProvider = class extends ContentProvider {
1087
1238
  for (const file of action.files || []) {
1088
1239
  if (!file.url) continue;
1089
1240
  const embedUrl = action.id ? `https://lessons.church/embed/action/${action.id}` : void 0;
1090
- const contentFile = {
1091
- type: "file",
1092
- id: file.id || "",
1093
- title: file.name || "",
1094
- mediaType: detectMediaType(file.url, file.fileType),
1095
- image: venue.lessonImage,
1096
- url: file.url,
1097
- embedUrl,
1098
- providerData: { seconds: file.seconds, streamUrl: file.streamUrl }
1099
- };
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 } };
1100
1242
  files.push(contentFile);
1101
1243
  allFiles.push(contentFile);
1102
1244
  }
@@ -1108,51 +1250,69 @@ var LessonsChurchProvider = class extends ContentProvider {
1108
1250
  sections.push({ id: section.id || "", name: section.name || "Untitled Section", presentations });
1109
1251
  }
1110
1252
  }
1111
- return {
1112
- id: venue.id || "",
1113
- name: venue.lessonName || venue.name || "Plan",
1114
- description: venue.lessonDescription,
1115
- image: venue.lessonImage,
1116
- sections,
1117
- allFiles
1118
- };
1253
+ return { id: venue.id || "", name: venue.lessonName || venue.name || "Plan", description: venue.lessonDescription, image: venue.lessonImage, sections, allFiles };
1119
1254
  }
1120
1255
  };
1121
1256
 
1122
- // src/providers/b1church/auth.ts
1123
- 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);
1124
1272
  const oauthParams = new URLSearchParams({
1273
+ response_type: "code",
1125
1274
  client_id: config.clientId,
1126
1275
  redirect_uri: redirectUri,
1127
- response_type: "code",
1128
- scope: config.scopes.join(" ")
1276
+ scope: config.scopes.join(" "),
1277
+ code_challenge: codeChallenge,
1278
+ code_challenge_method: "S256",
1279
+ state: state || ""
1129
1280
  });
1130
- if (state) {
1131
- 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;
1132
1305
  }
1133
- const returnUrl = `/oauth?${oauthParams.toString()}`;
1134
- const url = `${appBase}/login?returnUrl=${encodeURIComponent(returnUrl)}`;
1135
- return { url, challengeMethod: "none" };
1136
1306
  }
1137
1307
  async function exchangeCodeForTokensWithSecret(config, code, redirectUri, clientSecret) {
1138
1308
  try {
1139
- const params = {
1140
- grant_type: "authorization_code",
1141
- code,
1142
- client_id: config.clientId,
1143
- client_secret: clientSecret,
1144
- redirect_uri: redirectUri
1145
- };
1309
+ const params = { grant_type: "authorization_code", code, client_id: config.clientId, client_secret: clientSecret, redirect_uri: redirectUri };
1146
1310
  const tokenUrl = `${config.oauthBase}/token`;
1147
1311
  console.log(`B1Church token exchange request to: ${tokenUrl}`);
1148
1312
  console.log(` - client_id: ${config.clientId}`);
1149
1313
  console.log(` - redirect_uri: ${redirectUri}`);
1150
1314
  console.log(` - code: ${code.substring(0, 10)}...`);
1151
- const response = await fetch(tokenUrl, {
1152
- method: "POST",
1153
- headers: { "Content-Type": "application/json" },
1154
- body: JSON.stringify(params)
1155
- });
1315
+ const response = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params) });
1156
1316
  console.log(`B1Church token response status: ${response.status}`);
1157
1317
  if (!response.ok) {
1158
1318
  const errorText = await response.text();
@@ -1161,14 +1321,7 @@ async function exchangeCodeForTokensWithSecret(config, code, redirectUri, client
1161
1321
  }
1162
1322
  const data = await response.json();
1163
1323
  console.log(`B1Church token exchange successful, got access_token: ${!!data.access_token}`);
1164
- return {
1165
- access_token: data.access_token,
1166
- refresh_token: data.refresh_token,
1167
- token_type: data.token_type || "Bearer",
1168
- created_at: Math.floor(Date.now() / 1e3),
1169
- expires_in: data.expires_in,
1170
- scope: data.scope || config.scopes.join(" ")
1171
- };
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(" ") };
1172
1325
  } catch (error) {
1173
1326
  console.error("B1Church token exchange error:", error);
1174
1327
  return null;
@@ -1177,27 +1330,11 @@ async function exchangeCodeForTokensWithSecret(config, code, redirectUri, client
1177
1330
  async function refreshTokenWithSecret(config, auth, clientSecret) {
1178
1331
  if (!auth.refresh_token) return null;
1179
1332
  try {
1180
- const params = {
1181
- grant_type: "refresh_token",
1182
- refresh_token: auth.refresh_token,
1183
- client_id: config.clientId,
1184
- client_secret: clientSecret
1185
- };
1186
- const response = await fetch(`${config.oauthBase}/token`, {
1187
- method: "POST",
1188
- headers: { "Content-Type": "application/json" },
1189
- body: JSON.stringify(params)
1190
- });
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) });
1191
1335
  if (!response.ok) return null;
1192
1336
  const data = await response.json();
1193
- return {
1194
- access_token: data.access_token,
1195
- refresh_token: data.refresh_token || auth.refresh_token,
1196
- token_type: data.token_type || "Bearer",
1197
- created_at: Math.floor(Date.now() / 1e3),
1198
- expires_in: data.expires_in,
1199
- scope: data.scope || auth.scope
1200
- };
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 };
1201
1338
  } catch {
1202
1339
  return null;
1203
1340
  }
@@ -1205,14 +1342,7 @@ async function refreshTokenWithSecret(config, auth, clientSecret) {
1205
1342
  async function initiateDeviceFlow(config) {
1206
1343
  if (!config.supportsDeviceFlow || !config.deviceAuthEndpoint) return null;
1207
1344
  try {
1208
- const response = await fetch(`${config.oauthBase}${config.deviceAuthEndpoint}`, {
1209
- method: "POST",
1210
- headers: { "Content-Type": "application/json" },
1211
- body: JSON.stringify({
1212
- client_id: config.clientId,
1213
- scope: config.scopes.join(" ")
1214
- })
1215
- });
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(" ") }) });
1216
1346
  if (!response.ok) {
1217
1347
  const errorText = await response.text();
1218
1348
  console.error(`B1Church device authorize failed: ${response.status} - ${errorText}`);
@@ -1226,25 +1356,10 @@ async function initiateDeviceFlow(config) {
1226
1356
  }
1227
1357
  async function pollDeviceFlowToken(config, deviceCode) {
1228
1358
  try {
1229
- const response = await fetch(`${config.oauthBase}/token`, {
1230
- method: "POST",
1231
- headers: { "Content-Type": "application/json" },
1232
- body: JSON.stringify({
1233
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1234
- device_code: deviceCode,
1235
- client_id: config.clientId
1236
- })
1237
- });
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 }) });
1238
1360
  if (response.ok) {
1239
1361
  const data = await response.json();
1240
- return {
1241
- access_token: data.access_token,
1242
- refresh_token: data.refresh_token,
1243
- token_type: data.token_type || "Bearer",
1244
- created_at: Math.floor(Date.now() / 1e3),
1245
- expires_in: data.expires_in,
1246
- scope: data.scope || config.scopes.join(" ")
1247
- };
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(" ") };
1248
1363
  }
1249
1364
  const errorData = await response.json();
1250
1365
  switch (errorData.error) {
@@ -1264,7 +1379,7 @@ async function pollDeviceFlowToken(config, deviceCode) {
1264
1379
  }
1265
1380
  }
1266
1381
 
1267
- // src/providers/b1church/api.ts
1382
+ // src/providers/b1Church/api.ts
1268
1383
  var API_BASE = "https://api.churchapps.org";
1269
1384
  var LESSONS_API_BASE = "https://api.lessons.church";
1270
1385
  var CONTENT_API_BASE = "https://contentapi.churchapps.org";
@@ -1296,10 +1411,7 @@ async function fetchPlans(planTypeId, auth) {
1296
1411
  async function fetchVenueFeed(venueId) {
1297
1412
  try {
1298
1413
  const url = `${LESSONS_API_BASE}/venues/public/feed/${venueId}`;
1299
- const response = await fetch(url, {
1300
- method: "GET",
1301
- headers: { Accept: "application/json" }
1302
- });
1414
+ const response = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
1303
1415
  if (!response.ok) return null;
1304
1416
  return await response.json();
1305
1417
  } catch {
@@ -1309,9 +1421,29 @@ async function fetchVenueFeed(venueId) {
1309
1421
  async function fetchArrangementKey(churchId, arrangementId) {
1310
1422
  try {
1311
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;
1312
1443
  const response = await fetch(url, {
1313
- method: "GET",
1314
- headers: { Accept: "application/json" }
1444
+ method: "POST",
1445
+ headers,
1446
+ body: JSON.stringify(body)
1315
1447
  });
1316
1448
  if (!response.ok) return null;
1317
1449
  return await response.json();
@@ -1320,107 +1452,39 @@ async function fetchArrangementKey(churchId, arrangementId) {
1320
1452
  }
1321
1453
  }
1322
1454
 
1323
- // src/providers/b1church/converters.ts
1455
+ // src/providers/b1Church/converters.ts
1324
1456
  function ministryToFolder(ministry) {
1325
- return {
1326
- type: "folder",
1327
- id: ministry.id,
1328
- title: ministry.name,
1329
- image: ministry.photoUrl,
1330
- providerData: {
1331
- level: "ministry",
1332
- ministryId: ministry.id,
1333
- churchId: ministry.churchId
1334
- }
1335
- };
1457
+ return { type: "folder", id: ministry.id, title: ministry.name, path: "", image: ministry.photoUrl, providerData: { level: "ministry", ministryId: ministry.id, churchId: ministry.churchId } };
1336
1458
  }
1337
1459
  function planTypeToFolder(planType, ministryId) {
1338
- return {
1339
- type: "folder",
1340
- id: planType.id,
1341
- title: planType.name,
1342
- providerData: {
1343
- level: "planType",
1344
- planTypeId: planType.id,
1345
- ministryId,
1346
- churchId: planType.churchId
1347
- }
1348
- };
1460
+ return { type: "folder", id: planType.id, title: planType.name, path: "", providerData: { level: "planType", planTypeId: planType.id, ministryId, churchId: planType.churchId } };
1349
1461
  }
1350
1462
  function planToFolder(plan) {
1351
- return {
1352
- type: "folder",
1353
- id: plan.id,
1354
- title: plan.name,
1355
- providerData: {
1356
- isLeaf: true,
1357
- level: "plan",
1358
- planId: plan.id,
1359
- planTypeId: plan.planTypeId,
1360
- ministryId: plan.ministryId,
1361
- churchId: plan.churchId,
1362
- serviceDate: plan.serviceDate,
1363
- contentType: plan.contentType,
1364
- contentId: plan.contentId
1365
- }
1366
- };
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 } };
1367
1464
  }
1368
1465
  async function planItemToPresentation(item, venueFeed) {
1369
1466
  const itemType = item.itemType;
1370
1467
  if (itemType === "arrangementKey" && item.churchId && item.relatedId) {
1371
1468
  const songData = await fetchArrangementKey(item.churchId, item.relatedId);
1372
- if (songData) {
1373
- return arrangementToPresentation(item, songData);
1374
- }
1469
+ if (songData) return arrangementToPresentation(item, songData);
1375
1470
  }
1376
- if ((itemType === "lessonSection" || itemType === "lessonAction" || itemType === "lessonAddOn") && venueFeed) {
1471
+ if ((itemType === "lessonSection" || itemType === "section" || itemType === "lessonAction" || itemType === "action" || itemType === "lessonAddOn" || itemType === "addon") && venueFeed) {
1377
1472
  const files = getFilesFromVenueFeed(venueFeed, itemType, item.relatedId);
1378
- if (files.length > 0) {
1379
- return {
1380
- id: item.id,
1381
- name: item.label || "Lesson Content",
1382
- actionType: itemType === "lessonAddOn" ? "add-on" : "play",
1383
- files
1384
- };
1385
- }
1473
+ if (files.length > 0) return { id: item.id, name: item.label || "Lesson Content", actionType: itemType === "lessonAddOn" || itemType === "addon" ? "add-on" : "play", files };
1386
1474
  }
1387
1475
  if (itemType === "item" || itemType === "header") {
1388
- return {
1389
- id: item.id,
1390
- name: item.label || "",
1391
- actionType: "other",
1392
- files: [],
1393
- providerData: {
1394
- itemType,
1395
- description: item.description,
1396
- seconds: item.seconds
1397
- }
1398
- };
1476
+ return { id: item.id, name: item.label || "", actionType: "other", files: [], providerData: { itemType, description: item.description, seconds: item.seconds } };
1399
1477
  }
1400
1478
  return null;
1401
1479
  }
1402
1480
  function arrangementToPresentation(item, songData) {
1403
1481
  const title = songData.songDetail?.title || item.label || "Song";
1404
- return {
1405
- id: item.id,
1406
- name: title,
1407
- actionType: "other",
1408
- files: [],
1409
- providerData: {
1410
- itemType: "song",
1411
- title,
1412
- artist: songData.songDetail?.artist,
1413
- lyrics: songData.arrangement?.lyrics,
1414
- keySignature: songData.arrangementKey?.keySignature,
1415
- arrangementName: songData.arrangement?.name,
1416
- seconds: songData.songDetail?.seconds || item.seconds
1417
- }
1418
- };
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 } };
1419
1483
  }
1420
1484
  function getFilesFromVenueFeed(venueFeed, itemType, relatedId) {
1421
1485
  const files = [];
1422
1486
  if (!relatedId) return files;
1423
- if (itemType === "lessonSection") {
1487
+ if (itemType === "lessonSection" || itemType === "section") {
1424
1488
  for (const section of venueFeed.sections || []) {
1425
1489
  if (section.id === relatedId) {
1426
1490
  for (const action of section.actions || []) {
@@ -1432,7 +1496,7 @@ function getFilesFromVenueFeed(venueFeed, itemType, relatedId) {
1432
1496
  break;
1433
1497
  }
1434
1498
  }
1435
- } else if (itemType === "lessonAction") {
1499
+ } else if (itemType === "lessonAction" || itemType === "action") {
1436
1500
  for (const section of venueFeed.sections || []) {
1437
1501
  for (const action of section.actions || []) {
1438
1502
  if (action.id === relatedId) {
@@ -1445,75 +1509,51 @@ function getFilesFromVenueFeed(venueFeed, itemType, relatedId) {
1445
1509
  return files;
1446
1510
  }
1447
1511
  function convertFeedFiles(feedFiles, thumbnailImage) {
1448
- return feedFiles.filter((f) => f.url).map((f) => ({
1449
- type: "file",
1450
- id: f.id || "",
1451
- title: f.name || "",
1452
- mediaType: detectMediaType(f.url || "", f.fileType),
1453
- image: thumbnailImage,
1454
- url: f.url || "",
1455
- providerData: { seconds: f.seconds, streamUrl: f.streamUrl }
1456
- }));
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 } }));
1457
1513
  }
1458
1514
  function planItemToInstruction(item) {
1459
- return {
1460
- id: item.id,
1461
- itemType: item.itemType,
1462
- relatedId: item.relatedId,
1463
- label: item.label,
1464
- description: item.description,
1465
- seconds: item.seconds,
1466
- children: item.children?.map(planItemToInstruction)
1467
- };
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) };
1468
1528
  }
1469
1529
 
1470
- // src/providers/b1church/B1ChurchProvider.ts
1471
- 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 {
1472
1538
  constructor() {
1473
- super(...arguments);
1539
+ this.apiHelper = new ApiHelper();
1474
1540
  this.id = "b1church";
1475
1541
  this.name = "B1.Church";
1476
- this.logos = {
1477
- light: "https://b1.church/b1-church-logo.png",
1478
- dark: "https://b1.church/b1-church-logo.png"
1479
- };
1480
- this.config = {
1481
- id: "b1church",
1482
- name: "B1.Church",
1483
- apiBase: `${API_BASE}/doing`,
1484
- oauthBase: `${API_BASE}/membership/oauth`,
1485
- clientId: "",
1486
- // Consumer must provide client_id
1487
- scopes: ["plans"],
1488
- supportsDeviceFlow: true,
1489
- deviceAuthEndpoint: "/device/authorize",
1490
- endpoints: {
1491
- planItems: (churchId, planId) => `/planItems/presenter/${churchId}/${planId}`
1492
- }
1493
- };
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}` } };
1494
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 };
1495
1548
  }
1496
- // ============================================================
1497
- // Provider Info
1498
- // ============================================================
1499
- requiresAuth() {
1500
- return true;
1549
+ async apiRequest(path, authData) {
1550
+ return this.apiHelper.apiRequest(this.config, this.id, path, authData);
1501
1551
  }
1502
- getCapabilities() {
1503
- return {
1504
- browse: true,
1505
- presentations: true,
1506
- playlist: true,
1507
- instructions: true,
1508
- expandedInstructions: true,
1509
- mediaLicensing: false
1510
- };
1552
+ async buildAuthUrl(codeVerifier, redirectUri, state) {
1553
+ return buildB1AuthUrl(this.config, this.appBase, redirectUri, codeVerifier, state);
1511
1554
  }
1512
- // ============================================================
1513
- // Authentication
1514
- // ============================================================
1515
- async buildAuthUrl(_codeVerifier, redirectUri, state) {
1516
- return buildB1AuthUrl(this.config, this.appBase, redirectUri, state);
1555
+ async exchangeCodeForTokensWithPKCE(code, redirectUri, codeVerifier) {
1556
+ return exchangeCodeForTokensWithPKCE(this.config, code, redirectUri, codeVerifier);
1517
1557
  }
1518
1558
  async exchangeCodeForTokensWithSecret(code, redirectUri, clientSecret) {
1519
1559
  return exchangeCodeForTokensWithSecret(this.config, code, redirectUri, clientSecret);
@@ -1527,48 +1567,69 @@ var B1ChurchProvider = class extends ContentProvider {
1527
1567
  async pollDeviceFlowToken(deviceCode) {
1528
1568
  return pollDeviceFlowToken(this.config, deviceCode);
1529
1569
  }
1530
- // ============================================================
1531
- // Content Browsing
1532
- // ============================================================
1533
- /**
1534
- * Browse content hierarchy:
1535
- * - Root: List of ministries (groups with "ministry" tag)
1536
- * - Ministry: List of plan types
1537
- * - PlanType: List of plans (leaf nodes)
1538
- *
1539
- * Plans are leaf nodes - use getPresentations(), getPlaylist(), getInstructions()
1540
- * to get plan content.
1541
- */
1542
- async browse(folder, authData) {
1543
- 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) {
1544
1583
  const ministries = await fetchMinistries(authData);
1545
- 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
+ });
1546
1589
  }
1547
- const level = folder.providerData?.level;
1548
- if (level === "ministry") {
1549
- const ministryId = folder.providerData?.ministryId;
1550
- if (!ministryId) return [];
1590
+ if (depth === 2) {
1591
+ const ministryId = segments[1];
1551
1592
  const planTypes = await fetchPlanTypes(ministryId, authData);
1552
- 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
+ });
1553
1598
  }
1554
- if (level === "planType") {
1555
- const planTypeId = folder.providerData?.planTypeId;
1556
- if (!planTypeId) return [];
1599
+ if (depth === 3) {
1600
+ const ministryId = segments[1];
1601
+ const planTypeId = segments[2];
1557
1602
  const plans = await fetchPlans(planTypeId, authData);
1558
- 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
+ });
1559
1612
  }
1560
1613
  return [];
1561
1614
  }
1562
- // ============================================================
1563
- // Presentations
1564
- // ============================================================
1565
- async getPresentations(folder, authData) {
1566
- const level = folder.providerData?.level;
1567
- if (level !== "plan") return null;
1568
- const planId = folder.providerData?.planId;
1569
- const churchId = folder.providerData?.churchId;
1570
- const venueId = folder.providerData?.contentId;
1571
- 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;
1572
1633
  const pathFn = this.config.endpoints.planItems;
1573
1634
  const planItems = await this.apiRequest(pathFn(churchId, planId), authData);
1574
1635
  if (!planItems || !Array.isArray(planItems)) return null;
@@ -1578,49 +1639,101 @@ var B1ChurchProvider = class extends ContentProvider {
1578
1639
  for (const sectionItem of planItems) {
1579
1640
  const presentations = [];
1580
1641
  for (const child of sectionItem.children || []) {
1581
- const presentation = await planItemToPresentation(child, venueFeed);
1582
- if (presentation) {
1583
- presentations.push(presentation);
1584
- 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
+ }
1585
1662
  }
1586
1663
  }
1587
1664
  if (presentations.length > 0 || sectionItem.label) {
1588
- sections.push({
1589
- id: sectionItem.id,
1590
- name: sectionItem.label || "Section",
1591
- presentations
1592
- });
1665
+ sections.push({ id: sectionItem.id, name: sectionItem.label || "Section", presentations });
1593
1666
  }
1594
1667
  }
1595
- return { id: planId, name: folder.title, sections, allFiles };
1596
- }
1597
- // ============================================================
1598
- // Instructions
1599
- // ============================================================
1600
- async getInstructions(folder, authData) {
1601
- const level = folder.providerData?.level;
1602
- if (level !== "plan") return null;
1603
- const planId = folder.providerData?.planId;
1604
- const churchId = folder.providerData?.churchId;
1605
- 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;
1606
1687
  const pathFn = this.config.endpoints.planItems;
1607
1688
  const planItems = await this.apiRequest(pathFn(churchId, planId), authData);
1608
1689
  if (!planItems || !Array.isArray(planItems)) return null;
1609
- return {
1610
- venueName: folder.title,
1611
- items: planItems.map(planItemToInstruction)
1612
- };
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;
1613
1719
  }
1614
- // ============================================================
1615
- // Playlist
1616
- // ============================================================
1617
- async getPlaylist(folder, authData) {
1618
- const level = folder.providerData?.level;
1619
- if (level !== "plan") return [];
1620
- const planId = folder.providerData?.planId;
1621
- const churchId = folder.providerData?.churchId;
1622
- const venueId = folder.providerData?.contentId;
1623
- 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 [];
1624
1737
  const pathFn = this.config.endpoints.planItems;
1625
1738
  const planItems = await this.apiRequest(pathFn(churchId, planId), authData);
1626
1739
  if (!planItems || !Array.isArray(planItems)) return [];
@@ -1628,10 +1741,24 @@ var B1ChurchProvider = class extends ContentProvider {
1628
1741
  const files = [];
1629
1742
  for (const sectionItem of planItems) {
1630
1743
  for (const child of sectionItem.children || []) {
1631
- const itemType = child.itemType;
1632
- if ((itemType === "lessonSection" || itemType === "lessonAction" || itemType === "lessonAddOn") && venueFeed) {
1633
- const itemFiles = getFilesFromVenueFeed(venueFeed, itemType, child.relatedId);
1634
- 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
+ }
1635
1762
  }
1636
1763
  }
1637
1764
  }
@@ -1639,81 +1766,62 @@ var B1ChurchProvider = class extends ContentProvider {
1639
1766
  }
1640
1767
  };
1641
1768
 
1642
- // src/providers/PlanningCenterProvider.ts
1643
- var PlanningCenterProvider = class extends ContentProvider {
1769
+ // src/providers/planningCenter/PlanningCenterProvider.ts
1770
+ var PlanningCenterProvider = class {
1644
1771
  constructor() {
1645
- super(...arguments);
1772
+ this.apiHelper = new ApiHelper();
1646
1773
  this.id = "planningcenter";
1647
1774
  this.name = "Planning Center";
1648
- this.logos = {
1649
- light: "https://www.planningcenter.com/icons/icon-512x512.png",
1650
- dark: "https://www.planningcenter.com/icons/icon-512x512.png"
1651
- };
1652
- // Planning Center uses OAuth 2.0 with PKCE (handled by base ContentProvider class)
1653
- this.config = {
1654
- id: "planningcenter",
1655
- name: "Planning Center",
1656
- apiBase: "https://api.planningcenteronline.com",
1657
- oauthBase: "https://api.planningcenteronline.com/oauth",
1658
- clientId: "",
1659
- // Consumer must provide client_id
1660
- scopes: ["services"],
1661
- endpoints: {
1662
- serviceTypes: "/services/v2/service_types",
1663
- plans: (serviceTypeId) => `/services/v2/service_types/${serviceTypeId}/plans`,
1664
- planItems: (serviceTypeId, planId) => `/services/v2/service_types/${serviceTypeId}/plans/${planId}/items`,
1665
- song: (itemId) => `/services/v2/songs/${itemId}`,
1666
- arrangement: (songId, arrangementId) => `/services/v2/songs/${songId}/arrangements/${arrangementId}`,
1667
- arrangementSections: (songId, arrangementId) => `/services/v2/songs/${songId}/arrangements/${arrangementId}/sections`,
1668
- media: (mediaId) => `/services/v2/media/${mediaId}`,
1669
- mediaAttachments: (mediaId) => `/services/v2/media/${mediaId}/attachments`
1670
- }
1671
- };
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` } };
1672
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 };
1673
1781
  }
1674
- requiresAuth() {
1675
- return true;
1676
- }
1677
- getCapabilities() {
1678
- return {
1679
- browse: true,
1680
- presentations: true,
1681
- playlist: false,
1682
- instructions: false,
1683
- expandedInstructions: false,
1684
- mediaLicensing: false
1685
- };
1782
+ async apiRequest(path, auth) {
1783
+ return this.apiHelper.apiRequest(this.config, this.id, path, auth);
1686
1784
  }
1687
- async browse(folder, auth) {
1688
- if (!folder) {
1689
- const response = await this.apiRequest(
1690
- this.config.endpoints.serviceTypes,
1691
- auth
1692
- );
1693
- if (!response?.data) return [];
1694
- return response.data.map((serviceType) => ({
1785
+ async browse(path, auth) {
1786
+ const { segments, depth } = parsePath(path);
1787
+ if (depth === 0) {
1788
+ return [{
1695
1789
  type: "folder",
1696
- id: serviceType.id,
1697
- title: serviceType.attributes.name,
1698
- providerData: {
1699
- level: "serviceType",
1700
- serviceTypeId: serviceType.id
1701
- }
1702
- }));
1790
+ id: "serviceTypes-root",
1791
+ title: "Service Types",
1792
+ path: "/serviceTypes"
1793
+ }];
1703
1794
  }
1704
- const level = folder.providerData?.level;
1705
- switch (level) {
1706
- case "serviceType":
1707
- return this.getPlans(folder, auth);
1708
- case "plan":
1709
- return this.getPlanItems(folder, auth);
1710
- default:
1711
- return [];
1795
+ const root = segments[0];
1796
+ if (root !== "serviceTypes") return [];
1797
+ if (depth === 1) {
1798
+ return this.getServiceTypes(auth);
1712
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
+ }));
1713
1823
  }
1714
- async getPlans(folder, auth) {
1715
- const serviceTypeId = folder.providerData?.serviceTypeId;
1716
- if (!serviceTypeId) return [];
1824
+ async getPlans(serviceTypeId, currentPath, auth) {
1717
1825
  const pathFn = this.config.endpoints.plans;
1718
1826
  const response = await this.apiRequest(
1719
1827
  `${pathFn(serviceTypeId)}?filter=future&order=sort_date`,
@@ -1730,73 +1838,46 @@ var PlanningCenterProvider = class extends ContentProvider {
1730
1838
  type: "folder",
1731
1839
  id: plan.id,
1732
1840
  title: plan.attributes.title || this.formatDate(plan.attributes.sort_date),
1733
- providerData: {
1734
- level: "plan",
1735
- serviceTypeId,
1736
- planId: plan.id,
1737
- sortDate: plan.attributes.sort_date
1738
- }
1841
+ isLeaf: true,
1842
+ path: `${currentPath}/${plan.id}`,
1843
+ providerData: { sortDate: plan.attributes.sort_date }
1739
1844
  }));
1740
1845
  }
1741
- async getPlanItems(folder, auth) {
1742
- const serviceTypeId = folder.providerData?.serviceTypeId;
1743
- const planId = folder.providerData?.planId;
1744
- if (!serviceTypeId || !planId) return [];
1846
+ async getPlanItems(serviceTypeId, planId, auth) {
1745
1847
  const pathFn = this.config.endpoints.planItems;
1746
1848
  const response = await this.apiRequest(
1747
1849
  `${pathFn(serviceTypeId, planId)}?per_page=100`,
1748
1850
  auth
1749
1851
  );
1750
1852
  if (!response?.data) return [];
1751
- return response.data.map((item) => ({
1752
- type: "file",
1753
- id: item.id,
1754
- title: item.attributes.title || "",
1755
- mediaType: "image",
1756
- url: "",
1757
- providerData: {
1758
- itemType: item.attributes.item_type,
1759
- description: item.attributes.description,
1760
- length: item.attributes.length,
1761
- songId: item.relationships?.song?.data?.id,
1762
- arrangementId: item.relationships?.arrangement?.data?.id
1763
- }
1764
- }));
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 } }));
1765
1854
  }
1766
- async getPresentations(folder, auth) {
1767
- const level = folder.providerData?.level;
1768
- if (level !== "plan") return null;
1769
- const serviceTypeId = folder.providerData?.serviceTypeId;
1770
- const planId = folder.providerData?.planId;
1771
- 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];
1772
1860
  const pathFn = this.config.endpoints.planItems;
1773
1861
  const response = await this.apiRequest(
1774
1862
  `${pathFn(serviceTypeId, planId)}?per_page=100`,
1775
1863
  auth
1776
1864
  );
1777
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";
1778
1869
  const sections = [];
1779
1870
  const allFiles = [];
1780
1871
  let currentSection = null;
1781
1872
  for (const item of response.data) {
1782
1873
  const itemType = item.attributes.item_type;
1783
1874
  if (itemType === "header") {
1784
- if (currentSection && currentSection.presentations.length > 0) {
1785
- sections.push(currentSection);
1786
- }
1787
- currentSection = {
1788
- id: item.id,
1789
- name: item.attributes.title || "Section",
1790
- presentations: []
1791
- };
1875
+ if (currentSection && currentSection.presentations.length > 0) sections.push(currentSection);
1876
+ currentSection = { id: item.id, name: item.attributes.title || "Section", presentations: [] };
1792
1877
  continue;
1793
1878
  }
1794
1879
  if (!currentSection) {
1795
- currentSection = {
1796
- id: `default-${planId}`,
1797
- name: "Service",
1798
- presentations: []
1799
- };
1880
+ currentSection = { id: `default-${planId}`, name: "Service", presentations: [] };
1800
1881
  }
1801
1882
  const presentation = await this.convertToPresentation(item, auth);
1802
1883
  if (presentation) {
@@ -1807,12 +1888,7 @@ var PlanningCenterProvider = class extends ContentProvider {
1807
1888
  if (currentSection && currentSection.presentations.length > 0) {
1808
1889
  sections.push(currentSection);
1809
1890
  }
1810
- return {
1811
- id: planId,
1812
- name: folder.title,
1813
- sections,
1814
- allFiles
1815
- };
1891
+ return { id: planId, name: planTitle, sections, allFiles };
1816
1892
  }
1817
1893
  async convertToPresentation(item, auth) {
1818
1894
  const itemType = item.attributes.item_type;
@@ -1823,17 +1899,7 @@ var PlanningCenterProvider = class extends ContentProvider {
1823
1899
  return this.convertMediaToPresentation(item, auth);
1824
1900
  }
1825
1901
  if (itemType === "item") {
1826
- return {
1827
- id: item.id,
1828
- name: item.attributes.title || "",
1829
- actionType: "other",
1830
- files: [],
1831
- providerData: {
1832
- itemType: "item",
1833
- description: item.attributes.description,
1834
- length: item.attributes.length
1835
- }
1836
- };
1902
+ return { id: item.id, name: item.attributes.title || "", actionType: "other", files: [], providerData: { itemType: "item", description: item.attributes.description, length: item.attributes.length } };
1837
1903
  }
1838
1904
  return null;
1839
1905
  }
@@ -1841,13 +1907,7 @@ var PlanningCenterProvider = class extends ContentProvider {
1841
1907
  const songId = item.relationships?.song?.data?.id;
1842
1908
  const arrangementId = item.relationships?.arrangement?.data?.id;
1843
1909
  if (!songId) {
1844
- return {
1845
- id: item.id,
1846
- name: item.attributes.title || "Song",
1847
- actionType: "other",
1848
- files: [],
1849
- providerData: { itemType: "song" }
1850
- };
1910
+ return { id: item.id, name: item.attributes.title || "Song", actionType: "other", files: [], providerData: { itemType: "song" } };
1851
1911
  }
1852
1912
  const songFn = this.config.endpoints.song;
1853
1913
  const songResponse = await this.apiRequest(songFn(songId), auth);
@@ -1869,25 +1929,7 @@ var PlanningCenterProvider = class extends ContentProvider {
1869
1929
  }
1870
1930
  const song = songResponse?.data;
1871
1931
  const title = song?.attributes?.title || item.attributes.title || "Song";
1872
- return {
1873
- id: item.id,
1874
- name: title,
1875
- actionType: "other",
1876
- files: [],
1877
- providerData: {
1878
- itemType: "song",
1879
- title,
1880
- author: song?.attributes?.author,
1881
- copyright: song?.attributes?.copyright,
1882
- ccliNumber: song?.attributes?.ccli_number,
1883
- arrangementName: arrangement?.attributes?.name,
1884
- keySignature: arrangement?.attributes?.chord_chart_key,
1885
- bpm: arrangement?.attributes?.bpm,
1886
- sequence: arrangement?.attributes?.sequence,
1887
- sections: sections.map((s) => ({ label: s.label, lyrics: s.lyrics })),
1888
- length: item.attributes.length
1889
- }
1890
- };
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 } };
1891
1933
  }
1892
1934
  async convertMediaToPresentation(item, auth) {
1893
1935
  const files = [];
@@ -1907,25 +1949,10 @@ var PlanningCenterProvider = class extends ContentProvider {
1907
1949
  if (!url) continue;
1908
1950
  const contentType = attachment.attributes.content_type;
1909
1951
  const explicitType = contentType?.startsWith("video/") ? "video" : void 0;
1910
- files.push({
1911
- type: "file",
1912
- id: attachment.id,
1913
- title: attachment.attributes.filename,
1914
- mediaType: detectMediaType(url, explicitType),
1915
- url
1916
- });
1952
+ files.push({ type: "file", id: attachment.id, title: attachment.attributes.filename, mediaType: detectMediaType(url, explicitType), url });
1917
1953
  }
1918
1954
  }
1919
- return {
1920
- id: item.id,
1921
- name: item.attributes.title || "Media",
1922
- actionType: "play",
1923
- files,
1924
- providerData: {
1925
- itemType: "media",
1926
- length: item.attributes.length
1927
- }
1928
- };
1955
+ return { id: item.id, name: item.attributes.title || "Media", actionType: "play", files, providerData: { itemType: "media", length: item.attributes.length } };
1929
1956
  }
1930
1957
  formatDate(dateString) {
1931
1958
  const date = new Date(dateString);
@@ -1933,7 +1960,7 @@ var PlanningCenterProvider = class extends ContentProvider {
1933
1960
  }
1934
1961
  };
1935
1962
 
1936
- // src/providers/bibleproject/data.json
1963
+ // src/providers/bibleProject/data.json
1937
1964
  var data_default = {
1938
1965
  collections: [
1939
1966
  {
@@ -4041,10 +4068,9 @@ var data_default = {
4041
4068
  ]
4042
4069
  };
4043
4070
 
4044
- // src/providers/bibleproject/BibleProjectProvider.ts
4045
- var BibleProjectProvider = class extends ContentProvider {
4071
+ // src/providers/bibleProject/BibleProjectProvider.ts
4072
+ var BibleProjectProvider = class {
4046
4073
  constructor() {
4047
- super(...arguments);
4048
4074
  this.id = "bibleproject";
4049
4075
  this.name = "The Bible Project";
4050
4076
  this.logos = {
@@ -4063,74 +4089,109 @@ var BibleProjectProvider = class extends ContentProvider {
4063
4089
  }
4064
4090
  };
4065
4091
  this.data = data_default;
4066
- }
4067
- requiresAuth() {
4068
- return false;
4069
- }
4070
- getCapabilities() {
4071
- return {
4092
+ this.requiresAuth = false;
4093
+ this.authTypes = ["none"];
4094
+ this.capabilities = {
4072
4095
  browse: true,
4073
- presentations: false,
4074
- playlist: false,
4096
+ presentations: true,
4097
+ playlist: true,
4075
4098
  instructions: false,
4076
4099
  expandedInstructions: false,
4077
4100
  mediaLicensing: false
4078
4101
  };
4079
4102
  }
4080
- async browse(folder, _auth) {
4081
- if (!folder) {
4082
- return this.data.collections.filter((collection) => collection.videos.length > 0).map((collection) => this.createFolder(
4083
- this.slugify(collection.name),
4084
- collection.name,
4085
- collection.image || void 0,
4086
- { level: "collection", collectionName: collection.name }
4087
- ));
4103
+ async browse(path, _auth) {
4104
+ const { segments, depth } = parsePath(path);
4105
+ if (depth === 0) {
4106
+ return this.getCollections();
4088
4107
  }
4089
- const level = folder.providerData?.level;
4090
- const collectionName = folder.providerData?.collectionName;
4091
- if (level === "collection") {
4092
- return this.getLessonFolders(collectionName);
4108
+ if (depth === 1) {
4109
+ const collectionSlug = segments[0];
4110
+ return this.getLessonFolders(collectionSlug, path);
4093
4111
  }
4094
- if (level === "lesson") {
4095
- const videoData = folder.providerData?.videoData;
4096
- if (videoData) {
4097
- return [this.createFile(
4098
- videoData.id,
4099
- videoData.title,
4100
- videoData.videoUrl,
4101
- {
4102
- mediaType: "video",
4103
- muxPlaybackId: videoData.muxPlaybackId
4104
- }
4105
- )];
4106
- }
4107
- return [];
4112
+ if (depth === 2) {
4113
+ const collectionSlug = segments[0];
4114
+ const videoId = segments[1];
4115
+ return this.getVideoFile(collectionSlug, videoId);
4108
4116
  }
4109
4117
  return [];
4110
4118
  }
4111
- async getPresentations(_folder, _auth) {
4112
- 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
+ }));
4113
4127
  }
4114
- getLessonFolders(collectionName) {
4115
- const collection = this.data.collections.find((c) => c.name === collectionName);
4128
+ getLessonFolders(collectionSlug, currentPath) {
4129
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
4116
4130
  if (!collection) return [];
4117
- return collection.videos.map((video) => this.createFolder(
4118
- video.id,
4119
- video.title,
4120
- video.thumbnailUrl,
4121
- {
4122
- level: "lesson",
4123
- collectionName,
4124
- videoData: video
4125
- }
4126
- ));
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) {
4155
+ const allFiles = [];
4156
+ const presentations = collection.videos.map((video) => {
4157
+ const file = { type: "file", id: video.id, title: video.title, mediaType: "video", url: video.videoUrl, image: video.thumbnailUrl, muxPlaybackId: video.muxPlaybackId };
4158
+ allFiles.push(file);
4159
+ return { id: video.id, name: video.title, actionType: "play", files: [file] };
4160
+ });
4161
+ return { id: this.slugify(collection.name), name: collection.name, image: collection.image || void 0, sections: [{ id: "videos", name: "Videos", presentations }], allFiles };
4162
+ }
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] };
4169
+ }
4170
+ return null;
4171
+ }
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 }));
4180
+ }
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 }];
4186
+ }
4187
+ return null;
4127
4188
  }
4128
4189
  slugify(text) {
4129
4190
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4130
4191
  }
4131
4192
  };
4132
4193
 
4133
- // src/providers/highvoltage/data.json
4194
+ // src/providers/highVoltage/data.json
4134
4195
  var data_default2 = {
4135
4196
  collections: [
4136
4197
  {
@@ -11634,10 +11695,9 @@ var data_default2 = {
11634
11695
  ]
11635
11696
  };
11636
11697
 
11637
- // src/providers/HighVoltageKidsProvider.ts
11638
- var HighVoltageKidsProvider = class extends ContentProvider {
11698
+ // src/providers/highVoltage/HighVoltageKidsProvider.ts
11699
+ var HighVoltageKidsProvider = class {
11639
11700
  constructor() {
11640
- super(...arguments);
11641
11701
  this.id = "highvoltagekids";
11642
11702
  this.name = "High Voltage Kids";
11643
11703
  this.logos = {
@@ -11656,87 +11716,211 @@ var HighVoltageKidsProvider = class extends ContentProvider {
11656
11716
  }
11657
11717
  };
11658
11718
  this.data = data_default2;
11659
- }
11660
- requiresAuth() {
11661
- return false;
11662
- }
11663
- getCapabilities() {
11664
- return {
11719
+ this.requiresAuth = false;
11720
+ this.authTypes = ["none"];
11721
+ this.capabilities = {
11665
11722
  browse: true,
11666
- presentations: false,
11667
- playlist: false,
11723
+ presentations: true,
11724
+ playlist: true,
11668
11725
  instructions: false,
11669
- expandedInstructions: false,
11726
+ expandedInstructions: true,
11670
11727
  mediaLicensing: false
11671
11728
  };
11672
11729
  }
11673
- async browse(folder, _auth) {
11674
- if (!folder) {
11675
- return this.data.collections.filter((collection) => collection.folders.length > 0).map((collection) => this.createFolder(
11676
- this.slugify(collection.name),
11677
- collection.name,
11678
- void 0,
11679
- { level: "collection", collectionName: collection.name }
11680
- ));
11730
+ async browse(path, _auth) {
11731
+ const { segments, depth } = parsePath(path);
11732
+ if (depth === 0) {
11733
+ return this.getCollections();
11681
11734
  }
11682
- const level = folder.providerData?.level;
11683
- const collectionName = folder.providerData?.collectionName;
11684
- if (level === "collection") {
11685
- return this.getStudyFolders(collectionName);
11735
+ if (depth === 1) {
11736
+ const collectionSlug = segments[0];
11737
+ return this.getStudyFolders(collectionSlug, path);
11686
11738
  }
11687
- if (level === "study") {
11688
- const studyData = folder.providerData?.studyData;
11689
- if (studyData) {
11690
- return this.getLessonFolders(studyData);
11691
- }
11692
- return [];
11739
+ if (depth === 2) {
11740
+ const collectionSlug = segments[0];
11741
+ const studyId = segments[1];
11742
+ return this.getLessonFolders(collectionSlug, studyId, path);
11693
11743
  }
11694
- if (level === "lesson") {
11695
- const lessonData = folder.providerData?.lessonData;
11696
- if (lessonData?.files) {
11697
- return lessonData.files.map((file) => this.createFile(
11698
- file.id,
11699
- file.title,
11700
- file.url,
11701
- { mediaType: file.mediaType }
11702
- ));
11703
- }
11704
- 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);
11705
11749
  }
11706
11750
  return [];
11707
11751
  }
11708
- async getPresentations(_folder, _auth) {
11709
- 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
+ }));
11710
11759
  }
11711
- getStudyFolders(collectionName) {
11712
- const collection = this.data.collections.find((c) => c.name === collectionName);
11760
+ getStudyFolders(collectionSlug, currentPath) {
11761
+ const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
11713
11762
  if (!collection) return [];
11714
- return collection.folders.map((study) => this.createFolder(
11715
- study.id,
11716
- study.name,
11717
- study.image || void 0,
11718
- {
11719
- level: "study",
11720
- collectionName,
11721
- studyData: study
11722
- }
11723
- ));
11724
- }
11725
- getLessonFolders(study) {
11726
- return study.lessons.map((lesson) => this.createFolder(
11727
- lesson.id,
11728
- lesson.name,
11729
- lesson.image || void 0,
11730
- {
11731
- level: "lesson",
11732
- studyId: study.id,
11733
- lessonData: lesson
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) {
11806
+ const allFiles = [];
11807
+ const sections = study.lessons.map((lesson) => {
11808
+ const files = lesson.files.map((file) => {
11809
+ const contentFile = { type: "file", id: file.id, title: file.title, mediaType: file.mediaType, url: file.url, image: lesson.image };
11810
+ allFiles.push(contentFile);
11811
+ return contentFile;
11812
+ });
11813
+ const presentation = { id: lesson.id, name: lesson.name, actionType: "play", files };
11814
+ return { id: lesson.id, name: lesson.name, presentations: [presentation] };
11815
+ });
11816
+ return { id: study.id, name: study.name, description: study.description, image: study.image, sections, allFiles };
11817
+ }
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 };
11825
+ }
11826
+ return null;
11827
+ }
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) {
11838
+ const allFiles = [];
11839
+ for (const lesson of study.lessons) {
11840
+ for (const file of lesson.files) {
11841
+ allFiles.push({ type: "file", id: file.id, title: file.title, mediaType: file.mediaType, url: file.url, image: lesson.image });
11842
+ }
11734
11843
  }
11735
- ));
11844
+ return allFiles;
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
+ }
11852
+ return null;
11853
+ }
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;
11736
11879
  }
11737
11880
  slugify(text) {
11738
11881
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
11739
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
+ }
11740
11924
  };
11741
11925
 
11742
11926
  // src/providers/index.ts
@@ -11821,15 +12005,15 @@ function getProviderConfig(providerId) {
11821
12005
  const provider = getProvider(providerId);
11822
12006
  return provider?.config || null;
11823
12007
  }
11824
- function getAvailableProviders() {
12008
+ function getAvailableProviders(ids) {
11825
12009
  const implemented = getAllProviders().map((provider) => ({
11826
12010
  id: provider.id,
11827
12011
  name: provider.name,
11828
12012
  logos: provider.logos,
11829
12013
  implemented: true,
11830
- requiresAuth: provider.requiresAuth(),
11831
- authTypes: provider.getAuthTypes(),
11832
- capabilities: provider.getCapabilities()
12014
+ requiresAuth: provider.requiresAuth,
12015
+ authTypes: provider.authTypes,
12016
+ capabilities: provider.capabilities
11833
12017
  }));
11834
12018
  const comingSoon = unimplementedProviders.map((p) => ({
11835
12019
  id: p.id,
@@ -11840,27 +12024,56 @@ function getAvailableProviders() {
11840
12024
  authTypes: [],
11841
12025
  capabilities: { browse: false, presentations: false, playlist: false, instructions: false, expandedInstructions: false, mediaLicensing: false }
11842
12026
  }));
11843
- 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;
11844
12033
  }
11845
12034
 
11846
12035
  // src/index.ts
11847
12036
  var VERSION = "0.0.1";
11848
12037
  export {
11849
12038
  APlayProvider,
12039
+ ApiHelper,
11850
12040
  B1ChurchProvider,
11851
12041
  BibleProjectProvider,
11852
12042
  ContentProvider,
12043
+ DeviceFlowHelper,
12044
+ FormatConverters_exports as FormatConverters,
12045
+ FormatResolver,
12046
+ HighVoltageKidsProvider,
11853
12047
  LessonsChurchProvider,
12048
+ OAuthHelper,
11854
12049
  PlanningCenterProvider,
11855
12050
  SignPresenterProvider,
12051
+ TokenHelper,
11856
12052
  VERSION,
12053
+ appendToPath,
12054
+ buildPath,
12055
+ collapseInstructions,
12056
+ createFile,
12057
+ createFolder,
11857
12058
  detectMediaType,
12059
+ expandedInstructionsToPlaylist,
12060
+ expandedInstructionsToPresentations,
11858
12061
  getAllProviders,
11859
12062
  getAvailableProviders,
11860
12063
  getProvider,
11861
12064
  getProviderConfig,
12065
+ getSegment,
12066
+ instructionsToPlaylist,
12067
+ instructionsToPresentations,
11862
12068
  isContentFile,
11863
12069
  isContentFolder,
12070
+ parsePath,
12071
+ playlistToExpandedInstructions,
12072
+ playlistToInstructions,
12073
+ playlistToPresentations,
12074
+ presentationsToExpandedInstructions,
12075
+ presentationsToInstructions,
12076
+ presentationsToPlaylist,
11864
12077
  registerProvider
11865
12078
  };
11866
12079
  //# sourceMappingURL=index.js.map