mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-08 02:11:35 +00:00
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
202 lines
5.6 KiB
Vue
202 lines
5.6 KiB
Vue
<template>
|
|
<modals-modal v-model="show" name="youtube-download-modal" :width="800" :processing="processing">
|
|
<template #outer>
|
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
|
<p class="text-3xl text-white truncate">Download from YouTube</p>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
|
<!-- YouTube URL Input -->
|
|
<div class="mb-4">
|
|
<ui-text-input-with-label
|
|
v-model="youtubeUrl"
|
|
label="YouTube URL"
|
|
placeholder="https://www.youtube.com/watch?v=... or https://www.youtube.com/playlist?list=..."
|
|
@input="urlChanged"
|
|
/>
|
|
<p v-if="isPlaylist" class="text-xs text-gray-400 mt-1">Playlist detected - all videos will be downloaded</p>
|
|
</div>
|
|
|
|
<!-- Library Selection -->
|
|
<div class="mb-4">
|
|
<ui-dropdown
|
|
v-model="selectedLibraryId"
|
|
:items="libraryItems"
|
|
label="Library"
|
|
@input="libraryChanged"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Folder Selection -->
|
|
<div class="mb-4">
|
|
<ui-dropdown
|
|
v-model="selectedFolderId"
|
|
:items="folderItems"
|
|
label="Folder"
|
|
:disabled="!selectedLibraryId"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Audio Quality Selection -->
|
|
<div class="mb-4">
|
|
<ui-dropdown
|
|
v-model="audioQuality"
|
|
:items="qualityItems"
|
|
label="Audio Quality"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Info text -->
|
|
<div class="mb-4 p-3 bg-info bg-opacity-20 border border-info rounded">
|
|
<p class="text-sm">
|
|
<span class="font-semibold">Note:</span> Audio will be downloaded in MP3 format. Files will be organized as: <span class="font-mono text-xs bg-black bg-opacity-30 px-1 py-0.5 rounded">Uploader/Video Title/</span>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Buttons -->
|
|
<div class="flex justify-end pt-4">
|
|
<ui-btn @click="show = false" class="mr-2">Cancel</ui-btn>
|
|
<ui-btn color="success" :loading="processing" @click="submit">Download</ui-btn>
|
|
</div>
|
|
</div>
|
|
</modals-modal>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
data() {
|
|
return {
|
|
processing: false,
|
|
youtubeUrl: '',
|
|
selectedLibraryId: null,
|
|
selectedFolderId: null,
|
|
audioQuality: 'best'
|
|
}
|
|
},
|
|
computed: {
|
|
show: {
|
|
get() {
|
|
return this.$store.state.globals.showYouTubeDownloadModal
|
|
},
|
|
set(val) {
|
|
this.$store.commit('globals/setShowYouTubeDownloadModal', val)
|
|
}
|
|
},
|
|
libraries() {
|
|
return this.$store.state.libraries.libraries || []
|
|
},
|
|
libraryItems() {
|
|
return this.libraries.map(lib => ({
|
|
value: lib.id,
|
|
text: lib.name
|
|
}))
|
|
},
|
|
selectedLibrary() {
|
|
return this.libraries.find(lib => lib.id === this.selectedLibraryId)
|
|
},
|
|
folderItems() {
|
|
if (!this.selectedLibrary) return []
|
|
return (this.selectedLibrary.folders || []).map(folder => ({
|
|
value: folder.id,
|
|
text: folder.fullPath
|
|
}))
|
|
},
|
|
qualityItems() {
|
|
return [
|
|
{ value: 'best', text: 'Best Quality' },
|
|
{ value: '320', text: '320 kbps' },
|
|
{ value: '256', text: '256 kbps' },
|
|
{ value: '192', text: '192 kbps' },
|
|
{ value: '128', text: '128 kbps' }
|
|
]
|
|
},
|
|
isPlaylist() {
|
|
if (!this.youtubeUrl) return false
|
|
try {
|
|
const url = new URL(this.youtubeUrl)
|
|
return url.searchParams.has('list') || url.pathname.includes('/playlist')
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
urlChanged() {
|
|
// Validate URL format as user types
|
|
// Could add real-time validation here if needed
|
|
},
|
|
libraryChanged() {
|
|
this.selectedFolderId = null
|
|
if (this.selectedLibrary && this.selectedLibrary.folders?.length) {
|
|
this.selectedFolderId = this.selectedLibrary.folders[0].id
|
|
}
|
|
},
|
|
async submit() {
|
|
if (!this.youtubeUrl) {
|
|
this.$toast.error('YouTube URL is required')
|
|
return
|
|
}
|
|
if (!this.selectedLibraryId || !this.selectedFolderId) {
|
|
this.$toast.error('Please select a library and folder')
|
|
return
|
|
}
|
|
|
|
this.processing = true
|
|
|
|
const payload = {
|
|
url: this.youtubeUrl,
|
|
libraryId: this.selectedLibraryId,
|
|
folderId: this.selectedFolderId,
|
|
options: {
|
|
audioQuality: this.audioQuality
|
|
}
|
|
}
|
|
|
|
this.$axios
|
|
.$post('/api/youtube/download', payload)
|
|
.then((response) => {
|
|
if (response.isPlaylist) {
|
|
this.$toast.success(`Queued ${response.count} videos from playlist`)
|
|
} else {
|
|
this.$toast.success('YouTube download started')
|
|
}
|
|
this.show = false
|
|
this.reset()
|
|
})
|
|
.catch((error) => {
|
|
console.error('YouTube download failed', error)
|
|
const errorMsg = error.response?.data || 'YouTube download failed'
|
|
this.$toast.error(errorMsg)
|
|
})
|
|
.finally(() => {
|
|
this.processing = false
|
|
})
|
|
},
|
|
reset() {
|
|
this.youtubeUrl = ''
|
|
this.selectedLibraryId = null
|
|
this.selectedFolderId = null
|
|
this.audioQuality = 'best'
|
|
},
|
|
init() {
|
|
// Set default library from current library
|
|
const currentLibraryId = this.$store.state.libraries.currentLibraryId
|
|
if (currentLibraryId) {
|
|
this.selectedLibraryId = currentLibraryId
|
|
this.libraryChanged()
|
|
}
|
|
}
|
|
},
|
|
watch: {
|
|
show: {
|
|
immediate: true,
|
|
handler(newVal) {
|
|
if (newVal) {
|
|
this.init()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|