128 lines
3.5 KiB
Python
Executable File
128 lines
3.5 KiB
Python
Executable File
#!/usr/bin/env python3.9
|
|
import argparse
|
|
import datetime
|
|
import hashlib
|
|
import math
|
|
import re
|
|
|
|
import subprocess
|
|
import sys
|
|
import typing
|
|
from pathlib import Path
|
|
|
|
import ffmpeg
|
|
|
|
|
|
def create_gif(
|
|
video_path: Path,
|
|
from_time: str,
|
|
to_time: typing.Optional[str] = None,
|
|
save_path: typing.Optional[Path] = None,
|
|
duration: typing.Optional[int] = 10,
|
|
fps: int = 15,
|
|
width: int = 320,
|
|
) -> Path:
|
|
if not save_path:
|
|
# strip extra decimals
|
|
time_clean = re.sub(r"\.(\d)\d+", r".\1", from_time).replace(":", "")
|
|
save_path = video_path.with_name(f"{video_path.stem}_{time_clean}.gif")
|
|
|
|
info = ffmpeg.ffprobe(video_path)
|
|
scaled_width, scaled_height = width, math.ceil(width / info.ar / info.sample_ar)
|
|
|
|
subprocess.run(
|
|
[
|
|
"ffmpeg",
|
|
"-v",
|
|
"warning",
|
|
"-y",
|
|
"-ss",
|
|
from_time,
|
|
*(["-to", to_time] if to_time else ["-t", str(duration)]),
|
|
"-ignore_chapters",
|
|
"1",
|
|
"-i",
|
|
str(video_path),
|
|
"-filter_complex",
|
|
# f"fps={fps},scale={scale}:-1:flags=lanczos[x];[x]split[x1][x2];[x1]palettegen[p];[x2][p]paletteuse",
|
|
f"[0:v] fps={fps},scale={scaled_width}:{scaled_height},split [a][b];[a] palettegen=stats_mode=full [p];[b][p] paletteuse=new=1",
|
|
str(save_path),
|
|
],
|
|
check=True,
|
|
)
|
|
|
|
return save_path
|
|
|
|
|
|
def optimize_gif(gif_path: Path) -> Path:
|
|
subprocess.run(
|
|
[
|
|
"gifsicle",
|
|
"--lossy=98",
|
|
"--optimize=3",
|
|
"--batch",
|
|
"-i",
|
|
str(gif_path),
|
|
],
|
|
check=True,
|
|
)
|
|
return gif_path
|
|
|
|
|
|
def parse_args(argv: typing.List[str] = None) -> argparse.Namespace:
|
|
arger = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
arger.add_argument("video_path", type=str, help="Path to video file")
|
|
arger.add_argument(
|
|
"--start", "-s", dest="from_time", type=str, help="Time to start from, e.g. 02:59", required=True
|
|
)
|
|
arger.add_argument("--duration", "-d", default=3, type=int, help="Duration of gif in seconds")
|
|
arger.add_argument("--fps", default=15, type=int, help="Frames per second")
|
|
arger.add_argument("--width", default=400, type=int, help="Image width")
|
|
arger.add_argument("--optimize", default=True, action="store_true", help="Reduce GIF filesize")
|
|
|
|
if len(argv) == 0:
|
|
argv = ["--help"]
|
|
|
|
return arger.parse_args(argv)
|
|
|
|
|
|
def md5(text: str) -> str:
|
|
return hashlib.md5(text.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def main():
|
|
args = parse_args(sys.argv[1:])
|
|
video_path = Path(args.video_path)
|
|
|
|
from_time = args.from_time
|
|
try:
|
|
time = datetime.timedelta(seconds=round(float(from_time), 1))
|
|
from_time = str(time)
|
|
if time.total_seconds() < 3600:
|
|
from_time = from_time[2:]
|
|
except ValueError:
|
|
pass
|
|
|
|
save_path = None
|
|
if str(args.video_path).startswith("http"):
|
|
save_path = Path("~/Downloads").expanduser() / f"{md5(str(video_path))}__{from_time.replace(':', '')}.gif"
|
|
|
|
gif_path = create_gif(
|
|
video_path=video_path,
|
|
save_path=save_path,
|
|
from_time=from_time,
|
|
duration=args.duration,
|
|
fps=args.fps,
|
|
width=args.width,
|
|
)
|
|
if args.optimize:
|
|
try:
|
|
gif_path = optimize_gif(gif_path)
|
|
except:
|
|
pass
|
|
print(gif_path.resolve())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|