@churchapps/content-providers 0.9.1 → 0.9.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.d.cts CHANGED
@@ -64,7 +64,7 @@ interface ContentFile {
64
64
  type: "file";
65
65
  id: string;
66
66
  title: string;
67
- mediaType: "video" | "image";
67
+ mediaType: "video" | "image" | "audio";
68
68
  thumbnail?: string;
69
69
  url: string;
70
70
  downloadUrl?: string;
@@ -148,7 +148,7 @@ interface InstructionItem {
148
148
  children?: InstructionItem[];
149
149
  downloadUrl?: string;
150
150
  thumbnail?: string;
151
- mediaType?: "video" | "image";
151
+ mediaType?: "video" | "image" | "audio";
152
152
  }
153
153
  interface Instructions {
154
154
  name?: string;
@@ -259,7 +259,10 @@ declare class OAuthHelper {
259
259
  declare function toAuthData(data: Record<string, unknown>, fallbacks?: {
260
260
  refreshToken?: string;
261
261
  scope?: string;
262
- }): ContentProviderAuthData;
262
+ planTypeId?: string;
263
+ }): ContentProviderAuthData & {
264
+ planTypeId?: string;
265
+ };
263
266
  declare class TokenHelper {
264
267
  isAuthValid(auth: ContentProviderAuthData | null | undefined): boolean;
265
268
  isTokenExpired(auth: ContentProviderAuthData): boolean;
@@ -331,7 +334,11 @@ declare class SignPresenterProvider extends BaseProvider {
331
334
  browse(path?: string | null, auth?: ContentProviderAuthData | null): Promise<ContentItem[]>;
332
335
  private extractList;
333
336
  private getPlaylists;
337
+ /** image and video/stream slides play directly; web slides only when the url is itself a media file. html/pdf/multi_zone and generic web pages aren't playable in ChurchApps. */
338
+ private slideMediaType;
339
+ /** The documented player contract; /content/playlists/:id/messages only covers templates 1 and 3 so most playlists come back empty there. */
334
340
  private getMessages;
341
+ private getLegacyMessages;
335
342
  getPlaylist(path: string, auth?: ContentProviderAuthData | null, _resolution?: number): Promise<ContentFile[] | null>;
336
343
  getInstructions(path: string, auth?: ContentProviderAuthData | null): Promise<Instructions | null>;
337
344
  generateCodeVerifier(): string;
package/dist/index.d.ts CHANGED
@@ -64,7 +64,7 @@ interface ContentFile {
64
64
  type: "file";
65
65
  id: string;
66
66
  title: string;
67
- mediaType: "video" | "image";
67
+ mediaType: "video" | "image" | "audio";
68
68
  thumbnail?: string;
69
69
  url: string;
70
70
  downloadUrl?: string;
@@ -148,7 +148,7 @@ interface InstructionItem {
148
148
  children?: InstructionItem[];
149
149
  downloadUrl?: string;
150
150
  thumbnail?: string;
151
- mediaType?: "video" | "image";
151
+ mediaType?: "video" | "image" | "audio";
152
152
  }
153
153
  interface Instructions {
154
154
  name?: string;
@@ -259,7 +259,10 @@ declare class OAuthHelper {
259
259
  declare function toAuthData(data: Record<string, unknown>, fallbacks?: {
260
260
  refreshToken?: string;
261
261
  scope?: string;
262
- }): ContentProviderAuthData;
262
+ planTypeId?: string;
263
+ }): ContentProviderAuthData & {
264
+ planTypeId?: string;
265
+ };
263
266
  declare class TokenHelper {
264
267
  isAuthValid(auth: ContentProviderAuthData | null | undefined): boolean;
265
268
  isTokenExpired(auth: ContentProviderAuthData): boolean;
@@ -331,7 +334,11 @@ declare class SignPresenterProvider extends BaseProvider {
331
334
  browse(path?: string | null, auth?: ContentProviderAuthData | null): Promise<ContentItem[]>;
332
335
  private extractList;
333
336
  private getPlaylists;
337
+ /** image and video/stream slides play directly; web slides only when the url is itself a media file. html/pdf/multi_zone and generic web pages aren't playable in ChurchApps. */
338
+ private slideMediaType;
339
+ /** The documented player contract; /content/playlists/:id/messages only covers templates 1 and 3 so most playlists come back empty there. */
334
340
  private getMessages;
341
+ private getLegacyMessages;
335
342
  getPlaylist(path: string, auth?: ContentProviderAuthData | null, _resolution?: number): Promise<ContentFile[] | null>;
336
343
  getInstructions(path: string, auth?: ContentProviderAuthData | null): Promise<Instructions | null>;
337
344
  generateCodeVerifier(): string;
package/dist/index.js CHANGED
@@ -20,13 +20,15 @@ function navigateToPath(instructions, path) {
20
20
 
21
21
  // src/helpers/TokenHelper.ts
22
22
  function toAuthData(data, fallbacks) {
23
+ const planTypeId = data.plan_type_id || fallbacks?.planTypeId;
23
24
  return {
24
25
  access_token: data.access_token,
25
26
  refresh_token: data.refresh_token || fallbacks?.refreshToken || "",
26
27
  token_type: data.token_type || "Bearer",
27
28
  created_at: Math.floor(Date.now() / 1e3),
28
29
  expires_in: data.expires_in,
29
- scope: data.scope || fallbacks?.scope || ""
30
+ scope: data.scope || fallbacks?.scope || "",
31
+ ...planTypeId ? { planTypeId } : {}
30
32
  };
31
33
  }
32
34
  var TokenHelper = class {
@@ -54,7 +56,8 @@ var TokenHelper = class {
54
56
  return null;
55
57
  }
56
58
  const data = await response.json();
57
- return toAuthData(data, { refreshToken: auth.refresh_token, scope: auth.scope });
59
+ const planTypeId = auth.planTypeId;
60
+ return toAuthData(data, { refreshToken: auth.refresh_token, scope: auth.scope, planTypeId });
58
61
  } catch {
59
62
  return null;
60
63
  }
@@ -191,7 +194,8 @@ var ApiHelper = class {
191
194
  const options = { method, headers, ...body ? { body: JSON.stringify(body) } : {} };
192
195
  const response = await fetch(url, options);
193
196
  if (!response.ok) {
194
- console.warn(`[${providerId}] apiRequest failed: ${method} ${url} \u2192 HTTP ${response.status} ${response.statusText}`);
197
+ const detail = await response.text().catch(() => "");
198
+ console.warn(`[${providerId}] apiRequest failed: ${method} ${url} \u2192 HTTP ${response.status} ${response.statusText}${detail ? ` ${detail.slice(0, 500)}` : ""}`);
195
199
  return null;
196
200
  }
197
201
  return await response.json();
@@ -252,16 +256,19 @@ var IMAGE_EXTENSIONS = [
252
256
  ".tiff",
253
257
  ".tif"
254
258
  ];
259
+ var AUDIO_EXTENSIONS = [".mp3", ".m4a", ".aac", ".wav", ".flac", ".oga"];
255
260
  function detectMediaType(url, explicitType) {
256
261
  if (explicitType === "video" || explicitType?.startsWith("video/")) return "video";
257
262
  if (explicitType === "image" || explicitType?.startsWith("image/")) return "image";
263
+ if (explicitType === "audio" || explicitType?.startsWith("audio/")) return "audio";
258
264
  const lower = url.toLowerCase();
259
265
  if (VIDEO_EXTENSIONS.some((p) => lower.includes(p)) || lower.includes("stream.mux.com")) return "video";
266
+ if (AUDIO_EXTENSIONS.some((p) => lower.includes(p))) return "audio";
260
267
  return "image";
261
268
  }
262
269
  function isMediaFile(filename) {
263
270
  const lower = filename.toLowerCase();
264
- return [...VIDEO_EXTENSIONS, ...IMAGE_EXTENSIONS].some((ext) => lower.endsWith(ext));
271
+ return [...VIDEO_EXTENSIONS, ...IMAGE_EXTENSIONS, ...AUDIO_EXTENSIONS].some((ext) => lower.endsWith(ext));
265
272
  }
266
273
  function createFolder(id, title, path, thumbnail, isLeaf) {
267
274
  return { type: "folder", id, title, path, thumbnail, isLeaf };
@@ -541,13 +548,45 @@ var SignPresenterProvider = class extends BaseProvider {
541
548
  isLeaf: true
542
549
  }));
543
550
  }
551
+ /** image and video/stream slides play directly; web slides only when the url is itself a media file. html/pdf/multi_zone and generic web pages aren't playable in ChurchApps. */
552
+ slideMediaType(slideType, url) {
553
+ if (slideType === "image") return "image";
554
+ if (slideType === "video" || slideType === "stream") return "video";
555
+ if (slideType === "web" && isMediaFile(url)) return detectMediaType(url);
556
+ return null;
557
+ }
558
+ /** The documented player contract; /content/playlists/:id/messages only covers templates 1 and 3 so most playlists come back empty there. */
544
559
  async getMessages(playlistId, auth) {
560
+ const response = await this.apiRequest(`/v2/playlists/${playlistId}`, auth);
561
+ const messages = this.extractList(response, "messages");
562
+ if (messages.length === 0) return this.getLegacyMessages(playlistId, auth);
563
+ const files = [];
564
+ messages.forEach((msg, messageIndex) => {
565
+ const slides = Array.isArray(msg.slides) ? msg.slides : [];
566
+ slides.forEach((slide, slideIndex) => {
567
+ const data = slide.data || {};
568
+ const url = typeof data.url === "string" ? data.url.trim() : "";
569
+ const mediaType = url ? this.slideMediaType(slide.type, url) : null;
570
+ if (!mediaType) return;
571
+ const file = createFile(`${playlistId}-${messageIndex}-${slideIndex}`, msg.name, url, {
572
+ mediaType,
573
+ thumbnail: msg.thumbnail,
574
+ seconds: slide.seconds,
575
+ loop: data.loop
576
+ });
577
+ file.downloadUrl = url;
578
+ files.push(file);
579
+ });
580
+ });
581
+ return files;
582
+ }
583
+ async getLegacyMessages(playlistId, auth) {
545
584
  const response = await this.apiRequest(`/content/playlists/${playlistId}/messages`, auth);
546
585
  if (!response) return [];
547
586
  const files = [];
548
587
  for (const msg of this.extractList(response, "messages")) {
549
- if (!msg.url) continue;
550
- const url = msg.url;
588
+ const url = [msg.url, msg.thumbnail, msg.image].map((v) => typeof v === "string" ? v.trim() : "").find(Boolean) || "";
589
+ if (!url) continue;
551
590
  const file = createFile(msg.id, msg.name, url, {
552
591
  mediaType: detectMediaType(url, msg.mediaType),
553
592
  thumbnail: msg.thumbnail || msg.image,
@@ -1396,9 +1435,7 @@ var B1ChurchProvider = class extends BaseProvider {
1396
1435
  setPairingData(data) {
1397
1436
  if (data && typeof data === "object" && "planTypeId" in data) {
1398
1437
  const planTypeId = data.planTypeId;
1399
- this.planTypeId = typeof planTypeId === "string" ? planTypeId : null;
1400
- } else {
1401
- this.planTypeId = null;
1438
+ if (typeof planTypeId === "string") this.planTypeId = planTypeId;
1402
1439
  }
1403
1440
  }
1404
1441
  async getCurrentPlan(authData) {
@@ -17761,7 +17798,9 @@ var GoogleDriveProvider = class extends BaseProvider {
17761
17798
  orderBy: "folder,name_natural",
17762
17799
  pageSize: "1000",
17763
17800
  supportsAllDrives: "true",
17764
- includeItemsFromAllDrives: "true"
17801
+ // corpora=user (not includeItemsFromAllDrives) — "root" is only meaningful in the user's own corpus, and
17802
+ // combining it with an all-drives search makes Google reject the query, which browse saw as an empty folder.
17803
+ corpora: "user"
17765
17804
  });
17766
17805
  if (pageToken) params.set("pageToken", pageToken);
17767
17806
  const response = await this.apiRequest(`/files?${params.toString()}`, auth);
@@ -17775,7 +17814,7 @@ var GoogleDriveProvider = class extends BaseProvider {
17775
17814
  async childItems(folderId, auth) {
17776
17815
  const entries = await this.listFiles(`'${folderId}' in parents and trashed=false`, auth);
17777
17816
  const folders = entries.filter((e) => e.mimeType === FOLDER_MIME);
17778
- const mediaFiles = entries.filter((e) => e.mimeType.startsWith("video/") || e.mimeType.startsWith("image/"));
17817
+ const mediaFiles = entries.filter((e) => e.mimeType !== FOLDER_MIME && !e.mimeType.startsWith("application/vnd.google-apps") && (["video/", "image/", "audio/"].some((prefix) => e.mimeType.startsWith(prefix)) || isMediaFile(e.name)));
17779
17818
  return { folders, mediaFiles };
17780
17819
  }
17781
17820
  /** One batched query marks which sibling folders have subfolders; null = unknown (isLeaf omitted). */
@@ -17932,7 +17971,7 @@ function getAvailableProviders(ids) {
17932
17971
  }
17933
17972
 
17934
17973
  // src/index.ts
17935
- var VERSION = true ? "0.9.1" : "dev";
17974
+ var VERSION = true ? "0.9.3" : "dev";
17936
17975
  export {
17937
17976
  APlayProvider,
17938
17977
  B1ChurchProvider,