feat: webui
This commit is contained in:
parent
cc57d5c8dc
commit
3fc3f1badd
@ -1,148 +1,9 @@
|
|||||||
import json
|
import sys
|
||||||
import requests
|
from explorers.webui import main as webui_main
|
||||||
from explorers import __version__
|
from explorers.console import main as console_main
|
||||||
from explorers import constants
|
|
||||||
from explorers.utils import logger
|
|
||||||
from ffmpeg_progress_yield import FfmpegProgress
|
|
||||||
from subprocess import Popen, PIPE, STDOUT
|
|
||||||
from shutil import which
|
|
||||||
from tqdm import tqdm
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
VIDEO_PATH = Path("videos")
|
if "--console" in sys.argv:
|
||||||
VIDEO_PATH.mkdir(exist_ok=True)
|
console_main()
|
||||||
ffmpeg_bin: str = None
|
else:
|
||||||
|
webui_main()
|
||||||
def download_m3u8(m3u8_url: str, output_path: Path):
|
|
||||||
ff = FfmpegProgress(
|
|
||||||
[
|
|
||||||
ffmpeg_bin,
|
|
||||||
"-headers",
|
|
||||||
f"User-Agent: {constants.USER_AGENT}",
|
|
||||||
"-headers",
|
|
||||||
"Origin: https://mapstudy.vn",
|
|
||||||
"-headers",
|
|
||||||
"Referer: https://mapstudy.vn/",
|
|
||||||
"-i",
|
|
||||||
m3u8_url,
|
|
||||||
"-c",
|
|
||||||
"copy",
|
|
||||||
str(output_path),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with tqdm(total=100, position=1, desc=output_path.name) as pbar:
|
|
||||||
for progress in ff.run_command_with_progress():
|
|
||||||
pbar.update(progress - pbar.n)
|
|
||||||
except RuntimeError:
|
|
||||||
print("\r", end="")
|
|
||||||
logger.error(f"Failed to download {m3u8_url} to {output_path}: {ff.stderr}")
|
|
||||||
return
|
|
||||||
print("\r", end="")
|
|
||||||
logger.info(f"Downloaded {m3u8_url} to {output_path}.")
|
|
||||||
|
|
||||||
|
|
||||||
def read_config() -> dict:
|
|
||||||
try:
|
|
||||||
with open("config.json", "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.error(
|
|
||||||
"Config file not found, please see 'config.example.json' for an example."
|
|
||||||
)
|
|
||||||
exit(1)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
logger.error(
|
|
||||||
"Config file is not a valid JSON file, please see 'config.example.json' for a valid example."
|
|
||||||
)
|
|
||||||
exit(2)
|
|
||||||
|
|
||||||
|
|
||||||
def loop(config: dict):
|
|
||||||
token = config["token"]
|
|
||||||
client = requests.Session()
|
|
||||||
client.headers.update(
|
|
||||||
{
|
|
||||||
"User-Agent": constants.USER_AGENT,
|
|
||||||
"Origin": "https://mapstudy.vn",
|
|
||||||
"Referer": "https://mapstudy.vn/",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
lesson_input = input("Type the lesson ID/URL to you want to download: ")
|
|
||||||
try:
|
|
||||||
if lesson_input.isdigit():
|
|
||||||
lesson_id = int(lesson_input)
|
|
||||||
else:
|
|
||||||
lesson_id = int(lesson_input.split("/")[-2])
|
|
||||||
except (ValueError, IndexError):
|
|
||||||
logger.error("Invalid lesson ID/URL.")
|
|
||||||
continue
|
|
||||||
print(f"Fetching lesson metadata for lesson ID {lesson_id}...")
|
|
||||||
rsp = client.get(
|
|
||||||
f"{constants.API_URL}/lessons/{lesson_id}",
|
|
||||||
headers={"Authorization": f"Bearer {token}"},
|
|
||||||
)
|
|
||||||
if rsp.status_code != 200:
|
|
||||||
logger.error(f"Failed to fetch lesson metadata: {rsp.text}")
|
|
||||||
continue
|
|
||||||
lesson = rsp.json()["data"]["lesson"]
|
|
||||||
for i, video in enumerate(lesson["videos"]):
|
|
||||||
logger.info(f"Found video {i + 1}: {video['name']}")
|
|
||||||
video_input = input(
|
|
||||||
"Type the video number you want to download (use ',' to specify multiple videos, or 'all' for all videos): "
|
|
||||||
)
|
|
||||||
if video_input.lower() == "all":
|
|
||||||
video_indices = range(len(lesson["videos"]))
|
|
||||||
else:
|
|
||||||
video_indices = [
|
|
||||||
int(x) - 1 for x in video_input.split(",") if x.isdigit()
|
|
||||||
]
|
|
||||||
for i in video_indices:
|
|
||||||
video = lesson["videos"][i]
|
|
||||||
logger.info(f"Downloading video {i + 1}: {video['name']}")
|
|
||||||
rsp = client.get(
|
|
||||||
f"{constants.API_URL}/videos/{video['id']}",
|
|
||||||
headers={"Authorization": f"Bearer {token}"},
|
|
||||||
)
|
|
||||||
if rsp.status_code != 200:
|
|
||||||
logger.error(f"Failed to fetch video download URL: {rsp.text}")
|
|
||||||
continue
|
|
||||||
video_info = rsp.json()["data"]["video"]
|
|
||||||
m3u8_url = video_info["m3u8"]
|
|
||||||
download_m3u8(
|
|
||||||
m3u8_url=m3u8_url,
|
|
||||||
output_path=VIDEO_PATH.joinpath(
|
|
||||||
f"{video['name']} ({lesson_id}-{video['id']}).mp4"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
logger.info("\nExiting...")
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global ffmpeg_bin
|
|
||||||
print(f"Explorers v{__version__}\n")
|
|
||||||
logger.info(
|
|
||||||
"This program is free software, if you bought it then you were scammed."
|
|
||||||
)
|
|
||||||
logger.info("This program comes with absolutely no warranty, use at your own risk.")
|
|
||||||
config = read_config()
|
|
||||||
if config.get("ffmpeg") and config["ffmpeg"] != "":
|
|
||||||
ffmpeg_bin = config["ffmpeg"]
|
|
||||||
else:
|
|
||||||
ffmpeg_bin = which("ffmpeg")
|
|
||||||
if not ffmpeg_bin:
|
|
||||||
logger.error("ffmpeg not found in PATH, please install ffmpeg and add it to your PATH.")
|
|
||||||
exit(3)
|
|
||||||
if not Path(ffmpeg_bin).is_file():
|
|
||||||
logger.error(f"ffmpeg not found at {ffmpeg_bin}, please specify the correct path to ffmpeg in 'config.json'.")
|
|
||||||
exit(4)
|
|
||||||
logger.info("Config loaded.")
|
|
||||||
loop(config)
|
|
||||||
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
31
explorers/api.py
Normal file
31
explorers/api.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import requests
|
||||||
|
from explorers import constants
|
||||||
|
|
||||||
|
|
||||||
|
class API:
|
||||||
|
def __init__(self, token: str | None = None) -> None:
|
||||||
|
self.token = token
|
||||||
|
self._client = requests.Session()
|
||||||
|
self._client.headers.update(
|
||||||
|
{
|
||||||
|
"User-Agent": constants.USER_AGENT,
|
||||||
|
"Origin": "https://mapstudy.vn",
|
||||||
|
"Referer": "https://mapstudy.vn/",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_lesson(self, lesson_id: int) -> dict:
|
||||||
|
rsp = self._client.get(
|
||||||
|
f"{constants.API_URL}/lessons/{lesson_id}",
|
||||||
|
headers={"Authorization": f"Bearer {self.token}"},
|
||||||
|
)
|
||||||
|
rsp.raise_for_status()
|
||||||
|
return rsp.json()["data"]["lesson"]
|
||||||
|
|
||||||
|
def get_video(self, video_id: int) -> dict:
|
||||||
|
rsp = self._client.get(
|
||||||
|
f"{constants.API_URL}/videos/{video_id}",
|
||||||
|
headers={"Authorization": f"Bearer {self.token}"},
|
||||||
|
)
|
||||||
|
rsp.raise_for_status()
|
||||||
|
return rsp.json()["data"]["video"]
|
136
explorers/console.py
Normal file
136
explorers/console.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from explorers import __version__, constants
|
||||||
|
from explorers.api import API
|
||||||
|
from explorers.utils import logger
|
||||||
|
from ffmpeg_progress_yield import FfmpegProgress
|
||||||
|
from shutil import which
|
||||||
|
from tqdm import tqdm
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
VIDEO_PATH = Path("videos")
|
||||||
|
VIDEO_PATH.mkdir(exist_ok=True)
|
||||||
|
api = API()
|
||||||
|
ffmpeg_bin: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def download_m3u8(m3u8_url: str, output_path: Path):
|
||||||
|
ff = FfmpegProgress(
|
||||||
|
[
|
||||||
|
ffmpeg_bin, # type: ignore because FFmpeg at this point should be a string
|
||||||
|
"-headers",
|
||||||
|
f"User-Agent: {constants.USER_AGENT}",
|
||||||
|
"-headers",
|
||||||
|
"Origin: https://mapstudy.vn",
|
||||||
|
"-headers",
|
||||||
|
"Referer: https://mapstudy.vn/",
|
||||||
|
"-i",
|
||||||
|
m3u8_url,
|
||||||
|
"-c",
|
||||||
|
"copy",
|
||||||
|
str(output_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with tqdm(total=100, position=1, desc=output_path.name) as pbar:
|
||||||
|
for progress in ff.run_command_with_progress():
|
||||||
|
pbar.update(progress - pbar.n)
|
||||||
|
except RuntimeError:
|
||||||
|
print("\r", end="")
|
||||||
|
logger.error(f"Failed to download {m3u8_url} to {output_path}: {ff.stderr}")
|
||||||
|
return
|
||||||
|
print("\r", end="")
|
||||||
|
logger.info(f"Downloaded {m3u8_url} to {output_path}.")
|
||||||
|
|
||||||
|
|
||||||
|
def read_config() -> dict:
|
||||||
|
try:
|
||||||
|
with open("config.json", "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error(
|
||||||
|
"Config file not found, please see 'config.example.json' for an example."
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.error(
|
||||||
|
"Config file is not a valid JSON file, please see 'config.example.json' for a valid example."
|
||||||
|
)
|
||||||
|
exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
def loop(config: dict):
|
||||||
|
api.token = config["token"]
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
lesson_input = input("Type the lesson ID/URL to you want to download: ")
|
||||||
|
try:
|
||||||
|
if lesson_input.isdigit():
|
||||||
|
lesson_id = int(lesson_input)
|
||||||
|
else:
|
||||||
|
lesson_id = int(lesson_input.split("/")[-2])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
logger.error("Invalid lesson ID/URL.")
|
||||||
|
continue
|
||||||
|
print(f"Fetching lesson metadata for lesson ID {lesson_id}...")
|
||||||
|
try:
|
||||||
|
lesson = api.get_lesson(lesson_id)
|
||||||
|
except (requests.HTTPError, json.JSONDecodeError) as e:
|
||||||
|
logger.error(f"Failed to fetch lesson metadata: {e}")
|
||||||
|
continue
|
||||||
|
for i, video in enumerate(lesson["videos"]):
|
||||||
|
logger.info(f"Found video {i + 1}: {video['name']}")
|
||||||
|
video_input = input(
|
||||||
|
"Type the video number you want to download (use ',' to specify multiple videos, or 'all' for all videos): "
|
||||||
|
)
|
||||||
|
if video_input.lower() == "all":
|
||||||
|
video_indices = range(len(lesson["videos"]))
|
||||||
|
else:
|
||||||
|
video_indices = [
|
||||||
|
int(x) - 1 for x in video_input.split(",") if x.isdigit()
|
||||||
|
]
|
||||||
|
for i in video_indices:
|
||||||
|
video = lesson["videos"][i]
|
||||||
|
logger.info(f"Downloading video {i + 1}: {video['name']}")
|
||||||
|
try:
|
||||||
|
video_info = api.get_video(video["id"])
|
||||||
|
except (requests.HTTPError, json.JSONDecodeError) as e:
|
||||||
|
logger.error(f"Failed to fetch lesson metadata: {e}")
|
||||||
|
continue
|
||||||
|
m3u8_url = video_info["m3u8"]
|
||||||
|
download_m3u8(
|
||||||
|
m3u8_url=m3u8_url,
|
||||||
|
output_path=VIDEO_PATH.joinpath(
|
||||||
|
f"{video['name']} ({lesson_id}-{video['id']}).mp4"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("\nExiting...")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global ffmpeg_bin
|
||||||
|
print(f"Explorers v{__version__}\n")
|
||||||
|
logger.info(
|
||||||
|
"This program is free software, if you bought it then you were scammed."
|
||||||
|
)
|
||||||
|
logger.info("This program comes with absolutely no warranty, use at your own risk.")
|
||||||
|
config = read_config()
|
||||||
|
if config.get("ffmpeg") and config["ffmpeg"] != "":
|
||||||
|
ffmpeg_bin = config["ffmpeg"]
|
||||||
|
else:
|
||||||
|
ffmpeg_bin = which("ffmpeg")
|
||||||
|
if not ffmpeg_bin:
|
||||||
|
logger.error(
|
||||||
|
"ffmpeg not found in PATH, please install ffmpeg and add it to your PATH."
|
||||||
|
)
|
||||||
|
exit(3)
|
||||||
|
if not Path(ffmpeg_bin).is_file():
|
||||||
|
logger.error(
|
||||||
|
f"ffmpeg not found at {ffmpeg_bin}, please specify the correct path to ffmpeg in 'config.json'."
|
||||||
|
)
|
||||||
|
exit(4)
|
||||||
|
logger.info("Config loaded.")
|
||||||
|
loop(config)
|
78
explorers/webui.py
Normal file
78
explorers/webui.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
from shutil import which
|
||||||
|
from ffmpeg_progress_yield import FfmpegProgress
|
||||||
|
from explorers import constants
|
||||||
|
from explorers.api import API
|
||||||
|
from flask import Flask, send_from_directory, request
|
||||||
|
from urllib.parse import unquote
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
api = API()
|
||||||
|
app = Flask(__name__)
|
||||||
|
ffmpeg_bin = which("ffmpeg")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/v1/lessons/<int:lesson_id>")
|
||||||
|
def get_lesson(lesson_id):
|
||||||
|
return api.get_lesson(lesson_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/v1/download-video")
|
||||||
|
def download_video():
|
||||||
|
video_url = request.args.get("url")
|
||||||
|
output_path = request.args.get("output")
|
||||||
|
if not video_url or not output_path:
|
||||||
|
return "Missing 'url' or 'output' query parameter.", 400
|
||||||
|
video_url = unquote(video_url)
|
||||||
|
output_path = unquote(output_path)
|
||||||
|
ff = FfmpegProgress(
|
||||||
|
[
|
||||||
|
ffmpeg_bin, # type: ignore because FFmpeg at this point should be a string
|
||||||
|
"-headers",
|
||||||
|
f"User-Agent: {constants.USER_AGENT}",
|
||||||
|
"-headers",
|
||||||
|
"Origin: https://mapstudy.vn",
|
||||||
|
"-headers",
|
||||||
|
"Referer: https://mapstudy.vn/",
|
||||||
|
"-i",
|
||||||
|
video_url,
|
||||||
|
"-c",
|
||||||
|
"copy",
|
||||||
|
str(output_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def generate():
|
||||||
|
for progress in ff.run_command_with_progress():
|
||||||
|
yield str(progress)
|
||||||
|
print(progress)
|
||||||
|
return generate()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/v1/videos/<int:video_id>")
|
||||||
|
def get_video(video_id):
|
||||||
|
return api.get_video(video_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/v1/token", methods=["POST"])
|
||||||
|
def set_token():
|
||||||
|
# set token from query parameter
|
||||||
|
token = request.data.decode("utf-8")
|
||||||
|
api.token = token
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/assets/<path:filename>")
|
||||||
|
def get_assets(filename):
|
||||||
|
# Ensure the filename is secure to prevent potential security issues
|
||||||
|
filename = secure_filename(filename)
|
||||||
|
# Use send_from_directory to send the static file to the client
|
||||||
|
# as_attachment=False means the file will be displayed rather than downloaded
|
||||||
|
return send_from_directory("../web/assets/", filename, as_attachment=False)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def root():
|
||||||
|
return send_from_directory("../web/", "index.html")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app.run(host="0.0.0.0", port=8080)
|
163
poetry.lock
generated
163
poetry.lock
generated
@ -1,5 +1,16 @@
|
|||||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blinker"
|
||||||
|
version = "1.7.0"
|
||||||
|
description = "Fast, simple object-to-object and broadcast signaling"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9"},
|
||||||
|
{file = "blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2024.2.2"
|
version = "2024.2.2"
|
||||||
@ -110,6 +121,20 @@ files = [
|
|||||||
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
|
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.7"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||||
|
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@ -149,6 +174,28 @@ files = [
|
|||||||
{file = "ffmpeg_progress_yield-0.7.8-py2.py3-none-any.whl", hash = "sha256:732ef90d78f8b5c0b78be289050589299d128d03063be55a9deab80d67319a21"},
|
{file = "ffmpeg_progress_yield-0.7.8-py2.py3-none-any.whl", hash = "sha256:732ef90d78f8b5c0b78be289050589299d128d03063be55a9deab80d67319a21"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask"
|
||||||
|
version = "3.0.2"
|
||||||
|
description = "A simple framework for building complex web applications."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "flask-3.0.2-py3-none-any.whl", hash = "sha256:3232e0e9c850d781933cf0207523d1ece087eb8d87b23777ae38456e2fbe7c6e"},
|
||||||
|
{file = "flask-3.0.2.tar.gz", hash = "sha256:822c03f4b799204250a7ee84b1eddc40665395333973dfb9deebfe425fefcb7d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
blinker = ">=1.6.2"
|
||||||
|
click = ">=8.1.3"
|
||||||
|
itsdangerous = ">=2.1.2"
|
||||||
|
Jinja2 = ">=3.1.2"
|
||||||
|
Werkzeug = ">=3.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
async = ["asgiref (>=3.2)"]
|
||||||
|
dotenv = ["python-dotenv"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.6"
|
version = "3.6"
|
||||||
@ -160,6 +207,103 @@ files = [
|
|||||||
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itsdangerous"
|
||||||
|
version = "2.1.2"
|
||||||
|
description = "Safely pass data to untrusted environments and back."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
|
||||||
|
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jinja2"
|
||||||
|
version = "3.1.3"
|
||||||
|
description = "A very fast and expressive template engine."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
|
||||||
|
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
MarkupSafe = ">=2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
i18n = ["Babel (>=2.7)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "2.1.5"
|
||||||
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
|
||||||
|
{file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
|
||||||
|
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.31.0"
|
version = "2.31.0"
|
||||||
@ -218,7 +362,24 @@ h2 = ["h2 (>=4,<5)"]
|
|||||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
zstd = ["zstandard (>=0.18.0)"]
|
zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "werkzeug"
|
||||||
|
version = "3.0.1"
|
||||||
|
description = "The comprehensive WSGI web application library."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"},
|
||||||
|
{file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
MarkupSafe = ">=2.1.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
watchdog = ["watchdog (>=2.3)"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "1eea5ea5c4bb1723e6c81d524bbd7d4ba8d2c7e52de45ce26d6330791e291558"
|
content-hash = "e25ec01d4e3b486f870cbee97713b2237c87f402b50c47588adb2fad31fdd1b0"
|
||||||
|
@ -12,6 +12,7 @@ requests = "^2.31.0"
|
|||||||
colorlog = "^6.8.2"
|
colorlog = "^6.8.2"
|
||||||
ffmpeg-progress-yield = "^0.7.8"
|
ffmpeg-progress-yield = "^0.7.8"
|
||||||
tqdm = "^4.66.2"
|
tqdm = "^4.66.2"
|
||||||
|
flask = "^3.0.2"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
123
web/assets/index.js
Normal file
123
web/assets/index.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
const tokenInput = document.querySelector('#token');
|
||||||
|
const rememberTokenCheckbox = document.querySelector('#remember-token');
|
||||||
|
const setTokenButton = document.querySelector('#set-token');
|
||||||
|
const lessonInput = document.querySelector('#lesson-query');
|
||||||
|
const getLessonButton = document.querySelector('#get-lesson');
|
||||||
|
let token = localStorage.getItem('token') || '';
|
||||||
|
|
||||||
|
if (token !== '') {
|
||||||
|
tokenInput.value = token;
|
||||||
|
rememberTokenCheckbox.checked = true;
|
||||||
|
fetch("/api/v1/token", {
|
||||||
|
method: 'POST',
|
||||||
|
body: token,
|
||||||
|
}).then(rsp => {
|
||||||
|
if (rsp.status !== 200) {
|
||||||
|
setTokenButton.innerHTML = `:( (${rsp.status})`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomString(length) {
|
||||||
|
return `a${Math.random().toString(36).slice(2, length + 2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// React simulator
|
||||||
|
function getResult(id) {
|
||||||
|
const element = document.createElement("div");
|
||||||
|
element.className = "block";
|
||||||
|
document.body.appendChild(document.createElement("br"));
|
||||||
|
document.body.appendChild(element);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTokenButton.addEventListener('click', async () => {
|
||||||
|
token = tokenInput.value;
|
||||||
|
const rsp = await fetch("/api/v1/token", {
|
||||||
|
method: 'POST',
|
||||||
|
body: token,
|
||||||
|
});
|
||||||
|
if (rsp.status !== 200) {
|
||||||
|
setTokenButton.innerHTML = `:( (${rsp.status})`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rememberTokenCheckbox.checked) {
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getLessonButton.addEventListener('click', async () => {
|
||||||
|
const input = lessonInput.value;
|
||||||
|
let id = Number.parseInt(input);
|
||||||
|
if (Number.isNaN(id)) {
|
||||||
|
const url = input.split('/');
|
||||||
|
url.pop();
|
||||||
|
id = Number.parseInt(url.pop());
|
||||||
|
}
|
||||||
|
const rsp = await fetch(`/api/v1/lessons/${id}`);
|
||||||
|
const result = getResult("lesson");
|
||||||
|
const data = await rsp.json();
|
||||||
|
const ids = {};
|
||||||
|
// React :nerd:
|
||||||
|
result.innerHTML = "<h2>Lesson Information</h2>\n";
|
||||||
|
result.innerHTML += `Name: ${decodeURIComponent(data.name)}`;
|
||||||
|
result.innerHTML += "<h3>Videos available</h3>";
|
||||||
|
for (const video of data.videos) {
|
||||||
|
const uniqueId = randomString(15);
|
||||||
|
ids[uniqueId] = video;
|
||||||
|
result.innerHTML += `
|
||||||
|
<input type="checkbox" id="${uniqueId}" name="${uniqueId}" value="${uniqueId}">
|
||||||
|
<label for="${uniqueId}">${decodeURIComponent(video.name)}</label>
|
||||||
|
<br>`;
|
||||||
|
}
|
||||||
|
let videoDlResult = null;
|
||||||
|
const downloadSelectedButton = document.createElement("button");
|
||||||
|
downloadSelectedButton.innerHTML = "Download Selected";
|
||||||
|
downloadSelectedButton.addEventListener('click', async () => {
|
||||||
|
for (const [id, video] of Object.entries(ids)) {
|
||||||
|
const checkbox = document.querySelector(`#${id}`);
|
||||||
|
if (!checkbox.checked) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const videoRsp = await fetch(`/api/v1/videos/${video.id}`);
|
||||||
|
if (videoRsp.status !== 200) {
|
||||||
|
console.error(`Failed to get video ${video.name}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const videoData = await videoRsp.json();
|
||||||
|
if (videoDlResult === null) {
|
||||||
|
result.appendChild(document.createElement("br"));
|
||||||
|
result.appendChild(document.createElement("br"));
|
||||||
|
videoDlResult = document.createElement("div");
|
||||||
|
videoDlResult.className = "block";
|
||||||
|
result.appendChild(videoDlResult);
|
||||||
|
}
|
||||||
|
const progressId = randomString(15);
|
||||||
|
const progressStrId = randomString(15);
|
||||||
|
videoDlResult.innerHTML += `<span>${video.name}.mp4 <progress id="${progressId}" value="0" max="100"></progress> <span id="${progressStrId}">0%</span></span>`
|
||||||
|
const rsp = await fetch(`/api/v1/download-video?${new URLSearchParams({
|
||||||
|
url: encodeURIComponent(videoData.m3u8),
|
||||||
|
output: encodeURIComponent(`videos/${video.name}.mp4`),
|
||||||
|
})}`);
|
||||||
|
const reader = rsp.body.getReader();
|
||||||
|
while (true) {
|
||||||
|
// wait for next encoded chunk
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
// check if stream is done
|
||||||
|
if (done) break;
|
||||||
|
// Decodes data chunk and yields it
|
||||||
|
const progress = Math.floor(Number.parseFloat(new TextDecoder().decode(value)));
|
||||||
|
document.querySelector(`#${progressId}`).value = progress;
|
||||||
|
document.querySelector(`#${progressStrId}`).innerText = `${progress}%`;
|
||||||
|
}
|
||||||
|
if (rsp.status !== 200) {
|
||||||
|
console.error(`Failed to download ${video.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
result.appendChild(downloadSelectedButton);
|
||||||
|
// result.innerHTML += "<button>Download All</button>";
|
||||||
|
|
||||||
|
});
|
61
web/assets/style.css
Normal file
61
web/assets/style.css
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: "Be Vietnam Pro", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark {
|
||||||
|
background-color: #303030;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
font-size: 32px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #ffffff50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
width: fit-content;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 5px;
|
||||||
|
background-color: #303030;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition-duration: 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="password"],
|
||||||
|
input[type="text"] {
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
background-color: #303030;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: #303030;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
}
|
52
web/index.html
Normal file
52
web/index.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/style.css">
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
|
||||||
|
</head>
|
||||||
|
<body class="theme-dark">
|
||||||
|
<header>
|
||||||
|
Explorers
|
||||||
|
</header>
|
||||||
|
<br>
|
||||||
|
<div class="main">
|
||||||
|
<div class="block">
|
||||||
|
Token
|
||||||
|
<input id="token" type="password" name="token" placeholder="Put your token here">
|
||||||
|
<button id="set-token">Set</button>
|
||||||
|
<br>
|
||||||
|
<input type="checkbox" id="remember-token" name="remember-token" value="remember-token">
|
||||||
|
<label for="remember-token">Remember this token</label>
|
||||||
|
<br>
|
||||||
|
<button>How do I get token?</button>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="block">
|
||||||
|
Lesson ID/URL
|
||||||
|
<input type="text" id="lesson-query" name="query-lesson">
|
||||||
|
<button id="get-lesson">Get</button>
|
||||||
|
</div>
|
||||||
|
<!-- <br>
|
||||||
|
<div class="block">
|
||||||
|
<h2>Lesson Information</h1>
|
||||||
|
Name: Test
|
||||||
|
<h3>Videos available</h3>
|
||||||
|
<input type="checkbox" id="lesson-video-1" name="lesson-video-1" value="remember-token">
|
||||||
|
<label for="lesson-video-2">Video 1</label>
|
||||||
|
<br>
|
||||||
|
<input type="checkbox" id="lesson-video-1" name="lesson-video-2" value="remember-token">
|
||||||
|
<label for="lesson-video-2">Video 2</label>
|
||||||
|
<br>
|
||||||
|
<button>Download Selected</button>
|
||||||
|
<button>Download All</button>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<div class="block">
|
||||||
|
<span>Video 1 <progress id="file" value="32" max="100"></progress> 32%</span>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<script src="assets/index.js"></script>
|
||||||
|
</body>
|
Loading…
Reference in New Issue
Block a user