@churchapps/content-providers 0.7.3 → 0.9.0

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
@@ -82,7 +82,9 @@ interface ContentFile {
82
82
  type ContentItem = ContentFolder | ContentFile;
83
83
  declare function isContentFolder(item: ContentItem): item is ContentFolder;
84
84
  declare function isContentFile(item: ContentItem): item is ContentFile;
85
- type DeviceFlowPollResult = ContentProviderAuthData | {
85
+ type DeviceFlowPollResult = (ContentProviderAuthData & {
86
+ planTypeId?: string;
87
+ }) | {
86
88
  error: string;
87
89
  shouldSlowDown?: boolean;
88
90
  } | null;
package/dist/index.d.ts CHANGED
@@ -82,7 +82,9 @@ interface ContentFile {
82
82
  type ContentItem = ContentFolder | ContentFile;
83
83
  declare function isContentFolder(item: ContentItem): item is ContentFolder;
84
84
  declare function isContentFile(item: ContentItem): item is ContentFile;
85
- type DeviceFlowPollResult = ContentProviderAuthData | {
85
+ type DeviceFlowPollResult = (ContentProviderAuthData & {
86
+ planTypeId?: string;
87
+ }) | {
86
88
  error: string;
87
89
  shouldSlowDown?: boolean;
88
90
  } | null;
package/dist/index.js CHANGED
@@ -151,7 +151,8 @@ var DeviceFlowHelper = class {
151
151
  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 }) });
152
152
  if (response.ok) {
153
153
  const data = await response.json();
154
- return toAuthData(data, { scope: config.scopes.join(" ") });
154
+ const auth = toAuthData(data, { scope: config.scopes.join(" ") });
155
+ return data.plan_type_id ? { ...auth, planTypeId: data.plan_type_id } : auth;
155
156
  }
156
157
  const errorData = await response.json();
157
158
  switch (errorData.error) {
@@ -659,7 +660,7 @@ async function convertAddOnToFile(addOn) {
659
660
  } else {
660
661
  return null;
661
662
  }
662
- return { type: "file", id: addOn.id, title: addOn.name, mediaType, thumbnail: addOn.image, url, downloadUrl: url, seconds, loopVideo: video?.loopVideo || false };
663
+ return { type: "file", id: addOn.id, title: addOn.name ?? detail.name, mediaType, thumbnail: addOn.image ?? detail.image, url, downloadUrl: url, seconds, loopVideo: video?.loopVideo || false };
663
664
  }
664
665
  function buildSectionActionsMap(actionsResponse, lessonImage, feedResponse) {
665
666
  const sectionActionsMap = /* @__PURE__ */ new Map();
@@ -1184,7 +1185,7 @@ var SECTION_TYPES = /* @__PURE__ */ new Set(["item", "section", "lessonSection",
1184
1185
  var ACTION_TYPES = /* @__PURE__ */ new Set(["action", "lessonAction", "providerPresentation", "lessonAddOn", "addon", "providerFile", "file"]);
1185
1186
  function collectFilesFromNode(node) {
1186
1187
  const files = [];
1187
- if (node.downloadUrl) {
1188
+ if (node.downloadUrl && (node.itemType === "file" || !node.children?.length)) {
1188
1189
  files.push({
1189
1190
  id: node.id,
1190
1191
  name: node.label || "",
@@ -17702,6 +17703,150 @@ var GoCurriculumProvider = class extends BaseProvider {
17702
17703
  }
17703
17704
  };
17704
17705
 
17706
+ // src/providers/googledrive/GoogleDriveProvider.ts
17707
+ var FOLDER_MIME = "application/vnd.google-apps.folder";
17708
+ var MAX_PAGINATION_CALLS2 = 10;
17709
+ var LEAF_CHECK_MAX_FOLDERS = 50;
17710
+ var GoogleDriveProvider = class extends BaseProvider {
17711
+ constructor() {
17712
+ super(...arguments);
17713
+ this.oauthHelper = new OAuthHelper();
17714
+ this.id = "googledrive";
17715
+ this.name = "Google Drive";
17716
+ this.logos = {
17717
+ light: "https://ssl.gstatic.com/images/branding/product/2x/drive_2020q4_48dp.png",
17718
+ dark: "https://ssl.gstatic.com/images/branding/product/2x/drive_2020q4_48dp.png"
17719
+ };
17720
+ this.config = {
17721
+ id: "googledrive",
17722
+ name: "Google Drive",
17723
+ apiBase: "https://www.googleapis.com/drive/v3",
17724
+ // Token endpoint host; the user-facing consent screen is accounts.google.com (see buildAuthUrlFromChallenge)
17725
+ oauthBase: "https://oauth2.googleapis.com",
17726
+ // TODO owner action: Google Cloud OAuth web client id (drive.readonly scope) — fill before publishing
17727
+ clientId: "",
17728
+ // Google requires client_secret even for PKCE public clients; injected server-side via setProviderSecret("googledrive", ...)
17729
+ get clientSecret() {
17730
+ return getProviderSecret("googledrive");
17731
+ },
17732
+ scopes: ["https://www.googleapis.com/auth/drive.readonly"]
17733
+ };
17734
+ this.requiresAuth = true;
17735
+ this.authTypes = ["oauth_pkce"];
17736
+ this.capabilities = {
17737
+ browse: true,
17738
+ playlist: true,
17739
+ instructions: true,
17740
+ mediaLicensing: false
17741
+ };
17742
+ }
17743
+ // Paths are Drive folder ids ("/<id>"), not names; root is "" or "/"
17744
+ folderIdFromPath(path) {
17745
+ const trimmed = (path || "").replace(/^\//, "");
17746
+ return trimmed || "root";
17747
+ }
17748
+ isValidId(id) {
17749
+ return /^[\w-]+$/.test(id);
17750
+ }
17751
+ async listFiles(query, auth) {
17752
+ if (!auth) {
17753
+ console.error("[GoogleDrive] No auth token provided");
17754
+ return [];
17755
+ }
17756
+ const all = [];
17757
+ let pageToken;
17758
+ for (let i = 0; i < MAX_PAGINATION_CALLS2; i++) {
17759
+ const params = new URLSearchParams({
17760
+ q: query,
17761
+ fields: "nextPageToken,files(id,name,mimeType,thumbnailLink,webContentLink,parents)",
17762
+ orderBy: "folder,name_natural",
17763
+ pageSize: "1000",
17764
+ supportsAllDrives: "true",
17765
+ includeItemsFromAllDrives: "true"
17766
+ });
17767
+ if (pageToken) params.set("pageToken", pageToken);
17768
+ const response = await this.apiRequest(`/files?${params.toString()}`, auth);
17769
+ if (!response) break;
17770
+ all.push(...response.files || []);
17771
+ pageToken = response.nextPageToken;
17772
+ if (!pageToken) break;
17773
+ }
17774
+ return all;
17775
+ }
17776
+ async childItems(folderId, auth) {
17777
+ const entries = await this.listFiles(`'${folderId}' in parents and trashed=false`, auth);
17778
+ const folders = entries.filter((e) => e.mimeType === FOLDER_MIME);
17779
+ const mediaFiles = entries.filter((e) => e.mimeType.startsWith("video/") || e.mimeType.startsWith("image/"));
17780
+ return { folders, mediaFiles };
17781
+ }
17782
+ /** One batched query marks which sibling folders have subfolders; null = unknown (isLeaf omitted). */
17783
+ async leafIds(folders, auth) {
17784
+ if (folders.length === 0 || folders.length > LEAF_CHECK_MAX_FOLDERS) return null;
17785
+ const parentClause = folders.map((f) => `'${f.id}' in parents`).join(" or ");
17786
+ const subfolders = await this.listFiles(`mimeType='${FOLDER_MIME}' and trashed=false and (${parentClause})`, auth);
17787
+ const parentsWithSubfolders = new Set(subfolders.flatMap((s) => s.parents || []));
17788
+ return new Set(folders.filter((f) => !parentsWithSubfolders.has(f.id)).map((f) => f.id));
17789
+ }
17790
+ toContentFile(file) {
17791
+ const url = file.webContentLink || `https://drive.google.com/uc?id=${file.id}&export=download`;
17792
+ return {
17793
+ type: "file",
17794
+ id: file.id,
17795
+ title: file.name,
17796
+ mediaType: detectMediaType(file.name, file.mimeType),
17797
+ thumbnail: file.thumbnailLink,
17798
+ url,
17799
+ downloadUrl: url
17800
+ };
17801
+ }
17802
+ async browse(path, auth) {
17803
+ const folderId = this.folderIdFromPath(path);
17804
+ if (!this.isValidId(folderId)) return [];
17805
+ const { folders, mediaFiles } = await this.childItems(folderId, auth);
17806
+ const leaves = await this.leafIds(folders, auth);
17807
+ const folderItems = folders.map((f) => ({ type: "folder", id: f.id, title: f.name, path: `/${f.id}`, isLeaf: leaves ? leaves.has(f.id) : void 0 }));
17808
+ return [...folderItems, ...mediaFiles.map((f) => this.toContentFile(f))];
17809
+ }
17810
+ async getPlaylist(path, auth, _resolution) {
17811
+ const folderId = this.folderIdFromPath(path);
17812
+ if (folderId === "root" || !this.isValidId(folderId)) return null;
17813
+ const { mediaFiles } = await this.childItems(folderId, auth);
17814
+ return mediaFiles.length > 0 ? mediaFiles.map((f) => this.toContentFile(f)) : null;
17815
+ }
17816
+ async getInstructions(path, auth) {
17817
+ const files = await this.getPlaylist(path, auth);
17818
+ if (!files) return null;
17819
+ const meta = await this.apiRequest(`/files/${this.folderIdFromPath(path)}?fields=name&supportsAllDrives=true`, auth);
17820
+ const folderName = meta?.name || "Google Drive";
17821
+ return filesToInstructions(folderName, files, { id: "googledrive-section", label: folderName });
17822
+ }
17823
+ generateCodeVerifier() {
17824
+ return this.oauthHelper.generateCodeVerifier();
17825
+ }
17826
+ async buildAuthUrl(codeVerifier, redirectUri, state) {
17827
+ const codeChallenge = await this.oauthHelper.generateCodeChallenge(codeVerifier);
17828
+ return { url: this.buildAuthUrlFromChallenge(codeChallenge, redirectUri, state || this.id), challengeMethod: "S256" };
17829
+ }
17830
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
17831
+ const params = new URLSearchParams({
17832
+ response_type: "code",
17833
+ client_id: this.config.clientId,
17834
+ redirect_uri: redirectUri,
17835
+ scope: this.config.scopes.join(" "),
17836
+ code_challenge: codeChallenge,
17837
+ code_challenge_method: "S256",
17838
+ // offline + consent so Google issues a refresh_token on every connect
17839
+ access_type: "offline",
17840
+ prompt: "consent",
17841
+ state
17842
+ });
17843
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
17844
+ }
17845
+ async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
17846
+ return this.oauthHelper.exchangeCodeForTokens(this.config, this.id, code, codeVerifier, redirectUri);
17847
+ }
17848
+ };
17849
+
17705
17850
  // src/providers/index.ts
17706
17851
  var unimplementedProviders = [
17707
17852
  {
@@ -17743,6 +17888,7 @@ function initializeProviders() {
17743
17888
  new B1ChurchProvider(),
17744
17889
  new DropboxProvider(),
17745
17890
  new GoCurriculumProvider(),
17891
+ new GoogleDriveProvider(),
17746
17892
  new BibleProjectProvider(),
17747
17893
  new HighVoltageKidsProvider(),
17748
17894
  new JesusFilmProvider(),
@@ -17787,7 +17933,7 @@ function getAvailableProviders(ids) {
17787
17933
  }
17788
17934
 
17789
17935
  // src/index.ts
17790
- var VERSION = true ? "0.7.3" : "dev";
17936
+ var VERSION = true ? "0.9.0" : "dev";
17791
17937
  export {
17792
17938
  APlayProvider,
17793
17939
  B1ChurchProvider,