update 26.7.25
This commit is contained in:
Executable
+161
@@ -0,0 +1,161 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user