@daitanjs/media
Version:
A library for downloading and managing media, particularly YouTube content, using yt-dlp and YouTube Data API.
304 lines (301 loc) • 9.12 kB
JavaScript
// src/media/src/index.js
import { getLogger as getLogger2 } from "@daitanjs/development";
// src/media/src/youtube.js
import { query as apiQuery } from "@daitanjs/apiqueries";
import { getLogger } from "@daitanjs/development";
import { getConfigManager } from "@daitanjs/config";
import {
DaitanApiError,
DaitanConfigurationError,
DaitanNotFoundError,
DaitanOperationError,
DaitanInvalidInputError,
DaitanError
} from "@daitanjs/error";
import { transcribeAudio } from "@daitanjs/speech";
import { spawn } from "child_process";
import path from "path";
import fs from "fs/promises";
import os from "os";
var logger = getLogger("daitan-media-youtube");
var YOUTUBE_API_BASE_URL = "https://www.googleapis.com/youtube/v3";
var YOUTUBE_API_KEY_CACHE = null;
var getYoutubeApiKey = () => {
const configManager = getConfigManager();
if (YOUTUBE_API_KEY_CACHE) return YOUTUBE_API_KEY_CACHE;
const apiKey = configManager.get("YOUTUBE_API_KEY");
if (!apiKey)
throw new DaitanConfigurationError(
"YOUTUBE_API_KEY is required but not configured."
);
YOUTUBE_API_KEY_CACHE = apiKey;
return apiKey;
};
var fetchVideoDetails = async (videoId) => {
const apiKey = getYoutubeApiKey();
if (!videoId || typeof videoId !== "string" || !videoId.trim()) {
throw new DaitanInvalidInputError("Invalid videoId provided.");
}
const url = `${YOUTUBE_API_BASE_URL}/videos`;
const params = {
part: "snippet,contentDetails,statistics,status",
id: videoId,
key: apiKey
};
try {
const response = await apiQuery({
url,
params,
summary: `Fetch YouTube video details for ${videoId}`
});
if (!response?.items?.[0])
throw new DaitanNotFoundError(`Video not found for ID: ${videoId}`);
return response.items[0];
} catch (error) {
if (error instanceof DaitanError) throw error;
throw new DaitanApiError(
`Failed to fetch video details for ${videoId}: ${error.message}`,
"YouTube Data API",
error.response?.status,
{ videoId },
error
);
}
};
var searchVideos = async (queryStr, options = {}) => {
const apiKey = getYoutubeApiKey();
if (!queryStr || typeof queryStr !== "string" || !queryStr.trim()) {
throw new DaitanInvalidInputError(
"Search query must be a non-empty string."
);
}
const url = `${YOUTUBE_API_BASE_URL}/search`;
const params = {
part: "snippet",
q: queryStr,
type: "video",
maxResults: 10,
order: "relevance",
key: apiKey,
...options
};
try {
const response = await apiQuery({
url,
params,
summary: `YouTube search for: ${queryStr}`
});
return {
items: response?.items || [],
nextPageToken: response.nextPageToken,
pageInfo: response.pageInfo
};
} catch (error) {
if (error instanceof DaitanError) throw error;
throw new DaitanApiError(
`YouTube search failed for query "${queryStr}": ${error.message}`,
"YouTube Data API",
error.response?.status,
{ query: queryStr },
error
);
}
};
var fetchVideoComments = async (videoId, options = {}) => {
const apiKey = getYoutubeApiKey();
if (!videoId) throw new DaitanInvalidInputError("Invalid videoId provided.");
const url = `${YOUTUBE_API_BASE_URL}/commentThreads`;
const params = {
part: "snippet,replies",
videoId,
maxResults: 20,
order: "relevance",
key: apiKey,
...options
};
const response = await apiQuery({
url,
params,
summary: `Fetch comments for ${videoId}`
});
return {
comments: response?.items || [],
nextPageToken: response.nextPageToken,
pageInfo: response.pageInfo
};
};
var fetchChannelDetails = async (channelId) => {
const apiKey = getYoutubeApiKey();
if (!channelId)
throw new DaitanInvalidInputError("Invalid channelId provided.");
const url = `${YOUTUBE_API_BASE_URL}/channels`;
const params = { part: "snippet,statistics", id: channelId, key: apiKey };
const response = await apiQuery({
url,
params,
summary: `Fetch channel details for ${channelId}`
});
if (!response?.items?.[0])
throw new DaitanNotFoundError(`Channel not found for ID: ${channelId}`);
return response.items[0];
};
var fetchChannelVideos = async (channelId, options = {}) => {
const apiKey = getYoutubeApiKey();
if (!channelId)
throw new DaitanInvalidInputError("Invalid channelId provided.");
const url = `${YOUTUBE_API_BASE_URL}/search`;
const params = {
part: "snippet",
channelId,
maxResults: 10,
order: "date",
type: "video",
key: apiKey,
...options
};
const response = await apiQuery({
url,
params,
summary: `Fetch videos for channel ${channelId}`
});
return {
videos: response?.items || [],
nextPageToken: response.nextPageToken,
pageInfo: response.pageInfo
};
};
function convertURLtoMP3({ url: videoUrl, outputDir, baseName }) {
const configManager = getConfigManager();
return new Promise(async (resolve, reject) => {
if (!videoUrl || !outputDir || !baseName)
return reject(
new DaitanInvalidInputError(
"URL, outputDir, and baseName are required."
)
);
try {
await fs.mkdir(outputDir, { recursive: true });
} catch (dirError) {
return reject(
new DaitanFileOperationError(
`Failed to create output directory: ${dirError.message}`,
{ path: outputDir },
dirError
)
);
}
const outputTemplate = path.resolve(outputDir, `${baseName}.%(ext)s`);
const ytDlpCommand = configManager.get("YT_DLP_PATH", "yt-dlp");
const args = [
"--extract-audio",
"--audio-format",
"mp3",
"--output",
outputTemplate,
videoUrl
];
const ytDlpProcess = spawn(ytDlpCommand, args);
ytDlpProcess.on("close", async (code) => {
if (code === 0) {
const finalPath = path.resolve(outputDir, `${baseName}.mp3`);
try {
const stats = await fs.stat(finalPath);
if (stats.isFile() && stats.size > 0) resolve(finalPath);
else
reject(
new DaitanOperationError(
"yt-dlp succeeded but output file is empty or missing."
)
);
} catch (statError) {
reject(
new DaitanOperationError(
`yt-dlp succeeded but output file verification failed: ${statError.message}`
)
);
}
} else {
reject(new DaitanOperationError(`yt-dlp failed with code ${code}.`));
}
});
ytDlpProcess.on(
"error",
(err) => reject(
new DaitanOperationError(
`Failed to start yt-dlp: ${err.message}`,
{},
err
)
)
);
});
}
var transcribeYoutubeVideo = async ({ url, config = {} }) => {
const callId = `transcribe-yt-${Date.now().toString(36)}`;
logger.info(
`[${callId}] Initiating YouTube transcription workflow for URL: ${url}`
);
if (!url || typeof url !== "string" || !url.includes("youtube.com")) {
throw new DaitanInvalidInputError("A valid YouTube video URL is required.");
}
const tempDir = path.join(os.tmpdir(), "daitanjs-yt-transcripts");
const baseName = `audio_${callId}`;
let tempAudioPath = null;
try {
logger.debug(
`[${callId}] Step 1: Downloading audio to temporary directory...`
);
tempAudioPath = await convertURLtoMP3({
url,
outputDir: tempDir,
baseName
});
logger.info(
`[${callId}] Audio downloaded successfully to: ${tempAudioPath}`
);
logger.debug(`[${callId}] Step 2: Transcribing audio file...`);
const transcriptionResult = await transcribeAudio({
source: { filePath: tempAudioPath },
config
});
logger.info(`[${callId}] Transcription successful.`);
return transcriptionResult;
} catch (error) {
logger.error(
`[${callId}] YouTube transcription workflow failed: ${error.message}`
);
if (error instanceof DaitanError) throw error;
throw new DaitanOperationError(
`YouTube transcription workflow failed for URL "${url}": ${error.message}`,
{ url },
error
);
} finally {
if (tempAudioPath) {
try {
await fs.unlink(tempAudioPath);
logger.debug(
`[${callId}] Step 3: Successfully cleaned up temporary audio file: ${tempAudioPath}`
);
} catch (cleanupError) {
logger.error(
`[${callId}] CRITICAL: Failed to clean up temporary audio file at ${tempAudioPath}. Manual cleanup may be required. Error: ${cleanupError.message}`
);
}
}
}
};
// src/media/src/index.js
var mediaIndexLogger = getLogger2("daitan-media-index");
mediaIndexLogger.debug("Exporting DaitanJS Media module functionalities...");
mediaIndexLogger.info("DaitanJS Media module exports ready.");
export {
convertURLtoMP3,
fetchChannelDetails,
fetchChannelVideos,
fetchVideoComments,
fetchVideoDetails,
searchVideos,
transcribeYoutubeVideo
};
//# sourceMappingURL=index.js.map