Skip to content

Commit

Permalink
Merge pull request #22 from offish/v1.4.0
Browse files Browse the repository at this point in the history
v1.4.0
  • Loading branch information
offish authored Jan 22, 2021
2 parents ea6e08a + acd47d5 commit cd3fb86
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 88 deletions.
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@

Automatically make video compilations of the most viewed Twitch clips and upload them to YouTube using Python 3.

## Features
* Downloads the most popular clips from given `channel` or `game`
* Downloads only the needed clips to reach `VIDEO_LENGTH`
* Option for concatninating clips into one video
* Option for custom intro, transition and outro
* Option for custom resolution
* Option for custom frame rate
* Option for minimum video length
* Option for automatically uploading to YouTube
* Option for creating a json file with title, description and tags for given category if upload fails or is turned off
* Option for automatically deleting clips after render is done

## Example
![Screenshot](https://user-images.githubusercontent.com/30203217/103347433-4e5a7400-4a97-11eb-833a-0f5d59b0cd7e.png)

Expand Down Expand Up @@ -64,26 +76,26 @@ Set application type to Desktop app and name it whatever.
Click "Ok", and then click the download icon.
Open the JSON file that gets downloaded, select everything in this fiel and paste it into the [`client_secret.json`](twitchtube/client_secret.json) file.

### Adding/removing games
If you want to add a game/category, you simply write the name of the game how it appears on Twitch inside the `GAMES` list in [`config.py`](twitchtube/config.py).
If you want to add Rust for example, `GAMES` should look like this:
### Adding/removing to LIST
If you want to add a game or channel, you simply write the name of the game how it appears on Twitch inside the `LIST` list in [`config.py`](twitchtube/config.py).
If you want to add Rust for example, `LIST` should look like this:

```python
GAMES = ['Rust', 'Just Chatting', 'Team Fortress 2']
LIST = ['Rust', 'Just Chatting', 'Team Fortress 2']
```

Last entry in the list should not have a comma.

If you only want to have 1 game/category, `GAMES` should look like this:
If you only want to have 1 game or channel, `LIST` should look like this:

```python
GAMES = ['Just Chatting']
LIST = ['Just Chatting']
```

Example:

```python
GAMES = ['Just Chatting']
LIST = ['Just Chatting']

TAGS = {
'Just Chatting': 'just chatting, just chatting twitch, just chatting twitch highlights'
Expand All @@ -97,16 +109,16 @@ DESCRIPTIONS = {
Counter-Strike: Global Offensive is currently not supported since folders can't include colons in their folder name.

## Explanation
The script starts off by checking every game listed in the config. It will then create a folder with
The script starts off by checking every game or channel listed in the config. It will then create a folder with
the current date as folder name and inside this folder, it will create another folder for the
with the current game as folder name. It will send a request to Twitch's Kraken API
with the current game or channel as folder name. It will send a request to Twitch's Kraken API
and ask for the top 100 clips. It will then save this data in a JSON
file called `clips.json`. It will loop through the clip URLs and download each clip
till it reaches the limit specifed in the config. When the limit is reached, which means the video is
long enough it will take all the mp4 files in the game folder and doncatenete these clips into one
long enough it will take all the mp4 files in the game or channel folder and concatenete these clips into one
video (if specified). If time limit given is too big, it will just continue anyways. When the video is
done rendering, it will upload it to YouTube (if specified). When the video is uploaded it will delete
the clips (if specified) and create a new folder for the next game in the `GAMES` list (if any) and
the clips (if specified) and create a new folder for the next game or channel in `LIST` (if any) and
redo the process written above.

## Running
Expand Down
Binary file added assets/transition.mp4
Binary file not shown.
24 changes: 12 additions & 12 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@

while True:

for game in GAMES:
for category in LIST:

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

# Here we check if we've made a video for today
# by checking if the rendered file exists.
Expand All @@ -34,28 +34,28 @@
Path(path).mkdir(parents=True, exist_ok=True)

# Get the top Twitch clips
clips = get_clips(game, path)
clips = get_clips(category, path)

# 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 {category} video")
# Download all needed clips
names = download_clips(clips, VIDEO_LENGTH, path)
config = create_video_config(game, names)
config = create_video_config(category, names)

if RENDER_VIDEO:
render(path)

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

if UPLOAD_TO_YOUTUBE and RENDER_VIDEO:
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:
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
Expand All @@ -74,14 +74,14 @@

else:
# Response was most likely an Internal Server Error and we retry
log.error(
log.info(
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."
f"Already made a video for {category}. Rechecking after {TIMEOUT} seconds."
)

# Sleep for given timeout to check if it's a different date
Expand Down
2 changes: 1 addition & 1 deletion twitchtube/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__title__ = "twitchtube"
__author__ = "offish"
__license__ = "MIT"
__version__ = "1.3.3"
__version__ = "1.4.0"
6 changes: 3 additions & 3 deletions twitchtube/clips.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import urllib.request
import re

from .config import CLIENT_ID, OAUTH_TOKEN, PARAMS, HEADERS
from .config import CLIENT_ID, OAUTH_TOKEN, MODE, PARAMS, HEADERS
from .logging import Log
from .api import get

Expand Down Expand Up @@ -87,14 +87,14 @@ def download_clip(clip: str, basepath: str) -> None:
log.info(f"{slug} has been downloaded.")


def get_clips(game: str, path: str) -> dict:
def get_clips(category: str, path: str) -> dict:
"""
Gets the top clips for given game, returns JSON response
from the Kraken API endpoint.
"""
data = {}

PARAMS["game"] = game
PARAMS[MODE] = category

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

Expand Down
91 changes: 67 additions & 24 deletions twitchtube/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
# If you have a powerful enough computer you may set it to 1080p60

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

# Twitch OAuth Token
OAUTH_TOKEN = ""


# Paths
Expand All @@ -15,24 +18,64 @@


# 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
# Set the mode (game/channel)
MODE = "channel"

# If mode is channel put channel names e.g. ["trainwreckstv", "xqcow"]
# If mode is game put game names e.g. ["Team Fortress 2", "Just Chatting"]
LIST = ["trainwreckstv", "xqcow"]

# If clips should be rendered into one video (True/False)
# If set to False everything else under Video will be ignored
RENDER_VIDEO = True

# Resoultion of the rendered video (height, width) for 1080p: ((1080, 1920))
RESOLUTION = (720, 1280)

# Frames per second (30/60)
FRAMES = 30

# Minumum video length in minutes (doesn't always work)
VIDEO_LENGTH = 10.5

# Resize clips to fit RESOLUTION (True/False)
# If any RESIZE option is set to False the video might end up having a weird resolution
RESIZE_CLIPS = True

# Name of the rendered video
FILE_NAME = "rendered"

# Enable (True/False)
# Resize (True/False) read RESIZE_CLIPS
# Path to video file (str)
ENABLE_INTRO = False
RESIZE_INTRO = True
INTRO_FILE_PATH = PATH + "/assets/intro.mp4"

ENABLE_TRANSITION = True
RESIZE_TRANSITION = True
TRANSITION_FILE_PATH = PATH + "/assets/transition.mp4"

ENABLE_OUTRO = False
RESIZE_OUTRO = True
OUTRO_FILE_PATH = PATH + "/assets/outro.mp4"


# 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)
# If YouTube stuff should be saved to a separate file e.g. title, description & tags (True/False)
SAVE_TO_FILE = True

# Name of the file YouTube stuff should be saved to
SAVE_FILE_NAME = "youtube"

# If the rendered video should be uploaded to YouTube after rendering (True/False)
UPLOAD_TO_YOUTUBE = True

# If the downloaded clips should be deleted after rendering the video (True/False)
DELETE_CLIPS = True

# How often it should check if it has made a video today (in seconds)
TIMEOUT = 3600


# Twitch
Expand All @@ -44,21 +87,21 @@


# YouTube
# If empty, it would take the title of the first clip, and add "- *category* Highlights Twitch"
TITLE = ""

# YouTube Video
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

# 20 for Gaming
CATEGORY = 20

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

# YouTube Descriptions
# Descriptions
# {} will be replaced with a list of streamer names
DESCRIPTIONS = {
"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
36 changes: 18 additions & 18 deletions twitchtube/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,60 +11,60 @@ def get_date() -> str:
return date.today().strftime("%b-%d-%Y")


def create_video_config(game: str, streamers: list) -> dict:
def create_video_config(category: str, streamers: list) -> dict:
"""
Creates the video config used for uploading to YouTube
returns a dict.
"""
return {
"category": CATEGORY,
"keywords": get_tags(game),
"description": get_description(game, streamers),
"title": get_title(game),
"file": get_file(game),
"keywords": get_tags(category),
"description": get_description(category, streamers),
"title": get_title(category),
"file": get_file(category),
}


def get_tags(game: str) -> str:
def get_tags(category: str) -> str:
"""
Gets the tag for given game (if any) as a string.
Gets the tag for given category (if any) as a string.
"""
return str(TAGS.get(game))
return str(TAGS.get(category))


def get_description(game: str, streamers: list) -> str:
def get_description(category: str, streamers: list) -> str:
"""
Gets the description with given list of streamers
and game, returns the description as a string.
and category, returns the description as a string.
"""
names = "Streamers in this video:\n"

for name in streamers:
names += f"https://twitch.tv/{name}\n"

if game in DESCRIPTIONS:
return DESCRIPTIONS[game].format(names)
if category in DESCRIPTIONS:
return DESCRIPTIONS[category].format(names)
return names


def get_title(game: str) -> str:
def get_title(category: str) -> str:
"""
Gets the title and returns it as a string.
"""
if TITLE:
return TITLE

title = json.loads(
open(f"{CLIP_PATH.format(get_date(), game)}/clips.json", "r").read()
open(f"{CLIP_PATH.format(get_date(), category)}/clips.json", "r").read()
)

for i in title:
# Return the first entry's title
return f"{title[i]['title']} - {game} Twitch Highlights"
return f"{title[i]['title']} - {category} Twitch Highlights"


def get_file(game: str) -> str:
def get_file(category: str) -> str:
"""
Gets the path/file given game and returns it as a string.
Gets the path/file given category and returns it as a string.
"""
return f"{CLIP_PATH.format(get_date(), game)}\\{FILE_NAME}.mp4"
return f"{CLIP_PATH.format(get_date(), category)}\\{FILE_NAME}.mp4"
Loading

0 comments on commit cd3fb86

Please sign in to comment.