@churchapps/content-provider-helper 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1511 -1280
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +363 -447
- package/dist/index.d.ts +363 -447
- package/dist/index.js +1493 -1280
- package/dist/index.js.map +1 -1
- package/package.json +9 -2
package/dist/index.cjs
CHANGED
|
@@ -21,20 +21,44 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
APlayProvider: () => APlayProvider,
|
|
24
|
+
ApiHelper: () => ApiHelper,
|
|
24
25
|
B1ChurchProvider: () => B1ChurchProvider,
|
|
25
26
|
BibleProjectProvider: () => BibleProjectProvider,
|
|
26
27
|
ContentProvider: () => ContentProvider,
|
|
28
|
+
DeviceFlowHelper: () => DeviceFlowHelper,
|
|
29
|
+
FormatConverters: () => FormatConverters_exports,
|
|
30
|
+
FormatResolver: () => FormatResolver,
|
|
31
|
+
HighVoltageKidsProvider: () => HighVoltageKidsProvider,
|
|
27
32
|
LessonsChurchProvider: () => LessonsChurchProvider,
|
|
33
|
+
OAuthHelper: () => OAuthHelper,
|
|
28
34
|
PlanningCenterProvider: () => PlanningCenterProvider,
|
|
29
35
|
SignPresenterProvider: () => SignPresenterProvider,
|
|
36
|
+
TokenHelper: () => TokenHelper,
|
|
30
37
|
VERSION: () => VERSION,
|
|
38
|
+
appendToPath: () => appendToPath,
|
|
39
|
+
buildPath: () => buildPath,
|
|
40
|
+
collapseInstructions: () => collapseInstructions,
|
|
41
|
+
createFile: () => createFile,
|
|
42
|
+
createFolder: () => createFolder,
|
|
31
43
|
detectMediaType: () => detectMediaType,
|
|
44
|
+
expandedInstructionsToPlaylist: () => expandedInstructionsToPlaylist,
|
|
45
|
+
expandedInstructionsToPresentations: () => expandedInstructionsToPresentations,
|
|
32
46
|
getAllProviders: () => getAllProviders,
|
|
33
47
|
getAvailableProviders: () => getAvailableProviders,
|
|
34
48
|
getProvider: () => getProvider,
|
|
35
49
|
getProviderConfig: () => getProviderConfig,
|
|
50
|
+
getSegment: () => getSegment,
|
|
51
|
+
instructionsToPlaylist: () => instructionsToPlaylist,
|
|
52
|
+
instructionsToPresentations: () => instructionsToPresentations,
|
|
36
53
|
isContentFile: () => isContentFile,
|
|
37
54
|
isContentFolder: () => isContentFolder,
|
|
55
|
+
parsePath: () => parsePath,
|
|
56
|
+
playlistToExpandedInstructions: () => playlistToExpandedInstructions,
|
|
57
|
+
playlistToInstructions: () => playlistToInstructions,
|
|
58
|
+
playlistToPresentations: () => playlistToPresentations,
|
|
59
|
+
presentationsToExpandedInstructions: () => presentationsToExpandedInstructions,
|
|
60
|
+
presentationsToInstructions: () => presentationsToInstructions,
|
|
61
|
+
presentationsToPlaylist: () => presentationsToPlaylist,
|
|
38
62
|
registerProvider: () => registerProvider
|
|
39
63
|
});
|
|
40
64
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -53,105 +77,379 @@ function detectMediaType(url, explicitType) {
|
|
|
53
77
|
const videoPatterns = [".mp4", ".webm", ".m3u8", ".mov", "stream.mux.com"];
|
|
54
78
|
return videoPatterns.some((p) => url.includes(p)) ? "video" : "image";
|
|
55
79
|
}
|
|
80
|
+
function createFolder(id, title, path, image, providerData, isLeaf) {
|
|
81
|
+
return { type: "folder", id, title, path, image, isLeaf, providerData };
|
|
82
|
+
}
|
|
83
|
+
function createFile(id, title, url, options) {
|
|
84
|
+
return { type: "file", id, title, url, mediaType: options?.mediaType ?? detectMediaType(url), image: options?.image, muxPlaybackId: options?.muxPlaybackId, providerData: options?.providerData };
|
|
85
|
+
}
|
|
56
86
|
|
|
57
|
-
// src/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* Override in subclass if the provider supports playlists.
|
|
62
|
-
* @param _folder - The folder to get playlist for (typically a venue or playlist folder)
|
|
63
|
-
* @param _auth - Optional authentication data
|
|
64
|
-
* @param _resolution - Optional resolution hint for video quality
|
|
65
|
-
* @returns Array of ContentFile objects, or null if not supported
|
|
66
|
-
*/
|
|
67
|
-
getPlaylist(_folder, _auth, _resolution) {
|
|
68
|
-
return Promise.resolve(null);
|
|
87
|
+
// src/pathUtils.ts
|
|
88
|
+
function parsePath(path) {
|
|
89
|
+
if (!path || path === "/" || path === "") {
|
|
90
|
+
return { segments: [], depth: 0 };
|
|
69
91
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
92
|
+
const segments = path.replace(/^\//, "").split("/").filter(Boolean);
|
|
93
|
+
return { segments, depth: segments.length };
|
|
94
|
+
}
|
|
95
|
+
function getSegment(path, index) {
|
|
96
|
+
const { segments } = parsePath(path);
|
|
97
|
+
return segments[index] ?? null;
|
|
98
|
+
}
|
|
99
|
+
function buildPath(segments) {
|
|
100
|
+
if (segments.length === 0) return "";
|
|
101
|
+
return "/" + segments.join("/");
|
|
102
|
+
}
|
|
103
|
+
function appendToPath(basePath, segment) {
|
|
104
|
+
if (!basePath || basePath === "/" || basePath === "") {
|
|
105
|
+
return "/" + segment;
|
|
79
106
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
const cleanBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
108
|
+
return cleanBase + "/" + segment;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/FormatConverters.ts
|
|
112
|
+
var FormatConverters_exports = {};
|
|
113
|
+
__export(FormatConverters_exports, {
|
|
114
|
+
collapseInstructions: () => collapseInstructions,
|
|
115
|
+
expandedInstructionsToPlaylist: () => expandedInstructionsToPlaylist,
|
|
116
|
+
expandedInstructionsToPresentations: () => expandedInstructionsToPresentations,
|
|
117
|
+
instructionsToPlaylist: () => instructionsToPlaylist,
|
|
118
|
+
instructionsToPresentations: () => instructionsToPresentations,
|
|
119
|
+
playlistToExpandedInstructions: () => playlistToExpandedInstructions,
|
|
120
|
+
playlistToInstructions: () => playlistToInstructions,
|
|
121
|
+
playlistToPresentations: () => playlistToPresentations,
|
|
122
|
+
presentationsToExpandedInstructions: () => presentationsToExpandedInstructions,
|
|
123
|
+
presentationsToInstructions: () => presentationsToInstructions,
|
|
124
|
+
presentationsToPlaylist: () => presentationsToPlaylist
|
|
125
|
+
});
|
|
126
|
+
function generateId() {
|
|
127
|
+
return "gen-" + Math.random().toString(36).substring(2, 11);
|
|
128
|
+
}
|
|
129
|
+
function mapItemTypeToActionType(itemType) {
|
|
130
|
+
switch (itemType) {
|
|
131
|
+
case "action":
|
|
132
|
+
case "lessonAction":
|
|
133
|
+
case "providerPresentation":
|
|
134
|
+
case "play":
|
|
135
|
+
return "play";
|
|
136
|
+
case "addon":
|
|
137
|
+
case "add-on":
|
|
138
|
+
case "lessonAddOn":
|
|
139
|
+
case "providerFile":
|
|
140
|
+
return "add-on";
|
|
141
|
+
default:
|
|
142
|
+
return "other";
|
|
89
143
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
144
|
+
}
|
|
145
|
+
function mapActionTypeToItemType(actionType) {
|
|
146
|
+
switch (actionType) {
|
|
147
|
+
case "play":
|
|
148
|
+
return "action";
|
|
149
|
+
case "add-on":
|
|
150
|
+
return "addon";
|
|
151
|
+
default:
|
|
152
|
+
return "item";
|
|
96
153
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
154
|
+
}
|
|
155
|
+
function presentationsToPlaylist(plan) {
|
|
156
|
+
if (plan.allFiles && plan.allFiles.length > 0) {
|
|
157
|
+
return [...plan.allFiles];
|
|
158
|
+
}
|
|
159
|
+
const files = [];
|
|
160
|
+
for (const section of plan.sections) {
|
|
161
|
+
for (const presentation of section.presentations) {
|
|
162
|
+
files.push(...presentation.files);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return files;
|
|
166
|
+
}
|
|
167
|
+
function presentationsToInstructions(plan) {
|
|
168
|
+
return { venueName: plan.name, items: plan.sections.map((section) => ({ id: section.id, itemType: "section", label: section.name, children: section.presentations.map((pres) => {
|
|
169
|
+
const totalSeconds = pres.files.reduce((sum, f) => sum + (f.providerData?.seconds || 0), 0);
|
|
170
|
+
return { id: pres.id, itemType: mapActionTypeToItemType(pres.actionType), label: pres.name, seconds: totalSeconds || void 0, embedUrl: pres.files[0]?.embedUrl || pres.files[0]?.url };
|
|
171
|
+
}) })) };
|
|
172
|
+
}
|
|
173
|
+
function presentationsToExpandedInstructions(plan) {
|
|
174
|
+
return { venueName: plan.name, items: plan.sections.map((section) => ({ id: section.id, itemType: "section", label: section.name, children: section.presentations.map((pres) => ({ id: pres.id, itemType: mapActionTypeToItemType(pres.actionType), label: pres.name, description: pres.actionType !== "other" ? pres.actionType : void 0, seconds: pres.files.reduce((sum, f) => sum + (f.providerData?.seconds || 0), 0) || void 0, children: pres.files.map((f) => ({ id: f.id, itemType: "file", label: f.title, seconds: f.providerData?.seconds || void 0, embedUrl: f.embedUrl || f.url })) })) })) };
|
|
175
|
+
}
|
|
176
|
+
function instructionsToPlaylist(instructions) {
|
|
177
|
+
const files = [];
|
|
178
|
+
function extractFiles(items) {
|
|
179
|
+
for (const item of items) {
|
|
180
|
+
if (item.embedUrl && (item.itemType === "file" || !item.children?.length)) {
|
|
181
|
+
files.push({ type: "file", id: item.id || item.relatedId || generateId(), title: item.label || "Untitled", mediaType: detectMediaType(item.embedUrl), url: item.embedUrl, embedUrl: item.embedUrl, providerData: item.seconds ? { seconds: item.seconds } : void 0 });
|
|
182
|
+
}
|
|
183
|
+
if (item.children) {
|
|
184
|
+
extractFiles(item.children);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
extractFiles(instructions.items);
|
|
189
|
+
return files;
|
|
190
|
+
}
|
|
191
|
+
var expandedInstructionsToPlaylist = instructionsToPlaylist;
|
|
192
|
+
function instructionsToPresentations(instructions, planId) {
|
|
193
|
+
const allFiles = [];
|
|
194
|
+
const sections = instructions.items.filter((item) => item.children && item.children.length > 0).map((sectionItem) => {
|
|
195
|
+
const presentations = (sectionItem.children || []).map((presItem) => {
|
|
196
|
+
const files = [];
|
|
197
|
+
if (presItem.children && presItem.children.length > 0) {
|
|
198
|
+
for (const child of presItem.children) {
|
|
199
|
+
if (child.embedUrl) {
|
|
200
|
+
const file = { type: "file", id: child.id || child.relatedId || generateId(), title: child.label || "Untitled", mediaType: detectMediaType(child.embedUrl), url: child.embedUrl, embedUrl: child.embedUrl, providerData: child.seconds ? { seconds: child.seconds } : void 0 };
|
|
201
|
+
allFiles.push(file);
|
|
202
|
+
files.push(file);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (files.length === 0 && presItem.embedUrl) {
|
|
207
|
+
const file = { type: "file", id: presItem.id || presItem.relatedId || generateId(), title: presItem.label || "Untitled", mediaType: detectMediaType(presItem.embedUrl), url: presItem.embedUrl, embedUrl: presItem.embedUrl, providerData: presItem.seconds ? { seconds: presItem.seconds } : void 0 };
|
|
208
|
+
allFiles.push(file);
|
|
209
|
+
files.push(file);
|
|
210
|
+
}
|
|
211
|
+
return { id: presItem.id || presItem.relatedId || generateId(), name: presItem.label || "Presentation", actionType: mapItemTypeToActionType(presItem.itemType), files };
|
|
212
|
+
});
|
|
213
|
+
return { id: sectionItem.id || sectionItem.relatedId || generateId(), name: sectionItem.label || "Section", presentations };
|
|
214
|
+
});
|
|
215
|
+
return { id: planId || generateId(), name: instructions.venueName || "Plan", sections, allFiles };
|
|
216
|
+
}
|
|
217
|
+
var expandedInstructionsToPresentations = instructionsToPresentations;
|
|
218
|
+
function collapseInstructions(instructions, maxDepth = 2) {
|
|
219
|
+
function collapseItem(item, currentDepth) {
|
|
220
|
+
if (currentDepth >= maxDepth || !item.children || item.children.length === 0) {
|
|
221
|
+
const { children, ...rest } = item;
|
|
222
|
+
if (children && children.length > 0) {
|
|
223
|
+
const totalSeconds = children.reduce((sum, c) => sum + (c.seconds || 0), 0);
|
|
224
|
+
if (totalSeconds > 0) {
|
|
225
|
+
rest.seconds = totalSeconds;
|
|
226
|
+
}
|
|
227
|
+
if (!rest.embedUrl) {
|
|
228
|
+
const firstWithUrl = children.find((c) => c.embedUrl);
|
|
229
|
+
if (firstWithUrl) {
|
|
230
|
+
rest.embedUrl = firstWithUrl.embedUrl;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return rest;
|
|
235
|
+
}
|
|
103
236
|
return {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
playlist: false,
|
|
107
|
-
instructions: false,
|
|
108
|
-
expandedInstructions: false,
|
|
109
|
-
mediaLicensing: false
|
|
237
|
+
...item,
|
|
238
|
+
children: item.children.map((child) => collapseItem(child, currentDepth + 1))
|
|
110
239
|
};
|
|
111
240
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
241
|
+
return {
|
|
242
|
+
venueName: instructions.venueName,
|
|
243
|
+
items: instructions.items.map((item) => collapseItem(item, 0))
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function playlistToPresentations(files, planName = "Playlist", sectionName = "Content") {
|
|
247
|
+
const presentations = files.map((file, index) => ({ id: `pres-${index}-${file.id}`, name: file.title, actionType: "play", files: [file] }));
|
|
248
|
+
return { id: "playlist-plan-" + generateId(), name: planName, sections: [{ id: "main-section", name: sectionName, presentations }], allFiles: [...files] };
|
|
249
|
+
}
|
|
250
|
+
function playlistToInstructions(files, venueName = "Playlist") {
|
|
251
|
+
return { venueName, items: [{ id: "main-section", itemType: "section", label: "Content", children: files.map((file, index) => ({ id: file.id || `item-${index}`, itemType: "file", label: file.title, seconds: file.providerData?.seconds || void 0, embedUrl: file.embedUrl || file.url })) }] };
|
|
252
|
+
}
|
|
253
|
+
var playlistToExpandedInstructions = playlistToInstructions;
|
|
254
|
+
|
|
255
|
+
// src/FormatResolver.ts
|
|
256
|
+
var FormatResolver = class {
|
|
257
|
+
constructor(provider, options = {}) {
|
|
258
|
+
this.provider = provider;
|
|
259
|
+
this.options = { allowLossy: options.allowLossy ?? true };
|
|
121
260
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
* @returns Array of supported AuthType values ('none', 'oauth_pkce', 'device_flow')
|
|
125
|
-
*/
|
|
126
|
-
getAuthTypes() {
|
|
127
|
-
if (!this.requiresAuth()) return ["none"];
|
|
128
|
-
const types = ["oauth_pkce"];
|
|
129
|
-
if (this.supportsDeviceFlow()) types.push("device_flow");
|
|
130
|
-
return types;
|
|
261
|
+
getProvider() {
|
|
262
|
+
return this.provider;
|
|
131
263
|
}
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
*/
|
|
137
|
-
isAuthValid(auth) {
|
|
138
|
-
if (!auth) return false;
|
|
139
|
-
return !this.isTokenExpired(auth);
|
|
264
|
+
/** Extract the last segment from a path to use as fallback ID/title */
|
|
265
|
+
getIdFromPath(path) {
|
|
266
|
+
const { segments } = parsePath(path);
|
|
267
|
+
return segments[segments.length - 1] || "content";
|
|
140
268
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
269
|
+
async getPlaylist(path, auth) {
|
|
270
|
+
const caps = this.provider.capabilities;
|
|
271
|
+
if (caps.playlist && this.provider.getPlaylist) {
|
|
272
|
+
const result = await this.provider.getPlaylist(path, auth);
|
|
273
|
+
if (result && result.length > 0) return result;
|
|
274
|
+
}
|
|
275
|
+
if (caps.presentations) {
|
|
276
|
+
const plan = await this.provider.getPresentations(path, auth);
|
|
277
|
+
if (plan) return presentationsToPlaylist(plan);
|
|
278
|
+
}
|
|
279
|
+
if (caps.expandedInstructions && this.provider.getExpandedInstructions) {
|
|
280
|
+
const expanded = await this.provider.getExpandedInstructions(path, auth);
|
|
281
|
+
if (expanded) return instructionsToPlaylist(expanded);
|
|
282
|
+
}
|
|
283
|
+
if (this.options.allowLossy && caps.instructions && this.provider.getInstructions) {
|
|
284
|
+
const instructions = await this.provider.getInstructions(path, auth);
|
|
285
|
+
if (instructions) return instructionsToPlaylist(instructions);
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
150
288
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
289
|
+
async getPlaylistWithMeta(path, auth) {
|
|
290
|
+
const caps = this.provider.capabilities;
|
|
291
|
+
if (caps.playlist && this.provider.getPlaylist) {
|
|
292
|
+
const result = await this.provider.getPlaylist(path, auth);
|
|
293
|
+
if (result && result.length > 0) {
|
|
294
|
+
return { data: result, meta: { isNative: true, isLossy: false } };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (caps.presentations) {
|
|
298
|
+
const plan = await this.provider.getPresentations(path, auth);
|
|
299
|
+
if (plan) return { data: presentationsToPlaylist(plan), meta: { isNative: false, sourceFormat: "presentations", isLossy: false } };
|
|
300
|
+
}
|
|
301
|
+
if (caps.expandedInstructions && this.provider.getExpandedInstructions) {
|
|
302
|
+
const expanded = await this.provider.getExpandedInstructions(path, auth);
|
|
303
|
+
if (expanded) return { data: instructionsToPlaylist(expanded), meta: { isNative: false, sourceFormat: "expandedInstructions", isLossy: false } };
|
|
304
|
+
}
|
|
305
|
+
if (this.options.allowLossy && caps.instructions && this.provider.getInstructions) {
|
|
306
|
+
const instructions = await this.provider.getInstructions(path, auth);
|
|
307
|
+
if (instructions) return { data: instructionsToPlaylist(instructions), meta: { isNative: false, sourceFormat: "instructions", isLossy: true } };
|
|
308
|
+
}
|
|
309
|
+
return { data: null, meta: { isNative: false, isLossy: false } };
|
|
310
|
+
}
|
|
311
|
+
async getPresentations(path, auth) {
|
|
312
|
+
const caps = this.provider.capabilities;
|
|
313
|
+
const fallbackId = this.getIdFromPath(path);
|
|
314
|
+
if (caps.presentations) {
|
|
315
|
+
const result = await this.provider.getPresentations(path, auth);
|
|
316
|
+
if (result) return result;
|
|
317
|
+
}
|
|
318
|
+
if (caps.expandedInstructions && this.provider.getExpandedInstructions) {
|
|
319
|
+
const expanded = await this.provider.getExpandedInstructions(path, auth);
|
|
320
|
+
if (expanded) return instructionsToPresentations(expanded, fallbackId);
|
|
321
|
+
}
|
|
322
|
+
if (caps.instructions && this.provider.getInstructions) {
|
|
323
|
+
const instructions = await this.provider.getInstructions(path, auth);
|
|
324
|
+
if (instructions) return instructionsToPresentations(instructions, fallbackId);
|
|
325
|
+
}
|
|
326
|
+
if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
|
|
327
|
+
const playlist = await this.provider.getPlaylist(path, auth);
|
|
328
|
+
if (playlist && playlist.length > 0) {
|
|
329
|
+
return playlistToPresentations(playlist, fallbackId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
async getPresentationsWithMeta(path, auth) {
|
|
335
|
+
const caps = this.provider.capabilities;
|
|
336
|
+
const fallbackId = this.getIdFromPath(path);
|
|
337
|
+
if (caps.presentations) {
|
|
338
|
+
const result = await this.provider.getPresentations(path, auth);
|
|
339
|
+
if (result) {
|
|
340
|
+
return { data: result, meta: { isNative: true, isLossy: false } };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (caps.expandedInstructions && this.provider.getExpandedInstructions) {
|
|
344
|
+
const expanded = await this.provider.getExpandedInstructions(path, auth);
|
|
345
|
+
if (expanded) return { data: instructionsToPresentations(expanded, fallbackId), meta: { isNative: false, sourceFormat: "expandedInstructions", isLossy: false } };
|
|
346
|
+
}
|
|
347
|
+
if (caps.instructions && this.provider.getInstructions) {
|
|
348
|
+
const instructions = await this.provider.getInstructions(path, auth);
|
|
349
|
+
if (instructions) return { data: instructionsToPresentations(instructions, fallbackId), meta: { isNative: false, sourceFormat: "instructions", isLossy: true } };
|
|
350
|
+
}
|
|
351
|
+
if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
|
|
352
|
+
const playlist = await this.provider.getPlaylist(path, auth);
|
|
353
|
+
if (playlist && playlist.length > 0) return { data: playlistToPresentations(playlist, fallbackId), meta: { isNative: false, sourceFormat: "playlist", isLossy: true } };
|
|
354
|
+
}
|
|
355
|
+
return { data: null, meta: { isNative: false, isLossy: false } };
|
|
356
|
+
}
|
|
357
|
+
async getInstructions(path, auth) {
|
|
358
|
+
const caps = this.provider.capabilities;
|
|
359
|
+
const fallbackTitle = this.getIdFromPath(path);
|
|
360
|
+
if (caps.instructions && this.provider.getInstructions) {
|
|
361
|
+
const result = await this.provider.getInstructions(path, auth);
|
|
362
|
+
if (result) return result;
|
|
363
|
+
}
|
|
364
|
+
if (caps.expandedInstructions && this.provider.getExpandedInstructions) {
|
|
365
|
+
const expanded = await this.provider.getExpandedInstructions(path, auth);
|
|
366
|
+
if (expanded) return collapseInstructions(expanded);
|
|
367
|
+
}
|
|
368
|
+
if (caps.presentations) {
|
|
369
|
+
const plan = await this.provider.getPresentations(path, auth);
|
|
370
|
+
if (plan) return presentationsToInstructions(plan);
|
|
371
|
+
}
|
|
372
|
+
if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
|
|
373
|
+
const playlist = await this.provider.getPlaylist(path, auth);
|
|
374
|
+
if (playlist && playlist.length > 0) {
|
|
375
|
+
return playlistToInstructions(playlist, fallbackTitle);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
async getInstructionsWithMeta(path, auth) {
|
|
381
|
+
const caps = this.provider.capabilities;
|
|
382
|
+
const fallbackTitle = this.getIdFromPath(path);
|
|
383
|
+
if (caps.instructions && this.provider.getInstructions) {
|
|
384
|
+
const result = await this.provider.getInstructions(path, auth);
|
|
385
|
+
if (result) {
|
|
386
|
+
return { data: result, meta: { isNative: true, isLossy: false } };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (caps.expandedInstructions && this.provider.getExpandedInstructions) {
|
|
390
|
+
const expanded = await this.provider.getExpandedInstructions(path, auth);
|
|
391
|
+
if (expanded) return { data: collapseInstructions(expanded), meta: { isNative: false, sourceFormat: "expandedInstructions", isLossy: true } };
|
|
392
|
+
}
|
|
393
|
+
if (caps.presentations) {
|
|
394
|
+
const plan = await this.provider.getPresentations(path, auth);
|
|
395
|
+
if (plan) return { data: presentationsToInstructions(plan), meta: { isNative: false, sourceFormat: "presentations", isLossy: false } };
|
|
396
|
+
}
|
|
397
|
+
if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
|
|
398
|
+
const playlist = await this.provider.getPlaylist(path, auth);
|
|
399
|
+
if (playlist && playlist.length > 0) return { data: playlistToInstructions(playlist, fallbackTitle), meta: { isNative: false, sourceFormat: "playlist", isLossy: true } };
|
|
400
|
+
}
|
|
401
|
+
return { data: null, meta: { isNative: false, isLossy: false } };
|
|
402
|
+
}
|
|
403
|
+
async getExpandedInstructions(path, auth) {
|
|
404
|
+
const caps = this.provider.capabilities;
|
|
405
|
+
const fallbackTitle = this.getIdFromPath(path);
|
|
406
|
+
if (caps.expandedInstructions && this.provider.getExpandedInstructions) {
|
|
407
|
+
const result = await this.provider.getExpandedInstructions(path, auth);
|
|
408
|
+
if (result) return result;
|
|
409
|
+
}
|
|
410
|
+
if (caps.presentations) {
|
|
411
|
+
const plan = await this.provider.getPresentations(path, auth);
|
|
412
|
+
if (plan) return presentationsToExpandedInstructions(plan);
|
|
413
|
+
}
|
|
414
|
+
if (caps.instructions && this.provider.getInstructions) {
|
|
415
|
+
const instructions = await this.provider.getInstructions(path, auth);
|
|
416
|
+
if (instructions) return instructions;
|
|
417
|
+
}
|
|
418
|
+
if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
|
|
419
|
+
const playlist = await this.provider.getPlaylist(path, auth);
|
|
420
|
+
if (playlist && playlist.length > 0) {
|
|
421
|
+
return playlistToInstructions(playlist, fallbackTitle);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
async getExpandedInstructionsWithMeta(path, auth) {
|
|
427
|
+
const caps = this.provider.capabilities;
|
|
428
|
+
const fallbackTitle = this.getIdFromPath(path);
|
|
429
|
+
if (caps.expandedInstructions && this.provider.getExpandedInstructions) {
|
|
430
|
+
const result = await this.provider.getExpandedInstructions(path, auth);
|
|
431
|
+
if (result) {
|
|
432
|
+
return { data: result, meta: { isNative: true, isLossy: false } };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (caps.presentations) {
|
|
436
|
+
const plan = await this.provider.getPresentations(path, auth);
|
|
437
|
+
if (plan) return { data: presentationsToExpandedInstructions(plan), meta: { isNative: false, sourceFormat: "presentations", isLossy: false } };
|
|
438
|
+
}
|
|
439
|
+
if (caps.instructions && this.provider.getInstructions) {
|
|
440
|
+
const instructions = await this.provider.getInstructions(path, auth);
|
|
441
|
+
if (instructions) return { data: instructions, meta: { isNative: false, sourceFormat: "instructions", isLossy: true } };
|
|
442
|
+
}
|
|
443
|
+
if (this.options.allowLossy && caps.playlist && this.provider.getPlaylist) {
|
|
444
|
+
const playlist = await this.provider.getPlaylist(path, auth);
|
|
445
|
+
if (playlist && playlist.length > 0) return { data: playlistToInstructions(playlist, fallbackTitle), meta: { isNative: false, sourceFormat: "playlist", isLossy: true } };
|
|
446
|
+
}
|
|
447
|
+
return { data: null, meta: { isNative: false, isLossy: false } };
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// src/helpers/OAuthHelper.ts
|
|
452
|
+
var OAuthHelper = class {
|
|
155
453
|
generateCodeVerifier() {
|
|
156
454
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
157
455
|
const length = 64;
|
|
@@ -163,11 +461,6 @@ var ContentProvider = class {
|
|
|
163
461
|
}
|
|
164
462
|
return result;
|
|
165
463
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Generate a code challenge from a code verifier using SHA-256.
|
|
168
|
-
* @param verifier - The code verifier string
|
|
169
|
-
* @returns Base64url-encoded SHA-256 hash of the verifier
|
|
170
|
-
*/
|
|
171
464
|
async generateCodeChallenge(verifier) {
|
|
172
465
|
const encoder = new TextEncoder();
|
|
173
466
|
const data = encoder.encode(verifier);
|
|
@@ -180,142 +473,100 @@ var ContentProvider = class {
|
|
|
180
473
|
const base64 = btoa(binary);
|
|
181
474
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
182
475
|
}
|
|
183
|
-
|
|
184
|
-
* Build the OAuth authorization URL for PKCE flow.
|
|
185
|
-
* @param codeVerifier - The code verifier (store this for token exchange)
|
|
186
|
-
* @param redirectUri - The redirect URI to return to after authorization
|
|
187
|
-
* @param state - Optional state parameter for CSRF protection (defaults to provider ID)
|
|
188
|
-
* @returns Object with authorization URL and challenge method
|
|
189
|
-
*/
|
|
190
|
-
async buildAuthUrl(codeVerifier, redirectUri, state) {
|
|
476
|
+
async buildAuthUrl(config, codeVerifier, redirectUri, state) {
|
|
191
477
|
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
|
|
192
478
|
const params = new URLSearchParams({
|
|
193
479
|
response_type: "code",
|
|
194
|
-
client_id:
|
|
480
|
+
client_id: config.clientId,
|
|
195
481
|
redirect_uri: redirectUri,
|
|
196
|
-
scope:
|
|
482
|
+
scope: config.scopes.join(" "),
|
|
197
483
|
code_challenge: codeChallenge,
|
|
198
484
|
code_challenge_method: "S256",
|
|
199
|
-
state: state ||
|
|
485
|
+
state: state || ""
|
|
200
486
|
});
|
|
201
|
-
return { url: `${
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
* Exchange an authorization code for access and refresh tokens.
|
|
205
|
-
* @param code - The authorization code from the callback
|
|
206
|
-
* @param codeVerifier - The original code verifier used to generate the challenge
|
|
207
|
-
* @param redirectUri - The redirect URI (must match the one used in buildAuthUrl)
|
|
208
|
-
* @returns Authentication data, or null if exchange failed
|
|
209
|
-
*/
|
|
210
|
-
async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
|
|
487
|
+
return { url: `${config.oauthBase}/authorize?${params.toString()}`, challengeMethod: "S256" };
|
|
488
|
+
}
|
|
489
|
+
async exchangeCodeForTokens(config, providerId, code, codeVerifier, redirectUri) {
|
|
211
490
|
try {
|
|
212
491
|
const params = new URLSearchParams({
|
|
213
492
|
grant_type: "authorization_code",
|
|
214
493
|
code,
|
|
215
494
|
redirect_uri: redirectUri,
|
|
216
|
-
client_id:
|
|
495
|
+
client_id: config.clientId,
|
|
217
496
|
code_verifier: codeVerifier
|
|
218
497
|
});
|
|
219
|
-
const tokenUrl = `${
|
|
220
|
-
console.log(`${
|
|
221
|
-
console.log(` - client_id: ${
|
|
498
|
+
const tokenUrl = `${config.oauthBase}/token`;
|
|
499
|
+
console.log(`${providerId} token exchange request to: ${tokenUrl}`);
|
|
500
|
+
console.log(` - client_id: ${config.clientId}`);
|
|
222
501
|
console.log(` - redirect_uri: ${redirectUri}`);
|
|
223
502
|
console.log(` - code: ${code.substring(0, 10)}...`);
|
|
224
503
|
const response = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
|
|
225
|
-
console.log(`${
|
|
504
|
+
console.log(`${providerId} token response status: ${response.status}`);
|
|
226
505
|
if (!response.ok) {
|
|
227
506
|
const errorText = await response.text();
|
|
228
|
-
console.error(`${
|
|
507
|
+
console.error(`${providerId} token exchange failed: ${response.status} - ${errorText}`);
|
|
229
508
|
return null;
|
|
230
509
|
}
|
|
231
510
|
const data = await response.json();
|
|
232
|
-
console.log(`${
|
|
233
|
-
return {
|
|
234
|
-
access_token: data.access_token,
|
|
235
|
-
refresh_token: data.refresh_token,
|
|
236
|
-
token_type: data.token_type || "Bearer",
|
|
237
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
238
|
-
expires_in: data.expires_in,
|
|
239
|
-
scope: data.scope || this.config.scopes.join(" ")
|
|
240
|
-
};
|
|
511
|
+
console.log(`${providerId} token exchange successful, got access_token: ${!!data.access_token}`);
|
|
512
|
+
return { access_token: data.access_token, refresh_token: data.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || config.scopes.join(" ") };
|
|
241
513
|
} catch (error) {
|
|
242
|
-
console.error(`${
|
|
514
|
+
console.error(`${providerId} token exchange error:`, error);
|
|
243
515
|
return null;
|
|
244
516
|
}
|
|
245
517
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// src/helpers/TokenHelper.ts
|
|
521
|
+
var TokenHelper = class {
|
|
522
|
+
isAuthValid(auth) {
|
|
523
|
+
if (!auth) return false;
|
|
524
|
+
return !this.isTokenExpired(auth);
|
|
525
|
+
}
|
|
526
|
+
isTokenExpired(auth) {
|
|
527
|
+
if (!auth.created_at || !auth.expires_in) return true;
|
|
528
|
+
const expiresAt = (auth.created_at + auth.expires_in) * 1e3;
|
|
529
|
+
return Date.now() > expiresAt - 5 * 60 * 1e3;
|
|
530
|
+
}
|
|
531
|
+
async refreshToken(config, auth) {
|
|
252
532
|
if (!auth.refresh_token) return null;
|
|
253
533
|
try {
|
|
254
534
|
const params = new URLSearchParams({
|
|
255
535
|
grant_type: "refresh_token",
|
|
256
536
|
refresh_token: auth.refresh_token,
|
|
257
|
-
client_id:
|
|
537
|
+
client_id: config.clientId
|
|
258
538
|
});
|
|
259
|
-
const response = await fetch(`${
|
|
539
|
+
const response = await fetch(`${config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
|
|
260
540
|
if (!response.ok) return null;
|
|
261
541
|
const data = await response.json();
|
|
262
|
-
return {
|
|
263
|
-
access_token: data.access_token,
|
|
264
|
-
refresh_token: data.refresh_token || auth.refresh_token,
|
|
265
|
-
token_type: data.token_type || "Bearer",
|
|
266
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
267
|
-
expires_in: data.expires_in,
|
|
268
|
-
scope: data.scope || auth.scope
|
|
269
|
-
};
|
|
542
|
+
return { access_token: data.access_token, refresh_token: data.refresh_token || auth.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || auth.scope };
|
|
270
543
|
} catch {
|
|
271
544
|
return null;
|
|
272
545
|
}
|
|
273
546
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
supportsDeviceFlow() {
|
|
279
|
-
return !!
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// src/helpers/DeviceFlowHelper.ts
|
|
550
|
+
var DeviceFlowHelper = class {
|
|
551
|
+
supportsDeviceFlow(config) {
|
|
552
|
+
return !!config.supportsDeviceFlow && !!config.deviceAuthEndpoint;
|
|
280
553
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
* @returns Device authorization response with user_code and verification_uri, or null if not supported
|
|
284
|
-
*/
|
|
285
|
-
async initiateDeviceFlow() {
|
|
286
|
-
if (!this.supportsDeviceFlow()) return null;
|
|
554
|
+
async initiateDeviceFlow(config) {
|
|
555
|
+
if (!this.supportsDeviceFlow(config)) return null;
|
|
287
556
|
try {
|
|
288
|
-
const
|
|
289
|
-
const response = await fetch(`${this.config.oauthBase}${this.config.deviceAuthEndpoint}`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
|
|
557
|
+
const response = await fetch(`${config.oauthBase}${config.deviceAuthEndpoint}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: config.clientId, scope: config.scopes.join(" ") }) });
|
|
290
558
|
if (!response.ok) return null;
|
|
291
559
|
return await response.json();
|
|
292
560
|
} catch {
|
|
293
561
|
return null;
|
|
294
562
|
}
|
|
295
563
|
}
|
|
296
|
-
|
|
297
|
-
* Poll for a token after user has authorized the device.
|
|
298
|
-
* @param deviceCode - The device_code from initiateDeviceFlow response
|
|
299
|
-
* @returns Auth data if successful, error object if pending/slow_down, or null if failed/expired
|
|
300
|
-
*/
|
|
301
|
-
async pollDeviceFlowToken(deviceCode) {
|
|
564
|
+
async pollDeviceFlowToken(config, deviceCode) {
|
|
302
565
|
try {
|
|
303
|
-
const
|
|
304
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
305
|
-
device_code: deviceCode,
|
|
306
|
-
client_id: this.config.clientId
|
|
307
|
-
});
|
|
308
|
-
const response = await fetch(`${this.config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
|
|
566
|
+
const response = await fetch(`${config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: deviceCode, client_id: config.clientId }) });
|
|
309
567
|
if (response.ok) {
|
|
310
568
|
const data = await response.json();
|
|
311
|
-
return {
|
|
312
|
-
access_token: data.access_token,
|
|
313
|
-
refresh_token: data.refresh_token,
|
|
314
|
-
token_type: data.token_type || "Bearer",
|
|
315
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
316
|
-
expires_in: data.expires_in,
|
|
317
|
-
scope: data.scope || this.config.scopes.join(" ")
|
|
318
|
-
};
|
|
569
|
+
return { access_token: data.access_token, refresh_token: data.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || config.scopes.join(" ") };
|
|
319
570
|
}
|
|
320
571
|
const errorData = await response.json();
|
|
321
572
|
switch (errorData.error) {
|
|
@@ -334,221 +585,295 @@ var ContentProvider = class {
|
|
|
334
585
|
return { error: "network_error" };
|
|
335
586
|
}
|
|
336
587
|
}
|
|
337
|
-
/**
|
|
338
|
-
* Calculate the delay between device flow poll attempts.
|
|
339
|
-
* @param baseInterval - Base interval in seconds (default: 5)
|
|
340
|
-
* @param slowDownCount - Number of slow_down responses received
|
|
341
|
-
* @returns Delay in milliseconds
|
|
342
|
-
*/
|
|
343
588
|
calculatePollDelay(baseInterval = 5, slowDownCount = 0) {
|
|
344
589
|
return (baseInterval + slowDownCount * 5) * 1e3;
|
|
345
590
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
*/
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// src/helpers/ApiHelper.ts
|
|
594
|
+
var ApiHelper = class {
|
|
351
595
|
createAuthHeaders(auth) {
|
|
352
596
|
if (!auth) return null;
|
|
353
597
|
return { Authorization: `Bearer ${auth.access_token}`, Accept: "application/json" };
|
|
354
598
|
}
|
|
355
|
-
|
|
356
|
-
* Make an authenticated API request.
|
|
357
|
-
* @param path - API endpoint path (appended to config.apiBase)
|
|
358
|
-
* @param auth - Optional authentication data
|
|
359
|
-
* @param method - HTTP method (default: 'GET')
|
|
360
|
-
* @param body - Optional request body (for POST requests)
|
|
361
|
-
* @returns Parsed JSON response, or null if request failed
|
|
362
|
-
*/
|
|
363
|
-
async apiRequest(path, auth, method = "GET", body) {
|
|
599
|
+
async apiRequest(config, providerId, path, auth, method = "GET", body) {
|
|
364
600
|
try {
|
|
365
|
-
const url = `${
|
|
601
|
+
const url = `${config.apiBase}${path}`;
|
|
366
602
|
const headers = { Accept: "application/json" };
|
|
367
603
|
if (auth) headers["Authorization"] = `Bearer ${auth.access_token}`;
|
|
368
604
|
if (body) headers["Content-Type"] = "application/json";
|
|
369
|
-
console.log(`${
|
|
370
|
-
console.log(`${
|
|
605
|
+
console.log(`${providerId} API request: ${method} ${url}`);
|
|
606
|
+
console.log(`${providerId} API auth present: ${!!auth}`);
|
|
371
607
|
const options = { method, headers, ...body ? { body: JSON.stringify(body) } : {} };
|
|
372
608
|
const response = await fetch(url, options);
|
|
373
|
-
console.log(`${
|
|
609
|
+
console.log(`${providerId} API response status: ${response.status}`);
|
|
374
610
|
if (!response.ok) {
|
|
375
611
|
const errorText = await response.text();
|
|
376
|
-
console.error(`${
|
|
612
|
+
console.error(`${providerId} API request failed: ${response.status} - ${errorText}`);
|
|
377
613
|
return null;
|
|
378
614
|
}
|
|
379
615
|
return await response.json();
|
|
380
616
|
} catch (error) {
|
|
381
|
-
console.error(`${
|
|
617
|
+
console.error(`${providerId} API request error:`, error);
|
|
382
618
|
return null;
|
|
383
619
|
}
|
|
384
620
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// src/ContentProvider.ts
|
|
624
|
+
var ContentProvider = class {
|
|
625
|
+
constructor() {
|
|
626
|
+
this.oauthHelper = new OAuthHelper();
|
|
627
|
+
this.tokenHelper = new TokenHelper();
|
|
628
|
+
this.deviceFlowHelper = new DeviceFlowHelper();
|
|
629
|
+
this.apiHelper = new ApiHelper();
|
|
630
|
+
}
|
|
631
|
+
async getPlaylist(path, auth, _resolution) {
|
|
632
|
+
const caps = this.getCapabilities();
|
|
633
|
+
if (caps.presentations) {
|
|
634
|
+
const plan = await this.getPresentations(path, auth);
|
|
635
|
+
if (plan) return presentationsToPlaylist(plan);
|
|
636
|
+
}
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
async getInstructions(path, auth) {
|
|
640
|
+
const caps = this.getCapabilities();
|
|
641
|
+
if (caps.expandedInstructions) {
|
|
642
|
+
const expanded = await this.getExpandedInstructions(path, auth);
|
|
643
|
+
if (expanded) return collapseInstructions(expanded);
|
|
644
|
+
}
|
|
645
|
+
if (caps.presentations) {
|
|
646
|
+
const plan = await this.getPresentations(path, auth);
|
|
647
|
+
if (plan) return presentationsToInstructions(plan);
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
async getExpandedInstructions(path, auth) {
|
|
652
|
+
const caps = this.getCapabilities();
|
|
653
|
+
if (caps.presentations) {
|
|
654
|
+
const plan = await this.getPresentations(path, auth);
|
|
655
|
+
if (plan) return presentationsToExpandedInstructions(plan);
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
requiresAuth() {
|
|
660
|
+
return !!this.config.clientId;
|
|
661
|
+
}
|
|
662
|
+
getCapabilities() {
|
|
663
|
+
return { browse: true, presentations: false, playlist: false, instructions: false, expandedInstructions: false, mediaLicensing: false };
|
|
664
|
+
}
|
|
665
|
+
checkMediaLicense(_mediaId, _auth) {
|
|
666
|
+
return Promise.resolve(null);
|
|
667
|
+
}
|
|
668
|
+
getAuthTypes() {
|
|
669
|
+
if (!this.requiresAuth()) return ["none"];
|
|
670
|
+
const types = ["oauth_pkce"];
|
|
671
|
+
if (this.supportsDeviceFlow()) types.push("device_flow");
|
|
672
|
+
return types;
|
|
673
|
+
}
|
|
674
|
+
// Token management - delegated to TokenHelper
|
|
675
|
+
isAuthValid(auth) {
|
|
676
|
+
return this.tokenHelper.isAuthValid(auth);
|
|
677
|
+
}
|
|
678
|
+
isTokenExpired(auth) {
|
|
679
|
+
return this.tokenHelper.isTokenExpired(auth);
|
|
680
|
+
}
|
|
681
|
+
async refreshToken(auth) {
|
|
682
|
+
return this.tokenHelper.refreshToken(this.config, auth);
|
|
683
|
+
}
|
|
684
|
+
// OAuth PKCE - delegated to OAuthHelper
|
|
685
|
+
generateCodeVerifier() {
|
|
686
|
+
return this.oauthHelper.generateCodeVerifier();
|
|
687
|
+
}
|
|
688
|
+
async generateCodeChallenge(verifier) {
|
|
689
|
+
return this.oauthHelper.generateCodeChallenge(verifier);
|
|
690
|
+
}
|
|
691
|
+
async buildAuthUrl(codeVerifier, redirectUri, state) {
|
|
692
|
+
return this.oauthHelper.buildAuthUrl(this.config, codeVerifier, redirectUri, state || this.id);
|
|
693
|
+
}
|
|
694
|
+
async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
|
|
695
|
+
return this.oauthHelper.exchangeCodeForTokens(this.config, this.id, code, codeVerifier, redirectUri);
|
|
696
|
+
}
|
|
697
|
+
// Device flow - delegated to DeviceFlowHelper
|
|
698
|
+
supportsDeviceFlow() {
|
|
699
|
+
return this.deviceFlowHelper.supportsDeviceFlow(this.config);
|
|
700
|
+
}
|
|
701
|
+
async initiateDeviceFlow() {
|
|
702
|
+
return this.deviceFlowHelper.initiateDeviceFlow(this.config);
|
|
703
|
+
}
|
|
704
|
+
async pollDeviceFlowToken(deviceCode) {
|
|
705
|
+
return this.deviceFlowHelper.pollDeviceFlowToken(this.config, deviceCode);
|
|
706
|
+
}
|
|
707
|
+
calculatePollDelay(baseInterval = 5, slowDownCount = 0) {
|
|
708
|
+
return this.deviceFlowHelper.calculatePollDelay(baseInterval, slowDownCount);
|
|
709
|
+
}
|
|
710
|
+
// API requests - delegated to ApiHelper
|
|
711
|
+
createAuthHeaders(auth) {
|
|
712
|
+
return this.apiHelper.createAuthHeaders(auth);
|
|
713
|
+
}
|
|
714
|
+
async apiRequest(path, auth, method = "GET", body) {
|
|
715
|
+
return this.apiHelper.apiRequest(this.config, this.id, path, auth, method, body);
|
|
716
|
+
}
|
|
717
|
+
// Content factories
|
|
718
|
+
createFolder(id, title, path, image, providerData, isLeaf) {
|
|
719
|
+
return { type: "folder", id, title, path, image, isLeaf, providerData };
|
|
720
|
+
}
|
|
404
721
|
createFile(id, title, url, options) {
|
|
405
|
-
return {
|
|
406
|
-
type: "file",
|
|
407
|
-
id,
|
|
408
|
-
title,
|
|
409
|
-
url,
|
|
410
|
-
mediaType: options?.mediaType ?? detectMediaType(url),
|
|
411
|
-
image: options?.image,
|
|
412
|
-
muxPlaybackId: options?.muxPlaybackId,
|
|
413
|
-
providerData: options?.providerData
|
|
414
|
-
};
|
|
722
|
+
return { type: "file", id, title, url, mediaType: options?.mediaType ?? detectMediaType(url), image: options?.image, muxPlaybackId: options?.muxPlaybackId, providerData: options?.providerData };
|
|
415
723
|
}
|
|
416
724
|
};
|
|
417
725
|
|
|
418
|
-
// src/providers/APlayProvider.ts
|
|
419
|
-
var APlayProvider = class
|
|
726
|
+
// src/providers/aPlay/APlayProvider.ts
|
|
727
|
+
var APlayProvider = class {
|
|
420
728
|
constructor() {
|
|
421
|
-
|
|
729
|
+
this.apiHelper = new ApiHelper();
|
|
422
730
|
this.id = "aplay";
|
|
423
731
|
this.name = "APlay";
|
|
424
|
-
this.logos = {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
this.
|
|
429
|
-
id: "aplay",
|
|
430
|
-
name: "APlay",
|
|
431
|
-
apiBase: "https://api-prod.amazingkids.app",
|
|
432
|
-
oauthBase: "https://api.joinamazing.com/prod/aims/oauth",
|
|
433
|
-
clientId: "xFJFq7yNYuXXXMx0YBiQ",
|
|
434
|
-
scopes: ["openid", "profile", "email"],
|
|
435
|
-
endpoints: {
|
|
436
|
-
modules: "/prod/curriculum/modules",
|
|
437
|
-
productLibraries: (productId) => `/prod/curriculum/modules/products/${productId}/libraries`,
|
|
438
|
-
libraryMedia: (libraryId) => `/prod/creators/libraries/${libraryId}/media`
|
|
439
|
-
}
|
|
440
|
-
};
|
|
732
|
+
this.logos = { light: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg", dark: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg" };
|
|
733
|
+
this.config = { id: "aplay", name: "APlay", apiBase: "https://api-prod.amazingkids.app", oauthBase: "https://api.joinamazing.com/prod/aims/oauth", clientId: "xFJFq7yNYuXXXMx0YBiQ", scopes: ["openid", "profile", "email"], endpoints: { modules: "/prod/curriculum/modules", productLibraries: (productId) => `/prod/curriculum/modules/products/${productId}/libraries`, libraryMedia: (libraryId) => `/prod/creators/libraries/${libraryId}/media` } };
|
|
734
|
+
this.requiresAuth = true;
|
|
735
|
+
this.authTypes = ["oauth_pkce"];
|
|
736
|
+
this.capabilities = { browse: true, presentations: true, playlist: false, instructions: false, expandedInstructions: false, mediaLicensing: true };
|
|
441
737
|
}
|
|
442
|
-
|
|
443
|
-
return
|
|
444
|
-
browse: true,
|
|
445
|
-
presentations: true,
|
|
446
|
-
playlist: false,
|
|
447
|
-
instructions: false,
|
|
448
|
-
expandedInstructions: false,
|
|
449
|
-
mediaLicensing: true
|
|
450
|
-
};
|
|
738
|
+
async apiRequest(path, auth) {
|
|
739
|
+
return this.apiHelper.apiRequest(this.config, this.id, path, auth);
|
|
451
740
|
}
|
|
452
|
-
async browse(
|
|
453
|
-
|
|
454
|
-
console.log(
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (!Array.isArray(modules)) return [];
|
|
463
|
-
const items = [];
|
|
464
|
-
for (const m of modules) {
|
|
465
|
-
if (m.isLocked) continue;
|
|
466
|
-
const allProducts = m.products || [];
|
|
467
|
-
const products = allProducts.filter((p) => !p.isHidden);
|
|
468
|
-
if (products.length === 0) {
|
|
469
|
-
items.push({
|
|
470
|
-
type: "folder",
|
|
471
|
-
id: m.id || m.moduleId,
|
|
472
|
-
title: m.title || m.name,
|
|
473
|
-
image: m.image,
|
|
474
|
-
providerData: { level: "libraries", productId: m.id || m.moduleId }
|
|
475
|
-
});
|
|
476
|
-
} else if (products.length === 1) {
|
|
477
|
-
const product = products[0];
|
|
478
|
-
items.push({
|
|
479
|
-
type: "folder",
|
|
480
|
-
id: product.productId || product.id,
|
|
481
|
-
title: m.title || m.name,
|
|
482
|
-
image: m.image || product.image,
|
|
483
|
-
providerData: { level: "libraries", productId: product.productId || product.id }
|
|
484
|
-
});
|
|
485
|
-
} else {
|
|
486
|
-
items.push({
|
|
487
|
-
type: "folder",
|
|
488
|
-
id: m.id || m.moduleId,
|
|
489
|
-
title: m.title || m.name,
|
|
490
|
-
image: m.image,
|
|
491
|
-
providerData: {
|
|
492
|
-
level: "products",
|
|
493
|
-
products: products.map((p) => ({ id: p.productId || p.id, title: p.title || p.name, image: p.image }))
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
return items;
|
|
741
|
+
async browse(path, auth) {
|
|
742
|
+
const { segments, depth } = parsePath(path);
|
|
743
|
+
console.log("APlay browse called with path:", path, "depth:", depth);
|
|
744
|
+
if (depth === 0) {
|
|
745
|
+
return [{
|
|
746
|
+
type: "folder",
|
|
747
|
+
id: "modules-root",
|
|
748
|
+
title: "Modules",
|
|
749
|
+
path: "/modules"
|
|
750
|
+
}];
|
|
499
751
|
}
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
752
|
+
const root = segments[0];
|
|
753
|
+
if (root !== "modules") return [];
|
|
754
|
+
if (depth === 1) {
|
|
755
|
+
return this.getModules(auth);
|
|
756
|
+
}
|
|
757
|
+
if (depth === 2) {
|
|
758
|
+
const moduleId = segments[1];
|
|
759
|
+
return this.getModuleContent(moduleId, path, auth);
|
|
760
|
+
}
|
|
761
|
+
if (depth === 4 && segments[2] === "products") {
|
|
762
|
+
const productId = segments[3];
|
|
763
|
+
return this.getLibraryFolders(productId, path, auth);
|
|
764
|
+
}
|
|
765
|
+
if (depth === 5 && segments[2] === "products") {
|
|
766
|
+
const libraryId = segments[4];
|
|
767
|
+
return this.getMediaFiles(libraryId, auth);
|
|
768
|
+
}
|
|
769
|
+
if (depth === 4 && segments[2] === "libraries") {
|
|
770
|
+
const libraryId = segments[3];
|
|
771
|
+
return this.getMediaFiles(libraryId, auth);
|
|
772
|
+
}
|
|
773
|
+
return [];
|
|
774
|
+
}
|
|
775
|
+
async getModules(auth) {
|
|
776
|
+
console.log(`APlay fetching modules from: ${this.config.endpoints.modules}`);
|
|
777
|
+
const response = await this.apiRequest(this.config.endpoints.modules, auth);
|
|
778
|
+
console.log("APlay modules response:", response ? "received" : "null");
|
|
779
|
+
if (!response) return [];
|
|
780
|
+
const modules = response.data || response.modules || response;
|
|
781
|
+
console.log("APlay modules count:", Array.isArray(modules) ? modules.length : "not an array");
|
|
782
|
+
if (!Array.isArray(modules)) return [];
|
|
783
|
+
const items = [];
|
|
784
|
+
for (const m of modules) {
|
|
785
|
+
if (m.isLocked) continue;
|
|
786
|
+
const moduleId = m.id || m.moduleId;
|
|
787
|
+
const moduleTitle = m.title || m.name;
|
|
788
|
+
const moduleImage = m.image;
|
|
789
|
+
const allProducts = m.products || [];
|
|
790
|
+
const products = allProducts.filter((p) => !p.isHidden);
|
|
791
|
+
if (products.length === 0) {
|
|
792
|
+
items.push({
|
|
793
|
+
type: "folder",
|
|
794
|
+
id: moduleId,
|
|
795
|
+
title: moduleTitle,
|
|
796
|
+
image: moduleImage,
|
|
797
|
+
path: `/modules/${moduleId}`,
|
|
798
|
+
providerData: { productCount: 0 }
|
|
799
|
+
});
|
|
800
|
+
} else if (products.length === 1) {
|
|
801
|
+
const product = products[0];
|
|
802
|
+
items.push({
|
|
803
|
+
type: "folder",
|
|
804
|
+
id: product.productId || product.id,
|
|
805
|
+
title: moduleTitle,
|
|
806
|
+
image: moduleImage || product.image,
|
|
807
|
+
path: `/modules/${moduleId}`,
|
|
808
|
+
providerData: { productCount: 1, productId: product.productId || product.id }
|
|
809
|
+
});
|
|
810
|
+
} else {
|
|
811
|
+
items.push({
|
|
812
|
+
type: "folder",
|
|
813
|
+
id: moduleId,
|
|
814
|
+
title: moduleTitle,
|
|
815
|
+
image: moduleImage,
|
|
816
|
+
path: `/modules/${moduleId}`,
|
|
817
|
+
providerData: {
|
|
818
|
+
productCount: products.length,
|
|
819
|
+
products: products.map((p) => ({
|
|
820
|
+
id: p.productId || p.id,
|
|
821
|
+
title: p.title || p.name,
|
|
822
|
+
image: p.image
|
|
823
|
+
}))
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
}
|
|
510
827
|
}
|
|
828
|
+
return items;
|
|
511
829
|
}
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
830
|
+
async getModuleContent(moduleId, currentPath, auth) {
|
|
831
|
+
const modules = await this.getModules(auth);
|
|
832
|
+
const module2 = modules.find((m) => m.id === moduleId || m.providerData?.productId === moduleId);
|
|
833
|
+
if (!module2) return [];
|
|
834
|
+
const providerData = module2.providerData;
|
|
835
|
+
const productCount = providerData?.productCount || 0;
|
|
836
|
+
if (productCount === 0 || productCount === 1) {
|
|
837
|
+
const productId = providerData?.productId || moduleId;
|
|
838
|
+
return this.getLibraryFolders(productId, `${currentPath}/libraries`, auth);
|
|
839
|
+
} else {
|
|
840
|
+
const products = providerData?.products || [];
|
|
841
|
+
return products.map((p) => ({
|
|
842
|
+
type: "folder",
|
|
843
|
+
id: p.id,
|
|
844
|
+
title: p.title,
|
|
845
|
+
image: p.image,
|
|
846
|
+
path: `${currentPath}/products/${p.id}`
|
|
847
|
+
}));
|
|
848
|
+
}
|
|
521
849
|
}
|
|
522
|
-
async getLibraryFolders(
|
|
523
|
-
|
|
524
|
-
console.log(`APlay getLibraryFolders called with productId:`, productId);
|
|
525
|
-
if (!productId) return [];
|
|
850
|
+
async getLibraryFolders(productId, currentPath, auth) {
|
|
851
|
+
console.log("APlay getLibraryFolders called with productId:", productId);
|
|
526
852
|
const pathFn = this.config.endpoints.productLibraries;
|
|
527
|
-
const
|
|
528
|
-
console.log(`APlay fetching libraries from: ${
|
|
529
|
-
const response = await this.apiRequest(
|
|
530
|
-
console.log(
|
|
853
|
+
const apiPath = pathFn(productId);
|
|
854
|
+
console.log(`APlay fetching libraries from: ${apiPath}`);
|
|
855
|
+
const response = await this.apiRequest(apiPath, auth);
|
|
856
|
+
console.log("APlay libraries response:", response ? "received" : "null");
|
|
531
857
|
if (!response) return [];
|
|
532
858
|
const libraries = response.data || response.libraries || response;
|
|
533
|
-
console.log(
|
|
859
|
+
console.log("APlay libraries count:", Array.isArray(libraries) ? libraries.length : "not an array");
|
|
534
860
|
if (!Array.isArray(libraries)) return [];
|
|
535
861
|
return libraries.map((l) => ({
|
|
536
862
|
type: "folder",
|
|
537
863
|
id: l.libraryId || l.id,
|
|
538
864
|
title: l.title || l.name,
|
|
539
865
|
image: l.image,
|
|
540
|
-
|
|
866
|
+
isLeaf: true,
|
|
867
|
+
path: `${currentPath}/${l.libraryId || l.id}`
|
|
541
868
|
}));
|
|
542
869
|
}
|
|
543
|
-
async getMediaFiles(
|
|
544
|
-
|
|
545
|
-
console.log(`APlay getMediaFiles called with libraryId:`, libraryId);
|
|
546
|
-
if (!libraryId) return [];
|
|
870
|
+
async getMediaFiles(libraryId, auth) {
|
|
871
|
+
console.log("APlay getMediaFiles called with libraryId:", libraryId);
|
|
547
872
|
const pathFn = this.config.endpoints.libraryMedia;
|
|
548
|
-
const
|
|
549
|
-
console.log(`APlay fetching media from: ${
|
|
550
|
-
const response = await this.apiRequest(
|
|
551
|
-
console.log(
|
|
873
|
+
const apiPath = pathFn(libraryId);
|
|
874
|
+
console.log(`APlay fetching media from: ${apiPath}`);
|
|
875
|
+
const response = await this.apiRequest(apiPath, auth);
|
|
876
|
+
console.log("APlay media response:", response ? "received" : "null");
|
|
552
877
|
if (!response) return [];
|
|
553
878
|
const mediaItems = response.data || response.media || response;
|
|
554
879
|
if (!Array.isArray(mediaItems)) return [];
|
|
@@ -578,144 +903,97 @@ var APlayProvider = class extends ContentProvider {
|
|
|
578
903
|
if (!url) continue;
|
|
579
904
|
const detectedMediaType = detectMediaType(url, mediaType);
|
|
580
905
|
const fileId = item.mediaId || item.id;
|
|
581
|
-
files.push({
|
|
582
|
-
type: "file",
|
|
583
|
-
id: fileId,
|
|
584
|
-
title: item.title || item.name || item.fileName || "",
|
|
585
|
-
mediaType: detectedMediaType,
|
|
586
|
-
image: thumbnail,
|
|
587
|
-
url,
|
|
588
|
-
muxPlaybackId,
|
|
589
|
-
mediaId: fileId
|
|
590
|
-
});
|
|
906
|
+
files.push({ type: "file", id: fileId, title: item.title || item.name || item.fileName || "", mediaType: detectedMediaType, image: thumbnail, url, muxPlaybackId, mediaId: fileId });
|
|
591
907
|
}
|
|
592
908
|
return files;
|
|
593
909
|
}
|
|
594
|
-
async getPresentations(
|
|
595
|
-
const
|
|
596
|
-
if (
|
|
597
|
-
|
|
910
|
+
async getPresentations(path, auth) {
|
|
911
|
+
const { segments, depth } = parsePath(path);
|
|
912
|
+
if (depth < 4 || segments[0] !== "modules") return null;
|
|
913
|
+
let libraryId;
|
|
914
|
+
const title = "Library";
|
|
915
|
+
if (segments[2] === "products" && depth === 5) {
|
|
916
|
+
libraryId = segments[4];
|
|
917
|
+
} else if (segments[2] === "libraries" && depth === 4) {
|
|
918
|
+
libraryId = segments[3];
|
|
919
|
+
} else {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
const files = await this.getMediaFiles(libraryId, auth);
|
|
598
923
|
if (files.length === 0) return null;
|
|
599
|
-
const presentations = files.map((f) => ({
|
|
600
|
-
|
|
601
|
-
name: f.title,
|
|
602
|
-
actionType: "play",
|
|
603
|
-
files: [f]
|
|
604
|
-
}));
|
|
605
|
-
return {
|
|
606
|
-
id: libraryId,
|
|
607
|
-
name: folder.title,
|
|
608
|
-
image: folder.image,
|
|
609
|
-
sections: [{
|
|
610
|
-
id: `section-${libraryId}`,
|
|
611
|
-
name: folder.title || "Library",
|
|
612
|
-
presentations
|
|
613
|
-
}],
|
|
614
|
-
allFiles: files
|
|
615
|
-
};
|
|
924
|
+
const presentations = files.map((f) => ({ id: f.id, name: f.title, actionType: "play", files: [f] }));
|
|
925
|
+
return { id: libraryId, name: title, sections: [{ id: `section-${libraryId}`, name: title, presentations }], allFiles: files };
|
|
616
926
|
}
|
|
617
|
-
/**
|
|
618
|
-
* Check the license status for a specific media item.
|
|
619
|
-
* Returns license information including pingback URL if licensed.
|
|
620
|
-
*/
|
|
621
927
|
async checkMediaLicense(mediaId, auth) {
|
|
622
928
|
if (!auth) return null;
|
|
623
929
|
try {
|
|
624
930
|
const url = `${this.config.apiBase}/prod/reports/media/license-check`;
|
|
625
|
-
const response = await fetch(url, {
|
|
626
|
-
method: "POST",
|
|
627
|
-
headers: {
|
|
628
|
-
"Authorization": `Bearer ${auth.access_token}`,
|
|
629
|
-
"Content-Type": "application/json",
|
|
630
|
-
"Accept": "application/json"
|
|
631
|
-
},
|
|
632
|
-
body: JSON.stringify({ mediaIds: [mediaId] })
|
|
633
|
-
});
|
|
931
|
+
const response = await fetch(url, { method: "POST", headers: { "Authorization": `Bearer ${auth.access_token}`, "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ mediaIds: [mediaId] }) });
|
|
634
932
|
if (!response.ok) return null;
|
|
635
933
|
const data = await response.json();
|
|
636
934
|
const licenseData = Array.isArray(data) ? data : data.data || [];
|
|
637
935
|
const result = licenseData.find((item) => item.mediaId === mediaId);
|
|
638
936
|
if (result?.isLicensed) {
|
|
639
|
-
|
|
640
|
-
return {
|
|
641
|
-
mediaId,
|
|
642
|
-
status: "valid",
|
|
643
|
-
message: "Media is licensed for playback",
|
|
644
|
-
expiresAt: result.expiresAt
|
|
645
|
-
};
|
|
937
|
+
return { mediaId, status: "valid", message: "Media is licensed for playback", expiresAt: result.expiresAt };
|
|
646
938
|
}
|
|
647
|
-
return {
|
|
648
|
-
mediaId,
|
|
649
|
-
status: "not_licensed",
|
|
650
|
-
message: "Media is not licensed"
|
|
651
|
-
};
|
|
939
|
+
return { mediaId, status: "not_licensed", message: "Media is not licensed" };
|
|
652
940
|
} catch {
|
|
653
|
-
return {
|
|
654
|
-
mediaId,
|
|
655
|
-
status: "unknown",
|
|
656
|
-
message: "Unable to verify license status"
|
|
657
|
-
};
|
|
941
|
+
return { mediaId, status: "unknown", message: "Unable to verify license status" };
|
|
658
942
|
}
|
|
659
943
|
}
|
|
660
944
|
};
|
|
661
945
|
|
|
662
|
-
// src/providers/SignPresenterProvider.ts
|
|
663
|
-
var SignPresenterProvider = class
|
|
946
|
+
// src/providers/signPresenter/SignPresenterProvider.ts
|
|
947
|
+
var SignPresenterProvider = class {
|
|
664
948
|
constructor() {
|
|
665
|
-
|
|
949
|
+
this.apiHelper = new ApiHelper();
|
|
666
950
|
this.id = "signpresenter";
|
|
667
951
|
this.name = "SignPresenter";
|
|
668
|
-
this.logos = {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
this.
|
|
673
|
-
id: "signpresenter",
|
|
674
|
-
name: "SignPresenter",
|
|
675
|
-
apiBase: "https://api.signpresenter.com",
|
|
676
|
-
oauthBase: "https://api.signpresenter.com/oauth",
|
|
677
|
-
clientId: "lessonsscreen-tv",
|
|
678
|
-
scopes: ["openid", "profile", "content"],
|
|
679
|
-
supportsDeviceFlow: true,
|
|
680
|
-
deviceAuthEndpoint: "/device/authorize",
|
|
681
|
-
endpoints: {
|
|
682
|
-
playlists: "/content/playlists",
|
|
683
|
-
messages: (playlistId) => `/content/playlists/${playlistId}/messages`
|
|
684
|
-
}
|
|
685
|
-
};
|
|
952
|
+
this.logos = { light: "https://signpresenter.com/files/shared/images/logo.png", dark: "https://signpresenter.com/files/shared/images/logo.png" };
|
|
953
|
+
this.config = { id: "signpresenter", name: "SignPresenter", apiBase: "https://api.signpresenter.com", oauthBase: "https://api.signpresenter.com/oauth", clientId: "lessonsscreen-tv", scopes: ["openid", "profile", "content"], supportsDeviceFlow: true, deviceAuthEndpoint: "/device/authorize", endpoints: { playlists: "/content/playlists", messages: (playlistId) => `/content/playlists/${playlistId}/messages` } };
|
|
954
|
+
this.requiresAuth = true;
|
|
955
|
+
this.authTypes = ["oauth_pkce", "device_flow"];
|
|
956
|
+
this.capabilities = { browse: true, presentations: true, playlist: false, instructions: false, expandedInstructions: false, mediaLicensing: false };
|
|
686
957
|
}
|
|
687
|
-
|
|
688
|
-
return
|
|
689
|
-
browse: true,
|
|
690
|
-
presentations: true,
|
|
691
|
-
playlist: false,
|
|
692
|
-
instructions: false,
|
|
693
|
-
expandedInstructions: false,
|
|
694
|
-
mediaLicensing: false
|
|
695
|
-
};
|
|
958
|
+
async apiRequest(path, auth) {
|
|
959
|
+
return this.apiHelper.apiRequest(this.config, this.id, path, auth);
|
|
696
960
|
}
|
|
697
|
-
async browse(
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (!response) return [];
|
|
702
|
-
const playlists = Array.isArray(response) ? response : response.data || response.playlists || [];
|
|
703
|
-
if (!Array.isArray(playlists)) return [];
|
|
704
|
-
return playlists.map((p) => ({
|
|
961
|
+
async browse(path, auth) {
|
|
962
|
+
const { segments, depth } = parsePath(path);
|
|
963
|
+
if (depth === 0) {
|
|
964
|
+
return [{
|
|
705
965
|
type: "folder",
|
|
706
|
-
id:
|
|
707
|
-
title:
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
966
|
+
id: "playlists-root",
|
|
967
|
+
title: "Playlists",
|
|
968
|
+
path: "/playlists"
|
|
969
|
+
}];
|
|
970
|
+
}
|
|
971
|
+
const root = segments[0];
|
|
972
|
+
if (root !== "playlists") return [];
|
|
973
|
+
if (depth === 1) {
|
|
974
|
+
return this.getPlaylists(auth);
|
|
975
|
+
}
|
|
976
|
+
if (depth === 2) {
|
|
977
|
+
const playlistId = segments[1];
|
|
978
|
+
return this.getMessages(playlistId, auth);
|
|
711
979
|
}
|
|
712
|
-
const level = folder.providerData?.level;
|
|
713
|
-
if (level === "messages") return this.getMessages(folder, auth);
|
|
714
980
|
return [];
|
|
715
981
|
}
|
|
716
|
-
async
|
|
717
|
-
const
|
|
718
|
-
|
|
982
|
+
async getPlaylists(auth) {
|
|
983
|
+
const apiPath = this.config.endpoints.playlists;
|
|
984
|
+
const response = await this.apiRequest(apiPath, auth);
|
|
985
|
+
if (!response) return [];
|
|
986
|
+
const playlists = Array.isArray(response) ? response : response.data || response.playlists || [];
|
|
987
|
+
if (!Array.isArray(playlists)) return [];
|
|
988
|
+
return playlists.map((p) => ({
|
|
989
|
+
type: "folder",
|
|
990
|
+
id: p.id,
|
|
991
|
+
title: p.name,
|
|
992
|
+
image: p.image,
|
|
993
|
+
path: `/playlists/${p.id}`
|
|
994
|
+
}));
|
|
995
|
+
}
|
|
996
|
+
async getMessages(playlistId, auth) {
|
|
719
997
|
const pathFn = this.config.endpoints.messages;
|
|
720
998
|
const response = await this.apiRequest(pathFn(playlistId), auth);
|
|
721
999
|
if (!response) return [];
|
|
@@ -726,93 +1004,42 @@ var SignPresenterProvider = class extends ContentProvider {
|
|
|
726
1004
|
if (!msg.url) continue;
|
|
727
1005
|
const url = msg.url;
|
|
728
1006
|
const seconds = msg.seconds;
|
|
729
|
-
files.push({
|
|
730
|
-
type: "file",
|
|
731
|
-
id: msg.id,
|
|
732
|
-
title: msg.name,
|
|
733
|
-
mediaType: detectMediaType(url, msg.mediaType),
|
|
734
|
-
image: msg.thumbnail || msg.image,
|
|
735
|
-
url,
|
|
736
|
-
// For direct media providers, embedUrl is the media URL itself
|
|
737
|
-
embedUrl: url,
|
|
738
|
-
providerData: seconds !== void 0 ? { seconds } : void 0
|
|
739
|
-
});
|
|
1007
|
+
files.push({ type: "file", id: msg.id, title: msg.name, mediaType: detectMediaType(url, msg.mediaType), image: msg.thumbnail || msg.image, url, embedUrl: url, providerData: seconds !== void 0 ? { seconds } : void 0 });
|
|
740
1008
|
}
|
|
741
1009
|
return files;
|
|
742
1010
|
}
|
|
743
|
-
async getPresentations(
|
|
744
|
-
const
|
|
745
|
-
if (
|
|
746
|
-
const
|
|
1011
|
+
async getPresentations(path, auth) {
|
|
1012
|
+
const { segments, depth } = parsePath(path);
|
|
1013
|
+
if (depth < 2 || segments[0] !== "playlists") return null;
|
|
1014
|
+
const playlistId = segments[1];
|
|
1015
|
+
const files = await this.getMessages(playlistId, auth);
|
|
747
1016
|
if (files.length === 0) return null;
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
return {
|
|
755
|
-
id: playlistId,
|
|
756
|
-
name: folder.title,
|
|
757
|
-
image: folder.image,
|
|
758
|
-
sections: [{
|
|
759
|
-
id: `section-${playlistId}`,
|
|
760
|
-
name: folder.title || "Playlist",
|
|
761
|
-
presentations
|
|
762
|
-
}],
|
|
763
|
-
allFiles: files
|
|
764
|
-
};
|
|
1017
|
+
const playlists = await this.getPlaylists(auth);
|
|
1018
|
+
const playlist = playlists.find((p) => p.id === playlistId);
|
|
1019
|
+
const title = playlist?.title || "Playlist";
|
|
1020
|
+
const image = playlist?.image;
|
|
1021
|
+
const presentations = files.map((f) => ({ id: f.id, name: f.title, actionType: "play", files: [f] }));
|
|
1022
|
+
return { id: playlistId, name: title, image, sections: [{ id: `section-${playlistId}`, name: title, presentations }], allFiles: files };
|
|
765
1023
|
}
|
|
766
1024
|
};
|
|
767
1025
|
|
|
768
|
-
// src/providers/LessonsChurchProvider.ts
|
|
769
|
-
var LessonsChurchProvider = class
|
|
1026
|
+
// src/providers/lessonsChurch/LessonsChurchProvider.ts
|
|
1027
|
+
var LessonsChurchProvider = class {
|
|
770
1028
|
constructor() {
|
|
771
|
-
super(...arguments);
|
|
772
1029
|
this.id = "lessonschurch";
|
|
773
1030
|
this.name = "Lessons.church";
|
|
774
|
-
this.logos = {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
this.
|
|
779
|
-
id: "lessonschurch",
|
|
780
|
-
name: "Lessons.church",
|
|
781
|
-
apiBase: "https://api.lessons.church",
|
|
782
|
-
oauthBase: "",
|
|
783
|
-
clientId: "",
|
|
784
|
-
scopes: [],
|
|
785
|
-
endpoints: {
|
|
786
|
-
programs: "/programs/public",
|
|
787
|
-
studies: (programId) => `/studies/public/program/${programId}`,
|
|
788
|
-
lessons: (studyId) => `/lessons/public/study/${studyId}`,
|
|
789
|
-
venues: (lessonId) => `/venues/public/lesson/${lessonId}`,
|
|
790
|
-
playlist: (venueId) => `/venues/playlist/${venueId}`,
|
|
791
|
-
feed: (venueId) => `/venues/public/feed/${venueId}`,
|
|
792
|
-
addOns: "/addOns/public",
|
|
793
|
-
addOnDetail: (id) => `/addOns/public/${id}`
|
|
794
|
-
}
|
|
795
|
-
};
|
|
1031
|
+
this.logos = { light: "https://lessons.church/images/logo.png", dark: "https://lessons.church/images/logo-dark.png" };
|
|
1032
|
+
this.config = { id: "lessonschurch", name: "Lessons.church", apiBase: "https://api.lessons.church", oauthBase: "", clientId: "", scopes: [], endpoints: { programs: "/programs/public", studies: (programId) => `/studies/public/program/${programId}`, lessons: (studyId) => `/lessons/public/study/${studyId}`, venues: (lessonId) => `/venues/public/lesson/${lessonId}`, playlist: (venueId) => `/venues/playlist/${venueId}`, feed: (venueId) => `/venues/public/feed/${venueId}`, addOns: "/addOns/public", addOnDetail: (id) => `/addOns/public/${id}` } };
|
|
1033
|
+
this.requiresAuth = false;
|
|
1034
|
+
this.authTypes = ["none"];
|
|
1035
|
+
this.capabilities = { browse: true, presentations: true, playlist: true, instructions: true, expandedInstructions: true, mediaLicensing: false };
|
|
796
1036
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
getCapabilities() {
|
|
801
|
-
return {
|
|
802
|
-
browse: true,
|
|
803
|
-
presentations: true,
|
|
804
|
-
playlist: true,
|
|
805
|
-
instructions: true,
|
|
806
|
-
expandedInstructions: true,
|
|
807
|
-
mediaLicensing: false
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
async getPlaylist(folder, _auth, resolution) {
|
|
811
|
-
const venueId = folder.providerData?.venueId;
|
|
1037
|
+
async getPlaylist(path, _auth, resolution) {
|
|
1038
|
+
const venueId = getSegment(path, 4);
|
|
812
1039
|
if (!venueId) return null;
|
|
813
|
-
let
|
|
814
|
-
if (resolution)
|
|
815
|
-
const response = await this.apiRequest(
|
|
1040
|
+
let apiPath = `/venues/playlist/${venueId}`;
|
|
1041
|
+
if (resolution) apiPath += `?resolution=${resolution}`;
|
|
1042
|
+
const response = await this.apiRequest(apiPath);
|
|
816
1043
|
if (!response) return null;
|
|
817
1044
|
const files = [];
|
|
818
1045
|
const messages = response.messages || [];
|
|
@@ -824,15 +1051,7 @@ var LessonsChurchProvider = class extends ContentProvider {
|
|
|
824
1051
|
if (!f.url) continue;
|
|
825
1052
|
const url = f.url;
|
|
826
1053
|
const fileId = f.id || `playlist-${fileIndex++}`;
|
|
827
|
-
files.push({
|
|
828
|
-
type: "file",
|
|
829
|
-
id: fileId,
|
|
830
|
-
title: f.name || msg.name,
|
|
831
|
-
mediaType: detectMediaType(url, f.fileType),
|
|
832
|
-
image: response.lessonImage,
|
|
833
|
-
url,
|
|
834
|
-
providerData: { seconds: f.seconds, loop: f.loop, loopVideo: f.loopVideo }
|
|
835
|
-
});
|
|
1054
|
+
files.push({ type: "file", id: fileId, title: f.name || msg.name, mediaType: detectMediaType(url, f.fileType), image: response.lessonImage, url, providerData: { seconds: f.seconds, loop: f.loop, loopVideo: f.loopVideo } });
|
|
836
1055
|
}
|
|
837
1056
|
}
|
|
838
1057
|
return files;
|
|
@@ -847,48 +1066,32 @@ var LessonsChurchProvider = class extends ContentProvider {
|
|
|
847
1066
|
return null;
|
|
848
1067
|
}
|
|
849
1068
|
}
|
|
850
|
-
async browse(
|
|
851
|
-
|
|
1069
|
+
async browse(path, _auth) {
|
|
1070
|
+
const { segments, depth } = parsePath(path);
|
|
1071
|
+
console.log("[LessonsChurchProvider.browse] path:", path, "depth:", depth, "segments:", segments);
|
|
1072
|
+
if (depth === 0) {
|
|
852
1073
|
return [
|
|
853
|
-
{
|
|
854
|
-
|
|
855
|
-
id: "lessons-root",
|
|
856
|
-
title: "Lessons",
|
|
857
|
-
providerData: { level: "programs" }
|
|
858
|
-
},
|
|
859
|
-
{
|
|
860
|
-
type: "folder",
|
|
861
|
-
id: "addons-root",
|
|
862
|
-
title: "Add-Ons",
|
|
863
|
-
providerData: { level: "addOnCategories" }
|
|
864
|
-
}
|
|
1074
|
+
{ type: "folder", id: "lessons-root", title: "Lessons", path: "/lessons" },
|
|
1075
|
+
{ type: "folder", id: "addons-root", title: "Add-Ons", path: "/addons" }
|
|
865
1076
|
];
|
|
866
1077
|
}
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
// Add-ons hierarchy
|
|
881
|
-
case "addOnCategories":
|
|
882
|
-
return this.getAddOnCategories();
|
|
883
|
-
case "addOns":
|
|
884
|
-
return this.getAddOnsByCategory(folder);
|
|
885
|
-
default:
|
|
886
|
-
return [];
|
|
887
|
-
}
|
|
1078
|
+
const root = segments[0];
|
|
1079
|
+
if (root === "lessons") return this.browseLessons(path, segments);
|
|
1080
|
+
if (root === "addons") return this.browseAddOns(path, segments);
|
|
1081
|
+
return [];
|
|
1082
|
+
}
|
|
1083
|
+
async browseLessons(currentPath, segments) {
|
|
1084
|
+
const depth = segments.length;
|
|
1085
|
+
if (depth === 1) return this.getPrograms();
|
|
1086
|
+
if (depth === 2) return this.getStudies(segments[1], currentPath);
|
|
1087
|
+
if (depth === 3) return this.getLessons(segments[2], currentPath);
|
|
1088
|
+
if (depth === 4) return this.getVenues(segments[3], currentPath);
|
|
1089
|
+
if (depth === 5) return this.getPlaylistFiles(segments[4]);
|
|
1090
|
+
return [];
|
|
888
1091
|
}
|
|
889
1092
|
async getPrograms() {
|
|
890
|
-
const
|
|
891
|
-
const response = await this.apiRequest(
|
|
1093
|
+
const apiPath = this.config.endpoints.programs;
|
|
1094
|
+
const response = await this.apiRequest(apiPath);
|
|
892
1095
|
if (!response) return [];
|
|
893
1096
|
const programs = Array.isArray(response) ? response : [];
|
|
894
1097
|
return programs.map((p) => ({
|
|
@@ -896,12 +1099,10 @@ var LessonsChurchProvider = class extends ContentProvider {
|
|
|
896
1099
|
id: p.id,
|
|
897
1100
|
title: p.name,
|
|
898
1101
|
image: p.image,
|
|
899
|
-
|
|
1102
|
+
path: `/lessons/${p.id}`
|
|
900
1103
|
}));
|
|
901
1104
|
}
|
|
902
|
-
async getStudies(
|
|
903
|
-
const programId = folder.providerData?.programId;
|
|
904
|
-
if (!programId) return [];
|
|
1105
|
+
async getStudies(programId, currentPath) {
|
|
905
1106
|
const pathFn = this.config.endpoints.studies;
|
|
906
1107
|
const response = await this.apiRequest(pathFn(programId));
|
|
907
1108
|
if (!response) return [];
|
|
@@ -911,12 +1112,10 @@ var LessonsChurchProvider = class extends ContentProvider {
|
|
|
911
1112
|
id: s.id,
|
|
912
1113
|
title: s.name,
|
|
913
1114
|
image: s.image,
|
|
914
|
-
|
|
1115
|
+
path: `${currentPath}/${s.id}`
|
|
915
1116
|
}));
|
|
916
1117
|
}
|
|
917
|
-
async getLessons(
|
|
918
|
-
const studyId = folder.providerData?.studyId;
|
|
919
|
-
if (!studyId) return [];
|
|
1118
|
+
async getLessons(studyId, currentPath) {
|
|
920
1119
|
const pathFn = this.config.endpoints.lessons;
|
|
921
1120
|
const response = await this.apiRequest(pathFn(studyId));
|
|
922
1121
|
if (!response) return [];
|
|
@@ -926,31 +1125,42 @@ var LessonsChurchProvider = class extends ContentProvider {
|
|
|
926
1125
|
id: l.id,
|
|
927
1126
|
title: l.name || l.title,
|
|
928
1127
|
image: l.image,
|
|
929
|
-
|
|
1128
|
+
path: `${currentPath}/${l.id}`,
|
|
1129
|
+
providerData: { lessonImage: l.image }
|
|
1130
|
+
// Keep for display on venues
|
|
930
1131
|
}));
|
|
931
1132
|
}
|
|
932
|
-
async getVenues(
|
|
933
|
-
const lessonId = folder.providerData?.lessonId;
|
|
934
|
-
if (!lessonId) return [];
|
|
1133
|
+
async getVenues(lessonId, currentPath) {
|
|
935
1134
|
const pathFn = this.config.endpoints.venues;
|
|
936
1135
|
const response = await this.apiRequest(pathFn(lessonId));
|
|
937
1136
|
if (!response) return [];
|
|
1137
|
+
const lessonResponse = await this.apiRequest(`/lessons/public/${lessonId}`);
|
|
1138
|
+
const lessonImage = lessonResponse?.image;
|
|
938
1139
|
const venues = Array.isArray(response) ? response : [];
|
|
939
|
-
|
|
1140
|
+
const result = venues.map((v) => ({
|
|
940
1141
|
type: "folder",
|
|
941
1142
|
id: v.id,
|
|
942
1143
|
title: v.name,
|
|
943
|
-
image:
|
|
944
|
-
|
|
1144
|
+
image: lessonImage,
|
|
1145
|
+
isLeaf: true,
|
|
1146
|
+
path: `${currentPath}/${v.id}`
|
|
945
1147
|
}));
|
|
1148
|
+
console.log("[LessonsChurchProvider.getVenues] returning:", result.map((r) => ({ id: r.id, title: r.title, isLeaf: r.isLeaf })));
|
|
1149
|
+
return result;
|
|
946
1150
|
}
|
|
947
|
-
async getPlaylistFiles(
|
|
948
|
-
const files = await this.getPlaylist(
|
|
1151
|
+
async getPlaylistFiles(venueId) {
|
|
1152
|
+
const files = await this.getPlaylist(`/lessons/_/_/_/${venueId}`, null);
|
|
949
1153
|
return files || [];
|
|
950
1154
|
}
|
|
1155
|
+
async browseAddOns(_currentPath, segments) {
|
|
1156
|
+
const depth = segments.length;
|
|
1157
|
+
if (depth === 1) return this.getAddOnCategories();
|
|
1158
|
+
if (depth === 2) return this.getAddOnsByCategory(segments[1]);
|
|
1159
|
+
return [];
|
|
1160
|
+
}
|
|
951
1161
|
async getAddOnCategories() {
|
|
952
|
-
const
|
|
953
|
-
const response = await this.apiRequest(
|
|
1162
|
+
const apiPath = this.config.endpoints.addOns;
|
|
1163
|
+
const response = await this.apiRequest(apiPath);
|
|
954
1164
|
if (!response) return [];
|
|
955
1165
|
const addOns = Array.isArray(response) ? response : [];
|
|
956
1166
|
const categories = Array.from(new Set(addOns.map((a) => a.category).filter(Boolean)));
|
|
@@ -958,17 +1168,16 @@ var LessonsChurchProvider = class extends ContentProvider {
|
|
|
958
1168
|
type: "folder",
|
|
959
1169
|
id: `category-${category}`,
|
|
960
1170
|
title: category,
|
|
961
|
-
|
|
962
|
-
level: "addOns",
|
|
963
|
-
category,
|
|
964
|
-
allAddOns: addOns
|
|
965
|
-
}
|
|
1171
|
+
path: `/addons/${encodeURIComponent(category)}`
|
|
966
1172
|
}));
|
|
967
1173
|
}
|
|
968
|
-
async getAddOnsByCategory(
|
|
969
|
-
const
|
|
970
|
-
const
|
|
971
|
-
const
|
|
1174
|
+
async getAddOnsByCategory(category) {
|
|
1175
|
+
const decodedCategory = decodeURIComponent(category);
|
|
1176
|
+
const apiPath = this.config.endpoints.addOns;
|
|
1177
|
+
const response = await this.apiRequest(apiPath);
|
|
1178
|
+
if (!response) return [];
|
|
1179
|
+
const allAddOns = Array.isArray(response) ? response : [];
|
|
1180
|
+
const filtered = allAddOns.filter((a) => a.category === decodedCategory);
|
|
972
1181
|
const files = [];
|
|
973
1182
|
for (const addOn of filtered) {
|
|
974
1183
|
const file = await this.convertAddOnToFile(addOn);
|
|
@@ -978,8 +1187,8 @@ var LessonsChurchProvider = class extends ContentProvider {
|
|
|
978
1187
|
}
|
|
979
1188
|
async convertAddOnToFile(addOn) {
|
|
980
1189
|
const pathFn = this.config.endpoints.addOnDetail;
|
|
981
|
-
const
|
|
982
|
-
const detail = await this.apiRequest(
|
|
1190
|
+
const apiPath = pathFn(addOn.id);
|
|
1191
|
+
const detail = await this.apiRequest(apiPath);
|
|
983
1192
|
if (!detail) return null;
|
|
984
1193
|
let url = "";
|
|
985
1194
|
let mediaType = "video";
|
|
@@ -994,53 +1203,32 @@ var LessonsChurchProvider = class extends ContentProvider {
|
|
|
994
1203
|
const fileType = file.fileType;
|
|
995
1204
|
mediaType = fileType?.startsWith("video/") ? "video" : "image";
|
|
996
1205
|
} else {
|
|
997
|
-
return null;
|
|
998
|
-
}
|
|
999
|
-
return {
|
|
1000
|
-
type: "file",
|
|
1001
|
-
id: addOn.id,
|
|
1002
|
-
title: addOn.name,
|
|
1003
|
-
mediaType,
|
|
1004
|
-
image: addOn.image,
|
|
1005
|
-
url,
|
|
1006
|
-
embedUrl: `https://lessons.church/embed/addon/${addOn.id}`,
|
|
1007
|
-
providerData: {
|
|
1008
|
-
seconds,
|
|
1009
|
-
loopVideo: video?.loopVideo || false
|
|
1010
|
-
}
|
|
1011
|
-
};
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
return { type: "file", id: addOn.id, title: addOn.name, mediaType, image: addOn.image, url, embedUrl: `https://lessons.church/embed/addon/${addOn.id}`, providerData: { seconds, loopVideo: video?.loopVideo || false } };
|
|
1012
1209
|
}
|
|
1013
|
-
async getPresentations(
|
|
1014
|
-
const venueId =
|
|
1210
|
+
async getPresentations(path, _auth) {
|
|
1211
|
+
const venueId = getSegment(path, 4);
|
|
1015
1212
|
if (!venueId) return null;
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
const venueData = await this.apiRequest(path);
|
|
1213
|
+
const apiPath = `/venues/public/feed/${venueId}`;
|
|
1214
|
+
const venueData = await this.apiRequest(apiPath);
|
|
1019
1215
|
if (!venueData) return null;
|
|
1020
1216
|
return this.convertVenueToPlan(venueData);
|
|
1021
1217
|
}
|
|
1022
|
-
async getInstructions(
|
|
1023
|
-
const venueId =
|
|
1218
|
+
async getInstructions(path, _auth) {
|
|
1219
|
+
const venueId = getSegment(path, 4);
|
|
1024
1220
|
if (!venueId) return null;
|
|
1025
1221
|
const response = await this.apiRequest(`/venues/public/planItems/${venueId}`);
|
|
1026
1222
|
if (!response) return null;
|
|
1027
|
-
const processItem = (item) =>
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
relatedId: item.relatedId
|
|
1031
|
-
label: item.label,
|
|
1032
|
-
description: item.description,
|
|
1033
|
-
seconds: item.seconds,
|
|
1034
|
-
children: item.children?.map(processItem),
|
|
1035
|
-
embedUrl: this.getEmbedUrl(item.itemType, item.relatedId)
|
|
1036
|
-
});
|
|
1037
|
-
return {
|
|
1038
|
-
venueName: response.venueName,
|
|
1039
|
-
items: (response.items || []).map(processItem)
|
|
1223
|
+
const processItem = (item) => {
|
|
1224
|
+
const itemType = this.normalizeItemType(item.itemType);
|
|
1225
|
+
const relatedId = item.relatedId;
|
|
1226
|
+
return { id: item.id, itemType, relatedId, label: item.label, description: item.description, seconds: item.seconds, children: item.children?.map(processItem), embedUrl: this.getEmbedUrl(itemType, relatedId) };
|
|
1040
1227
|
};
|
|
1228
|
+
return { venueName: response.venueName, items: (response.items || []).map(processItem) };
|
|
1041
1229
|
}
|
|
1042
|
-
async getExpandedInstructions(
|
|
1043
|
-
const venueId =
|
|
1230
|
+
async getExpandedInstructions(path, _auth) {
|
|
1231
|
+
const venueId = getSegment(path, 4);
|
|
1044
1232
|
if (!venueId) return null;
|
|
1045
1233
|
const [planItemsResponse, actionsResponse] = await Promise.all([
|
|
1046
1234
|
this.apiRequest(`/venues/public/planItems/${venueId}`),
|
|
@@ -1051,66 +1239,47 @@ var LessonsChurchProvider = class extends ContentProvider {
|
|
|
1051
1239
|
if (actionsResponse?.sections) {
|
|
1052
1240
|
for (const section of actionsResponse.sections) {
|
|
1053
1241
|
if (section.id && section.actions) {
|
|
1054
|
-
sectionActionsMap.set(section.id, section.actions.map((action) =>
|
|
1055
|
-
|
|
1056
|
-
itemType: "
|
|
1057
|
-
|
|
1058
|
-
label: action.name,
|
|
1059
|
-
description: action.actionType,
|
|
1060
|
-
seconds: action.seconds,
|
|
1061
|
-
embedUrl: this.getEmbedUrl("lessonAction", action.id)
|
|
1062
|
-
})));
|
|
1242
|
+
sectionActionsMap.set(section.id, section.actions.map((action) => {
|
|
1243
|
+
const embedUrl = this.getEmbedUrl("action", action.id);
|
|
1244
|
+
return { id: action.id, itemType: "action", relatedId: action.id, label: action.name, description: action.actionType, seconds: action.seconds, children: [{ id: action.id + "-file", itemType: "file", label: action.name, seconds: action.seconds, embedUrl }] };
|
|
1245
|
+
}));
|
|
1063
1246
|
}
|
|
1064
1247
|
}
|
|
1065
1248
|
}
|
|
1066
1249
|
const processItem = (item) => {
|
|
1067
1250
|
const relatedId = item.relatedId;
|
|
1068
|
-
const itemType = item.itemType;
|
|
1251
|
+
const itemType = this.normalizeItemType(item.itemType);
|
|
1069
1252
|
const children = item.children;
|
|
1070
1253
|
let processedChildren;
|
|
1071
1254
|
if (children) {
|
|
1072
1255
|
processedChildren = children.map((child) => {
|
|
1073
1256
|
const childRelatedId = child.relatedId;
|
|
1257
|
+
const childItemType = this.normalizeItemType(child.itemType);
|
|
1074
1258
|
if (childRelatedId && sectionActionsMap.has(childRelatedId)) {
|
|
1075
|
-
return {
|
|
1076
|
-
id: child.id,
|
|
1077
|
-
itemType: child.itemType,
|
|
1078
|
-
relatedId: childRelatedId,
|
|
1079
|
-
label: child.label,
|
|
1080
|
-
description: child.description,
|
|
1081
|
-
seconds: child.seconds,
|
|
1082
|
-
children: sectionActionsMap.get(childRelatedId),
|
|
1083
|
-
embedUrl: this.getEmbedUrl(child.itemType, childRelatedId)
|
|
1084
|
-
};
|
|
1259
|
+
return { id: child.id, itemType: childItemType, relatedId: childRelatedId, label: child.label, description: child.description, seconds: child.seconds, children: sectionActionsMap.get(childRelatedId), embedUrl: this.getEmbedUrl(childItemType, childRelatedId) };
|
|
1085
1260
|
}
|
|
1086
1261
|
return processItem(child);
|
|
1087
1262
|
});
|
|
1088
1263
|
}
|
|
1089
|
-
return {
|
|
1090
|
-
id: item.id,
|
|
1091
|
-
itemType,
|
|
1092
|
-
relatedId,
|
|
1093
|
-
label: item.label,
|
|
1094
|
-
description: item.description,
|
|
1095
|
-
seconds: item.seconds,
|
|
1096
|
-
children: processedChildren,
|
|
1097
|
-
embedUrl: this.getEmbedUrl(itemType, relatedId)
|
|
1098
|
-
};
|
|
1099
|
-
};
|
|
1100
|
-
return {
|
|
1101
|
-
venueName: planItemsResponse.venueName,
|
|
1102
|
-
items: (planItemsResponse.items || []).map(processItem)
|
|
1264
|
+
return { id: item.id, itemType, relatedId, label: item.label, description: item.description, seconds: item.seconds, children: processedChildren, embedUrl: this.getEmbedUrl(itemType, relatedId) };
|
|
1103
1265
|
};
|
|
1266
|
+
return { venueName: planItemsResponse.venueName, items: (planItemsResponse.items || []).map(processItem) };
|
|
1267
|
+
}
|
|
1268
|
+
normalizeItemType(type) {
|
|
1269
|
+
if (type === "lessonSection") return "section";
|
|
1270
|
+
if (type === "lessonAction") return "action";
|
|
1271
|
+
if (type === "lessonAddOn") return "addon";
|
|
1272
|
+
return type;
|
|
1104
1273
|
}
|
|
1105
1274
|
getEmbedUrl(itemType, relatedId) {
|
|
1106
1275
|
if (!relatedId) return void 0;
|
|
1107
1276
|
const baseUrl = "https://lessons.church";
|
|
1108
1277
|
switch (itemType) {
|
|
1109
|
-
case "
|
|
1278
|
+
case "action":
|
|
1110
1279
|
return `${baseUrl}/embed/action/${relatedId}`;
|
|
1111
|
-
case "
|
|
1280
|
+
case "addon":
|
|
1112
1281
|
return `${baseUrl}/embed/addon/${relatedId}`;
|
|
1113
|
-
case "
|
|
1282
|
+
case "section":
|
|
1114
1283
|
return `${baseUrl}/embed/section/${relatedId}`;
|
|
1115
1284
|
default:
|
|
1116
1285
|
return void 0;
|
|
@@ -1128,16 +1297,7 @@ var LessonsChurchProvider = class extends ContentProvider {
|
|
|
1128
1297
|
for (const file of action.files || []) {
|
|
1129
1298
|
if (!file.url) continue;
|
|
1130
1299
|
const embedUrl = action.id ? `https://lessons.church/embed/action/${action.id}` : void 0;
|
|
1131
|
-
const contentFile = {
|
|
1132
|
-
type: "file",
|
|
1133
|
-
id: file.id || "",
|
|
1134
|
-
title: file.name || "",
|
|
1135
|
-
mediaType: detectMediaType(file.url, file.fileType),
|
|
1136
|
-
image: venue.lessonImage,
|
|
1137
|
-
url: file.url,
|
|
1138
|
-
embedUrl,
|
|
1139
|
-
providerData: { seconds: file.seconds, streamUrl: file.streamUrl }
|
|
1140
|
-
};
|
|
1300
|
+
const contentFile = { type: "file", id: file.id || "", title: file.name || "", mediaType: detectMediaType(file.url, file.fileType), image: venue.lessonImage, url: file.url, embedUrl, providerData: { seconds: file.seconds, streamUrl: file.streamUrl } };
|
|
1141
1301
|
files.push(contentFile);
|
|
1142
1302
|
allFiles.push(contentFile);
|
|
1143
1303
|
}
|
|
@@ -1149,51 +1309,69 @@ var LessonsChurchProvider = class extends ContentProvider {
|
|
|
1149
1309
|
sections.push({ id: section.id || "", name: section.name || "Untitled Section", presentations });
|
|
1150
1310
|
}
|
|
1151
1311
|
}
|
|
1152
|
-
return {
|
|
1153
|
-
id: venue.id || "",
|
|
1154
|
-
name: venue.lessonName || venue.name || "Plan",
|
|
1155
|
-
description: venue.lessonDescription,
|
|
1156
|
-
image: venue.lessonImage,
|
|
1157
|
-
sections,
|
|
1158
|
-
allFiles
|
|
1159
|
-
};
|
|
1312
|
+
return { id: venue.id || "", name: venue.lessonName || venue.name || "Plan", description: venue.lessonDescription, image: venue.lessonImage, sections, allFiles };
|
|
1160
1313
|
}
|
|
1161
1314
|
};
|
|
1162
1315
|
|
|
1163
|
-
// src/providers/
|
|
1164
|
-
function
|
|
1316
|
+
// src/providers/b1Church/auth.ts
|
|
1317
|
+
async function generateCodeChallenge(verifier) {
|
|
1318
|
+
const encoder = new TextEncoder();
|
|
1319
|
+
const data = encoder.encode(verifier);
|
|
1320
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1321
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
1322
|
+
let binary = "";
|
|
1323
|
+
for (let i = 0; i < hashArray.length; i++) {
|
|
1324
|
+
binary += String.fromCharCode(hashArray[i]);
|
|
1325
|
+
}
|
|
1326
|
+
const base64 = btoa(binary);
|
|
1327
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1328
|
+
}
|
|
1329
|
+
async function buildB1AuthUrl(config, appBase, redirectUri, codeVerifier, state) {
|
|
1330
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
1165
1331
|
const oauthParams = new URLSearchParams({
|
|
1332
|
+
response_type: "code",
|
|
1166
1333
|
client_id: config.clientId,
|
|
1167
1334
|
redirect_uri: redirectUri,
|
|
1168
|
-
|
|
1169
|
-
|
|
1335
|
+
scope: config.scopes.join(" "),
|
|
1336
|
+
code_challenge: codeChallenge,
|
|
1337
|
+
code_challenge_method: "S256",
|
|
1338
|
+
state: state || ""
|
|
1170
1339
|
});
|
|
1171
|
-
|
|
1172
|
-
|
|
1340
|
+
const url = `${appBase}/oauth?${oauthParams.toString()}`;
|
|
1341
|
+
return { url, challengeMethod: "S256" };
|
|
1342
|
+
}
|
|
1343
|
+
async function exchangeCodeForTokensWithPKCE(config, code, redirectUri, codeVerifier) {
|
|
1344
|
+
try {
|
|
1345
|
+
const params = { grant_type: "authorization_code", code, client_id: config.clientId, code_verifier: codeVerifier, redirect_uri: redirectUri };
|
|
1346
|
+
const tokenUrl = `${config.oauthBase}/token`;
|
|
1347
|
+
console.log(`B1Church PKCE token exchange request to: ${tokenUrl}`);
|
|
1348
|
+
console.log(` - client_id: ${config.clientId}`);
|
|
1349
|
+
console.log(` - redirect_uri: ${redirectUri}`);
|
|
1350
|
+
console.log(` - code: ${code.substring(0, 10)}...`);
|
|
1351
|
+
const response = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params) });
|
|
1352
|
+
console.log(`B1Church token response status: ${response.status}`);
|
|
1353
|
+
if (!response.ok) {
|
|
1354
|
+
const errorText = await response.text();
|
|
1355
|
+
console.error(`B1Church token exchange failed: ${response.status} - ${errorText}`);
|
|
1356
|
+
return null;
|
|
1357
|
+
}
|
|
1358
|
+
const data = await response.json();
|
|
1359
|
+
console.log(`B1Church token exchange successful, got access_token: ${!!data.access_token}`);
|
|
1360
|
+
return { access_token: data.access_token, refresh_token: data.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || config.scopes.join(" ") };
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
console.error("B1Church token exchange error:", error);
|
|
1363
|
+
return null;
|
|
1173
1364
|
}
|
|
1174
|
-
const returnUrl = `/oauth?${oauthParams.toString()}`;
|
|
1175
|
-
const url = `${appBase}/login?returnUrl=${encodeURIComponent(returnUrl)}`;
|
|
1176
|
-
return { url, challengeMethod: "none" };
|
|
1177
1365
|
}
|
|
1178
1366
|
async function exchangeCodeForTokensWithSecret(config, code, redirectUri, clientSecret) {
|
|
1179
1367
|
try {
|
|
1180
|
-
const params = {
|
|
1181
|
-
grant_type: "authorization_code",
|
|
1182
|
-
code,
|
|
1183
|
-
client_id: config.clientId,
|
|
1184
|
-
client_secret: clientSecret,
|
|
1185
|
-
redirect_uri: redirectUri
|
|
1186
|
-
};
|
|
1368
|
+
const params = { grant_type: "authorization_code", code, client_id: config.clientId, client_secret: clientSecret, redirect_uri: redirectUri };
|
|
1187
1369
|
const tokenUrl = `${config.oauthBase}/token`;
|
|
1188
1370
|
console.log(`B1Church token exchange request to: ${tokenUrl}`);
|
|
1189
1371
|
console.log(` - client_id: ${config.clientId}`);
|
|
1190
1372
|
console.log(` - redirect_uri: ${redirectUri}`);
|
|
1191
1373
|
console.log(` - code: ${code.substring(0, 10)}...`);
|
|
1192
|
-
const response = await fetch(tokenUrl, {
|
|
1193
|
-
method: "POST",
|
|
1194
|
-
headers: { "Content-Type": "application/json" },
|
|
1195
|
-
body: JSON.stringify(params)
|
|
1196
|
-
});
|
|
1374
|
+
const response = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params) });
|
|
1197
1375
|
console.log(`B1Church token response status: ${response.status}`);
|
|
1198
1376
|
if (!response.ok) {
|
|
1199
1377
|
const errorText = await response.text();
|
|
@@ -1202,14 +1380,7 @@ async function exchangeCodeForTokensWithSecret(config, code, redirectUri, client
|
|
|
1202
1380
|
}
|
|
1203
1381
|
const data = await response.json();
|
|
1204
1382
|
console.log(`B1Church token exchange successful, got access_token: ${!!data.access_token}`);
|
|
1205
|
-
return {
|
|
1206
|
-
access_token: data.access_token,
|
|
1207
|
-
refresh_token: data.refresh_token,
|
|
1208
|
-
token_type: data.token_type || "Bearer",
|
|
1209
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
1210
|
-
expires_in: data.expires_in,
|
|
1211
|
-
scope: data.scope || config.scopes.join(" ")
|
|
1212
|
-
};
|
|
1383
|
+
return { access_token: data.access_token, refresh_token: data.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || config.scopes.join(" ") };
|
|
1213
1384
|
} catch (error) {
|
|
1214
1385
|
console.error("B1Church token exchange error:", error);
|
|
1215
1386
|
return null;
|
|
@@ -1218,27 +1389,11 @@ async function exchangeCodeForTokensWithSecret(config, code, redirectUri, client
|
|
|
1218
1389
|
async function refreshTokenWithSecret(config, auth, clientSecret) {
|
|
1219
1390
|
if (!auth.refresh_token) return null;
|
|
1220
1391
|
try {
|
|
1221
|
-
const params = {
|
|
1222
|
-
|
|
1223
|
-
refresh_token: auth.refresh_token,
|
|
1224
|
-
client_id: config.clientId,
|
|
1225
|
-
client_secret: clientSecret
|
|
1226
|
-
};
|
|
1227
|
-
const response = await fetch(`${config.oauthBase}/token`, {
|
|
1228
|
-
method: "POST",
|
|
1229
|
-
headers: { "Content-Type": "application/json" },
|
|
1230
|
-
body: JSON.stringify(params)
|
|
1231
|
-
});
|
|
1392
|
+
const params = { grant_type: "refresh_token", refresh_token: auth.refresh_token, client_id: config.clientId, client_secret: clientSecret };
|
|
1393
|
+
const response = await fetch(`${config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params) });
|
|
1232
1394
|
if (!response.ok) return null;
|
|
1233
1395
|
const data = await response.json();
|
|
1234
|
-
return {
|
|
1235
|
-
access_token: data.access_token,
|
|
1236
|
-
refresh_token: data.refresh_token || auth.refresh_token,
|
|
1237
|
-
token_type: data.token_type || "Bearer",
|
|
1238
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
1239
|
-
expires_in: data.expires_in,
|
|
1240
|
-
scope: data.scope || auth.scope
|
|
1241
|
-
};
|
|
1396
|
+
return { access_token: data.access_token, refresh_token: data.refresh_token || auth.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || auth.scope };
|
|
1242
1397
|
} catch {
|
|
1243
1398
|
return null;
|
|
1244
1399
|
}
|
|
@@ -1246,14 +1401,7 @@ async function refreshTokenWithSecret(config, auth, clientSecret) {
|
|
|
1246
1401
|
async function initiateDeviceFlow(config) {
|
|
1247
1402
|
if (!config.supportsDeviceFlow || !config.deviceAuthEndpoint) return null;
|
|
1248
1403
|
try {
|
|
1249
|
-
const response = await fetch(`${config.oauthBase}${config.deviceAuthEndpoint}`, {
|
|
1250
|
-
method: "POST",
|
|
1251
|
-
headers: { "Content-Type": "application/json" },
|
|
1252
|
-
body: JSON.stringify({
|
|
1253
|
-
client_id: config.clientId,
|
|
1254
|
-
scope: config.scopes.join(" ")
|
|
1255
|
-
})
|
|
1256
|
-
});
|
|
1404
|
+
const response = await fetch(`${config.oauthBase}${config.deviceAuthEndpoint}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: config.clientId, scope: config.scopes.join(" ") }) });
|
|
1257
1405
|
if (!response.ok) {
|
|
1258
1406
|
const errorText = await response.text();
|
|
1259
1407
|
console.error(`B1Church device authorize failed: ${response.status} - ${errorText}`);
|
|
@@ -1267,25 +1415,10 @@ async function initiateDeviceFlow(config) {
|
|
|
1267
1415
|
}
|
|
1268
1416
|
async function pollDeviceFlowToken(config, deviceCode) {
|
|
1269
1417
|
try {
|
|
1270
|
-
const response = await fetch(`${config.oauthBase}/token`, {
|
|
1271
|
-
method: "POST",
|
|
1272
|
-
headers: { "Content-Type": "application/json" },
|
|
1273
|
-
body: JSON.stringify({
|
|
1274
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
1275
|
-
device_code: deviceCode,
|
|
1276
|
-
client_id: config.clientId
|
|
1277
|
-
})
|
|
1278
|
-
});
|
|
1418
|
+
const response = await fetch(`${config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: deviceCode, client_id: config.clientId }) });
|
|
1279
1419
|
if (response.ok) {
|
|
1280
1420
|
const data = await response.json();
|
|
1281
|
-
return {
|
|
1282
|
-
access_token: data.access_token,
|
|
1283
|
-
refresh_token: data.refresh_token,
|
|
1284
|
-
token_type: data.token_type || "Bearer",
|
|
1285
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
1286
|
-
expires_in: data.expires_in,
|
|
1287
|
-
scope: data.scope || config.scopes.join(" ")
|
|
1288
|
-
};
|
|
1421
|
+
return { access_token: data.access_token, refresh_token: data.refresh_token, token_type: data.token_type || "Bearer", created_at: Math.floor(Date.now() / 1e3), expires_in: data.expires_in, scope: data.scope || config.scopes.join(" ") };
|
|
1289
1422
|
}
|
|
1290
1423
|
const errorData = await response.json();
|
|
1291
1424
|
switch (errorData.error) {
|
|
@@ -1305,7 +1438,7 @@ async function pollDeviceFlowToken(config, deviceCode) {
|
|
|
1305
1438
|
}
|
|
1306
1439
|
}
|
|
1307
1440
|
|
|
1308
|
-
// src/providers/
|
|
1441
|
+
// src/providers/b1Church/api.ts
|
|
1309
1442
|
var API_BASE = "https://api.churchapps.org";
|
|
1310
1443
|
var LESSONS_API_BASE = "https://api.lessons.church";
|
|
1311
1444
|
var CONTENT_API_BASE = "https://contentapi.churchapps.org";
|
|
@@ -1337,10 +1470,7 @@ async function fetchPlans(planTypeId, auth) {
|
|
|
1337
1470
|
async function fetchVenueFeed(venueId) {
|
|
1338
1471
|
try {
|
|
1339
1472
|
const url = `${LESSONS_API_BASE}/venues/public/feed/${venueId}`;
|
|
1340
|
-
const response = await fetch(url, {
|
|
1341
|
-
method: "GET",
|
|
1342
|
-
headers: { Accept: "application/json" }
|
|
1343
|
-
});
|
|
1473
|
+
const response = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
|
1344
1474
|
if (!response.ok) return null;
|
|
1345
1475
|
return await response.json();
|
|
1346
1476
|
} catch {
|
|
@@ -1350,9 +1480,29 @@ async function fetchVenueFeed(venueId) {
|
|
|
1350
1480
|
async function fetchArrangementKey(churchId, arrangementId) {
|
|
1351
1481
|
try {
|
|
1352
1482
|
const url = `${CONTENT_API_BASE}/arrangementKeys/presenter/${churchId}/${arrangementId}`;
|
|
1483
|
+
const response = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
|
1484
|
+
if (!response.ok) return null;
|
|
1485
|
+
return await response.json();
|
|
1486
|
+
} catch {
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
async function fetchFromProviderProxy(method, ministryId, providerId, path, authData, resolution) {
|
|
1491
|
+
try {
|
|
1492
|
+
const url = `${API_BASE}/doing/providerProxy/${method}`;
|
|
1493
|
+
const headers = {
|
|
1494
|
+
"Content-Type": "application/json",
|
|
1495
|
+
Accept: "application/json"
|
|
1496
|
+
};
|
|
1497
|
+
if (authData) {
|
|
1498
|
+
headers["Authorization"] = `Bearer ${authData.access_token}`;
|
|
1499
|
+
}
|
|
1500
|
+
const body = { ministryId, providerId, path };
|
|
1501
|
+
if (resolution !== void 0) body.resolution = resolution;
|
|
1353
1502
|
const response = await fetch(url, {
|
|
1354
|
-
method: "
|
|
1355
|
-
headers
|
|
1503
|
+
method: "POST",
|
|
1504
|
+
headers,
|
|
1505
|
+
body: JSON.stringify(body)
|
|
1356
1506
|
});
|
|
1357
1507
|
if (!response.ok) return null;
|
|
1358
1508
|
return await response.json();
|
|
@@ -1361,107 +1511,39 @@ async function fetchArrangementKey(churchId, arrangementId) {
|
|
|
1361
1511
|
}
|
|
1362
1512
|
}
|
|
1363
1513
|
|
|
1364
|
-
// src/providers/
|
|
1514
|
+
// src/providers/b1Church/converters.ts
|
|
1365
1515
|
function ministryToFolder(ministry) {
|
|
1366
|
-
return {
|
|
1367
|
-
type: "folder",
|
|
1368
|
-
id: ministry.id,
|
|
1369
|
-
title: ministry.name,
|
|
1370
|
-
image: ministry.photoUrl,
|
|
1371
|
-
providerData: {
|
|
1372
|
-
level: "ministry",
|
|
1373
|
-
ministryId: ministry.id,
|
|
1374
|
-
churchId: ministry.churchId
|
|
1375
|
-
}
|
|
1376
|
-
};
|
|
1516
|
+
return { type: "folder", id: ministry.id, title: ministry.name, path: "", image: ministry.photoUrl, providerData: { level: "ministry", ministryId: ministry.id, churchId: ministry.churchId } };
|
|
1377
1517
|
}
|
|
1378
1518
|
function planTypeToFolder(planType, ministryId) {
|
|
1379
|
-
return {
|
|
1380
|
-
type: "folder",
|
|
1381
|
-
id: planType.id,
|
|
1382
|
-
title: planType.name,
|
|
1383
|
-
providerData: {
|
|
1384
|
-
level: "planType",
|
|
1385
|
-
planTypeId: planType.id,
|
|
1386
|
-
ministryId,
|
|
1387
|
-
churchId: planType.churchId
|
|
1388
|
-
}
|
|
1389
|
-
};
|
|
1519
|
+
return { type: "folder", id: planType.id, title: planType.name, path: "", providerData: { level: "planType", planTypeId: planType.id, ministryId, churchId: planType.churchId } };
|
|
1390
1520
|
}
|
|
1391
1521
|
function planToFolder(plan) {
|
|
1392
|
-
return {
|
|
1393
|
-
type: "folder",
|
|
1394
|
-
id: plan.id,
|
|
1395
|
-
title: plan.name,
|
|
1396
|
-
providerData: {
|
|
1397
|
-
isLeaf: true,
|
|
1398
|
-
level: "plan",
|
|
1399
|
-
planId: plan.id,
|
|
1400
|
-
planTypeId: plan.planTypeId,
|
|
1401
|
-
ministryId: plan.ministryId,
|
|
1402
|
-
churchId: plan.churchId,
|
|
1403
|
-
serviceDate: plan.serviceDate,
|
|
1404
|
-
contentType: plan.contentType,
|
|
1405
|
-
contentId: plan.contentId
|
|
1406
|
-
}
|
|
1407
|
-
};
|
|
1522
|
+
return { type: "folder", id: plan.id, title: plan.name, path: "", isLeaf: true, providerData: { level: "plan", planId: plan.id, planTypeId: plan.planTypeId, ministryId: plan.ministryId, churchId: plan.churchId, serviceDate: plan.serviceDate, contentType: plan.contentType, contentId: plan.contentId } };
|
|
1408
1523
|
}
|
|
1409
1524
|
async function planItemToPresentation(item, venueFeed) {
|
|
1410
1525
|
const itemType = item.itemType;
|
|
1411
1526
|
if (itemType === "arrangementKey" && item.churchId && item.relatedId) {
|
|
1412
1527
|
const songData = await fetchArrangementKey(item.churchId, item.relatedId);
|
|
1413
|
-
if (songData)
|
|
1414
|
-
return arrangementToPresentation(item, songData);
|
|
1415
|
-
}
|
|
1528
|
+
if (songData) return arrangementToPresentation(item, songData);
|
|
1416
1529
|
}
|
|
1417
|
-
if ((itemType === "lessonSection" || itemType === "lessonAction" || itemType === "lessonAddOn") && venueFeed) {
|
|
1530
|
+
if ((itemType === "lessonSection" || itemType === "section" || itemType === "lessonAction" || itemType === "action" || itemType === "lessonAddOn" || itemType === "addon") && venueFeed) {
|
|
1418
1531
|
const files = getFilesFromVenueFeed(venueFeed, itemType, item.relatedId);
|
|
1419
|
-
if (files.length > 0) {
|
|
1420
|
-
return {
|
|
1421
|
-
id: item.id,
|
|
1422
|
-
name: item.label || "Lesson Content",
|
|
1423
|
-
actionType: itemType === "lessonAddOn" ? "add-on" : "play",
|
|
1424
|
-
files
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1532
|
+
if (files.length > 0) return { id: item.id, name: item.label || "Lesson Content", actionType: itemType === "lessonAddOn" || itemType === "addon" ? "add-on" : "play", files };
|
|
1427
1533
|
}
|
|
1428
1534
|
if (itemType === "item" || itemType === "header") {
|
|
1429
|
-
return {
|
|
1430
|
-
id: item.id,
|
|
1431
|
-
name: item.label || "",
|
|
1432
|
-
actionType: "other",
|
|
1433
|
-
files: [],
|
|
1434
|
-
providerData: {
|
|
1435
|
-
itemType,
|
|
1436
|
-
description: item.description,
|
|
1437
|
-
seconds: item.seconds
|
|
1438
|
-
}
|
|
1439
|
-
};
|
|
1535
|
+
return { id: item.id, name: item.label || "", actionType: "other", files: [], providerData: { itemType, description: item.description, seconds: item.seconds } };
|
|
1440
1536
|
}
|
|
1441
1537
|
return null;
|
|
1442
1538
|
}
|
|
1443
1539
|
function arrangementToPresentation(item, songData) {
|
|
1444
1540
|
const title = songData.songDetail?.title || item.label || "Song";
|
|
1445
|
-
return {
|
|
1446
|
-
id: item.id,
|
|
1447
|
-
name: title,
|
|
1448
|
-
actionType: "other",
|
|
1449
|
-
files: [],
|
|
1450
|
-
providerData: {
|
|
1451
|
-
itemType: "song",
|
|
1452
|
-
title,
|
|
1453
|
-
artist: songData.songDetail?.artist,
|
|
1454
|
-
lyrics: songData.arrangement?.lyrics,
|
|
1455
|
-
keySignature: songData.arrangementKey?.keySignature,
|
|
1456
|
-
arrangementName: songData.arrangement?.name,
|
|
1457
|
-
seconds: songData.songDetail?.seconds || item.seconds
|
|
1458
|
-
}
|
|
1459
|
-
};
|
|
1541
|
+
return { id: item.id, name: title, actionType: "other", files: [], providerData: { itemType: "song", title, artist: songData.songDetail?.artist, lyrics: songData.arrangement?.lyrics, keySignature: songData.arrangementKey?.keySignature, arrangementName: songData.arrangement?.name, seconds: songData.songDetail?.seconds || item.seconds } };
|
|
1460
1542
|
}
|
|
1461
1543
|
function getFilesFromVenueFeed(venueFeed, itemType, relatedId) {
|
|
1462
1544
|
const files = [];
|
|
1463
1545
|
if (!relatedId) return files;
|
|
1464
|
-
if (itemType === "lessonSection") {
|
|
1546
|
+
if (itemType === "lessonSection" || itemType === "section") {
|
|
1465
1547
|
for (const section of venueFeed.sections || []) {
|
|
1466
1548
|
if (section.id === relatedId) {
|
|
1467
1549
|
for (const action of section.actions || []) {
|
|
@@ -1473,7 +1555,7 @@ function getFilesFromVenueFeed(venueFeed, itemType, relatedId) {
|
|
|
1473
1555
|
break;
|
|
1474
1556
|
}
|
|
1475
1557
|
}
|
|
1476
|
-
} else if (itemType === "lessonAction") {
|
|
1558
|
+
} else if (itemType === "lessonAction" || itemType === "action") {
|
|
1477
1559
|
for (const section of venueFeed.sections || []) {
|
|
1478
1560
|
for (const action of section.actions || []) {
|
|
1479
1561
|
if (action.id === relatedId) {
|
|
@@ -1486,75 +1568,51 @@ function getFilesFromVenueFeed(venueFeed, itemType, relatedId) {
|
|
|
1486
1568
|
return files;
|
|
1487
1569
|
}
|
|
1488
1570
|
function convertFeedFiles(feedFiles, thumbnailImage) {
|
|
1489
|
-
return feedFiles.filter((f) => f.url).map((f) => ({
|
|
1490
|
-
type: "file",
|
|
1491
|
-
id: f.id || "",
|
|
1492
|
-
title: f.name || "",
|
|
1493
|
-
mediaType: detectMediaType(f.url || "", f.fileType),
|
|
1494
|
-
image: thumbnailImage,
|
|
1495
|
-
url: f.url || "",
|
|
1496
|
-
providerData: { seconds: f.seconds, streamUrl: f.streamUrl }
|
|
1497
|
-
}));
|
|
1571
|
+
return feedFiles.filter((f) => f.url).map((f) => ({ type: "file", id: f.id || "", title: f.name || "", mediaType: detectMediaType(f.url || "", f.fileType), image: thumbnailImage, url: f.url || "", providerData: { seconds: f.seconds, streamUrl: f.streamUrl } }));
|
|
1498
1572
|
}
|
|
1499
1573
|
function planItemToInstruction(item) {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1574
|
+
let itemType = item.itemType;
|
|
1575
|
+
switch (item.itemType) {
|
|
1576
|
+
case "lessonSection":
|
|
1577
|
+
itemType = "section";
|
|
1578
|
+
break;
|
|
1579
|
+
case "lessonAction":
|
|
1580
|
+
itemType = "action";
|
|
1581
|
+
break;
|
|
1582
|
+
case "lessonAddOn":
|
|
1583
|
+
itemType = "addon";
|
|
1584
|
+
break;
|
|
1585
|
+
}
|
|
1586
|
+
return { id: item.id, itemType, relatedId: item.relatedId, label: item.label, description: item.description, seconds: item.seconds, children: item.children?.map(planItemToInstruction) };
|
|
1509
1587
|
}
|
|
1510
1588
|
|
|
1511
|
-
// src/providers/
|
|
1512
|
-
var
|
|
1589
|
+
// src/providers/b1Church/B1ChurchProvider.ts
|
|
1590
|
+
var INTERNAL_PROVIDERS = ["b1church", "lessonschurch"];
|
|
1591
|
+
function isExternalProviderItem(item) {
|
|
1592
|
+
if (!item.providerId || INTERNAL_PROVIDERS.includes(item.providerId)) return false;
|
|
1593
|
+
const itemType = item.itemType || "";
|
|
1594
|
+
return itemType.startsWith("provider");
|
|
1595
|
+
}
|
|
1596
|
+
var B1ChurchProvider = class {
|
|
1513
1597
|
constructor() {
|
|
1514
|
-
|
|
1598
|
+
this.apiHelper = new ApiHelper();
|
|
1515
1599
|
this.id = "b1church";
|
|
1516
1600
|
this.name = "B1.Church";
|
|
1517
|
-
this.logos = {
|
|
1518
|
-
|
|
1519
|
-
dark: "https://b1.church/b1-church-logo.png"
|
|
1520
|
-
};
|
|
1521
|
-
this.config = {
|
|
1522
|
-
id: "b1church",
|
|
1523
|
-
name: "B1.Church",
|
|
1524
|
-
apiBase: `${API_BASE}/doing`,
|
|
1525
|
-
oauthBase: `${API_BASE}/membership/oauth`,
|
|
1526
|
-
clientId: "",
|
|
1527
|
-
// Consumer must provide client_id
|
|
1528
|
-
scopes: ["plans"],
|
|
1529
|
-
supportsDeviceFlow: true,
|
|
1530
|
-
deviceAuthEndpoint: "/device/authorize",
|
|
1531
|
-
endpoints: {
|
|
1532
|
-
planItems: (churchId, planId) => `/planItems/presenter/${churchId}/${planId}`
|
|
1533
|
-
}
|
|
1534
|
-
};
|
|
1601
|
+
this.logos = { light: "https://b1.church/b1-church-logo.png", dark: "https://b1.church/b1-church-logo.png" };
|
|
1602
|
+
this.config = { id: "b1church", name: "B1.Church", apiBase: `${API_BASE}/doing`, oauthBase: `${API_BASE}/membership/oauth`, clientId: "nsowldn58dk", scopes: ["plans"], supportsDeviceFlow: true, deviceAuthEndpoint: "/device/authorize", endpoints: { planItems: (churchId, planId) => `/planItems/presenter/${churchId}/${planId}` } };
|
|
1535
1603
|
this.appBase = "https://admin.b1.church";
|
|
1604
|
+
this.requiresAuth = true;
|
|
1605
|
+
this.authTypes = ["oauth_pkce", "device_flow"];
|
|
1606
|
+
this.capabilities = { browse: true, presentations: true, playlist: true, instructions: true, expandedInstructions: true, mediaLicensing: false };
|
|
1536
1607
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
// ============================================================
|
|
1540
|
-
requiresAuth() {
|
|
1541
|
-
return true;
|
|
1608
|
+
async apiRequest(path, authData) {
|
|
1609
|
+
return this.apiHelper.apiRequest(this.config, this.id, path, authData);
|
|
1542
1610
|
}
|
|
1543
|
-
|
|
1544
|
-
return
|
|
1545
|
-
browse: true,
|
|
1546
|
-
presentations: true,
|
|
1547
|
-
playlist: true,
|
|
1548
|
-
instructions: true,
|
|
1549
|
-
expandedInstructions: true,
|
|
1550
|
-
mediaLicensing: false
|
|
1551
|
-
};
|
|
1611
|
+
async buildAuthUrl(codeVerifier, redirectUri, state) {
|
|
1612
|
+
return buildB1AuthUrl(this.config, this.appBase, redirectUri, codeVerifier, state);
|
|
1552
1613
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
// ============================================================
|
|
1556
|
-
async buildAuthUrl(_codeVerifier, redirectUri, state) {
|
|
1557
|
-
return buildB1AuthUrl(this.config, this.appBase, redirectUri, state);
|
|
1614
|
+
async exchangeCodeForTokensWithPKCE(code, redirectUri, codeVerifier) {
|
|
1615
|
+
return exchangeCodeForTokensWithPKCE(this.config, code, redirectUri, codeVerifier);
|
|
1558
1616
|
}
|
|
1559
1617
|
async exchangeCodeForTokensWithSecret(code, redirectUri, clientSecret) {
|
|
1560
1618
|
return exchangeCodeForTokensWithSecret(this.config, code, redirectUri, clientSecret);
|
|
@@ -1568,48 +1626,69 @@ var B1ChurchProvider = class extends ContentProvider {
|
|
|
1568
1626
|
async pollDeviceFlowToken(deviceCode) {
|
|
1569
1627
|
return pollDeviceFlowToken(this.config, deviceCode);
|
|
1570
1628
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
if (!folder) {
|
|
1629
|
+
async browse(path, authData) {
|
|
1630
|
+
const { segments, depth } = parsePath(path);
|
|
1631
|
+
if (depth === 0) {
|
|
1632
|
+
return [{
|
|
1633
|
+
type: "folder",
|
|
1634
|
+
id: "ministries-root",
|
|
1635
|
+
title: "Ministries",
|
|
1636
|
+
path: "/ministries"
|
|
1637
|
+
}];
|
|
1638
|
+
}
|
|
1639
|
+
const root = segments[0];
|
|
1640
|
+
if (root !== "ministries") return [];
|
|
1641
|
+
if (depth === 1) {
|
|
1585
1642
|
const ministries = await fetchMinistries(authData);
|
|
1586
|
-
return ministries.map(
|
|
1643
|
+
return ministries.map((m) => {
|
|
1644
|
+
const folder = ministryToFolder(m);
|
|
1645
|
+
const ministryId = folder.providerData?.ministryId || folder.id;
|
|
1646
|
+
return { ...folder, path: `/ministries/${ministryId}` };
|
|
1647
|
+
});
|
|
1587
1648
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
const ministryId = folder.providerData?.ministryId;
|
|
1591
|
-
if (!ministryId) return [];
|
|
1649
|
+
if (depth === 2) {
|
|
1650
|
+
const ministryId = segments[1];
|
|
1592
1651
|
const planTypes = await fetchPlanTypes(ministryId, authData);
|
|
1593
|
-
return planTypes.map((pt) =>
|
|
1652
|
+
return planTypes.map((pt) => {
|
|
1653
|
+
const folder = planTypeToFolder(pt, ministryId);
|
|
1654
|
+
const planTypeId = folder.providerData?.planTypeId || folder.id;
|
|
1655
|
+
return { ...folder, path: `/ministries/${ministryId}/${planTypeId}` };
|
|
1656
|
+
});
|
|
1594
1657
|
}
|
|
1595
|
-
if (
|
|
1596
|
-
const
|
|
1597
|
-
|
|
1658
|
+
if (depth === 3) {
|
|
1659
|
+
const ministryId = segments[1];
|
|
1660
|
+
const planTypeId = segments[2];
|
|
1598
1661
|
const plans = await fetchPlans(planTypeId, authData);
|
|
1599
|
-
return plans.map(
|
|
1662
|
+
return plans.map((p) => {
|
|
1663
|
+
const folder = planToFolder(p);
|
|
1664
|
+
const planId = folder.providerData?.planId || folder.id;
|
|
1665
|
+
return {
|
|
1666
|
+
...folder,
|
|
1667
|
+
isLeaf: true,
|
|
1668
|
+
path: `/ministries/${ministryId}/${planTypeId}/${planId}`
|
|
1669
|
+
};
|
|
1670
|
+
});
|
|
1600
1671
|
}
|
|
1601
1672
|
return [];
|
|
1602
1673
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
const
|
|
1610
|
-
const
|
|
1611
|
-
|
|
1612
|
-
|
|
1674
|
+
async getPresentations(path, authData) {
|
|
1675
|
+
const { segments, depth } = parsePath(path);
|
|
1676
|
+
if (depth < 4 || segments[0] !== "ministries") return null;
|
|
1677
|
+
const ministryId = segments[1];
|
|
1678
|
+
const planId = segments[3];
|
|
1679
|
+
const planTypeId = segments[2];
|
|
1680
|
+
const plans = await fetchPlans(planTypeId, authData);
|
|
1681
|
+
const planFolder = plans.find((p) => {
|
|
1682
|
+
const folder2 = planToFolder(p);
|
|
1683
|
+
return folder2.providerData?.planId === planId || folder2.id === planId;
|
|
1684
|
+
});
|
|
1685
|
+
if (!planFolder) return null;
|
|
1686
|
+
const folder = planToFolder(planFolder);
|
|
1687
|
+
const providerData = folder.providerData;
|
|
1688
|
+
const churchId = providerData?.churchId;
|
|
1689
|
+
const venueId = providerData?.contentId;
|
|
1690
|
+
const planTitle = folder.title || "Plan";
|
|
1691
|
+
if (!churchId) return null;
|
|
1613
1692
|
const pathFn = this.config.endpoints.planItems;
|
|
1614
1693
|
const planItems = await this.apiRequest(pathFn(churchId, planId), authData);
|
|
1615
1694
|
if (!planItems || !Array.isArray(planItems)) return null;
|
|
@@ -1619,49 +1698,101 @@ var B1ChurchProvider = class extends ContentProvider {
|
|
|
1619
1698
|
for (const sectionItem of planItems) {
|
|
1620
1699
|
const presentations = [];
|
|
1621
1700
|
for (const child of sectionItem.children || []) {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1701
|
+
if (isExternalProviderItem(child) && child.providerId && child.providerPath) {
|
|
1702
|
+
const externalPlan = await fetchFromProviderProxy(
|
|
1703
|
+
"getPresentations",
|
|
1704
|
+
ministryId,
|
|
1705
|
+
child.providerId,
|
|
1706
|
+
child.providerPath,
|
|
1707
|
+
authData
|
|
1708
|
+
);
|
|
1709
|
+
if (externalPlan) {
|
|
1710
|
+
for (const section of externalPlan.sections) {
|
|
1711
|
+
presentations.push(...section.presentations);
|
|
1712
|
+
}
|
|
1713
|
+
allFiles.push(...externalPlan.allFiles);
|
|
1714
|
+
}
|
|
1715
|
+
} else {
|
|
1716
|
+
const presentation = await planItemToPresentation(child, venueFeed);
|
|
1717
|
+
if (presentation) {
|
|
1718
|
+
presentations.push(presentation);
|
|
1719
|
+
allFiles.push(...presentation.files);
|
|
1720
|
+
}
|
|
1626
1721
|
}
|
|
1627
1722
|
}
|
|
1628
1723
|
if (presentations.length > 0 || sectionItem.label) {
|
|
1629
|
-
sections.push({
|
|
1630
|
-
id: sectionItem.id,
|
|
1631
|
-
name: sectionItem.label || "Section",
|
|
1632
|
-
presentations
|
|
1633
|
-
});
|
|
1724
|
+
sections.push({ id: sectionItem.id, name: sectionItem.label || "Section", presentations });
|
|
1634
1725
|
}
|
|
1635
1726
|
}
|
|
1636
|
-
return { id: planId, name:
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
const
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1727
|
+
return { id: planId, name: planTitle, sections, allFiles };
|
|
1728
|
+
}
|
|
1729
|
+
async getInstructions(path, authData) {
|
|
1730
|
+
const { segments, depth } = parsePath(path);
|
|
1731
|
+
if (depth < 4 || segments[0] !== "ministries") return null;
|
|
1732
|
+
const ministryId = segments[1];
|
|
1733
|
+
const planId = segments[3];
|
|
1734
|
+
const planTypeId = segments[2];
|
|
1735
|
+
const plans = await fetchPlans(planTypeId, authData);
|
|
1736
|
+
const planFolder = plans.find((p) => {
|
|
1737
|
+
const folder2 = planToFolder(p);
|
|
1738
|
+
return folder2.providerData?.planId === planId || folder2.id === planId;
|
|
1739
|
+
});
|
|
1740
|
+
if (!planFolder) return null;
|
|
1741
|
+
const folder = planToFolder(planFolder);
|
|
1742
|
+
const providerData = folder.providerData;
|
|
1743
|
+
const churchId = providerData?.churchId;
|
|
1744
|
+
const planTitle = folder.title || "Plan";
|
|
1745
|
+
if (!churchId) return null;
|
|
1647
1746
|
const pathFn = this.config.endpoints.planItems;
|
|
1648
1747
|
const planItems = await this.apiRequest(pathFn(churchId, planId), authData);
|
|
1649
1748
|
if (!planItems || !Array.isArray(planItems)) return null;
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1749
|
+
const processedItems = await this.processInstructionItems(planItems, ministryId, authData);
|
|
1750
|
+
return { venueName: planTitle, items: processedItems };
|
|
1751
|
+
}
|
|
1752
|
+
async getExpandedInstructions(path, authData) {
|
|
1753
|
+
return this.getInstructions(path, authData);
|
|
1754
|
+
}
|
|
1755
|
+
async processInstructionItems(items, ministryId, authData) {
|
|
1756
|
+
const result = [];
|
|
1757
|
+
for (const item of items) {
|
|
1758
|
+
if (isExternalProviderItem(item) && item.providerId && item.providerPath) {
|
|
1759
|
+
const externalInstructions = await fetchFromProviderProxy(
|
|
1760
|
+
"getExpandedInstructions",
|
|
1761
|
+
ministryId,
|
|
1762
|
+
item.providerId,
|
|
1763
|
+
item.providerPath,
|
|
1764
|
+
authData
|
|
1765
|
+
);
|
|
1766
|
+
if (externalInstructions) {
|
|
1767
|
+
result.push(...externalInstructions.items);
|
|
1768
|
+
}
|
|
1769
|
+
} else {
|
|
1770
|
+
const instructionItem = planItemToInstruction(item);
|
|
1771
|
+
if (item.children && item.children.length > 0) {
|
|
1772
|
+
instructionItem.children = await this.processInstructionItems(item.children, ministryId, authData);
|
|
1773
|
+
}
|
|
1774
|
+
result.push(instructionItem);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return result;
|
|
1654
1778
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1661
|
-
const
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1779
|
+
async getPlaylist(path, authData, resolution) {
|
|
1780
|
+
const { segments, depth } = parsePath(path);
|
|
1781
|
+
if (depth < 4 || segments[0] !== "ministries") return [];
|
|
1782
|
+
const ministryId = segments[1];
|
|
1783
|
+
const planId = segments[3];
|
|
1784
|
+
const planTypeId = segments[2];
|
|
1785
|
+
const plans = await fetchPlans(planTypeId, authData);
|
|
1786
|
+
const planFolder = plans.find((p) => {
|
|
1787
|
+
const folder2 = planToFolder(p);
|
|
1788
|
+
return folder2.providerData?.planId === planId || folder2.id === planId;
|
|
1789
|
+
});
|
|
1790
|
+
if (!planFolder) return [];
|
|
1791
|
+
const folder = planToFolder(planFolder);
|
|
1792
|
+
const providerData = folder.providerData;
|
|
1793
|
+
const churchId = providerData?.churchId;
|
|
1794
|
+
const venueId = providerData?.contentId;
|
|
1795
|
+
if (!churchId) return [];
|
|
1665
1796
|
const pathFn = this.config.endpoints.planItems;
|
|
1666
1797
|
const planItems = await this.apiRequest(pathFn(churchId, planId), authData);
|
|
1667
1798
|
if (!planItems || !Array.isArray(planItems)) return [];
|
|
@@ -1669,10 +1800,24 @@ var B1ChurchProvider = class extends ContentProvider {
|
|
|
1669
1800
|
const files = [];
|
|
1670
1801
|
for (const sectionItem of planItems) {
|
|
1671
1802
|
for (const child of sectionItem.children || []) {
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1803
|
+
if (isExternalProviderItem(child) && child.providerId && child.providerPath) {
|
|
1804
|
+
const externalFiles = await fetchFromProviderProxy(
|
|
1805
|
+
"getPlaylist",
|
|
1806
|
+
ministryId,
|
|
1807
|
+
child.providerId,
|
|
1808
|
+
child.providerPath,
|
|
1809
|
+
authData,
|
|
1810
|
+
resolution
|
|
1811
|
+
);
|
|
1812
|
+
if (externalFiles) {
|
|
1813
|
+
files.push(...externalFiles);
|
|
1814
|
+
}
|
|
1815
|
+
} else {
|
|
1816
|
+
const itemType = child.itemType;
|
|
1817
|
+
if ((itemType === "lessonSection" || itemType === "section" || itemType === "lessonAction" || itemType === "action" || itemType === "lessonAddOn" || itemType === "addon") && venueFeed) {
|
|
1818
|
+
const itemFiles = getFilesFromVenueFeed(venueFeed, itemType, child.relatedId);
|
|
1819
|
+
files.push(...itemFiles);
|
|
1820
|
+
}
|
|
1676
1821
|
}
|
|
1677
1822
|
}
|
|
1678
1823
|
}
|
|
@@ -1680,81 +1825,62 @@ var B1ChurchProvider = class extends ContentProvider {
|
|
|
1680
1825
|
}
|
|
1681
1826
|
};
|
|
1682
1827
|
|
|
1683
|
-
// src/providers/PlanningCenterProvider.ts
|
|
1684
|
-
var PlanningCenterProvider = class
|
|
1828
|
+
// src/providers/planningCenter/PlanningCenterProvider.ts
|
|
1829
|
+
var PlanningCenterProvider = class {
|
|
1685
1830
|
constructor() {
|
|
1686
|
-
|
|
1831
|
+
this.apiHelper = new ApiHelper();
|
|
1687
1832
|
this.id = "planningcenter";
|
|
1688
1833
|
this.name = "Planning Center";
|
|
1689
|
-
this.logos = {
|
|
1690
|
-
|
|
1691
|
-
dark: "https://www.planningcenter.com/icons/icon-512x512.png"
|
|
1692
|
-
};
|
|
1693
|
-
// Planning Center uses OAuth 2.0 with PKCE (handled by base ContentProvider class)
|
|
1694
|
-
this.config = {
|
|
1695
|
-
id: "planningcenter",
|
|
1696
|
-
name: "Planning Center",
|
|
1697
|
-
apiBase: "https://api.planningcenteronline.com",
|
|
1698
|
-
oauthBase: "https://api.planningcenteronline.com/oauth",
|
|
1699
|
-
clientId: "",
|
|
1700
|
-
// Consumer must provide client_id
|
|
1701
|
-
scopes: ["services"],
|
|
1702
|
-
endpoints: {
|
|
1703
|
-
serviceTypes: "/services/v2/service_types",
|
|
1704
|
-
plans: (serviceTypeId) => `/services/v2/service_types/${serviceTypeId}/plans`,
|
|
1705
|
-
planItems: (serviceTypeId, planId) => `/services/v2/service_types/${serviceTypeId}/plans/${planId}/items`,
|
|
1706
|
-
song: (itemId) => `/services/v2/songs/${itemId}`,
|
|
1707
|
-
arrangement: (songId, arrangementId) => `/services/v2/songs/${songId}/arrangements/${arrangementId}`,
|
|
1708
|
-
arrangementSections: (songId, arrangementId) => `/services/v2/songs/${songId}/arrangements/${arrangementId}/sections`,
|
|
1709
|
-
media: (mediaId) => `/services/v2/media/${mediaId}`,
|
|
1710
|
-
mediaAttachments: (mediaId) => `/services/v2/media/${mediaId}/attachments`
|
|
1711
|
-
}
|
|
1712
|
-
};
|
|
1834
|
+
this.logos = { light: "https://www.planningcenter.com/icons/icon-512x512.png", dark: "https://www.planningcenter.com/icons/icon-512x512.png" };
|
|
1835
|
+
this.config = { id: "planningcenter", name: "Planning Center", apiBase: "https://api.planningcenteronline.com", oauthBase: "https://api.planningcenteronline.com/oauth", clientId: "", scopes: ["services"], endpoints: { serviceTypes: "/services/v2/service_types", plans: (serviceTypeId) => `/services/v2/service_types/${serviceTypeId}/plans`, planItems: (serviceTypeId, planId) => `/services/v2/service_types/${serviceTypeId}/plans/${planId}/items`, song: (itemId) => `/services/v2/songs/${itemId}`, arrangement: (songId, arrangementId) => `/services/v2/songs/${songId}/arrangements/${arrangementId}`, arrangementSections: (songId, arrangementId) => `/services/v2/songs/${songId}/arrangements/${arrangementId}/sections`, media: (mediaId) => `/services/v2/media/${mediaId}`, mediaAttachments: (mediaId) => `/services/v2/media/${mediaId}/attachments` } };
|
|
1713
1836
|
this.ONE_WEEK_MS = 6048e5;
|
|
1837
|
+
this.requiresAuth = true;
|
|
1838
|
+
this.authTypes = ["oauth_pkce"];
|
|
1839
|
+
this.capabilities = { browse: true, presentations: true, playlist: false, instructions: false, expandedInstructions: false, mediaLicensing: false };
|
|
1714
1840
|
}
|
|
1715
|
-
|
|
1716
|
-
return
|
|
1717
|
-
}
|
|
1718
|
-
getCapabilities() {
|
|
1719
|
-
return {
|
|
1720
|
-
browse: true,
|
|
1721
|
-
presentations: true,
|
|
1722
|
-
playlist: false,
|
|
1723
|
-
instructions: false,
|
|
1724
|
-
expandedInstructions: false,
|
|
1725
|
-
mediaLicensing: false
|
|
1726
|
-
};
|
|
1841
|
+
async apiRequest(path, auth) {
|
|
1842
|
+
return this.apiHelper.apiRequest(this.config, this.id, path, auth);
|
|
1727
1843
|
}
|
|
1728
|
-
async browse(
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
auth
|
|
1733
|
-
);
|
|
1734
|
-
if (!response?.data) return [];
|
|
1735
|
-
return response.data.map((serviceType) => ({
|
|
1844
|
+
async browse(path, auth) {
|
|
1845
|
+
const { segments, depth } = parsePath(path);
|
|
1846
|
+
if (depth === 0) {
|
|
1847
|
+
return [{
|
|
1736
1848
|
type: "folder",
|
|
1737
|
-
id:
|
|
1738
|
-
title:
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
serviceTypeId: serviceType.id
|
|
1742
|
-
}
|
|
1743
|
-
}));
|
|
1849
|
+
id: "serviceTypes-root",
|
|
1850
|
+
title: "Service Types",
|
|
1851
|
+
path: "/serviceTypes"
|
|
1852
|
+
}];
|
|
1744
1853
|
}
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
case "plan":
|
|
1750
|
-
return this.getPlanItems(folder, auth);
|
|
1751
|
-
default:
|
|
1752
|
-
return [];
|
|
1854
|
+
const root = segments[0];
|
|
1855
|
+
if (root !== "serviceTypes") return [];
|
|
1856
|
+
if (depth === 1) {
|
|
1857
|
+
return this.getServiceTypes(auth);
|
|
1753
1858
|
}
|
|
1859
|
+
if (depth === 2) {
|
|
1860
|
+
const serviceTypeId = segments[1];
|
|
1861
|
+
return this.getPlans(serviceTypeId, path, auth);
|
|
1862
|
+
}
|
|
1863
|
+
if (depth === 3) {
|
|
1864
|
+
const serviceTypeId = segments[1];
|
|
1865
|
+
const planId = segments[2];
|
|
1866
|
+
return this.getPlanItems(serviceTypeId, planId, auth);
|
|
1867
|
+
}
|
|
1868
|
+
return [];
|
|
1869
|
+
}
|
|
1870
|
+
async getServiceTypes(auth) {
|
|
1871
|
+
const response = await this.apiRequest(
|
|
1872
|
+
this.config.endpoints.serviceTypes,
|
|
1873
|
+
auth
|
|
1874
|
+
);
|
|
1875
|
+
if (!response?.data) return [];
|
|
1876
|
+
return response.data.map((serviceType) => ({
|
|
1877
|
+
type: "folder",
|
|
1878
|
+
id: serviceType.id,
|
|
1879
|
+
title: serviceType.attributes.name,
|
|
1880
|
+
path: `/serviceTypes/${serviceType.id}`
|
|
1881
|
+
}));
|
|
1754
1882
|
}
|
|
1755
|
-
async getPlans(
|
|
1756
|
-
const serviceTypeId = folder.providerData?.serviceTypeId;
|
|
1757
|
-
if (!serviceTypeId) return [];
|
|
1883
|
+
async getPlans(serviceTypeId, currentPath, auth) {
|
|
1758
1884
|
const pathFn = this.config.endpoints.plans;
|
|
1759
1885
|
const response = await this.apiRequest(
|
|
1760
1886
|
`${pathFn(serviceTypeId)}?filter=future&order=sort_date`,
|
|
@@ -1771,73 +1897,46 @@ var PlanningCenterProvider = class extends ContentProvider {
|
|
|
1771
1897
|
type: "folder",
|
|
1772
1898
|
id: plan.id,
|
|
1773
1899
|
title: plan.attributes.title || this.formatDate(plan.attributes.sort_date),
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
planId: plan.id,
|
|
1778
|
-
sortDate: plan.attributes.sort_date
|
|
1779
|
-
}
|
|
1900
|
+
isLeaf: true,
|
|
1901
|
+
path: `${currentPath}/${plan.id}`,
|
|
1902
|
+
providerData: { sortDate: plan.attributes.sort_date }
|
|
1780
1903
|
}));
|
|
1781
1904
|
}
|
|
1782
|
-
async getPlanItems(
|
|
1783
|
-
const serviceTypeId = folder.providerData?.serviceTypeId;
|
|
1784
|
-
const planId = folder.providerData?.planId;
|
|
1785
|
-
if (!serviceTypeId || !planId) return [];
|
|
1905
|
+
async getPlanItems(serviceTypeId, planId, auth) {
|
|
1786
1906
|
const pathFn = this.config.endpoints.planItems;
|
|
1787
1907
|
const response = await this.apiRequest(
|
|
1788
1908
|
`${pathFn(serviceTypeId, planId)}?per_page=100`,
|
|
1789
1909
|
auth
|
|
1790
1910
|
);
|
|
1791
1911
|
if (!response?.data) return [];
|
|
1792
|
-
return response.data.map((item) => ({
|
|
1793
|
-
type: "file",
|
|
1794
|
-
id: item.id,
|
|
1795
|
-
title: item.attributes.title || "",
|
|
1796
|
-
mediaType: "image",
|
|
1797
|
-
url: "",
|
|
1798
|
-
providerData: {
|
|
1799
|
-
itemType: item.attributes.item_type,
|
|
1800
|
-
description: item.attributes.description,
|
|
1801
|
-
length: item.attributes.length,
|
|
1802
|
-
songId: item.relationships?.song?.data?.id,
|
|
1803
|
-
arrangementId: item.relationships?.arrangement?.data?.id
|
|
1804
|
-
}
|
|
1805
|
-
}));
|
|
1912
|
+
return response.data.map((item) => ({ type: "file", id: item.id, title: item.attributes.title || "", mediaType: "image", url: "", providerData: { itemType: item.attributes.item_type, description: item.attributes.description, length: item.attributes.length, songId: item.relationships?.song?.data?.id, arrangementId: item.relationships?.arrangement?.data?.id } }));
|
|
1806
1913
|
}
|
|
1807
|
-
async getPresentations(
|
|
1808
|
-
const
|
|
1809
|
-
if (
|
|
1810
|
-
const serviceTypeId =
|
|
1811
|
-
const planId =
|
|
1812
|
-
if (!serviceTypeId || !planId) return null;
|
|
1914
|
+
async getPresentations(path, auth) {
|
|
1915
|
+
const { segments, depth } = parsePath(path);
|
|
1916
|
+
if (depth < 3 || segments[0] !== "serviceTypes") return null;
|
|
1917
|
+
const serviceTypeId = segments[1];
|
|
1918
|
+
const planId = segments[2];
|
|
1813
1919
|
const pathFn = this.config.endpoints.planItems;
|
|
1814
1920
|
const response = await this.apiRequest(
|
|
1815
1921
|
`${pathFn(serviceTypeId, planId)}?per_page=100`,
|
|
1816
1922
|
auth
|
|
1817
1923
|
);
|
|
1818
1924
|
if (!response?.data) return null;
|
|
1925
|
+
const plans = await this.getPlans(serviceTypeId, `/serviceTypes/${serviceTypeId}`, auth);
|
|
1926
|
+
const plan = plans.find((p) => p.id === planId);
|
|
1927
|
+
const planTitle = plan?.title || "Plan";
|
|
1819
1928
|
const sections = [];
|
|
1820
1929
|
const allFiles = [];
|
|
1821
1930
|
let currentSection = null;
|
|
1822
1931
|
for (const item of response.data) {
|
|
1823
1932
|
const itemType = item.attributes.item_type;
|
|
1824
1933
|
if (itemType === "header") {
|
|
1825
|
-
if (currentSection && currentSection.presentations.length > 0)
|
|
1826
|
-
|
|
1827
|
-
}
|
|
1828
|
-
currentSection = {
|
|
1829
|
-
id: item.id,
|
|
1830
|
-
name: item.attributes.title || "Section",
|
|
1831
|
-
presentations: []
|
|
1832
|
-
};
|
|
1934
|
+
if (currentSection && currentSection.presentations.length > 0) sections.push(currentSection);
|
|
1935
|
+
currentSection = { id: item.id, name: item.attributes.title || "Section", presentations: [] };
|
|
1833
1936
|
continue;
|
|
1834
1937
|
}
|
|
1835
1938
|
if (!currentSection) {
|
|
1836
|
-
currentSection = {
|
|
1837
|
-
id: `default-${planId}`,
|
|
1838
|
-
name: "Service",
|
|
1839
|
-
presentations: []
|
|
1840
|
-
};
|
|
1939
|
+
currentSection = { id: `default-${planId}`, name: "Service", presentations: [] };
|
|
1841
1940
|
}
|
|
1842
1941
|
const presentation = await this.convertToPresentation(item, auth);
|
|
1843
1942
|
if (presentation) {
|
|
@@ -1848,12 +1947,7 @@ var PlanningCenterProvider = class extends ContentProvider {
|
|
|
1848
1947
|
if (currentSection && currentSection.presentations.length > 0) {
|
|
1849
1948
|
sections.push(currentSection);
|
|
1850
1949
|
}
|
|
1851
|
-
return {
|
|
1852
|
-
id: planId,
|
|
1853
|
-
name: folder.title,
|
|
1854
|
-
sections,
|
|
1855
|
-
allFiles
|
|
1856
|
-
};
|
|
1950
|
+
return { id: planId, name: planTitle, sections, allFiles };
|
|
1857
1951
|
}
|
|
1858
1952
|
async convertToPresentation(item, auth) {
|
|
1859
1953
|
const itemType = item.attributes.item_type;
|
|
@@ -1864,17 +1958,7 @@ var PlanningCenterProvider = class extends ContentProvider {
|
|
|
1864
1958
|
return this.convertMediaToPresentation(item, auth);
|
|
1865
1959
|
}
|
|
1866
1960
|
if (itemType === "item") {
|
|
1867
|
-
return {
|
|
1868
|
-
id: item.id,
|
|
1869
|
-
name: item.attributes.title || "",
|
|
1870
|
-
actionType: "other",
|
|
1871
|
-
files: [],
|
|
1872
|
-
providerData: {
|
|
1873
|
-
itemType: "item",
|
|
1874
|
-
description: item.attributes.description,
|
|
1875
|
-
length: item.attributes.length
|
|
1876
|
-
}
|
|
1877
|
-
};
|
|
1961
|
+
return { id: item.id, name: item.attributes.title || "", actionType: "other", files: [], providerData: { itemType: "item", description: item.attributes.description, length: item.attributes.length } };
|
|
1878
1962
|
}
|
|
1879
1963
|
return null;
|
|
1880
1964
|
}
|
|
@@ -1882,13 +1966,7 @@ var PlanningCenterProvider = class extends ContentProvider {
|
|
|
1882
1966
|
const songId = item.relationships?.song?.data?.id;
|
|
1883
1967
|
const arrangementId = item.relationships?.arrangement?.data?.id;
|
|
1884
1968
|
if (!songId) {
|
|
1885
|
-
return {
|
|
1886
|
-
id: item.id,
|
|
1887
|
-
name: item.attributes.title || "Song",
|
|
1888
|
-
actionType: "other",
|
|
1889
|
-
files: [],
|
|
1890
|
-
providerData: { itemType: "song" }
|
|
1891
|
-
};
|
|
1969
|
+
return { id: item.id, name: item.attributes.title || "Song", actionType: "other", files: [], providerData: { itemType: "song" } };
|
|
1892
1970
|
}
|
|
1893
1971
|
const songFn = this.config.endpoints.song;
|
|
1894
1972
|
const songResponse = await this.apiRequest(songFn(songId), auth);
|
|
@@ -1910,25 +1988,7 @@ var PlanningCenterProvider = class extends ContentProvider {
|
|
|
1910
1988
|
}
|
|
1911
1989
|
const song = songResponse?.data;
|
|
1912
1990
|
const title = song?.attributes?.title || item.attributes.title || "Song";
|
|
1913
|
-
return {
|
|
1914
|
-
id: item.id,
|
|
1915
|
-
name: title,
|
|
1916
|
-
actionType: "other",
|
|
1917
|
-
files: [],
|
|
1918
|
-
providerData: {
|
|
1919
|
-
itemType: "song",
|
|
1920
|
-
title,
|
|
1921
|
-
author: song?.attributes?.author,
|
|
1922
|
-
copyright: song?.attributes?.copyright,
|
|
1923
|
-
ccliNumber: song?.attributes?.ccli_number,
|
|
1924
|
-
arrangementName: arrangement?.attributes?.name,
|
|
1925
|
-
keySignature: arrangement?.attributes?.chord_chart_key,
|
|
1926
|
-
bpm: arrangement?.attributes?.bpm,
|
|
1927
|
-
sequence: arrangement?.attributes?.sequence,
|
|
1928
|
-
sections: sections.map((s) => ({ label: s.label, lyrics: s.lyrics })),
|
|
1929
|
-
length: item.attributes.length
|
|
1930
|
-
}
|
|
1931
|
-
};
|
|
1991
|
+
return { id: item.id, name: title, actionType: "other", files: [], providerData: { itemType: "song", title, author: song?.attributes?.author, copyright: song?.attributes?.copyright, ccliNumber: song?.attributes?.ccli_number, arrangementName: arrangement?.attributes?.name, keySignature: arrangement?.attributes?.chord_chart_key, bpm: arrangement?.attributes?.bpm, sequence: arrangement?.attributes?.sequence, sections: sections.map((s) => ({ label: s.label, lyrics: s.lyrics })), length: item.attributes.length } };
|
|
1932
1992
|
}
|
|
1933
1993
|
async convertMediaToPresentation(item, auth) {
|
|
1934
1994
|
const files = [];
|
|
@@ -1948,25 +2008,10 @@ var PlanningCenterProvider = class extends ContentProvider {
|
|
|
1948
2008
|
if (!url) continue;
|
|
1949
2009
|
const contentType = attachment.attributes.content_type;
|
|
1950
2010
|
const explicitType = contentType?.startsWith("video/") ? "video" : void 0;
|
|
1951
|
-
files.push({
|
|
1952
|
-
type: "file",
|
|
1953
|
-
id: attachment.id,
|
|
1954
|
-
title: attachment.attributes.filename,
|
|
1955
|
-
mediaType: detectMediaType(url, explicitType),
|
|
1956
|
-
url
|
|
1957
|
-
});
|
|
2011
|
+
files.push({ type: "file", id: attachment.id, title: attachment.attributes.filename, mediaType: detectMediaType(url, explicitType), url });
|
|
1958
2012
|
}
|
|
1959
2013
|
}
|
|
1960
|
-
return {
|
|
1961
|
-
id: item.id,
|
|
1962
|
-
name: item.attributes.title || "Media",
|
|
1963
|
-
actionType: "play",
|
|
1964
|
-
files,
|
|
1965
|
-
providerData: {
|
|
1966
|
-
itemType: "media",
|
|
1967
|
-
length: item.attributes.length
|
|
1968
|
-
}
|
|
1969
|
-
};
|
|
2014
|
+
return { id: item.id, name: item.attributes.title || "Media", actionType: "play", files, providerData: { itemType: "media", length: item.attributes.length } };
|
|
1970
2015
|
}
|
|
1971
2016
|
formatDate(dateString) {
|
|
1972
2017
|
const date = new Date(dateString);
|
|
@@ -1974,7 +2019,7 @@ var PlanningCenterProvider = class extends ContentProvider {
|
|
|
1974
2019
|
}
|
|
1975
2020
|
};
|
|
1976
2021
|
|
|
1977
|
-
// src/providers/
|
|
2022
|
+
// src/providers/bibleProject/data.json
|
|
1978
2023
|
var data_default = {
|
|
1979
2024
|
collections: [
|
|
1980
2025
|
{
|
|
@@ -4082,10 +4127,9 @@ var data_default = {
|
|
|
4082
4127
|
]
|
|
4083
4128
|
};
|
|
4084
4129
|
|
|
4085
|
-
// src/providers/
|
|
4086
|
-
var BibleProjectProvider = class
|
|
4130
|
+
// src/providers/bibleProject/BibleProjectProvider.ts
|
|
4131
|
+
var BibleProjectProvider = class {
|
|
4087
4132
|
constructor() {
|
|
4088
|
-
super(...arguments);
|
|
4089
4133
|
this.id = "bibleproject";
|
|
4090
4134
|
this.name = "The Bible Project";
|
|
4091
4135
|
this.logos = {
|
|
@@ -4104,74 +4148,109 @@ var BibleProjectProvider = class extends ContentProvider {
|
|
|
4104
4148
|
}
|
|
4105
4149
|
};
|
|
4106
4150
|
this.data = data_default;
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
}
|
|
4111
|
-
getCapabilities() {
|
|
4112
|
-
return {
|
|
4151
|
+
this.requiresAuth = false;
|
|
4152
|
+
this.authTypes = ["none"];
|
|
4153
|
+
this.capabilities = {
|
|
4113
4154
|
browse: true,
|
|
4114
|
-
presentations:
|
|
4115
|
-
playlist:
|
|
4155
|
+
presentations: true,
|
|
4156
|
+
playlist: true,
|
|
4116
4157
|
instructions: false,
|
|
4117
4158
|
expandedInstructions: false,
|
|
4118
4159
|
mediaLicensing: false
|
|
4119
4160
|
};
|
|
4120
4161
|
}
|
|
4121
|
-
async browse(
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
collection.name,
|
|
4126
|
-
collection.image || void 0,
|
|
4127
|
-
{ level: "collection", collectionName: collection.name }
|
|
4128
|
-
));
|
|
4162
|
+
async browse(path, _auth) {
|
|
4163
|
+
const { segments, depth } = parsePath(path);
|
|
4164
|
+
if (depth === 0) {
|
|
4165
|
+
return this.getCollections();
|
|
4129
4166
|
}
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
return this.getLessonFolders(collectionName);
|
|
4167
|
+
if (depth === 1) {
|
|
4168
|
+
const collectionSlug = segments[0];
|
|
4169
|
+
return this.getLessonFolders(collectionSlug, path);
|
|
4134
4170
|
}
|
|
4135
|
-
if (
|
|
4136
|
-
const
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
videoData.id,
|
|
4140
|
-
videoData.title,
|
|
4141
|
-
videoData.videoUrl,
|
|
4142
|
-
{
|
|
4143
|
-
mediaType: "video",
|
|
4144
|
-
muxPlaybackId: videoData.muxPlaybackId
|
|
4145
|
-
}
|
|
4146
|
-
)];
|
|
4147
|
-
}
|
|
4148
|
-
return [];
|
|
4171
|
+
if (depth === 2) {
|
|
4172
|
+
const collectionSlug = segments[0];
|
|
4173
|
+
const videoId = segments[1];
|
|
4174
|
+
return this.getVideoFile(collectionSlug, videoId);
|
|
4149
4175
|
}
|
|
4150
4176
|
return [];
|
|
4151
4177
|
}
|
|
4152
|
-
|
|
4153
|
-
return
|
|
4178
|
+
getCollections() {
|
|
4179
|
+
return this.data.collections.filter((collection) => collection.videos.length > 0).map((collection) => ({
|
|
4180
|
+
type: "folder",
|
|
4181
|
+
id: this.slugify(collection.name),
|
|
4182
|
+
title: collection.name,
|
|
4183
|
+
image: collection.image || void 0,
|
|
4184
|
+
path: `/${this.slugify(collection.name)}`
|
|
4185
|
+
}));
|
|
4154
4186
|
}
|
|
4155
|
-
getLessonFolders(
|
|
4156
|
-
const collection = this.data.collections.find((c) => c.name ===
|
|
4187
|
+
getLessonFolders(collectionSlug, currentPath) {
|
|
4188
|
+
const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
|
|
4157
4189
|
if (!collection) return [];
|
|
4158
|
-
return collection.videos.map((video) =>
|
|
4159
|
-
|
|
4160
|
-
video.
|
|
4161
|
-
video.
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4190
|
+
return collection.videos.map((video) => ({
|
|
4191
|
+
type: "folder",
|
|
4192
|
+
id: video.id,
|
|
4193
|
+
title: video.title,
|
|
4194
|
+
image: video.thumbnailUrl,
|
|
4195
|
+
isLeaf: true,
|
|
4196
|
+
path: `${currentPath}/${video.id}`,
|
|
4197
|
+
providerData: { videoData: video }
|
|
4198
|
+
}));
|
|
4199
|
+
}
|
|
4200
|
+
getVideoFile(collectionSlug, videoId) {
|
|
4201
|
+
const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
|
|
4202
|
+
if (!collection) return [];
|
|
4203
|
+
const video = collection.videos.find((v) => v.id === videoId);
|
|
4204
|
+
if (!video) return [];
|
|
4205
|
+
return [createFile(video.id, video.title, video.videoUrl, { mediaType: "video", muxPlaybackId: video.muxPlaybackId })];
|
|
4206
|
+
}
|
|
4207
|
+
async getPresentations(path, _auth) {
|
|
4208
|
+
const { segments, depth } = parsePath(path);
|
|
4209
|
+
if (depth < 1) return null;
|
|
4210
|
+
const collectionSlug = segments[0];
|
|
4211
|
+
const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
|
|
4212
|
+
if (!collection) return null;
|
|
4213
|
+
if (depth === 1) {
|
|
4214
|
+
const allFiles = [];
|
|
4215
|
+
const presentations = collection.videos.map((video) => {
|
|
4216
|
+
const file = { type: "file", id: video.id, title: video.title, mediaType: "video", url: video.videoUrl, image: video.thumbnailUrl, muxPlaybackId: video.muxPlaybackId };
|
|
4217
|
+
allFiles.push(file);
|
|
4218
|
+
return { id: video.id, name: video.title, actionType: "play", files: [file] };
|
|
4219
|
+
});
|
|
4220
|
+
return { id: this.slugify(collection.name), name: collection.name, image: collection.image || void 0, sections: [{ id: "videos", name: "Videos", presentations }], allFiles };
|
|
4221
|
+
}
|
|
4222
|
+
if (depth === 2) {
|
|
4223
|
+
const videoId = segments[1];
|
|
4224
|
+
const video = collection.videos.find((v) => v.id === videoId);
|
|
4225
|
+
if (!video) return null;
|
|
4226
|
+
const file = { type: "file", id: video.id, title: video.title, mediaType: "video", url: video.videoUrl, image: video.thumbnailUrl, muxPlaybackId: video.muxPlaybackId };
|
|
4227
|
+
return { id: video.id, name: video.title, image: video.thumbnailUrl, sections: [{ id: "main", name: "Content", presentations: [{ id: video.id, name: video.title, actionType: "play", files: [file] }] }], allFiles: [file] };
|
|
4228
|
+
}
|
|
4229
|
+
return null;
|
|
4230
|
+
}
|
|
4231
|
+
async getPlaylist(path, _auth, _resolution) {
|
|
4232
|
+
const { segments, depth } = parsePath(path);
|
|
4233
|
+
if (depth < 1) return null;
|
|
4234
|
+
const collectionSlug = segments[0];
|
|
4235
|
+
const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
|
|
4236
|
+
if (!collection) return null;
|
|
4237
|
+
if (depth === 1) {
|
|
4238
|
+
return collection.videos.map((video) => ({ type: "file", id: video.id, title: video.title, mediaType: "video", url: video.videoUrl, image: video.thumbnailUrl, muxPlaybackId: video.muxPlaybackId }));
|
|
4239
|
+
}
|
|
4240
|
+
if (depth === 2) {
|
|
4241
|
+
const videoId = segments[1];
|
|
4242
|
+
const video = collection.videos.find((v) => v.id === videoId);
|
|
4243
|
+
if (!video) return null;
|
|
4244
|
+
return [{ type: "file", id: video.id, title: video.title, mediaType: "video", url: video.videoUrl, image: video.thumbnailUrl, muxPlaybackId: video.muxPlaybackId }];
|
|
4245
|
+
}
|
|
4246
|
+
return null;
|
|
4168
4247
|
}
|
|
4169
4248
|
slugify(text) {
|
|
4170
4249
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4171
4250
|
}
|
|
4172
4251
|
};
|
|
4173
4252
|
|
|
4174
|
-
// src/providers/
|
|
4253
|
+
// src/providers/highVoltage/data.json
|
|
4175
4254
|
var data_default2 = {
|
|
4176
4255
|
collections: [
|
|
4177
4256
|
{
|
|
@@ -11675,10 +11754,9 @@ var data_default2 = {
|
|
|
11675
11754
|
]
|
|
11676
11755
|
};
|
|
11677
11756
|
|
|
11678
|
-
// src/providers/HighVoltageKidsProvider.ts
|
|
11679
|
-
var HighVoltageKidsProvider = class
|
|
11757
|
+
// src/providers/highVoltage/HighVoltageKidsProvider.ts
|
|
11758
|
+
var HighVoltageKidsProvider = class {
|
|
11680
11759
|
constructor() {
|
|
11681
|
-
super(...arguments);
|
|
11682
11760
|
this.id = "highvoltagekids";
|
|
11683
11761
|
this.name = "High Voltage Kids";
|
|
11684
11762
|
this.logos = {
|
|
@@ -11697,87 +11775,211 @@ var HighVoltageKidsProvider = class extends ContentProvider {
|
|
|
11697
11775
|
}
|
|
11698
11776
|
};
|
|
11699
11777
|
this.data = data_default2;
|
|
11700
|
-
|
|
11701
|
-
|
|
11702
|
-
|
|
11703
|
-
}
|
|
11704
|
-
getCapabilities() {
|
|
11705
|
-
return {
|
|
11778
|
+
this.requiresAuth = false;
|
|
11779
|
+
this.authTypes = ["none"];
|
|
11780
|
+
this.capabilities = {
|
|
11706
11781
|
browse: true,
|
|
11707
|
-
presentations:
|
|
11708
|
-
playlist:
|
|
11782
|
+
presentations: true,
|
|
11783
|
+
playlist: true,
|
|
11709
11784
|
instructions: false,
|
|
11710
|
-
expandedInstructions:
|
|
11785
|
+
expandedInstructions: true,
|
|
11711
11786
|
mediaLicensing: false
|
|
11712
11787
|
};
|
|
11713
11788
|
}
|
|
11714
|
-
async browse(
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
collection.name,
|
|
11719
|
-
void 0,
|
|
11720
|
-
{ level: "collection", collectionName: collection.name }
|
|
11721
|
-
));
|
|
11789
|
+
async browse(path, _auth) {
|
|
11790
|
+
const { segments, depth } = parsePath(path);
|
|
11791
|
+
if (depth === 0) {
|
|
11792
|
+
return this.getCollections();
|
|
11722
11793
|
}
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
return this.getStudyFolders(collectionName);
|
|
11794
|
+
if (depth === 1) {
|
|
11795
|
+
const collectionSlug = segments[0];
|
|
11796
|
+
return this.getStudyFolders(collectionSlug, path);
|
|
11727
11797
|
}
|
|
11728
|
-
if (
|
|
11729
|
-
const
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
}
|
|
11733
|
-
return [];
|
|
11798
|
+
if (depth === 2) {
|
|
11799
|
+
const collectionSlug = segments[0];
|
|
11800
|
+
const studyId = segments[1];
|
|
11801
|
+
return this.getLessonFolders(collectionSlug, studyId, path);
|
|
11734
11802
|
}
|
|
11735
|
-
if (
|
|
11736
|
-
const
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
file.title,
|
|
11741
|
-
file.url,
|
|
11742
|
-
{ mediaType: file.mediaType }
|
|
11743
|
-
));
|
|
11744
|
-
}
|
|
11745
|
-
return [];
|
|
11803
|
+
if (depth === 3) {
|
|
11804
|
+
const collectionSlug = segments[0];
|
|
11805
|
+
const studyId = segments[1];
|
|
11806
|
+
const lessonId = segments[2];
|
|
11807
|
+
return this.getLessonFiles(collectionSlug, studyId, lessonId);
|
|
11746
11808
|
}
|
|
11747
11809
|
return [];
|
|
11748
11810
|
}
|
|
11749
|
-
|
|
11750
|
-
return
|
|
11811
|
+
getCollections() {
|
|
11812
|
+
return this.data.collections.filter((collection) => collection.folders.length > 0).map((collection) => ({
|
|
11813
|
+
type: "folder",
|
|
11814
|
+
id: this.slugify(collection.name),
|
|
11815
|
+
title: collection.name,
|
|
11816
|
+
path: `/${this.slugify(collection.name)}`
|
|
11817
|
+
}));
|
|
11751
11818
|
}
|
|
11752
|
-
getStudyFolders(
|
|
11753
|
-
const collection = this.data.collections.find((c) => c.name ===
|
|
11819
|
+
getStudyFolders(collectionSlug, currentPath) {
|
|
11820
|
+
const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
|
|
11754
11821
|
if (!collection) return [];
|
|
11755
|
-
return collection.folders.map((study) =>
|
|
11756
|
-
|
|
11757
|
-
study.
|
|
11758
|
-
study.
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11822
|
+
return collection.folders.map((study) => ({
|
|
11823
|
+
type: "folder",
|
|
11824
|
+
id: study.id,
|
|
11825
|
+
title: study.name,
|
|
11826
|
+
image: study.image || void 0,
|
|
11827
|
+
path: `${currentPath}/${study.id}`,
|
|
11828
|
+
providerData: { studyData: study }
|
|
11829
|
+
}));
|
|
11830
|
+
}
|
|
11831
|
+
getLessonFolders(collectionSlug, studyId, currentPath) {
|
|
11832
|
+
const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
|
|
11833
|
+
if (!collection) return [];
|
|
11834
|
+
const study = collection.folders.find((s) => s.id === studyId);
|
|
11835
|
+
if (!study) return [];
|
|
11836
|
+
return study.lessons.map((lesson) => ({
|
|
11837
|
+
type: "folder",
|
|
11838
|
+
id: lesson.id,
|
|
11839
|
+
title: lesson.name,
|
|
11840
|
+
image: lesson.image || void 0,
|
|
11841
|
+
isLeaf: true,
|
|
11842
|
+
path: `${currentPath}/${lesson.id}`,
|
|
11843
|
+
providerData: { lessonData: lesson, studyName: study.name }
|
|
11844
|
+
}));
|
|
11845
|
+
}
|
|
11846
|
+
getLessonFiles(collectionSlug, studyId, lessonId) {
|
|
11847
|
+
const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
|
|
11848
|
+
if (!collection) return [];
|
|
11849
|
+
const study = collection.folders.find((s) => s.id === studyId);
|
|
11850
|
+
if (!study) return [];
|
|
11851
|
+
const lesson = study.lessons.find((l) => l.id === lessonId);
|
|
11852
|
+
if (!lesson?.files) return [];
|
|
11853
|
+
return lesson.files.map((file) => createFile(file.id, file.title, file.url, { mediaType: file.mediaType }));
|
|
11854
|
+
}
|
|
11855
|
+
async getPresentations(path, _auth) {
|
|
11856
|
+
const { segments, depth } = parsePath(path);
|
|
11857
|
+
if (depth < 2) return null;
|
|
11858
|
+
const collectionSlug = segments[0];
|
|
11859
|
+
const studyId = segments[1];
|
|
11860
|
+
const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
|
|
11861
|
+
if (!collection) return null;
|
|
11862
|
+
const study = collection.folders.find((s) => s.id === studyId);
|
|
11863
|
+
if (!study) return null;
|
|
11864
|
+
if (depth === 2) {
|
|
11865
|
+
const allFiles = [];
|
|
11866
|
+
const sections = study.lessons.map((lesson) => {
|
|
11867
|
+
const files = lesson.files.map((file) => {
|
|
11868
|
+
const contentFile = { type: "file", id: file.id, title: file.title, mediaType: file.mediaType, url: file.url, image: lesson.image };
|
|
11869
|
+
allFiles.push(contentFile);
|
|
11870
|
+
return contentFile;
|
|
11871
|
+
});
|
|
11872
|
+
const presentation = { id: lesson.id, name: lesson.name, actionType: "play", files };
|
|
11873
|
+
return { id: lesson.id, name: lesson.name, presentations: [presentation] };
|
|
11874
|
+
});
|
|
11875
|
+
return { id: study.id, name: study.name, description: study.description, image: study.image, sections, allFiles };
|
|
11876
|
+
}
|
|
11877
|
+
if (depth === 3) {
|
|
11878
|
+
const lessonId = segments[2];
|
|
11879
|
+
const lesson = study.lessons.find((l) => l.id === lessonId);
|
|
11880
|
+
if (!lesson?.files) return null;
|
|
11881
|
+
const files = lesson.files.map((file) => ({ type: "file", id: file.id, title: file.title, mediaType: file.mediaType, url: file.url, image: lesson.image }));
|
|
11882
|
+
const presentation = { id: lesson.id, name: lesson.name, actionType: "play", files };
|
|
11883
|
+
return { id: lesson.id, name: lesson.name, image: lesson.image, sections: [{ id: "main", name: "Content", presentations: [presentation] }], allFiles: files };
|
|
11884
|
+
}
|
|
11885
|
+
return null;
|
|
11886
|
+
}
|
|
11887
|
+
async getPlaylist(path, _auth, _resolution) {
|
|
11888
|
+
const { segments, depth } = parsePath(path);
|
|
11889
|
+
if (depth < 2) return null;
|
|
11890
|
+
const collectionSlug = segments[0];
|
|
11891
|
+
const studyId = segments[1];
|
|
11892
|
+
const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
|
|
11893
|
+
if (!collection) return null;
|
|
11894
|
+
const study = collection.folders.find((s) => s.id === studyId);
|
|
11895
|
+
if (!study) return null;
|
|
11896
|
+
if (depth === 2) {
|
|
11897
|
+
const allFiles = [];
|
|
11898
|
+
for (const lesson of study.lessons) {
|
|
11899
|
+
for (const file of lesson.files) {
|
|
11900
|
+
allFiles.push({ type: "file", id: file.id, title: file.title, mediaType: file.mediaType, url: file.url, image: lesson.image });
|
|
11901
|
+
}
|
|
11775
11902
|
}
|
|
11776
|
-
|
|
11903
|
+
return allFiles;
|
|
11904
|
+
}
|
|
11905
|
+
if (depth === 3) {
|
|
11906
|
+
const lessonId = segments[2];
|
|
11907
|
+
const lesson = study.lessons.find((l) => l.id === lessonId);
|
|
11908
|
+
if (!lesson?.files) return null;
|
|
11909
|
+
return lesson.files.map((file) => ({ type: "file", id: file.id, title: file.title, mediaType: file.mediaType, url: file.url, image: lesson.image }));
|
|
11910
|
+
}
|
|
11911
|
+
return null;
|
|
11912
|
+
}
|
|
11913
|
+
async getExpandedInstructions(path, _auth) {
|
|
11914
|
+
const { segments, depth } = parsePath(path);
|
|
11915
|
+
if (depth < 2) return null;
|
|
11916
|
+
const collectionSlug = segments[0];
|
|
11917
|
+
const studyId = segments[1];
|
|
11918
|
+
const collection = this.data.collections.find((c) => this.slugify(c.name) === collectionSlug);
|
|
11919
|
+
if (!collection) return null;
|
|
11920
|
+
const study = collection.folders.find((s) => s.id === studyId);
|
|
11921
|
+
if (!study) return null;
|
|
11922
|
+
if (depth === 2) {
|
|
11923
|
+
const lessonItems = study.lessons.map((lesson) => {
|
|
11924
|
+
const fileItems = lesson.files.map((file) => ({ id: file.id, itemType: "file", label: file.title, embedUrl: file.url }));
|
|
11925
|
+
return { id: lesson.id, itemType: "action", label: lesson.name, description: "play", children: fileItems };
|
|
11926
|
+
});
|
|
11927
|
+
return { venueName: study.name, items: [{ id: study.id, itemType: "header", label: study.name, children: [{ id: "main", itemType: "section", label: "Content", children: lessonItems }] }] };
|
|
11928
|
+
}
|
|
11929
|
+
if (depth === 3) {
|
|
11930
|
+
const lessonId = segments[2];
|
|
11931
|
+
const lesson = study.lessons.find((l) => l.id === lessonId);
|
|
11932
|
+
if (!lesson?.files) return null;
|
|
11933
|
+
const headerLabel = `${study.name} - ${lesson.name}`;
|
|
11934
|
+
const actionItems = this.groupFilesIntoActions(lesson.files);
|
|
11935
|
+
return { venueName: lesson.name, items: [{ id: lesson.id, itemType: "header", label: headerLabel, children: [{ id: "main", itemType: "section", label: lesson.name, children: actionItems }] }] };
|
|
11936
|
+
}
|
|
11937
|
+
return null;
|
|
11777
11938
|
}
|
|
11778
11939
|
slugify(text) {
|
|
11779
11940
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
11780
11941
|
}
|
|
11942
|
+
groupFilesIntoActions(files) {
|
|
11943
|
+
const actionItems = [];
|
|
11944
|
+
let currentGroup = [];
|
|
11945
|
+
let currentBaseName = null;
|
|
11946
|
+
const flushGroup = () => {
|
|
11947
|
+
if (currentGroup.length === 0) return;
|
|
11948
|
+
const children = currentGroup.map((file) => ({
|
|
11949
|
+
id: file.id,
|
|
11950
|
+
itemType: "file",
|
|
11951
|
+
label: file.title,
|
|
11952
|
+
embedUrl: file.url
|
|
11953
|
+
}));
|
|
11954
|
+
const label = currentGroup.length > 1 && currentBaseName ? currentBaseName : currentGroup[0].title;
|
|
11955
|
+
actionItems.push({
|
|
11956
|
+
id: currentGroup[0].id + "-action",
|
|
11957
|
+
itemType: "action",
|
|
11958
|
+
label,
|
|
11959
|
+
description: "play",
|
|
11960
|
+
children
|
|
11961
|
+
});
|
|
11962
|
+
currentGroup = [];
|
|
11963
|
+
currentBaseName = null;
|
|
11964
|
+
};
|
|
11965
|
+
for (const file of files) {
|
|
11966
|
+
const baseName = this.getBaseName(file.title);
|
|
11967
|
+
const isNumbered = baseName !== file.title;
|
|
11968
|
+
if (isNumbered && baseName === currentBaseName) {
|
|
11969
|
+
currentGroup.push(file);
|
|
11970
|
+
} else {
|
|
11971
|
+
flushGroup();
|
|
11972
|
+
currentGroup = [file];
|
|
11973
|
+
currentBaseName = isNumbered ? baseName : null;
|
|
11974
|
+
}
|
|
11975
|
+
}
|
|
11976
|
+
flushGroup();
|
|
11977
|
+
return actionItems;
|
|
11978
|
+
}
|
|
11979
|
+
getBaseName(title) {
|
|
11980
|
+
const match = title.match(/^(.+?)\s*\d+$/);
|
|
11981
|
+
return match ? match[1].trim() : title;
|
|
11982
|
+
}
|
|
11781
11983
|
};
|
|
11782
11984
|
|
|
11783
11985
|
// src/providers/index.ts
|
|
@@ -11862,15 +12064,15 @@ function getProviderConfig(providerId) {
|
|
|
11862
12064
|
const provider = getProvider(providerId);
|
|
11863
12065
|
return provider?.config || null;
|
|
11864
12066
|
}
|
|
11865
|
-
function getAvailableProviders() {
|
|
12067
|
+
function getAvailableProviders(ids) {
|
|
11866
12068
|
const implemented = getAllProviders().map((provider) => ({
|
|
11867
12069
|
id: provider.id,
|
|
11868
12070
|
name: provider.name,
|
|
11869
12071
|
logos: provider.logos,
|
|
11870
12072
|
implemented: true,
|
|
11871
|
-
requiresAuth: provider.requiresAuth
|
|
11872
|
-
authTypes: provider.
|
|
11873
|
-
capabilities: provider.
|
|
12073
|
+
requiresAuth: provider.requiresAuth,
|
|
12074
|
+
authTypes: provider.authTypes,
|
|
12075
|
+
capabilities: provider.capabilities
|
|
11874
12076
|
}));
|
|
11875
12077
|
const comingSoon = unimplementedProviders.map((p) => ({
|
|
11876
12078
|
id: p.id,
|
|
@@ -11881,7 +12083,12 @@ function getAvailableProviders() {
|
|
|
11881
12083
|
authTypes: [],
|
|
11882
12084
|
capabilities: { browse: false, presentations: false, playlist: false, instructions: false, expandedInstructions: false, mediaLicensing: false }
|
|
11883
12085
|
}));
|
|
11884
|
-
|
|
12086
|
+
const all = [...implemented, ...comingSoon];
|
|
12087
|
+
if (ids && ids.length > 0) {
|
|
12088
|
+
const idSet = new Set(ids);
|
|
12089
|
+
return all.filter((provider) => idSet.has(provider.id));
|
|
12090
|
+
}
|
|
12091
|
+
return all;
|
|
11885
12092
|
}
|
|
11886
12093
|
|
|
11887
12094
|
// src/index.ts
|
|
@@ -11889,20 +12096,44 @@ var VERSION = "0.0.1";
|
|
|
11889
12096
|
// Annotate the CommonJS export names for ESM import in node:
|
|
11890
12097
|
0 && (module.exports = {
|
|
11891
12098
|
APlayProvider,
|
|
12099
|
+
ApiHelper,
|
|
11892
12100
|
B1ChurchProvider,
|
|
11893
12101
|
BibleProjectProvider,
|
|
11894
12102
|
ContentProvider,
|
|
12103
|
+
DeviceFlowHelper,
|
|
12104
|
+
FormatConverters,
|
|
12105
|
+
FormatResolver,
|
|
12106
|
+
HighVoltageKidsProvider,
|
|
11895
12107
|
LessonsChurchProvider,
|
|
12108
|
+
OAuthHelper,
|
|
11896
12109
|
PlanningCenterProvider,
|
|
11897
12110
|
SignPresenterProvider,
|
|
12111
|
+
TokenHelper,
|
|
11898
12112
|
VERSION,
|
|
12113
|
+
appendToPath,
|
|
12114
|
+
buildPath,
|
|
12115
|
+
collapseInstructions,
|
|
12116
|
+
createFile,
|
|
12117
|
+
createFolder,
|
|
11899
12118
|
detectMediaType,
|
|
12119
|
+
expandedInstructionsToPlaylist,
|
|
12120
|
+
expandedInstructionsToPresentations,
|
|
11900
12121
|
getAllProviders,
|
|
11901
12122
|
getAvailableProviders,
|
|
11902
12123
|
getProvider,
|
|
11903
12124
|
getProviderConfig,
|
|
12125
|
+
getSegment,
|
|
12126
|
+
instructionsToPlaylist,
|
|
12127
|
+
instructionsToPresentations,
|
|
11904
12128
|
isContentFile,
|
|
11905
12129
|
isContentFolder,
|
|
12130
|
+
parsePath,
|
|
12131
|
+
playlistToExpandedInstructions,
|
|
12132
|
+
playlistToInstructions,
|
|
12133
|
+
playlistToPresentations,
|
|
12134
|
+
presentationsToExpandedInstructions,
|
|
12135
|
+
presentationsToInstructions,
|
|
12136
|
+
presentationsToPlaylist,
|
|
11906
12137
|
registerProvider
|
|
11907
12138
|
});
|
|
11908
12139
|
//# sourceMappingURL=index.cjs.map
|