From d22fd74357756952594155bd63a9e38b77b1febd Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Sun, 17 May 2020 14:35:33 +0200 Subject: [PATCH] Add filtering videos by game --- CHANGELOG.md | 1 + twitchdl/commands.py | 30 ++++++++++++++++++++++++------ twitchdl/console.py | 11 +++++++++++ twitchdl/twitch.py | 44 +++++++++++++++++++++++++++++++++++++++----- 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0b48f5..4820932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Twitch Downloader change log * **Breaking**: `videos` command no longer takes the `--offset` parameter due to API changes * Add paging to `videos` command to replace offset +* Add `--game` option to `videos` command to filter by game 1.7.0 (2020-04-25) ------------------ diff --git a/twitchdl/commands.py b/twitchdl/commands.py index 346bed2..86fb4a2 100644 --- a/twitchdl/commands.py +++ b/twitchdl/commands.py @@ -30,16 +30,32 @@ def _continue(): return True -def videos(channel_name, limit, sort, type, **kwargs): - print_out("Loading videos...") - generator = twitch.channel_videos_generator(channel_name, limit, sort, type) + +def _get_game_ids(names): + if not names: + return [] + + game_ids = [] + for name in names: + print_out("Looking up game '{}'...".format(name)) + game_id = twitch.get_game_id(name) + if not game_id: + raise ConsoleError("Game '{}' not found".format(name)) + game_ids.append(int(game_id)) + + return game_ids + + +def videos(channel_name, limit, sort, type, game, **kwargs): + game_ids = _get_game_ids(game) + + print_out("Loading videos...") + generator = twitch.channel_videos_generator( + channel_name, limit, sort, type, game_ids=game_ids) first = 1 for videos, has_more in generator: - if "edges" not in videos: - break - count = len(videos["edges"]) if "edges" in videos else 0 total = videos["totalCount"] last = first + count - 1 @@ -54,6 +70,8 @@ def videos(channel_name, limit, sort, type, **kwargs): break first += count + else: + print_out("No videos found") def _select_quality(playlists): diff --git a/twitchdl/console.py b/twitchdl/console.py index 39bc76c..c635bc5 100644 --- a/twitchdl/console.py +++ b/twitchdl/console.py @@ -7,6 +7,7 @@ from collections import namedtuple from twitchdl.exceptions import ConsoleError from twitchdl.output import print_err +from twitchdl.twitch import GQLError from . import commands, __version__ @@ -54,6 +55,11 @@ COMMANDS = [ "help": "channel name", "type": str, }), + (["-g", "--game"], { + "help": "Show videos of given game (can be given multiple times)", + "action": "append", + "type": str, + }), (["-l", "--limit"], { "help": "Number of videos to fetch (default 10, max 100)", "type": limit, @@ -163,3 +169,8 @@ def main(): except ConsoleError as e: print_err(e) sys.exit(1) + except GQLError as e: + print_err(e) + for err in e.errors: + print_err("*", err["message"]) + sys.exit(1) diff --git a/twitchdl/twitch.py b/twitchdl/twitch.py index 366ae14..df0840f 100644 --- a/twitchdl/twitch.py +++ b/twitchdl/twitch.py @@ -8,6 +8,12 @@ from twitchdl import CLIENT_ID from twitchdl.exceptions import ConsoleError +class GQLError(Exception): + def __init__(self, errors): + super().__init__("GraphQL query failed") + self.errors = errors + + def authenticated_get(url, params={}, headers={}): headers['Client-ID'] = CLIENT_ID @@ -46,7 +52,12 @@ def kraken_get(url, params={}, headers={}): def gql_query(query): url = "https://gql.twitch.tv/gql" payload = {"query": query} - return authenticated_post(url, json={"query": query}).json() + response = authenticated_post(url, json={"query": query}).json() + + if "errors" in response: + raise GQLError(response["errors"]) + + return response def get_video(video_id): @@ -86,13 +97,15 @@ def get_clip(slug): return response["data"]["clip"] -def get_channel_videos(channel_id, limit, sort, type="archive", after=None): +def get_channel_videos(channel_id, limit, sort, type="archive", game_ids=[], after=None): url = "https://gql.twitch.tv/gql" query = """ {{ user(login: "{channel_id}") {{ - videos(options: {{ }}, first: {limit}, type: {type}, sort: {sort}, after: "{after}") {{ + videos(options: {{ + gameIDs: {game_ids} + }}, first: {limit}, type: {type}, sort: {sort}, after: "{after}") {{ totalCount edges {{ cursor @@ -119,6 +132,7 @@ def get_channel_videos(channel_id, limit, sort, type="archive", after=None): query = query.format(**{ "channel_id": channel_id, + "game_ids": game_ids, "after": after, "limit": limit, "sort": sort.upper(), @@ -129,10 +143,15 @@ def get_channel_videos(channel_id, limit, sort, type="archive", after=None): return response["data"]["user"]["videos"] -def channel_videos_generator(channel_id, limit, sort, type): +def channel_videos_generator(channel_id, limit, sort, type, game_ids=None): cursor = None while True: - videos = get_channel_videos(channel_id, limit, sort, type, after=cursor) + videos = get_channel_videos( + channel_id, limit, sort, type, game_ids=game_ids, after=cursor) + + if not videos["edges"]: + break + cursor = videos["edges"][-1]["cursor"] yield videos, cursor is not None @@ -161,3 +180,18 @@ def get_playlists(video_id, access_token): }) response.raise_for_status() return response.content.decode('utf-8') + + +def get_game_id(name): + query = """ + {{ + game(name: "{}") {{ + id + }} + }} + """ + + response = gql_query(query.format(name.strip())) + game = response["data"]["game"] + if game: + return game["id"]