mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-08 10:21:38 +00:00
Add YouTube download feature with yt-dlp integration
This commit implements a comprehensive YouTube download feature that allows administrators to download audio from YouTube videos and playlists. Backend Changes: - Add yt-dlp-wrap npm dependency for YouTube downloading - Create YouTubeDownloadManager to handle download queue and execution - Create YouTubeDownloadController with API endpoints for download/queue/cancel - Add YouTube utility functions for URL validation and metadata extraction - Create YouTubeDownload object model for tracking download state - Add API routes: POST /api/youtube/download, GET /api/youtube/queue, DELETE /api/youtube/download/:id - Implement playlist support - automatically queue all videos from playlist URLs - Download audio in MP3 format with best quality - Organize files in subfolder structure: <Uploader>/<Video Title>/ - Extract metadata and create library items automatically - Real-time progress updates via Socket.io Frontend Changes: - Create YouTubeDownloadModal component for user-friendly download interface - Add download button to Appbar header (admin-only, visible with current library) - Update Vuex store with modal state management - Register modal globally in default layout - Implement Socket.io listeners for download events (started, progress, completed, failed, queued) - Show toast notifications for download status updates Features: - Admin-only access control - Support for single videos and playlists - Audio quality selection (best, 320kbps, 256kbps, 192kbps, 128kbps) - Library and folder selection - Download queue management - Real-time progress tracking - Automatic thumbnail download as cover image - Automatic library item creation with metadata - Error handling and user feedback Technical Details: - Uses yt-dlp for reliable YouTube downloading - Integrates with existing task management system - Respects library folder permissions - Follows existing code patterns (similar to podcast downloads) - Socket.io events for real-time updates
This commit is contained in:
parent
122fc34a75
commit
aaf87821a3
11 changed files with 1253 additions and 2 deletions
144
server/utils/youtubeUtils.js
Normal file
144
server/utils/youtubeUtils.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
const Logger = require('../Logger')
|
||||
|
||||
/**
|
||||
* Validate if a URL is a valid YouTube URL
|
||||
* Supports: youtube.com, youtu.be, music.youtube.com
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidYouTubeUrl(url) {
|
||||
if (!url || typeof url !== 'string') return false
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
const hostname = urlObj.hostname.toLowerCase()
|
||||
|
||||
// Support various YouTube domains
|
||||
const validHosts = [
|
||||
'www.youtube.com',
|
||||
'youtube.com',
|
||||
'youtu.be',
|
||||
'music.youtube.com',
|
||||
'm.youtube.com'
|
||||
]
|
||||
|
||||
if (!validHosts.includes(hostname)) return false
|
||||
|
||||
// Check for video ID or playlist ID
|
||||
if (hostname === 'youtu.be') {
|
||||
return urlObj.pathname.length > 1
|
||||
}
|
||||
|
||||
return urlObj.searchParams.has('v') || urlObj.searchParams.has('list') || urlObj.pathname.includes('/watch')
|
||||
} catch (error) {
|
||||
Logger.error('[youtubeUtils] Invalid URL:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract video ID from YouTube URL
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function extractVideoId(url) {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
const hostname = urlObj.hostname.toLowerCase()
|
||||
|
||||
if (hostname === 'youtu.be') {
|
||||
return urlObj.pathname.slice(1).split('?')[0]
|
||||
}
|
||||
|
||||
return urlObj.searchParams.get('v')
|
||||
} catch (error) {
|
||||
Logger.error('[youtubeUtils] Failed to extract video ID:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if URL is a playlist
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isPlaylistUrl(url) {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
return urlObj.searchParams.has('list') || urlObj.pathname.includes('/playlist')
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract playlist ID from YouTube URL
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function extractPlaylistId(url) {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
return urlObj.searchParams.get('list')
|
||||
} catch (error) {
|
||||
Logger.error('[youtubeUtils] Failed to extract playlist ID:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize title for filename
|
||||
* Remove invalid characters and limit length
|
||||
*
|
||||
* @param {string} title
|
||||
* @returns {string}
|
||||
*/
|
||||
function sanitizeTitleForFilename(title) {
|
||||
if (!title) return 'untitled'
|
||||
|
||||
// Remove invalid filename characters
|
||||
let sanitized = title.replace(/[<>:"/\\|?*\x00-\x1f]/g, '')
|
||||
|
||||
// Replace multiple spaces with single space
|
||||
sanitized = sanitized.replace(/\s+/g, ' ').trim()
|
||||
|
||||
// Limit length to 200 characters
|
||||
if (sanitized.length > 200) {
|
||||
sanitized = sanitized.substring(0, 200).trim()
|
||||
}
|
||||
|
||||
return sanitized || 'untitled'
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration from seconds to readable format
|
||||
*
|
||||
* @param {number} seconds
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatDuration(seconds) {
|
||||
if (!seconds || isNaN(seconds)) return '0:00'
|
||||
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const secs = Math.floor(seconds % 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValidYouTubeUrl,
|
||||
extractVideoId,
|
||||
isPlaylistUrl,
|
||||
extractPlaylistId,
|
||||
sanitizeTitleForFilename,
|
||||
formatDuration
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue