Repository: yt-dlp/yt-dlp

Issue:
RedGifs extractor seems to cache the auth/temporary token and never expire it
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I remove or skip any mandatory\* field

### Checklist

- [X] I'm reporting a bug unrelated to a specific site
- [X] I've verified that I'm running yt-dlp version **2022.10.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)

### Provide a description that is worded well enough to be understood

I am using commit 2530b68d4476fe6cb4b25897b906cbb1774ca7c9 (10/21/2022) as a library in a long running Python process to download videos from Redgifs. There have been recent changes to Redgifs that require an auth token, and I'm extremely impressed with how quickly this project has been able to keep up.

I noticed that when my Python process starts, the first few downloads have no issue. The following logs are written to stdout for the first download:
```
[RedGifs] video_name: Fetching temporary token
[RedGifs] video_name: Downloading video info
```

Subsequent downloads seem to skip fetching the token; I am guessing it gets cached. 
```
[RedGifs] video_name: Downloading video info
```

I am using the following code to check that a video exists before it gets downloaded, and it fails here:
```
            with YoutubeDL({}) as ydl:
                info = ydl.extract_info(url, download=False)
                assert info['url']
```
Once the token expires, I start to see
```
ERROR: [RedGifs] video_name: Unable to download JSON metadata: HTTP Error 401: Unauthorized (caused by <HTTPError 401: 'Unauthorized'>); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
```

So,
1. I think the cached token should be invalidates after some TTL
2. If there is some way (via an option or something) to invalidate the cached token, that works too.

I won't speculate any further since I'm already making an assumption about the token being cached.

Thanks all. If someone can check my assumption and maybe point me at the token code, I'll see if I can submit a PR myself.

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
This is covered in the problem description.
```



Discussion:
Looking at the code some, what do you think about this solution? Note I am new to this codebase as of a few minutes ago.

https://github.com/yt-dlp/yt-dlp/blob/c66ed4e2e5b1a904687120afda0003b77d326c22/yt_dlp/extractor/redgifs.py#L81
Here perhaps HTTP 401 could be handled by removing the authorization header from `self._API_HEADERS`, and then the request can be retried.
That seems like a good idea. I would wrap in a try-except and call `self._fetch_oauth_token(video_id)` on 401 only, then retry the `_download_json`.
Hmm... I started a fix, but I think it's trickier. Just from reading, it seems that the implementation of `PostProcessor._download_json` from `common.py` handles HTTPError and may not re-raise. I was trying to follow where the error gets logged, but the indirection from `__create_download_methods` makes that not really very straightforward. I see that some extractors override `_download_json`, and I wonder if that will be needed here too.

I'll work on something, but don't let me stop someone more familiar with the code from stepping in with a quick fix. :)
you can use something like this:
```py
try:
    self._download_json(...)

except ExtractorError as e:
    if not isinstance(e.cause, urllib.error.HTTPError) or e.cause.code != 401:
        raise

    ...
```

Resolving patch:
diff --git a/yt_dlp/extractor/redgifs.py b/yt_dlp/extractor/redgifs.py
--- a/yt_dlp/extractor/redgifs.py
+++ b/yt_dlp/extractor/redgifs.py
@@ -1,4 +1,5 @@
 import functools
+import urllib
 
 from .common import InfoExtractor
 from ..compat import compat_parse_qs
@@ -72,14 +73,20 @@ def _fetch_oauth_token(self, video_id):
         self._API_HEADERS['authorization'] = f'Bearer {auth["token"]}'
 
     def _call_api(self, ep, video_id, *args, **kwargs):
-        if 'authorization' not in self._API_HEADERS:
-            self._fetch_oauth_token(video_id)
-        assert 'authorization' in self._API_HEADERS
-
-        headers = dict(self._API_HEADERS)
-        headers['x-customheader'] = f'https://www.redgifs.com/watch/{video_id}'
-        data = self._download_json(
-            f'https://api.redgifs.com/v2/{ep}', video_id, headers=headers, *args, **kwargs)
+        for attempt in range(2):
+            if 'authorization' not in self._API_HEADERS:
+                self._fetch_oauth_token(video_id)
+            try:
+                headers = dict(self._API_HEADERS)
+                headers['x-customheader'] = f'https://www.redgifs.com/watch/{video_id}'
+                data = self._download_json(
+                    f'https://api.redgifs.com/v2/{ep}', video_id, headers=headers, *args, **kwargs)
+                break
+            except ExtractorError as e:
+                if not attempt and isinstance(e.cause, urllib.error.HTTPError) and e.cause.code == 401:
+                    del self._API_HEADERS['authorization']  # refresh the token
+                raise
+
         if 'error' in data:
             raise ExtractorError(f'RedGifs said: {data["error"]}', expected=True, video_id=video_id)
         return data


Test patch: