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:
As a side note, I was NOT able to reproduce this on Ubuntu 18.04. Logging in to crunchyroll was successful for this
Can confirm I have the same issue on windows - both stable and dev builds and on multiple videos, both those that need credentials and those that don't.
I think it's saving the time stamp for the credential expiry wrong?

Here is my output:
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.8.6
[cli][debug] Streamlink: 1.7.0+115.g83fb57c
[cli][debug] Requests(2.25.0), Socks(1.7.1), Websocket(0.57.0)
[cli][info] Found matching plugin crunchyroll for URL https://www.crunchyroll.com/en-gb/attack-on-titan/episode-60-the-other-side-of-the-sea-799901
[cli][debug] Plugin specific arguments:
[cli][debug]  --crunchyroll-username=*******@******.com (username)
[cli][debug]  --crunchyroll-password=************ (password)
[utils.l10n][debug] Language code: en_AU
[plugins.crunchyroll][debug] Device ID: 4c7fa1ba-29ba-485d-af30-d2c8ad9da6d4
[plugins.crunchyroll][debug] Creating session with locale: en_AU
[plugins.crunchyroll][debug] Session created with ID: ec66d76f6abfe0c61472cfb6c9179e53
[plugins.crunchyroll][debug] Using saved credentials
[plugins.crunchyroll][warning] Saved credentials have expired
[plugins.crunchyroll][debug] Attempting to login using username and password
[plugins.crunchyroll][debug] Credentials expire at: 1969-12-31 08:00:00
Traceback (most recent call last):
  File "runpy.py", line 194, in _run_module_as_main
  File "runpy.py", line 87, in _run_code
  File "g:\Video\Streamlink\bin\streamlink.exe\__main__.py", line 18, in <module>
  File "g:\Video\Streamlink\pkgs\streamlink_cli\main.py", line 1050, in main
    handle_url()
  File "g:\Video\Streamlink\pkgs\streamlink_cli\main.py", line 580, in handle_url
    streams = fetch_streams(plugin)
  File "g:\Video\Streamlink\pkgs\streamlink_cli\main.py", line 459, in fetch_streams
    return plugin.streams(stream_types=args.stream_types,
  File "g:\Video\Streamlink\pkgs\streamlink\plugin\plugin.py", line 317, in streams
    ostreams = self._get_streams()
  File "g:\Video\Streamlink\pkgs\streamlink\plugins\crunchyroll.py", line 305, in _get_streams
    api = self._create_api()
  File "g:\Video\Streamlink\pkgs\streamlink\plugins\crunchyroll.py", line 377, in _create_api
    login = api.authenticate()
  File "g:\Video\Streamlink\pkgs\streamlink\plugins\crunchyroll.py", line 224, in authenticate
    self.cache.set("auth", self.auth, expires_at=data["expires"])
  File "g:\Video\Streamlink\pkgs\streamlink\cache.py", line 75, in set
    expires = mktime(expires_at.timetuple())
OverflowError: mktime argument out of range

Can  also confirm this same problem on windows, this was my output

[cli][info] Found matching plugin crunchyroll for URL https://www.crunchyroll.com/es/attack-on-titan/episode-61-midnight-train-799902
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
By the way, I noticed that if I first type in:

` streamlink --crunchyroll-username=xxxx --crunchyroll-password=yyy --crunchyroll-purge-credentials https://www.crunchyroll.com/my/episode/to/watch best`

And then type in the same command WITHOUT specifying username or password via:

` streamlink https://www.crunchyroll.com/my/episode/to/watch best`

Where the url is a Crunchyroll Premium video, I am able to watch it


> Credentials expire at: 1969-12-31 08:00:00
> OverflowError: mktime argument out of range

This looks like a time zone issue when the returned expiration time is `1970-01-01T00:00:00Z` (a UNIX timestamp of 0).

https://github.com/streamlink/streamlink/blob/aa9fc0897c6527c2f27855f496b6380cb898986c/src/streamlink/plugins/crunchyroll.py#L25-L31

> As a side note, I was NOT able to reproduce this on Ubuntu 18.04.

This is indeed only a problem on Windows, as negative timestamps are not supported:
https://stackoverflow.com/a/58203399

To fix this, Streamlink's plugin data cache needs to ignore negative timestamps (before parsing them) or filter out the `OverflowError` on `win32` systems. Also, why doesn't the Crunchyroll plugin use `isodate.parse_datetime` for parsing the ISO8601 strings?

Resolving patch:
diff --git a/src/streamlink/cache.py b/src/streamlink/cache.py
--- a/src/streamlink/cache.py
+++ b/src/streamlink/cache.py
@@ -38,7 +38,7 @@ def _prune(self):
         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 @@ def set(self, key, value, expires=60 * 60 * 24 * 7, expires_at=None):
         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
--- a/tests/test_cache.py
+++ b/tests/test_cache.py
@@ -55,6 +55,11 @@ def test_expired_at_after(self):
         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"))