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