Skip to content

Commit

Permalink
Merge pull request #20 from offish/v1.3.3
Browse files Browse the repository at this point in the history
v1.3.3
  • Loading branch information
offish authored Jan 14, 2021
2 parents 9fc41ce + 4c34d43 commit ea6e08a
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 130 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
[![Stars](https://img.shields.io/github/stars/offish/twitchtube.svg)](https://github.com/offish/twitchtube/stargazers)
[![Issues](https://img.shields.io/github/issues/offish/twitchtube.svg)](https://github.com/offish/twitchtube/issues)
[![Size](https://img.shields.io/github/repo-size/offish/twitchtube.svg)](https://github.com/offish/twitchtube)
[![Chat](https://img.shields.io/discord/467040686982692865.svg)](https://discord.gg/t8nHSvA)
[![Discord](https://img.shields.io/discord/467040686982692865?color=7289da&label=Discord&logo=discord)](https://discord.gg/t8nHSvA)
[![Code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

[![Donate Steam](https://img.shields.io/badge/donate-steam-green.svg)](https://steamcommunity.com/tradeoffer/new/?partner=293059984&token=0-l_idZR)
[![Donate PayPal](https://img.shields.io/badge/donate-paypal-blue.svg)](https://www.paypal.me/0ffish)
Expand Down
35 changes: 21 additions & 14 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from os.path import exists
from pathlib import Path
from time import sleep
from json import dump
from json import dump, JSONDecodeError
from glob import glob
from os import remove

Expand All @@ -15,16 +15,16 @@

log = Log()


while True:

for game in GAMES:

path = CLIP_PATH.format(get_date(), game)

# Here we check if we've made a video for today
# by checking if the rendered file exists.
if not exists(path + f'\\{FILE_NAME}.mp4'):
if not exists(path + f"\\{FILE_NAME}.mp4"):

# We want to retry because Twitch often gives a
# 500 Internal Server Error when trying to get clips
Expand All @@ -38,7 +38,7 @@

# Check if the API gave us a successful response
if clips:
log.info(f'Starting to make a video for {game}')
log.info(f"Starting to make a video for {game}")
# Download all needed clips
names = download_clips(clips, VIDEO_LENGTH, path)
config = create_video_config(game, names)
Expand All @@ -47,35 +47,42 @@
render(path)

if UPLOAD_TO_YOUTUBE:
upload_video_to_youtube(config)

try:
upload_video_to_youtube(config)
except JSONDecodeError:
log.error("Your client_secret is empty or has wrong syntax")

if SAVE_TO_FILE:
with open(path + f'\\{SAVE_FILE_NAME}.json', 'w') as f:
with open(path + f"\\{SAVE_FILE_NAME}.json", "w") as f:
dump(config, f, indent=4)

if DELETE_CLIPS:
# Get all the mp4 files in the path and delte them
# if they're not the rendered video
files = glob(f'{path}/*.mp4')
files = glob(f"{path}/*.mp4")

for file in files:
if not file == path + f'\\{FILE_NAME}.mp4':
if not file == path + f"\\{FILE_NAME}.mp4":
try:
remove(file)
# Sometimes a clip is "still being used" giving
# us an exception that would else crash the program
except PermissionError as e:
log.error(f'Could not delete {file} because {e}')
log.error(f"Could not delete {file} because {e}")

break

else:
# Response was most likely an Internal Server Error and we retry
log.error(f'There was an error or timeout on Twitch\'s end, retrying... {i + 1}/{RETRIES}')
log.error(
f"There was an error or timeout on Twitch's end, retrying... {i + 1}/{RETRIES}"
)

else:
# Rendered video does already exist
log.info(f'Already made a video for {game}. Rechecking after {TIMEOUT} seconds.')
log.info(
f"Already made a video for {game}. Rechecking after {TIMEOUT} seconds."
)

# Sleep for given timeout to check if it's a different date
sleep(TIMEOUT)
9 changes: 4 additions & 5 deletions twitchtube/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

__title__ = 'twitchtube'
__author__ = 'offish'
__license__ = 'MIT'
__version__ = '1.3.2'
__title__ = "twitchtube"
__author__ = "offish"
__license__ = "MIT"
__version__ = "1.3.3"
19 changes: 5 additions & 14 deletions twitchtube/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,20 @@

def request(endpoint: str, headers: dict, params: dict) -> dict:
return requests.get(
'https://api.twitch.tv/' + endpoint,
headers = headers,
params = params
"https://api.twitch.tv/" + endpoint, headers=headers, params=params
)


def data(slug: str) -> dict:
return request(
'helix/clips',
{
'Authorization': f'Bearer {OAUTH_TOKEN}',
'Client-Id': CLIENT_ID
},
{'id': slug}
"helix/clips",
{"Authorization": f"Bearer {OAUTH_TOKEN}", "Client-Id": CLIENT_ID},
{"id": slug},
)


def top_clips(headers: dict, params: dict) -> dict:
return request(
'kraken/clips/top',
headers,
params
)
return request("kraken/clips/top", headers, params)


def get(name: str, **args) -> dict:
Expand Down
79 changes: 40 additions & 39 deletions twitchtube/clips.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ def get_data(slug: str) -> dict:
Gets the data from a given slug,
returns a JSON respone from the Helix API endpoint
"""
response = get('data', slug=slug)
response = get("data", slug=slug)

try:
return response['data'][0]
return response["data"][0]
except KeyError as e:
log.error(f'Ran into exception: {e}')
log.error(f'Response: {response}')
log.error(f"Ran into exception: {e}")
log.error(f"Response: {response}")
return response


Expand All @@ -36,34 +36,35 @@ def get_clip_data(slug: str) -> tuple:
"""
clip_info = get_data(slug)

if 'thumbnail_url' in clip_info \
and 'title' in clip_info:
# All to get what we need to return
if "thumbnail_url" in clip_info and "title" in clip_info:
# All to get what we need to return
# the mp4_url and title of the clip
thumb_url = clip_info['thumbnail_url']
title = clip_info['title']
slice_point = thumb_url.index('-preview-')
mp4_url = thumb_url[:slice_point] + '.mp4'
thumb_url = clip_info["thumbnail_url"]
title = clip_info["title"]
slice_point = thumb_url.index("-preview-")
mp4_url = thumb_url[:slice_point] + ".mp4"

return mp4_url, title

raise TypeError(f'We didn\'t receieve what we wanted. /helix/clips endpoint gave:\n{clip_info}')
raise TypeError(
f"We didn't receieve what we wanted. /helix/clips endpoint gave:\n{clip_info}"
)


def get_progress(count, block_size, total_size) -> None:
"""
Used for printing the download progress
"""
percent = int(count * block_size * 100 / total_size)
print(f'Downloading clip... {percent}%', end='\r', flush=True)
print(f"Downloading clip... {percent}%", end="\r", flush=True)


def get_slug(clip: str) -> str:
"""
Splits up the URL given and returns the slug
of the clip.
"""
slug = clip.split('/')
slug = clip.split("/")
return slug[len(slug) - 1]


Expand All @@ -73,17 +74,17 @@ def download_clip(clip: str, basepath: str) -> None:
"""
slug = get_slug(clip)
mp4_url, clip_title = get_clip_data(slug)
# Remove special characters so we can save the video
regex = re.compile('[^a-zA-Z0-9_]')
clip_title = clip_title.replace(' ', '_')
out_filename = regex.sub('', clip_title) + '.mp4'
output_path = (basepath + '/' + out_filename)
# Remove special characters so we can save the video
regex = re.compile("[^a-zA-Z0-9_]")
clip_title = clip_title.replace(" ", "_")
out_filename = regex.sub("", clip_title) + ".mp4"
output_path = basepath + "/" + out_filename

log.info(f'Downloading clip with slug: {slug}.')
log.info(f"Downloading clip with slug: {slug}.")
log.info(f'Saving "{clip_title}" as "{out_filename}".')
# Download the clip with given mp4_url
urllib.request.urlretrieve(mp4_url, output_path, reporthook=get_progress)
log.info(f'{slug} has been downloaded.')
log.info(f"{slug} has been downloaded.")


def get_clips(game: str, path: str) -> dict:
Expand All @@ -93,28 +94,28 @@ def get_clips(game: str, path: str) -> dict:
"""
data = {}

PARAMS['game'] = game
PARAMS["game"] = game

response = get('top_clips', headers=HEADERS, params=PARAMS)
response = get("top_clips", headers=HEADERS, params=PARAMS)

if 'clips' in response:
if "clips" in response:

for clip in response['clips']:
data[clip['tracking_id']] = {
'url': 'https://clips.twitch.tv/' + clip['slug'],
'title': clip['title'],
'display_name': clip['broadcaster']['display_name'],
'duration': clip['duration']
for clip in response["clips"]:
data[clip["tracking_id"]] = {
"url": "https://clips.twitch.tv/" + clip["slug"],
"title": clip["title"],
"display_name": clip["broadcaster"]["display_name"],
"duration": clip["duration"],
}

# Save the data to a JSON file
with open(f'{path}/clips.json', 'w') as f:
with open(f"{path}/clips.json", "w") as f:
dump(data, f, indent=4)

return data

else:
log.error(f'Could not find \'clips\' in response. {response}')
log.error(f"Could not find 'clips' in response. {response}")

return {}

Expand All @@ -129,23 +130,23 @@ def download_clips(data: dict, length: float, path: str) -> list:

for clip in data:

download_clip(data[clip]['url'], path)
length -= data[clip]['duration']
download_clip(data[clip]["url"], path)
length -= data[clip]["duration"]

name = data[clip]['display_name']
name = data[clip]["display_name"]
amount += 1

if name not in names:
names.append(name)
log.info(f'Remaining video length: {ceil(length)} seconds.\n')

log.info(f"Remaining video length: {ceil(length)} seconds.\n")

# If the rendered video would be long enough
# we break out of the loop, else continue
if length <= 0:
break

# If the rendered video would be long enough or we
# have ran out of clips, we return the streamer names
log.info(f'Downloaded {amount} clips.\n')
log.info(f"Downloaded {amount} clips.\n")
return names
50 changes: 27 additions & 23 deletions twitchtube/config.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,64 @@
import pathlib

# Note:
# Note:
# Changing FRAMES and or RESOULTION will heavily impact load on CPU.
# If you have a powerful enough computer you may set it to 1080p60
# If you have a powerful enough computer you may set it to 1080p60

# Secrets
CLIENT_ID = '' # Twitch Client ID
OAUTH_TOKEN = '' # Twitch OAuth Token
CLIENT_ID = "" # Twitch Client ID
OAUTH_TOKEN = "" # Twitch OAuth Token


# Paths
PATH = str(pathlib.Path().absolute())
CLIP_PATH = PATH + '\\clips\\{}\\{}'
CLIP_PATH = PATH + "\\clips\\{}\\{}"


# Video
GAMES = ['Just Chatting', 'Team Fortress 2']
VIDEO_LENGTH = 10.5 # in minutes (doesn't always work for some reason)
RENDER_VIDEO = True # If downloaded clips should be rendered into one video (True/False)
FRAMES = 30 # Frames per second (30/60)
RESOLUTION = (720, 1280) # (height, width) for 1080p: (1080, 1920)
FILE_NAME = 'rendered' # Name of the rendered video
GAMES = ["Just Chatting", "Team Fortress 2"]
VIDEO_LENGTH = 10.5 # in minutes (doesn't always work for some reason)
RENDER_VIDEO = (
True # If downloaded clips should be rendered into one video (True/False)
)
FRAMES = 30 # Frames per second (30/60)
RESOLUTION = (720, 1280) # (height, width) for 1080p: (1080, 1920)
FILE_NAME = "rendered" # Name of the rendered video


# Other
SAVE_TO_FILE = True # If YouTube stuff should be saved to a separate file e.g. title, description & tags (True/False)
SAVE_FILE_NAME = 'youtube' # Name of the file YouTube stuff should be saved to
UPLOAD_TO_YOUTUBE = True # If the video should be uploaded to YouTube after rendering (True/False)
DELETE_CLIPS = True # If the downloaded clips should be deleted after rendering the video (True/False)
TIMEOUT = 3600 # How often it should check if it has made a video today (in seconds)
SAVE_TO_FILE = True # If YouTube stuff should be saved to a separate file e.g. title, description & tags (True/False)
SAVE_FILE_NAME = "youtube" # Name of the file YouTube stuff should be saved to
UPLOAD_TO_YOUTUBE = (
True # If the video should be uploaded to YouTube after rendering (True/False)
)
DELETE_CLIPS = True # If the downloaded clips should be deleted after rendering the video (True/False)
TIMEOUT = 3600 # How often it should check if it has made a video today (in seconds)


# Twitch
RETRIES = 5

# Twitch API Request Options
HEADERS = {'Accept': 'application/vnd.twitchtv.v5+json', 'Client-ID': CLIENT_ID}
PARAMS = {'period': 'day', 'language': 'en', 'limit': 100} # 100 is max
HEADERS = {"Accept": "application/vnd.twitchtv.v5+json", "Client-ID": CLIENT_ID}
PARAMS = {"period": "day", "language": "en", "limit": 100} # 100 is max


# YouTube

# YouTube Video
TITLE = '' # If not given a title it would take the title of the first clip, and add "- *game* Highlights Twitch"
TITLE = "" # If not given a title it would take the title of the first clip, and add "- *game* Highlights Twitch"
CATEGORY = 20 # 20 for Gaming


# YouTube Tags
TAGS = {
'Just Chatting': 'just chatting, just chatting clips, just chatting twitch clips',
'Team Fortress 2': 'tf2, tf2 twitch, tf2 twitch clips'
"Just Chatting": "just chatting, just chatting clips, just chatting twitch clips",
"Team Fortress 2": "tf2, tf2 twitch, tf2 twitch clips",
}

# YouTube Descriptions
DESCRIPTIONS = {
'Just Chatting': 'Just Chatting twitch clips \n\n{}\n',
'Team Fortress 2': 'TF2 twitch clips\n\n{}\n'
"Just Chatting": "Just Chatting twitch clips \n\n{}\n",
"Team Fortress 2": "TF2 twitch clips\n\n{}\n",
}
# {} will be replaced with a list of streamer names
Loading

0 comments on commit ea6e08a

Please sign in to comment.