From 2b199c8bffe71a679d832b0fd0a268e29fb82c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A4=D0=BE=D1=84=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=94=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=92=D0=B8=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Tue, 1 Sep 2026 11:29:37 +0300 Subject: [PATCH] Initial import: secure Emby STRM exporter --- .env.example | 2 + .gitignore | 5 +++ README.md | 18 +++++++++ pyproject.toml | 16 ++++++++ src/emby_strm_exporter/__init__.py | 1 + src/emby_strm_exporter/cli.py | 23 +++++++++++ src/emby_strm_exporter/exporter.py | 65 ++++++++++++++++++++++++++++++ 7 files changed, 130 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/emby_strm_exporter/__init__.py create mode 100644 src/emby_strm_exporter/cli.py create mode 100644 src/emby_strm_exporter/exporter.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..acdf586 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +DATABASE_URL=postgresql://user:password@localhost:5432/media +ACE_STREAM_URL=http://localhost:6878/ace/getstream?infohash={infohash}&.mp4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fef3cf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.py[cod] +.venv/ +output/ +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..978a8aa --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Emby STRM Exporter + +Восстановленный и безопасно переоформленный вариант `GenerateEmbyContent.py`. +Экспортёр читает строки медиатеки из PostgreSQL и создаёт дерево `.strm`/`.nfo` +для Emby. Настройки и учётные данные передаются только через окружение. + +## Подготовка + +```bash +python -m pip install -e . +cp .env.example .env # заполните значения и экспортируйте их в окружение +export DATABASE_URL='postgresql://...' +export ACE_STREAM_URL='http://localhost:6878/ace/getstream?infohash={infohash}&.mp4' +emby-strm-export output/ 1 +``` + +Ожидается прежняя схема с таблицами `categorys` и `torrents`. Запросы оставлены +параметризованными; исходные пароли, IP-адреса и API-ключи не переносились. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..006ebd8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "emby-strm-exporter" +version = "0.1.0" +description = "Export media rows from PostgreSQL to an Emby STRM library." +requires-python = ">=3.10" +dependencies = ["psycopg[binary]>=3.1"] + +[project.scripts] +emby-strm-export = "emby_strm_exporter.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/emby_strm_exporter/__init__.py b/src/emby_strm_exporter/__init__.py new file mode 100644 index 0000000..1064aef --- /dev/null +++ b/src/emby_strm_exporter/__init__.py @@ -0,0 +1 @@ +"""Экспорт медиазаписей PostgreSQL в файловую библиотеку Emby.""" diff --git a/src/emby_strm_exporter/cli.py b/src/emby_strm_exporter/cli.py new file mode 100644 index 0000000..e95990c --- /dev/null +++ b/src/emby_strm_exporter/cli.py @@ -0,0 +1,23 @@ +"""CLI-оболочка экспортёра: параметры среды остаются вне репозитория.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from .exporter import export_category, required_environment + + +def main() -> None: + """Экспортировать выбранную категорию и вывести количество файлов.""" + + parser = argparse.ArgumentParser(description="Export a PostgreSQL media category to STRM files") + parser.add_argument("output", type=Path) + parser.add_argument("category", type=int) + args = parser.parse_args() + database_url, stream_url = required_environment() + print(f"Exported {export_category(args.output, args.category, database_url, stream_url)} media rows") + + +if __name__ == "__main__": + main() diff --git a/src/emby_strm_exporter/exporter.py b/src/emby_strm_exporter/exporter.py new file mode 100644 index 0000000..f1a3377 --- /dev/null +++ b/src/emby_strm_exporter/exporter.py @@ -0,0 +1,65 @@ +"""Изолированная логика экспорта без секретов и жёстко заданных путей.""" + +from __future__ import annotations + +import os +import re +from pathlib import Path + +import psycopg + + +def safe_path_part(value: object) -> str: + """Нормализовать название фильма перед созданием каталога.""" + + return re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", str(value)).strip(". ") or "untitled" + + +def export_category(output_dir: Path, category_id: int, database_url: str, stream_url: str) -> int: + """Экспортировать наиболее раздаваемую версию каждого названия в `.strm`.""" + + query = ''' + SELECT s.name, s.year, s.quality, s.imdb, s.infohash + FROM torrents s + JOIN ( + SELECT category_id, name, quality, MAX(seeds) AS seeds + FROM torrents GROUP BY category_id, name, quality + ) best ON best.category_id = s.category_id AND best.name = s.name + AND best.quality = s.quality AND best.seeds = s.seeds + WHERE s.category_id = %s + ORDER BY s.name, s.quality + ''' + with psycopg.connect(database_url) as connection, connection.cursor() as cursor: + cursor.execute("SELECT name FROM categorys WHERE site_code = %s", (category_id,)) + category = cursor.fetchone() + if not category: + raise ValueError(f"Category {category_id} was not found") + cursor.execute(query, (category_id,)) + rows = cursor.fetchall() + + root = output_dir / safe_path_part(category[0]) + for name, year, quality, imdb, infohash in rows: + title, release_year = safe_path_part(name), safe_path_part(year or "unknown") + movie_dir = root / release_year / f"{title} ({release_year})" + movie_dir.mkdir(parents=True, exist_ok=True) + base_name = f"{title} ({release_year})" + (movie_dir / f"{base_name}-{safe_path_part(quality)}.strm").write_text( + stream_url.format(infohash=infohash) + "\n", encoding="utf-8" + ) + if imdb: + (movie_dir / f"{base_name}.nfo").write_text( + f"{imdb}\n", encoding="utf-8" + ) + return len(rows) + + +def required_environment() -> tuple[str, str]: + """Получить обязательные настройки и не допустить запуска с пустым секретом.""" + + database_url = os.environ.get("DATABASE_URL") + stream_url = os.environ.get("ACE_STREAM_URL") + if not database_url or not stream_url: + raise RuntimeError("DATABASE_URL and ACE_STREAM_URL must be set") + if "{infohash}" not in stream_url: + raise RuntimeError("ACE_STREAM_URL must contain {infohash}") + return database_url, stream_url