async def on_new_message(self, chat: Chat, message: Message): # If a message has text saying to rotate, and is a reply to a video, then cut it # `rotate left`, `rotate right`, `flip horizontal`?, `rotate 90`, `rotate 180` text_clean = message.text.strip().lower().replace("-", "") if text_clean.startswith("rotate"): transpose = self.get_rotate_direction( text_clean[len("rotate"):].strip()) elif text_clean.startswith("flip"): transpose = self.get_flip_direction( text_clean[len("flip"):].strip()) else: return video = find_video_for_message(chat, message) if video is None: await self.send_text_reply( chat, message, "Cannot work out which video you want to rotate/flip.") if transpose is None: return [ await self.send_text_reply( chat, message, "I do not understand this rotate/flip command.") ] async with self.progress_message(chat, message, "Rotating or flipping video.."): output_path = random_sandbox_video_path() task = FfmpegTask(inputs={video.message_data.file_path: None}, outputs={output_path: f"-vf \"{transpose}\""}) await self.worker.await_task(task) return [ await self.send_video_reply(chat, message, output_path, video.tags(self.database)) ]
async def on_new_message(self, chat: Chat, message: Message) -> Optional[List[Message]]: text_clean = message.text.lower().strip() if not text_clean.startswith("video"): return video = find_video_for_message(chat, message) if video is None: return [ await self.send_text_reply( chat, message, "I'm not sure which video you want to video.") ] # Parse arguments, if present args = text_clean[5:].strip().split() gif_settings = None if args: gif_settings = GifSettings.from_input(args) gif_settings.audio = True # Convert video output_path = random_sandbox_video_path() async with self.progress_message(chat, message, "Converting video into video"): if not await self.video_has_audio_track(video): task = add_audio_track_task(video.message_data.file_path, output_path) await self.worker.await_task(task) else: tasks = video_to_video(video.message_data.file_path, output_path, gif_settings) for task in tasks: await self.worker.await_task(task) return [ await self.send_video_reply(chat, message, output_path, video.tags(self.database)) ]
async def merge_messages( self, chat: Chat, cmd_message: Message, messages_to_merge: List[Message]) -> Optional[List[Message]]: if len(messages_to_merge) < 2: error_text = \ "Merge commands require at least 2 videos to merge. " \ "Please reply to a message, and provide telegram links to the other messages" return [await self.send_text_reply(chat, cmd_message, error_text)] num_files = len(messages_to_merge) filter_args = "".join([f"[{x}:v][{x}:a]" for x in range(num_files) ]) + f" concat=n={num_files}:v=1:a=1 [v] [a]" output_args = f"-filter_complex \"{filter_args}\" -map \"[v]\" -map \"[a]\" -vsync 2" async with self.progress_message(chat, cmd_message, "Merging videos"): file_paths = await self.align_video_dimensions( [m.message_data.file_path for m in messages_to_merge]) output_path = random_sandbox_video_path() task = FfmpegTask( inputs={file_path: None for file_path in file_paths}, outputs={output_path: output_args}) await self.worker.await_task(task) tags = messages_to_merge[0].tags(self.database) tags.merge_all( [msg.tags(self.database) for msg in messages_to_merge[1:]]) return [ await self.send_video_reply(chat, cmd_message, output_path, tags) ]
async def with_audio_track(self, file_path: str) -> str: audio_check_task = video_has_audio_track_task(file_path) if not await self.worker.await_task(audio_check_task): output_path = random_sandbox_video_path() await self.worker.await_task( add_audio_track_task(file_path, output_path)) return output_path return file_path
async def convert_file(self, video_path: str) -> str: if video_path.endswith(".gif"): return await self.convert_video_to_telegram_gif(video_path) else: processed_path = random_sandbox_video_path() task = FfmpegTask(inputs={video_path: None}, outputs={processed_path: "-qscale 0"}) await self.worker.await_task(task) return processed_path
async def single_pass_convert(self, video_path: str, gif_settings: GifSettings): first_pass_filename = random_sandbox_video_path() # first attempt ffmpeg_args = gif_settings.ffmpeg_options_one_pass task = FfmpegTask(inputs={video_path: None}, outputs={first_pass_filename: ffmpeg_args}) await self.worker.await_task(task) return first_pass_filename
async def cut_out_video(self, video: Message, start: str, end: str) -> str: first_part_path = random_sandbox_video_path() second_part_path = random_sandbox_video_path() task1 = FfmpegTask(inputs={video.message_data.file_path: None}, outputs={first_part_path: f"-to {start}"}) task2 = FfmpegTask(inputs={video.message_data.file_path: None}, outputs={second_part_path: f"-ss {end}"}) await self.worker.await_tasks([task1, task2]) inputs_file = random_sandbox_video_path("txt") with open(inputs_file, "w") as f: f.write( f"file '{first_part_path.split('/')[1]}'\nfile '{second_part_path.split('/')[1]}'" ) output_path = random_sandbox_video_path() task_concat = FfmpegTask(inputs={inputs_file: "-safe 0 -f concat"}, outputs={output_path: "-c copy"}) await self.worker.await_task(task_concat) return output_path
async def cut_video(self, video: Message, start: Optional[str], end: Optional[str]) -> str: new_path = random_sandbox_video_path() out_string = (f"-ss {start}" if start is not None else "") + " " + (f"-to {end}" if end is not None else "") task = FfmpegTask(inputs={video.message_data.file_path: None}, outputs={new_path: out_string}) await self.worker.await_task(task) return new_path
async def convert_gif_link(self, chat: Chat, message: Message, gif_link: str) -> Message: resp = requests.get(gif_link) gif_path = random_sandbox_video_path("gif") with open(gif_path, "wb") as f: f.write(resp.content) new_path = await self.convert_video_to_telegram_gif(gif_path) tags = VideoTags() tags.add_tag_value(VideoTags.source, gif_link) return await self.send_video_reply(chat, message, new_path, tags)
async def send_imgur_video(self, chat: Chat, message: Message, image: Dict[str, str], gallery_link: str) -> Message: file_url = image["mp4"] file_ext = file_url.split(".")[-1] resp = requests.get(file_url) file_path = random_sandbox_video_path(file_ext) with open(file_path, "wb") as f: f.write(resp.content) tags = VideoTags() tags.add_tag_value(VideoTags.source, gallery_link) return await self.send_video_reply(chat, message, file_path, tags)
async def scale_and_pad_to_dimensions(self, file_path: str, dimensions: Tuple[int, int]) -> str: orig_dimensions = await self.get_video_dimensions(file_path) if orig_dimensions == dimensions: return file_path output_path = random_sandbox_video_path() x, y = dimensions args = f"-vf \"scale={x}:{y}:force_original_aspect_ratio=decrease,pad={x}:{y}:(ow-iw)/2:(oh-ih)/2,setsar=1\"" task = FfmpegTask(inputs={file_path: None}, outputs={output_path: args}) await self.worker.await_task(task) return output_path
async def two_pass_convert(self, video_path: str, gif_settings: GifSettings): # If it's too big, do a 2 pass run two_pass_filename = random_sandbox_video_path() # First pass two_pass_args = gif_settings.ffmpeg_options_two_pass task1 = FfmpegTask(global_options=["-y"], inputs={video_path: None}, outputs={os.devnull: two_pass_args[0]}) await self.worker.await_task(task1) task2 = FfmpegTask(global_options=["-y"], inputs={video_path: None}, outputs={two_pass_filename: two_pass_args[1]}) await self.worker.await_task(task2) return two_pass_filename
async def on_new_message(self, chat: Chat, message: Message): # If a message has text saying to crop, some percentages maybe? # And is a reply to a video, then crop it text_clean = message.text.lower().strip() if not text_clean.startswith("crop"): return video = find_video_for_message(chat, message) if video is None: return [ await self.send_text_reply( chat, message, "I'm not sure which video you would like to crop.") ] crop_args = text_clean[len("crop"):].strip() if crop_args.lower() == "auto": async with self.progress_message(chat, message, "Detecting auto crop settings"): crop_string = await self.detect_crop( video.message_data.file_path) if crop_string is None: return [ await self.send_text_reply( chat, message, "That video could not be auto cropped.") ] else: crop_string = self.parse_crop_input(crop_args) if crop_string is None: return [ await self.send_text_reply( chat, message, "I don't understand this crop command. " "Please specify what percentage to cut off the left, right, top, bottom. " "Alternatively specify the desired percentage for the width and height. " "Use the format `crop left 20% right 20% top 10%`. " "If the video has black bars you wish to crop, just use `crop auto`" ) ] output_path = random_sandbox_video_path() async with self.progress_message(chat, message, "Cropping video"): task = FfmpegTask(inputs={video.message_data.file_path: None}, outputs={ output_path: f"-filter:v \"{crop_string}\" -c:a copy" }) await self.worker.await_task(task) return [ await self.send_video_reply(chat, message, output_path, video.tags(self.database)) ]
async def on_new_message(self, chat: Chat, message: Message) -> Optional[List[Message]]: clean_text = message.text.strip().lower() if clean_text != "reverse": return None video = find_video_for_message(chat, message) if video is None: return [ await self.send_text_reply( chat, message, "Please reply to the video you want to reverse") ] output_path = random_sandbox_video_path() reverse_task = FfmpegTask( inputs={video.message_data.file_path: None}, outputs={output_path: "-vf reverse -af areverse"}) async with self.progress_message(chat, message, "Reversing video"): await self.worker.await_task(reverse_task) return [ await self.send_video_reply(chat, message, output_path, video.tags(self.database)) ]
async def unzip(self, chat: Chat, message: Message) -> Optional[List[Message]]: video_paths = [] with zipfile.ZipFile(message.message_data.file_path, "r") as zip_ref: for filename in zip_ref.namelist(): mime_type, _ = mimetypes.guess_type(filename) if mime_type_is_video(mime_type): file_ext = filename.split(".")[-1] video_path = random_sandbox_video_path(file_ext) with zip_ref.open(filename) as zf, open(video_path, "wb") as f: shutil.copyfileobj(zf, f) video_paths.append(video_path) # Convert to mp4s processed_paths = await asyncio.gather(*(self.convert_file(path) for path in video_paths)) # Send them if processed_paths: return await asyncio.gather( *(self.send_video_reply(chat, message, path, VideoTags()) for path in processed_paths)) return None
async def on_new_message(self, chat: Chat, message: Message) -> Optional[List[Message]]: text_clean = message.text.lower().strip() if text_clean not in [ "stabilise", "stabilize", "stab", "deshake", "unshake" ]: return video = find_video_for_message(chat, message) if video is None: return [ await self.send_text_reply( chat, message, "I'm not sure which video you would like to stabilise.") ] output_path = random_sandbox_video_path() async with self.progress_message(chat, message, "Stabilising video"): task = FfmpegTask(inputs={video.message_data.file_path: None}, outputs={output_path: "-vf deshake"}) await self.worker.await_task(task) return [ await self.send_video_reply(chat, message, output_path, video.tags(self.database)) ]
async def align_video_dimensions(self, file_paths: List[str]) -> List[str]: first = file_paths[0] the_rest = file_paths[1:] # Get dimensions of first video, scale the rest to match dimensions = await self.get_video_dimensions(first) rescaled = await asyncio.gather(*[ self.scale_and_pad_to_dimensions(path, dimensions) for path in the_rest ]) same_dimension_paths = [first] + rescaled # Add audio tracks if necessary with_audio = await asyncio.gather( *[self.with_audio_track(path) for path in same_dimension_paths]) # Handle duplicate file paths. Copy them to sandbox files output_paths = [] for path in with_audio: if path in output_paths: new_path = random_sandbox_video_path(path.split(".")[-1]) shutil.copyfile(path, new_path) output_paths.append(new_path) else: output_paths.append(path) return output_paths
async def handle_post_link(self, chat: Chat, message: Message, post_id: str): api_link = f"https://faexport.spangle.org.uk/submission/{post_id}.json" api_resp = requests.get(api_link, headers={"User-Agent": "Gif pipeline"}) api_data = api_resp.json() file_url = api_data["download"] file_ext = file_url.split(".")[-1] if file_ext not in ["gif"]: return await self.send_text_reply( chat, message, "That post doesn't seem to be a gif.") # Download file resp = requests.get(file_url) file_path = random_sandbox_video_path(file_ext) with open(file_path, "wb") as f: f.write(resp.content) # If gif, convert to telegram gif if file_ext == "gif": file_path = await self.convert_video_to_telegram_gif(file_path) tags = VideoTags() tags.add_tag_value(VideoTags.source, f"https://furaffinity.net/view/{post_id}/") return await self.send_video_reply(chat, message, file_path, tags)
async def handle_post_link(self, chat: Chat, message: Message, post_id: str): api_link = f"https://e621.net/posts/{post_id}.json" api_resp = requests.get( api_link, headers={"User-Agent": "Gif pipeline (my username is dr-spangle)"}) api_data = api_resp.json() file_ext = api_data["post"]["file"]["ext"] if file_ext not in ["gif", "webm"]: return await self.send_text_reply( chat, message, "That post doesn't seem to be a gif or webm.") file_url = api_data["post"]["file"]["url"] # Download file resp = requests.get(file_url) file_path = random_sandbox_video_path(file_ext) with open(file_path, "wb") as f: f.write(resp.content) # If gif, convert to telegram gif if file_ext == "gif": file_path = await self.convert_video_to_telegram_gif(file_path) tags = VideoTags() tags.add_tag_value(VideoTags.source, f"https://e621.net/posts/{post_id}") return await self.send_video_reply(chat, message, file_path, tags)
async def download_link(self, link: str) -> str: output_path = random_sandbox_video_path("") task = YoutubeDLTask(link, output_path) return await self.worker.await_task(task)
def pass_log_file(self) -> str: if self._pass_log_file is None: self._pass_log_file = random_sandbox_video_path("") return self._pass_log_file