weeb-cli 2.8.0__tar.gz → 2.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/PKG-INFO +8 -1
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/README.md +7 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/pyproject.toml +1 -1
- weeb_cli-2.8.1/tests/test_anilist_tracker.py +222 -0
- weeb_cli-2.8.1/tests/test_kitsu_tracker.py +179 -0
- weeb_cli-2.8.1/tests/test_mal_tracker.py +322 -0
- weeb_cli-2.8.1/weeb_cli/__init__.py +1 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/downloads.py +8 -1
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/search/watch_flow.py +2 -2
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/settings/settings_trackers.py +123 -1
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/locales/en.json +15 -1
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/locales/tr.json +15 -1
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/main.py +8 -1
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/tracker.py +255 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli.egg-info/PKG-INFO +8 -1
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli.egg-info/SOURCES.txt +3 -0
- weeb_cli-2.8.0/weeb_cli/__init__.py +0 -1
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/LICENSE +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/setup.cfg +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/tests/test_api.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/tests/test_cache.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/tests/test_exceptions.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/tests/test_providers.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/tests/test_sanitizer.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/__main__.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/api.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/search/__init__.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/search/anime_details.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/search/download_flow.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/search/episode_utils.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/search/search_handlers.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/search/stream_utils.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/search.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/serve.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/settings/__init__.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/settings/settings_backup.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/settings/settings_cache.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/settings/settings_config.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/settings/settings_download.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/settings/settings_drives.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/settings/settings_menu.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/settings/settings_shortcuts.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/settings.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/setup.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/commands/watchlist.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/config.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/exceptions.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/i18n.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/providers/__init__.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/providers/allanime.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/providers/animecix.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/providers/anizle.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/providers/base.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/providers/extractors/__init__.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/providers/extractors/megacloud.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/providers/hianime.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/providers/registry.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/providers/turkanime.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/__init__.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/_base.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/_tracker_base.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/cache.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/database.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/dependency_manager.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/details.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/discord_rpc.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/downloader.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/error_handler.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/headless_downloader.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/local_library.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/logger.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/notifier.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/player.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/progress.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/scraper.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/search.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/shortcuts.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/stream_validator.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/updater.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/services/watch.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/templates/anilist_error.html +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/templates/anilist_success.html +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/templates/mal_error.html +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/templates/mal_success.html +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/ui/__init__.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/ui/header.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/ui/menu.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/ui/prompt.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/utils/__init__.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli/utils/sanitizer.py +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli.egg-info/dependency_links.txt +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli.egg-info/entry_points.txt +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli.egg-info/requires.txt +0 -0
- {weeb_cli-2.8.0 → weeb_cli-2.8.1}/weeb_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weeb-cli
|
|
3
|
-
Version: 2.8.
|
|
3
|
+
Version: 2.8.1
|
|
4
4
|
Summary: Tarayıcı yok, reklam yok, dikkat dağıtıcı unsur yok. Sadece siz ve eşsiz bir anime izleme deneyimi.
|
|
5
5
|
Author-email: ewgsta <ewgst@proton.me>
|
|
6
6
|
License-Expression: CC-BY-NC-ND-4.0
|
|
@@ -83,6 +83,13 @@ Dynamic: license-file
|
|
|
83
83
|
- Resume interrupted downloads
|
|
84
84
|
- Smart file naming (`Anime Name - S1E1.mp4`)
|
|
85
85
|
|
|
86
|
+
### Tracking & Sync
|
|
87
|
+
- **AniList** integration with OAuth
|
|
88
|
+
- **MyAnimeList** integration with OAuth
|
|
89
|
+
- **Kitsu** integration with email/password
|
|
90
|
+
- Automatic progress sync
|
|
91
|
+
- Offline queue for pending updates
|
|
92
|
+
|
|
86
93
|
### Local Library
|
|
87
94
|
- Auto-scan downloaded anime
|
|
88
95
|
- External drive support (USB, HDD)
|
|
@@ -45,6 +45,13 @@
|
|
|
45
45
|
- Resume interrupted downloads
|
|
46
46
|
- Smart file naming (`Anime Name - S1E1.mp4`)
|
|
47
47
|
|
|
48
|
+
### Tracking & Sync
|
|
49
|
+
- **AniList** integration with OAuth
|
|
50
|
+
- **MyAnimeList** integration with OAuth
|
|
51
|
+
- **Kitsu** integration with email/password
|
|
52
|
+
- Automatic progress sync
|
|
53
|
+
- Offline queue for pending updates
|
|
54
|
+
|
|
48
55
|
### Local Library
|
|
49
56
|
- Auto-scan downloaded anime
|
|
50
57
|
- External drive support (USB, HDD)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weeb-cli"
|
|
7
|
-
version = "2.8.
|
|
7
|
+
version = "2.8.1"
|
|
8
8
|
description = "Tarayıcı yok, reklam yok, dikkat dağıtıcı unsur yok. Sadece siz ve eşsiz bir anime izleme deneyimi."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "ewgsta", email = "ewgst@proton.me" }]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
from weeb_cli.services.tracker import AniListTracker
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def anilist_tracker():
|
|
8
|
+
tracker = AniListTracker()
|
|
9
|
+
tracker._db = MagicMock()
|
|
10
|
+
return tracker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestAniListAuthentication:
|
|
14
|
+
|
|
15
|
+
def test_authenticate_success(self, anilist_tracker):
|
|
16
|
+
mock_viewer = {"id": 123, "name": "TestUser"}
|
|
17
|
+
|
|
18
|
+
with patch.object(anilist_tracker, "_get_viewer", return_value=mock_viewer):
|
|
19
|
+
result = anilist_tracker.authenticate("test_token_123")
|
|
20
|
+
|
|
21
|
+
assert result is True
|
|
22
|
+
anilist_tracker.db.set_config.assert_any_call("anilist_token", "test_token_123")
|
|
23
|
+
anilist_tracker.db.set_config.assert_any_call("anilist_user_id", "123")
|
|
24
|
+
anilist_tracker.db.set_config.assert_any_call("anilist_username", "TestUser")
|
|
25
|
+
|
|
26
|
+
def test_authenticate_failure(self, anilist_tracker):
|
|
27
|
+
with patch.object(anilist_tracker, "_get_viewer", return_value=None):
|
|
28
|
+
result = anilist_tracker.authenticate("invalid_token")
|
|
29
|
+
|
|
30
|
+
assert result is False
|
|
31
|
+
|
|
32
|
+
def test_is_authenticated(self, anilist_tracker):
|
|
33
|
+
anilist_tracker.db.get_config.return_value = "test_token"
|
|
34
|
+
assert anilist_tracker.is_authenticated() is True
|
|
35
|
+
|
|
36
|
+
anilist_tracker.db.get_config.return_value = None
|
|
37
|
+
anilist_tracker._token = None
|
|
38
|
+
assert anilist_tracker.is_authenticated() is False
|
|
39
|
+
|
|
40
|
+
def test_logout(self, anilist_tracker):
|
|
41
|
+
anilist_tracker._token = "test_token"
|
|
42
|
+
anilist_tracker._user_id = "123"
|
|
43
|
+
|
|
44
|
+
anilist_tracker.logout()
|
|
45
|
+
|
|
46
|
+
assert anilist_tracker._token is None
|
|
47
|
+
assert anilist_tracker._user_id is None
|
|
48
|
+
anilist_tracker.db.set_config.assert_any_call("anilist_token", None)
|
|
49
|
+
anilist_tracker.db.set_config.assert_any_call("anilist_user_id", None)
|
|
50
|
+
anilist_tracker.db.set_config.assert_any_call("anilist_username", None)
|
|
51
|
+
|
|
52
|
+
def test_get_username(self, anilist_tracker):
|
|
53
|
+
anilist_tracker.db.get_config.return_value = "TestUser"
|
|
54
|
+
username = anilist_tracker.get_username()
|
|
55
|
+
assert username == "TestUser"
|
|
56
|
+
anilist_tracker.db.get_config.assert_called_with("anilist_username")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestAniListGraphQL:
|
|
60
|
+
|
|
61
|
+
def test_graphql_success(self, anilist_tracker):
|
|
62
|
+
anilist_tracker._token = "test_token"
|
|
63
|
+
|
|
64
|
+
mock_resp = MagicMock()
|
|
65
|
+
mock_resp.status_code = 200
|
|
66
|
+
mock_resp.json.return_value = {"data": {"Media": {"id": 1}}}
|
|
67
|
+
|
|
68
|
+
with patch("requests.post", return_value=mock_resp):
|
|
69
|
+
result = anilist_tracker._graphql("query { Media { id } }")
|
|
70
|
+
|
|
71
|
+
assert result == {"Media": {"id": 1}}
|
|
72
|
+
|
|
73
|
+
def test_graphql_no_token(self, anilist_tracker):
|
|
74
|
+
anilist_tracker._token = None
|
|
75
|
+
result = anilist_tracker._graphql("query { Media { id } }")
|
|
76
|
+
assert result is None
|
|
77
|
+
|
|
78
|
+
def test_graphql_error(self, anilist_tracker):
|
|
79
|
+
anilist_tracker._token = "test_token"
|
|
80
|
+
|
|
81
|
+
mock_resp = MagicMock()
|
|
82
|
+
mock_resp.status_code = 400
|
|
83
|
+
|
|
84
|
+
with patch("requests.post", return_value=mock_resp):
|
|
85
|
+
result = anilist_tracker._graphql("invalid query")
|
|
86
|
+
|
|
87
|
+
assert result is None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestAniListSearch:
|
|
91
|
+
|
|
92
|
+
def test_search_anime_success(self, anilist_tracker):
|
|
93
|
+
mock_data = {
|
|
94
|
+
"Media": {
|
|
95
|
+
"id": 6547,
|
|
96
|
+
"title": {"romaji": "Angel Beats!", "english": "Angel Beats!"},
|
|
97
|
+
"episodes": 13
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
with patch.object(anilist_tracker, "_graphql", return_value=mock_data):
|
|
102
|
+
result = anilist_tracker.search_anime("Angel Beats")
|
|
103
|
+
|
|
104
|
+
assert result is not None
|
|
105
|
+
assert result["id"] == 6547
|
|
106
|
+
assert result["episodes"] == 13
|
|
107
|
+
|
|
108
|
+
def test_search_anime_not_found(self, anilist_tracker):
|
|
109
|
+
with patch.object(anilist_tracker, "_graphql", return_value=None):
|
|
110
|
+
result = anilist_tracker.search_anime("NonexistentAnime")
|
|
111
|
+
|
|
112
|
+
assert result is None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestAniListProgressUpdate:
|
|
116
|
+
|
|
117
|
+
def test_update_progress_not_authenticated(self, anilist_tracker):
|
|
118
|
+
anilist_tracker.db.get_config.return_value = None
|
|
119
|
+
anilist_tracker._token = None
|
|
120
|
+
|
|
121
|
+
result = anilist_tracker.update_progress("Angel Beats", 5, 13)
|
|
122
|
+
|
|
123
|
+
assert result is False
|
|
124
|
+
pending_calls = [call for call in anilist_tracker.db.set_config.call_args_list
|
|
125
|
+
if "anilist_pending" in str(call)]
|
|
126
|
+
assert len(pending_calls) > 0
|
|
127
|
+
|
|
128
|
+
def test_update_progress_anime_not_found(self, anilist_tracker):
|
|
129
|
+
anilist_tracker._token = "test_token"
|
|
130
|
+
anilist_tracker.db.get_config.return_value = "test_token"
|
|
131
|
+
|
|
132
|
+
with patch.object(anilist_tracker, "search_anime", return_value=None):
|
|
133
|
+
result = anilist_tracker.update_progress("NonexistentAnime", 1, 12)
|
|
134
|
+
|
|
135
|
+
assert result is False
|
|
136
|
+
|
|
137
|
+
def test_update_progress_success_current(self, anilist_tracker):
|
|
138
|
+
anilist_tracker._token = "test_token"
|
|
139
|
+
anilist_tracker.db.get_config.return_value = "test_token"
|
|
140
|
+
|
|
141
|
+
mock_anime = {"id": 6547, "episodes": 13}
|
|
142
|
+
mock_result = {"SaveMediaListEntry": {"id": 1, "progress": 5, "status": "CURRENT"}}
|
|
143
|
+
|
|
144
|
+
with patch.object(anilist_tracker, "search_anime", return_value=mock_anime):
|
|
145
|
+
with patch.object(anilist_tracker, "_graphql", return_value=mock_result):
|
|
146
|
+
result = anilist_tracker.update_progress("Angel Beats", 5, 13)
|
|
147
|
+
|
|
148
|
+
assert result is True
|
|
149
|
+
|
|
150
|
+
def test_update_progress_success_completed(self, anilist_tracker):
|
|
151
|
+
anilist_tracker._token = "test_token"
|
|
152
|
+
anilist_tracker.db.get_config.return_value = "test_token"
|
|
153
|
+
|
|
154
|
+
mock_anime = {"id": 6547, "episodes": 13}
|
|
155
|
+
mock_result = {"SaveMediaListEntry": {"id": 1, "progress": 13, "status": "COMPLETED"}}
|
|
156
|
+
|
|
157
|
+
with patch.object(anilist_tracker, "search_anime", return_value=mock_anime):
|
|
158
|
+
with patch.object(anilist_tracker, "_graphql", return_value=mock_result):
|
|
159
|
+
result = anilist_tracker.update_progress("Angel Beats", 13, 13)
|
|
160
|
+
|
|
161
|
+
assert result is True
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestAniListPendingSync:
|
|
165
|
+
|
|
166
|
+
def test_queue_update(self, anilist_tracker):
|
|
167
|
+
anilist_tracker.db.get_config.return_value = []
|
|
168
|
+
|
|
169
|
+
anilist_tracker._queue_update("Angel Beats", 5, 13)
|
|
170
|
+
|
|
171
|
+
set_calls = anilist_tracker.db.set_config.call_args_list
|
|
172
|
+
pending_call = [call for call in set_calls if "anilist_pending" in str(call)][0]
|
|
173
|
+
pending_data = pending_call[0][1]
|
|
174
|
+
|
|
175
|
+
assert len(pending_data) == 1
|
|
176
|
+
assert pending_data[0]["title"] == "Angel Beats"
|
|
177
|
+
assert pending_data[0]["episode"] == 5
|
|
178
|
+
assert pending_data[0]["total"] == 13
|
|
179
|
+
|
|
180
|
+
def test_sync_pending_success(self, anilist_tracker):
|
|
181
|
+
anilist_tracker._token = "test_token"
|
|
182
|
+
anilist_tracker.db.get_config.side_effect = lambda key: {
|
|
183
|
+
"anilist_token": "test_token",
|
|
184
|
+
"anilist_pending": [
|
|
185
|
+
{"title": "Anime1", "episode": 5, "total": 12, "timestamp": 123456},
|
|
186
|
+
{"title": "Anime2", "episode": 3, "total": 24, "timestamp": 123457}
|
|
187
|
+
]
|
|
188
|
+
}.get(key, None)
|
|
189
|
+
|
|
190
|
+
with patch.object(anilist_tracker, "update_progress", return_value=True):
|
|
191
|
+
synced = anilist_tracker.sync_pending()
|
|
192
|
+
|
|
193
|
+
assert synced == 2
|
|
194
|
+
|
|
195
|
+
def test_sync_pending_partial_failure(self, anilist_tracker):
|
|
196
|
+
anilist_tracker._token = "test_token"
|
|
197
|
+
anilist_tracker.db.get_config.side_effect = lambda key: {
|
|
198
|
+
"anilist_token": "test_token",
|
|
199
|
+
"anilist_pending": [
|
|
200
|
+
{"title": "Anime1", "episode": 5, "total": 12, "timestamp": 123456},
|
|
201
|
+
{"title": "Anime2", "episode": 3, "total": 24, "timestamp": 123457}
|
|
202
|
+
]
|
|
203
|
+
}.get(key, None)
|
|
204
|
+
|
|
205
|
+
with patch.object(anilist_tracker, "update_progress", side_effect=[True, False]):
|
|
206
|
+
synced = anilist_tracker.sync_pending()
|
|
207
|
+
|
|
208
|
+
assert synced == 1
|
|
209
|
+
|
|
210
|
+
def test_get_pending_count(self, anilist_tracker):
|
|
211
|
+
anilist_tracker.db.get_config.return_value = [
|
|
212
|
+
{"title": "Anime1", "episode": 5},
|
|
213
|
+
{"title": "Anime2", "episode": 3}
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
count = anilist_tracker.get_pending_count()
|
|
217
|
+
assert count == 2
|
|
218
|
+
|
|
219
|
+
def test_get_pending_count_empty(self, anilist_tracker):
|
|
220
|
+
anilist_tracker.db.get_config.return_value = []
|
|
221
|
+
count = anilist_tracker.get_pending_count()
|
|
222
|
+
assert count == 0
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
from weeb_cli.services.tracker import KitsuTracker
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def kitsu_tracker():
|
|
8
|
+
tracker = KitsuTracker()
|
|
9
|
+
tracker._db = MagicMock()
|
|
10
|
+
return tracker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestKitsuAuthentication:
|
|
14
|
+
|
|
15
|
+
def test_authenticate_success(self, kitsu_tracker):
|
|
16
|
+
mock_resp = MagicMock()
|
|
17
|
+
mock_resp.status_code = 200
|
|
18
|
+
mock_resp.json.return_value = {"access_token": "test_token"}
|
|
19
|
+
|
|
20
|
+
mock_user_resp = MagicMock()
|
|
21
|
+
mock_user_resp.status_code = 200
|
|
22
|
+
mock_user_resp.json.return_value = {
|
|
23
|
+
"data": [{
|
|
24
|
+
"id": "123",
|
|
25
|
+
"attributes": {"name": "TestUser"}
|
|
26
|
+
}]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
with patch("requests.post", return_value=mock_resp):
|
|
30
|
+
with patch("requests.get", return_value=mock_user_resp):
|
|
31
|
+
result = kitsu_tracker.authenticate("test@example.com", "password")
|
|
32
|
+
|
|
33
|
+
assert result is True
|
|
34
|
+
kitsu_tracker.db.set_config.assert_any_call("kitsu_access_token", "test_token")
|
|
35
|
+
kitsu_tracker.db.set_config.assert_any_call("kitsu_user_id", "123")
|
|
36
|
+
kitsu_tracker.db.set_config.assert_any_call("kitsu_username", "TestUser")
|
|
37
|
+
|
|
38
|
+
def test_authenticate_failure(self, kitsu_tracker):
|
|
39
|
+
mock_resp = MagicMock()
|
|
40
|
+
mock_resp.status_code = 401
|
|
41
|
+
|
|
42
|
+
with patch("requests.post", return_value=mock_resp):
|
|
43
|
+
result = kitsu_tracker.authenticate("test@example.com", "wrong_password")
|
|
44
|
+
|
|
45
|
+
assert result is False
|
|
46
|
+
|
|
47
|
+
def test_is_authenticated(self, kitsu_tracker):
|
|
48
|
+
kitsu_tracker.db.get_config.return_value = "test_token"
|
|
49
|
+
assert kitsu_tracker.is_authenticated() is True
|
|
50
|
+
|
|
51
|
+
kitsu_tracker.db.get_config.return_value = None
|
|
52
|
+
kitsu_tracker._access_token = None
|
|
53
|
+
assert kitsu_tracker.is_authenticated() is False
|
|
54
|
+
|
|
55
|
+
def test_logout(self, kitsu_tracker):
|
|
56
|
+
kitsu_tracker._access_token = "test_token"
|
|
57
|
+
kitsu_tracker._user_id = "123"
|
|
58
|
+
|
|
59
|
+
kitsu_tracker.logout()
|
|
60
|
+
|
|
61
|
+
assert kitsu_tracker._access_token is None
|
|
62
|
+
assert kitsu_tracker._user_id is None
|
|
63
|
+
kitsu_tracker.db.set_config.assert_any_call("kitsu_access_token", None)
|
|
64
|
+
kitsu_tracker.db.set_config.assert_any_call("kitsu_user_id", None)
|
|
65
|
+
kitsu_tracker.db.set_config.assert_any_call("kitsu_username", None)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestKitsuSearch:
|
|
69
|
+
|
|
70
|
+
def test_search_anime_success(self, kitsu_tracker):
|
|
71
|
+
mock_resp = MagicMock()
|
|
72
|
+
mock_resp.status_code = 200
|
|
73
|
+
mock_resp.json.return_value = {
|
|
74
|
+
"data": [{
|
|
75
|
+
"id": "1",
|
|
76
|
+
"attributes": {"canonicalTitle": "Cowboy Bebop"}
|
|
77
|
+
}]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
with patch("requests.get", return_value=mock_resp):
|
|
81
|
+
result = kitsu_tracker.search_anime("Cowboy Bebop")
|
|
82
|
+
|
|
83
|
+
assert result is not None
|
|
84
|
+
assert result["id"] == "1"
|
|
85
|
+
|
|
86
|
+
def test_search_anime_no_results(self, kitsu_tracker):
|
|
87
|
+
mock_resp = MagicMock()
|
|
88
|
+
mock_resp.status_code = 200
|
|
89
|
+
mock_resp.json.return_value = {"data": []}
|
|
90
|
+
|
|
91
|
+
with patch("requests.get", return_value=mock_resp):
|
|
92
|
+
result = kitsu_tracker.search_anime("NonexistentAnime")
|
|
93
|
+
|
|
94
|
+
assert result is None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestKitsuProgressUpdate:
|
|
98
|
+
|
|
99
|
+
def test_update_progress_not_authenticated(self, kitsu_tracker):
|
|
100
|
+
kitsu_tracker.db.get_config.return_value = None
|
|
101
|
+
kitsu_tracker._access_token = None
|
|
102
|
+
|
|
103
|
+
result = kitsu_tracker.update_progress("Cowboy Bebop", 5, 26)
|
|
104
|
+
|
|
105
|
+
assert result is False
|
|
106
|
+
pending = kitsu_tracker.db.set_config.call_args_list
|
|
107
|
+
assert any("kitsu_pending" in str(call) for call in pending)
|
|
108
|
+
|
|
109
|
+
def test_update_progress_anime_not_found(self, kitsu_tracker):
|
|
110
|
+
kitsu_tracker._access_token = "test_token"
|
|
111
|
+
kitsu_tracker._user_id = "123"
|
|
112
|
+
kitsu_tracker.db.get_config.side_effect = lambda key: {
|
|
113
|
+
"kitsu_access_token": "test_token",
|
|
114
|
+
"kitsu_user_id": "123"
|
|
115
|
+
}.get(key)
|
|
116
|
+
|
|
117
|
+
mock_search_resp = MagicMock()
|
|
118
|
+
mock_search_resp.status_code = 200
|
|
119
|
+
mock_search_resp.json.return_value = {"data": []}
|
|
120
|
+
|
|
121
|
+
with patch("requests.get", return_value=mock_search_resp):
|
|
122
|
+
result = kitsu_tracker.update_progress("NonexistentAnime", 1, 12)
|
|
123
|
+
|
|
124
|
+
assert result is False
|
|
125
|
+
|
|
126
|
+
def test_update_progress_create_entry(self, kitsu_tracker):
|
|
127
|
+
kitsu_tracker._access_token = "test_token"
|
|
128
|
+
kitsu_tracker._user_id = "123"
|
|
129
|
+
kitsu_tracker.db.get_config.side_effect = lambda key: {
|
|
130
|
+
"kitsu_access_token": "test_token",
|
|
131
|
+
"kitsu_user_id": "123"
|
|
132
|
+
}.get(key)
|
|
133
|
+
|
|
134
|
+
mock_search_resp = MagicMock()
|
|
135
|
+
mock_search_resp.status_code = 200
|
|
136
|
+
mock_search_resp.json.return_value = {
|
|
137
|
+
"data": [{"id": "1", "attributes": {"canonicalTitle": "Cowboy Bebop"}}]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
mock_entry_resp = MagicMock()
|
|
141
|
+
mock_entry_resp.status_code = 200
|
|
142
|
+
mock_entry_resp.json.return_value = {"data": []}
|
|
143
|
+
|
|
144
|
+
mock_update_resp = MagicMock()
|
|
145
|
+
mock_update_resp.status_code = 201
|
|
146
|
+
|
|
147
|
+
with patch("requests.get", side_effect=[mock_search_resp, mock_entry_resp]):
|
|
148
|
+
with patch("requests.post", return_value=mock_update_resp):
|
|
149
|
+
result = kitsu_tracker.update_progress("Cowboy Bebop", 5, 26)
|
|
150
|
+
|
|
151
|
+
assert result is True
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestKitsuPendingSync:
|
|
155
|
+
|
|
156
|
+
def test_sync_pending_success(self, kitsu_tracker):
|
|
157
|
+
kitsu_tracker._access_token = "test_token"
|
|
158
|
+
kitsu_tracker._user_id = "123"
|
|
159
|
+
kitsu_tracker.db.get_config.side_effect = lambda key: {
|
|
160
|
+
"kitsu_access_token": "test_token",
|
|
161
|
+
"kitsu_user_id": "123",
|
|
162
|
+
"kitsu_pending": [
|
|
163
|
+
{"title": "Anime1", "episode": 5, "total": 12, "timestamp": 123456}
|
|
164
|
+
]
|
|
165
|
+
}.get(key, None)
|
|
166
|
+
|
|
167
|
+
with patch.object(kitsu_tracker, "update_progress", return_value=True):
|
|
168
|
+
synced = kitsu_tracker.sync_pending()
|
|
169
|
+
|
|
170
|
+
assert synced == 1
|
|
171
|
+
|
|
172
|
+
def test_get_pending_count(self, kitsu_tracker):
|
|
173
|
+
kitsu_tracker.db.get_config.return_value = [
|
|
174
|
+
{"title": "Anime1", "episode": 5},
|
|
175
|
+
{"title": "Anime2", "episode": 3}
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
count = kitsu_tracker.get_pending_count()
|
|
179
|
+
assert count == 2
|