Repository: streamlink/streamlink

Issue:
mktime argument out of range when logging in to Crunchyroll after --crunchyroll-purge-credentials
<!--
Thanks for reporting a plugin issue!
USE THE TEMPLATE. Otherwise your plugin issue may be rejected.

First, see the contribution guidelines:
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink

Also check the list of open and closed plugin issues:
https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22

Please see the text preview to avoid unnecessary formatting errors.
-->


## Plugin Issue

<!-- Replace the space character between the square brackets with an x in order to check the boxes -->
- [x] This is a plugin issue and I have read the contribution guidelines.
- [x] I am using the latest development version from the master branch.


### Description

Whenver I try to log in to Crunchyroll via streamlink I get an error.
<!-- Explain the plugin issue as thoroughly as you can. -->


### Reproduction steps / Explicit stream URLs to test

<!-- How can we reproduce this? Please note the exact steps below using the list format supplied. If you need more steps please add them. -->

1. I enter the command streamlink --crunchyroll-username=xxxx --crunchyroll-password=yyy --crunchyroll-purge-credentials https://www.crunchyroll.com/my/episode/to/watch best and I get it every time

### Log output

<!--
TEXT LOG OUTPUT IS REQUIRED for a plugin issue!
Use the `--loglevel debug` parameter and avoid using parameters which suppress log output.
https://streamlink.github.io/cli.html#cmdoption-l

Make sure to **remove usernames and passwords**
You can copy the output to https://gist.github.com/ or paste it below.

Don't post screenshots of the log output and instead copy the text from your terminal application.
-->

```
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.6.6
[cli][debug] Streamlink: 1.7.0
[cli][debug] Requests(2.24.0), Socks(1.7.1), Websocket(0.57.0)
[cli][info] Found matching plugin crunchyroll for URL https://www.crunchyroll.com/jujutsu-kaisen/episode-10-idle-transfiguration-797874
[cli][debug] Plugin specific arguments:
[cli][debug]  --crunchyroll-username=kashiman290@gmail.com (username)
[cli][debug]  --crunchyroll-password=******** (password)
[cli][debug]  --crunchyroll-purge-credentials=True (purge_credentials)
[utils.l10n][debug] Language code: en_US
[plugin.crunchyroll][debug] Creating session with locale: en_US
[plugin.crunchyroll][debug] Session created with ID: 576c3d5d7a83d1dd18217343a6df37ff
[plugin.crunchyroll][debug] Attempting to login using username and password
[plugin.crunchyroll][debug] Credentials expire at: 1969-12-31 08:00:00
Traceback (most recent call last):
  File "runpy.py", line 193, in _run_module_as_main
  File "runpy.py", line 85, in _run_code
  File "C:\Program Files (x86)\Streamlink\bin\streamlink.exe\__main__.py", line 18, in <module>
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink_cli\main.py", line 1029, in main
    handle_url()
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink_cli\main.py", line 585, in handle_url
    streams = fetch_streams(plugin)
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink_cli\main.py", line 465, in fetch_streams
    sorting_excludes=args.stream_sorting_excludes)
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink\plugin\plugin.py", line 317, in streams
    ostreams = self._get_streams()
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink\plugins\crunchyroll.py", line 312, in _get_streams
    api = self._create_api()
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink\plugins\crunchyroll.py", line 384, in _create_api
    login = api.authenticate()
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink\plugins\crunchyroll.py", line 225, in authenticate
    self.cache.set("auth", self.auth, expires_at=data["expires"])
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink\cache.py", line 75, in set
    expires = mktime(expires_at.timetuple())
OverflowError: mktime argument out of range
```


### Additional comments, etc.



[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)


Discussion:
meh2481: Unscientific, but I manually patched these changes into cache.py in my windows streamlink install (release 1.7.0) and it does seem to fix #3396 
back-to: it might work, but it doesn't really fix the issue.

we need to rely on UTC not some random timestamps for `time.mktime()`
the timestamps might be wrong with it.

the plugins needs an update for **UTC** and the **Cache** class too
instead of using `now()` or `time()` we might need to use `utcnow()`

```diff
@@ -73,7 +74,8 @@ _login_schema = validate.Schema({
     "auth": validate.any(validate.text, None),
     "expires": validate.all(
         validate.text,
-        validate.transform(parse_timestamp)
+        validate.transform(isodate.parse_datetime),
+        validate.transform(lambda t: t.astimezone(tz=datetime.timezone.utc)),
     ),
     "user": {
         "username": validate.any(validate.text, None),
```
bastimeyer: I am aware that this doesn't fix the crunchyroll plugin. This wasn't my intention.

The ideal solution for the `Cache` class would be caching the expiration time as ISO8601 strings (which includes TZ data) instead of converting it to UNIX timestamps in UTC. Then the expiration time could be compared via `datetime` objects instead of integers and the timezone differences would be trivial. That would however be an API break and a translation for old data would need to be added.
back-to: should be good enough for a temporary solution.

Patch:
diff --git a/src/streamlink/cache.py b/src/streamlink/cache.py
index a86aa0ce..9e55e4b1 100644
--- a/src/streamlink/cache.py
+++ b/src/streamlink/cache.py
@@ -38,7 +38,7 @@ class Cache:
         pruned = []
 
         for key, value in self._cache.items():
-            expires = value.get("expires", time())
+            expires = value.get("expires", now)
             if expires <= now:
                 pruned.append(key)
 
@@ -69,10 +69,13 @@ class Cache:
         if self.key_prefix:
             key = "{0}:{1}".format(self.key_prefix, key)
 
-        expires += time()
-
-        if expires_at:
-            expires = mktime(expires_at.timetuple())
+        if expires_at is None:
+            expires += time()
+        else:
+            try:
+                expires = mktime(expires_at.timetuple())
+            except OverflowError:
+                expires = 0
 
         self._cache[key] = dict(value=value, expires=expires)
         self._save()


Test patch:
diff --git a/tests/test_cache.py b/tests/test_cache.py
index 712d2c4f..895aabdd 100644
--- a/tests/test_cache.py
+++ b/tests/test_cache.py
@@ -55,6 +55,11 @@ class TestCache(unittest.TestCase):
         self.cache.set("value", 10, expires_at=datetime.datetime.now() + datetime.timedelta(seconds=20))
         self.assertEqual(10, self.cache.get("value"))
 
+    @patch("streamlink.cache.mktime", side_effect=OverflowError)
+    def test_expired_at_raise_overflowerror(self, mock):
+        self.cache.set("value", 10, expires_at=datetime.datetime.now())
+        self.assertEqual(None, self.cache.get("value"))
+
     def test_not_expired(self):
         self.cache.set("value", 10, expires=20)
         self.assertEqual(10, self.cache.get("value"))