Initial import: playlist to STRM converter
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
output/
|
||||
*.strm
|
||||
@@ -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.
|
||||
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
"""Инструменты импорта M3U-плейлистов в набор STRM-файлов."""
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user