Add twitch-dl info command

This commit is contained in:
Ivan Habunek 2021-01-14 21:38:56 +01:00
parent 2380dc5a35
commit 548a9350ba
No known key found for this signature in database
GPG Key ID: CDBD63C43A30BB95
9 changed files with 200 additions and 51 deletions

View File

@ -1,9 +1,11 @@
from .clips import clips
from .download import download
from .info import info
from .videos import videos
__all__ = [
clips,
download,
info,
videos,
]

View File

@ -55,7 +55,7 @@ def _clip_target_filename(clip):
name = "_".join([
date,
clip["id"],
clip["broadcaster"]["channel"]["name"],
clip["broadcaster"]["login"],
utils.slugify(clip["title"]),
])
@ -96,6 +96,7 @@ def clips(args):
print_out("<yellow>Showing clips {}-{} of ??</yellow>".format(first, last))
for clip in clips["edges"]:
print_out()
print_clip(clip["node"])
if not args.pager:

View File

@ -68,13 +68,13 @@ def _join_vods(playlist_path, target, overwrite):
def _video_target_filename(video, format):
match = re.search(r"^(\d{4})-(\d{2})-(\d{2})T", video['published_at'])
match = re.search(r"^(\d{4})-(\d{2})-(\d{2})T", video['publishedAt'])
date = "".join(match.groups())
name = "_".join([
date,
video['_id'][1:],
video['channel']['name'],
video['id'][1:],
video['creator']['login'],
utils.slugify(video['title']),
])
@ -92,7 +92,7 @@ def _clip_target_filename(clip):
name = "_".join([
date,
clip["id"],
clip["broadcaster"]["channel"]["name"],
clip["broadcaster"]["login"],
utils.slugify(clip["title"]),
])
@ -127,32 +127,16 @@ def _crete_temp_dir(base_uri):
return temp_dir
VIDEO_PATTERNS = [
r"^(?P<id>\d+)?$",
r"^https://(www.)?twitch.tv/videos/(?P<id>\d+)(\?.+)?$",
]
CLIP_PATTERNS = [
r"^(?P<slug>[A-Za-z0-9]+)$",
r"^https://(www.)?twitch.tv/\w+/clip/(?P<slug>[A-Za-z0-9]+)(\?.+)?$",
r"^https://clips.twitch.tv/(?P<slug>[A-Za-z0-9]+)(\?.+)?$",
]
def download(args):
for pattern in VIDEO_PATTERNS:
match = re.match(pattern, args.video)
if match:
video_id = match.group('id')
video_id = utils.parse_video_identifier(args.video)
if video_id:
return _download_video(video_id, args)
for pattern in CLIP_PATTERNS:
match = re.match(pattern, args.video)
if match:
clip_slug = match.group('slug')
clip_slug = utils.parse_clip_identifier(args.video)
if clip_slug:
return _download_clip(clip_slug, args)
raise ConsoleError("Invalid video: {}".format(args.video))
raise ConsoleError("Invalid input: {}".format(args.video))
def _get_clip_url(clip, args):
@ -216,7 +200,7 @@ def _download_video(video_id, args):
video = twitch.get_video(video_id)
print_out("Found: <blue>{}</blue> by <yellow>{}</yellow>".format(
video['title'], video['channel']['display_name']))
video['title'], video['creator']['displayName']))
print_out("<dim>Fetching access token...</dim>")
access_token = twitch.get_access_token(video_id)

78
twitchdl/commands/info.py Normal file
View File

@ -0,0 +1,78 @@
import m3u8
from twitchdl import utils, twitch
from twitchdl.exceptions import ConsoleError
from twitchdl.output import print_video, print_clip, print_json, print_out, print_log
def info(args):
video_id = utils.parse_video_identifier(args.identifier)
if video_id:
print_log("Fetching video...")
video = twitch.get_video(video_id)
print_log("Fetching access token...")
access_token = twitch.get_access_token(video_id)
print_log("Fetching playlists...")
playlists = twitch.get_playlists(video_id, access_token)
if video:
if args.json:
video_json(video, playlists)
else:
video_info(video, playlists)
return
raise ConsoleError("Video #{} not found".format(video_id))
clip_slug = utils.parse_clip_identifier(args.identifier)
if clip_slug:
print_log("Fetching clip...")
clip = twitch.get_clip(clip_slug)
if clip:
if args.json:
print_json(clip)
else:
clip_info(clip)
return
raise ConsoleError("Clip {} not found".format(clip_slug))
raise ConsoleError("Invalid input: {}".format(args.video))
def video_info(video, playlists):
print_out()
print_video(video)
print_out()
print_out("Playlists:")
for p in m3u8.loads(playlists).playlists:
print_out("<b>{}</b> {}".format(p.stream_info.video, p.uri))
def video_json(video, playlists):
playlists = m3u8.loads(playlists).playlists
video["playlists"] = [
{
"bandwidth": p.stream_info.bandwidth,
"resolution": p.stream_info.resolution,
"codecs": p.stream_info.codecs,
"video": p.stream_info.video,
"uri": p.uri
} for p in playlists
]
print_json(video)
def clip_info(clip):
print_out()
print_clip(clip)
print_out()
print_out("Download links:")
for q in clip["videoQualities"]:
print_out("<b>{quality}p{frameRate}</b> {sourceURL}".format(**q))

View File

@ -51,6 +51,7 @@ def videos(args):
print_out("<yellow>Showing videos {}-{} of {}</yellow>".format(first, last, total))
for video in videos["edges"]:
print_out()
print_video(video["node"])
if not args.pager:

View File

@ -171,6 +171,21 @@ COMMANDS = [
})
],
),
Command(
name="info",
description="Print information for a given Twitch URL, video ID or clip slug",
arguments=[
(["identifier"], {
"help": "identifier",
"type": str,
}),
(["-j", "--json"], {
"help": "Show results as JSON",
"action": "store_true",
"default": False,
}),
],
)
]
COMMON_ARGUMENTS = [

View File

@ -62,10 +62,16 @@ def print_err(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def print_log(*args, **kwargs):
args = ["<dim>{}</dim>".format(a) for a in args]
args = [colorize(a) if USE_ANSI_COLOR else strip_tags(a) for a in args]
print(*args, file=sys.stderr, **kwargs)
def print_video(video):
published_at = video["publishedAt"].replace("T", " @ ").replace("Z", "")
length = utils.format_duration(video["lengthSeconds"])
channel = video["creator"]["channel"]["displayName"]
channel = video["creator"]["displayName"]
playing = (
"playing <blue>{}</blue>".format(video["game"]["name"])
if video["game"] else ""
@ -74,7 +80,7 @@ def print_video(video):
# Can't find URL in video object, strange
url = "https://www.twitch.tv/videos/{}".format(video["id"])
print_out("\n<b>{}</b>".format(video["id"]))
print_out("<b>Video {}</b>".format(video["id"]))
print_out("<green>{}</green>".format(video["title"]))
print_out("<blue>{}</blue> {}".format(channel, playing))
print_out("Published <blue>{}</blue> Length: <blue>{}</blue> ".format(published_at, length))
@ -84,13 +90,13 @@ def print_video(video):
def print_clip(clip):
published_at = clip["createdAt"].replace("T", " @ ").replace("Z", "")
length = utils.format_duration(clip["durationSeconds"])
channel = clip["broadcaster"]["channel"]["displayName"]
channel = clip["broadcaster"]["displayName"]
playing = (
"playing <blue>{}</blue>".format(clip["game"]["name"])
if clip["game"] else ""
)
print_out("\n<b>{}</b>".format(clip["slug"]))
print_out("Clip <b>{}</b>".format(clip["slug"]))
print_out("<green>{}</green>".format(clip["title"]))
print_out("<blue>{}</blue> {}".format(channel, playing))
print_out(
@ -98,3 +104,8 @@ def print_clip(clip):
" Length: <blue>{}</blue>"
" Views: <blue>{}</blue>".format(published_at, length, clip["viewCount"]))
print_out("<i>{}</i>".format(clip["url"]))
def print_clip_urls(clip):
from pprint import pprint
pprint(clip)

View File

@ -61,7 +61,7 @@ def gql_query(query):
return response
def get_video(video_id):
def get_video_legacy(video_id):
"""
https://dev.twitch.tv/docs/v5/reference/videos#get-video
"""
@ -70,29 +70,61 @@ def get_video(video_id):
return kraken_get(url).json()
VIDEO_FIELDS = """
id
title
publishedAt
broadcastType
lengthSeconds
game {
name
}
creator {
login
displayName
}
"""
def get_video(video_id):
query = """
{{
video(id: "{video_id}") {{
{fields}
}}
}}
"""
query = query.format(video_id=video_id, fields=VIDEO_FIELDS)
response = gql_query(query)
return response["data"]["video"]
def get_clip(slug):
query = """
{{
clip(slug: "{}") {{
id
slug
title
createdAt
viewCount
durationSeconds
game {{
name
}}
broadcaster {{
login
displayName
channel {{
name
}}
}}
url
videoQualities {{
frameRate
quality
sourceURL
}}
game {{
id
name
}}
broadcaster {{
displayName
login
}}
}}
}}
"""
@ -139,10 +171,8 @@ def get_channel_clips(channel_id, period, limit, after=None):
name
}}
broadcaster {{
channel {{
name
displayName
}}
login
}}
}}
}}
@ -213,7 +243,7 @@ def get_channel_videos(channel_id, limit, sort, type="archive", game_ids=[], aft
name
}}
creator {{
channel {{
login
displayName
}}
}}
@ -221,7 +251,6 @@ def get_channel_videos(channel_id, limit, sort, type="archive", game_ids=[], aft
}}
}}
}}
}}
"""
query = query.format(**{

View File

@ -61,3 +61,31 @@ def slugify(value):
value = unicodedata.normalize('NFKC', value)
value = re_pattern.sub('', value).strip().lower()
return re_spaces.sub('_', value)
VIDEO_PATTERNS = [
r"^(?P<id>\d+)?$",
r"^https://(www.)?twitch.tv/videos/(?P<id>\d+)(\?.+)?$",
]
CLIP_PATTERNS = [
r"^(?P<slug>[A-Za-z0-9]+)$",
r"^https://(www.)?twitch.tv/\w+/clip/(?P<slug>[A-Za-z0-9]+)(\?.+)?$",
r"^https://clips.twitch.tv/(?P<slug>[A-Za-z0-9]+)(\?.+)?$",
]
def parse_video_identifier(identifier):
"""Given a video ID or URL returns the video ID, or null if not matched"""
for pattern in VIDEO_PATTERNS:
match = re.match(pattern, identifier)
if match:
return match.group("id")
def parse_clip_identifier(identifier):
"""Given a clip slug or URL returns the clip slug, or null if not matched"""
for pattern in CLIP_PATTERNS:
match = re.match(pattern, identifier)
if match:
return match.group("slug")