Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Twitch changed the way on how downloading clips work. #64

Closed
Kuyumee opened this issue May 27, 2021 · 16 comments
Closed

Twitch changed the way on how downloading clips work. #64

Kuyumee opened this issue May 27, 2021 · 16 comments

Comments

@Kuyumee
Copy link

Kuyumee commented May 27, 2021

Twitch changed the way on how downloading clips work. The URL that you use to download the clips anymore does not work.

image

I find a way around this by going to the main clip URL: https://clips.twitch.tv/DependableAmazingOrcaLeeroyJenkins-M1Q94jy6g9UsbUqZ

Right-clicking it, Inspect Element, then opening the link that corresponds to the video. It is the same endpoint as the old one but Twitch made it so that it requires authorization shit

@ihabunek
Copy link
Owner

@ihabunek
Copy link
Owner

youtube-dl has the same problem ytdl-org/youtube-dl#29136

@Kuyumee
Copy link
Author

Kuyumee commented May 27, 2021

A temporary fix would be to add the arguments after the old download link. eg: https://production.assets.clips.twitchcdn.net/AT-cm|1195078391.mp4

The arguments can be constant but only valid for a day.

The argument examle is: ?sig=b63df53a9b417154b851fe1e7a42ca11bb69fcb1\&token={"authorization":{"forbidden":false,"reason":""},"clip_uri":"","device_id":"a65a6a6ef754b36b","expires":1622169633,"user_id":"44791398","version":2}

This can be used for any clips but only limited to a day.

Example:

https://production.assets.clips.twitchcdn.net/AT-cm|[CLIP ID].mp4?sig=b63df53a9b417154b851fe1e7a42ca11bb69fcb1\&token={"authorization":{"forbidden":false,"reason":""},"clip_uri":"","device_id":"a65a6a6ef754b36b","expires":1622169633,"user_id":"44791398","version":2}

You could change [CLIP ID] to the clip you wish to download. It would work, but again, only valid for a day

@Kuyumee
Copy link
Author

Kuyumee commented May 27, 2021

Another thing I've noticed is the sig changes when you entered it in your browser. The old sig would work but the new dummy sig that replaced the old sig would not work.

eg:
Before: 2980a7ab5a947e4012b45e43934d59bd6cdb3185
After: 2980a7ab5a947e4012b45e43934d59bd6cdb3180

@Kuyumee
Copy link
Author

Kuyumee commented May 27, 2021

Also, the arguments are all linked up together. If you change one of them, it will not work

@2c2c
Copy link

2c2c commented May 27, 2021

i see a post to gql.twitch.tv/gql with body

[{"operationName":"VideoAccessToken_Clip","variables":{"slug":"DeadTardySpaghettiGivePLZ-GKat_JfVPd2mhgBT"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11"}}}]

(this sha256hash seems unchanged from slug to slug, no idea what it is)
with response

[{"data":{"user":{"id":"23814799","lastBroadcast":{"id":"42077676926","game":{"id":"505705","name":"Noita","__typename":"Game"},"__typename":"Broadcast"},"broadcastSettings":{"id":"23814799","language":"EN","__typename":"BroadcastSettings"},"self":null,"hosting":null,"stream":null,"__typename":"User"}},"extensions":{"durationMilliseconds":59,"operationName":"WatchTrackQuery","requestID":"01F6QG1FA5XX99299NJE530QCJ"}},{"data":{"clip":{"id":"958085563","playbackAccessToken":{"signature":"856d645e82a931b21dd5d7cdb1130b1fd2c819cf","value":"{\"authorization\":{\"forbidden\":false,\"reason\":\"\"},\"clip_uri\":\"\",\"device_id\":\"hSAlxhebyZShOSbW57d0T3Lc6o1iZW9V\",\"expires\":1622210731,\"user_id\":\"\",\"version\":2}","__typename":"PlaybackAccessToken"},"videoQualities":[{"frameRate":60,"quality":"1080","sourceURL":"https://production.assets.clips.twitchcdn.net/AT-cm%7C958085563.mp4","__typename":"ClipVideoQuality"},{"frameRate":60,"quality":"720","sourceURL":"https://production.assets.clips.twitchcdn.net/AT-cm%7C958085563-720.mp4","__typename":"ClipVideoQuality"},{"frameRate":30,"quality":"480","sourceURL":"https://production.assets.clips.twitchcdn.net/AT-cm%7C958085563-480.mp4","__typename":"ClipVideoQuality"},{"frameRate":30,"quality":"360","sourceURL":"https://production.assets.clips.twitchcdn.net/AT-cm%7C958085563-360.mp4","__typename":"ClipVideoQuality"}],"__typename":"Clip"}},"extensions":{"durationMilliseconds":62,"operationName":"VideoAccessToken_Clip","requestID":"01F6QG1FA5XX99299NJE530QCJ"}},{"data":{"clip":{"id":"958085563","videoOffsetSeconds":null,"durationSeconds":8,"video":null,"__typename":"Clip"}},"extensions":{"durationMilliseconds":18,"operationName":"ChatClip","requestID":"01F6QG1FA5XX99299NJE530QCJ"}}]

which has the token and value that coincide with sig and token query params

@FiXato
Copy link

FiXato commented May 27, 2021

For now I've worked around it by adding:
url = re.sub(r'https://[^/]+', 'https://clips-media-assets2.twitch.tv', url)
to _download_clip in commands/download.py locally. :)

@ghost
Copy link

ghost commented May 27, 2021

(this sha256hash seems unchanged from slug to slug, no idea what it is)

This is called a "Persisted Query" in GraphQL. Instead of sending a megabyte of newlines and GQL query data on every request (huge waste of bandwidth), you can store query server side and call it by its hashed value.

@ihabunek
Copy link
Owner

Gah, they disabled query introspection. That makes it much harder to root around the API.

@FiXato
Copy link

FiXato commented May 28, 2021

It's not a clean solution perhaps, but the following patch fixes it for me:

diff --git a/twitchdl/commands/download.py b/twitchdl/commands/download.py
index 5656807..e7c226f 100644
--- a/twitchdl/commands/download.py
+++ b/twitchdl/commands/download.py
@@ -139,18 +139,26 @@ def download(args):
     raise ConsoleError("Invalid input: {}".format(args.video))
 
 
+def _clip_source_url_with_token(clip, source_url):
+    clip_download_url = "{}?sig={}&token={}".format(
+        source_url,
+        clip['playbackAccessToken']['signature'],
+        clip['playbackAccessToken']['value'])
+    return clip_download_url
+
+
 def _get_clip_url(clip, args):
     qualities = clip["videoQualities"]
 
     # Quality given as an argument
     if args.quality:
         if args.quality == "source":
-            return qualities[0]["sourceURL"]
+            return _clip_source_url_with_token(clip, qualities[0]["sourceURL"])
 
         selected_quality = args.quality.rstrip("p")  # allow 720p as well as 720
         for q in qualities:
             if q["quality"] == selected_quality:
-                return q["sourceURL"]
+                return _clip_source_url_with_token(clip, q["sourceURL"])
 
         available = ", ".join([str(q["quality"]) for q in qualities])
         msg = "Quality '{}' not found. Available qualities are: {}".format(args.quality, available)
@@ -164,7 +172,7 @@ def _get_clip_url(clip, args):
 
     no = utils.read_int("Choose quality", min=1, max=len(qualities), default=1)
     selected_quality = qualities[no - 1]
-    return selected_quality["sourceURL"]
+    return _clip_source_url_with_token(clip, selected_quality["sourceURL"])
 
 
 def _download_clip(slug, args):
diff --git a/twitchdl/twitch.py b/twitchdl/twitch.py
index 57bdb7b..2414b89 100644
--- a/twitchdl/twitch.py
+++ b/twitchdl/twitch.py
@@ -102,7 +102,7 @@ def get_video(video_id):
 
 
 def get_clip(slug):
-    query = """
+    clip_details_query = """
     {{
         clip(slug: "{}") {{
             id
@@ -128,9 +128,29 @@ def get_clip(slug):
         }}
     }}
     """
+    # Along with the clip details we normally request, also request a persisted query for the clip's video access token
+    clip_request = [
+        {
+          "operationName": "VideoAccessToken_Clip",
+          "variables": {
+            "slug": "{}".format(slug),
+          },
+          "extensions": {
+            "persistedQuery": {
+              "version": 1,
+              "sha256Hash": "36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11"
+            }
+          }
+        },
+        {
+            "query": clip_details_query.format(slug)
+        }
+    ]
 
-    response = gql_query(query.format(slug))
-    return response["data"]["clip"]
+    # FIXME: should probably be merged with gql_query, but didn't want to modify that method to support an existing object rather than just a string
+    response = authenticated_post("https://gql.twitch.tv/gql", json=clip_request).json()
+    # merge both clip dicts and return
+    return {**response[1]['data']['clip'], **response[0]['data']['clip']}
 
 
 def get_channel_clips(channel_id, period, limit, after=None):

I basically request both the clip details as twitch-dl already did, as well as the persisted query mentioned in ytdl-org/youtube-dl@9a71e3c, and then merge in the access token info, which is then used to add to the clip's download URL.

I hope it helps with finding a cleaner solution. :)

@ihabunek
Copy link
Owner

Thank you. That's very helpful. I'm busy these days but I'll try it out as soon as I get the chance.

@infozzle
Copy link

infozzle commented Jun 2, 2021

Thank you. That's very helpful. I'm busy these days but I'll try it out as soon as I get the chance.

Thanks mate..any approx eta for this fix..fully understand that you're doing the as a favour for all of us but would be good to know as I used this in a project so can plan my work accordingly. Thank you and no rush.

@ihabunek
Copy link
Owner

ihabunek commented Jun 2, 2021

Sorry but I can't commit to a deadline. I just applied the patch, and it doesn't seem to work, I still get 0 bytes files. I haven't made any attempt to debug it yet though.

@infozzle
Copy link

infozzle commented Jun 2, 2021 via email

@FiXato
Copy link

FiXato commented Jun 3, 2021

Sorry but I can't commit to a deadline. I just applied the patch, and it doesn't seem to work, I still get 0 bytes files. I haven't made any attempt to debug it yet though.

huh, that's strange. I haven't used it again since I created the diff (at which point it was working for me), but I'll have a look at it tomorrow to see if something changed since.

Edit: I just tried running twitch-dl download -q source $CLIP_URL and it still downloaded an actual clip.

However, twitch-dl clips $CHANNEL_NAME -P last_day -d did return just zero-byte files, so I guess that command uses a different logic that hasn't been fixed yet. :)

Edit 2: hopefully I'll have some time tomorrow to change that too.

@ihabunek
Copy link
Owner

ihabunek commented Jun 9, 2021

This has been fixed in 1.16.0. Thanks to everyone who participated, you made it an easy fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants