from datetime import UTC, datetime from pathlib import Path from yt_dlp.postprocessor.common import PostProcessor from yt_dlp.utils import hyphenate_date class NFOMaker(PostProcessor): """ A Post-Processor that writes metadata to NFO file. """ _MAPPING: dict = {} _TEMPLATE: str | None = None _date_fields: tuple = ("upload_date", "release_date") @classmethod def pp_key(cls) -> str: return "NFOMaker" def run(self, info: dict = None) -> tuple[list, dict]: if not info: self.to_screen("No info provided to NFO Maker.") return [], {} if not self._TEMPLATE: self.to_screen("NFO template not set, skipping NFO creation.") return [], info nfo_file = Path(info.get("filename")).with_suffix(".nfo") if nfo_file.exists(): self.to_screen(f"NFO file '{nfo_file!s}' already exists, skipping creation.") return [], info nfo_data: dict = {} for nfo_name, ytdlp_name in self._MAPPING.items(): try: if isinstance(ytdlp_name, str): ytdlp_name = (ytdlp_name,) _key = None _value = None for name in ytdlp_name: if name is None: continue _key = name _value = info.get(name) if _value is not None: break if _value: if _key in self._date_fields: _value = hyphenate_date(_value) if "description" == _key: _value = _value.replace("\n", " ").replace("\r", " ").strip() if _value: nfo_data[nfo_name] = _value except Exception as e: if self._downloader: self._downloader.report_error(f"Error processing {nfo_name} -> {ytdlp_name}: {e}") if len(nfo_data) < 1: self.to_screen("No metadata found to write to NFO file.") return [], info if "year" not in nfo_data and (k in nfo_data for k in self._date_fields): try: _date = any(nfo_data.get(k) for k in self._date_fields if k in nfo_data) if _date: nfo_data["year"] = _date.split("-")[0] except Exception as e: self.to_screen(f"Error extracting year from date: {e}") file_path = Path(info["filepath"]) status = self._write_episode_info(nfo_file, file_path, nfo_data) if status and nfo_file.exists(): mtime = file_path.stat().st_mtime self.try_utime(str(nfo_file), mtime, mtime) return [], info def _write_episode_info(self, nfo_file: Path, real_file: Path, data: dict) -> bool: year, month, day = data.get("aired", "0000-00-00").split("-") if not (year and month and day): self.to_screen("Invalid aired date format, skipping NFO creation.") return False self.to_screen(f"Creating NFO file at {nfo_file!s}") nfo_file.touch(exist_ok=True) dt = datetime(int(year), int(month), int(day), tzinfo=UTC) data["unique_id"] = "1{:>02}{:>02}{:>04}".format( dt.strftime("%m"), dt.strftime("%d"), self._extend_id(real_file) ) self._write(nfo_file=nfo_file, text=self._TEMPLATE, repl=data) return True @staticmethod def _extend_id(file: Path) -> int: try: import hashlib hash_object = hashlib.sha256(file.stem.lower().encode("utf-8")) hash_hex: str = hash_object.hexdigest() ascii_values: str = "".join([str(ord(c)) for c in hash_hex]) four_digit_string: str = ascii_values[:4] if len(ascii_values) >= 4 else ascii_values.ljust(4, "9") return int(four_digit_string) except Exception: return 1000 def _write(self, nfo_file: Path, text: str, repl: dict): from xml.sax.saxutils import escape for key, value in repl.items(): if isinstance(value, str): repl[key] = escape(value) for key, value in repl.items(): if value is None: continue text = text.replace(f"{{{key}}}", str(value)) for key in {**self._MAPPING, **repl}: if f"{{{key}}}" in text: self.write_debug(f"Missing replacement for '{key}'.") text = "\n".join(line for line in text.splitlines() if f"{{{key}}}" not in line) try: if not nfo_file.parent.exists(): nfo_file.parent.mkdir(parents=True, exist_ok=True) nfo_file.write_text(text, encoding="utf-8") self.to_screen(f"NFO file written successfully at {nfo_file!s}") except Exception as e: self.to_screen(f"Error writing NFO file: {e}") class NFOMakerTvPP(NFOMaker): _MAPPING = { "title": "title", "season": ("season_number", "season", "year", None), "episode": ("episode_number", "episode"), "aired": ("release_date", "upload_date"), "author": "uploader", "plot": "description", "id": "id", "extractor": "extractor", } _TEMPLATE = """ {title} {season} {episode} {aired} {unique_id} {id} {plot} """ 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 = """ {title} {plot} {runtime} {thumb} {id} {unique_id} {id} {country} {aired} {year} {trailer} """