mirror of
https://github.com/ihabunek/twitch-dl
synced 2024-08-30 18:32:25 +00:00
parent
96f13e9cf7
commit
07f3a2fa48
@ -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
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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,
|
||||||
}),
|
}),
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user