mirror of
https://github.com/ihabunek/twitch-dl
synced 2024-08-30 18:32:25 +00:00
Add twitch-dl info command
This commit is contained in:
parent
2380dc5a35
commit
548a9350ba
@ -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,
|
||||||
]
|
]
|
||||||
|
@ -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:
|
||||||
|
@ -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
78
twitchdl/commands/info.py
Normal 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))
|
@ -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:
|
||||||
|
@ -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 = [
|
||||||
|
@ -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)
|
@ -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
|
||||||
}}
|
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user