# Extra Crispy Soundpost Machine - Sponsored by KFP # When you only want to directly pull from a stream with a little fuss. # This script works by taking the stream URL and desired filename as input and redirecting you to the Streamable site for preview and adjustment. It will then prompt you for the download URL of the clip produced. # Example Youtube URL: # https://youtu.be/RR4m1HVbkBU # Should redirect to: # https://streamable.com/clipper?url=https://youtu.be/RR4m1HVbkBU # You may also skip this step by directly including the streamable URL as a parameter (I'm not going to fucking check if you actually use a streamable URL, but this uses the streamable web API to pull the video so you will know very soon if something fucked up). # soundposter.py --url https://streamable.com/0cxuky # Once it has the Streamable download URL, it will download and split the MP4 file into its video and audio streams via ffmpeg. When done, it will output the formatted soundpost file in the current running directory. # NOTE: This script uses ffmpy and requests, it's recommended to run the following before running this: # pip install ffmpy requests requests_toolbelt # NOTE: Typically the output video is downloaded at 720p. # NOTE: For completely seamless operation you could easily edit this script to use youtube-dl to directly download the stream and pipe the timestamps into ffmpeg. However the manual step has some advantages from using streamable's features for cropping, timestamp adjustments, etc. (along with having a backup link in case the original sound file gets archived). #!/usr/bin/env python import argparse import ffmpy import os from pathlib import Path from pathlib import PurePosixPath import random import requests from requests_toolbelt import MultipartEncoder import string import urllib import webbrowser # Streamable URL for manual adjustments def form_streamable_url(yUrl): return f"https://streamable.com/clipper?url={yUrl}" # Open Streamable in new browser tab using input YT URL def open_streamable(sUrl): print(f"Redirecting to {sUrl}") webbrowser.open_new_tab(sUrl) # Request for soundpost's filename or make some shit up def get_soundpost_filename(): sFile = input("Enter the desired soundpost's filename (e.g. am frog): ") if not sFile: # Alert: NOT cryptographically secure!! If you plan to use this soundpost filename as a password, carefully reconsider!!!! sFile = ''.join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=20)) return sFile # Request for YT URL or link to a cute video def get_yt_url(): yUrl = input("Enter the Youtube URL: ") if not yUrl: print("No clip URL found! Using the default") yUrl = 'https://youtu.be/RR4m1HVbkBU' return yUrl # Request for the Streamable clip URL once ready. Even if the script closes, you can run the script again with the URL as a parameter. def get_streamable_clip(): sClipUrl = input("Enter Streamable clip URL: ") if not sClipUrl: raise Exception("No clip URL found") return sClipUrl # Get the Streamable API response and return the MP4 CDN URL. def get_streamable_cdn_url(sApiUrl): with requests.get(sApiUrl) as r: r.raise_for_status() resBody = r.json() if resBody['status'] == 2: mp4Url = resBody['files']['mp4']['url'] else: raise Exception(f"Streamable clip is not yet ready, it is {resBody['percent']}% completed") return mp4Url # Download from API to file def download_to_file(fileName, url): print(f"Downloading to {fileName}...") Path(fileName).touch() # We need to use streaming since these are video files and usually hefty. with requests.get(url, stream=True) as r: r.raise_for_status() with open(fileName, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) # Download the streamable clip as the desired file name def download_streamable_clip(sClipUrl, sFile): clipFile = f"{sFile}.mp4" sharkcode = PurePosixPath( urllib.parse.unquote( urllib.parse.urlparse( sClipUrl ).path ) ).parts[1] sApiUrl = f"https://api.streamable.com/videos/{sharkcode}" print(f"Connecting to URL {sApiUrl}") mp4Url = get_streamable_cdn_url(sApiUrl) download_to_file(clipFile, mp4Url) # Use the FFMPEG library to split the original clip file into separate video and audio files. def split_mp4(sFile): clipFile = sFile + '.mp4' vFile = sFile + '.webm' aFile = sFile + '.m4a' print(f"Creating video output {vFile}...") # >ffmpeg -i "am frog.mp4" -map 0:v -c:v libvpx -crf 15 -b:v 1M "am frog.webm" ff = ffmpy.FFmpeg( inputs={clipFile: None}, outputs={vFile: '-map 0:v -c:v libvpx -crf 15 -b:v 1M'} ) ff.run() print(f"Creating audio output {aFile}...") # >ffmpeg -i "am frog.mp4" -map 0:a -c copy "am frog.m4a" ff = ffmpy.FFmpeg( inputs={clipFile: None}, outputs={aFile: '-map 0:a'} ) ff.run() # Clean up the original clip file. os.remove(clipFile) # Make a request to catbox.moe to upload. # Code stolen from https://github.com/yukinotenshi/pyupload/blob/master/pyupload/uploader/base.py def upload_audio(sFile): catboxApiUrl = 'https://catbox.moe/user/api.php' aFile = sFile + ".m4a" print(f"Uploading audio file {aFile} to catbox.moe...") # curl -F "reqtype=fileupload" -F "fileToUpload=@cutie.png" https://catbox.moe/user/api.php file = open(aFile, 'rb') try: data = { 'reqtype': 'fileupload', 'userhash': '', 'fileToUpload': (file.name, file, 'audio/mp4') } encoder = MultipartEncoder(fields=data) response = requests.post(catboxApiUrl, data=encoder, headers={'Content-Type': encoder.content_type}) finally: file.close() response.raise_for_status() print(f"Uploaded audio to {response.text}") return response.text # Everything has been prepared. Rename the video file to link to the uploaded audio. def create_soundpost_file(sFile, audioUrl): aFile = sFile + '.m4a' vFile = sFile + '.webm' htmlEncodedA = urllib.parse.quote(audioUrl, safe='') soundpostFile = f"{sFile}[sound={htmlEncodedA}].webm" # Soundposts follow the same format; the audio must be URL encoded. Technically soundposts support more than one audio file, but that's outside of the scope here. path = Path(vFile) path.rename(Path(path.parent, soundpostFile)) print(f"Created file {soundpostFile}") # Clean up the remaining audio file. os.remove(aFile) # Pass the first input, whatever it may be def parse_args(): parser=argparse.ArgumentParser(description="A shitty soundpost maker.") parser.add_argument("--url", nargs='?', const='', default='', help='Optional. Enter if you want to add an already created Streamable URL. If not, leave blank.') args=parser.parse_args() return args # Finally. The good stuff. def main(): inputs = parse_args() if not inputs.url: yUrl = get_yt_url() sUrl = form_streamable_url(yUrl) open_streamable(sUrl) sClipUrl = get_streamable_clip() else: sClipUrl = inputs.url sFile = get_soundpost_filename() download_streamable_clip(sClipUrl, sFile) split_mp4(sFile) audioUrl = upload_audio(sFile) create_soundpost_file(sFile, audioUrl) if __name__ == '__main__': main()