Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/googledrive-audio-listing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@churchapps/content-providers": patch
---

List Google Drive audio files and extension-matched media (e.g. octet-stream .mp4 uploads) when browsing plans content, and add "audio" as a media type (ChurchAppsSupport #944).
4 changes: 2 additions & 2 deletions content-providers/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export interface ContentFile {
type: "file";
id: string;
title: string;
mediaType: "video" | "image";
mediaType: "video" | "image" | "audio";
thumbnail?: string;
url: string;
downloadUrl?: string;
Expand Down Expand Up @@ -172,7 +172,7 @@ export interface InstructionItem {
children?: InstructionItem[];
downloadUrl?: string;
thumbnail?: string;
mediaType?: "video" | "image";
mediaType?: "video" | "image" | "audio";
}

export interface Instructions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "../../interfaces";
import { OAuthHelper } from "../../helpers";
import { getProviderSecret } from "../../helpers/ProviderSecrets";
import { detectMediaType, filesToInstructions } from "../../utils";
import { detectMediaType, filesToInstructions, isMediaFile } from "../../utils";
import { BaseProvider } from "../BaseProvider";
import { DriveFile, DriveFileListResponse } from "./GoogleDriveInterfaces";

Expand Down Expand Up @@ -82,7 +82,10 @@ export class GoogleDriveProvider extends BaseProvider {
private async childItems(folderId: string, auth?: ContentProviderAuthData | null): Promise<{ folders: DriveFile[]; mediaFiles: DriveFile[] }> {
const entries = await this.listFiles(`'${folderId}' in parents and trashed=false`, auth);
const folders = entries.filter(e => e.mimeType === FOLDER_MIME);
const mediaFiles = entries.filter(e => e.mimeType.startsWith("video/") || e.mimeType.startsWith("image/"));
// Drive reports uploads like .mp3/.mp4 as application/octet-stream at times; fall back to the filename. Google-apps docs (Docs/Sheets/Slides) are never playable.
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)));
return { folders, mediaFiles };
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function buildSectionActionsMap(actionsResponse: VenueActionsResponseInte
const actionThumbnailMap = new Map<string, string>();
const actionUrlMap = new Map<string, string>();
const actionContentMap = new Map<string, string>();
const actionMediaTypeMap = new Map<string, "video" | "image">();
const actionMediaTypeMap = new Map<string, "video" | "image" | "audio">();
if (feedResponse?.sections) {
for (const section of feedResponse.sections) {
for (const action of section.actions || []) {
Expand Down
12 changes: 8 additions & 4 deletions content-providers/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,29 @@ const VIDEO_EXTENSIONS = [".mp4", ".webm", ".m3u8", ".mov", ".avi", ".mkv", ".m4
const IMAGE_EXTENSIONS = [
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".tiff", ".tif"
];
// Matches the formats the serving player already handles
const AUDIO_EXTENSIONS = [".mp3", ".m4a", ".aac", ".wav", ".flac", ".oga"];

export function detectMediaType(url: string, explicitType?: string): "video" | "image" {
export function detectMediaType(url: string, explicitType?: string): "video" | "image" | "audio" {
if (explicitType === "video" || explicitType?.startsWith("video/")) return "video";
if (explicitType === "image" || explicitType?.startsWith("image/")) return "image";
if (explicitType === "audio" || explicitType?.startsWith("audio/")) return "audio";
const lower = url.toLowerCase();
if (VIDEO_EXTENSIONS.some(p => lower.includes(p)) || lower.includes("stream.mux.com")) return "video";
if (AUDIO_EXTENSIONS.some(p => lower.includes(p))) return "audio";
return "image";
}

export function isMediaFile(filename: string): boolean {
const lower = filename.toLowerCase();
return [...VIDEO_EXTENSIONS, ...IMAGE_EXTENSIONS].some(ext => lower.endsWith(ext));
return [...VIDEO_EXTENSIONS, ...IMAGE_EXTENSIONS, ...AUDIO_EXTENSIONS].some(ext => lower.endsWith(ext));
}

export function createFolder(id: string, title: string, path: string, thumbnail?: string, isLeaf?: boolean): ContentFolder {
return { type: "folder", id, title, path, thumbnail, isLeaf };
}

export function createFile(id: string, title: string, url: string, options?: { mediaType?: "video" | "image"; thumbnail?: string; muxPlaybackId?: string; seconds?: number; loop?: boolean; loopVideo?: boolean; streamUrl?: string; }): ContentFile {
export function createFile(id: string, title: string, url: string, options?: { mediaType?: "video" | "image" | "audio"; thumbnail?: string; muxPlaybackId?: string; seconds?: number; loop?: boolean; loopVideo?: boolean; streamUrl?: string; }): ContentFile {
return { type: "file", id, title, url, mediaType: options?.mediaType ?? detectMediaType(url), thumbnail: options?.thumbnail, muxPlaybackId: options?.muxPlaybackId, seconds: options?.seconds, loop: options?.loop, loopVideo: options?.loopVideo, streamUrl: options?.streamUrl };
}

Expand Down Expand Up @@ -71,7 +75,7 @@ export function filesToInstructions(name: string, files: ContentFile[], section?
}

/** Declared type first; else sniff the label (usually the original filename) alongside the URL — extension-less URLs (Dropbox temp links) defeat URL sniffing alone. */
export function instructionItemMediaType(item: InstructionItem): "video" | "image" {
export function instructionItemMediaType(item: InstructionItem): "video" | "image" | "audio" {
return detectMediaType(`${item.downloadUrl || ""} ${item.label || ""}`, item.mediaType);
}

Expand Down
15 changes: 12 additions & 3 deletions content-providers/tests/googleDriveProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const listing = {
{ id: "folderB", name: "Week 10", mimeType: "application/vnd.google-apps.folder" },
{ id: "vid1", name: "Opener.mp4", mimeType: "video/mp4", thumbnailLink: "https://x/t.jpg", webContentLink: "https://drive.example/dl?id=vid1" },
{ id: "img1", name: "Slide", mimeType: "image/png" },
{ id: "doc1", name: "Notes", mimeType: "application/vnd.google-apps.document" }
{ id: "aud1", name: "Worship Set.mp3", mimeType: "audio/mpeg" },
{ id: "octet1", name: "Recap.mp4", mimeType: "application/octet-stream" },
{ id: "pdf1", name: "Notes.pdf", mimeType: "application/pdf" }
]
};
const subfolderListing = { files: [{ id: "sub1", name: "Inner", mimeType: "application/vnd.google-apps.folder", parents: ["folderA"] }] };
Expand All @@ -36,7 +38,7 @@ test("browse maps folders (with leaf detection) and media files, dropping non-me
const items = await new GoogleDriveProvider().browse("/parent1", auth);

assert.equal(new URL(requests[0]).searchParams.get("q"), "'parent1' in parents and trashed=false");
assert.deepEqual(items.map(i => [i.type, i.id]), [["folder", "folderA"], ["folder", "folderB"], ["file", "vid1"], ["file", "img1"]]);
assert.deepEqual(items.map(i => [i.type, i.id]), [["folder", "folderA"], ["folder", "folderB"], ["file", "vid1"], ["file", "img1"], ["file", "aud1"], ["file", "octet1"]]);

const [folderA, folderB] = items as any[];
assert.equal(folderA.path, "/folderA");
Expand All @@ -51,6 +53,13 @@ test("browse maps folders (with leaf detection) and media files, dropping non-me
const image = items[3] as any;
assert.equal(image.mediaType, "image");
assert.equal(image.url, "https://drive.google.com/uc?id=img1&export=download");

const audio = items[4] as any;
assert.equal(audio.mediaType, "audio");

// Extension rescues files Drive reports as octet-stream; the .mp4 name types it as video
const octet = items[5] as any;
assert.equal(octet.mediaType, "video");
} finally {
restore();
}
Expand All @@ -77,7 +86,7 @@ test("getPlaylist returns media files for a folder and null at root or when empt
try {
const provider = new GoogleDriveProvider();
const files = await provider.getPlaylist("/parent1", auth);
assert.deepEqual(files?.map(f => f.id), ["vid1", "img1"]);
assert.deepEqual(files?.map(f => f.id), ["vid1", "img1", "aud1", "octet1"]);
assert.equal(await provider.getPlaylist("/", auth), null);
} finally {
restore();
Expand Down
Loading