eaf_base_api 3.3.5__tar.gz → 4.0.0__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.
- eaf_base_api-4.0.0/PKG-INFO +174 -0
- eaf_base_api-4.0.0/README.md +154 -0
- eaf_base_api-4.0.0/base_api/__init__.py +78 -0
- eaf_base_api-4.0.0/base_api/base.py +3141 -0
- eaf_base_api-4.0.0/base_api/modules/config.py +141 -0
- eaf_base_api-4.0.0/base_api/modules/errors.py +346 -0
- eaf_base_api-4.0.0/base_api/modules/type_hints.py +130 -0
- eaf_base_api-4.0.0/base_api/py.typed +0 -0
- {eaf_base_api-3.3.5 → eaf_base_api-4.0.0}/pyproject.toml +2 -1
- eaf_base_api-3.3.5/PKG-INFO +0 -50
- eaf_base_api-3.3.5/README.md +0 -31
- eaf_base_api-3.3.5/base_api/__init__.py +0 -8
- eaf_base_api-3.3.5/base_api/base.py +0 -1908
- eaf_base_api-3.3.5/base_api/modules/config.py +0 -69
- eaf_base_api-3.3.5/base_api/modules/errors.py +0 -194
- eaf_base_api-3.3.5/base_api/modules/type_hints.py +0 -41
- {eaf_base_api-3.3.5 → eaf_base_api-4.0.0}/LICENSE +0 -0
- {eaf_base_api-3.3.5 → eaf_base_api-4.0.0}/base_api/modules/__init__.py +0 -0
- {eaf_base_api-3.3.5 → eaf_base_api-4.0.0}/base_api/modules/logger.py +0 -0
- {eaf_base_api-3.3.5 → eaf_base_api-4.0.0}/base_api/modules/progress_bars.py +0 -0
- {eaf_base_api-3.3.5 → eaf_base_api-4.0.0}/base_api/modules/static_functions.py +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eaf_base_api
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: A base API for EchterAlsFake's Porn APIs
|
|
5
|
+
Author: Johannes Habel
|
|
6
|
+
Author-email: Johannes Habel <EchterAlsFake@proton.me>
|
|
7
|
+
License-Expression: AGPL-3.0-or-later
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Requires-Dist: cachetools>=7.0.0
|
|
11
|
+
Requires-Dist: curl-cffi
|
|
12
|
+
Requires-Dist: tenacity>=9.1.2
|
|
13
|
+
Requires-Dist: m3u8 ; extra == 'hls'
|
|
14
|
+
Requires-Dist: av ; python_full_version >= '3.12' and extra == 'hls'
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Project-URL: Homepage, https://github.com/EchterAlsFake/eaf_base_api
|
|
17
|
+
Project-URL: Repository, https://github.com/EchterAlsFake/eaf_base_api
|
|
18
|
+
Provides-Extra: hls
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
> [!WARNING]
|
|
22
|
+
> Version 4 deliberately removes the legacy `BaseMedia.load(api=..., html=...)`
|
|
23
|
+
> and TaskGroup-based `Helper.iterator()` contracts. Applications must migrate
|
|
24
|
+
> to source-aware media fields and the new scrape stream described below.
|
|
25
|
+
|
|
26
|
+
# EAF Base API
|
|
27
|
+
|
|
28
|
+
# What is this?
|
|
29
|
+
When using one of my Porn site APIs, you probably came across this package and wondered what it actually does, so here's
|
|
30
|
+
a detailed answer.
|
|
31
|
+
|
|
32
|
+
A lot of Porn sites use very similar methods for m3u8 (HLS) parsing and other things. I also wanted to implement proxy
|
|
33
|
+
support, and there was a lot of code that I would have rewritten in every API again and again. That's why I made this API
|
|
34
|
+
package. The `BaseCore` class does all the necessary stuff like m3u8 parsing, a great caching system, network request
|
|
35
|
+
fetching with retry attempts and proxy support.
|
|
36
|
+
|
|
37
|
+
# Documentation (IMPORTANT!)
|
|
38
|
+
> [!IMPORTANT]
|
|
39
|
+
> Configuring eaf_base_api is necessary if you use any of my Porn APIs, because they all depend on this project.
|
|
40
|
+
> Please read through the documentation to learn how `PROXIES`, `CACHING` and `LOGGING` etc... work!
|
|
41
|
+
|
|
42
|
+
You can find the documentation here ->: https://github.com/EchterAlsFake/API_Docs/blob/master/Porn_APIs/eaf_base_api.md
|
|
43
|
+
|
|
44
|
+
## Source-aware media models
|
|
45
|
+
|
|
46
|
+
Use `media_field()` for every attribute populated by a remote loader. The first
|
|
47
|
+
source is the highest-priority source if multiple sources provide the same field.
|
|
48
|
+
Each configured loader is async and returns a complete mapping for all fields
|
|
49
|
+
assigned to that source; loaders do not mutate the model directly.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from dataclasses import dataclass
|
|
53
|
+
from typing import ClassVar
|
|
54
|
+
|
|
55
|
+
from base_api import BaseMedia, media_field
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(kw_only=True, slots=True)
|
|
59
|
+
class Video(BaseMedia):
|
|
60
|
+
title: str | None = media_field("html", "api")
|
|
61
|
+
available_qualities: list[int] | None = media_field("html")
|
|
62
|
+
|
|
63
|
+
loader_methods: ClassVar[dict[str, str]] = {
|
|
64
|
+
"html": "_load_html",
|
|
65
|
+
"api": "_load_api",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async def _load_html(self) -> dict[str, object]:
|
|
69
|
+
data = await fetch_and_parse_html(self.url)
|
|
70
|
+
return {
|
|
71
|
+
"title": data.get("title"),
|
|
72
|
+
"available_qualities": data.get("available_qualities"),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async def _load_api(self) -> dict[str, object]:
|
|
76
|
+
data = await fetch_and_parse_api(self.url)
|
|
77
|
+
return {"title": data.get("title")}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Load exactly the information a caller needs:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
video = Video(url=url, core=core)
|
|
84
|
+
await video.load_fields("title", "available_qualities")
|
|
85
|
+
|
|
86
|
+
# Or request a known source explicitly.
|
|
87
|
+
await video.load_sources("html")
|
|
88
|
+
|
|
89
|
+
# Convenience form that loads one field and returns it.
|
|
90
|
+
title = await video.get_field("title")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
An unresolved field raises `DataNotLoadedError` with the exact field and eligible
|
|
94
|
+
sources. A loader returning `None` marks the field as loaded and does not raise.
|
|
95
|
+
Loader mappings are validated before any values are committed, preventing partial
|
|
96
|
+
model updates after parser failures.
|
|
97
|
+
|
|
98
|
+
## HTTP requests and caching
|
|
99
|
+
|
|
100
|
+
`BaseCore` exposes one method per response representation. Use the core as an
|
|
101
|
+
async context manager so its connection pool is closed deterministically:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from base_api import BaseCore, CachePolicy
|
|
105
|
+
|
|
106
|
+
async with BaseCore() as core:
|
|
107
|
+
response = await core.request("https://example.com/status")
|
|
108
|
+
text = await core.fetch_text("https://example.com/page")
|
|
109
|
+
data = await core.fetch_bytes("https://example.com/file")
|
|
110
|
+
|
|
111
|
+
fresh_text = await core.fetch_text(
|
|
112
|
+
"https://example.com/live",
|
|
113
|
+
cache_policy=CachePolicy.REFRESH,
|
|
114
|
+
)
|
|
115
|
+
uncached_text = await core.fetch_text(
|
|
116
|
+
"https://example.com/volatile",
|
|
117
|
+
cache_policy=CachePolicy.BYPASS,
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Only successful GET text responses are cached. Cache keys distinguish parameters,
|
|
122
|
+
request bodies, headers, and cookies without storing credentials in plaintext.
|
|
123
|
+
`CachePolicy.USE` reads and writes the cache, `REFRESH` skips the read and replaces
|
|
124
|
+
the entry, and `BYPASS` neither reads nor writes. Concurrent misses for the same
|
|
125
|
+
request share one network operation.
|
|
126
|
+
|
|
127
|
+
Network failures and retryable HTTP statuses are retried automatically for
|
|
128
|
+
idempotent methods. Set `retry_non_idempotent=True` only when repeating a POST or
|
|
129
|
+
PATCH is known to be safe.
|
|
130
|
+
|
|
131
|
+
## Concurrent page and media iteration
|
|
132
|
+
|
|
133
|
+
`Helper` uses bounded `asyncio` task sets. Completion order is the default because
|
|
134
|
+
it exposes fast media without waiting for slower earlier media. Original page and
|
|
135
|
+
extractor order is available with `ResultOrder.ORIGINAL`.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from base_api import Helper, ResultOrder
|
|
139
|
+
|
|
140
|
+
helper = Helper(core=core, constructor=Video)
|
|
141
|
+
stream = helper.iterator(
|
|
142
|
+
page_urls,
|
|
143
|
+
extractor_videos,
|
|
144
|
+
max_page_concurrency=3,
|
|
145
|
+
max_item_concurrency=20,
|
|
146
|
+
load_fields=("title", "available_qualities"),
|
|
147
|
+
order=ResultOrder.COMPLETION, # The default.
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# The context manager guarantees immediate task cleanup if this loop breaks early.
|
|
151
|
+
async with stream:
|
|
152
|
+
async for result in stream:
|
|
153
|
+
if not result.succeeded:
|
|
154
|
+
logger.error("%s failed: %s", result.stage, result.error)
|
|
155
|
+
continue
|
|
156
|
+
video = result.unwrap()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Use `order=ResultOrder.ORIGINAL` when presentation order matters. Page and item
|
|
160
|
+
failures independently support `ErrorMode.YIELD`, `ErrorMode.SKIP`, or
|
|
161
|
+
`ErrorMode.RAISE`. `RetryPolicy` provides a strict maximum attempt count and
|
|
162
|
+
optional exponential delay; error handlers return an `ErrorAction` and cannot
|
|
163
|
+
create an unbounded retry loop.
|
|
164
|
+
|
|
165
|
+
# Can I use this for myself?
|
|
166
|
+
Yes, you can, but I may change stuff here and there from time to time, and it would maybe break your project.
|
|
167
|
+
I would not recommend you to install and use it as a package, but just copy the code you need.
|
|
168
|
+
|
|
169
|
+
I can recommend everyone the download functions for HLS streaming since, for example, the threaded preset is very well
|
|
170
|
+
optimized. If you just use mine, you need to consume less caffeine and brain cells to make such a function :)
|
|
171
|
+
|
|
172
|
+
# License
|
|
173
|
+
Licensed under The [AGPLv3](https://opensource.org/license/agpl-3-0) license.
|
|
174
|
+
<br>Copyright (C) 2024-2026 Johannes Habel
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
> [!WARNING]
|
|
2
|
+
> Version 4 deliberately removes the legacy `BaseMedia.load(api=..., html=...)`
|
|
3
|
+
> and TaskGroup-based `Helper.iterator()` contracts. Applications must migrate
|
|
4
|
+
> to source-aware media fields and the new scrape stream described below.
|
|
5
|
+
|
|
6
|
+
# EAF Base API
|
|
7
|
+
|
|
8
|
+
# What is this?
|
|
9
|
+
When using one of my Porn site APIs, you probably came across this package and wondered what it actually does, so here's
|
|
10
|
+
a detailed answer.
|
|
11
|
+
|
|
12
|
+
A lot of Porn sites use very similar methods for m3u8 (HLS) parsing and other things. I also wanted to implement proxy
|
|
13
|
+
support, and there was a lot of code that I would have rewritten in every API again and again. That's why I made this API
|
|
14
|
+
package. The `BaseCore` class does all the necessary stuff like m3u8 parsing, a great caching system, network request
|
|
15
|
+
fetching with retry attempts and proxy support.
|
|
16
|
+
|
|
17
|
+
# Documentation (IMPORTANT!)
|
|
18
|
+
> [!IMPORTANT]
|
|
19
|
+
> Configuring eaf_base_api is necessary if you use any of my Porn APIs, because they all depend on this project.
|
|
20
|
+
> Please read through the documentation to learn how `PROXIES`, `CACHING` and `LOGGING` etc... work!
|
|
21
|
+
|
|
22
|
+
You can find the documentation here ->: https://github.com/EchterAlsFake/API_Docs/blob/master/Porn_APIs/eaf_base_api.md
|
|
23
|
+
|
|
24
|
+
## Source-aware media models
|
|
25
|
+
|
|
26
|
+
Use `media_field()` for every attribute populated by a remote loader. The first
|
|
27
|
+
source is the highest-priority source if multiple sources provide the same field.
|
|
28
|
+
Each configured loader is async and returns a complete mapping for all fields
|
|
29
|
+
assigned to that source; loaders do not mutate the model directly.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import ClassVar
|
|
34
|
+
|
|
35
|
+
from base_api import BaseMedia, media_field
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(kw_only=True, slots=True)
|
|
39
|
+
class Video(BaseMedia):
|
|
40
|
+
title: str | None = media_field("html", "api")
|
|
41
|
+
available_qualities: list[int] | None = media_field("html")
|
|
42
|
+
|
|
43
|
+
loader_methods: ClassVar[dict[str, str]] = {
|
|
44
|
+
"html": "_load_html",
|
|
45
|
+
"api": "_load_api",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async def _load_html(self) -> dict[str, object]:
|
|
49
|
+
data = await fetch_and_parse_html(self.url)
|
|
50
|
+
return {
|
|
51
|
+
"title": data.get("title"),
|
|
52
|
+
"available_qualities": data.get("available_qualities"),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async def _load_api(self) -> dict[str, object]:
|
|
56
|
+
data = await fetch_and_parse_api(self.url)
|
|
57
|
+
return {"title": data.get("title")}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Load exactly the information a caller needs:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
video = Video(url=url, core=core)
|
|
64
|
+
await video.load_fields("title", "available_qualities")
|
|
65
|
+
|
|
66
|
+
# Or request a known source explicitly.
|
|
67
|
+
await video.load_sources("html")
|
|
68
|
+
|
|
69
|
+
# Convenience form that loads one field and returns it.
|
|
70
|
+
title = await video.get_field("title")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
An unresolved field raises `DataNotLoadedError` with the exact field and eligible
|
|
74
|
+
sources. A loader returning `None` marks the field as loaded and does not raise.
|
|
75
|
+
Loader mappings are validated before any values are committed, preventing partial
|
|
76
|
+
model updates after parser failures.
|
|
77
|
+
|
|
78
|
+
## HTTP requests and caching
|
|
79
|
+
|
|
80
|
+
`BaseCore` exposes one method per response representation. Use the core as an
|
|
81
|
+
async context manager so its connection pool is closed deterministically:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from base_api import BaseCore, CachePolicy
|
|
85
|
+
|
|
86
|
+
async with BaseCore() as core:
|
|
87
|
+
response = await core.request("https://example.com/status")
|
|
88
|
+
text = await core.fetch_text("https://example.com/page")
|
|
89
|
+
data = await core.fetch_bytes("https://example.com/file")
|
|
90
|
+
|
|
91
|
+
fresh_text = await core.fetch_text(
|
|
92
|
+
"https://example.com/live",
|
|
93
|
+
cache_policy=CachePolicy.REFRESH,
|
|
94
|
+
)
|
|
95
|
+
uncached_text = await core.fetch_text(
|
|
96
|
+
"https://example.com/volatile",
|
|
97
|
+
cache_policy=CachePolicy.BYPASS,
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Only successful GET text responses are cached. Cache keys distinguish parameters,
|
|
102
|
+
request bodies, headers, and cookies without storing credentials in plaintext.
|
|
103
|
+
`CachePolicy.USE` reads and writes the cache, `REFRESH` skips the read and replaces
|
|
104
|
+
the entry, and `BYPASS` neither reads nor writes. Concurrent misses for the same
|
|
105
|
+
request share one network operation.
|
|
106
|
+
|
|
107
|
+
Network failures and retryable HTTP statuses are retried automatically for
|
|
108
|
+
idempotent methods. Set `retry_non_idempotent=True` only when repeating a POST or
|
|
109
|
+
PATCH is known to be safe.
|
|
110
|
+
|
|
111
|
+
## Concurrent page and media iteration
|
|
112
|
+
|
|
113
|
+
`Helper` uses bounded `asyncio` task sets. Completion order is the default because
|
|
114
|
+
it exposes fast media without waiting for slower earlier media. Original page and
|
|
115
|
+
extractor order is available with `ResultOrder.ORIGINAL`.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from base_api import Helper, ResultOrder
|
|
119
|
+
|
|
120
|
+
helper = Helper(core=core, constructor=Video)
|
|
121
|
+
stream = helper.iterator(
|
|
122
|
+
page_urls,
|
|
123
|
+
extractor_videos,
|
|
124
|
+
max_page_concurrency=3,
|
|
125
|
+
max_item_concurrency=20,
|
|
126
|
+
load_fields=("title", "available_qualities"),
|
|
127
|
+
order=ResultOrder.COMPLETION, # The default.
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# The context manager guarantees immediate task cleanup if this loop breaks early.
|
|
131
|
+
async with stream:
|
|
132
|
+
async for result in stream:
|
|
133
|
+
if not result.succeeded:
|
|
134
|
+
logger.error("%s failed: %s", result.stage, result.error)
|
|
135
|
+
continue
|
|
136
|
+
video = result.unwrap()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Use `order=ResultOrder.ORIGINAL` when presentation order matters. Page and item
|
|
140
|
+
failures independently support `ErrorMode.YIELD`, `ErrorMode.SKIP`, or
|
|
141
|
+
`ErrorMode.RAISE`. `RetryPolicy` provides a strict maximum attempt count and
|
|
142
|
+
optional exponential delay; error handlers return an `ErrorAction` and cannot
|
|
143
|
+
create an unbounded retry loop.
|
|
144
|
+
|
|
145
|
+
# Can I use this for myself?
|
|
146
|
+
Yes, you can, but I may change stuff here and there from time to time, and it would maybe break your project.
|
|
147
|
+
I would not recommend you to install and use it as a package, but just copy the code you need.
|
|
148
|
+
|
|
149
|
+
I can recommend everyone the download functions for HLS streaming since, for example, the threaded preset is very well
|
|
150
|
+
optimized. If you just use mine, you need to consume less caffeine and brain cells to make such a function :)
|
|
151
|
+
|
|
152
|
+
# License
|
|
153
|
+
Licensed under The [AGPLv3](https://opensource.org/license/agpl-3-0) license.
|
|
154
|
+
<br>Copyright (C) 2024-2026 Johannes Habel
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"BaseCore",
|
|
3
|
+
"BaseMedia",
|
|
4
|
+
"Cache",
|
|
5
|
+
"CacheBackend",
|
|
6
|
+
"CachePolicy",
|
|
7
|
+
"Callback",
|
|
8
|
+
"DownloadConfigHLS",
|
|
9
|
+
"DownloadConfigRAW",
|
|
10
|
+
"DataNotLoadedError",
|
|
11
|
+
"ErrorAction",
|
|
12
|
+
"ErrorHandler",
|
|
13
|
+
"ErrorHandlerError",
|
|
14
|
+
"ErrorMode",
|
|
15
|
+
"FieldNotLoadableError",
|
|
16
|
+
"Helper",
|
|
17
|
+
"ItemFetchError",
|
|
18
|
+
"LoadState",
|
|
19
|
+
"LoaderConfigurationError",
|
|
20
|
+
"LoaderContractError",
|
|
21
|
+
"MediaLoadError",
|
|
22
|
+
"MediaLoadErrors",
|
|
23
|
+
"PageFetchError",
|
|
24
|
+
"ResultOrder",
|
|
25
|
+
"RequestCacheKey",
|
|
26
|
+
"RequestRetriesExhausted",
|
|
27
|
+
"RetryPolicy",
|
|
28
|
+
"ScrapeErrorContext",
|
|
29
|
+
"ScrapeOperationError",
|
|
30
|
+
"ScrapeResult",
|
|
31
|
+
"ScrapeStage",
|
|
32
|
+
"ScrapeStream",
|
|
33
|
+
"SegmentCacheKey",
|
|
34
|
+
"config",
|
|
35
|
+
"errors",
|
|
36
|
+
"media_field",
|
|
37
|
+
"UnknownMediaFieldError",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
from base_api.modules import errors
|
|
42
|
+
from base_api.modules.progress_bars import Callback
|
|
43
|
+
from base_api.modules.errors import (
|
|
44
|
+
DataNotLoadedError,
|
|
45
|
+
ErrorHandlerError,
|
|
46
|
+
FieldNotLoadableError,
|
|
47
|
+
ItemFetchError,
|
|
48
|
+
LoaderConfigurationError,
|
|
49
|
+
LoaderContractError,
|
|
50
|
+
MediaLoadError,
|
|
51
|
+
MediaLoadErrors,
|
|
52
|
+
PageFetchError,
|
|
53
|
+
RequestRetriesExhausted,
|
|
54
|
+
ScrapeOperationError,
|
|
55
|
+
UnknownMediaFieldError,
|
|
56
|
+
)
|
|
57
|
+
from base_api.base import (
|
|
58
|
+
BaseCore,
|
|
59
|
+
BaseMedia,
|
|
60
|
+
Cache,
|
|
61
|
+
CacheBackend,
|
|
62
|
+
CachePolicy,
|
|
63
|
+
ErrorAction,
|
|
64
|
+
ErrorHandler,
|
|
65
|
+
ErrorMode,
|
|
66
|
+
Helper,
|
|
67
|
+
LoadState,
|
|
68
|
+
ResultOrder,
|
|
69
|
+
RequestCacheKey,
|
|
70
|
+
RetryPolicy,
|
|
71
|
+
ScrapeErrorContext,
|
|
72
|
+
ScrapeResult,
|
|
73
|
+
ScrapeStage,
|
|
74
|
+
ScrapeStream,
|
|
75
|
+
SegmentCacheKey,
|
|
76
|
+
media_field,
|
|
77
|
+
)
|
|
78
|
+
from base_api.modules.config import config, DownloadConfigHLS, DownloadConfigRAW
|