redfetch 1.3.0__py3-none-any.whl
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.
- redfetch/__about__.py +24 -0
- redfetch/__init__.py +0 -0
- redfetch/api.py +134 -0
- redfetch/auth.py +573 -0
- redfetch/config.py +483 -0
- redfetch/config_firstrun.py +455 -0
- redfetch/desktop_shortcut.py +81 -0
- redfetch/detecteq.py +116 -0
- redfetch/download.py +312 -0
- redfetch/listener.py +216 -0
- redfetch/main.py +744 -0
- redfetch/meta.py +561 -0
- redfetch/navmesh.py +371 -0
- redfetch/net.py +109 -0
- redfetch/processes.py +118 -0
- redfetch/push.py +246 -0
- redfetch/redfetch.ico +0 -0
- redfetch/runtime_errors.py +96 -0
- redfetch/settings.toml +303 -0
- redfetch/special.py +81 -0
- redfetch/store.py +505 -0
- redfetch/sync.py +261 -0
- redfetch/sync_discovery.py +352 -0
- redfetch/sync_executor.py +164 -0
- redfetch/sync_planner.py +196 -0
- redfetch/sync_remote.py +182 -0
- redfetch/sync_types.py +348 -0
- redfetch/terminal_ui.py +2318 -0
- redfetch/terminal_ui.tcss +569 -0
- redfetch/unloadmq.py +148 -0
- redfetch/update_status.py +62 -0
- redfetch/utils.py +256 -0
- redfetch-1.3.0.dist-info/METADATA +257 -0
- redfetch-1.3.0.dist-info/RECORD +37 -0
- redfetch-1.3.0.dist-info/WHEEL +4 -0
- redfetch-1.3.0.dist-info/entry_points.txt +2 -0
- redfetch-1.3.0.dist-info/licenses/LICENSE +674 -0
redfetch/__about__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 3, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
redfetch/__init__.py
ADDED
|
File without changes
|
redfetch/api.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Resource API client: fetch_*() takes a client, get_*() is self-contained."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from redfetch import auth
|
|
7
|
+
from redfetch import net
|
|
8
|
+
from redfetch.sync_types import RemoteStatus, SyncModel
|
|
9
|
+
|
|
10
|
+
BASE_URL = net.BASE_URL
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ResourceRecord(SyncModel):
|
|
14
|
+
"""Result of a single-resource API check: status plus optional payload or error."""
|
|
15
|
+
resource_id: str
|
|
16
|
+
status: RemoteStatus
|
|
17
|
+
resource: dict | None = None
|
|
18
|
+
error: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def fetch_watched_page(client: httpx.AsyncClient, page: int) -> tuple[list, int]:
|
|
22
|
+
"""Fetch a single page of watched resources."""
|
|
23
|
+
url = f'{BASE_URL}/api/rgwatched'
|
|
24
|
+
try:
|
|
25
|
+
data = await net.get_json(client, url, params={'page': page})
|
|
26
|
+
last_page = data['pagination']['last_page']
|
|
27
|
+
items = data.get('resources', [])
|
|
28
|
+
return items, last_page
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(f"Error fetching watched resources page {page}: {e}")
|
|
31
|
+
return [], 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def fetch_licenses_page(client: httpx.AsyncClient, page: int) -> tuple[list, int]:
|
|
35
|
+
"""Fetch a single page of user licenses."""
|
|
36
|
+
url = f'{BASE_URL}/api/user-licenses'
|
|
37
|
+
try:
|
|
38
|
+
data = await net.get_json(client, url, params={'page': page})
|
|
39
|
+
last_page = data['pagination']['last_page']
|
|
40
|
+
items = data.get('licenses', [])
|
|
41
|
+
return items, last_page
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print(f"Error fetching licenses page {page}: {e}")
|
|
44
|
+
return [], 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def fetch_resource_record(client: httpx.AsyncClient, resource_id: str) -> ResourceRecord:
|
|
48
|
+
"""Fetch a single resource and classify its availability."""
|
|
49
|
+
url = f'{BASE_URL}/api/resources/{resource_id}'
|
|
50
|
+
try:
|
|
51
|
+
data = await net.get_json(client, url)
|
|
52
|
+
resource = data['resource']
|
|
53
|
+
current_files = resource.get('current_files') or []
|
|
54
|
+
if not resource.get('can_download', False):
|
|
55
|
+
status: RemoteStatus = 'access_denied'
|
|
56
|
+
elif len(current_files) == 0:
|
|
57
|
+
status = 'no_files'
|
|
58
|
+
elif len(current_files) > 1:
|
|
59
|
+
status = 'multiple_files'
|
|
60
|
+
else:
|
|
61
|
+
status = 'downloadable'
|
|
62
|
+
return ResourceRecord(resource_id=resource_id, status=status, resource=resource)
|
|
63
|
+
except httpx.HTTPStatusError as e:
|
|
64
|
+
status = 'not_found' if e.response.status_code == 404 else 'fetch_error'
|
|
65
|
+
return ResourceRecord(resource_id=resource_id, status=status, error=str(e))
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return ResourceRecord(resource_id=resource_id, status='fetch_error', error=str(e))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def fetch_resource_records_batch(client: httpx.AsyncClient, resource_ids: list[str]) -> list[ResourceRecord]:
|
|
71
|
+
"""Fetch multiple resource records concurrently."""
|
|
72
|
+
if not resource_ids:
|
|
73
|
+
return []
|
|
74
|
+
return list(await asyncio.gather(
|
|
75
|
+
*(fetch_resource_record(client, rid) for rid in resource_ids)
|
|
76
|
+
))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def get_resource_details(resource_id: int, headers: dict) -> dict:
|
|
80
|
+
"""Retrieve details of a specific resource from the API."""
|
|
81
|
+
url = f'{BASE_URL}/api/resources/{resource_id}'
|
|
82
|
+
async with httpx.AsyncClient(headers=headers, http2=True, timeout=30.0) as client:
|
|
83
|
+
response = await client.get(url)
|
|
84
|
+
response.raise_for_status()
|
|
85
|
+
return response.json()['resource']
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def fetch_watched_resources(client: httpx.AsyncClient) -> list:
|
|
89
|
+
"""Fetch all watched resources with concurrent pagination."""
|
|
90
|
+
items, total_pages = await fetch_watched_page(client, 1)
|
|
91
|
+
if total_pages <= 1:
|
|
92
|
+
return items
|
|
93
|
+
|
|
94
|
+
coros = [fetch_watched_page(client, p) for p in range(2, total_pages + 1)]
|
|
95
|
+
page_results = await asyncio.gather(*coros)
|
|
96
|
+
|
|
97
|
+
for page_items, _ in page_results:
|
|
98
|
+
items.extend(page_items)
|
|
99
|
+
|
|
100
|
+
return items
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def fetch_licenses(client: httpx.AsyncClient) -> list:
|
|
104
|
+
"""Fetch all user licenses with concurrent pagination."""
|
|
105
|
+
items, total_pages = await fetch_licenses_page(client, 1)
|
|
106
|
+
if total_pages <= 1:
|
|
107
|
+
return items
|
|
108
|
+
coros = [fetch_licenses_page(client, p) for p in range(2, total_pages + 1)]
|
|
109
|
+
page_results = await asyncio.gather(*coros)
|
|
110
|
+
|
|
111
|
+
for page_items, _ in page_results:
|
|
112
|
+
items.extend(page_items)
|
|
113
|
+
|
|
114
|
+
return items
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
_KISS_CACHE_TTL_SECONDS = 60
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def is_kiss_downloadable(headers, force_refresh: bool = False):
|
|
121
|
+
"""Check for level 2 access by checking KISS."""
|
|
122
|
+
cache = auth.get_disk_cache()
|
|
123
|
+
cache_key = "kiss"
|
|
124
|
+
|
|
125
|
+
if not force_refresh:
|
|
126
|
+
cached = cache.get(cache_key)
|
|
127
|
+
if cached is not None:
|
|
128
|
+
return bool(cached)
|
|
129
|
+
|
|
130
|
+
async with httpx.AsyncClient(headers=headers, http2=True) as client:
|
|
131
|
+
record = await fetch_resource_record(client, "4")
|
|
132
|
+
result = record.status == "downloadable"
|
|
133
|
+
cache.set(cache_key, bool(result), expire=_KISS_CACHE_TTL_SECONDS)
|
|
134
|
+
return result
|