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)