Initial import: playlist to STRM converter

This commit is contained in:
2026-09-01 11:29:33 +03:00
commit 9445e8a724
6 changed files with 113 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
__pycache__/
*.py[cod]
.venv/
output/
*.strm
+15
View File
@@ -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.
+15
View File
@@ -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"]
+1
View File
@@ -0,0 +1 @@
"""Инструменты импорта M3U-плейлистов в набор STRM-файлов."""
+23
View File
@@ -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()
+54
View File
@@ -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)