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
```
Download a clip by slug or URL:
```
twitch-dl download VenomousTameWormHumbleLife
twitch-dl download https://www.twitch.tv/bananasaurus_rex/clip/VenomousTameWormHumbleLife
```
Man page
--------

View File

@ -24,13 +24,13 @@ List recent videos from bananasaurus\_rex's channel:
twitch-dl videos bananasaurus_rex
```
Download by URL:
Download video by URL:
```
twitch-dl download https://www.twitch.tv/videos/377220226
```
Download by ID:
Download video by ID:
```
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
```
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
youtube-dl(1)

View File

@ -7,10 +7,11 @@ import shutil
import subprocess
import tempfile
from pathlib import Path
from urllib.parse import urlparse
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.output import print_out, print_video
@ -83,18 +84,6 @@ def _video_target_filename(video, 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):
"""Extract files for download from playlist."""
vod_start = 0
@ -120,9 +109,69 @@ def _crete_temp_dir(base_uri):
return directory
def download(video_id, max_workers, format='mkv', start=None, end=None, keep=False, **kwargs):
video_id = _parse_video_id(video_id)
VIDEO_PATTERNS = [
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:
raise ConsoleError("End time must be greater than start time")

View File

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

View File

@ -21,6 +21,19 @@ def authenticated_get(url, params={}, headers={}):
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={}):
"""
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
"""
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()
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):
"""
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):
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()