Files
Docker_Compose/4600Pro/tgd/module/nfo.py
T
2026-07-25 02:31:18 +08:00

162 lines
4.6 KiB
Python
Executable File

"""Kodi/Jellyfin/Emby compatible NFO generation."""
import math
import os
import re
import xml.etree.ElementTree as ET
from typing import List, Optional
from module.download_record import DownloadRecord
HASHTAG_RE = re.compile(r"(?<!\w)#\S+")
PROMOTION_KEYWORDS = (
"点击下方",
"评论区",
"置顶",
"预览视频",
"支持一下",
"谢谢大家",
)
def get_nfo_path(video_path: str) -> str:
"""Return the sidecar NFO path for a video path."""
return os.path.splitext(video_path)[0] + ".nfo"
def clean_caption_line(line: str) -> str:
"""Remove hashtags and surrounding whitespace from one caption line."""
return HASHTAG_RE.sub("", line).strip()
def extract_genres(caption: Optional[str]) -> List[str]:
"""Extract ordered unique genres from caption hashtags."""
if not caption:
return []
genres: List[str] = []
seen = set()
for tag in HASHTAG_RE.findall(caption):
genre = tag.lstrip("#").strip()
if genre and genre not in seen:
genres.append(genre)
seen.add(genre)
return genres
def is_promotional_line(line: str) -> bool:
"""Return True when a caption line is likely channel promotion text."""
return any(keyword in line for keyword in PROMOTION_KEYWORDS)
def clean_caption(caption: Optional[str]) -> str:
"""Return media-library friendly caption text without tags or promo lines."""
if not caption:
return ""
lines: List[str] = []
for raw_line in caption.splitlines():
line = clean_caption_line(raw_line)
if not line or is_promotional_line(line):
continue
lines.append(line)
paragraphs: List[str] = []
current: List[str] = []
for line in lines:
if line:
current.append(line)
elif current:
paragraphs.append("\n".join(current))
current = []
if current:
paragraphs.append("\n".join(current))
return "\n\n".join(paragraphs).strip()
def title_from_record(record: DownloadRecord) -> str:
"""Choose a human-readable title for the media library."""
cleaned_caption = clean_caption(record.caption)
if cleaned_caption:
return cleaned_caption.splitlines()[0]
if record.original_file_name:
return os.path.splitext(record.original_file_name)[0]
if record.chat_title:
return f"{record.chat_title} {record.message_id}"
return f"{record.chat_id} {record.message_id}"
def date_part(value: Optional[str]) -> Optional[str]:
"""Return YYYY-MM-DD from an ISO-like timestamp."""
if not value:
return None
return value[:10]
def year_part(value: Optional[str]) -> Optional[str]:
"""Return YYYY from an ISO-like timestamp."""
if not value:
return None
return value[:4]
def runtime_minutes(duration: Optional[int]) -> Optional[str]:
"""Convert seconds to rounded-up minutes for NFO runtime."""
if not duration:
return None
return str(int(math.ceil(duration / 60.0)))
def add_text(parent, tag: str, text: Optional[str]):
"""Add an XML child only when text is meaningful."""
if text is None or text == "":
return
child = ET.SubElement(parent, tag)
child.text = str(text)
def build_video_nfo_xml(record: DownloadRecord) -> str:
"""Build Kodi/Jellyfin/Emby-compatible movie XML."""
root = ET.Element("movie")
title = title_from_record(record)
plot = clean_caption(record.caption)
add_text(root, "title", title)
add_text(root, "plot", plot)
unique_id = ET.SubElement(
root,
"uniqueid",
{"type": "telegram", "default": "true"},
)
unique_id.text = f"{record.chat_id}_{record.message_id}"
add_text(root, "premiered", date_part(record.message_date))
add_text(root, "year", year_part(record.message_date))
add_text(root, "runtime", runtime_minutes(record.duration))
add_text(root, "studio", record.chat_title)
for genre in extract_genres(record.caption):
add_text(root, "genre", genre)
return ET.tostring(root, encoding="unicode")
def write_video_nfo(record: DownloadRecord) -> str:
"""Write a sidecar NFO file for successful video records."""
if (
record.media_type != "video"
or record.download_status != "success"
or not record.local_path
):
return ""
nfo_path = get_nfo_path(record.local_path)
nfo_dir = os.path.dirname(nfo_path)
if nfo_dir:
os.makedirs(nfo_dir, exist_ok=True)
root = ET.fromstring(build_video_nfo_xml(record))
tree = ET.ElementTree(root)
tree.write(nfo_path, encoding="utf-8", xml_declaration=True)
return nfo_path