commit 9445e8a724ec548601855622e9226a755fcf5de9 Author: Фофанов Дмитрий Викторович Date: Tue Sep 1 11:29:33 2026 +0300 Initial import: playlist to STRM converter diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2642c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.py[cod] +.venv/ +output/ +*.strm diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2a6485 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Stream Playlist Tools + +Небольшая утилита, восстановленная из `GenerateStream.py`. Она преобразует записи +из M3U-плейлиста в отдельные `.strm`-файлы, которые можно подключать к медиасерверу. + +## Запуск + +```bash +python -m pip install -e . +playlist-to-strm input.m3u output/ +``` + +Утилита не загружает контент и не хранит ключи или адреса серверов: она записывает +ссылки из входного плейлиста как есть. Название файла берётся из `tvg-name`, +`#EXTINF` или URL. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cf3ad0a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "stream-playlist-tools" +version = "0.1.0" +description = "Convert M3U playlists into Emby-compatible STRM files." +requires-python = ">=3.10" + +[project.scripts] +playlist-to-strm = "stream_playlist_tools.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/stream_playlist_tools/__init__.py b/src/stream_playlist_tools/__init__.py new file mode 100644 index 0000000..128c745 --- /dev/null +++ b/src/stream_playlist_tools/__init__.py @@ -0,0 +1 @@ +"""Инструменты импорта M3U-плейлистов в набор STRM-файлов.""" diff --git a/src/stream_playlist_tools/cli.py b/src/stream_playlist_tools/cli.py new file mode 100644 index 0000000..536e601 --- /dev/null +++ b/src/stream_playlist_tools/cli.py @@ -0,0 +1,23 @@ +"""Командная точка входа: M3U на входе, каталог STRM на выходе.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from .m3u import parse_m3u, write_strm + + +def main() -> None: + """Разобрать аргументы и сообщить число экспортированных записей.""" + + parser = argparse.ArgumentParser(description="Convert an M3U playlist to STRM files") + parser.add_argument("playlist", type=Path) + parser.add_argument("output", type=Path) + args = parser.parse_args() + entries = parse_m3u(args.playlist.read_text(encoding="utf-8-sig")) + print(f"Written {write_strm(entries, args.output)} STRM files to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/src/stream_playlist_tools/m3u.py b/src/stream_playlist_tools/m3u.py new file mode 100644 index 0000000..ab59815 --- /dev/null +++ b/src/stream_playlist_tools/m3u.py @@ -0,0 +1,54 @@ +"""Минимальный и намеренно прозрачный разбор M3U без сетевых запросов.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class PlaylistEntry: + """Одна запись плейлиста: отображаемое имя и адрес потока.""" + + name: str + url: str + + +def parse_m3u(text: str) -> list[PlaylistEntry]: + """Извлечь пары `#EXTINF`/URL, игнорируя служебные и неполные строки.""" + + entries: list[PlaylistEntry] = [] + pending_name: str | None = None + for raw_line in text.splitlines(): + line = raw_line.strip() + if line.startswith("#EXTINF"): + match = re.search(r'tvg-name="([^"]+)"', line) + pending_name = match.group(1) if match else line.rsplit(",", 1)[-1].strip() + elif line and not line.startswith("#") and pending_name: + entries.append(PlaylistEntry(name=pending_name, url=line)) + pending_name = None + return entries + + +def safe_filename(value: str) -> str: + """Сделать имя переносимым между macOS, Linux и Windows.""" + + cleaned = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", value).strip(". ") + return cleaned[:180] or "unnamed-stream" + + +def write_strm(entries: list[PlaylistEntry], output_dir: Path) -> int: + """Записать URL каждой записи в отдельный UTF-8 `.strm`-файл.""" + + output_dir.mkdir(parents=True, exist_ok=True) + used: set[str] = set() + for entry in entries: + stem = safe_filename(entry.name) + candidate, index = stem, 2 + while candidate.casefold() in used: + candidate = f"{stem} ({index})" + index += 1 + used.add(candidate.casefold()) + (output_dir / f"{candidate}.strm").write_text(f"{entry.url}\n", encoding="utf-8") + return len(entries)