From dc25e9f1eaf5e63d9df0d99501d209eae773ce12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20Je=C5=BEek?= Date: Sun, 19 Apr 2026 15:00:44 +0200 Subject: [PATCH] feat(apps): add YouTube connector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Web: OAuth AppDefinition using youtube.readonly as the default scope (channels, playlists, subscriptions, uploads) - Gateway: route youtube.googleapis.com and the legacy www.googleapis.com/youtube/ path via Bearer auth with shared GOOGLE_REFRESH config Signed-off-by: Radek Ježek --- apps/gateway/src/apps.rs | 26 ++++++++++++++++ apps/web/public/icons/youtube.svg | 4 +++ apps/web/src/lib/apps/registry.ts | 2 ++ apps/web/src/lib/apps/youtube.ts | 51 +++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 apps/web/public/icons/youtube.svg create mode 100644 apps/web/src/lib/apps/youtube.ts diff --git a/apps/gateway/src/apps.rs b/apps/gateway/src/apps.rs index d7c3fcd..a74f0d7 100644 --- a/apps/gateway/src/apps.rs +++ b/apps/gateway/src/apps.rs @@ -257,6 +257,24 @@ static APP_PROVIDERS: &[AppProvider] = &[ }], refresh: Some(&GOOGLE_REFRESH), }, + AppProvider { + provider: "youtube", + display_name: "YouTube", + host_rules: &[ + HostRule { + host: "youtube.googleapis.com", + path_prefix: None, + strategy: AuthStrategy::Bearer, + }, + // Legacy endpoint — some clients still use www.googleapis.com/youtube/ + HostRule { + host: "www.googleapis.com", + path_prefix: Some("/youtube/"), + strategy: AuthStrategy::Bearer, + }, + ], + refresh: Some(&GOOGLE_REFRESH), + }, AppProvider { provider: "google-health", display_name: "Google Health", @@ -712,6 +730,10 @@ mod tests { providers_for_host("health.googleapis.com"), vec!["google-health"] ); + assert_eq!( + providers_for_host("youtube.googleapis.com"), + vec!["youtube"] + ); } #[test] @@ -729,6 +751,7 @@ mod tests { ("google-meet", "meet.googleapis.com"), ("google-photos", "photoslibrary.googleapis.com"), ("google-health", "health.googleapis.com"), + ("youtube", "youtube.googleapis.com"), ]; for (provider, host) in &hosts { let injections = build_app_injections(provider, host, "ya29.test"); @@ -856,6 +879,9 @@ mod tests { let result = provider_for_host_and_path("www.googleapis.com", "/drive/v3/files"); assert_eq!(result, Some(("google-drive", "Google Drive"))); + + let result = provider_for_host_and_path("www.googleapis.com", "/youtube/v3/playlists"); + assert_eq!(result, Some(("youtube", "YouTube"))); } #[test] diff --git a/apps/web/public/icons/youtube.svg b/apps/web/public/icons/youtube.svg new file mode 100644 index 0000000..12b5892 --- /dev/null +++ b/apps/web/public/icons/youtube.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/lib/apps/registry.ts b/apps/web/src/lib/apps/registry.ts index 7b37c99..637f59f 100644 --- a/apps/web/src/lib/apps/registry.ts +++ b/apps/web/src/lib/apps/registry.ts @@ -17,6 +17,7 @@ import { googleSlides } from "./google-slides"; import { googleTasks } from "./google-tasks"; import { resend } from "./resend"; import { spotify } from "./spotify"; +import { youtube } from "./youtube"; export const apps: AppDefinition[] = [ github, @@ -37,6 +38,7 @@ export const apps: AppDefinition[] = [ googleTasks, resend, spotify, + youtube, ]; export const getApp = (id: string): AppDefinition | undefined => diff --git a/apps/web/src/lib/apps/youtube.ts b/apps/web/src/lib/apps/youtube.ts new file mode 100644 index 0000000..1b949af --- /dev/null +++ b/apps/web/src/lib/apps/youtube.ts @@ -0,0 +1,51 @@ +import type { AppDefinition } from "./types"; +import { + buildGoogleAuthUrl, + exchangeGoogleCode, + googleConfigFields, + googleEnvDefaults, +} from "./google-oauth"; + +export const youtube: AppDefinition = { + id: "youtube", + name: "YouTube", + icon: "/icons/youtube.svg", + description: + "Read your channels, playlists, playlist items, and subscriptions.", + connectionMethod: { + type: "oauth", + defaultScopes: [ + "openid", + "email", + "profile", + "https://www.googleapis.com/auth/youtube.readonly", + ], + permissions: [ + { + scope: "https://www.googleapis.com/auth/youtube.readonly", + name: "YouTube account", + description: "Channels, playlists, subscriptions, and uploads", + access: "read", + }, + { + scope: "https://www.googleapis.com/auth/userinfo.email", + name: "Email address", + description: "View your email address", + access: "read", + }, + { + scope: "https://www.googleapis.com/auth/userinfo.profile", + name: "Profile", + description: "Name and profile picture", + access: "read", + }, + ], + buildAuthUrl: buildGoogleAuthUrl, + exchangeCode: exchangeGoogleCode, + }, + available: true, + configurable: { + fields: googleConfigFields, + envDefaults: googleEnvDefaults, + }, +};