Compare commits

...

17 Commits

Author SHA1 Message Date
Benexl
8837c542f2 chore: bump version 2025-12-30 14:33:33 +03:00
Benexl
eb8c443775 fix(player): vlc on android 2025-12-30 14:32:25 +03:00
Benexl
b052ee8300 ci: only run on request 2025-12-30 12:05:38 +03:00
Benedict Xavier
f684f561df Merge pull request #175 from komposer-aml/fix/auth-input-mac
feat(auth): Allow non-interactive Anilist authentication
2025-12-30 12:00:54 +03:00
Albert Medrano-Lopez
7ed45ce07e chore: improved grammar in one other sentence during authentication flow
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-30 00:07:30 -08:00
Albert Medrano-Lopez
10d1211388 chore: improved grammar in "already logged in as" message
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-29 23:44:32 -08:00
Albert Medrano-Lopez
efa6f4d142 fix(tests): Resolve pyright type errors in anilist test_mapper.py
Updated mock data in `test_to_generic_user_profile_success` to conform to `AnilistViewerData` requirements.
Adjusted type annotations in tests with intentionally malformed data to `Any` to prevent pyright errors, ensuring proper validation of error handling.
2025-12-29 23:25:40 -08:00
Benedict Xavier
0ca63dd765 Merge branch 'master' into fix/auth-input-mac 2025-12-29 16:49:37 +03:00
Albert Medrano-Lopez
b62d878a0e feat(auth): Allow non-interactive Anilist authentication
This enhances the `anilist auth` command by allowing the authentication token to be provided directly as an argument or from a file. This provides a non-interactive way to authenticate, which is useful for scripting or for users who have issues with the interactive browser-based flow.

The `auth` command now accepts an optional `token_input` argument.
- If the argument is a valid file path, the token is read from the file.
- Otherwise, the argument is treated as the token itself.
- If no argument is provided, the command falls back to the previous interactive method.

The README is also updated to document these new authentication options.

This commit also includes:
- `test`: Unit tests for the new authentication logic (``test_auth.py``) and for the mapper fix (``test_mapper.py``).
- `fix(api)`: A fix in the Anilist API mapper to handle potentially missing data in the API response, making it more robust.
- `style`: Minor code style and formatting fixes throughout the codebase.
2025-12-29 04:05:28 -08:00
Benexl
bcc5e7df8e feat: allow disabling of initial config creation 2025-12-29 11:53:02 +03:00
Benedict Xavier
df8e925eec Update README with project reference and contribution info
Added a project reference and updated contributing section.
2025-12-29 00:32:33 +03:00
Benexl
9d9fa55b69 chore: bump version 2025-12-28 17:54:14 +03:00
Benedict Xavier
42f7e1d4e2 Merge pull request #174 from viu-media/minor-fixes 2025-12-28 17:51:57 +03:00
Type-Delta
7f4a1f265a fix: animepahe unable to stream/download 2025-12-28 19:20:46 +07:00
Benexl
12ef447eaf feat: add vlc player which i somehow forgot lol 2025-12-28 01:25:18 +03:00
Benexl
75b1b8fab4 chore: bump version 2025-12-27 19:39:18 +03:00
Benexl
6f4155dd65 feat: add logger param 2025-12-27 19:39:10 +03:00
23 changed files with 521 additions and 85 deletions

View File

@@ -1,9 +1,9 @@
name: Mark Stale Issues and Pull Requests
on:
schedule:
# Runs every day at 6:30 UTC
- cron: "30 6 * * *"
# schedule:
# Runs every day at 6:30 UTC
# - cron: "30 6 * * *"
# Allows you to run this workflow manually from the Actions tab for testing
workflow_dispatch:

View File

@@ -181,7 +181,7 @@ Get up and running in three simple steps:
```bash
viu anilist auth
```
This will open your browser. Authorize the app and paste the obtained token back into the terminal.
This will open your browser. Authorize the app and paste the obtained token back into the terminal. Alternatively, you can pass the token directly as an argument, or provide a path to a text file containing the token.
2. **Launch the Interactive TUI:**
```bash
@@ -362,6 +362,9 @@ You can run the background worker as a systemd service for persistence.
systemctl --user daemon-reload
systemctl --user enable --now viu-worker.service
```
## Project using it
**[Inazuma](https://github.com/viu-media/Inazuma)** - official gui wrapper over viu built in kivymd
## Contributing

View File

@@ -1,6 +1,6 @@
[project]
name = "viu-media"
version = "3.3.3"
version = "3.3.6"
description = "A browser anime site experience from the terminal"
license = "UNLICENSE"
readme = "README.md"

View File

@@ -0,0 +1,284 @@
from unittest.mock import MagicMock, patch
import pytest
from click.testing import CliRunner
from viu_media.cli.commands.anilist.commands.auth import auth
@pytest.fixture
def runner():
return CliRunner()
@pytest.fixture
def mock_config():
config = MagicMock()
config.user.interactive = True
return config
@pytest.fixture
def mock_auth_service():
with patch("viu_media.cli.service.auth.AuthService") as mock:
yield mock
@pytest.fixture
def mock_feedback_service():
with patch("viu_media.cli.service.feedback.FeedbackService") as mock:
yield mock
@pytest.fixture
def mock_selector():
with patch("viu_media.libs.selectors.selector.create_selector") as mock:
yield mock
@pytest.fixture
def mock_api_client():
with patch("viu_media.libs.media_api.api.create_api_client") as mock:
yield mock
@pytest.fixture
def mock_webbrowser():
with patch("viu_media.cli.commands.anilist.commands.auth.webbrowser") as mock:
yield mock
def test_auth_with_token_argument(
runner,
mock_config,
mock_auth_service,
mock_feedback_service,
mock_selector,
mock_api_client,
):
"""Test 'viu anilist auth <token>'."""
api_client_instance = mock_api_client.return_value
profile_mock = MagicMock()
profile_mock.name = "testuser"
api_client_instance.authenticate.return_value = profile_mock
auth_service_instance = mock_auth_service.return_value
auth_service_instance.get_auth.return_value = None
result = runner.invoke(auth, ["test_token"], obj=mock_config)
assert result.exit_code == 0
mock_api_client.assert_called_with("anilist", mock_config)
api_client_instance.authenticate.assert_called_with("test_token")
auth_service_instance.save_user_profile.assert_called_with(
profile_mock, "test_token"
)
feedback_instance = mock_feedback_service.return_value
feedback_instance.info.assert_called_with("Successfully logged in as testuser! ✨")
def test_auth_with_token_file(
runner,
mock_config,
mock_auth_service,
mock_feedback_service,
mock_selector,
mock_api_client,
tmp_path,
):
"""Test 'viu anilist auth <path/to/token.txt>'."""
token_file = tmp_path / "token.txt"
token_file.write_text("file_token")
api_client_instance = mock_api_client.return_value
profile_mock = MagicMock()
profile_mock.name = "testuser"
api_client_instance.authenticate.return_value = profile_mock
auth_service_instance = mock_auth_service.return_value
auth_service_instance.get_auth.return_value = None
result = runner.invoke(auth, [str(token_file)], obj=mock_config)
assert result.exit_code == 0
mock_api_client.assert_called_with("anilist", mock_config)
api_client_instance.authenticate.assert_called_with("file_token")
auth_service_instance.save_user_profile.assert_called_with(
profile_mock, "file_token"
)
feedback_instance = mock_feedback_service.return_value
feedback_instance.info.assert_called_with("Successfully logged in as testuser! ✨")
def test_auth_with_empty_token_file(
runner,
mock_config,
mock_auth_service,
mock_feedback_service,
mock_selector,
mock_api_client,
tmp_path,
):
"""Test 'viu anilist auth' with an empty token file."""
token_file = tmp_path / "token.txt"
token_file.write_text("")
auth_service_instance = mock_auth_service.return_value
auth_service_instance.get_auth.return_value = None
result = runner.invoke(auth, [str(token_file)], obj=mock_config)
assert result.exit_code == 0
feedback_instance = mock_feedback_service.return_value
feedback_instance.error.assert_called_with(f"Token file is empty: {token_file}")
def test_auth_interactive(
runner,
mock_config,
mock_auth_service,
mock_feedback_service,
mock_selector,
mock_api_client,
mock_webbrowser,
):
"""Test 'viu anilist auth' interactive mode."""
mock_webbrowser.open.return_value = True
selector_instance = mock_selector.return_value
selector_instance.ask.return_value = "interactive_token"
api_client_instance = mock_api_client.return_value
profile_mock = MagicMock()
profile_mock.name = "testuser"
api_client_instance.authenticate.return_value = profile_mock
auth_service_instance = mock_auth_service.return_value
auth_service_instance.get_auth.return_value = None
result = runner.invoke(auth, [], obj=mock_config)
assert result.exit_code == 0
selector_instance.ask.assert_called_with("Enter your AniList Access Token")
api_client_instance.authenticate.assert_called_with("interactive_token")
auth_service_instance.save_user_profile.assert_called_with(
profile_mock, "interactive_token"
)
feedback_instance = mock_feedback_service.return_value
feedback_instance.info.assert_called_with("Successfully logged in as testuser! ✨")
def test_auth_status_logged_in(
runner, mock_config, mock_auth_service, mock_feedback_service
):
"""Test 'viu anilist auth --status' when logged in."""
auth_service_instance = mock_auth_service.return_value
user_data_mock = MagicMock()
user_data_mock.user_profile = "testuser"
auth_service_instance.get_auth.return_value = user_data_mock
result = runner.invoke(auth, ["--status"], obj=mock_config)
assert result.exit_code == 0
feedback_instance = mock_feedback_service.return_value
feedback_instance.info.assert_called_with("Logged in as: testuser")
def test_auth_status_logged_out(
runner, mock_config, mock_auth_service, mock_feedback_service
):
"""Test 'viu anilist auth --status' when logged out."""
auth_service_instance = mock_auth_service.return_value
auth_service_instance.get_auth.return_value = None
result = runner.invoke(auth, ["--status"], obj=mock_config)
assert result.exit_code == 0
feedback_instance = mock_feedback_service.return_value
feedback_instance.error.assert_called_with("Not logged in.")
def test_auth_logout(
runner, mock_config, mock_auth_service, mock_feedback_service, mock_selector
):
"""Test 'viu anilist auth --logout'."""
selector_instance = mock_selector.return_value
selector_instance.confirm.return_value = True
result = runner.invoke(auth, ["--logout"], obj=mock_config)
assert result.exit_code == 0
auth_service_instance = mock_auth_service.return_value
auth_service_instance.clear_user_profile.assert_called_once()
feedback_instance = mock_feedback_service.return_value
feedback_instance.info.assert_called_with("You have been logged out.")
def test_auth_logout_cancel(
runner, mock_config, mock_auth_service, mock_feedback_service, mock_selector
):
"""Test 'viu anilist auth --logout' when user cancels."""
selector_instance = mock_selector.return_value
selector_instance.confirm.return_value = False
result = runner.invoke(auth, ["--logout"], obj=mock_config)
assert result.exit_code == 0
auth_service_instance = mock_auth_service.return_value
auth_service_instance.clear_user_profile.assert_not_called()
def test_auth_already_logged_in_relogin_yes(
runner,
mock_config,
mock_auth_service,
mock_feedback_service,
mock_selector,
mock_api_client,
):
"""Test 'viu anilist auth' when already logged in and user chooses to relogin."""
auth_service_instance = mock_auth_service.return_value
auth_profile_mock = MagicMock()
auth_profile_mock.user_profile.name = "testuser"
auth_service_instance.get_auth.return_value = auth_profile_mock
selector_instance = mock_selector.return_value
selector_instance.confirm.return_value = True
selector_instance.ask.return_value = "new_token"
api_client_instance = mock_api_client.return_value
new_profile_mock = MagicMock()
new_profile_mock.name = "newuser"
api_client_instance.authenticate.return_value = new_profile_mock
result = runner.invoke(auth, [], obj=mock_config)
assert result.exit_code == 0
selector_instance.confirm.assert_called_with(
"You are already logged in as testuser. Would you like to relogin"
)
auth_service_instance.save_user_profile.assert_called_with(
new_profile_mock, "new_token"
)
feedback_instance = mock_feedback_service.return_value
feedback_instance.info.assert_called_with("Successfully logged in as newuser! ✨")
def test_auth_already_logged_in_relogin_no(
runner, mock_config, mock_auth_service, mock_feedback_service, mock_selector
):
"""Test 'viu anilist auth' when already logged in and user chooses not to relogin."""
auth_service_instance = mock_auth_service.return_value
auth_profile_mock = MagicMock()
auth_profile_mock.user_profile.name = "testuser"
auth_service_instance.get_auth.return_value = auth_profile_mock
selector_instance = mock_selector.return_value
selector_instance.confirm.return_value = False
result = runner.invoke(auth, [], obj=mock_config)
assert result.exit_code == 0
auth_service_instance.save_user_profile.assert_not_called()
feedback_instance = mock_feedback_service.return_value
feedback_instance.info.assert_not_called()

0
tests/libs/__init__.py Normal file
View File

View File

View File

View File

@@ -0,0 +1,54 @@
from typing import Any
from viu_media.libs.media_api.anilist.mapper import to_generic_user_profile
from viu_media.libs.media_api.anilist.types import AnilistViewerData
from viu_media.libs.media_api.types import UserProfile
def test_to_generic_user_profile_success():
data: AnilistViewerData = {
"data": {
"Viewer": {
"id": 123,
"name": "testuser",
"avatar": {
"large": "https://example.com/avatar.png",
"medium": "https://example.com/avatar_medium.png",
"extraLarge": "https://example.com/avatar_extraLarge.png",
"small": "https://example.com/avatar_small.png",
},
"bannerImage": "https://example.com/banner.png",
"token": "test_token",
}
}
}
profile = to_generic_user_profile(data)
assert isinstance(profile, UserProfile)
assert profile.id == 123
assert profile.name == "testuser"
assert profile.avatar_url == "https://example.com/avatar.png"
assert profile.banner_url == "https://example.com/banner.png"
def test_to_generic_user_profile_data_none():
data: Any = {"data": None}
profile = to_generic_user_profile(data)
assert profile is None
def test_to_generic_user_profile_no_data_key():
data: Any = {"errors": [{"message": "Invalid token"}]}
profile = to_generic_user_profile(data)
assert profile is None
def test_to_generic_user_profile_no_viewer_key():
data: Any = {"data": {"Page": {}}}
profile = to_generic_user_profile(data)
assert profile is None
def test_to_generic_user_profile_viewer_none():
data: Any = {"data": {"Viewer": None}}
profile = to_generic_user_profile(data)
assert profile is None

90
uv.lock generated
View File

@@ -89,11 +89,11 @@ wheels = [
[[package]]
name = "filelock"
version = "3.20.0"
version = "3.20.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
{ url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" },
]
[[package]]
@@ -353,11 +353,11 @@ wheels = [
[[package]]
name = "nodeenv"
version = "1.9.1"
version = "1.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
]
[[package]]
@@ -389,11 +389,11 @@ wheels = [
[[package]]
name = "platformdirs"
version = "4.5.0"
version = "4.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
]
[[package]]
@@ -416,7 +416,7 @@ wheels = [
[[package]]
name = "pre-commit"
version = "4.5.0"
version = "4.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
@@ -425,9 +425,9 @@ dependencies = [
{ name = "pyyaml" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" }
sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" },
{ url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
]
[[package]]
@@ -623,15 +623,15 @@ wheels = [
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2025.10"
version = "2025.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/26/4f/e33132acdb8f732978e577b8a0130a412cbfe7a3414605e3fd380a975522/pyinstaller_hooks_contrib-2025.10.tar.gz", hash = "sha256:a1a737e5c0dccf1cf6f19a25e2efd109b9fec9ddd625f97f553dac16ee884881", size = 168155, upload-time = "2025-11-22T09:34:36.138Z" }
sdist = { url = "https://files.pythonhosted.org/packages/45/2f/2c68b6722d233dae3e5243751aafc932940b836919cfaca22dd0c60d417c/pyinstaller_hooks_contrib-2025.11.tar.gz", hash = "sha256:dfe18632e06655fa88d218e0d768fd753e1886465c12a6d4bce04f1aaeec917d", size = 169183, upload-time = "2025-12-23T12:59:37.361Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/de/a7688eed49a1d3df337cdaa4c0d64e231309a52f269850a72051975e3c4a/pyinstaller_hooks_contrib-2025.10-py3-none-any.whl", hash = "sha256:aa7a378518772846221f63a84d6306d9827299323243db890851474dfd1231a9", size = 447760, upload-time = "2025-11-22T09:34:34.753Z" },
{ url = "https://files.pythonhosted.org/packages/a7/c4/3a096c6e701832443b957b9dac18a163103360d0c7f5842ca41695371148/pyinstaller_hooks_contrib-2025.11-py3-none-any.whl", hash = "sha256:777e163e2942474aa41a8e6d31ac1635292d63422c3646c176d584d04d971c34", size = 449478, upload-time = "2025-12-23T12:59:35.987Z" },
]
[[package]]
@@ -3457,7 +3457,7 @@ wheels = [
[[package]]
name = "pytest"
version = "8.4.2"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -3466,22 +3466,22 @@ dependencies = [
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-httpx"
version = "0.35.0"
version = "0.36.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/89/5b12b7b29e3d0af3a4b9c071ee92fa25a9017453731a38f08ba01c280f4c/pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f", size = 54146, upload-time = "2024-11-28T19:16:54.237Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/5574834da9499066fa1a5ea9c336f94dba2eae02298d36dab192fcf95c86/pytest_httpx-0.36.0.tar.gz", hash = "sha256:9edb66a5fd4388ce3c343189bc67e7e1cb50b07c2e3fc83b97d511975e8a831b", size = 56793, upload-time = "2025-12-02T16:34:57.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/ed/026d467c1853dd83102411a78126b4842618e86c895f93528b0528c7a620/pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744", size = 19442, upload-time = "2024-11-28T19:16:52.787Z" },
{ url = "https://files.pythonhosted.org/packages/e2/d2/1eb1ea9c84f0d2033eb0b49675afdc71aa4ea801b74615f00f3c33b725e3/pytest_httpx-0.36.0-py3-none-any.whl", hash = "sha256:bd4c120bb80e142df856e825ec9f17981effb84d159f9fa29ed97e2357c3a9c8", size = 20229, upload-time = "2025-12-02T16:34:56.45Z" },
]
[[package]]
@@ -3661,28 +3661,28 @@ wheels = [
[[package]]
name = "ruff"
version = "0.14.7"
version = "0.14.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" }
sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" },
{ url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" },
{ url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" },
{ url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" },
{ url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" },
{ url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" },
{ url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" },
{ url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" },
{ url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" },
{ url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" },
{ url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" },
{ url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" },
{ url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" },
{ url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" },
{ url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" },
{ url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" },
{ url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" },
{ url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" },
{ url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" },
{ url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" },
{ url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" },
{ url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" },
{ url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" },
{ url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" },
{ url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" },
{ url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" },
{ url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" },
{ url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" },
{ url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" },
{ url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" },
{ url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" },
{ url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" },
{ url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" },
{ url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" },
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
]
[[package]]
@@ -3743,7 +3743,7 @@ wheels = [
[[package]]
name = "viu-media"
version = "3.3.3"
version = "3.3.6"
source = { editable = "." }
dependencies = [
{ name = "click" },
@@ -3846,9 +3846,9 @@ wheels = [
[[package]]
name = "yt-dlp"
version = "2025.11.12"
version = "2025.12.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/41/53ad8c6e74d6627bd598dfbb8ad7c19d5405e438210ad0bbaf1b288387e7/yt_dlp-2025.11.12.tar.gz", hash = "sha256:5f0795a6b8fc57a5c23332d67d6c6acf819a0b46b91a6324bae29414fa97f052", size = 3076928, upload-time = "2025-11-12T01:00:38.43Z" }
sdist = { url = "https://files.pythonhosted.org/packages/14/77/db924ebbd99d0b2b571c184cb08ed232cf4906c6f9b76eed763cd2c84170/yt_dlp-2025.12.8.tar.gz", hash = "sha256:b773c81bb6b71cb2c111cfb859f453c7a71cf2ef44eff234ff155877184c3e4f", size = 3088947, upload-time = "2025-12-08T00:16:01.649Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/16/fdebbee6473473a1c0576bd165a50e4a70762484d638c1d59fa9074e175b/yt_dlp-2025.11.12-py3-none-any.whl", hash = "sha256:b47af37bbb16b08efebb36825a280ea25a507c051f93bf413a6e4a0e586c6e79", size = 3279151, upload-time = "2025-11-12T01:00:35.813Z" },
{ url = "https://files.pythonhosted.org/packages/6e/2f/98c3596ad923f8efd32c90dca62e241e8ad9efcebf20831173c357042ba0/yt_dlp-2025.12.8-py3-none-any.whl", hash = "sha256:36e2584342e409cfbfa0b5e61448a1c5189e345cf4564294456ee509e7d3e065", size = 3291464, upload-time = "2025-12-08T00:15:58.556Z" },
]

View File

@@ -189,7 +189,7 @@ You can disable this message by turning off the welcome_screen option in the con
):
import subprocess
_cli_cmd_name="viu" if not shutil.which("viu-media") else "viu-media"
_cli_cmd_name = "viu" if not shutil.which("viu-media") else "viu-media"
cmd = [_cli_cmd_name, "config", "--update"]
print(f"running '{' '.join(cmd)}'...")
subprocess.run(cmd)

View File

@@ -1,25 +1,72 @@
import click
import webbrowser
from pathlib import Path
import click
from .....core.config.model import AppConfig
def _get_token(feedback, selector, token_input: str | None) -> str | None:
"""
Retrieves the authentication token from a file path, a direct string, or an interactive prompt.
"""
if token_input:
path = Path(token_input)
if path.is_file():
try:
token = path.read_text().strip()
if not token:
feedback.error(f"Token file is empty: {path}")
return None
return token
except Exception as e:
feedback.error(f"Error reading token from file: {e}")
return None
return token_input
from .....core.constants import ANILIST_AUTH
open_success = webbrowser.open(ANILIST_AUTH, new=2)
if open_success:
feedback.info("Your browser has been opened to obtain an AniList token.")
feedback.info(
f"Or you can visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta]."
)
else:
feedback.warning(
f"Failed to open the browser. Please visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta]."
)
feedback.info(
"After authorizing, copy the token from the address bar and paste it below."
)
return selector.ask("Enter your AniList Access Token")
@click.command(help="Login to your AniList account to enable progress tracking.")
@click.option("--status", "-s", is_flag=True, help="Check current login status.")
@click.option("--logout", "-l", is_flag=True, help="Log out and erase credentials.")
@click.argument("token_input", required=False, type=str)
@click.pass_obj
def auth(config: AppConfig, status: bool, logout: bool):
"""Handles user authentication and credential management."""
from .....core.constants import ANILIST_AUTH
def auth(config: AppConfig, status: bool, logout: bool, token_input: str | None):
"""
Handles user authentication and credential management.
This command allows you to log in to your AniList account to enable
progress tracking and other features.
You can provide your authentication token in three ways:
1. Interactively: Run the command without arguments to open a browser
and be prompted to paste the token.
2. As an argument: Pass the token string directly to the command.
$ viu anilist auth "your_token_here"
3. As a file: Pass the path to a text file containing the token.
$ viu anilist auth /path/to/token.txt
"""
from .....libs.media_api.api import create_api_client
from .....libs.selectors.selector import create_selector
from ....service.auth import AuthService
from ....service.feedback import FeedbackService
auth_service = AuthService("anilist")
feedback = FeedbackService(config)
selector = create_selector(config)
feedback.clear_console()
if status:
user_data = auth_service.get_auth()
@@ -29,6 +76,11 @@ def auth(config: AppConfig, status: bool, logout: bool):
feedback.error("Not logged in.")
return
from .....libs.selectors.selector import create_selector
selector = create_selector(config)
feedback.clear_console()
if logout:
if selector.confirm("Are you sure you want to log out and erase your token?"):
auth_service.clear_user_profile()
@@ -40,27 +92,14 @@ def auth(config: AppConfig, status: bool, logout: bool):
f"You are already logged in as {auth_profile.user_profile.name}.Would you like to relogin"
):
return
api_client = create_api_client("anilist", config)
token = _get_token(feedback, selector, token_input)
open_success = webbrowser.open(ANILIST_AUTH, new=2)
if open_success:
feedback.info("Your browser has been opened to obtain an AniList token.")
feedback.info(
f"or you can visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta]."
)
else:
feedback.warning(
f"Failed to open the browser. Please visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta]."
)
feedback.info(
"After authorizing, copy the token from the address bar and paste it below."
)
token = selector.ask("Enter your AniList Access Token")
if not token:
feedback.error("Login cancelled.")
if not token_input:
feedback.error("Login cancelled.")
return
api_client = create_api_client("anilist", config)
# Use the API client to validate the token and get profile info
profile = api_client.authenticate(token.strip())

View File

@@ -71,7 +71,7 @@ class ConfigLoader:
return app_config
def load(self, update: Dict = {}) -> AppConfig:
def load(self, update: Dict = {}, allow_setup=True) -> AppConfig:
"""
Loads the configuration and returns a populated, validated AppConfig object.
@@ -84,7 +84,7 @@ class ConfigLoader:
Raises:
ConfigError: If the configuration file contains validation or parsing errors.
"""
if not self.config_path.exists():
if not self.config_path.exists() and allow_setup:
return self._handle_first_run()
try:

View File

@@ -132,6 +132,7 @@ APP_SERVICE = "Configuration for the background download service."
APP_FZF = "Settings for the FZF selector interface."
APP_ROFI = "Settings for the Rofi selector interface."
APP_MPV = "Configuration for the MPV media player."
APP_VLC = "Configuration for the VLC media player."
APP_MEDIA_REGISTRY = "Configuration for the media registry."
APP_SESSIONS = "Configuration for sessions."

View File

@@ -534,6 +534,7 @@ class AppConfig(BaseModel):
description=desc.APP_ROFI,
)
mpv: MpvConfig = Field(default_factory=MpvConfig, description=desc.APP_MPV)
vlc: VlcConfig = Field(default_factory=VlcConfig, description=desc.APP_VLC)
media_registry: MediaRegistryConfig = Field(
default_factory=MediaRegistryConfig, description=desc.APP_MEDIA_REGISTRY
)

View File

@@ -9,6 +9,7 @@ class DownloadParams:
episode_title: str
silent: bool
progress_hooks: list[Callable] = field(default_factory=list)
logger: object | None = None
vid_format: str = "best"
force_unknown_ext: bool = False
verbose: bool = False

View File

@@ -30,6 +30,9 @@ class YtDLPDownloader(BaseDownloader):
sub_paths = []
merged_path = None
logger.debug(f"Starting download for URL: {params.url}")
logger.debug(f"Using Headers: {params.headers}")
if TORRENT_REGEX.match(params.url):
from .torrents import download_torrent_with_webtorrent_cli
@@ -91,6 +94,7 @@ class YtDLPDownloader(BaseDownloader):
else tuple(),
"progress_hooks": params.progress_hooks,
"nocheckcertificate": params.no_check_certificate,
"logger": params.logger,
}
opts = opts
if params.force_ffmpeg or params.hls_use_mpegts or params.hls_use_h264:

View File

@@ -323,7 +323,14 @@ def to_generic_user_list_result(data: AnilistMediaLists) -> Optional[MediaSearch
def to_generic_user_profile(data: AnilistViewerData) -> Optional[UserProfile]:
"""Maps a raw AniList viewer response to a generic UserProfile."""
viewer_data: Optional[AnilistCurrentlyLoggedInUser] = data["data"]["Viewer"]
data_node = data.get("data")
if not data_node:
return None
viewer_data: Optional[AnilistCurrentlyLoggedInUser] = data_node.get("Viewer")
if not viewer_data:
return None
return UserProfile(
id=viewer_data["id"],

View File

@@ -52,7 +52,7 @@ class MpvPlayer(BasePlayer):
if TORRENT_REGEX.match(params.url) and detect.is_running_in_termux():
raise ViuError("Unable to play torrents on termux")
elif params.syncplay and detect.is_running_in_termux():
raise ViuError("Unable to play torrents on termux")
raise ViuError("Unable to play with syncplay on termux")
elif detect.is_running_in_termux():
return self._play_on_mobile(params)
else:

View File

@@ -41,6 +41,10 @@ class PlayerFactory:
from .mpv.player import MpvPlayer
return MpvPlayer(config.mpv)
elif player_name == "vlc":
from .vlc.player import VlcPlayer
return VlcPlayer(config.vlc)
raise NotImplementedError(
f"Configuration logic for player '{player_name}' not implemented in factory."
)

View File

@@ -46,10 +46,11 @@ class VlcPlayer(BasePlayer):
Returns:
PlayerResult: Information about the playback session.
"""
if not self.executable:
raise ViuError("VLC executable not found in PATH.")
if TORRENT_REGEX.match(params.url) and detect.is_running_in_termux():
raise ViuError("Unable to play torrents on termux")
elif params.syncplay and detect.is_running_in_termux():
raise ViuError("Unable to play with syncplay on termux")
elif detect.is_running_in_termux():
return self._play_on_mobile(params)
else:
return self._play_on_desktop(params)
@@ -116,6 +117,9 @@ class VlcPlayer(BasePlayer):
Returns:
PlayerResult: Information about the playback session.
"""
if not self.executable:
raise ViuError("VLC executable not found in PATH.")
if TORRENT_REGEX.search(params.url):
return self._stream_on_desktop_with_webtorrent_cli(params)

View File

@@ -3,6 +3,8 @@ import re
ANIMEPAHE = "animepahe.si"
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}"
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api"
CDN_PROVIDER = "kwik.cx"
CDN_PROVIDER_BASE = f"https://{CDN_PROVIDER}"
SERVERS_AVAILABLE = ["kwik"]
REQUEST_HEADERS = {
@@ -25,7 +27,7 @@ SERVER_HEADERS = {
"Accept-Encoding": "Utf-8",
"DNT": "1",
"Connection": "keep-alive",
"Referer": "https://animepahe.si/",
"Referer": ANIMEPAHE_BASE + "/",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "iframe",
"Sec-Fetch-Mode": "navigate",
@@ -33,5 +35,22 @@ SERVER_HEADERS = {
"Priority": "u=4",
"TE": "trailers",
}
STREAM_HEADERS = {
# "Host": "vault-16.owocdn.top", # This will have to be the actual host of the stream (behind Kwik)
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Origin": CDN_PROVIDER_BASE,
"Sec-GPC": "1",
"Connection": "keep-alive",
"Referer": CDN_PROVIDER_BASE + "/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
"TE": "trailers",
}
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
KWIK_RE = re.compile(r"Player\|(.+?)'")

View File

@@ -88,6 +88,7 @@ def map_to_server(
episode: AnimeEpisodeInfo,
translation_type: str,
stream_links: list[tuple[str, str]],
headers: dict[str, str],
) -> Server:
links = [
EpisodeStream(
@@ -97,4 +98,6 @@ def map_to_server(
)
for link in stream_links
]
return Server(name="kwik", links=links, episode_title=episode.title)
return Server(
name="kwik", links=links, episode_title=episode.title, headers=headers
)

View File

@@ -1,6 +1,7 @@
import logging
from functools import lru_cache
from typing import Iterator, Optional
from urllib.parse import urlparse
from ..base import BaseAnimeProvider
from ..params import AnimeParams, EpisodeStreamsParams, SearchParams
@@ -9,9 +10,11 @@ from ..utils.debug import debug_provider
from .constants import (
ANIMEPAHE_BASE,
ANIMEPAHE_ENDPOINT,
CDN_PROVIDER,
JUICY_STREAM_REGEX,
REQUEST_HEADERS,
SERVER_HEADERS,
STREAM_HEADERS,
)
from .extractor import process_animepahe_embed_page
from .mappers import map_to_anime_result, map_to_search_results, map_to_server
@@ -132,6 +135,7 @@ class AnimePahe(BaseAnimeProvider):
quality = None
translation_type = None
stream_links = []
stream_host = None
# TODO: better document the scraping process
for res_dict in res_dicts:
@@ -170,13 +174,21 @@ class AnimePahe(BaseAnimeProvider):
continue
logger.debug(f"Found juicy stream: {juicy_stream.group(1)}")
juicy_stream = juicy_stream.group(1)
stream_host = urlparse(juicy_stream).hostname
quality = res_dict["resolution"]
logger.debug(f"Found quality: {quality}")
translation_type = data_audio
stream_links.append((quality, juicy_stream))
if translation_type and stream_links:
yield map_to_server(episode, translation_type, stream_links)
headers = {
"User-Agent": self.client.headers["User-Agent"],
"Host": stream_host or CDN_PROVIDER,
**STREAM_HEADERS,
}
yield map_to_server(
episode, translation_type, stream_links, headers=headers
)
@lru_cache()
def _get_episode_info(