Implement downloading clips

issue #15
This commit is contained in:
Ivan Habunek 2020-04-11 16:07:17 +02:00
parent 96f13e9cf7
commit 07f3a2fa48
No known key found for this signature in database
GPG Key ID: CDBD63C43A30BB95
5 changed files with 135 additions and 22 deletions

View File

@ -50,6 +50,13 @@ twitch-dl download 221837124
twitch-dl download https://www.twitch.tv/videos/221837124 twitch-dl download https://www.twitch.tv/videos/221837124
``` ```
Download a clip by slug or URL:
```
twitch-dl download VenomousTameWormHumbleLife
twitch-dl download https://www.twitch.tv/bananasaurus_rex/clip/VenomousTameWormHumbleLife
```
Man page Man page
-------- --------

View File

@ -24,13 +24,13 @@ List recent videos from bananasaurus\_rex's channel:
twitch-dl videos bananasaurus_rex twitch-dl videos bananasaurus_rex
``` ```
Download by URL: Download video by URL:
``` ```
twitch-dl download https://www.twitch.tv/videos/377220226 twitch-dl download https://www.twitch.tv/videos/377220226
``` ```
Download by ID: Download video by ID:
``` ```
twitch-dl download 377220226 twitch-dl download 377220226
@ -48,6 +48,21 @@ Partial download by setting start and end time (hh:mm or hh:mm:ss):
twitch-dl download --start=00:10 --end=02:15 377220226 twitch-dl download --start=00:10 --end=02:15 377220226
``` ```
Download clip by URL:
```
twitch-dl download https://www.twitch.tv/bananasaurus_rex/clip/VenomousTameWormHumbleLife
```
Download clip by slug:
```
twitch-dl download VenomousTameWormHumbleLife
```
Note that clips are a single download, and don't benefit from the paralelism
used when downloading videos.
# SEE ALSO # SEE ALSO
youtube-dl(1) youtube-dl(1)

View File

@ -7,10 +7,11 @@ import shutil
import subprocess import subprocess
import tempfile import tempfile
from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
from twitchdl import twitch, utils from twitchdl import twitch, utils
from twitchdl.download import download_files from twitchdl.download import download_file, download_files
from twitchdl.exceptions import ConsoleError from twitchdl.exceptions import ConsoleError
from twitchdl.output import print_out, print_video from twitchdl.output import print_out, print_video
@ -83,18 +84,6 @@ def _video_target_filename(video, format):
return name + "." + format return name + "." + format
def _parse_video_id(video_id):
"""This can be either a integer ID or an URL to the video on twitch."""
if re.search(r"^\d+$", video_id):
return int(video_id)
match = re.search(r"^https://www.twitch.tv/videos/(\d+)(\?.+)?$", video_id)
if match:
return int(match.group(1))
raise ConsoleError("Invalid video ID given, expected integer ID or Twitch URL")
def _get_files(playlist, start, end): def _get_files(playlist, start, end):
"""Extract files for download from playlist.""" """Extract files for download from playlist."""
vod_start = 0 vod_start = 0
@ -120,9 +109,69 @@ def _crete_temp_dir(base_uri):
return directory return directory
def download(video_id, max_workers, format='mkv', start=None, end=None, keep=False, **kwargs): VIDEO_PATTERNS = [
video_id = _parse_video_id(video_id) r"^(?P<id>\d+)?$",
r"^https://www.twitch.tv/videos/(?P<id>\d+)(\?.+)?$",
]
CLIP_PATTERNS = [
r"^(?P<slug>[A-Za-z]+)$",
r"^https://www.twitch.tv/\w+/clip/(?P<slug>[A-Za-z]+)(\?.+)?$",
r"^https://clips.twitch.tv/(?P<slug>[A-Za-z]+)(\?.+)?$",
]
def download(video, **kwargs):
for pattern in CLIP_PATTERNS:
match = re.match(pattern, video)
if match:
clip_slug = match.group('slug')
return _download_clip(clip_slug, **kwargs)
for pattern in VIDEO_PATTERNS:
match = re.match(pattern, video)
if match:
video_id = match.group('id')
return _download_video(video_id, **kwargs)
raise ConsoleError("Invalid video: {}".format(video_id))
def _download_clip(slug, **kwargs):
print_out("Looking up clip...")
clip = twitch.get_clip(slug)
print_out("Found: <green>{}</green> by <yellow>{}</yellow>, playing <blue>{}</blue> ({})".format(
clip["title"],
clip["broadcaster"]["displayName"],
clip["game"]["name"],
utils.format_duration(clip["durationSeconds"])
))
print_out("\nAvailable qualities:")
qualities = clip["videoQualities"]
for n, q in enumerate(qualities):
print_out("{}) {} [{} fps]".format(n + 1, q["quality"], q["frameRate"]))
no = utils.read_int("Choose quality", min=1, max=len(qualities), default=1)
selected_quality = qualities[no - 1]
url = selected_quality["sourceURL"]
url_path = urlparse(url).path
extension = Path(url_path).suffix
filename = "{}_{}{}".format(
clip["broadcaster"]["login"],
utils.slugify(clip["title"]),
extension
)
print("Downloading clip...")
download_file(url, filename)
print("Downloaded: {}".format(filename))
def _download_video(video_id, max_workers, format='mkv', start=None, end=None, keep=False, **kwargs):
if start and end and end <= start: if start and end and end <= start:
raise ConsoleError("End time must be greater than start time") raise ConsoleError("End time must be greater than start time")

View File

@ -63,13 +63,13 @@ COMMANDS = [
name="download", name="download",
description="Download a video", description="Download a video",
arguments=[ arguments=[
(["video_id"], { (["video"], {
"help": "video ID", "help": "video ID, clip slug, or URL",
"type": str, "type": str,
}), }),
(["-w", "--max_workers"], { (["-w", "--max_workers"], {
"help": "maximal number of threads for downloading vods " "help": "maximal number of threads for downloading vods "
"concurrently (default 5)", "concurrently (default 20)",
"type": int, "type": int,
"default": 20, "default": 20,
}), }),

View File

@ -21,6 +21,19 @@ def authenticated_get(url, params={}, headers={}):
return response return response
def authenticated_post(url, data=None, json=None, headers={}):
headers['Client-ID'] = CLIENT_ID
response = requests.post(url, data=data, json=json, headers=headers)
if response.status_code == 400:
data = response.json()
raise ConsoleError(data["message"])
response.raise_for_status()
return response
def kraken_get(url, params={}, headers={}): def kraken_get(url, params={}, headers={}):
""" """
Add accept header required by kraken API v5. Add accept header required by kraken API v5.
@ -46,11 +59,40 @@ def get_video(video_id):
""" """
https://dev.twitch.tv/docs/v5/reference/videos#get-video https://dev.twitch.tv/docs/v5/reference/videos#get-video
""" """
url = "https://api.twitch.tv/kraken/videos/%d" % video_id url = "https://api.twitch.tv/kraken/videos/{}".format(video_id)
return kraken_get(url).json() return kraken_get(url).json()
def get_clip(slug):
url = "https://gql.twitch.tv/gql"
query = """
{{
clip(slug: "{}") {{
title
durationSeconds
game {{
name
}}
broadcaster {{
login
displayName
}}
videoQualities {{
frameRate
quality
sourceURL
}}
}}
}}
"""
payload = {"query": query.format(slug)}
data = authenticated_post(url, json=payload).json()
return data["data"]["clip"]
def get_channel_videos(channel_id, limit, offset, sort): def get_channel_videos(channel_id, limit, offset, sort):
""" """
https://dev.twitch.tv/docs/v5/reference/channels#get-channel-videos https://dev.twitch.tv/docs/v5/reference/channels#get-channel-videos
@ -66,7 +108,7 @@ def get_channel_videos(channel_id, limit, offset, sort):
def get_access_token(video_id): def get_access_token(video_id):
url = "https://api.twitch.tv/api/vods/%d/access_token" % video_id url = "https://api.twitch.tv/api/vods/{}/access_token".format(video_id)
return authenticated_get(url).json() return authenticated_get(url).json()