Skip to content

Commit

Permalink
improve comments
Browse files Browse the repository at this point in the history
  • Loading branch information
ajcwebdev committed Oct 28, 2024
1 parent 760e62f commit f654642
Show file tree
Hide file tree
Showing 7 changed files with 503 additions and 123 deletions.
37 changes: 34 additions & 3 deletions src/utils/checkDependencies.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,51 @@
// src/utils/checkDependencies.ts

/**
* @file Utility for verifying required command-line dependencies.
* Checks if necessary external tools are installed and accessible in the system PATH.
* @packageDocumentation
*/

import { execFile } from 'node:child_process'
import { promisify } from 'node:util'

// Promisify execFile for async/await usage
const execFilePromise = promisify(execFile)

/**
* Check if required dependencies are installed.
* @param dependencies - List of command-line tools to check.
* @returns A promise that resolves when all dependencies are checked.
* Verifies that required command-line dependencies are installed and accessible.
* Attempts to execute each dependency with --version flag to confirm availability.
*
* Common dependencies checked include:
* - yt-dlp: For downloading online content
* - ffmpeg: For audio processing
*
* @param {string[]} dependencies - Array of command names to verify.
* Each command should support the --version flag.
*
* @returns {Promise<void>} Resolves when all dependencies are verified.
*
* @throws {Error} If any dependency is:
* - Not installed
* - Not found in system PATH
* - Not executable
* - Returns non-zero exit code
*
* @example
* try {
* await checkDependencies(['yt-dlp', 'ffmpeg'])
* console.log('All dependencies are available')
* } catch (error) {
* console.error('Missing dependency:', error.message)
* }
*/
export async function checkDependencies(dependencies: string[]): Promise<void> {
for (const command of dependencies) {
try {
// Attempt to execute command with --version flag
await execFilePromise(command, ['--version'])
} catch (error) {
// Throw descriptive error if command check fails
throw new Error(
`Dependency '${command}' is not installed or not found in PATH. Please install it to proceed.`
)
Expand Down
57 changes: 50 additions & 7 deletions src/utils/cleanUpFiles.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,72 @@
// src/utils/cleanUpFiles.ts

/**
* @file Utility for cleaning up temporary files generated during processing.
* Handles removal of intermediate files with specific extensions.
* @packageDocumentation
*/

import { unlink } from 'node:fs/promises'
import { log, step, success } from '../models.js'

/**
* Asynchronous function to clean up temporary files.
* @param {string} id - The base filename (without extension) for the files to be cleaned up.
* @returns {Promise<void>}
* @throws {Error} - If an error occurs while deleting files.
* Removes temporary files generated during content processing.
* Attempts to delete files with specific extensions and logs the results.
* Silently ignores attempts to delete non-existent files.
*
* Files cleaned up include:
* - .wav: Audio files
* - .txt: Transcription text
* - .md: Markdown content
* - .lrc: Lyrics/subtitles
*
* @param {string} id - Base filename (without extension) used to identify related files.
* All files matching pattern `${id}${extension}` will be deleted.
*
* @returns {Promise<void>} Resolves when cleanup is complete.
*
* @throws {Error} If deletion fails for reasons other than file not existing:
* - Permission denied
* - File is locked/in use
* - I/O errors
*
* @example
* try {
* await cleanUpFiles('content/my-video-2024-03-21')
* // Will attempt to delete:
* // - content/my-video-2024-03-21.wav
* // - content/my-video-2024-03-21.txt
* // - content/my-video-2024-03-21.md
* // - content/my-video-2024-03-21.lrc
* } catch (error) {
* console.error('Cleanup failed:', error)
* }
*/
export async function cleanUpFiles(id: string): Promise<void> {
log(step('\nStep 5 - Cleaning up temporary files...\n'))
// Array of file extensions to delete
const extensions = ['.wav', '.txt', '.md', '.lrc']

// Define extensions of temporary files to be cleaned up
const extensions = [
'.wav', // Audio files
'.txt', // Transcription text
'.md', // Markdown content
'.lrc' // Lyrics/subtitles
]

log(success(` Temporary files deleted:`))

// Attempt to delete each file type
for (const ext of extensions) {
try {
// Delete file and log success
await unlink(`${id}${ext}`)
log(success(` - ${id}${ext}`))
} catch (error) {
// Only log errors that aren't "file not found" (ENOENT)
if (error instanceof Error && (error as Error).message !== 'ENOENT') {
console.error(`Error deleting file ${id}${ext}: ${(error as Error).message}`)
}
// If the file does not exist, silently continue
// Silently continue if file doesn't exist
}
}
}
138 changes: 104 additions & 34 deletions src/utils/downloadAudio.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// src/utils/downloadAudio.ts

/**
* @file Utility for downloading and processing audio from various sources.
* Handles both online content (via yt-dlp) and local files (via ffmpeg),
* converting them to a standardized WAV format suitable for transcription.
* @packageDocumentation
*/

import { exec, execFile } from 'node:child_process'
import { promisify } from 'node:util'
import { readFile, access } from 'node:fs/promises'
Expand All @@ -8,83 +15,146 @@ import { checkDependencies } from './checkDependencies.js'
import { log, step, success, wait } from '../models.js'
import type { SupportedFileType, ProcessingOptions } from '../types.js'

// Promisify node:child_process functions for async/await usage
const execFilePromise = promisify(execFile)
const execPromise = promisify(exec)

/**
* Function to download or process audio based on the input type.
* @param {ProcessingOptions} options - The processing options specifying the type of content to generate.
* @param {string} input - The URL of the video or path to the local file.
* @param {string} filename - The base filename to save the audio as.
* @returns {Promise<string>} - Returns the path to the downloaded or processed WAV file.
* @throws {Error} - If there is an error during the download or processing.
* Downloads or processes audio content from various sources and converts it to a standardized WAV format.
*
* The function handles two main scenarios:
* 1. Online content (YouTube, RSS feeds) - Downloads using yt-dlp
* 2. Local files - Converts using ffmpeg
*
* In both cases, the output is converted to:
* - WAV format
* - 16kHz sample rate
* - Mono channel
* - 16-bit PCM encoding
*
* @param {ProcessingOptions} options - Processing configuration containing:
* - video: Flag for YouTube video processing
* - playlist: Flag for YouTube playlist processing
* - urls: Flag for processing from URL list
* - rss: Flag for RSS feed processing
* - file: Flag for local file processing
*
* @param {string} input - The source to process:
* - For online content: URL of the content
* - For local files: File path on the system
*
* @param {string} filename - Base filename for the output WAV file
* (without extension, will be saved in content/ directory)
*
* @returns {Promise<string>} Path to the processed WAV file
*
* @throws {Error} If:
* - Required dependencies (yt-dlp, ffmpeg) are missing
* - File access fails
* - File type is unsupported
* - Conversion process fails
* - Invalid options are provided
*
* Supported file formats include:
* - Audio: wav, mp3, m4a, aac, ogg, flac
* - Video: mp4, mkv, avi, mov, webm
*
* @example
* // Download from YouTube
* const wavPath = await downloadAudio(
* { video: true },
* 'https://www.youtube.com/watch?v=...',
* 'my-video'
* )
*
* @example
* // Process local file
* const wavPath = await downloadAudio(
* { file: true },
* '/path/to/audio.mp3',
* 'my-audio'
* )
*/
export async function downloadAudio(options: ProcessingOptions, input: string, filename: string): Promise<string> {
export async function downloadAudio(
options: ProcessingOptions,
input: string,
filename: string
): Promise<string> {
// Define output paths using the provided filename
const finalPath = `content/${filename}`
const outputPath = `${finalPath}.wav`

// Handle online content (YouTube, RSS feeds, etc.)
if (options.video || options.playlist || options.urls || options.rss) {
log(step('\nStep 2 - Downloading URL audio...\n'))
try {
// Check for required dependencies
// Verify yt-dlp is available
await checkDependencies(['yt-dlp'])

// Execute yt-dlp to download the audio
// Download and convert audio using yt-dlp
const { stderr } = await execFilePromise('yt-dlp', [
'--no-warnings',
'--restrict-filenames',
'--extract-audio',
'--audio-format', 'wav',
'--postprocessor-args', 'ffmpeg:-ar 16000 -ac 1',
'--no-playlist',
'-o', outputPath,
'--no-warnings', // Suppress warning messages
'--restrict-filenames', // Use safe filenames
'--extract-audio', // Extract audio stream
'--audio-format', 'wav', // Convert to WAV
'--postprocessor-args', 'ffmpeg:-ar 16000 -ac 1', // 16kHz mono
'--no-playlist', // Don't expand playlists
'-o', outputPath, // Output path
input,
])

// Log any errors from yt-dlp
// Log any non-fatal warnings from yt-dlp
if (stderr) {
console.error(`yt-dlp warnings: ${stderr}`)
}

log(success(` Audio downloaded successfully:\n - ${outputPath}`))
} catch (error) {
console.error(`Error downloading audio: ${error instanceof Error ? (error as Error).message : String(error)}`)
console.error(
`Error downloading audio: ${
error instanceof Error ? (error as Error).message : String(error)
}`
)
throw error
}
} else if (options.file) {
}
// Handle local file processing
else if (options.file) {
log(step('\nStep 2 - Processing file audio...\n'))
// Define supported audio and video formats
// Define supported media formats
const supportedFormats: Set<SupportedFileType> = new Set([
'wav', 'mp3', 'm4a', 'aac', 'ogg', 'flac', 'mp4', 'mkv', 'avi', 'mov', 'webm',
// Audio formats
'wav', 'mp3', 'm4a', 'aac', 'ogg', 'flac',
// Video formats
'mp4', 'mkv', 'avi', 'mov', 'webm',
])
try {
// Check if the file exists
// Verify file exists and is accessible
await access(input)

// Read the file into a buffer
// Read file and determine its type
const buffer = await readFile(input)

// Determine the file type
const fileType = await fileTypeFromBuffer(buffer)
// Validate file type is supported
if (!fileType || !supportedFormats.has(fileType.ext as SupportedFileType)) {
throw new Error(
fileType ? `Unsupported file type: ${fileType.ext}` : 'Unable to determine file type'
)
}
log(wait(` File type detected as ${fileType.ext}, converting to WAV...\n`))

// Convert the file to WAV format
// Convert to standardized WAV format using ffmpeg
await execPromise(
`ffmpeg -i "${input}" -ar 16000 -ac 1 -c:a pcm_s16le "${outputPath}"`
)
log(success(` File converted to WAV format successfully:\n - ${outputPath}`))
} catch (error) {
console.error(`Error processing local file: ${error instanceof Error ? (error as Error).message : String(error)}`)
console.error(
`Error processing local file: ${
error instanceof Error ? (error as Error).message : String(error)
}`
)
throw error
}
} else {
}
// Handle invalid options
else {
throw new Error('Invalid option provided for audio download/processing.')
}

return outputPath
}
Loading

0 comments on commit f654642

Please sign in to comment.