Initial import: secure Emby STRM exporter

This commit is contained in:
2026-09-01 11:29:37 +03:00
commit 2b199c8bff
7 changed files with 130 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
DATABASE_URL=postgresql://user:password@localhost:5432/media
ACE_STREAM_URL=http://localhost:6878/ace/getstream?infohash={infohash}&.mp4
+5
View File
@@ -0,0 +1,5 @@
__pycache__/
*.py[cod]
.venv/
output/
.env
+18
View File
@@ -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-ключи не переносились.
+16
View File
@@ -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"]
+1
View File
@@ -0,0 +1 @@
"""Экспорт медиазаписей PostgreSQL в файловую библиотеку Emby."""
+23
View File
@@ -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()
+65
View File
@@ -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"<movie><imdbid>{imdb}</imdbid></movie>\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