nodex.pw-convertor/converter.py

211 lines
7.2 KiB
Python
Raw Permalink Normal View History

2026-06-04 13:41:12 +05:00
import subprocess
import shutil
import tempfile
from pathlib import Path
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tiff", ".tif", ".ico", ".avif", ".heic", ".heif"}
VIDEO_EXTS = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".flv", ".wmv", ".m4v", ".3gp", ".ts", ".mts", ".mpeg", ".mpg"}
AUDIO_EXTS = {".mp3", ".wav", ".flac", ".ogg", ".m4a", ".opus", ".aac", ".wma", ".aiff", ".alac"}
ARCHIVE_EXTS = {".zip", ".tar", ".gz", ".bz2", ".7z", ".rar", ".xz"}
IMAGE_TARGETS = ["png", "jpg", "webp", "bmp", "gif", "tiff", "ico"]
VIDEO_TARGETS = ["mp4", "avi", "mov", "mkv", "webm", "gif"]
VIDEO_TO_AUDIO = ["mp3", "wav", "flac", "aac", "opus"]
AUDIO_TARGETS = ["mp3", "wav", "flac", "ogg", "m4a", "opus", "aac"]
ARCHIVE_TARGETS = ["zip", "tar.gz", "tar.bz2", "7z"]
def detect_type(path: Path) -> str:
name = path.name.lower()
# двойные суффиксы — tar.gz и т.д.
if name.endswith((".tar.gz", ".tar.bz2", ".tar.xz", ".tgz", ".tbz2")):
return "archive"
suf = path.suffix.lower()
if suf in IMAGE_EXTS:
return "image"
if suf in VIDEO_EXTS:
return "video"
if suf in AUDIO_EXTS:
return "audio"
if suf in ARCHIVE_EXTS:
return "archive"
return "unknown"
def get_targets(file_type: str) -> list:
if file_type == "image":
return IMAGE_TARGETS
if file_type == "video":
return VIDEO_TARGETS + VIDEO_TO_AUDIO
if file_type == "audio":
return AUDIO_TARGETS
if file_type == "archive":
return ARCHIVE_TARGETS
return []
def _convert_image(src: Path, dst: Path) -> tuple[bool, str]:
try:
from PIL import Image
img = Image.open(src)
ext = dst.suffix.lower()
# jpeg не понимает прозрачность — заливаем белым
if ext in (".jpg", ".jpeg"):
if img.mode in ("RGBA", "LA", "P"):
bg = Image.new("RGB", img.size, (255, 255, 255))
if img.mode == "P":
img = img.convert("RGBA")
mask = img.split()[-1] if img.mode in ("RGBA", "LA") else None
bg.paste(img, mask=mask)
img = bg
elif img.mode != "RGB":
img = img.convert("RGB")
if ext == ".ico":
img.save(dst, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
return True, ""
kwargs = {}
if ext in (".jpg", ".jpeg"):
kwargs["quality"] = 95
elif ext == ".webp":
kwargs["quality"] = 90
img.save(dst, **kwargs)
return True, ""
except Exception as e:
return False, str(e)
def _convert_ffmpeg(src: Path, dst: Path, ffmpeg_bin: str) -> tuple[bool, str]:
ext = dst.suffix.lower()
# gif из видео нормальный — с палитрой
if ext == ".gif":
cmd = [
ffmpeg_bin, "-i", str(src), "-y",
"-vf", "fps=12,scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse",
str(dst),
]
else:
cmd = [ffmpeg_bin, "-i", str(src), "-y"]
if ext == ".mp3":
cmd += ["-codec:a", "libmp3lame", "-qscale:a", "2", "-vn"]
elif ext == ".flac":
cmd += ["-codec:a", "flac", "-vn"]
elif ext == ".ogg":
cmd += ["-codec:a", "libvorbis", "-qscale:a", "6", "-vn"]
elif ext == ".opus":
cmd += ["-codec:a", "libopus", "-b:a", "192k", "-vn"]
elif ext == ".aac":
cmd += ["-codec:a", "aac", "-b:a", "256k", "-vn"]
elif ext == ".wav":
cmd += ["-codec:a", "pcm_s16le", "-vn"]
elif ext == ".m4a":
cmd += ["-codec:a", "aac", "-b:a", "256k", "-vn"]
cmd.append(str(dst))
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
if r.returncode != 0:
return False, r.stderr[-1000:]
return True, ""
except subprocess.TimeoutExpired:
return False, "таймаут конвертации"
except FileNotFoundError:
return False, "ffmpeg не найден — запусти setup-ffmpeg или поставь через пакетный менеджер"
except Exception as e:
return False, str(e)
def _convert_archive(src: Path, dst: Path) -> tuple[bool, str]:
import zipfile
import tarfile
tmp = Path(tempfile.mkdtemp())
try:
src_name = src.name.lower()
# распаковываем что пришло
if src_name.endswith(".zip"):
with zipfile.ZipFile(src) as z:
z.extractall(tmp)
elif src_name.endswith((".tar.gz", ".tgz")):
with tarfile.open(src, "r:gz") as t:
t.extractall(tmp)
elif src_name.endswith((".tar.bz2", ".tbz2")):
with tarfile.open(src, "r:bz2") as t:
t.extractall(tmp)
elif src_name.endswith((".tar.xz", ".txz")):
with tarfile.open(src, "r:xz") as t:
t.extractall(tmp)
elif src_name.endswith(".tar"):
with tarfile.open(src, "r:") as t:
t.extractall(tmp)
elif src_name.endswith(".7z"):
import py7zr
with py7zr.SevenZipFile(src, mode="r") as z:
z.extractall(tmp)
elif src_name.endswith(".rar"):
import rarfile
with rarfile.RarFile(src) as r:
r.extractall(tmp)
else:
return False, "неизвестный формат архива"
# пакуем в нужное
dst_name = dst.name.lower()
if dst_name.endswith(".zip"):
with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as z:
for f in tmp.rglob("*"):
if f.is_file():
z.write(f, f.relative_to(tmp))
elif dst_name.endswith(".tar.gz"):
with tarfile.open(dst, "w:gz") as t:
for f in tmp.rglob("*"):
if f.is_file():
t.add(f, arcname=str(f.relative_to(tmp)))
elif dst_name.endswith(".tar.bz2"):
with tarfile.open(dst, "w:bz2") as t:
for f in tmp.rglob("*"):
if f.is_file():
t.add(f, arcname=str(f.relative_to(tmp)))
elif dst_name.endswith(".7z"):
import py7zr
with py7zr.SevenZipFile(dst, "w") as z:
for f in tmp.rglob("*"):
if f.is_file():
z.write(f, str(f.relative_to(tmp)))
else:
return False, "неизвестный целевой формат"
return True, ""
except Exception as e:
return False, str(e)
finally:
shutil.rmtree(tmp, ignore_errors=True)
def convert(src: Path, dst: Path, target: str, ffmpeg_bin: str = "ffmpeg") -> tuple[bool, str]:
file_type = detect_type(src)
if file_type == "image":
return _convert_image(src, dst)
if file_type in ("video", "audio"):
return _convert_ffmpeg(src, dst, ffmpeg_bin)
if file_type == "archive":
return _convert_archive(src, dst)
# хуй знает что за файл — пусть ffmpeg попробует
return _convert_ffmpeg(src, dst, ffmpeg_bin)