social media crossposting tool. 3rd time's the charm
mastodon misskey crossposting bluesky
1import requests 2import subprocess 3import json 4import re, urllib.parse, os 5from util import LOGGER 6 7FILENAME = re.compile(r'filename="?([^\";]*)"?') 8 9def get_filename_from_url(url): 10 try: 11 response = requests.head(url, allow_redirects=True) 12 disposition = response.headers.get('Content-Disposition') 13 if disposition: 14 filename = FILENAME.findall(disposition) 15 if filename: 16 return filename[0] 17 except requests.RequestException: 18 pass 19 20 parsed_url = urllib.parse.urlparse(url) 21 return os.path.basename(parsed_url.path) 22 23def probe_bytes(bytes: bytes) -> dict: 24 cmd = [ 25 'ffprobe', 26 '-v', 'error', 27 '-show_format', 28 '-show_streams', 29 '-print_format', 'json', 30 'pipe:0' 31 ] 32 proc = subprocess.run(cmd, input=bytes, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 33 34 if proc.returncode != 0: 35 raise RuntimeError(f"ffprobe failed: {proc.stderr.decode()}") 36 37 return json.loads(proc.stdout) 38 39def convert_to_mp4(video_bytes: bytes) -> bytes: 40 cmd = [ 41 'ffmpeg', 42 '-i', 'pipe:0', 43 '-c:v', 'libx264', 44 '-crf', '30', 45 '-preset', 'slow', 46 '-c:a', 'aac', 47 '-b:a', '128k', 48 '-movflags', 'frag_keyframe+empty_moov+default_base_moof', 49 '-f', 'mp4', 50 'pipe:1' 51 ] 52 53 proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 54 out_bytes, err = proc.communicate(input=video_bytes) 55 56 if proc.returncode != 0: 57 raise RuntimeError(f"ffmpeg compress failed: {err.decode()}") 58 59 return out_bytes 60 61def compress_image(image_bytes: bytes, quality: int = 90): 62 cmd = [ 63 'ffmpeg', 64 '-f', 'image2pipe', 65 '-i', 'pipe:0', 66 '-c:v', 'webp', 67 '-q:v', str(quality), 68 '-f', 'image2pipe', 69 'pipe:1' 70 ] 71 72 proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 73 out_bytes, err = proc.communicate(input=image_bytes) 74 75 if proc.returncode != 0: 76 raise RuntimeError(f"ffmpeg compress failed: {err.decode()}") 77 78 return out_bytes 79 80def download_blob(url: str, max_bytes: int = 5_000_000) -> bytes | None: 81 response = requests.get(url, stream=True, timeout=20) 82 if response.status_code != 200: 83 LOGGER.info("Failed to download %s! %s", url, response.text) 84 return None 85 86 downloaded_bytes = b"" 87 current_size = 0 88 89 for chunk in response.iter_content(chunk_size=8192): 90 if not chunk: 91 continue 92 93 current_size += len(chunk) 94 if current_size > max_bytes: 95 response.close() 96 return None 97 98 downloaded_bytes += chunk 99 100 return downloaded_bytes 101 102 103def get_media_meta(bytes: bytes): 104 probe = probe_bytes(bytes) 105 streams = [s for s in probe['streams'] if s['codec_type'] == 'video'] 106 if not streams: 107 raise ValueError("No video stream found") 108 109 media = streams[0] 110 return { 111 'width': int(media['width']), 112 'height': int(media['height']), 113 'duration': float(media.get('duration', probe['format'].get('duration', -1))) 114 }