From 0cd36ea4be5997d8fe7614b9500a6420c1278592 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Sun, 3 Nov 2024 17:23:56 +0300 Subject: [PATCH 1/5] validating user supplied filename. --- app/main.py | 19 +++++++++---------- frontend/src/main.js | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app/main.py b/app/main.py index 105367d0..dd0c6966 100644 --- a/app/main.py +++ b/app/main.py @@ -519,9 +519,11 @@ class Main: sd: int = request.query.get('sd') vc: int = int(request.query.get('vc', 0)) ac: int = int(request.query.get('ac', 0)) + file_path: str = os.path.normpath(os.path.join(self.config.download_path, file)) + if not file_path.startswith(self.config.download_path): + raise web.HTTPBadRequest(reason='Invalid file path.') if request.if_modified_since: - file_path = os.path.join(self.config.download_path, file) if os.path.exists(file_path) and request.if_modified_since.timestamp() == os.path.getmtime(file_path): return web.Response(status=304) @@ -546,9 +548,7 @@ class Main: 'Access-Control-Allow-Origin': '*', 'Pragma': 'public', 'Cache-Control': f'public, max-age={time.time() + 31536000}', - 'Last-Modified': time.strftime('%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp( - os.path.getmtime(os.path.join(self.config.download_path, file))).timetuple() - ), + 'Last-Modified': time.strftime('%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp(os.path.getmtime(file_path)).timetuple()), 'Expires': time.strftime('%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp(time.time() + 31536000).timetuple()), } ) @@ -556,12 +556,13 @@ class Main: @self.routes.get(self.config.url_prefix + 'player/subtitle/{file:.*}.vtt') async def subtitles(request: Request) -> Response: file: str = request.match_info.get('file') + file_path: str = os.path.normpath(os.path.join(self.config.download_path, file)) + if not file_path.startswith(self.config.download_path): + raise web.HTTPBadRequest(reason='Invalid file path.') if request.if_modified_since: - file_path = os.path.join(self.config.download_path, file) lastMod = time.strftime('%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp( - os.path.getmtime(os.path.join(self.config.download_path, file))).timetuple() - ) + os.path.getmtime(file_path)).timetuple()) if os.path.exists(file_path) and request.if_modified_since.timestamp() == os.path.getmtime(file_path): return web.Response(status=304, headers={'Last-Modified': lastMod}) @@ -576,9 +577,7 @@ class Main: 'Access-Control-Allow-Origin': '*', 'Pragma': 'public', 'Cache-Control': f'public, max-age={time.time() + 31536000}', - 'Last-Modified': time.strftime('%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp( - os.path.getmtime(os.path.join(self.config.download_path, file))).timetuple() - ), + 'Last-Modified': time.strftime('%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp(os.path.getmtime(file_path)).timetuple()), 'Expires': time.strftime('%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp(time.time() + 31536000).timetuple()), } ) diff --git a/frontend/src/main.js b/frontend/src/main.js index 7eb92e12..8b77b952 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -41,7 +41,7 @@ app.config.globalProperties.makeDownload = (config, item, base = 'download') => let baseDir = 'download' === base ? `${base}/` : 'player/playlist/'; if (item.folder) { - item.folder = item.folder.replace('#', '%23'); + item.folder = item.folder.replace(/#/g, '%23'); baseDir += item.folder + '/'; } From 7a5a06219cb4f80917c3b30e7dbbd690823044df Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Sun, 3 Nov 2024 17:56:12 +0300 Subject: [PATCH 2/5] Added possible fallback for playlist incase filename got changed. --- app/Utils.py | 31 +++++++++++++++++++++++++++++++ app/main.py | 2 ++ app/player/Playlist.py | 11 +++++++++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/app/Utils.py b/app/Utils.py index 1452a333..4448896e 100644 --- a/app/Utils.py +++ b/app/Utils.py @@ -4,6 +4,8 @@ from datetime import datetime, timezone import json import logging import os +import pathlib +import re from typing import Any import yt_dlp from socketio import AsyncServer @@ -388,3 +390,32 @@ def load_file(file: str, check_type=None) -> tuple[dict | list, bool, str]: return ({}, False, f"Failed to assert that the contents '{type(opts)}' are of type '{check_type}'.",) except Exception as e: return ({}, False, f'{e}',) + + +def checkId(basePath: str, file: pathlib.Path) -> bool | str: + """ + Check if we are able to get an id from the file name. + if so check if any video file with the same id exists. + + :param basePath: Base path to strip. + :param file: File to check. + + :return: False if no id found, otherwise the id. + """ + + match = re.search(r'(?<=\[)(?:youtube-)?(?P[a-zA-Z0-9\-_]{11})(?=\])', file.stem, re.IGNORECASE) + if not match: + return False + + id = match.groupdict().get('id') + + for f in file.parent.iterdir(): + if id not in f.stem: + continue + + if f.suffix not in ('.mp4', '.mkv', '.webm', '.m4v', '.m4a', '.mp3', '.aac', '.ogg',): + continue + + return f.absolute() + + return False diff --git a/app/main.py b/app/main.py index dd0c6966..c9869448 100644 --- a/app/main.py +++ b/app/main.py @@ -465,6 +465,8 @@ class Main: download_path=self.config.download_path, file=file ) + if isinstance(text, Response): + return text except Exception as e: return web.HTTPNotFound(reason=str(e)) diff --git a/app/player/Playlist.py b/app/player/Playlist.py index 254fa292..48c96216 100644 --- a/app/player/Playlist.py +++ b/app/player/Playlist.py @@ -6,6 +6,7 @@ from Utils import calcDownloadPath import pathlib from .ffprobe import FFProbe from .Subtitle import Subtitle +from aiohttp.web import Response class Playlist: @@ -14,11 +15,17 @@ class Playlist: def __init__(self, url: str): self.url = url - async def make(self, download_path: str, file: str): + async def make(self, download_path: str, file: str) -> str | Response: rFile = pathlib.Path(calcDownloadPath(basePath=download_path, folder=file, createPath=False)) if not rFile.exists(): - raise Exception(f"File '{rFile}' does not exist.") + possibleFile = self.checkId(download_path, rFile) + if not possibleFile: + raise Exception(f"File '{rFile}' does not exist.") + + return Response(status=302, headers={ + 'Location': f"{self.url}player/playlist/{quote(possibleFile.replace(download_path, '').strip('/'))}.m3u8" + }) try: ffprobe = FFProbe(rFile) From 91c067acb30f6618cdd2b2d8772cf5e67726d50e Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Sun, 3 Nov 2024 18:12:54 +0300 Subject: [PATCH 3/5] Bug fixes. --- app/player/Playlist.py | 4 +-- frontend/package-lock.json | 65 +++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/app/player/Playlist.py b/app/player/Playlist.py index 48c96216..d6132f7d 100644 --- a/app/player/Playlist.py +++ b/app/player/Playlist.py @@ -2,7 +2,7 @@ import glob import pathlib import re from urllib.parse import quote -from Utils import calcDownloadPath +from Utils import calcDownloadPath, checkId import pathlib from .ffprobe import FFProbe from .Subtitle import Subtitle @@ -19,7 +19,7 @@ class Playlist: rFile = pathlib.Path(calcDownloadPath(basePath=download_path, folder=file, createPath=False)) if not rFile.exists(): - possibleFile = self.checkId(download_path, rFile) + possibleFile = checkId(download_path, rFile) if not possibleFile: raise Exception(f"File '{rFile}' does not exist.") diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fe565906..458c98dd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2239,6 +2239,17 @@ "@types/json-schema": "*" } }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2331,9 +2342,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.8.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", - "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "version": "22.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", + "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2435,9 +2446,9 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", "dev": true, "license": "MIT", "dependencies": { @@ -3468,16 +3479,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4164,9 +4165,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001676", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001676.tgz", - "integrity": "sha512-Qz6zwGCiPghQXGJvgQAem79esjitvJ+CxSbSQkW9H/UX5hg8XM88d4lp2W+MEQ81j+Hip58Il+jGVdazk1z9cw==", + "version": "1.0.30001677", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz", + "integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==", "dev": true, "funding": [ { @@ -8174,9 +8175,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz", - "integrity": "sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", "dev": true, "license": "MIT", "dependencies": { @@ -11203,9 +11204,9 @@ "license": "MIT" }, "node_modules/tslib": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", - "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD" }, @@ -11675,19 +11676,19 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.95.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", - "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.5", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", From a672d273d7ec15a76adc811fa32a5d95ed8b03bb Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Sun, 3 Nov 2024 18:23:00 +0300 Subject: [PATCH 4/5] Stringify path for replacement. --- app/player/Playlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/player/Playlist.py b/app/player/Playlist.py index d6132f7d..50983b91 100644 --- a/app/player/Playlist.py +++ b/app/player/Playlist.py @@ -24,7 +24,7 @@ class Playlist: raise Exception(f"File '{rFile}' does not exist.") return Response(status=302, headers={ - 'Location': f"{self.url}player/playlist/{quote(possibleFile.replace(download_path, '').strip('/'))}.m3u8" + 'Location': f"{self.url}player/playlist/{quote(str(possibleFile).replace(download_path, '').strip('/'))}.m3u8" }) try: From 8b85a39180b66910d5e4daecd603f0891df446bd Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Sun, 3 Nov 2024 18:40:48 +0300 Subject: [PATCH 5/5] Added video title to window title if it was present. --- frontend/src/App.vue | 8 ++++++-- frontend/src/components/Video-Player.vue | 13 +++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6b9402e6..0f232833 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -16,7 +16,7 @@ @@ -53,6 +53,7 @@ const socket = ref() const downloading = reactive({}) const completed = reactive({}) const video_link = ref('') +const video_title = ref('') const addForm = useStorage('addForm', true) const showTasks = useStorage('showTasks', false) const cli_output = ref([]) @@ -231,7 +232,10 @@ const addItem = (item) => { }); }; -const playItem = item => video_link.value = makeDownload(config, item, 'm3u8'); +const playItem = item => { + video_link.value = makeDownload(config, item, 'm3u8'); + video_title.value = item.title; +} const reloadWindow = () => window.location.reload(); diff --git a/frontend/src/components/Video-Player.vue b/frontend/src/components/Video-Player.vue index 991bced9..13523a52 100644 --- a/frontend/src/components/Video-Player.vue +++ b/frontend/src/components/Video-Player.vue @@ -77,7 +77,13 @@ onUnmounted(() => { if (hls) { hls.destroy() } + window.removeEventListener('keydown', eventFunc) + + if (props.title) { + window.document.title = 'YTPTube' + } + }) const prepareVideoPlayer = () => { @@ -97,6 +103,7 @@ const prepareVideoPlayer = () => { enabled: true, key: 'plyr' }, + title: props.title, mediaMetadata: { title: props.title }, @@ -118,5 +125,11 @@ const prepareVideoPlayer = () => { if (video.value) { hls.attachMedia(video.value) } + + if (props.title) { + window.document.title = `YTPTube - Playing: ${props.title}` + } + } +