Merge pull request #427 from arabcoders/dev

Bundles deno runtime
This commit is contained in:
Abdulmohsen 2025-09-24 22:39:50 +03:00 committed by GitHub
commit 8a852bcd24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 126 additions and 81 deletions

View file

@ -49,6 +49,7 @@
"daterange",
"defusedxml",
"delenv",
"denoland",
"dlfields",
"dotdot",
"dotenv",
@ -100,6 +101,7 @@
"mccabe",
"mebibytes",
"MEIPASS",
"merch",
"Microformat",
"microformats",
"mkvtoolsnix",
@ -153,6 +155,7 @@
"threadsafe",
"tiktok",
"timespec",
"tinyurl",
"tmpfilename",
"toplevel",
"ungroup",

View file

@ -66,12 +66,13 @@ COPY --chown=app:app --from=python_builder /opt/python /opt/python
COPY --from=ghcr.io/arabcoders/alpine-mp4box /usr/bin/mp4box /usr/bin/mp4box
COPY --from=ghcr.io/arabcoders/jellyfin-ffmpeg /usr/bin/ffmpeg /usr/bin/ffmpeg
COPY --from=ghcr.io/arabcoders/jellyfin-ffmpeg /usr/bin/ffprobe /usr/bin/ffprobe
COPY --from=denoland/deno:latest /usr/bin/deno /usr/bin/deno
COPY --chown=app:app ./healthcheck.sh /usr/local/bin/healthcheck
ENV PATH="/opt/python/bin:$PATH"
RUN chown -R app:app /config /downloads && \
chmod +x /usr/local/bin/healthcheck /usr/bin/mp4box /usr/bin/ffmpeg /usr/bin/ffprobe
chmod +x /usr/local/bin/healthcheck /usr/bin/mp4box /usr/bin/ffmpeg /usr/bin/ffprobe /usr/bin/deno
VOLUME /config
VOLUME /downloads

View file

@ -54,16 +54,24 @@ DEFAULT_PRESETS: list[dict[int, dict[str, str | bool]]] = [
"id": "2ade2c28-cad4-4a06-b7eb-2439fdf46f60",
"name": "Info Reader Plugin",
"description": 'This preset generate specific filename format and metadata to work with yt-dlp info reader plugins for jellyfin/emby and plex and to make play state sync work for WatchState.\n\nThere is one more step you need to do via Other > Terminal if you have it enabled or directly from container shell\n\nyt-dlp -I0 --write-info-json --write-thumbnail --convert-thumbnails jpg --paths /downloads/youtube -o "%(channel|Unknown_title)s [%(channel_id|Unknown_id)s]/%(title).180B [%(channel_id|Unknown_id)s].%(ext)s" -- https://www.youtube.com/channel/UCClfFsWcT3N2I7VTXXyt84A\n\nChange the url to the channel you want to download.\n\nFor more information please visit \nhttps://github.com/arabcoders/watchstate/blob/master/FAQ.md#how-to-get-watchstate-working-with-youtube-contentlibrary',
"template": "%(channel)s [%(channel_id|Unknown_id)s]/Season %(release_date>%Y,upload_date>%Y|Unknown)s/%(release_date>%Y%m%d,upload_date>%Y%m%d)s - %(title).180B [%(extractor)s-%(id)s].%(ext)s",
"template": "%(channel)s [%(channel_id|Unknown_id)s]/Season %(release_date>%Y,upload_date>%Y|Unknown)s/%(release_date>%Y%m%d,upload_date>%Y%m%d)s - %(title).180B [%(id)s].%(ext)s",
"cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log\n--windows-filenames --write-info-json --embed-info-json \n--convert-thumbnails jpg --write-thumbnail --embed-metadata",
"default": True,
},
{
"id": "2ade2c28-cad4-4a06-b7eb-2439fdf46f61",
"name": "NFO Maker TV",
"description": "This preset generate specific filename format and metadata to work with media servers like jellyfin/emby and plex.",
"template": "%(channel)s [%(channel_id|Unknown_id)s]/Season %(release_date>%Y,upload_date>%Y|Unknown)s/S%(release_date>%Y,upload_date>%Y)sE%(release_date>%m%d,upload_date>%m%d)s - %(title).100B [%(extractor)s-%(id)s].%(ext)s",
"cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log \n--windows-filenames --convert-thumbnails jpg --write-thumbnail \n--use-postprocessor NFOMakerTvPP",
"description": "This preset generate specific filename format and metadata to work with media servers like jellyfin/emby and plex for episodes.",
"template": "%(channel)s [%(channel_id|Unknown_id)s]/Season %(release_date>%Y,upload_date>%Y|Unknown)s/S%(release_date>%Y,upload_date>%Y)sE%(release_date>%m%d,upload_date>%m%d)s - %(title).100B [%(id)s].%(ext)s",
"cli": '--socket-timeout 30 --download-archive %(config_path)s/archive.log\n--use-postprocessor "NFOMakerPP:when=after_move" \n--windows-filenames --convert-thumbnails jpg --write-thumbnail',
"default": True,
},
{
"id": "2ade2c28-cad4-4a06-b7eb-2439fdf46f61",
"name": "NFO Maker Movie",
"description": "This preset generate specific filename format and metadata to work with media servers like jellyfin/emby and plex for movies.",
"template": "%(channel)s [%(channel_id|Unknown_id)s]/Season %(release_date>%Y,upload_date>%Y|Unknown)s/S%(release_date>%Y,upload_date>%Y)sE%(release_date>%m%d,upload_date>%m%d)s - %(title).100B [%(id)s].%(ext)s",
"cli": '--socket-timeout 30 --download-archive %(config_path)s/archive.log\n--use-postprocessor "NFOMakerPP:when=after_move;mode=movie" \n--windows-filenames --convert-thumbnails jpg --write-thumbnail',
"default": True,
},
]

View file

@ -129,10 +129,10 @@ class YoutubeHandler(BaseHandler):
None if the URL is neither.
"""
if m := YoutubeHandler.CHANNEL_REGEX.match(url):
if (m := YoutubeHandler.CHANNEL_REGEX.match(url)) and m.group("id"):
return {"type": "channel_id", "id": m.group("id")}
if m := YoutubeHandler.PLAYLIST_REGEX.match(url):
if (m := YoutubeHandler.PLAYLIST_REGEX.match(url)) and m.group("id"):
return {"type": "playlist_id", "id": m.group("id")}
return None

View file

@ -1,7 +1,7 @@
# flake8: noqa: F401
from yt_dlp.globals import postprocessors
from .make_nfo import NFOMakerMoviePP, NFOMakerTvPP
from .nfo_maker import NFOMakerPP
_ytptube_pps = {name: value for name, value in globals().items() if name.endswith("PP")}

View file

@ -13,7 +13,7 @@ if TYPE_CHECKING:
from collections.abc import Iterable
class NFOMaker(PostProcessor):
class NFOMakerPP(PostProcessor):
"""
A Post-Processor that writes metadata to an NFO file using a simple
placeholder-based template engine.
@ -24,8 +24,6 @@ class NFOMaker(PostProcessor):
- ("date_field", "%Y") -> year from date_field
"""
_MAPPING: dict[str, Any] = {}
_TEMPLATE: str | None = None
_DATE_FIELDS: tuple[str, ...] = ("upload_date", "release_date", "aired", "premiered")
_URL_PAT = re.compile(
@ -43,6 +41,79 @@ class NFOMaker(PostProcessor):
r"snapchat|reddit|telegram|t\.me|whatsapp|link\s+in\s+bio|bit\.ly|tinyurl|goo\.gl"
r")\b"
)
_args: dict[str, Any] = {}
_MODE: dict = {
"tv": {
"mapping": {
"title": "title",
"season": ("season_number", "season", "year", ("release_date", "%Y"), ("upload_date", "%Y")),
"episode": (
"episode_number",
"episode",
("release_date", "%m%d"),
("upload_date", "%m%d"),
"unique_id",
),
"aired": ("release_date", "upload_date"),
"author": "uploader",
"plot": "description",
"id": "id",
"extractor": "extractor",
},
"template": """
<episodedetails>
<title>{title}</title>
<season>{season}</season>
<episode>{episode}</episode>
<aired>{aired}</aired>
<uniqueid type="cmdb">{unique_id}</uniqueid>
<uniqueid type="{extractor}">{id}</uniqueid>
<plot>{plot}</plot>
</episodedetails>
""".strip("\n"),
},
"movie": {
"mapping": {
"title": "title",
"plot": "description",
"runtime": "duration",
"thumb": "thumbnail",
"id": "id",
"country": "language",
"aired": ("release_date", "upload_date"),
"extractor": "extractor",
"year": ("release_year", "year"),
"author": "uploader",
"trailer": "webpage_url",
},
"template": """
<movie>
<title>{title}</title>
<plot>{plot}</plot>
<runtime>{runtime}</runtime>
<thumb aspect="poster" preview="{thumb}">{thumb}</thumb>
<id>{id}</id>
<uniqueid type="cmdb">{unique_id}</uniqueid>
<uniqueid type="{extractor}" default="true">{id}</uniqueid>
<country>{country}</country>
<premiered>{aired}</premiered>
<year>{year}</year>
<trailer>{trailer}</trailer>
</movie>
""".strip("\n"),
},
}
mode: str = "tv"
"The NFO mode to use. Either 'tv' or 'movie'. Default is 'tv'."
prefix: bool = True
"Prefix episodes with 1 for better sorting."
def __init__(self, downloader, mode: str = "tv", prefix: bool = True) -> None:
PostProcessor.__init__(self, downloader)
self.mode = str(mode).lower()
self.prefix = prefix in (True, "true", "1", 1)
@classmethod
def pp_key(cls) -> str:
@ -53,8 +124,8 @@ class NFOMaker(PostProcessor):
self.to_screen("No info provided to NFO Maker.")
return [], {}
if not self._TEMPLATE:
self.to_screen("NFO template not set, skipping NFO creation.")
if self.mode not in self._MODE:
self.to_screen(f"Invalid mode '{self.mode}'.")
return [], info
# prefer explicit final path if present, else fall back to filename
@ -103,7 +174,7 @@ class NFOMaker(PostProcessor):
def _collect_nfo_data(self, info: dict) -> dict[str, Any]:
data: dict[str, Any] = {}
for nfo_name, spec in self._MAPPING.items():
for nfo_name, spec in self._MODE[self.mode].get("mapping", {}).items():
try:
values = spec if isinstance(spec, tuple) else (spec,)
resolved_key = None
@ -166,7 +237,7 @@ class NFOMaker(PostProcessor):
self.to_screen("Invalid aired date parts, skipping NFO creation.")
return False
self.to_screen(f"Creating NFO file at {nfo_file!s}")
self.to_screen(f"Creating {self.mode} NFO file at {nfo_file!s}")
nfo_file.parent.mkdir(parents=True, exist_ok=True)
nfo_file.touch(exist_ok=True)
@ -174,7 +245,7 @@ class NFOMaker(PostProcessor):
data = dict(data) # do not mutate original
data["unique_id"] = self._build_unique_id(dt, real_file)
self._write(nfo_file=nfo_file, text=self._TEMPLATE or "", repl=data)
self._write(nfo_file=nfo_file, text=self._MODE[self.mode].get("template", ""), repl=data)
return True
@staticmethod
@ -230,7 +301,7 @@ class NFOMaker(PostProcessor):
return raw
if fmt and isinstance(fmt, str):
date_s = NFOMaker._normalize_date(raw)
date_s = NFOMakerPP._normalize_date(raw)
if date_s:
try:
dt = datetime.now(tz=UTC).strptime(date_s, "%Y-%m-%d")
@ -265,10 +336,18 @@ class NFOMaker(PostProcessor):
for key, value in safe_repl.items():
if value is None:
continue
if self.prefix and key in ("episode",):
try:
value = f"1{value}"
except Exception:
pass
rendered = rendered.replace(f"{{{key}}}", str(value))
# remove any unresolved placeholder lines
unresolved_keys: Iterable[str] = set({*self._MAPPING.keys(), *safe_repl.keys()})
mapping = self._MODE[self.mode].get("mapping", {})
unresolved_keys: Iterable[str] = set({*mapping, *safe_repl.keys()})
pattern = re.compile(rf".*{{(?:{'|'.join(map(re.escape, unresolved_keys))})}}.*")
rendered = "\n".join(line for line in rendered.splitlines() if not pattern.fullmatch(line))
@ -336,58 +415,3 @@ class NFOMaker(PostProcessor):
summary = cut[: dot + 1] if 0 < dot else cut.rstrip()
return summary
class NFOMakerTvPP(NFOMaker):
_MAPPING = {
"title": "title",
"season": ("season_number", "season", "year", ("release_date", "%Y"), ("upload_date", "%Y")),
"episode": ("episode_number", "episode", ("release_date", "%m%d"), ("upload_date", "%m%d"), "unique_id"),
"aired": ("release_date", "upload_date"),
"author": "uploader",
"plot": "description",
"id": "id",
"extractor": "extractor",
}
_TEMPLATE = """
<episodedetails>
<title>{title}</title>
<season>{season}</season>
<episode>{episode}</episode>
<aired>{aired}</aired>
<uniqueid type="cmdb">{unique_id}</uniqueid>
<uniqueid type="{extractor}">{id}</uniqueid>
<plot>{plot}</plot>
</episodedetails>
""".strip("\n")
class NFOMakerMoviePP(NFOMaker):
_MAPPING = {
"title": "title",
"plot": "description",
"runtime": "duration",
"thumb": "thumbnail",
"id": "id",
"country": "language",
"aired": ("release_date", "upload_date"),
"extractor": "extractor",
"year": ("release_year", "year"),
"author": "uploader",
"trailer": "webpage_url",
}
_TEMPLATE = """
<movie>
<title>{title}</title>
<plot>{plot}</plot>
<runtime>{runtime}</runtime>
<thumb aspect="poster" preview="{thumb}">{thumb}</thumb>
<id>{id}</id>
<uniqueid type="cmdb">{unique_id}</uniqueid>
<uniqueid type="{extractor}" default="true">{id}</uniqueid>
<country>{country}</country>
<premiered>{aired}</premiered>
<year>{year}</year>
<trailer>{trailer}</trailer>
</movie>
""".strip("\n")

View file

@ -764,7 +764,7 @@ const removeItem = async (item: StoreItem) => {
}
}
const retryItem = (item: StoreItem, re_add = false) => {
const retryItem = (item: StoreItem, re_add: boolean = false, remove_file: boolean = false) => {
const item_req: Partial<StoreItem> = {
url: item.url,
preset: item.preset,
@ -776,7 +776,7 @@ const retryItem = (item: StoreItem, re_add = false) => {
auto_start: item.auto_start,
}
socket.emit('item_delete', { id: item._id, remove_file: false })
socket.emit('item_delete', { id: item._id, remove_file: remove_file })
if (selectedElms.value.includes(item._id || '')) {
selectedElms.value = selectedElms.value.filter(i => i !== item._id)
@ -860,14 +860,18 @@ const removeFromArchiveDialog = (item: StoreItem) => {
dialog_confirm.value.visible = true
dialog_confirm.value.title = 'Remove from Archive'
dialog_confirm.value.message = `Remove '${item.title || item.id || item.url || '??'}' from archive?`
dialog_confirm.value.options = [
{ key: 'remove_history', label: 'Also, Remove from history.' },
const opts = [
{ key: 'remove_history', label: 'Remove from history.' },
{ key: 're_add', label: 'Re-add to download form.' },
]
];
if (config.app.remove_files) {
opts.push({ key: 'dont_remove_file', label: "Don't remove associated files." })
}
dialog_confirm.value.options = opts
dialog_confirm.value.confirm = (opts: any) => removeFromArchive(item, opts)
}
const removeFromArchive = async (item: StoreItem, opts?: { re_add?: boolean, remove_history?: boolean }) => {
const removeFromArchive = async (item: StoreItem, opts?: { re_add?: boolean, remove_history?: boolean, dont_remove_file?: boolean }) => {
try {
const req = await request(`/api/history/${item._id}/archive`, { method: 'DELETE' })
const data = await req.json()
@ -883,13 +887,18 @@ const removeFromArchive = async (item: StoreItem, opts?: { re_add?: boolean, rem
dialog_confirm.value.visible = false
}
let file_delete = config.app.remove_files
if (opts?.dont_remove_file) {
file_delete = false
}
if (opts?.re_add) {
retryItem(item, true)
retryItem(item, true, file_delete)
return
}
if (opts?.remove_history) {
socket.emit('item_delete', { id: item._id, remove_file: false })
socket.emit('item_delete', { id: item._id, remove_file: file_delete })
}
}