UNPKG

@daitanjs/media

Version:

A library for downloading and managing media, particularly YouTube content, using yt-dlp and YouTube Data API.

8 lines (7 loc) 16.9 kB
{ "version": 3, "sources": ["../src/index.js", "../src/youtube.js"], "sourcesContent": ["// media/src/index.js\n/**\n * @file Main entry point for the @daitanjs/media package.\n * @module @daitanjs/media\n *\n * @description\n * This package provides utilities for interacting with media sources, primarily focusing\n * on YouTube. It allows fetching video and channel information, searching for videos,\n * retrieving comments, and converting YouTube videos to MP3 audio using `yt-dlp`.\n * A high-level `transcribeYoutubeVideo` function orchestrates the download and\n * transcription of a video's audio in a single call.\n *\n * All operations are asynchronous and return Promises. Errors are handled using\n * custom DaitanJS error types for consistency.\n *\n * For YouTube Data API interactions, ensure `YOUTUBE_API_KEY` is configured.\n * For `convertURLtoMP3`, ensure `yt-dlp` is installed and accessible, or its path\n * is set via the `YT_DLP_PATH` environment variable.\n */\nimport { getLogger } from '@daitanjs/development';\n\nconst mediaIndexLogger = getLogger('daitan-media-index');\n\nmediaIndexLogger.debug('Exporting DaitanJS Media module functionalities...');\n\n// All functions related to YouTube, including the new high-level one.\nexport {\n fetchVideoDetails,\n searchVideos,\n fetchVideoComments,\n fetchChannelDetails,\n fetchChannelVideos,\n convertURLtoMP3,\n transcribeYoutubeVideo,\n} from './youtube.js';\n\nmediaIndexLogger.info('DaitanJS Media module exports ready.');\n", "// media/src/youtube.js\n/**\n * @file YouTube Data API and yt-dlp interaction utilities.\n * @module @daitanjs/media/youtube\n */\n\nimport { query as apiQuery } from '@daitanjs/apiqueries';\nimport { getLogger } from '@daitanjs/development';\nimport { getConfigManager } from '@daitanjs/config';\nimport {\n DaitanApiError,\n DaitanConfigurationError,\n DaitanNotFoundError,\n DaitanOperationError,\n DaitanInvalidInputError,\n DaitanError,\n} from '@daitanjs/error';\nimport { transcribeAudio } from '@daitanjs/speech';\nimport { spawn } from 'child_process';\nimport path from 'path';\nimport fs from 'fs/promises';\nimport os from 'os';\n\nconst logger = getLogger('daitan-media-youtube');\n\nconst YOUTUBE_API_BASE_URL = 'https://www.googleapis.com/youtube/v3';\nlet YOUTUBE_API_KEY_CACHE = null;\n\nconst getYoutubeApiKey = () => {\n const configManager = getConfigManager();\n\n if (YOUTUBE_API_KEY_CACHE) return YOUTUBE_API_KEY_CACHE;\n const apiKey = configManager.get('YOUTUBE_API_KEY');\n if (!apiKey)\n throw new DaitanConfigurationError(\n 'YOUTUBE_API_KEY is required but not configured.'\n );\n YOUTUBE_API_KEY_CACHE = apiKey;\n return apiKey;\n};\n\nexport const fetchVideoDetails = async (videoId) => {\n const apiKey = getYoutubeApiKey();\n if (!videoId || typeof videoId !== 'string' || !videoId.trim()) {\n throw new DaitanInvalidInputError('Invalid videoId provided.');\n }\n const url = `${YOUTUBE_API_BASE_URL}/videos`;\n const params = {\n part: 'snippet,contentDetails,statistics,status',\n id: videoId,\n key: apiKey,\n };\n try {\n const response = await apiQuery({\n url,\n params,\n summary: `Fetch YouTube video details for ${videoId}`,\n });\n if (!response?.items?.[0])\n throw new DaitanNotFoundError(`Video not found for ID: ${videoId}`);\n return response.items[0];\n } catch (error) {\n if (error instanceof DaitanError) throw error;\n throw new DaitanApiError(\n `Failed to fetch video details for ${videoId}: ${error.message}`,\n 'YouTube Data API',\n error.response?.status,\n { videoId },\n error\n );\n }\n};\n\nexport const searchVideos = async (queryStr, options = {}) => {\n const apiKey = getYoutubeApiKey();\n if (!queryStr || typeof queryStr !== 'string' || !queryStr.trim()) {\n throw new DaitanInvalidInputError(\n 'Search query must be a non-empty string.'\n );\n }\n const url = `${YOUTUBE_API_BASE_URL}/search`;\n const params = {\n part: 'snippet',\n q: queryStr,\n type: 'video',\n maxResults: 10,\n order: 'relevance',\n key: apiKey,\n ...options,\n };\n try {\n const response = await apiQuery({\n url,\n params,\n summary: `YouTube search for: ${queryStr}`,\n });\n return {\n items: response?.items || [],\n nextPageToken: response.nextPageToken,\n pageInfo: response.pageInfo,\n };\n } catch (error) {\n if (error instanceof DaitanError) throw error;\n throw new DaitanApiError(\n `YouTube search failed for query \"${queryStr}\": ${error.message}`,\n 'YouTube Data API',\n error.response?.status,\n { query: queryStr },\n error\n );\n }\n};\n\nexport const fetchVideoComments = async (videoId, options = {}) => {\n const apiKey = getYoutubeApiKey();\n if (!videoId) throw new DaitanInvalidInputError('Invalid videoId provided.');\n const url = `${YOUTUBE_API_BASE_URL}/commentThreads`;\n const params = {\n part: 'snippet,replies',\n videoId,\n maxResults: 20,\n order: 'relevance',\n key: apiKey,\n ...options,\n };\n const response = await apiQuery({\n url,\n params,\n summary: `Fetch comments for ${videoId}`,\n });\n return {\n comments: response?.items || [],\n nextPageToken: response.nextPageToken,\n pageInfo: response.pageInfo,\n };\n};\n\nexport const fetchChannelDetails = async (channelId) => {\n const apiKey = getYoutubeApiKey();\n if (!channelId)\n throw new DaitanInvalidInputError('Invalid channelId provided.');\n const url = `${YOUTUBE_API_BASE_URL}/channels`;\n const params = { part: 'snippet,statistics', id: channelId, key: apiKey };\n const response = await apiQuery({\n url,\n params,\n summary: `Fetch channel details for ${channelId}`,\n });\n if (!response?.items?.[0])\n throw new DaitanNotFoundError(`Channel not found for ID: ${channelId}`);\n return response.items[0];\n};\n\nexport const fetchChannelVideos = async (channelId, options = {}) => {\n const apiKey = getYoutubeApiKey();\n if (!channelId)\n throw new DaitanInvalidInputError('Invalid channelId provided.');\n const url = `${YOUTUBE_API_BASE_URL}/search`;\n const params = {\n part: 'snippet',\n channelId,\n maxResults: 10,\n order: 'date',\n type: 'video',\n key: apiKey,\n ...options,\n };\n const response = await apiQuery({\n url,\n params,\n summary: `Fetch videos for channel ${channelId}`,\n });\n return {\n videos: response?.items || [],\n nextPageToken: response.nextPageToken,\n pageInfo: response.pageInfo,\n };\n};\n\nexport function convertURLtoMP3({ url: videoUrl, outputDir, baseName }) {\n const configManager = getConfigManager();\n\n return new Promise(async (resolve, reject) => {\n if (!videoUrl || !outputDir || !baseName)\n return reject(\n new DaitanInvalidInputError(\n 'URL, outputDir, and baseName are required.'\n )\n );\n try {\n await fs.mkdir(outputDir, { recursive: true });\n } catch (dirError) {\n return reject(\n new DaitanFileOperationError(\n `Failed to create output directory: ${dirError.message}`,\n { path: outputDir },\n dirError\n )\n );\n }\n const outputTemplate = path.resolve(outputDir, `${baseName}.%(ext)s`);\n const ytDlpCommand = configManager.get('YT_DLP_PATH', 'yt-dlp');\n const args = [\n '--extract-audio',\n '--audio-format',\n 'mp3',\n '--output',\n outputTemplate,\n videoUrl,\n ];\n const ytDlpProcess = spawn(ytDlpCommand, args);\n ytDlpProcess.on('close', async (code) => {\n if (code === 0) {\n const finalPath = path.resolve(outputDir, `${baseName}.mp3`);\n try {\n const stats = await fs.stat(finalPath);\n if (stats.isFile() && stats.size > 0) resolve(finalPath);\n else\n reject(\n new DaitanOperationError(\n 'yt-dlp succeeded but output file is empty or missing.'\n )\n );\n } catch (statError) {\n reject(\n new DaitanOperationError(\n `yt-dlp succeeded but output file verification failed: ${statError.message}`\n )\n );\n }\n } else {\n reject(new DaitanOperationError(`yt-dlp failed with code ${code}.`));\n }\n });\n ytDlpProcess.on('error', (err) =>\n reject(\n new DaitanOperationError(\n `Failed to start yt-dlp: ${err.message}`,\n {},\n err\n )\n )\n );\n });\n}\n\n/**\n * @typedef {import('@daitanjs/speech').SttConfig} SttConfig\n */\n\n/**\n * @typedef {Object} TranscribeYoutubeVideoParams\n * @property {string} url - The full URL of the YouTube video.\n * @property {SttConfig} [config] - Configuration options for the speech-to-text process.\n */\n\n/**\n * Downloads a YouTube video's audio, transcribes it, and cleans up the temporary audio file.\n * @public\n * @async\n * @param {TranscribeYoutubeVideoParams} params - The parameters for the transcription workflow.\n * @returns {Promise<string|object>} The transcribed text or JSON object from the STT service.\n */\nexport const transcribeYoutubeVideo = async ({ url, config = {} }) => {\n const callId = `transcribe-yt-${Date.now().toString(36)}`;\n logger.info(\n `[${callId}] Initiating YouTube transcription workflow for URL: ${url}`\n );\n\n if (!url || typeof url !== 'string' || !url.includes('youtube.com')) {\n throw new DaitanInvalidInputError('A valid YouTube video URL is required.');\n }\n\n const tempDir = path.join(os.tmpdir(), 'daitanjs-yt-transcripts');\n const baseName = `audio_${callId}`;\n let tempAudioPath = null;\n\n try {\n logger.debug(\n `[${callId}] Step 1: Downloading audio to temporary directory...`\n );\n tempAudioPath = await convertURLtoMP3({\n url,\n outputDir: tempDir,\n baseName,\n });\n logger.info(\n `[${callId}] Audio downloaded successfully to: ${tempAudioPath}`\n );\n\n logger.debug(`[${callId}] Step 2: Transcribing audio file...`);\n const transcriptionResult = await transcribeAudio({\n source: { filePath: tempAudioPath },\n config,\n });\n logger.info(`[${callId}] Transcription successful.`);\n\n return transcriptionResult;\n } catch (error) {\n logger.error(\n `[${callId}] YouTube transcription workflow failed: ${error.message}`\n );\n if (error instanceof DaitanError) throw error;\n throw new DaitanOperationError(\n `YouTube transcription workflow failed for URL \"${url}\": ${error.message}`,\n { url },\n error\n );\n } finally {\n if (tempAudioPath) {\n try {\n await fs.unlink(tempAudioPath);\n logger.debug(\n `[${callId}] Step 3: Successfully cleaned up temporary audio file: ${tempAudioPath}`\n );\n } catch (cleanupError) {\n logger.error(\n `[${callId}] CRITICAL: Failed to clean up temporary audio file at ${tempAudioPath}. Manual cleanup may be required. Error: ${cleanupError.message}`\n );\n }\n }\n }\n};\n"], "mappings": "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", "names": ["import_development", "apiQuery", "fs", "path", "os"] }