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 .clips import clips
from .download import download from .download import download
from .info import info
from .videos import videos from .videos import videos
__all__ = [ __all__ = [
clips, clips,
download, download,
info,
videos, videos,
] ]

View File

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

View File

@ -68,13 +68,13 @@ def _join_vods(playlist_path, target, overwrite):
def _video_target_filename(video, format): 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()) date = "".join(match.groups())
name = "_".join([ name = "_".join([
date, date,
video['_id'][1:], video['id'][1:],
video['channel']['name'], video['creator']['login'],
utils.slugify(video['title']), utils.slugify(video['title']),
]) ])
@ -92,7 +92,7 @@ def _clip_target_filename(clip):
name = "_".join([ name = "_".join([
date, date,
clip["id"], clip["id"],
clip["broadcaster"]["channel"]["name"], clip["broadcaster"]["login"],
utils.slugify(clip["title"]), utils.slugify(clip["title"]),
]) ])
@ -127,32 +127,16 @@ def _crete_temp_dir(base_uri):
return temp_dir 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): def download(args):
for pattern in VIDEO_PATTERNS: video_id = utils.parse_video_identifier(args.video)
match = re.match(pattern, args.video) if video_id:
if match: return _download_video(video_id, args)
video_id = match.group('id')
return _download_video(video_id, args)
for pattern in CLIP_PATTERNS: clip_slug = utils.parse_clip_identifier(args.video)
match = re.match(pattern, args.video) if clip_slug:
if match: return _download_clip(clip_slug, args)
clip_slug = match.group('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): def _get_clip_url(clip, args):
@ -216,7 +200,7 @@ def _download_video(video_id, args):
video = twitch.get_video(video_id) video = twitch.get_video(video_id)
print_out("Found: <blue>{}</blue> by <yellow>{}</yellow>".format( 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>") print_out("<dim>Fetching access token...</dim>")
access_token = twitch.get_access_token(video_id) 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)) print_out("<yellow>Showing videos {}-{} of {}</yellow>".format(first, last, total))
for video in videos["edges"]: for video in videos["edges"]:
print_out()
print_video(video["node"]) print_video(video["node"])
if not args.pager: 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 = [ COMMON_ARGUMENTS = [

View File

@ -62,10 +62,16 @@ def print_err(*args, **kwargs):
print(*args, file=sys.stderr, **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): def print_video(video):
published_at = video["publishedAt"].replace("T", " @ ").replace("Z", "") published_at = video["publishedAt"].replace("T", " @ ").replace("Z", "")
length = utils.format_duration(video["lengthSeconds"]) length = utils.format_duration(video["lengthSeconds"])
channel = video["creator"]["channel"]["displayName"] channel = video["creator"]["displayName"]
playing = ( playing = (
"playing <blue>{}</blue>".format(video["game"]["name"]) "playing <blue>{}</blue>".format(video["game"]["name"])
if video["game"] else "" if video["game"] else ""
@ -74,7 +80,7 @@ def print_video(video):
# Can't find URL in video object, strange # Can't find URL in video object, strange
url = "https://www.twitch.tv/videos/{}".format(video["id"]) 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("<green>{}</green>".format(video["title"]))
print_out("<blue>{}</blue> {}".format(channel, playing)) print_out("<blue>{}</blue> {}".format(channel, playing))
print_out("Published <blue>{}</blue> Length: <blue>{}</blue> ".format(published_at, length)) print_out("Published <blue>{}</blue> Length: <blue>{}</blue> ".format(published_at, length))
@ -84,13 +90,13 @@ def print_video(video):
def print_clip(clip): def print_clip(clip):
published_at = clip["createdAt"].replace("T", " @ ").replace("Z", "") published_at = clip["createdAt"].replace("T", " @ ").replace("Z", "")
length = utils.format_duration(clip["durationSeconds"]) length = utils.format_duration(clip["durationSeconds"])
channel = clip["broadcaster"]["channel"]["displayName"] channel = clip["broadcaster"]["displayName"]
playing = ( playing = (
"playing <blue>{}</blue>".format(clip["game"]["name"]) "playing <blue>{}</blue>".format(clip["game"]["name"])
if clip["game"] else "" 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("<green>{}</green>".format(clip["title"]))
print_out("<blue>{}</blue> {}".format(channel, playing)) print_out("<blue>{}</blue> {}".format(channel, playing))
print_out( print_out(
@ -98,3 +104,8 @@ def print_clip(clip):
" Length: <blue>{}</blue>" " Length: <blue>{}</blue>"
" Views: <blue>{}</blue>".format(published_at, length, clip["viewCount"])) " Views: <blue>{}</blue>".format(published_at, length, clip["viewCount"]))
print_out("<i>{}</i>".format(clip["url"])) 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 return response
def get_video(video_id): def get_video_legacy(video_id):
""" """
https://dev.twitch.tv/docs/v5/reference/videos#get-video https://dev.twitch.tv/docs/v5/reference/videos#get-video
""" """
@ -70,29 +70,61 @@ def get_video(video_id):
return kraken_get(url).json() 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): def get_clip(slug):
query = """ query = """
{{ {{
clip(slug: "{}") {{ clip(slug: "{}") {{
id id
slug
title title
createdAt createdAt
viewCount
durationSeconds durationSeconds
game {{ url
name
}}
broadcaster {{
login
displayName
channel {{
name
}}
}}
videoQualities {{ videoQualities {{
frameRate frameRate
quality quality
sourceURL sourceURL
}} }}
game {{
id
name
}}
broadcaster {{
displayName
login
}}
}} }}
}} }}
""" """
@ -139,10 +171,8 @@ def get_channel_clips(channel_id, period, limit, after=None):
name name
}} }}
broadcaster {{ broadcaster {{
channel {{ displayName
name login
displayName
}}
}} }}
}} }}
}} }}
@ -213,9 +243,8 @@ def get_channel_videos(channel_id, limit, sort, type="archive", game_ids=[], aft
name name
}} }}
creator {{ creator {{
channel {{ login
displayName displayName
}}
}} }}
}} }}
}} }}

View File

@ -61,3 +61,31 @@ def slugify(value):
value = unicodedata.normalize('NFKC', value) value = unicodedata.normalize('NFKC', value)
value = re_pattern.sub('', value).strip().lower() value = re_pattern.sub('', value).strip().lower()
return re_spaces.sub('_', value) 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")