210 lines
7.2 KiB
Python
210 lines
7.2 KiB
Python
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)
|