commit
8a852bcd24
7 changed files with 126 additions and 81 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue