├── .gitignore ├── music ├── links.txt └── music_url.py ├── requirements.txt ├── LICENSE ├── README.md └── app.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .python-version 3 | data/ 4 | -------------------------------------------------------------------------------- /music/links.txt: -------------------------------------------------------------------------------- 1 | 194 - 0ryS4iy3sbY 2 | 171 - hJv1oZuJr4w 3 | 189 - nocCzSmiZX4 4 | 215 - n8zTuhd0tZU 5 | 197 - 7M1kdRwANhI 6 | 202 - mhS3XHtgFe8 7 | 181 - GgS5ffFklm8 8 | 282 - RL7ABht1LeI 9 | 251 - Ab8dZ5HIDWA 10 | 210 - BXVIkcJMp_8 11 | 183 - 3r2UBjTRWaA 12 | 206 - OLu2TNw8csk 13 | 308 - vkq2onWT97M 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2017.7.27.1 2 | chardet==3.0.4 3 | decorator==4.0.11 4 | ffmpy==0.2.2 5 | idna==2.6 6 | imageio==2.1.2 7 | moviepy==0.2.3.2 8 | numpy==1.13.1 9 | olefile==0.44 10 | pafy==0.5.3.1 11 | Pillow==4.2.1 12 | praw==5.1.0 13 | prawcore==0.12.0 14 | requests==2.18.4 15 | scipy==0.19.1 16 | tqdm==4.11.2 17 | update-checker==0.16 18 | urllib3==1.22 19 | youtube-dl==2017.8.23 20 | -------------------------------------------------------------------------------- /music/music_url.py: -------------------------------------------------------------------------------- 1 | import pafy 2 | 3 | pl = 'https://www.youtube.com/playlist?list=PLDcPimbLEWH99SxqupfspMsNjuBrjEjoI' 4 | playlist = pafy.get_playlist(pl) 5 | with open('links.txt', 'w') as links: 6 | for v in playlist['items']: 7 | dur = v['playlist_meta']['length_seconds'] 8 | yt_id = v['playlist_meta']['encrypted_id'] 9 | 10 | if 120 < int(dur) < 540: 11 | links.write('%s - %s\n' % (dur, yt_id)) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Balázs Sáros 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is this? 2 | A python script that creates a compilation video with music from gifs from a given subreddit. [Here is an example 3 | video.](https://www.youtube.com/watch?v=mT8efxzTsSc) 4 | 5 | ## What does it need? 6 | Python3 and FFmpeg. 7 | 8 | ## How to use it? 9 | - clone or download the repo 10 | - inside the repo make a virtualenv (for 3.6): `python3 -m venv bot-env` 11 | - activate virtualenv (for 3.6): `source bot-env/bin/activate` 12 | - install dependencies: `pip3 install -r requirements.txt` 13 | - run script: `python3 app.py` 14 | - or run script with options (see details below): `python3 app.py -s woahdude -m ~/Downloads/random-song.mp3` 15 | - the result will be created in the data folder with the current date as the name and the video titled 'final.mp4' 16 | - please note: the video rendering will take some time, usually 60 to 120 minutes 17 | 18 | ## What command line options does it have? 19 | - specifying the subreddit (defaults to [r/oddlysatisfying](https://www.reddit.com/r/oddlysatisfying/)): `--subreddit [SUB NAME]` or `-s [SUB NAME]` 20 | - specifying the background music (defaults to a random track [from here](https://www.youtube.com/playlist?list=PLDcPimbLEWH99SxqupfspMsNjuBrjEjoI)): `--music [MUSIC PATH]` or `-m [MUSIC PATH]` 21 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import random 3 | import os 4 | import re 5 | import glob 6 | import shutil 7 | import time 8 | from urllib import request 9 | import json 10 | import subprocess 11 | import sys 12 | 13 | from moviepy import editor as mp 14 | from scipy.ndimage.filters import gaussian_filter 15 | from praw import Reddit 16 | import pafy 17 | from ffmpy import FFmpeg 18 | 19 | 20 | def create_dir(): 21 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 22 | dirn = 'data/%s' % time.strftime("%d_%m") 23 | 24 | if os.path.exists(dirn): 25 | shutil.rmtree(dirn) 26 | os.makedirs(dirn) 27 | os.chdir(dirn) 28 | 29 | 30 | def music_download(): 31 | with open('../../music/links.txt', 'r') as links: 32 | l = random.choice(list(links)).split(' - ') 33 | music_dur = int(l[0]) 34 | link = 'https://www.youtube.com/watch?v=%s' % l[1] 35 | 36 | audio = pafy.new(link).getbestaudio() 37 | audio.download(filepath='music.%s' % audio.extension) 38 | 39 | music = glob.glob('music.*')[0] 40 | if 'webm' in music: 41 | FFmpeg(inputs={'music.webm': None}, outputs={'music.mp3': None}).run() 42 | os.remove(music) 43 | 44 | return os.path.abspath('music.mp3'), music_dur 45 | 46 | 47 | def total_duration(): 48 | files = [f for f in glob.glob('*.mp4') if re.match('[0-9]+.mp4', f)] 49 | return sum([get_duration(f) for f in files]) 50 | 51 | 52 | def get_duration(f): 53 | intermediate = subprocess.check_output('ffmpeg -i "%s" 2>&1 | grep Duration' % f, 54 | shell=True).decode().split(': ')[1].split(', ')[0].split(':') 55 | return int(float(intermediate[-2])*60 + float(intermediate[-1])) 56 | 57 | 58 | def content_downloader(sub, dur): 59 | # get list of top content in given sub from last week 60 | search_limit = 100 61 | ro = Reddit(client_secret='', client_id='', user_agent='collects gifs for yt compilation') 62 | contents = ro.subreddit(sub).top('week', limit=search_limit) 63 | 64 | # get list of urls for 'acceptable' content 65 | final_urls = [] 66 | for x in contents: 67 | # fancy regex for every type of imgur url 68 | if bool(re.match(r'http[s]?://(i\.)?imgur.com/.*?\.gif[v]?$', x.url)): 69 | final_urls.append(re.sub(r'\.gif[v]?$', '.mp4', x.url)) 70 | elif bool(re.match(r'http[s]?://gfycat.com/', x.url)): 71 | json_url = 'https://gfycat.com/cajax/get/' + x.url.split('/')[-1] 72 | try: 73 | json_file = json.loads(request.urlopen(json_url) 74 | .read().decode()) 75 | final_urls.append(json_file["gfyItem"]["mp4Url"]) 76 | except Exception as e: 77 | print('ERROR with gfycat url\n' + str(e)) 78 | 79 | elif bool(re.match(r'http[s]?://giphy.com/', x.url)): 80 | # giphy has multiple ways of constructing urls for the same gif, this deals with that 81 | giphy_id = x.url.split('/')[-2] \ 82 | if x.url.split('.')[-1] == 'gif' \ 83 | else x.url.split('-')[-1] 84 | 85 | json_url = 'http://api.giphy.com/v1/gifs/ \ 86 | %s ?api_key=' % giphy_id 87 | try: 88 | jsonf = json.loads(request.urlopen(json_url) 89 | .read().decode()) 90 | final_urls.append(jsonf["data"]["images"]["original"]["mp4"]) 91 | except Exception as e: 92 | print('ERROR with giphy url\n' + str(e)) 93 | # TODO implement standard gif, reddit's own video and youtube support 94 | 95 | total_duration = 0 96 | audio_duration = dur 97 | for i, url in enumerate(final_urls): 98 | ext = url.split('.')[-1] 99 | try: 100 | file_name = str(i) + '.' + ext 101 | request.urlretrieve(url, file_name) 102 | f_dur = get_duration(file_name) 103 | if f_dur < 2: 104 | print('GIF is too short: %ss!' % f_dur) 105 | os.remove(file_name) 106 | elif f_dur > 15: 107 | os.remove(file_name) 108 | print('GIF is too long: %ss!' % f_dur) 109 | else: 110 | total_duration += f_dur 111 | if total_duration >= audio_duration: 112 | os.remove(file_name) 113 | print('Found enough GIF for the music!') 114 | print(total_duration) 115 | break 116 | except Exception as e: 117 | raise Exception(e) 118 | print('FINISHED!') 119 | 120 | 121 | def blur(image): 122 | return gaussian_filter(image.astype(float), sigma=12) 123 | 124 | def videofier(music_path): 125 | w, h = 1280, 720 126 | crop_size = 2 127 | files = glob.glob('*.mp4') 128 | random.shuffle(files) 129 | clip_list = [] 130 | 131 | for f in files: 132 | clip = mp.VideoFileClip(f) 133 | bc_args = {'height':h} 134 | clip_args = {'width':w} 135 | center = {'x_center':w / 2} 136 | 137 | if clip.w / clip.h < 16 / 9: 138 | bc_args, clip_args = clip_args, bc_args 139 | center = {'y_center':h / 2} 140 | 141 | blurred_clip = clip.resize(**bc_args).crop(**center, **clip_args).fl_image(blur) 142 | clip = clip.resize(**clip_args).crop(x1=crop_size, width=w - crop_size * 2, 143 | y1=crop_size, height=h - crop_size * 2).margin(crop_size, color=(0, 0, 0)) 144 | 145 | clip_list.append(mp.CompositeVideoClip([blurred_clip, clip.set_pos('center')])) 146 | 147 | final_clip = mp.concatenate_videoclips(clip_list).fadein(2).fadeout(2) 148 | final_clip.write_videofile('silent.mp4', fps=24, audio=None) 149 | 150 | FFmpeg(inputs={'silent.mp4': None, music_path: None}, outputs={'final.mp4': '-shortest'}).run() 151 | os.remove('silent.mp4') 152 | 153 | 154 | if __name__ == '__main__': 155 | dirname = create_dir() 156 | 157 | parser = argparse.ArgumentParser() 158 | parser.add_argument('--subreddit', '-s', help="name of the subreddit where it gets the gifs from", type=str, default='oddlysatisfying') 159 | parser.add_argument('--music', '-m', help="music file path to put under the final video", type=str) 160 | args=parser.parse_args() 161 | 162 | subreddit = args.subreddit 163 | if args.music: 164 | music = args.music 165 | duration = get_duration(args.music) 166 | else: 167 | music, duration = music_download() 168 | 169 | content_downloader(sub = subreddit, dur = duration) 170 | videofier(music_path = music) 171 | --------------------------------------------------------------------------------