├── api ├── __init__.py ├── requirements.txt ├── .gitignore ├── models.py ├── progress.py ├── downloader.py └── main.py ├── app ├── .npmrc ├── src │ ├── routes │ │ ├── +page.js │ │ ├── +layout.svelte │ │ └── +page.svelte │ ├── lib │ │ ├── downloader.js │ │ ├── VideoCardWaiting.svelte │ │ ├── VideoCardError.svelte │ │ ├── UrlField.svelte │ │ ├── VideoCard.svelte │ │ ├── VideoCardInfo.svelte │ │ └── DownloadBar.svelte │ ├── variables.scss │ ├── app.d.ts │ ├── app.scss │ └── app.html ├── static │ ├── logo.png │ └── css │ │ └── main.css ├── .gitignore ├── .eslintignore ├── .prettierrc ├── env.example ├── .prettierignore ├── vite.config.js ├── .eslintrc.cjs ├── svelte.config.js ├── jsconfig.json └── package.json └── README.md /api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /app/src/routes/+page.js: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /api/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | yt_dlp 3 | fastapi_socketio 4 | uvicorn[standard] -------------------------------------------------------------------------------- /app/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebarocks/videodlpro/HEAD/app/static/logo.png -------------------------------------------------------------------------------- /app/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | /downloads 5 | /dist -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | -------------------------------------------------------------------------------- /app/src/lib/downloader.js: -------------------------------------------------------------------------------- 1 | import io from "socket.io-client"; 2 | import { env } from '$env/dynamic/public'; 3 | 4 | 5 | const socket = io(`${env.PUBLIC_HOSTNAME}`, { path: "/api/ws/" }); 6 | 7 | export default socket; 8 | 9 | -------------------------------------------------------------------------------- /app/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "pluginSearchDirs": ["."], 7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 8 | } 9 | -------------------------------------------------------------------------------- /app/env.example: -------------------------------------------------------------------------------- 1 | # dot env file for development enviroment 2 | 3 | # hostname used by socket io using ws:// 4 | PUBLIC_HOSTNAME=localhost:8000 5 | 6 | # api url used to download files and call fetch 7 | PUBLIC_API_URL=http://localhost:8000 -------------------------------------------------------------------------------- /app/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /app/src/variables.scss: -------------------------------------------------------------------------------- 1 | /* Variables and mixins declared here will be available in all other SCSS files */ /* https://github.com/jgthms/bulma/issues/1293 */ 2 | $body-overflow-y: auto; 3 | $family-sans-serif : "Fira Sans"; 4 | $card-content-padding: 1rem; 5 | -------------------------------------------------------------------------------- /app/src/lib/VideoCardWaiting.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | Obteniendo informacion 4 | Obteniendo informacion 5 |
6 |
-------------------------------------------------------------------------------- /app/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | // and what to do when importing types 4 | declare namespace App { 5 | // interface Locals {} 6 | // interface PageData {} 7 | // interface Platform {} 8 | } 9 | -------------------------------------------------------------------------------- /app/vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | 3 | /** @type {import('vite').UserConfig} */ 4 | const config = { 5 | plugins: [sveltekit()], 6 | 7 | css: { 8 | preprocessorOptions: { 9 | scss: { 10 | additionalData: '@use "src/variables.scss" as *;' 11 | } 12 | } 13 | } 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /app/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['eslint:recommended', 'prettier'], 4 | plugins: ['svelte3'], 5 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 6 | parserOptions: { 7 | sourceType: 'module', 8 | ecmaVersion: 2020 9 | }, 10 | env: { 11 | browser: true, 12 | es2017: true, 13 | node: true 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /app/static/css/main.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | body { 4 | margin: 0; 5 | display: flex; 6 | place-items: top; 7 | min-width: 320px; 8 | min-height: 100vh; 9 | } 10 | 11 | h1 { 12 | font-size: 3.2em; 13 | line-height: 1; 14 | } 15 | 16 | 17 | #app { 18 | max-width: 1280px; 19 | margin: 0 auto; 20 | padding: 2rem; 21 | text-align: center; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /app/src/lib/VideoCardError.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 |
14 | 15 | Ocurrio un error! 16 |

{error}

17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /app/src/app.scss: -------------------------------------------------------------------------------- 1 | /* Write your global styles here, in SCSS syntax. Variables and mixins from the src/variables.scss file are available here without importing */ /* Import only what you need from Bulma */ 2 | @import 'bulma/sass/utilities/_all'; 3 | @import 'bulma/sass/base/_all'; 4 | @import 'bulma/sass/elements/_all'; 5 | @import 'bulma/sass/form/_all'; 6 | @import 'bulma/sass/components/_all'; 7 | @import 'bulma/sass/grid/_all'; 8 | @import 'bulma/sass/helpers/_all'; 9 | @import 'bulma/sass/layout/_all'; 10 | 11 | @import url('https://code.cdn.mozilla.net/fonts/fira.css'); 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/svelte.config.js: -------------------------------------------------------------------------------- 1 | import preprocess from 'svelte-preprocess'; 2 | import adapter from '@sveltejs/adapter-static'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | kit: { 7 | adapter: adapter({ 8 | // default options are shown. On some platforms 9 | // these options are set automatically — see below 10 | pages: 'build', 11 | assets: 'build', 12 | fallback: null, 13 | precompress: false 14 | }) 15 | }, 16 | preprocess: [ 17 | preprocess({ 18 | scss: { 19 | prependData: '@use "src/variables.scss" as *;' 20 | } 21 | }) 22 | ] 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /app/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Video-DL Pro 7 | 8 | 9 | 10 | 16 | %sveltekit.head% 17 | 18 | 19 | %sveltekit.body% 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": false, 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /api/models.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from pydantic import BaseModel 3 | 4 | class VideoInfo(BaseModel): 5 | title: str 6 | thumbUrl: str 7 | url: str 8 | site: str 9 | 10 | class VideoUrl(BaseModel): 11 | url: str 12 | 13 | class ProgressInfo(BaseModel): 14 | download_id : int 15 | status : str 16 | percentage : float 17 | filename : str 18 | 19 | class FormatInfo(BaseModel): 20 | format_id: str 21 | ext: str 22 | resolution: str 23 | fps: Optional[str] 24 | filesize: Optional[int] 25 | tbr: Optional[float] 26 | vcodec: str 27 | acodec: str 28 | format_note: str 29 | 30 | class VideoProgress(BaseModel): 31 | url : str 32 | status : str 33 | percentage : float 34 | filename : str -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | Videodlpro is a webapp for downloading videos using yt-dlp module. It allows downloading videos and converting to mp3 (using FFMpeg). Built using FastAPI backend and Svelte frontend, it uses WebSockets to show download status in real time. 4 | 5 | # Installation 6 | 7 | ## Requirements: 8 | 9 | * Python (pip) 10 | * Node.js (npm) 11 | * FFMpeg 12 | 13 | ## Setting up .env 14 | 15 | First, copy .env file 16 | 17 | `cd app` 18 | 19 | `cp .env.example .env ` 20 | 21 | Then feel free to edit it according to your enviroment 22 | 23 | 24 | ## Python: 25 | 26 | `cd api` 27 | 28 | `pip install -r requirements.txt` 29 | 30 | `uvicorn main:app` 31 | 32 | It will run on port 8000 by default 33 | 34 | ## Node.js (npm) 35 | 36 | `cd app` 37 | 38 | `npm install` 39 | 40 | `npm run prod` 41 | 42 | It will run on port 3000 by default -------------------------------------------------------------------------------- /app/src/lib/UrlField.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 |
20 | 21 | 22 |
23 | 24 | 25 | 41 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videodlpro", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch", 11 | "lint": "prettier --check . && eslint .", 12 | "format": "prettier --write ." 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "next", 16 | "@sveltejs/adapter-static": "^1.0.0-next.41", 17 | "@sveltejs/kit": "next", 18 | "bulma": "^0.9.4", 19 | "eslint": "^8.16.0", 20 | "eslint-config-prettier": "^8.3.0", 21 | "eslint-plugin-svelte3": "^4.0.0", 22 | "prettier": "^2.6.2", 23 | "prettier-plugin-svelte": "^2.7.0", 24 | "sass": "^1.53.0", 25 | "svelte": "^3.44.0", 26 | "svelte-check": "^2.7.1", 27 | "svelte-preprocess": "^4.10.7", 28 | "typescript": "^4.7.4", 29 | "vite": "^3.1.0-beta.1" 30 | }, 31 | "type": "module", 32 | "dependencies": { 33 | "bulma": "^0.9.4", 34 | "sass": "^1.54.8", 35 | "socket.io-client": "^4.5.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/lib/VideoCard.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 | {#await fetchInfo} 28 | 29 | {:then data} 30 | {#if !data.detail} 31 | 32 | {/if} 33 | {:catch error} 34 | 35 | {/await} 36 |
37 | 38 | -------------------------------------------------------------------------------- /app/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 |
27 | 28 | 29 |

Video-DL Pro

30 | 31 |
32 | 33 |
34 | 35 |
36 | {#each videos as vid (vid.id)} 37 | 38 | {/each} 39 |
40 | 41 |
42 | 43 | 70 | -------------------------------------------------------------------------------- /api/progress.py: -------------------------------------------------------------------------------- 1 | from models import VideoProgress 2 | 3 | 4 | class ProgressTracker(): 5 | 6 | def __init__(self): 7 | self.videos: dict[int, VideoProgress] = {} 8 | 9 | def attachDownload(self, url, download_id): 10 | self.videos[download_id] = VideoProgress( 11 | url=url, 12 | status='created', 13 | percentage=0, 14 | filename='' 15 | ) 16 | 17 | def setStatus(self, download_id, status): 18 | self.videos[download_id].status = status 19 | 20 | def getStatus(self, download_id): 21 | vidProg = self.videos.get(download_id, None) 22 | if vidProg: 23 | return vidProg.status 24 | else: 25 | return 'unknown' 26 | 27 | def getPercentage(self, download_id): 28 | vidProg = self.videos.get(download_id, None) 29 | if vidProg: 30 | return vidProg.percentage 31 | else: 32 | return 0 33 | 34 | def getFilename(self, download_id): 35 | vidProg = self.videos.get(download_id, None) 36 | if vidProg: 37 | return vidProg.filename 38 | else: 39 | return '-' 40 | 41 | def loadHookInfo(self, download_id, hook_info): 42 | vidProg = self.videos.get(download_id, None) 43 | if vidProg: 44 | vidProg.status = hook_info['status'] 45 | total = float(hook_info['total_bytes']) 46 | current = float(hook_info['downloaded_bytes']) 47 | vidProg.percentage = 100 * current / total if total != 0 else 0 48 | vidProg.filename = hook_info['filename'] 49 | -------------------------------------------------------------------------------- /app/src/lib/VideoCardInfo.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 |

18 | {data.site} 19 | {data.title} 20 |

21 | 26 | 31 |
32 | 33 | {#if cardOpen} 34 |
35 |
36 | {data.site} video - {data.title} 37 |
38 |
39 | {data.url} 40 |

{data.title}

41 |
42 |
43 | {/if} 44 | 45 |
46 | 47 |
48 | 49 | -------------------------------------------------------------------------------- /app/src/lib/DownloadBar.svelte: -------------------------------------------------------------------------------- 1 | 58 | 59 |
60 | {#if status == 'ready'} 61 |
62 | 63 | 64 |
65 | 66 | {:else if status == 'downloading'} 67 | 68 | {progressData.status} 69 | {progressData.filename} 70 | {Math.round(progressData.percentage, 2)}% 71 | 72 | 73 | 74 | {:else if status == 'complete'} 75 | {status} {download_filename} 76 | 78 | 81 | 82 | {/if} 83 |
84 | 85 | 95 | -------------------------------------------------------------------------------- /api/downloader.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from yt_dlp import YoutubeDL 3 | from models import FormatInfo 4 | from yt_dlp.postprocessor.ffmpeg import FFmpegExtractAudioPP 5 | 6 | # dict_keys(['status', 'downloaded_bytes', 'total_bytes', 'tmpfilename', 'filename', 'eta', 'speed', 'elapsed', 'ctx_id', 'info_dict', '_eta_str', 7 | # '_speed_str', '_percent_str', '_total_bytes_str', '_total_bytes_estimate_str', '_downloaded_bytes_str', '_elapsed_str', '_default_template']) 8 | 9 | def processHookInfo(d): 10 | info={ 11 | "status": d.get('status'), 12 | "downloaded_bytes": d.get('downloaded_bytes',0), 13 | "total_bytes": d.get('total_bytes',1), 14 | "filename": d.get('filename','.\\').split("\\")[-1], 15 | } 16 | return info 17 | 18 | 19 | ydl_opts = { 20 | 'outtmpl': './downloads/%(id)s.%(extractor)s.%(ext)s', 21 | 'quiet': True, 22 | 'noplaylist': True 23 | } 24 | 25 | 26 | class Downloader(YoutubeDL): 27 | 28 | def __init__(self): 29 | super().__init__(ydl_opts) 30 | 31 | def tryInfo(self, url): 32 | try: 33 | info = self.extract_info(url, download=False) 34 | except: 35 | return None 36 | return info 37 | 38 | def getInfo(self, url): 39 | return self.tryInfo(url) 40 | 41 | def getInfoSanitized(self, url): 42 | info = self.tryInfo(url) 43 | return self.sanitize_info(info) 44 | 45 | def getFilename(self, url): 46 | info = self.tryInfo(url) 47 | if info is None: 48 | return "" 49 | return f"{info['id']}.{info['extractor']}.{info['ext']}" 50 | 51 | def getFilenameMp3(self, url): 52 | info = self.tryInfo(url) 53 | if info is None: 54 | return "" 55 | return f"{info['id']}.{info['extractor']}.mp3" 56 | 57 | def tryDownload(self, url): 58 | try: 59 | return self.download([url]) 60 | except: 61 | return -1 62 | 63 | def getFormats(self,url): 64 | info = self.getInfo(url) 65 | formats = [] 66 | if info is not None: 67 | for fmt in info['formats']: 68 | fi = FormatInfo.parse_obj(fmt) 69 | formats.append(fi) 70 | return formats 71 | else: 72 | return None 73 | 74 | def pickFormat(self,format_id): 75 | self.format_selector = self.build_format_selector(format_id) 76 | 77 | def mp3Mode(self): 78 | self.add_post_processor(FFmpegExtractAudioPP(preferredcodec='mp3')) 79 | 80 | 81 | if __name__ == "__main__": 82 | yee = 'https://www.youtube.com/watch?v=q6EoRBvdVPQ' 83 | ydl = Downloader() 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /api/main.py: -------------------------------------------------------------------------------- 1 | 2 | import asyncio 3 | 4 | from fastapi import FastAPI, HTTPException 5 | from fastapi.middleware.cors import CORSMiddleware 6 | from fastapi.staticfiles import StaticFiles 7 | from fastapi.responses import FileResponse 8 | 9 | from fastapi_socketio import SocketManager 10 | 11 | from progress import ProgressTracker 12 | from downloader import Downloader, processHookInfo 13 | from models import VideoInfo, VideoUrl, FormatInfo 14 | 15 | api = FastAPI() 16 | 17 | api.add_middleware( 18 | CORSMiddleware, 19 | allow_origins="*", 20 | allow_credentials=True, 21 | allow_methods=["*"], 22 | allow_headers=["*"], 23 | ) 24 | 25 | api.mount("/files", StaticFiles(directory="downloads"), name="downloads") 26 | 27 | 28 | sm = SocketManager(app=api, cors_allowed_origins=[], socketio_path='') 29 | pt = ProgressTracker() 30 | 31 | @api.post("/download") 32 | async def downloadUrl(vidUrl: VideoUrl): 33 | ydl = Downloader() 34 | filename = ydl.getFilename(vidUrl.url) 35 | res = ydl.download([vidUrl.url]) 36 | print(res) 37 | return filename 38 | 39 | @api.post("/mp3") 40 | async def downloadMp3(vidUrl: VideoUrl): 41 | ydl = Downloader() 42 | filename = ydl.getFilename(vidUrl.url) 43 | ydl.mp3Mode() 44 | res = ydl.download([vidUrl.url]) 45 | print(res) 46 | return filename 47 | 48 | @api.post("/formats") 49 | async def formatsUrl(vidUrl: VideoUrl): 50 | url = vidUrl.url 51 | ydl = Downloader() 52 | info = ydl.getFormats(url) 53 | if info is None: 54 | raise HTTPException(status_code=404, detail="URL not supported") 55 | else: 56 | return info 57 | 58 | @api.post("/info") 59 | async def infoUrl(vidUrl: VideoUrl): 60 | url = vidUrl.url 61 | ydl = Downloader() 62 | info = ydl.getInfo(url) 63 | if info is None: 64 | raise HTTPException(status_code=404, detail="URL not supported") 65 | else: 66 | return VideoInfo( 67 | title=info['title'], 68 | thumbUrl=info['thumbnail'], 69 | site=info['webpage_url_domain'], 70 | url=info['webpage_url'] 71 | ) 72 | 73 | 74 | @api.sio.on('queryprogress') 75 | async def handle_join(sid, data): 76 | #print(f"query: {data}") 77 | dl_id = int(data) 78 | res = { 79 | "status": pt.getStatus(dl_id), 80 | "percentage": pt.getPercentage(dl_id), 81 | "filename": pt.getFilename(dl_id) 82 | } 83 | await sm.emit(f"progress.{dl_id}", res) 84 | #print(f"emit progress.{dl_id}") 85 | 86 | @api.sio.on('download') 87 | async def handle_join(sid, data): 88 | 89 | print(f"Download requested: {data}") 90 | 91 | dl_id = int(data['download_id']) 92 | url = data['url'] 93 | 94 | pt.attachDownload(url, dl_id) 95 | 96 | def temphook(d): 97 | pt.loadHookInfo(dl_id,processHookInfo(d)) 98 | 99 | ydl = Downloader() 100 | 101 | if data.get('mp3'): 102 | ydl.mp3Mode() 103 | ext='mp3' 104 | else: 105 | ext = None 106 | 107 | ydl.add_progress_hook(temphook) 108 | 109 | loop = asyncio.get_event_loop() 110 | res = await loop.run_in_executor(None, ydl.extract_info, url) 111 | 112 | ext = res['ext'] if ext is None else ext 113 | filename = f"{res['id']}.{res['extractor']}.{ext}" 114 | 115 | print(filename) 116 | await sm.emit(f"finished.{dl_id}", filename) 117 | #print(f"emit finished.{dl_id}") 118 | 119 | 120 | app = FastAPI() 121 | app.mount("/api",api) 122 | 123 | # Correr build con /index.html 124 | #app.mount("/", StaticFiles(directory="../app/build"), name="site") 125 | 126 | --------------------------------------------------------------------------------