Skip to main content
Glama
loomClient.ts7.43 kB
export interface LoomConfig { apiKey?: string personalAccessToken?: string baseUrl: string } export interface LoomTranscript { text: string segments: Array<{ text: string start_time: number end_time: number confidence?: number }> } export interface LoomVideoOwner { id: string name: string email?: string } export interface LoomVideoMetadata { id: string name: string description?: string duration: number created_at: string updated_at: string owner: LoomVideoOwner privacy: 'public' | 'private' | 'password' | 'organization' thumbnail_url: string gif_thumbnail_url?: string video_url?: string embed_url: string transcript_available: boolean } export interface VideoChapter { id: string title: string start_time: number end_time: number } export class LoomClient { private config: LoomConfig private headers: Record<string, string> constructor(config: LoomConfig) { this.config = { baseUrl: 'https://api.loom.com/v2', ...config, } this.headers = { Accept: 'application/json', 'Content-Type': 'application/json', } if (config.personalAccessToken) { this.headers['Authorization'] = `Bearer ${config.personalAccessToken}` } else if (config.apiKey) { this.headers['X-API-Key'] = config.apiKey } } async getVideo(videoId: string): Promise<LoomVideoMetadata | null> { try { const response = await fetch(`${this.config.baseUrl}/videos/${videoId}`, { method: 'GET', headers: this.headers, }) if (!response.ok) { if (response.status === 404) { console.error(`Loom video not found: ${videoId}`) return null } if (response.status === 401 || response.status === 403) { console.error(`Loom API authentication failed for video: ${videoId}`) return null } throw new Error(`Failed to fetch Loom video: ${response.statusText}`) } return await response.json() } catch (error) { console.error('Error fetching Loom video metadata:', error) return null } } async getTranscript(videoId: string): Promise<LoomTranscript | null> { try { const response = await fetch(`${this.config.baseUrl}/videos/${videoId}/transcript`, { method: 'GET', headers: this.headers, }) if (!response.ok) { if (response.status === 404) { console.error(`Transcript not available for video: ${videoId}`) return null } throw new Error(`Failed to fetch transcript: ${response.statusText}`) } return await response.json() } catch (error) { console.error('Error fetching Loom transcript:', error) return null } } async getVideoChapters(videoId: string): Promise<VideoChapter[]> { try { const response = await fetch(`${this.config.baseUrl}/videos/${videoId}/chapters`, { method: 'GET', headers: this.headers, }) if (!response.ok) { if (response.status === 404) { return [] } throw new Error(`Failed to fetch chapters: ${response.statusText}`) } const data = await response.json() return data.chapters || [] } catch (error) { console.error('Error fetching Loom chapters:', error) return [] } } /** * Get video metadata without authentication (for public videos) * This uses the OEmbed endpoint which doesn't require authentication */ async getPublicVideoInfo(videoId: string): Promise<any> { try { // Try the share page API endpoint first (which includes HLS sources) const shareUrl = `https://www.loom.com/share/${videoId}` const pageResponse = await fetch(shareUrl, { method: 'GET', headers: { Accept: 'text/html', 'User-Agent': 'Mozilla/5.0 (compatible; LoomBot/1.0)', }, }) if (pageResponse.ok) { const html = await pageResponse.text() // Extract the __NEXT_DATA__ JSON from the page const scriptMatch = html.match( /<script id="__NEXT_DATA__" type="application\/json">(.*?)<\/script>/ ) if (scriptMatch && scriptMatch[1]) { try { const nextData = JSON.parse(scriptMatch[1]) const videoData = nextData?.props?.pageProps?.videoData if (videoData) { console.log('Successfully extracted video data from share page') return videoData } } catch (parseError) { console.error('Failed to parse __NEXT_DATA__:', parseError) } } } // Fallback to OEmbed API const oembedUrl = `https://www.loom.com/v1/oembed?url=https://www.loom.com/share/${videoId}&format=json` const response = await fetch(oembedUrl, { method: 'GET', headers: { Accept: 'application/json', }, }) if (!response.ok) { throw new Error(`Failed to fetch public video info: ${response.statusText}`) } return await response.json() } catch (error) { console.error('Error fetching public Loom video info:', error) return null } } /** * Get video metadata (alias for getVideo for compatibility) */ async getVideoMetadata(videoId: string): Promise<any> { // First try authenticated API const videoData = await this.getVideo(videoId) if (videoData) { return videoData } // Fall back to public API return await this.getPublicVideoInfo(videoId) } /** * Get video transcript (returns in expected format for video analysis) */ async getVideoTranscript(videoId: string): Promise<any> { const transcript = await this.getTranscript(videoId) if (!transcript) return null return { segments: transcript.segments, fullText: transcript.text, } } /** * Extract video ID from various Loom URL formats */ static extractVideoId(url: string): string | null { const patterns = [ /loom\.com\/share\/([a-zA-Z0-9]+)/, /loom\.com\/embed\/([a-zA-Z0-9]+)/, /loom\.com\/v\/([a-zA-Z0-9]+)/, ] for (const pattern of patterns) { const match = url.match(pattern) if (match && match[1]) { return match[1] } } return null } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/currentspace/shortcut_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server