class Downloader: def __init__(self, error_messages, packager, p_tokens_file): self.__error_messages = error_messages self.__packager = packager self.__p_tokens_file = p_tokens_file self.__logger = Logger() def commander(self, command, args): ''' download (package name[0]): downloads clips from given package ''' if command == 'download': filter = self.__filter(args, 1) if filter: name = args[0] package = self.__packager.get(name) for streamer in package.get_data()['streamers']: self.__download(streamer, package) return True return False def __filter(self, args, ammount): if len(args) >= ammount: return True else: print(ConsoleColors.RED + 'Error: {0}'.format(self.__error_messages[0]) + ConsoleColors.RESET) def __check_paths(self, path): if not os.path.exists(self.__p_packages_file): os.mkdir(path) def __download(self, streamer, package): with open(self.__p_tokens_file, 'r') as f: client_id = json.load(f)['twitch']['client_id'] access_token, broad_id = Downloader.check_username(streamer).split("&") if package.get_data()['period'] == 'week': start = generate(datetime.utcnow().replace(tzinfo=pytz.utc) - timedelta(days=7)) end = generate(datetime.utcnow().replace(tzinfo=pytz.utc)) elif package.get_data()['period'] == 'month': start = generate(datetime.utcnow().replace(tzinfo=pytz.utc) - timedelta(days=30)) end = generate(datetime.utcnow().replace(tzinfo=pytz.utc)) count = 0 pagination = '' times = list() game_ids = dict() while count < int(package.get_data()['limit']): r = requests.get( 'https://api.twitch.tv/helix/clips?broadcaster_id={}&first={}&started_at={}&ended_at={}&after={}' .format(broad_id, package.get_data()['limit'], start, end, pagination), headers={ 'Authorization': 'Bearer ' + access_token, 'Client-ID': client_id }) clips = r.json()['data'] if len(clips) > 0: for clip in clips: if type(clip) == dict: if clip['game_id'] not in game_ids: game_ids[clip['game_id']] = 1 else: game_ids[clip['game_id']] += 1 thumbnail_url = clip['thumbnail_url'] mp4_url = thumbnail_url.split('-preview', 1)[0] + '.mp4' number = count + 1 if len(str(number)) == 1: number = '0' + str(number) mp4_name = str( Path(package.get_data()['clips_folder']) / str(str(number) + '.mp4')) self.__logger.log("Downloading: " + str(mp4_name)) res = requests.get(mp4_url) with open(mp4_name, 'wb') as f: f.write(res.content) cmd = 'ffprobe -show_entries format=duration -v quiet -of csv="p=0" ' + mp4_name duration = float(os.popen(cmd).read()) created_at = datetime.strptime(clip['created_at'], '%Y-%m-%dT%H:%M:%SZ') ended_at = created_at + timedelta(0, duration) exist = False if len(times) > 0: for i in range(len(times)): if times[i]['created_at'] < created_at < times[ i]['ended_at'] or times[i][ 'created_at'] < ended_at < times[ i]['ended_at']: exist = True break else: time_meta = { 'created_at': created_at, 'ended_at': ended_at } times.append(time_meta) else: time_meta = { 'created_at': created_at, 'ended_at': ended_at } times.append(time_meta) if exist: os.remove(mp4_name) else: count += 1 if count >= int(package.get_data()['limit']): break pagination = r.json()['pagination']['cursor'] else: raise Exception('Clips not found') games_sorted = sorted(game_ids.items(), key=lambda x: x[1], reverse=True) stop = 1 if len(games_sorted) > 1: stop = 2 games = list() for i in range(stop): r = requests.get('https://api.twitch.tv/helix/games?id=' + games_sorted[i][0], headers={ 'Authorization': 'Bearer ' + access_token, 'Client-ID': client_id }) games.append(r.json()['data'][0]['name']) package.get_data()['additional_info']['games'] = games package.update() self.__logger.separator() @staticmethod def check_username(username): with open(Path('tokens/tokens.json'), 'r') as f: credentials = json.load(f)['twitch'] try: r = requests.post( 'https://id.twitch.tv/oauth2/token?client_id={0}&client_secret={1}&grant_type=client_credentials' .format(credentials['client_id'], credentials['client_secret'])) access_token = r.json()['access_token'] r = requests.get('https://api.twitch.tv/helix/users?login='******'Authorization': 'Bearer ' + access_token, 'Client-ID': credentials['client_id'] }) broad_id = r.json()['data'][0]['id'] if not broad_id: print('Bad request, check username or user banned') return False except: print('Failed to connect to Twitch API') return False return access_token + '&' + broad_id
class Editor: def __init__(self, error_messages, packager, p_videos_media): self.__error_messages = error_messages self.__packager = packager self.__p_videos_media = p_videos_media self.__logger = Logger() def commander(self, command, args): ''' edit (package name[0]): concatenates all clips + outro, then inserts video intro at the beginning ''' if command == 'edit': filter = self.__filter(args, 1) if filter: name = args[0] package = self.__packager.get(name) if not package: return True p_clips = package.get_data()['clips_folder'] p_output = package.get_data()['output_folder'] concat_output = Path(p_output) / 'concat.txt' if os.path.exists(concat_output): os.remove(concat_output) with open(concat_output, 'x') as f: for subdir, dirs, files in os.walk(p_clips): files.sort() for file in files: mp4_file = Path(p_clips) / file f.write('file ' + str(mp4_file) + '\n') f.write('file ' + str(self.__p_videos_media / 'outro.mp4')) date = datetime.now() output_video = Path(p_output) / str(name + date.strftime('%Y-%m-%d') + '.mp4') self.__generate_video(package, concat_output, output_video) self.__logger.log('video {} created'.format(output_video)) self.__logger.separator() return True else: return False def __filter(self, args, ammount): if len(args) >= ammount: return True else: print(ConsoleColors.RED + 'Error: {0}'.format(self.__error_messages[0]) + ConsoleColors.RESET) def __generate_video(self, p, of, ov): cmd = 'ffmpeg -f concat -safe 0 -i {} -c copy {}'.format(of, ov) r = subprocess.run(cmd, shell=True, capture_output=True) intro = Path(self.__p_videos_media) / 'intro.mov' final_ov = str(ov).replace('mp4', '') + 'final.mp4' cmd = 'ffmpeg -i {} -i {} -filter_complex "[1:v]setpts=PTS-0/TB[a]; [0:v][a]overlay=enable=gte(t\,0):eof_action=pass[out]; [0][1]amix[a]" -map [out] -map [a] -c:v libx264 -crf 18 -pix_fmt yuv420p {}'.format(ov, intro, final_ov) r = subprocess.run(cmd, shell=True, capture_output=True) p.get_data()['additional_info']['output_video'] = str(final_ov) p.update() ''' create twitter_video ''' twitter_video = 'test.mp4' p.get_data()['additional_info']['twitter_video'] = twitter_video p.update()
class Encoder: __DEFAULT = { 'FPS': '60/1', 'WIDTH': '1920', 'HEIGHT': '1080', 'TBN': '1/15360', 'HZ': '1/44100' } def __init__(self, error_messages, packager): self.__error_messages = error_messages self.__packager = packager self.__logger = Logger() def commander(self, command, args): ''' encode (package name[0]): encodes clips to make all clips has the same parameters options ''' if command == 'encode': filter = self.__filter(args, 1) if filter: name = args[0] package = self.__packager.get(name) if not package: return True folder = package.get_data()['clips_folder'] for subdir, dirs, files in os.walk(folder): files.sort() for file in files: mp4_file = Path(folder) / file opt = self.__check_video(mp4_file) if opt == '': continue else: self.__logger.log('Re-encoding video {}'.format(mp4_file)) cmd = 'ffmpeg -i ' + str(mp4_file) + opt + str(mp4_file).replace('.mp4', '') + 'converted.mp4' r = subprocess.run(cmd, capture_output=True, shell=True) os.remove(mp4_file) self.__logger.separator() return True else: return False def __filter(self, args, ammount): if len(args) >= ammount: return True else: print(ConsoleColors.RED + 'Error: {0}'.format(self.__error_messages[0]) + ConsoleColors.RESET) def __check_video(self, video): cmd = 'ffprobe -v error -of json -show_entries stream=time_base,r_frame_rate,width,height ' + str(video) r = os.popen(cmd).read() j_streams = json.loads(r)['streams'] streams = list([{}, {}]) for j_stream in j_streams: if 'width' in j_stream.keys(): streams[0] = j_stream else: streams[1] = j_stream opt = '' if str(streams[0]['width']) != self.__DEFAULT['WIDTH']: opt = opt + ' -vf scale=' + self.__DEFAULT['WIDTH'] + ':' + self.__DEFAULT['HEIGHT'] opt = opt + ' -video_track_timescale ' + self.__DEFAULT['TBN'][2:] opt = opt + ' -r ' + self.__DEFAULT['FPS'][:-2] else: if streams[0]['time_base'] != self.__DEFAULT['TBN']: opt = opt + ' -video_track_timescale ' + self.__DEFAULT['TBN'][2:] if streams[0]['r_frame_rate'] != self.__DEFAULT['FPS']: opt = opt + ' -r ' + self.__DEFAULT['FPS'][:-2] if streams[1]['time_base'] != self.__DEFAULT['HZ']: opt = opt + ' -ar ' + self.__DEFAULT['HZ'][2:] if opt != '': opt = opt + ' ' return opt
class Uploader: def __init__(self, error_mesages, packager, p_tokens, p_videos_media): self.__error_messages = error_mesages self.__packager = packager self.__p_request_token = p_tokens / 'youtube/request.token' self.__p_client_secrets = p_tokens / 'youtube/client_secrets.json' self.__p_videos_metada = p_videos_media / 'metadata.json' self.__logger = Logger() def commander(self, command, args): ''' upload (package name[0]): uploads video to google storage and youtube depending on package settings ''' if command == 'upload': filter = self.__filter(args, 1) if filter: name = args[0] package = self.__packager.get(name) if not package: return True if package.get_data()['upload_google']: self.__upload_google(package) if package.get_data()['upload_youtube']: self.__upload_youtube(package) return True else: return False def __filter(self, args, ammount): if len(args) >= ammount: return True else: print(ConsoleColors.RED + 'Error: {0}'.format(self.__error_messages[0]) + ConsoleColors.RESET) def __upload_google(self, package): cmd = 'gsutil cp {} gs://clipperstorage/output/'.format( package.get_data()['additional_info']['output_video']) r = subprocess.run(cmd, shell=True, capture_output=True) self.__logger.log('Video {} uploaded to google storage'.format( package.get_data()['additional_info']['output_video'])) self.__logger.separator() def __upload_youtube(self, package): with open(self.__p_videos_metada, 'r') as f: lang = package.get_data()['language'] for md in json.loads(f.read())['video_metadata']: if md['language'] == lang: data = md number = str(random.randint(1, 15)) thumbnail = Path( package.get_data()['thumbanils_folder']) / str(number + '.png') name = package.get_data()['streamers'][0] meta = { 'title': data['title'].format(name), 'description': data['description'].format(name, package.get_data()['twitch_urls'][0]), 'privacyStatus': 'public', 'playlistTitles': [data['playlist'].format(name)], 'tags': data['tags'].format(name).split(','), 'language': data['language'] } for game in package.get_data()['additional_info']['games']: meta['tags'].append(game) meta['tags'].append(game + ' ' + name) meta_file = Path( package.get_data()['output_folder']) / 'meta_file.json' if os.path.exists(meta_file): os.remove(meta_file) with open(meta_file, 'x') as f: json.dump(meta, f) cmd = './youtubeuploader -metaJSON {0} -thumbnail {1} -filename {2} -cache {3} -secrets {4}'.format( meta_file, thumbnail, package.get_data()['additional_info']['output_video'], self.__p_request_token, self.__p_client_secrets) r = subprocess.run(cmd, shell=True, capture_output=True) output = r.stdout error = str(r.stderr) if 'quota' in error: self.__logger.log( 'Video {} cold not be uploaded to YouTube due to API quota limits' .format(package.get_data()['additional_info']['output_video'])) self.__logger.separator() else: self.__logger.log('Video {} uploaded to YouTube'.format( package.get_data()['additional_info']['output_video'])) self.__logger.separator() output = output.decode('utf-8') list_output = output.split(' ') for i in range(len(list_output)): if list_output[i] == 'ID:': index = i + 1 video_id = list_output[index].replace('\nThumbnail', '') url = 'https://www.youtube.com/watch?v=' + video_id package.get_data()['additional_info']['url_video'] = url package.update() return True