From ccff77647c581b3ca924ea8bf265a5ae3d4ce974 Mon Sep 17 00:00:00 2001 From: breakid Date: Tue, 9 May 2023 16:05:38 -0400 Subject: [PATCH 1/5] Added support for yt-dlp 'temp' path - Added support for yt-dlp emp path - Formatted with Black - Updated README to reflect new TEMP_DIR setting; linted - Modified Dockerfile to strip carriage return characters from docker-entrypoint.sh script to fix building the image on Windows - Added example docker-compose.yml config --- Dockerfile | 12 +- README.md | 16 ++- app/main.py | 6 +- app/ytdl.py | 263 ++++++++++++++++++++++++++----------------- docker-compose.yml | 30 +++++ docker-entrypoint.sh | 6 +- 6 files changed, 220 insertions(+), 113 deletions(-) create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile index 28bc986..5c9353c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,10 +10,10 @@ FROM python:3.8-alpine WORKDIR /app -COPY Pipfile* docker-entrypoint.sh ./ +COPY Pipfile* . -RUN chmod +x docker-entrypoint.sh && \ - apk add --update ffmpeg aria2 coreutils shadow su-exec && \ +# Install dependencies +RUN apk add --update ffmpeg aria2 coreutils shadow su-exec && \ apk add --update --virtual .build-deps gcc g++ musl-dev && \ pip install --no-cache-dir pipenv && \ pipenv install --system --deploy --clear && \ @@ -23,6 +23,11 @@ RUN chmod +x docker-entrypoint.sh && \ mkdir /.cache && chmod 777 /.cache COPY favicon ./favicon +COPY docker-entrypoint.sh . + +# Use sed to strip carriage-return characters from the entrypoint script (in case building on Windows) +RUN sed -i 's/\r$//g' docker-entrypoint.sh && chmod +x docker-entrypoint.sh + COPY app ./app COPY --from=builder /metube/dist/metube ./ui/dist/metube @@ -32,6 +37,7 @@ ENV UMASK=022 ENV DOWNLOAD_DIR /downloads ENV STATE_DIR /downloads/.metube +ENV TEMP_DIR /tmp VOLUME /downloads EXPOSE 8081 CMD [ "./docker-entrypoint.sh" ] diff --git a/README.md b/README.md index 1b70cf5..f6d10bc 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![Build Status](https://github.com/alexta69/metube/actions/workflows/main.yml/badge.svg) ![Docker Pulls](https://img.shields.io/docker/pulls/alexta69/metube.svg) -Web GUI for youtube-dl (using the [yt-dlp](https://github.com/yt-dlp/yt-dlp) fork) with playlist support. Allows you to download videos from YouTube and dozens of other sites (https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md). +Web GUI for youtube-dl (using the [yt-dlp](https://github.com/yt-dlp/yt-dlp) fork) with playlist support. Allows you to download videos from YouTube and dozens of other sites (). ![screenshot1](https://github.com/alexta69/metube/raw/master/screenshot.gif) @@ -12,6 +12,7 @@ Web GUI for youtube-dl (using the [yt-dlp](https://github.com/yt-dlp/yt-dlp) for ```bash docker run -d -p 8081:8081 -v /path/to/downloads:/downloads ghcr.io/alexta69/metube ``` + ## Run using docker-compose ```yaml @@ -40,6 +41,9 @@ Certain values can be set via environment variables, using the `-e` parameter on * __CUSTOM_DIRS__: whether to enable downloading videos into custom directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__). When enabled, a drop-down appears next to the Add button to specify the download directory. Defaults to `true`. * __CREATE_CUSTOM_DIRS__: whether to support automatically creating directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__) if they do not exist. When enabled, the download directory selector becomes supports free-text input, and the specified directory will be created recursively. Defaults to `true`. * __STATE_DIR__: path to where the queue persistence files will be saved. Defaults to `/downloads/.metube` in the docker image, and `.` otherwise. +* __TEMP_DIR__: path where intermediary download files will be saved. Defaults to `/downloads/.metube` in the docker image, and `.` otherwise. + * Set this to an SSD or RAM filesystem (e.g., `tmpfs`) for better performance + * __Note__: Using a RAM filesystem may prevent downloads from being resumed * __DELETE_FILE_ON_TRASHCAN__: if `true`, downloaded files are deleted on the server, when they are trashed from the "Completed" section of the UI. Defaults to `false`. * __URL_PREFIX__: base path for the web server (for use when hosting behind a reverse proxy). Defaults to `/`. * __OUTPUT_TEMPLATE__: the template for the filenames of the downloaded videos, formatted according to [this spec](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#output-template). Defaults to `%(title)s.%(ext)s`. @@ -47,11 +51,13 @@ Certain values can be set via environment variables, using the `-e` parameter on * __YTDL_OPTIONS__: Additional options to pass to youtube-dl, in JSON format. [See available options here](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L183). They roughly correspond to command-line options, though some do not have exact equivalents here, for example `--recode-video` has to be specified via `postprocessors`. Also note that dashes are replaced with underscores. The following example value for `YTDL_OPTIONS` embeds English subtitles and chapter markers (for videos that have them), and also changes the permissions on the downloaded video: -``` + +```json {"writesubtitles": true, "subtitleslangs": ["en", "-live_chat"], "postprocessors": [{"key": "Exec", "exec_cmd": "chmod 0664", "when": "after_move"}, {"key": "FFmpegEmbedSubtitle", "already_have_subtitle": false}, {"key": "FFmpegMetadata", "add_chapters": true}]} ``` ## Using browser cookies + In case you need to use your browser's cookies with MeTube, for example to download restricted or private videos: * Add the following to your docker-compose.yml: @@ -62,9 +68,10 @@ In case you need to use your browser's cookies with MeTube, for example to downl environment: - YTDL_OPTIONS={"cookiefile":"/cookies/cookies.txt"} ``` + * Install in your browser an extension to extract cookies: - * [Firefox](https://addons.mozilla.org/en-US/firefox/addon/export-cookies-txt/) - * [Chrome](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc) + * [Firefox](https://addons.mozilla.org/en-US/firefox/addon/export-cookies-txt/) + * [Chrome](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc) * Extract the cookies you need with the extension and rename the file `cookies.txt` * Drop the file in the folder you configured in the docker-compose.yml above * Restart the container @@ -88,6 +95,7 @@ javascript:!function(){xhr=new XMLHttpRequest();xhr.open("POST","https://metube. ``` [shoonya75](https://github.com/shoonya75) has contributed a Firefox version: + ```javascript javascript:(function(){xhr=new XMLHttpRequest();xhr.open("POST","https://metube.domain.com/add");xhr.send(JSON.stringify({"url":document.location.href,"quality":"best"}));xhr.onload=function(){if(xhr.status==200){alert("Sent to metube!")}else{alert("Send to metube failed. Check the javascript console for clues.")}}})(); ``` diff --git a/app/main.py b/app/main.py index 1faa7a2..fb55854 100644 --- a/app/main.py +++ b/app/main.py @@ -17,6 +17,7 @@ class Config: _DEFAULTS = { 'DOWNLOAD_DIR': '.', 'AUDIO_DOWNLOAD_DIR': '%%DOWNLOAD_DIR', + 'TEMP_DIR': '%%DOWNLOAD_DIR', 'DOWNLOAD_DIRS_INDEXABLE': 'false', 'CUSTOM_DIRS': 'true', 'CREATE_CUSTOM_DIRS': 'true', @@ -138,13 +139,13 @@ def get_custom_dirs(): dirs = list(filter(None, map(convert, path.glob('**')))) return dirs - + download_dir = recursive_dirs(config.DOWNLOAD_DIR) audio_download_dir = download_dir if config.DOWNLOAD_DIR != config.AUDIO_DOWNLOAD_DIR: audio_download_dir = recursive_dirs(config.AUDIO_DOWNLOAD_DIR) - + return { "download_dir": download_dir, "audio_download_dir": audio_download_dir @@ -192,4 +193,5 @@ app.on_response_prepare.append(on_prepare) if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) + log.info(f"Listening on {config.HOST}:{config.PORT}") web.run_app(app, host=config.HOST, port=config.PORT, reuse_port=True) diff --git a/app/ytdl.py b/app/ytdl.py index 84fa4e6..83a01b0 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -9,7 +9,8 @@ import logging import re from dl_formats import get_format, get_opts, AUDIO_FORMATS -log = logging.getLogger('ytdl') +log = logging.getLogger("ytdl") + class DownloadQueueNotifier: async def added(self, dl): @@ -27,10 +28,11 @@ class DownloadQueueNotifier: async def cleared(self, id): raise NotImplementedError + class DownloadInfo: def __init__(self, id, title, url, quality, format, folder, custom_name_prefix): - self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}' - self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}' + self.id = id if len(custom_name_prefix) == 0 else f"{custom_name_prefix}.{id}" + self.title = title if len(custom_name_prefix) == 0 else f"{custom_name_prefix}.{title}" self.url = url self.quality = quality self.format = format @@ -39,11 +41,15 @@ class DownloadInfo: self.status = self.msg = self.percent = self.speed = self.eta = None self.timestamp = time.time_ns() + class Download: manager = None - def __init__(self, download_dir, output_template, output_template_chapter, quality, format, ytdl_opts, info): + def __init__( + self, download_dir, temp_dir, output_template, output_template_chapter, quality, format, ytdl_opts, info + ): self.download_dir = download_dir + self.temp_dir = temp_dir self.output_template = output_template self.output_template_chapter = output_template_chapter self.format = get_format(format, quality) @@ -58,40 +64,54 @@ class Download: def _download(self): try: + def put_status(st): - self.status_queue.put({k: v for k, v in st.items() if k in ( - 'tmpfilename', - 'filename', - 'status', - 'msg', - 'total_bytes', - 'total_bytes_estimate', - 'downloaded_bytes', - 'speed', - 'eta', - )}) + self.status_queue.put( + { + k: v + for k, v in st.items() + if k + in ( + "tmpfilename", + "filename", + "status", + "msg", + "total_bytes", + "total_bytes_estimate", + "downloaded_bytes", + "speed", + "eta", + ) + } + ) + def put_status_postprocessor(d): - if d['postprocessor'] == 'MoveFiles' and d['status'] == 'finished': - if '__finaldir' in d['info_dict']: - filename = os.path.join(d['info_dict']['__finaldir'], os.path.basename(d['info_dict']['filepath'])) + if d["postprocessor"] == "MoveFiles" and d["status"] == "finished": + if "__finaldir" in d["info_dict"]: + filename = os.path.join( + d["info_dict"]["__finaldir"], os.path.basename(d["info_dict"]["filepath"]) + ) else: - filename = d['info_dict']['filepath'] - self.status_queue.put({'status': 'finished', 'filename': filename}) - ret = yt_dlp.YoutubeDL(params={ - 'quiet': True, - 'no_color': True, - #'skip_download': True, - 'paths': {"home": self.download_dir}, - 'outtmpl': { "default": self.output_template, "chapter": self.output_template_chapter }, - 'format': self.format, - 'socket_timeout': 30, - 'progress_hooks': [put_status], - 'postprocessor_hooks': [put_status_postprocessor], - **self.ytdl_opts, - }).download([self.info.url]) - self.status_queue.put({'status': 'finished' if ret == 0 else 'error'}) + filename = d["info_dict"]["filepath"] + self.status_queue.put({"status": "finished", "filename": filename}) + + ret = yt_dlp.YoutubeDL( + params={ + "quiet": True, + "no_color": True, + #'skip_download': True, + "paths": {"home": self.download_dir, "temp": self.temp_dir}, + "outtmpl": {"default": self.output_template, "chapter": self.output_template_chapter}, + "format": self.format, + "socket_timeout": 30, + "progress_hooks": [put_status], + "postprocessor_hooks": [put_status_postprocessor], + **self.ytdl_opts, + } + ).download([self.info.url]) + self.status_queue.put({"status": "finished" if ret == 0 else "error"}) except yt_dlp.utils.YoutubeDLError as exc: - self.status_queue.put({'status': 'error', 'msg': str(exc)}) + self.status_queue.put({"status": "error", "msg": str(exc)}) async def start(self, notifier): if Download.manager is None: @@ -101,7 +121,7 @@ class Download: self.proc.start() self.loop = asyncio.get_running_loop() self.notifier = notifier - self.info.status = 'preparing' + self.info.status = "preparing" await self.notifier.updated(self.info) asyncio.create_task(self.update_status()) return await self.loop.run_in_executor(None, self.proc.join) @@ -130,65 +150,66 @@ class Download: status = await self.loop.run_in_executor(None, self.status_queue.get) if status is None: return - self.tmpfilename = status.get('tmpfilename') - if 'filename' in status: - self.info.filename = os.path.relpath(status.get('filename'), self.download_dir) + self.tmpfilename = status.get("tmpfilename") + if "filename" in status: + self.info.filename = os.path.relpath(status.get("filename"), self.download_dir) # Set correct file extension for thumbnails - if(self.info.format == 'thumbnail'): - self.info.filename = re.sub(r'\.webm$', '.jpg', self.info.filename) - self.info.status = status['status'] - self.info.msg = status.get('msg') - if 'downloaded_bytes' in status: - total = status.get('total_bytes') or status.get('total_bytes_estimate') + if self.info.format == "thumbnail": + self.info.filename = re.sub(r"\.webm$", ".jpg", self.info.filename) + self.info.status = status["status"] + self.info.msg = status.get("msg") + 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') + self.info.percent = status["downloaded_bytes"] / total * 100 + self.info.speed = status.get("speed") + self.info.eta = status.get("eta") await self.notifier.updated(self.info) + class PersistentQueue: def __init__(self, path): pdir = os.path.dirname(path) if not os.path.isdir(pdir): os.mkdir(pdir) - with shelve.open(path, 'c'): + with shelve.open(path, "c"): pass self.path = path self.dict = OrderedDict() - + def load(self): for k, v in self.saved_items(): - self.dict[k] = Download(None, None, None, None, None, {}, v) + self.dict[k] = Download(None, None, None, None, None, None, {}, v) def exists(self, key): return key in self.dict - + def get(self, key): return self.dict[key] - + def items(self): return self.dict.items() def saved_items(self): - with shelve.open(self.path, 'r') as shelf: + with shelve.open(self.path, "r") as shelf: return sorted(shelf.items(), key=lambda item: item[1].timestamp) def put(self, value): key = value.info.url self.dict[key] = value - with shelve.open(self.path, 'w') as shelf: + with shelve.open(self.path, "w") as shelf: shelf[key] = value.info - + def delete(self, key): del self.dict[key] - with shelve.open(self.path, 'w') as shelf: + with shelve.open(self.path, "w") as shelf: shelf.pop(key) def next(self): k, v = next(iter(self.dict.items())) return k, v - + def empty(self): return not bool(self.dict) @@ -197,10 +218,10 @@ class DownloadQueue: def __init__(self, config, notifier): self.config = config self.notifier = notifier - self.queue = PersistentQueue(self.config.STATE_DIR + '/queue') - self.done = PersistentQueue(self.config.STATE_DIR + '/completed') + self.queue = PersistentQueue(self.config.STATE_DIR + "/queue") + self.done = PersistentQueue(self.config.STATE_DIR + "/completed") self.done.load() - + async def __import_queue(self): for k, v in self.queue.saved_items(): await self.add(v.url, v.quality, v.format, v.folder, v.custom_name_prefix) @@ -211,119 +232,159 @@ class DownloadQueue: asyncio.create_task(self.__import_queue()) def __extract_info(self, url): - return yt_dlp.YoutubeDL(params={ - 'quiet': True, - 'no_color': True, - 'extract_flat': True, - **self.config.YTDL_OPTIONS, - }).extract_info(url, download=False) + return yt_dlp.YoutubeDL( + params={ + "quiet": True, + "no_color": True, + "extract_flat": True, + **self.config.YTDL_OPTIONS, + } + ).extract_info(url, download=False) async def __add_entry(self, entry, quality, format, folder, custom_name_prefix, already): - etype = entry.get('_type') or 'video' - if etype == 'playlist': - entries = entry['entries'] - log.info(f'playlist detected with {len(entries)} entries') + etype = entry.get("_type") or "video" + if etype == "playlist": + entries = entry["entries"] + log.info(f"playlist detected with {len(entries)} entries") playlist_index_digits = len(str(len(entries))) results = [] for index, etr in enumerate(entries, start=1): etr["playlist"] = entry["id"] - etr["playlist_index"] = '{{0:0{0:d}d}}'.format(playlist_index_digits).format(index) + etr["playlist_index"] = "{{0:0{0:d}d}}".format(playlist_index_digits).format(index) for property in ("id", "title", "uploader", "uploader_id"): if property in entry: etr[f"playlist_{property}"] = entry[property] results.append(await self.__add_entry(etr, quality, format, folder, custom_name_prefix, 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'} - elif etype == 'video' or etype.startswith('url') and 'id' in entry and 'title' in entry: - if not self.queue.exists(entry['id']): - dl = DownloadInfo(entry['id'], entry['title'], entry.get('webpage_url') or entry['url'], quality, format, folder, custom_name_prefix) + 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"} + elif etype == "video" or etype.startswith("url") and "id" in entry and "title" in entry: + if not self.queue.exists(entry["id"]): + dl = DownloadInfo( + entry["id"], + entry["title"], + entry.get("webpage_url") or entry["url"], + quality, + format, + folder, + custom_name_prefix, + ) # Keep consistent with frontend - base_directory = self.config.DOWNLOAD_DIR if (quality != 'audio' and format not in AUDIO_FORMATS) else self.config.AUDIO_DOWNLOAD_DIR + base_directory = ( + self.config.DOWNLOAD_DIR + if (quality != "audio" and format not in AUDIO_FORMATS) + else self.config.AUDIO_DOWNLOAD_DIR + ) if folder: if not self.config.CUSTOM_DIRS: - return {'status': 'error', 'msg': f'A folder for the download was specified but CUSTOM_DIRS is not true in the configuration.'} + return { + "status": "error", + "msg": f"A folder for the download was specified but CUSTOM_DIRS is not true in the configuration.", + } dldirectory = os.path.realpath(os.path.join(base_directory, folder)) real_base_directory = os.path.realpath(base_directory) if not dldirectory.startswith(real_base_directory): - return {'status': 'error', 'msg': f'Folder "{folder}" must resolve inside the base download directory "{real_base_directory}"'} + return { + "status": "error", + "msg": f'Folder "{folder}" must resolve inside the base download directory "{real_base_directory}"', + } if not os.path.isdir(dldirectory): if not self.config.CREATE_CUSTOM_DIRS: - return {'status': 'error', 'msg': f'Folder "{folder}" for download does not exist inside base directory "{real_base_directory}", and CREATE_CUSTOM_DIRS is not true in the configuration.'} + return { + "status": "error", + "msg": f'Folder "{folder}" for download does not exist inside base directory "{real_base_directory}", and CREATE_CUSTOM_DIRS is not true in the configuration.', + } os.makedirs(dldirectory, exist_ok=True) else: dldirectory = base_directory - output = self.config.OUTPUT_TEMPLATE if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}' + output = ( + self.config.OUTPUT_TEMPLATE + if len(custom_name_prefix) == 0 + else f"{custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}" + ) output_chapter = self.config.OUTPUT_TEMPLATE_CHAPTER for property, value in entry.items(): if property.startswith("playlist"): output = output.replace(f"%({property})s", str(value)) - self.queue.put(Download(dldirectory, output, output_chapter, quality, format, self.config.YTDL_OPTIONS, dl)) + self.queue.put( + Download( + dldirectory, + self.config.TEMP_DIR, + output, + output_chapter, + quality, + format, + self.config.YTDL_OPTIONS, + dl, + ) + ) self.event.set() await self.notifier.added(dl) - return {'status': 'ok'} - elif etype.startswith('url'): - return await self.add(entry['url'], quality, format, folder, custom_name_prefix, already) - return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'} + return {"status": "ok"} + elif etype.startswith("url"): + return await self.add(entry["url"], quality, format, folder, custom_name_prefix, already) + return {"status": "error", "msg": f'Unsupported resource "{etype}"'} async def add(self, url, quality, format, folder, custom_name_prefix, already=None): - log.info(f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=}') + log.info(f"adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=}") already = set() if already is None else already if url in already: - log.info('recursion detected, skipping') - return {'status': 'ok'} + 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)} + return {"status": "error", "msg": str(exc)} return await self.__add_entry(entry, quality, format, folder, custom_name_prefix, already) async def cancel(self, ids): for id in ids: if not self.queue.exists(id): - log.warn(f'requested cancel for non-existent download {id}') + log.warn(f"requested cancel for non-existent download {id}") continue if self.queue.get(id).started(): self.queue.get(id).cancel() else: self.queue.delete(id) await self.notifier.canceled(id) - return {'status': 'ok'} + return {"status": "ok"} async def clear(self, ids): for id in ids: if not self.done.exists(id): - log.warn(f'requested delete for non-existent download {id}') + log.warn(f"requested delete for non-existent download {id}") continue if self.config.DELETE_FILE_ON_TRASHCAN: dl = self.done.get(id) os.remove(os.path.join(dl.download_dir, dl.info.filename)) self.done.delete(id) await self.notifier.cleared(id) - return {'status': 'ok'} + return {"status": "ok"} def get(self): - return(list((k, v.info) for k, v in self.queue.items()), - list((k, v.info) for k, v in self.done.items())) + return (list((k, v.info) for k, v in self.queue.items()), list((k, v.info) for k, v in self.done.items())) async def __download(self): while True: while self.queue.empty(): - log.info('waiting for item to download') + log.info("waiting for item to download") await self.event.wait() self.event.clear() id, entry = self.queue.next() - log.info(f'downloading {entry.info.title}') + log.info(f"downloading {entry.info.title}") await entry.start(self.notifier) - if entry.info.status != 'finished': + 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' + entry.info.status = "error" entry.close() if self.queue.exists(id): self.queue.delete(id) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c2e3c50 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3" +services: + metube: + # Local Build: Use the following to build the Docker image locally + build: + context: . + dockerfile: ./Dockerfile + image: metube + # Official Image: Comment out the lines above and uncomment the following line to use the official image + # image: ghcr.io/alexta69/metube + container_name: metube + restart: unless-stopped + ports: + - "8081:8081" + + # Example environment variables + environment: + # UID: 1000 + # GID: 1000 + YTDL_OPTIONS: '{"writesubtitles": true, "subtitleslangs": ["en", "-live_chat"], "postprocessors": [{"key": "FFmpegEmbedSubtitle", "already_have_subtitle": false}, {"key": "FFmpegMetadata", "add_chapters": true}]}' + # Configure yt-dlp to download intermediary files to /temp + TEMP_DIR: /temp + + volumes: + - ./downloads:/downloads + # Mount /temp using tmpfs + # This will store the intermediary files in memory, making downloads faster; however, you won't be able to + # resume a download if you stop the container + - type: tmpfs + target: /temp:rw,nosuid,nodev,exec diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index ccf58d7..d58692c 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -2,15 +2,15 @@ echo "Setting umask to ${UMASK}" umask ${UMASK} -echo "Creating download directory ${DOWNLOAD_DIR} and state directory ${STATE_DIR}" -mkdir -p "${DOWNLOAD_DIR}" "${STATE_DIR}" +echo "Creating download directory (${DOWNLOAD_DIR}), state directory (${STATE_DIR}), and temp dir (${TEMP_DIR})" +mkdir -p "${DOWNLOAD_DIR}" "${STATE_DIR}" "${TEMP_DIR}" if [ `id -u` -eq 0 ] && [ `id -g` -eq 0 ]; then if [ "${UID}" -eq 0 ]; then echo "Warning: it is not recommended to run as root user, please check your setting of the UID environment variable" fi echo "Changing ownership of download and state directories to ${UID}:${GID}" - chown -R "${UID}":"${GID}" /app "${DOWNLOAD_DIR}" "${STATE_DIR}" + chown -R "${UID}":"${GID}" /app "${DOWNLOAD_DIR}" "${STATE_DIR}" "${TEMP_DIR}" echo "Running MeTube as user ${UID}:${GID}" su-exec "${UID}":"${GID}" python3 app/main.py else From fb7e15bd56494ec5b229671915ee14325d34176d Mon Sep 17 00:00:00 2001 From: breakid Date: Tue, 9 May 2023 16:10:30 -0400 Subject: [PATCH 2/5] Fixed default TEMP_DIR path --- Dockerfile | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5c9353c..e8bdf7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ ENV UMASK=022 ENV DOWNLOAD_DIR /downloads ENV STATE_DIR /downloads/.metube -ENV TEMP_DIR /tmp +ENV TEMP_DIR /downloads VOLUME /downloads EXPOSE 8081 CMD [ "./docker-entrypoint.sh" ] diff --git a/README.md b/README.md index f6d10bc..65cf8ba 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Certain values can be set via environment variables, using the `-e` parameter on * __CUSTOM_DIRS__: whether to enable downloading videos into custom directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__). When enabled, a drop-down appears next to the Add button to specify the download directory. Defaults to `true`. * __CREATE_CUSTOM_DIRS__: whether to support automatically creating directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__) if they do not exist. When enabled, the download directory selector becomes supports free-text input, and the specified directory will be created recursively. Defaults to `true`. * __STATE_DIR__: path to where the queue persistence files will be saved. Defaults to `/downloads/.metube` in the docker image, and `.` otherwise. -* __TEMP_DIR__: path where intermediary download files will be saved. Defaults to `/downloads/.metube` in the docker image, and `.` otherwise. +* __TEMP_DIR__: path where intermediary download files will be saved. Defaults to `/downloads` in the docker image, and `.` otherwise. * Set this to an SSD or RAM filesystem (e.g., `tmpfs`) for better performance * __Note__: Using a RAM filesystem may prevent downloads from being resumed * __DELETE_FILE_ON_TRASHCAN__: if `true`, downloaded files are deleted on the server, when they are trashed from the "Completed" section of the UI. Defaults to `false`. From 70b8bedc64c00c25eab93612b56377febba4eb18 Mon Sep 17 00:00:00 2001 From: breakid Date: Wed, 10 May 2023 18:29:32 -0400 Subject: [PATCH 3/5] Made changes requested by original author - Recombined COPY and RUN commands in Dockerfile - Modified 'supported sites' URL in README - Undid automatic formatting in ytdlp.py --- Dockerfile | 12 +++++------- README.md | 2 +- app/ytdl.py | 33 ++++++++++++--------------------- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/Dockerfile b/Dockerfile index e8bdf7a..d678a03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,10 +10,13 @@ FROM python:3.8-alpine WORKDIR /app -COPY Pipfile* . +COPY Pipfile* docker-entrypoint.sh . +# Use sed to strip carriage-return characters from the entrypoint script (in case building on Windows) # Install dependencies -RUN apk add --update ffmpeg aria2 coreutils shadow su-exec && \ +RUN sed -i 's/\r$//g' docker-entrypoint.sh && \ + chmod +x docker-entrypoint.sh && \ + apk add --update ffmpeg aria2 coreutils shadow su-exec && \ apk add --update --virtual .build-deps gcc g++ musl-dev && \ pip install --no-cache-dir pipenv && \ pipenv install --system --deploy --clear && \ @@ -23,11 +26,6 @@ RUN apk add --update ffmpeg aria2 coreutils shadow su-exec && \ mkdir /.cache && chmod 777 /.cache COPY favicon ./favicon -COPY docker-entrypoint.sh . - -# Use sed to strip carriage-return characters from the entrypoint script (in case building on Windows) -RUN sed -i 's/\r$//g' docker-entrypoint.sh && chmod +x docker-entrypoint.sh - COPY app ./app COPY --from=builder /metube/dist/metube ./ui/dist/metube diff --git a/README.md b/README.md index 65cf8ba..abaca29 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![Build Status](https://github.com/alexta69/metube/actions/workflows/main.yml/badge.svg) ![Docker Pulls](https://img.shields.io/docker/pulls/alexta69/metube.svg) -Web GUI for youtube-dl (using the [yt-dlp](https://github.com/yt-dlp/yt-dlp) fork) with playlist support. Allows you to download videos from YouTube and dozens of other sites (). +Web GUI for youtube-dl (using the [yt-dlp](https://github.com/yt-dlp/yt-dlp) fork) with playlist support. Allows you to download videos from YouTube and [dozens of other sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md). ![screenshot1](https://github.com/alexta69/metube/raw/master/screenshot.gif) diff --git a/app/ytdl.py b/app/ytdl.py index 83a01b0..7632227 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -45,9 +45,7 @@ class DownloadInfo: class Download: manager = None - def __init__( - self, download_dir, temp_dir, output_template, output_template_chapter, quality, format, ytdl_opts, info - ): + def __init__(self, download_dir, temp_dir, output_template, output_template_chapter, quality, format, ytdl_opts, info): self.download_dir = download_dir self.temp_dir = temp_dir self.output_template = output_template @@ -66,24 +64,17 @@ class Download: try: def put_status(st): - self.status_queue.put( - { - k: v - for k, v in st.items() - if k - in ( - "tmpfilename", - "filename", - "status", - "msg", - "total_bytes", - "total_bytes_estimate", - "downloaded_bytes", - "speed", - "eta", - ) - } - ) + self.status_queue.put({k: v for k, v in st.items() if k in ( + 'tmpfilename', + 'filename', + 'status', + 'msg', + 'total_bytes', + 'total_bytes_estimate', + 'downloaded_bytes', + 'speed', + 'eta', + )}) def put_status_postprocessor(d): if d["postprocessor"] == "MoveFiles" and d["status"] == "finished": From c30173cdc064e7b9c6950098216a93be398cc6a7 Mon Sep 17 00:00:00 2001 From: breakid Date: Sun, 14 May 2023 00:37:09 -0400 Subject: [PATCH 4/5] Reverted all formatting changes --- app/main.py | 6 +- app/ytdl.py | 229 ++++++++++++++++++++-------------------------------- 2 files changed, 92 insertions(+), 143 deletions(-) diff --git a/app/main.py b/app/main.py index fb55854..7aff1cf 100644 --- a/app/main.py +++ b/app/main.py @@ -139,13 +139,13 @@ def get_custom_dirs(): dirs = list(filter(None, map(convert, path.glob('**')))) return dirs - + download_dir = recursive_dirs(config.DOWNLOAD_DIR) audio_download_dir = download_dir if config.DOWNLOAD_DIR != config.AUDIO_DOWNLOAD_DIR: audio_download_dir = recursive_dirs(config.AUDIO_DOWNLOAD_DIR) - + return { "download_dir": download_dir, "audio_download_dir": audio_download_dir @@ -194,4 +194,4 @@ app.on_response_prepare.append(on_prepare) if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) log.info(f"Listening on {config.HOST}:{config.PORT}") - web.run_app(app, host=config.HOST, port=config.PORT, reuse_port=True) + web.run_app(app, host=config.HOST, port=config.PORT, reuse_port=True) \ No newline at end of file diff --git a/app/ytdl.py b/app/ytdl.py index 7632227..ed65e19 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -9,8 +9,7 @@ import logging import re from dl_formats import get_format, get_opts, AUDIO_FORMATS -log = logging.getLogger("ytdl") - +log = logging.getLogger('ytdl') class DownloadQueueNotifier: async def added(self, dl): @@ -28,11 +27,10 @@ class DownloadQueueNotifier: async def cleared(self, id): raise NotImplementedError - class DownloadInfo: def __init__(self, id, title, url, quality, format, folder, custom_name_prefix): - self.id = id if len(custom_name_prefix) == 0 else f"{custom_name_prefix}.{id}" - self.title = title if len(custom_name_prefix) == 0 else f"{custom_name_prefix}.{title}" + self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}' + self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}' self.url = url self.quality = quality self.format = format @@ -41,7 +39,6 @@ class DownloadInfo: self.status = self.msg = self.percent = self.speed = self.eta = None self.timestamp = time.time_ns() - class Download: manager = None @@ -62,7 +59,6 @@ class Download: def _download(self): try: - def put_status(st): self.status_queue.put({k: v for k, v in st.items() if k in ( 'tmpfilename', @@ -75,34 +71,28 @@ class Download: 'speed', 'eta', )}) - def put_status_postprocessor(d): - if d["postprocessor"] == "MoveFiles" and d["status"] == "finished": - if "__finaldir" in d["info_dict"]: - filename = os.path.join( - d["info_dict"]["__finaldir"], os.path.basename(d["info_dict"]["filepath"]) - ) + if d['postprocessor'] == 'MoveFiles' and d['status'] == 'finished': + if '__finaldir' in d['info_dict']: + filename = os.path.join(d['info_dict']['__finaldir'], os.path.basename(d['info_dict']['filepath'])) else: - filename = d["info_dict"]["filepath"] - self.status_queue.put({"status": "finished", "filename": filename}) - - ret = yt_dlp.YoutubeDL( - params={ - "quiet": True, - "no_color": True, - #'skip_download': True, - "paths": {"home": self.download_dir, "temp": self.temp_dir}, - "outtmpl": {"default": self.output_template, "chapter": self.output_template_chapter}, - "format": self.format, - "socket_timeout": 30, - "progress_hooks": [put_status], - "postprocessor_hooks": [put_status_postprocessor], - **self.ytdl_opts, - } - ).download([self.info.url]) - self.status_queue.put({"status": "finished" if ret == 0 else "error"}) + filename = d['info_dict']['filepath'] + self.status_queue.put({'status': 'finished', 'filename': filename}) + ret = yt_dlp.YoutubeDL(params={ + 'quiet': True, + 'no_color': True, + #'skip_download': True, + 'paths': {"home": self.download_dir, "temp": self.temp_dir}, + 'outtmpl': { "default": self.output_template, "chapter": self.output_template_chapter }, + 'format': self.format, + 'socket_timeout': 30, + 'progress_hooks': [put_status], + 'postprocessor_hooks': [put_status_postprocessor], + **self.ytdl_opts, + }).download([self.info.url]) + self.status_queue.put({'status': 'finished' if ret == 0 else 'error'}) except yt_dlp.utils.YoutubeDLError as exc: - self.status_queue.put({"status": "error", "msg": str(exc)}) + self.status_queue.put({'status': 'error', 'msg': str(exc)}) async def start(self, notifier): if Download.manager is None: @@ -112,7 +102,7 @@ class Download: self.proc.start() self.loop = asyncio.get_running_loop() self.notifier = notifier - self.info.status = "preparing" + self.info.status = 'preparing' await self.notifier.updated(self.info) asyncio.create_task(self.update_status()) return await self.loop.run_in_executor(None, self.proc.join) @@ -141,66 +131,65 @@ class Download: status = await self.loop.run_in_executor(None, self.status_queue.get) if status is None: return - self.tmpfilename = status.get("tmpfilename") - if "filename" in status: - self.info.filename = os.path.relpath(status.get("filename"), self.download_dir) + self.tmpfilename = status.get('tmpfilename') + if 'filename' in status: + self.info.filename = os.path.relpath(status.get('filename'), self.download_dir) # Set correct file extension for thumbnails - if self.info.format == "thumbnail": - self.info.filename = re.sub(r"\.webm$", ".jpg", self.info.filename) - self.info.status = status["status"] - self.info.msg = status.get("msg") - if "downloaded_bytes" in status: - total = status.get("total_bytes") or status.get("total_bytes_estimate") + if(self.info.format == 'thumbnail'): + self.info.filename = re.sub(r'\.webm$', '.jpg', self.info.filename) + self.info.status = status['status'] + self.info.msg = status.get('msg') + 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") + self.info.percent = status['downloaded_bytes'] / total * 100 + self.info.speed = status.get('speed') + self.info.eta = status.get('eta') await self.notifier.updated(self.info) - class PersistentQueue: def __init__(self, path): pdir = os.path.dirname(path) if not os.path.isdir(pdir): os.mkdir(pdir) - with shelve.open(path, "c"): + with shelve.open(path, 'c'): pass self.path = path self.dict = OrderedDict() - + def load(self): for k, v in self.saved_items(): self.dict[k] = Download(None, None, None, None, None, None, {}, v) def exists(self, key): return key in self.dict - + def get(self, key): return self.dict[key] - + def items(self): return self.dict.items() def saved_items(self): - with shelve.open(self.path, "r") as shelf: + with shelve.open(self.path, 'r') as shelf: return sorted(shelf.items(), key=lambda item: item[1].timestamp) def put(self, value): key = value.info.url self.dict[key] = value - with shelve.open(self.path, "w") as shelf: + with shelve.open(self.path, 'w') as shelf: shelf[key] = value.info - + def delete(self, key): del self.dict[key] - with shelve.open(self.path, "w") as shelf: + with shelve.open(self.path, 'w') as shelf: shelf.pop(key) def next(self): k, v = next(iter(self.dict.items())) return k, v - + def empty(self): return not bool(self.dict) @@ -209,10 +198,10 @@ class DownloadQueue: def __init__(self, config, notifier): self.config = config self.notifier = notifier - self.queue = PersistentQueue(self.config.STATE_DIR + "/queue") - self.done = PersistentQueue(self.config.STATE_DIR + "/completed") + self.queue = PersistentQueue(self.config.STATE_DIR + '/queue') + self.done = PersistentQueue(self.config.STATE_DIR + '/completed') self.done.load() - + async def __import_queue(self): for k, v in self.queue.saved_items(): await self.add(v.url, v.quality, v.format, v.folder, v.custom_name_prefix) @@ -223,159 +212,119 @@ class DownloadQueue: asyncio.create_task(self.__import_queue()) def __extract_info(self, url): - return yt_dlp.YoutubeDL( - params={ - "quiet": True, - "no_color": True, - "extract_flat": True, - **self.config.YTDL_OPTIONS, - } - ).extract_info(url, download=False) + return yt_dlp.YoutubeDL(params={ + 'quiet': True, + 'no_color': True, + 'extract_flat': True, + **self.config.YTDL_OPTIONS, + }).extract_info(url, download=False) async def __add_entry(self, entry, quality, format, folder, custom_name_prefix, already): - etype = entry.get("_type") or "video" - if etype == "playlist": - entries = entry["entries"] - log.info(f"playlist detected with {len(entries)} entries") + etype = entry.get('_type') or 'video' + if etype == 'playlist': + entries = entry['entries'] + log.info(f'playlist detected with {len(entries)} entries') playlist_index_digits = len(str(len(entries))) results = [] for index, etr in enumerate(entries, start=1): etr["playlist"] = entry["id"] - etr["playlist_index"] = "{{0:0{0:d}d}}".format(playlist_index_digits).format(index) + etr["playlist_index"] = '{{0:0{0:d}d}}'.format(playlist_index_digits).format(index) for property in ("id", "title", "uploader", "uploader_id"): if property in entry: etr[f"playlist_{property}"] = entry[property] results.append(await self.__add_entry(etr, quality, format, folder, custom_name_prefix, 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"} - elif etype == "video" or etype.startswith("url") and "id" in entry and "title" in entry: - if not self.queue.exists(entry["id"]): - dl = DownloadInfo( - entry["id"], - entry["title"], - entry.get("webpage_url") or entry["url"], - quality, - format, - folder, - custom_name_prefix, - ) + 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'} + elif etype == 'video' or etype.startswith('url') and 'id' in entry and 'title' in entry: + if not self.queue.exists(entry['id']): + dl = DownloadInfo(entry['id'], entry['title'], entry.get('webpage_url') or entry['url'], quality, format, folder, custom_name_prefix) # Keep consistent with frontend - base_directory = ( - self.config.DOWNLOAD_DIR - if (quality != "audio" and format not in AUDIO_FORMATS) - else self.config.AUDIO_DOWNLOAD_DIR - ) + base_directory = self.config.DOWNLOAD_DIR if (quality != 'audio' and format not in AUDIO_FORMATS) else self.config.AUDIO_DOWNLOAD_DIR if folder: if not self.config.CUSTOM_DIRS: - return { - "status": "error", - "msg": f"A folder for the download was specified but CUSTOM_DIRS is not true in the configuration.", - } + return {'status': 'error', 'msg': f'A folder for the download was specified but CUSTOM_DIRS is not true in the configuration.'} dldirectory = os.path.realpath(os.path.join(base_directory, folder)) real_base_directory = os.path.realpath(base_directory) if not dldirectory.startswith(real_base_directory): - return { - "status": "error", - "msg": f'Folder "{folder}" must resolve inside the base download directory "{real_base_directory}"', - } + return {'status': 'error', 'msg': f'Folder "{folder}" must resolve inside the base download directory "{real_base_directory}"'} if not os.path.isdir(dldirectory): if not self.config.CREATE_CUSTOM_DIRS: - return { - "status": "error", - "msg": f'Folder "{folder}" for download does not exist inside base directory "{real_base_directory}", and CREATE_CUSTOM_DIRS is not true in the configuration.', - } + return {'status': 'error', 'msg': f'Folder "{folder}" for download does not exist inside base directory "{real_base_directory}", and CREATE_CUSTOM_DIRS is not true in the configuration.'} os.makedirs(dldirectory, exist_ok=True) else: dldirectory = base_directory - output = ( - self.config.OUTPUT_TEMPLATE - if len(custom_name_prefix) == 0 - else f"{custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}" - ) + output = self.config.OUTPUT_TEMPLATE if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}' output_chapter = self.config.OUTPUT_TEMPLATE_CHAPTER for property, value in entry.items(): if property.startswith("playlist"): output = output.replace(f"%({property})s", str(value)) - self.queue.put( - Download( - dldirectory, - self.config.TEMP_DIR, - output, - output_chapter, - quality, - format, - self.config.YTDL_OPTIONS, - dl, - ) - ) + self.queue.put(Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, self.config.YTDL_OPTIONS, dl)) self.event.set() await self.notifier.added(dl) - return {"status": "ok"} - elif etype.startswith("url"): - return await self.add(entry["url"], quality, format, folder, custom_name_prefix, already) - return {"status": "error", "msg": f'Unsupported resource "{etype}"'} + return {'status': 'ok'} + elif etype.startswith('url'): + return await self.add(entry['url'], quality, format, folder, custom_name_prefix, already) + return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'} async def add(self, url, quality, format, folder, custom_name_prefix, already=None): - log.info(f"adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=}") + log.info(f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=}') already = set() if already is None else already if url in already: - log.info("recursion detected, skipping") - return {"status": "ok"} + 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)} + return {'status': 'error', 'msg': str(exc)} return await self.__add_entry(entry, quality, format, folder, custom_name_prefix, already) async def cancel(self, ids): for id in ids: if not self.queue.exists(id): - log.warn(f"requested cancel for non-existent download {id}") + log.warn(f'requested cancel for non-existent download {id}') continue if self.queue.get(id).started(): self.queue.get(id).cancel() else: self.queue.delete(id) await self.notifier.canceled(id) - return {"status": "ok"} + return {'status': 'ok'} async def clear(self, ids): for id in ids: if not self.done.exists(id): - log.warn(f"requested delete for non-existent download {id}") + log.warn(f'requested delete for non-existent download {id}') continue if self.config.DELETE_FILE_ON_TRASHCAN: dl = self.done.get(id) os.remove(os.path.join(dl.download_dir, dl.info.filename)) self.done.delete(id) await self.notifier.cleared(id) - return {"status": "ok"} + return {'status': 'ok'} def get(self): - return (list((k, v.info) for k, v in self.queue.items()), list((k, v.info) for k, v in self.done.items())) + return(list((k, v.info) for k, v in self.queue.items()), + list((k, v.info) for k, v in self.done.items())) async def __download(self): while True: while self.queue.empty(): - log.info("waiting for item to download") + log.info('waiting for item to download') await self.event.wait() self.event.clear() id, entry = self.queue.next() - log.info(f"downloading {entry.info.title}") + log.info(f'downloading {entry.info.title}') await entry.start(self.notifier) - if entry.info.status != "finished": + 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" + entry.info.status = 'error' entry.close() if self.queue.exists(id): self.queue.delete(id) @@ -383,4 +332,4 @@ class DownloadQueue: await self.notifier.canceled(id) else: self.done.put(entry) - await self.notifier.completed(entry.info) + await self.notifier.completed(entry.info) \ No newline at end of file From 2a7cf3e7753779a56f2c88b407ffe20d7a8fcbb4 Mon Sep 17 00:00:00 2001 From: Alex Shnitman Date: Sat, 27 May 2023 19:14:52 +0300 Subject: [PATCH 5/5] formatting --- app/main.py | 2 +- app/ytdl.py | 2 +- docker-compose.yml | 30 ------------------------------ 3 files changed, 2 insertions(+), 32 deletions(-) delete mode 100644 docker-compose.yml diff --git a/app/main.py b/app/main.py index 7aff1cf..7b3817b 100644 --- a/app/main.py +++ b/app/main.py @@ -194,4 +194,4 @@ app.on_response_prepare.append(on_prepare) if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) log.info(f"Listening on {config.HOST}:{config.PORT}") - web.run_app(app, host=config.HOST, port=config.PORT, reuse_port=True) \ No newline at end of file + web.run_app(app, host=config.HOST, port=config.PORT, reuse_port=True) diff --git a/app/ytdl.py b/app/ytdl.py index ed65e19..2c52f72 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -332,4 +332,4 @@ class DownloadQueue: await self.notifier.canceled(id) else: self.done.put(entry) - await self.notifier.completed(entry.info) \ No newline at end of file + await self.notifier.completed(entry.info) diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index c2e3c50..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,30 +0,0 @@ -version: "3" -services: - metube: - # Local Build: Use the following to build the Docker image locally - build: - context: . - dockerfile: ./Dockerfile - image: metube - # Official Image: Comment out the lines above and uncomment the following line to use the official image - # image: ghcr.io/alexta69/metube - container_name: metube - restart: unless-stopped - ports: - - "8081:8081" - - # Example environment variables - environment: - # UID: 1000 - # GID: 1000 - YTDL_OPTIONS: '{"writesubtitles": true, "subtitleslangs": ["en", "-live_chat"], "postprocessors": [{"key": "FFmpegEmbedSubtitle", "already_have_subtitle": false}, {"key": "FFmpegMetadata", "add_chapters": true}]}' - # Configure yt-dlp to download intermediary files to /temp - TEMP_DIR: /temp - - volumes: - - ./downloads:/downloads - # Mount /temp using tmpfs - # This will store the intermediary files in memory, making downloads faster; however, you won't be able to - # resume a download if you stop the container - - type: tmpfs - target: /temp:rw,nosuid,nodev,exec