better fallback if file was renamed and retains the same ID

This commit is contained in:
ArabCoders 2025-03-01 20:04:28 +03:00
parent f0836ddd77
commit ea98562944
6 changed files with 184 additions and 141 deletions

View file

@ -11,6 +11,7 @@ import uuid
from collections.abc import Awaitable from collections.abc import Awaitable
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from urllib.parse import quote
import anyio import anyio
import httpx import httpx
@ -37,7 +38,7 @@ from .Utils import (
IGNORED_KEYS, IGNORED_KEYS,
StreamingError, StreamingError,
arg_converter, arg_converter,
calc_download_path, get_file,
get_mime_type, get_mime_type,
get_sidecar_subtitles, get_sidecar_subtitles,
get_video_info, get_video_info,
@ -119,7 +120,7 @@ class HttpAPI(Common):
HttpAPI: The instance of the HttpAPI. HttpAPI: The instance of the HttpAPI.
""" """
app.middlewares.append(HttpAPI.middle_wares()) app.middlewares.append(HttpAPI.middle_wares(download_path=self.config.download_path))
if self.config.auth_username and self.config.auth_password: if self.config.auth_username and self.config.auth_password:
app.middlewares.append(HttpAPI.basic_auth(self.config.auth_username, self.config.auth_password)) app.middlewares.append(HttpAPI.basic_auth(self.config.auth_username, self.config.auth_password))
@ -312,9 +313,19 @@ class HttpAPI(Common):
return middleware_handler return middleware_handler
@staticmethod @staticmethod
def middle_wares() -> Awaitable: def middle_wares(download_path: str) -> Awaitable:
@web.middleware @web.middleware
async def middleware_handler(request: Request, handler: RequestHandler) -> Response: async def middleware_handler(request: Request, handler: RequestHandler) -> Response:
if request.path.startswith("/api/download"):
realFile, status = get_file(download_path=download_path, file=request.path.replace("/api/download/", ""))
if web.HTTPFound.status_code == status:
return Response(
status=status,
headers={
"Location": f"/api/download/{quote(str(realFile).replace(download_path, '').strip('/'))}"
},
)
response = await handler(request) response = await handler(request)
if isinstance(response, web.FileResponse): if isinstance(response, web.FileResponse):
@ -918,25 +929,33 @@ class HttpAPI(Common):
file: str = request.match_info.get("file") file: str = request.match_info.get("file")
if not file: if not file:
raise web.HTTPBadRequest(text="file is required.") return web.json_response(data={"error": "file is required"}, status=web.HTTPBadRequest.status_code)
try: try:
text = await Playlist(url="/").make(download_path=self.config.download_path, file=file) realFile, status = get_file(download_path=self.config.download_path, file=file)
if isinstance(text, Response): if web.HTTPFound.status_code == status:
return text return Response(
status=web.HTTPFound.status_code,
headers={
"Location": f"/api/player/playlist/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}.m3u8"
},
)
if web.HTTPNotFound.status_code == status:
return web.json_response(data={"error": f"File '{file}' does not exist."}, status=status)
return web.Response(
text=await Playlist(download_path=self.config.download_path, url="/").make(file=realFile),
headers={
"Content-Type": "application/x-mpegURL",
"Cache-Control": "no-cache",
"Access-Control-Max-Age": "300",
},
status=web.HTTPOk.status_code,
)
except StreamingError as e: except StreamingError as e:
return web.json_response(data={"error": str(e)}, status=web.HTTPNotFound.status_code) return web.json_response(data={"error": str(e)}, status=web.HTTPNotFound.status_code)
return web.Response(
text=text,
headers={
"Content-Type": "application/x-mpegURL",
"Cache-Control": "no-cache",
"Access-Control-Max-Age": "300",
},
status=web.HTTPOk.status_code,
)
@route("GET", "api/player/m3u8/{mode}/{file:.*}.m3u8") @route("GET", "api/player/m3u8/{mode}/{file:.*}.m3u8")
async def m3u8(self, request: Request) -> Response: async def m3u8(self, request: Request) -> Response:
""" """
@ -969,11 +988,24 @@ class HttpAPI(Common):
duration = float(duration) duration = float(duration)
try: try:
cls = M3u8("/") cls = M3u8(download_path=self.config.download_path, url="/")
realFile, status = get_file(download_path=self.config.download_path, file=file)
if web.HTTPFound.status_code == status:
return Response(
status=status,
headers={
"Location": f"/api/player/m3u8/{mode}/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}.m3u8"
},
)
if web.HTTPNotFound.status_code == status:
return web.json_response(data={"error": f"File '{file}' does not exist."}, status=status)
if "subtitle" in mode: if "subtitle" in mode:
text = await cls.make_subtitle(self.config.download_path, file, duration) text = await cls.make_subtitle(file=realFile, duration=duration)
else: else:
text = await cls.make_stream(self.config.download_path, file) text = await cls.make_stream(file=realFile)
except StreamingError as e: except StreamingError as e:
LOG.exception(e) LOG.exception(e)
return web.json_response(data={"error": str(e)}, status=web.HTTPNotFound.status_code) return web.json_response(data={"error": str(e)}, status=web.HTTPNotFound.status_code)
@ -1005,16 +1037,6 @@ class HttpAPI(Common):
sd: int = request.query.get("sd") sd: int = request.query.get("sd")
vc: int = int(request.query.get("vc", 0)) vc: int = int(request.query.get("vc", 0))
ac: int = int(request.query.get("ac", 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):
return web.json_response(data={"error": "Invalid file path."}, status=web.HTTPBadRequest.status_code)
if request.if_modified_since:
lastMod = time.strftime(
"%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(os.path.getmtime(file_path), tz=UTC).timetuple()
)
if os.path.exists(file_path) and request.if_modified_since.timestamp() == os.path.getmtime(file_path):
return web.Response(status=web.HTTPNotModified.status_code, headers={"Last-Modified": lastMod})
if not file: if not file:
return web.json_response(data={"error": "file is required"}, status=web.HTTPBadRequest.status_code) return web.json_response(data={"error": "file is required"}, status=web.HTTPBadRequest.status_code)
@ -1022,7 +1044,26 @@ class HttpAPI(Common):
if not segment: if not segment:
return web.json_response(data={"error": "segment id is required."}, status=web.HTTPBadRequest.status_code) return web.json_response(data={"error": "segment id is required."}, status=web.HTTPBadRequest.status_code)
realFile, status = get_file(download_path=self.config.download_path, file=file)
if web.HTTPFound.status_code == status:
return Response(
status=status,
headers={
"Location": f"/api/player/segments/{segment}/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}.ts"
},
)
if web.HTTPNotFound.status_code == status:
return web.json_response(data={"error": f"File '{file}' does not exist."}, status=status)
mtime = realFile.stat().st_mtime
if request.if_modified_since and request.if_modified_since.timestamp() == mtime:
lastMod = time.strftime("%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(mtime, tz=UTC).timetuple())
return web.Response(status=web.HTTPNotModified.status_code, headers={"Last-Modified": lastMod})
segmenter = Segments( segmenter = Segments(
download_path=self.config.download_path,
index=int(segment), index=int(segment),
duration=float(f"{float(sd if sd else M3u8.duration):.6f}"), duration=float(f"{float(sd if sd else M3u8.duration):.6f}"),
vconvert=vc == 1, vconvert=vc == 1,
@ -1030,7 +1071,7 @@ class HttpAPI(Common):
) )
return web.Response( return web.Response(
body=await segmenter.stream(path=self.config.download_path, file=file), body=await segmenter.stream(file=realFile),
headers={ headers={
"Content-Type": "video/mpegts", "Content-Type": "video/mpegts",
"X-Accel-Buffering": "no", "X-Accel-Buffering": "no",
@ -1038,7 +1079,7 @@ class HttpAPI(Common):
"Pragma": "public", "Pragma": "public",
"Cache-Control": f"public, max-age={time.time() + 31536000}", "Cache-Control": f"public, max-age={time.time() + 31536000}",
"Last-Modified": time.strftime( "Last-Modified": time.strftime(
"%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(os.path.getmtime(file_path), tz=UTC).timetuple() "%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(mtime, tz=UTC).timetuple()
), ),
"Expires": time.strftime( "Expires": time.strftime(
"%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(time.time() + 31536000, tz=UTC).timetuple() "%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(time.time() + 31536000, tz=UTC).timetuple()
@ -1060,22 +1101,30 @@ class HttpAPI(Common):
""" """
file: str = request.match_info.get("file") 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):
return web.json_response(data={"error": "Invalid file path."}, status=web.HTTPBadRequest.status_code)
if request.if_modified_since:
lastMod = time.strftime(
"%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(os.path.getmtime(file_path), tz=UTC).timetuple()
)
if os.path.exists(file_path) and request.if_modified_since.timestamp() == os.path.getmtime(file_path):
return web.Response(status=web.HTTPNotModified.status_code, headers={"Last-Modified": lastMod})
if not file: if not file:
return web.json_response(data={"error": "file is required"}, status=web.HTTPBadRequest.status_code) return web.json_response(data={"error": "file is required"}, status=web.HTTPBadRequest.status_code)
realFile, status = get_file(download_path=self.config.download_path, file=file)
if web.HTTPFound.status_code == status:
return Response(
status=status,
headers={
"Location": f"/api/player/subtitle/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}.vtt"
},
)
if web.HTTPNotFound.status_code == status:
return web.json_response(data={"error": f"File '{file}' does not exist."}, status=status)
mtime = realFile.stat().st_mtime
if request.if_modified_since and request.if_modified_since.timestamp() == mtime:
lastMod = time.strftime("%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(mtime, tz=UTC).timetuple())
return web.Response(status=web.HTTPNotModified.status_code, headers={"Last-Modified": lastMod})
return web.Response( return web.Response(
body=await Subtitle().make(path=self.config.download_path, file=file), body=await Subtitle(download_path=self.config.download_path).make(file=realFile),
headers={ headers={
"Content-Type": "text/vtt; charset=UTF-8", "Content-Type": "text/vtt; charset=UTF-8",
"X-Accel-Buffering": "no", "X-Accel-Buffering": "no",
@ -1083,7 +1132,7 @@ class HttpAPI(Common):
"Pragma": "public", "Pragma": "public",
"Cache-Control": f"public, max-age={time.time() + 31536000}", "Cache-Control": f"public, max-age={time.time() + 31536000}",
"Last-Modified": time.strftime( "Last-Modified": time.strftime(
"%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(os.path.getmtime(file_path), tz=UTC).timetuple() "%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(mtime, tz=UTC).timetuple()
), ),
"Expires": time.strftime( "Expires": time.strftime(
"%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(time.time() + 31536000, tz=UTC).timetuple() "%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(time.time() + 31536000, tz=UTC).timetuple()
@ -1187,12 +1236,18 @@ class HttpAPI(Common):
return web.json_response(data={"error": "file is required."}, status=web.HTTPBadRequest.status_code) return web.json_response(data={"error": "file is required."}, status=web.HTTPBadRequest.status_code)
try: try:
realFile: str = calc_download_path(base_path=self.config.download_path, folder=file, create_path=False) realFile, status = get_file(download_path=self.config.download_path, file=file)
if not os.path.exists(realFile) or not os.path.isfile(realFile): if web.HTTPFound.status_code == status:
return web.json_response( return Response(
data={"error": f"File '{file}' does not exist."}, status=web.HTTPNotFound.status_code status=web.HTTPFound.status_code,
headers={
"Location": f"/api/file/ffprobe/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}"
},
) )
if web.HTTPNotFound.status_code == status:
return web.json_response(data={"error": f"File '{file}' does not exist."}, status=status)
return web.json_response( return web.json_response(
data=await ffprobe(realFile), status=web.HTTPOk.status_code, dumps=self.encoder.encode data=await ffprobe(realFile), status=web.HTTPOk.status_code, dumps=self.encoder.encode
) )
@ -1216,13 +1271,17 @@ class HttpAPI(Common):
return web.json_response(data={"error": "file is required."}, status=web.HTTPBadRequest.status_code) return web.json_response(data={"error": "file is required."}, status=web.HTTPBadRequest.status_code)
try: try:
realFile: str = calc_download_path(base_path=self.config.download_path, folder=file, create_path=False) realFile, status = get_file(download_path=self.config.download_path, file=file)
if not os.path.exists(realFile) or not os.path.isfile(realFile): if web.HTTPFound.status_code == status:
return web.json_response( return Response(
data={"error": f"File '{file}' does not exist."}, status=web.HTTPNotFound.status_code status=web.HTTPFound.status_code,
headers={
"Location": f"/api/file/info/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}"
},
) )
realFile = Path(realFile) if web.HTTPNotFound.status_code == status:
return web.json_response(data={"error": f"File '{file}' does not exist."}, status=status)
ff_info = await ffprobe(realFile) ff_info = await ffprobe(realFile)

View file

@ -1,9 +1,9 @@
import math import math
import os from pathlib import Path
from urllib.parse import quote from urllib.parse import quote
from .ffprobe import ffprobe from .ffprobe import ffprobe
from .Utils import StreamingError, calc_download_path from .Utils import StreamingError
class M3u8: class M3u8:
@ -12,24 +12,21 @@ class M3u8:
ok_vcodecs: tuple = ("h264", "x264", "avc") ok_vcodecs: tuple = ("h264", "x264", "avc")
ok_acodecs: tuple = ("aac", "m4a", "mp3") ok_acodecs: tuple = ("aac", "m4a", "mp3")
def __init__(self, url: str, segment_duration: float | None = None): def __init__(self, download_path: str, url: str, segment_duration: float | None = None):
self.url = url self.url = url
self.download_path = download_path
self.duration = float(segment_duration) if segment_duration is not None else self.duration self.duration = float(segment_duration) if segment_duration is not None else self.duration
async def make_stream(self, download_path: str, file: str) -> str: async def make_stream(self, file: Path) -> str:
realFile: str = calc_download_path(base_path=download_path, folder=file, create_path=False)
if not os.path.exists(realFile):
error = f"File '{realFile}' does not exist."
raise StreamingError(error)
try: try:
ff = await ffprobe(realFile) ff = await ffprobe(file)
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
file = str(file).replace(self.download_path, "").strip("/")
if "duration" not in ff.metadata: if "duration" not in ff.metadata:
error = f"Unable to get '{realFile}' play duration." error = f"Unable to get '{file}' play duration."
raise StreamingError(error) raise StreamingError(error)
duration: float = float(ff.metadata.get("duration")) duration: float = float(ff.metadata.get("duration"))
@ -70,13 +67,7 @@ class M3u8:
return "\n".join(m3u8) return "\n".join(m3u8)
async def make_subtitle(self, download_path: str, file: str, duration: float) -> str: async def make_subtitle(self, file: Path, duration: float) -> str:
realFile: str = calc_download_path(base_path=download_path, folder=file, create_path=False)
if not os.path.exists(realFile):
error = f"File '{realFile}' does not exist."
raise StreamingError(error)
m3u8 = [] m3u8 = []
m3u8.append("#EXTM3U") m3u8.append("#EXTM3U")
@ -85,7 +76,7 @@ class M3u8:
m3u8.append("#EXT-X-MEDIA-SEQUENCE:0") m3u8.append("#EXT-X-MEDIA-SEQUENCE:0")
m3u8.append("#EXT-X-PLAYLIST-TYPE:VOD") m3u8.append("#EXT-X-PLAYLIST-TYPE:VOD")
m3u8.append(f"#EXTINF:{duration},") m3u8.append(f"#EXTINF:{duration},")
m3u8.append(f"{self.url}api/player/subtitle/{quote(file)}.vtt") m3u8.append(f"{self.url}api/player/subtitle/{quote(str(file).replace(self.download_path, '').strip('/'))}.vtt")
m3u8.append("#EXT-X-ENDLIST") m3u8.append("#EXT-X-ENDLIST")
return "\n".join(m3u8) return "\n".join(m3u8)

View file

@ -1,41 +1,27 @@
from pathlib import Path from pathlib import Path
from urllib.parse import quote from urllib.parse import quote
from aiohttp.web import HTTPFound, Response
from .ffprobe import ffprobe from .ffprobe import ffprobe
from .Utils import StreamingError, calc_download_path, check_id, get_sidecar_subtitles from .Utils import StreamingError, get_sidecar_subtitles
class Playlist: class Playlist:
_url: str = None _url: str = None
def __init__(self, url: str): def __init__(self, download_path: str, url: str):
self.url = url self.url = url
self.download_path = download_path
async def make(self, download_path: str, file: str) -> str | Response: async def make(self, file: Path) -> str:
rFile = Path(calc_download_path(base_path=download_path, folder=file, create_path=False)) ref = str(file).replace(self.download_path, "").strip("/")
if not rFile.exists():
possibleFile = check_id(file=rFile)
if not possibleFile:
msg = f"File '{rFile}' does not exist."
raise StreamingError(msg)
return Response(
status=HTTPFound.status_code,
headers={
"Location": f"{self.url}api/player/playlist/{quote(str(possibleFile).replace(download_path, '').strip('/'))}.m3u8"
},
)
try: try:
ff = await ffprobe(rFile) ff = await ffprobe(file)
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
if "duration" not in ff.metadata: if "duration" not in ff.metadata:
msg = f"Unable to get '{rFile}' duration." msg = f"Unable to get '{ref}' duration."
raise StreamingError(msg) raise StreamingError(msg)
playlist = [] playlist = []
@ -44,18 +30,18 @@ class Playlist:
subs = "" subs = ""
duration: float = float(ff.metadata.get("duration")) duration: float = float(ff.metadata.get("duration"))
for sub_file in get_sidecar_subtitles(rFile): for sub_file in get_sidecar_subtitles(file):
lang = sub_file["lang"] lang = sub_file["lang"]
item = sub_file["file"] item = sub_file["file"]
name = sub_file["name"] name = sub_file["name"]
subs = ',SUBTITLES="subs"' subs = ',SUBTITLES="subs"'
url = f"{self.url}api/player/m3u8/subtitle/{quote(str(Path(file).with_name(item.name)))}.m3u8?duration={duration}" url = f"{self.url}api/player/m3u8/subtitle/{quote(str(Path(ref).with_name(item.name)))}.m3u8?duration={duration}"
playlist.append( playlist.append(
f'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="{name}",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE="{lang}",URI="{url}"' f'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="{name}",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE="{lang}",URI="{url}"'
) )
playlist.append(f"#EXT-X-STREAM-INF:PROGRAM-ID=1{subs}") playlist.append(f"#EXT-X-STREAM-INF:PROGRAM-ID=1{subs}")
playlist.append(f"{self.url}api/player/m3u8/video/{quote(file)}.m3u8") playlist.append(f"{self.url}api/player/m3u8/video/{quote(ref)}.m3u8")
return "\n".join(playlist) return "\n".join(playlist)

View file

@ -3,17 +3,19 @@ import hashlib
import logging import logging
import os import os
import tempfile import tempfile
from pathlib import Path
from .config import Config from .config import Config
from .ffprobe import ffprobe from .ffprobe import ffprobe
from .Utils import StreamingError, calc_download_path from .Utils import StreamingError
LOG = logging.getLogger("player.segments") LOG = logging.getLogger("player.segments")
class Segments: class Segments:
def __init__(self, index: int, duration: float, vconvert: bool, aconvert: bool): def __init__(self, download_path: str, index: int, duration: float, vconvert: bool, aconvert: bool):
config = Config.get_instance() config = Config.get_instance()
self.download_path = download_path
self.index = int(index) self.index = int(index)
self.duration = float(duration) self.duration = float(duration)
self.vconvert = bool(vconvert) self.vconvert = bool(vconvert)
@ -24,23 +26,17 @@ class Segments:
self.vconvert = True self.vconvert = True
self.aconvert = True self.aconvert = True
async def stream(self, path: str, file: str) -> bytes: async def stream(self, file: Path) -> bytes:
realFile: str = calc_download_path(base_path=path, folder=file, create_path=False)
if not os.path.exists(realFile):
msg = f"File {realFile} does not exist."
raise StreamingError(msg)
try: try:
ff = await ffprobe(realFile) ff = await ffprobe(file)
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
tmpDir: str = tempfile.gettempdir() tmpDir: str = tempfile.gettempdir()
tmpFile = os.path.join(tmpDir, f'ytptube_stream.{hashlib.sha256(realFile.encode("utf-8")).hexdigest()}') tmpFile = os.path.join(tmpDir, f'ytptube_stream.{hashlib.sha256(str(file).encode("utf-8")).hexdigest()}')
if not os.path.exists(tmpFile): if not os.path.exists(tmpFile):
os.symlink(realFile, tmpFile) os.symlink(file, tmpFile)
if self.index == 0: if self.index == 0:
startTime: float = f"{0:.6f}" startTime: float = f"{0:.6f}"
@ -95,7 +91,7 @@ class Segments:
fargs.append("mpegts") fargs.append("mpegts")
fargs.append("pipe:1") fargs.append("pipe:1")
LOG.debug(f"Streaming '{realFile}' segment '{self.index}'. ffmpeg: {' '.join(fargs)}") LOG.debug(f"Streaming '{file}' segment '{self.index}'. ffmpeg: {' '.join(fargs)}")
proc = await asyncio.subprocess.create_subprocess_exec( proc = await asyncio.subprocess.create_subprocess_exec(
"ffmpeg", "ffmpeg",
@ -108,8 +104,8 @@ class Segments:
data, err = await proc.communicate() data, err = await proc.communicate()
if 0 != proc.returncode: if 0 != proc.returncode:
LOG.error(f"Failed to stream '{realFile}' segment '{self.index}'. {err.decode('utf-8')}.") LOG.error(f"Failed to stream '{file}' segment '{self.index}'. {err.decode('utf-8')}.")
msg = f"Failed to stream '{realFile}' segment '{self.index}'." msg = f"Failed to stream '{file}' segment '{self.index}'."
raise StreamingError(msg) raise StreamingError(msg)
return data return data

View file

@ -1,12 +1,12 @@
import logging import logging
import pathlib from pathlib import Path
import anyio import anyio
import pysubs2 import pysubs2
from pysubs2.formats.substation import SubstationFormat from pysubs2.formats.substation import SubstationFormat
from pysubs2.time import ms_to_times from pysubs2.time import ms_to_times
from .Utils import calc_download_path from .Utils import ALLOWED_SUBS_EXTENSIONS
LOG = logging.getLogger("player.subtitle") LOG = logging.getLogger("player.subtitle")
@ -22,33 +22,22 @@ SubstationFormat.ms_to_timestamp = ms_to_timestamp
class Subtitle: class Subtitle:
allowed_extensions: tuple[str] = ( def __init__(self, download_path: str):
".srt", self.download_path = download_path
".vtt",
".ass",
)
async def make(self, path: str, file: str) -> str: async def make(self, file: Path) -> str:
realFile: str = calc_download_path(base_path=path, folder=file, create_path=False) if file.suffix not in ALLOWED_SUBS_EXTENSIONS:
rFile = pathlib.Path(realFile)
if not rFile.exists():
msg = f"File '{file}' does not exist."
raise Exception(msg)
if rFile.suffix not in self.allowed_extensions:
msg = f"File '{file}' subtitle type is not supported." msg = f"File '{file}' subtitle type is not supported."
raise Exception(msg) raise Exception(msg)
if rFile.suffix == ".vtt": if file.suffix == ".vtt":
async with await anyio.open_file(realFile) as f: async with await anyio.open_file(file) as f:
return await f.read() return await f.read()
subs = pysubs2.load(path=str(rFile)) subs = pysubs2.load(path=str(file))
if len(subs.events) < 1: if len(subs.events) < 1:
msg = f"No subtitle events were found in '{rFile}'." msg = f"No subtitle events were found in '{file}'."
raise Exception(msg) raise Exception(msg)
if len(subs.events) < 2: if len(subs.events) < 2:

View file

@ -19,13 +19,7 @@ from .LogWrapper import LogWrapper
LOG = logging.getLogger("Utils") LOG = logging.getLogger("Utils")
IGNORED_KEYS: tuple[str] = ( IGNORED_KEYS: tuple[str] = ("paths", "outtmpl", "progress_hooks", "postprocessor_hooks", "download_archive")
"paths",
"outtmpl",
"progress_hooks",
"postprocessor_hooks",
"download_archive",
)
YTDLP_INFO_CLS: yt_dlp.YoutubeDL = None YTDLP_INFO_CLS: yt_dlp.YoutubeDL = None
ALLOWED_SUBS_EXTENSIONS: tuple[str] = (".srt", ".vtt", ".ass") ALLOWED_SUBS_EXTENSIONS: tuple[str] = (".srt", ".vtt", ".ass")
@ -365,7 +359,7 @@ def check_id(file: pathlib.Path) -> bool | str:
if id not in f.stem: if id not in f.stem:
continue continue
if f.suffix not in (".mp4", ".mkv", ".webm", ".m4v", ".m4a", ".mp3", ".aac", ".ogg"): if f.suffix != file.suffix:
continue continue
return f.absolute() return f.absolute()
@ -599,3 +593,31 @@ def get_mime_type(metadata: dict, file_path: pathlib.Path) -> str:
# Final fallback: Return generic binary type # Final fallback: Return generic binary type
return "application/octet-stream" return "application/octet-stream"
def get_file(download_path: str, file: str | pathlib.Path) -> tuple[pathlib.Path, int]:
"""
Get the real file path.
Args:
download_path (str): Base download path.
file (str|pathlib.Path): File path.
Returns:
pathlib.Path: Real file path.
int: http status code.
"""
file_path: str = os.path.normpath(os.path.join(download_path, str(file)))
if not file_path.startswith(download_path):
return (pathlib.Path(file_path), 404)
realFile: str = pathlib.Path(calc_download_path(base_path=str(download_path), folder=str(file), create_path=False))
if realFile.exists():
return (realFile, 200)
possibleFile = check_id(file=realFile)
if not possibleFile:
return (realFile, 404)
return (pathlib.Path(possibleFile), 302)