metube/app/ytdl.py

235 lines
8.8 KiB
Python
Raw Normal View History

2019-11-29 19:31:34 +02:00
import os
import yt_dlp
2019-11-29 19:31:34 +02:00
from collections import OrderedDict
import asyncio
import multiprocessing
import logging
2021-10-28 11:19:17 +01:00
from dl_formats import get_format, get_opts
2019-11-29 19:31:34 +02:00
log = logging.getLogger('ytdl')
2019-12-07 21:49:31 +02:00
class DownloadQueueNotifier:
async def added(self, dl):
raise NotImplementedError
async def updated(self, dl):
raise NotImplementedError
async def completed(self, dl):
raise NotImplementedError
async def canceled(self, id):
raise NotImplementedError
async def cleared(self, id):
raise NotImplementedError
2019-11-29 19:31:34 +02:00
class DownloadInfo:
2021-09-13 20:25:32 +03:00
def __init__(self, id, title, url, quality, format):
2019-11-29 19:31:34 +02:00
self.id, self.title, self.url = id, title, url
2021-07-29 11:12:40 +03:00
self.quality = quality
2021-09-13 20:25:32 +03:00
self.format = format
2019-12-06 16:30:07 +02:00
self.status = self.msg = self.percent = self.speed = self.eta = None
2019-11-29 19:31:34 +02:00
class Download:
manager = None
2021-09-13 20:25:32 +03:00
def __init__(self, download_dir, output_template, quality, format, ytdl_opts, info):
2019-11-29 19:31:34 +02:00
self.download_dir = download_dir
self.output_template = output_template
2021-10-28 11:19:17 +01:00
self.format = get_format(format, quality)
self.ytdl_opts = get_opts(format, quality, ytdl_opts)
2019-11-29 19:31:34 +02:00
self.info = info
2019-12-07 21:49:31 +02:00
self.canceled = False
2019-11-29 19:31:34 +02:00
self.tmpfilename = None
self.status_queue = None
self.proc = None
self.loop = None
2019-12-07 21:49:31 +02:00
self.notifier = None
2021-07-29 11:12:40 +03:00
2019-11-29 19:31:34 +02:00
def _download(self):
2019-12-06 16:30:07 +02:00
try:
def put_status(st):
self.status_queue.put({k: v for k, v in st.items() if k in (
'tmpfilename',
'status',
'msg',
'total_bytes',
'total_bytes_estimate',
'downloaded_bytes',
'speed',
'eta',
)})
ret = yt_dlp.YoutubeDL(params={
2019-12-06 16:30:07 +02:00
'quiet': True,
'no_color': True,
#'skip_download': True,
'outtmpl': os.path.join(self.download_dir, self.output_template),
2019-12-13 22:43:58 +02:00
'format': self.format,
2019-12-06 16:30:07 +02:00
'cachedir': False,
'socket_timeout': 30,
'progress_hooks': [put_status],
**self.ytdl_opts,
2019-12-06 16:30:07 +02:00
}).download([self.info.url])
self.status_queue.put({'status': 'finished' if ret == 0 else 'error'})
except yt_dlp.utils.YoutubeDLError as exc:
2019-12-06 16:30:07 +02:00
self.status_queue.put({'status': 'error', 'msg': str(exc)})
2021-07-29 11:12:40 +03:00
2019-12-07 21:49:31 +02:00
async def start(self, notifier):
2019-11-29 19:31:34 +02:00
if Download.manager is None:
Download.manager = multiprocessing.Manager()
self.status_queue = Download.manager.Queue()
self.proc = multiprocessing.Process(target=self._download)
self.proc.start()
self.loop = asyncio.get_running_loop()
2019-12-07 21:49:31 +02:00
self.notifier = notifier
self.info.status = 'preparing'
await self.notifier.updated(self.info)
2021-11-13 20:07:14 +02:00
asyncio.create_task(self.update_status())
2021-07-29 11:12:40 +03:00
return await self.loop.run_in_executor(None, self.proc.join)
2019-11-29 19:31:34 +02:00
def cancel(self):
if self.running():
self.proc.kill()
2019-12-07 21:49:31 +02:00
self.canceled = True
2019-11-29 19:31:34 +02:00
def close(self):
2019-12-07 21:49:31 +02:00
if self.started():
2019-11-29 19:31:34 +02:00
self.proc.close()
self.status_queue.put(None)
def running(self):
2019-12-06 16:30:07 +02:00
try:
return self.proc is not None and self.proc.is_alive()
except ValueError:
return False
2019-11-29 19:31:34 +02:00
2019-12-07 21:49:31 +02:00
def started(self):
return self.proc is not None
async def update_status(self):
while True:
2019-11-29 19:31:34 +02:00
status = await self.loop.run_in_executor(None, self.status_queue.get)
if status is None:
return
self.tmpfilename = status.get('tmpfilename')
self.info.status = status['status']
2019-12-06 16:30:07 +02:00
self.info.msg = status.get('msg')
2019-11-29 19:31:34 +02:00
if 'downloaded_bytes' in status:
total = status.get('total_bytes') or status.get('total_bytes_estimate')
if total:
self.info.percent = status['downloaded_bytes'] / total * 100
self.info.speed = status.get('speed')
self.info.eta = status.get('eta')
2019-12-07 21:49:31 +02:00
await self.notifier.updated(self.info)
2019-11-29 19:31:34 +02:00
class DownloadQueue:
def __init__(self, config, notifier):
self.config = config
self.notifier = notifier
self.queue = OrderedDict()
2019-12-03 22:32:07 +02:00
self.done = OrderedDict()
2021-11-13 20:07:14 +02:00
self.initialized = False
def __initialize(self):
if not self.initialized:
self.initialized = True
self.event = asyncio.Event()
asyncio.create_task(self.__download())
2019-11-29 19:31:34 +02:00
def __extract_info(self, url):
return yt_dlp.YoutubeDL(params={
2019-11-29 19:31:34 +02:00
'quiet': True,
'no_color': True,
'extract_flat': True,
**self.config.YTDL_OPTIONS,
2019-11-29 19:31:34 +02:00
}).extract_info(url, download=False)
2021-09-13 20:25:32 +03:00
async def __add_entry(self, entry, quality, format, already):
etype = entry.get('_type') or 'video'
2019-12-06 16:30:07 +02:00
if etype == 'playlist':
entries = entry['entries']
2019-11-29 19:31:34 +02:00
log.info(f'playlist detected with {len(entries)} entries')
results = []
for etr in entries:
2021-09-13 20:25:32 +03:00
results.append(await self.__add_entry(etr, quality, format, already))
if any(res['status'] == 'error' for res in results):
return {'status': 'error', 'msg': ', '.join(res['msg'] for res in results if res['status'] == 'error' and 'msg' in res)}
return {'status': 'ok'}
2021-09-15 15:51:18 +03:00
elif etype == 'video' or etype.startswith('url') and 'id' in entry and 'title' in entry:
2019-11-29 19:31:34 +02:00
if entry['id'] not in self.queue:
2021-09-13 20:25:32 +03:00
dl = DownloadInfo(entry['id'], entry['title'], entry.get('webpage_url') or entry['url'], quality, format)
2021-11-20 10:12:08 +02:00
dldirectory = self.config.DOWNLOAD_DIR if (quality != 'audio' and format != 'mp3') else self.config.AUDIO_DOWNLOAD_DIR
2021-09-13 20:25:32 +03:00
self.queue[entry['id']] = Download(dldirectory, self.config.OUTPUT_TEMPLATE, quality, format, self.config.YTDL_OPTIONS, dl)
self.event.set()
2019-11-29 19:31:34 +02:00
await self.notifier.added(dl)
return {'status': 'ok'}
elif etype == 'url':
2021-09-13 20:25:32 +03:00
return await self.add(entry['url'], quality, format, already)
return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'}
2021-09-13 20:25:32 +03:00
async def add(self, url, quality, format, already=None):
log.info(f'adding {url}')
2021-11-13 20:07:14 +02:00
self.__initialize()
already = set() if already is None else already
if url in already:
log.info('recursion detected, skipping')
return {'status': 'ok'}
else:
already.add(url)
try:
entry = await asyncio.get_running_loop().run_in_executor(None, self.__extract_info, url)
except yt_dlp.utils.YoutubeDLError as exc:
return {'status': 'error', 'msg': str(exc)}
2021-09-13 20:25:32 +03:00
return await self.__add_entry(entry, quality, format, already)
2021-07-29 11:12:40 +03:00
2019-12-03 22:32:07 +02:00
async def cancel(self, ids):
2019-11-29 19:31:34 +02:00
for id in ids:
if id not in self.queue:
2019-12-03 22:32:07 +02:00
log.warn(f'requested cancel for non-existent download {id}')
continue
2019-12-07 21:49:31 +02:00
if self.queue[id].started():
self.queue[id].cancel()
else:
del self.queue[id]
await self.notifier.canceled(id)
2019-12-03 22:32:07 +02:00
return {'status': 'ok'}
async def clear(self, ids):
for id in ids:
if id not in self.done:
2019-11-29 19:31:34 +02:00
log.warn(f'requested delete for non-existent download {id}')
continue
2019-12-03 22:32:07 +02:00
del self.done[id]
await self.notifier.cleared(id)
2019-11-29 19:31:34 +02:00
return {'status': 'ok'}
def get(self):
2019-12-03 22:32:07 +02:00
return(list((k, v.info) for k, v in self.queue.items()),
list((k, v.info) for k, v in self.done.items()))
2021-07-29 11:12:40 +03:00
2019-11-29 19:31:34 +02:00
async def __download(self):
while True:
while not self.queue:
log.info('waiting for item to download')
await self.event.wait()
self.event.clear()
id, entry = next(iter(self.queue.items()))
log.info(f'downloading {entry.info.title}')
2019-12-07 21:49:31 +02:00
await entry.start(self.notifier)
2019-12-03 22:32:07 +02:00
if entry.info.status != 'finished':
if entry.tmpfilename and os.path.isfile(entry.tmpfilename):
try:
os.remove(entry.tmpfilename)
except:
pass
entry.info.status = 'error'
2019-11-29 19:31:34 +02:00
entry.close()
2019-12-03 22:32:07 +02:00
if id in self.queue:
del self.queue[id]
2019-12-07 21:49:31 +02:00
if entry.canceled:
await self.notifier.canceled(id)
else:
self.done[id] = entry
await self.notifier.completed(entry.info)