anicli_api 0.4.11__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.
- anicli_api-0.4.11/PKG-INFO +180 -0
- anicli_api-0.4.11/README.MD +153 -0
- anicli_api-0.4.11/anicli_api/_http.py +109 -0
- anicli_api-0.4.11/anicli_api/_logger.py +20 -0
- anicli_api-0.4.11/anicli_api/base.py +135 -0
- anicli_api-0.4.11/anicli_api/player/__init__.py +23 -0
- anicli_api-0.4.11/anicli_api/player/__template__.py +33 -0
- anicli_api-0.4.11/anicli_api/player/aniboom.py +60 -0
- anicli_api-0.4.11/anicli_api/player/animejoy.py +27 -0
- anicli_api-0.4.11/anicli_api/player/base.py +119 -0
- anicli_api-0.4.11/anicli_api/player/csst.py +31 -0
- anicli_api-0.4.11/anicli_api/player/dzen.py +40 -0
- anicli_api-0.4.11/anicli_api/player/kodik.py +96 -0
- anicli_api-0.4.11/anicli_api/player/mailru.py +49 -0
- anicli_api-0.4.11/anicli_api/player/okru.py +51 -0
- anicli_api-0.4.11/anicli_api/player/sibnet.py +30 -0
- anicli_api-0.4.11/anicli_api/player/sovetromantica.py +29 -0
- anicli_api-0.4.11/anicli_api/player/vkcom.py +36 -0
- anicli_api-0.4.11/anicli_api/source/__init__.py +0 -0
- anicli_api-0.4.11/anicli_api/source/__template__.py +105 -0
- anicli_api-0.4.11/anicli_api/source/anilibria.py +266 -0
- anicli_api-0.4.11/anicli_api/source/animego.py +278 -0
- anicli_api-0.4.11/anicli_api/source/animejoy.py +237 -0
- anicli_api-0.4.11/anicli_api/source/animevost.py +217 -0
- anicli_api-0.4.11/anicli_api/source/sovetromantica.py +188 -0
- anicli_api-0.4.11/anicli_api/tools/__init__.py +1 -0
- anicli_api-0.4.11/anicli_api/tools/random_useragent.py +109 -0
- anicli_api-0.4.11/anicli_api/tools/utils.py +8 -0
- anicli_api-0.4.11/pyproject.toml +99 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: anicli_api
|
|
3
|
+
Version: 0.4.11
|
|
4
|
+
Summary: Anime extractor api implementation
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: anime,api,ru,russia,asyncio,parser,httpx,dev
|
|
7
|
+
Author: Georgiy aka Vypivshiy
|
|
8
|
+
Requires-Python: >=3.8,<4.0
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: chompjs (>=1.2.2,<2.0.0)
|
|
21
|
+
Requires-Dist: httpx[http2] (>=0.24.0,<0.25.0)
|
|
22
|
+
Requires-Dist: scrape-schema (>=0.5.0,<0.6.0)
|
|
23
|
+
Project-URL: Bug Tracker, https://github.com/vypivshiy/anicli-api/issues
|
|
24
|
+
Project-URL: Cli app, https://github.com/vypivshiy/ani-cli-ru
|
|
25
|
+
Description-Content-Type: text/plain
|
|
26
|
+
|
|
27
|
+
# anicli-api
|
|
28
|
+
|
|
29
|
+
Программный интерфейс набора парсеров аниме с различных источников.
|
|
30
|
+
|
|
31
|
+
Присутствует поддержка sync и async методов с помощью `httpx` библиотеки
|
|
32
|
+
Парсеры работают на REST-API (если у источника есть доступ), parsel и обёртки scrape-schema
|
|
33
|
+
|
|
34
|
+
# install
|
|
35
|
+
`pip install anicli-api`
|
|
36
|
+
|
|
37
|
+
# Overview
|
|
38
|
+
Структура проекта
|
|
39
|
+
```
|
|
40
|
+
anicli_api
|
|
41
|
+
├── base.py - базовый класс модуля-парсера
|
|
42
|
+
├── _http.py - сконфигурированные классы httpx
|
|
43
|
+
├── _logger.py - логгер
|
|
44
|
+
├── player - модули получения ссылок на видео
|
|
45
|
+
│ ├── __template__.py - шаблон модуля PlayerExtractor
|
|
46
|
+
│ ├── ... ready-made модули
|
|
47
|
+
│ ...
|
|
48
|
+
├── source - модули парсеров с источников
|
|
49
|
+
│ ├── __template__.py
|
|
50
|
+
│ ├─ ... ready-made парсеры
|
|
51
|
+
│ ...
|
|
52
|
+
└── tools - прочие модули
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Модули имеют следующий алгоритм пошагового получения объектов:
|
|
56
|
+
|
|
57
|
+
```shell
|
|
58
|
+
# Extractor works schema:
|
|
59
|
+
[Extractor] # результат поиска
|
|
60
|
+
| search(<query>)/ongoing() -> List[Search | Ongoing]
|
|
61
|
+
V
|
|
62
|
+
[Search | Ongoing] # информация о тайтле
|
|
63
|
+
| get_anime() -> AnimeInfo
|
|
64
|
+
V
|
|
65
|
+
[Anime] # доступные эпизоды
|
|
66
|
+
| get_episodes() -> List[Episode]
|
|
67
|
+
V
|
|
68
|
+
[Episode] # доступ к доступным видео (озвучки + хост)
|
|
69
|
+
| get_sources() -> List[Video]
|
|
70
|
+
V
|
|
71
|
+
[Source] # прямые ссылки на видео, минимально необходимые заголовки, минимальная метаинформация
|
|
72
|
+
| get_videos() -> Video
|
|
73
|
+
V
|
|
74
|
+
Video(type, quality, url, extra_headers)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
# Quickstart example
|
|
78
|
+
Демонстрация простого prompt-line приложения.
|
|
79
|
+
```python
|
|
80
|
+
from anicli_api.source.animejoy import Extractor # или любой другой источник
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == '__main__':
|
|
84
|
+
ex = Extractor()
|
|
85
|
+
while True:
|
|
86
|
+
print("PRESS CTRL + C for exit")
|
|
87
|
+
results = ex.search(input("search query > "))
|
|
88
|
+
print(*[f"{i}) {r}" for i, r in enumerate(results)], sep="\n")
|
|
89
|
+
anime = results[int(input("anime > "))].get_anime()
|
|
90
|
+
episodes = anime.get_episodes()
|
|
91
|
+
print(*[f"{i}) {ep}" for i, ep in enumerate(episodes)], sep="\n")
|
|
92
|
+
episode = episodes[int(input("episode > "))]
|
|
93
|
+
sources = episode.get_sources()
|
|
94
|
+
print(*[f"{i}) {source}" for i, source in enumerate(sources)])
|
|
95
|
+
source = sources[int(input("source > "))]
|
|
96
|
+
videos = source.get_videos()
|
|
97
|
+
print(*videos, sep="\n")
|
|
98
|
+
video = videos[int(input("video > "))]
|
|
99
|
+
print(video.type, video.quality, video.url, video.headers)
|
|
100
|
+
```
|
|
101
|
+
С asyncio аналогично, но методы получения объектов имеют префикс `a_`
|
|
102
|
+
## Структура объектов
|
|
103
|
+
|
|
104
|
+
> Эта информация не относится к прямым реализациям API интерфейсов таких как anilibria и animevost -
|
|
105
|
+
> они передают все полученные значения
|
|
106
|
+
> также, могут быть проблемы с получением дополнительной мета-информации
|
|
107
|
+
> если необходимо её "обогатить" используйте дополнительные источники. Например, shikimori, animedb
|
|
108
|
+
|
|
109
|
+
### История реализации структуры проекта
|
|
110
|
+
|
|
111
|
+
Это была сложная проблема проекта, которая была решена с помощью реализации дополнительной
|
|
112
|
+
библиотеки [scrape-schema](https://github.com/vypivshiy/scrape-schema) - dataclass-like
|
|
113
|
+
(также схожая с ORM) подходом написания парсеров. Ну... насколько получилось решить проблему,
|
|
114
|
+
ведь идет работа с html документами и сырым текстом, а не структурами json...
|
|
115
|
+
|
|
116
|
+
Изначально планировалось все реализовывать на регулярных выражениях (как в yt-dlp), но
|
|
117
|
+
выбрал css и xpath селекторы по следующим причинам:
|
|
118
|
+
- CSS и XPath селекторы проще сопровождать: просто открыть браузер, зайти в инструменты разработчика,
|
|
119
|
+
скопировать запрос и во вкладке `Elements` вставить в поиск. Или воспользоваться [тестер 1](http://xpather.com/),
|
|
120
|
+
[тестер 2](https://try.jsoup.org/)
|
|
121
|
+
- Просты в написании. Нужен только chromium-based браузер, плагин [selectorGadget](https://chrome.google.com/webstore/detail/selectorgadget/mhjhnkcfbdhnjickkkdbjoemdmbfginb)
|
|
122
|
+
и потом сгенерированный селектор идёт в код
|
|
123
|
+
- Чуть меньше кода писать, удобнее читать. Ну, почти... Ведь работаем с сырым html, а не структурами!
|
|
124
|
+
Также, дополнительно присутствует лог работы (в проекте выключен по умолчанию)
|
|
125
|
+
и автоматическое приведение типов
|
|
126
|
+
- Быстрее и удобнее добавлять дополнительные поля по необходимости
|
|
127
|
+
|
|
128
|
+
### SearchResult
|
|
129
|
+
- url: str - URL на тайтл
|
|
130
|
+
- title: str - имя найденного тайтла
|
|
131
|
+
- thumbnail: str - изображение
|
|
132
|
+
|
|
133
|
+
### Ongoing
|
|
134
|
+
- url: str - URL на тайтл
|
|
135
|
+
- title: str - имя найденного тайтла
|
|
136
|
+
- thumbnail: str - изображение
|
|
137
|
+
|
|
138
|
+
### Anime
|
|
139
|
+
В некоторых источниках поля могут возвращать None
|
|
140
|
+
- title: str - имя тайтла (на русском)
|
|
141
|
+
- alt_titles: List[str] - альтернативные названия (английский, японский...)
|
|
142
|
+
- thumbnail: str - изображение
|
|
143
|
+
- description: Optional[str] - описание тайтла
|
|
144
|
+
- genres: List[str] - жанры
|
|
145
|
+
- episodes_available: Optional[int] - доступных эпизодов
|
|
146
|
+
- episodes_total: Optional[int] - максимальное число эпизодов
|
|
147
|
+
- aired: Optional[str] - дата выхода (или год)
|
|
148
|
+
|
|
149
|
+
### Episode
|
|
150
|
+
- title: str - имя эпизода
|
|
151
|
+
- num: str - номер эпизода
|
|
152
|
+
|
|
153
|
+
### Source
|
|
154
|
+
- url: str - ссылка на источник
|
|
155
|
+
- name: str - даббер или имя источника
|
|
156
|
+
|
|
157
|
+
### Video
|
|
158
|
+
|
|
159
|
+
Объект `Video` (полученный из `Source.get_video` или `Source.a_get_video`) имеет следующую структуру:
|
|
160
|
+
|
|
161
|
+
* type - тип видео
|
|
162
|
+
* quality - разрешение видео
|
|
163
|
+
* url - прямая ссылка на видео
|
|
164
|
+
* headers - заголовки требуемые для воспроизведения видео.
|
|
165
|
+
Если возвращает пустой словарь - заголовки не нужны
|
|
166
|
+
|
|
167
|
+
# Примечания
|
|
168
|
+
|
|
169
|
+
- Проект разработан преимущественно на личное, некоммерческое использование с client-side
|
|
170
|
+
стороны.
|
|
171
|
+
Автор проекта не несет ответственности за поломки, убытки в высоко нагруженных проектах и решение
|
|
172
|
+
предоставляется "Как есть" в соответствии с [MIT](LIENSE) лицензией.
|
|
173
|
+
|
|
174
|
+
- Основная цель этого проекта — связать автоматизацию и эффективность извлечения того,
|
|
175
|
+
что предоставляется пользователю в Интернете.
|
|
176
|
+
Весь контент, доступный в рамках проекта, размещается на внешних неаффилированных источниках.
|
|
177
|
+
|
|
178
|
+
**Этот проект не включает инструменты кеширования и сохранения всех полученных данных,
|
|
179
|
+
только готовые реализации парсеров**
|
|
180
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# anicli-api
|
|
2
|
+
|
|
3
|
+
Программный интерфейс набора парсеров аниме с различных источников.
|
|
4
|
+
|
|
5
|
+
Присутствует поддержка sync и async методов с помощью `httpx` библиотеки
|
|
6
|
+
Парсеры работают на REST-API (если у источника есть доступ), parsel и обёртки scrape-schema
|
|
7
|
+
|
|
8
|
+
# install
|
|
9
|
+
`pip install anicli-api`
|
|
10
|
+
|
|
11
|
+
# Overview
|
|
12
|
+
Структура проекта
|
|
13
|
+
```
|
|
14
|
+
anicli_api
|
|
15
|
+
├── base.py - базовый класс модуля-парсера
|
|
16
|
+
├── _http.py - сконфигурированные классы httpx
|
|
17
|
+
├── _logger.py - логгер
|
|
18
|
+
├── player - модули получения ссылок на видео
|
|
19
|
+
│ ├── __template__.py - шаблон модуля PlayerExtractor
|
|
20
|
+
│ ├── ... ready-made модули
|
|
21
|
+
│ ...
|
|
22
|
+
├── source - модули парсеров с источников
|
|
23
|
+
│ ├── __template__.py
|
|
24
|
+
│ ├─ ... ready-made парсеры
|
|
25
|
+
│ ...
|
|
26
|
+
└── tools - прочие модули
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Модули имеют следующий алгоритм пошагового получения объектов:
|
|
30
|
+
|
|
31
|
+
```shell
|
|
32
|
+
# Extractor works schema:
|
|
33
|
+
[Extractor] # результат поиска
|
|
34
|
+
| search(<query>)/ongoing() -> List[Search | Ongoing]
|
|
35
|
+
V
|
|
36
|
+
[Search | Ongoing] # информация о тайтле
|
|
37
|
+
| get_anime() -> AnimeInfo
|
|
38
|
+
V
|
|
39
|
+
[Anime] # доступные эпизоды
|
|
40
|
+
| get_episodes() -> List[Episode]
|
|
41
|
+
V
|
|
42
|
+
[Episode] # доступ к доступным видео (озвучки + хост)
|
|
43
|
+
| get_sources() -> List[Video]
|
|
44
|
+
V
|
|
45
|
+
[Source] # прямые ссылки на видео, минимально необходимые заголовки, минимальная метаинформация
|
|
46
|
+
| get_videos() -> Video
|
|
47
|
+
V
|
|
48
|
+
Video(type, quality, url, extra_headers)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
# Quickstart example
|
|
52
|
+
Демонстрация простого prompt-line приложения.
|
|
53
|
+
```python
|
|
54
|
+
from anicli_api.source.animejoy import Extractor # или любой другой источник
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
ex = Extractor()
|
|
59
|
+
while True:
|
|
60
|
+
print("PRESS CTRL + C for exit")
|
|
61
|
+
results = ex.search(input("search query > "))
|
|
62
|
+
print(*[f"{i}) {r}" for i, r in enumerate(results)], sep="\n")
|
|
63
|
+
anime = results[int(input("anime > "))].get_anime()
|
|
64
|
+
episodes = anime.get_episodes()
|
|
65
|
+
print(*[f"{i}) {ep}" for i, ep in enumerate(episodes)], sep="\n")
|
|
66
|
+
episode = episodes[int(input("episode > "))]
|
|
67
|
+
sources = episode.get_sources()
|
|
68
|
+
print(*[f"{i}) {source}" for i, source in enumerate(sources)])
|
|
69
|
+
source = sources[int(input("source > "))]
|
|
70
|
+
videos = source.get_videos()
|
|
71
|
+
print(*videos, sep="\n")
|
|
72
|
+
video = videos[int(input("video > "))]
|
|
73
|
+
print(video.type, video.quality, video.url, video.headers)
|
|
74
|
+
```
|
|
75
|
+
С asyncio аналогично, но методы получения объектов имеют префикс `a_`
|
|
76
|
+
## Структура объектов
|
|
77
|
+
|
|
78
|
+
> Эта информация не относится к прямым реализациям API интерфейсов таких как anilibria и animevost -
|
|
79
|
+
> они передают все полученные значения
|
|
80
|
+
> также, могут быть проблемы с получением дополнительной мета-информации
|
|
81
|
+
> если необходимо её "обогатить" используйте дополнительные источники. Например, shikimori, animedb
|
|
82
|
+
|
|
83
|
+
### История реализации структуры проекта
|
|
84
|
+
|
|
85
|
+
Это была сложная проблема проекта, которая была решена с помощью реализации дополнительной
|
|
86
|
+
библиотеки [scrape-schema](https://github.com/vypivshiy/scrape-schema) - dataclass-like
|
|
87
|
+
(также схожая с ORM) подходом написания парсеров. Ну... насколько получилось решить проблему,
|
|
88
|
+
ведь идет работа с html документами и сырым текстом, а не структурами json...
|
|
89
|
+
|
|
90
|
+
Изначально планировалось все реализовывать на регулярных выражениях (как в yt-dlp), но
|
|
91
|
+
выбрал css и xpath селекторы по следующим причинам:
|
|
92
|
+
- CSS и XPath селекторы проще сопровождать: просто открыть браузер, зайти в инструменты разработчика,
|
|
93
|
+
скопировать запрос и во вкладке `Elements` вставить в поиск. Или воспользоваться [тестер 1](http://xpather.com/),
|
|
94
|
+
[тестер 2](https://try.jsoup.org/)
|
|
95
|
+
- Просты в написании. Нужен только chromium-based браузер, плагин [selectorGadget](https://chrome.google.com/webstore/detail/selectorgadget/mhjhnkcfbdhnjickkkdbjoemdmbfginb)
|
|
96
|
+
и потом сгенерированный селектор идёт в код
|
|
97
|
+
- Чуть меньше кода писать, удобнее читать. Ну, почти... Ведь работаем с сырым html, а не структурами!
|
|
98
|
+
Также, дополнительно присутствует лог работы (в проекте выключен по умолчанию)
|
|
99
|
+
и автоматическое приведение типов
|
|
100
|
+
- Быстрее и удобнее добавлять дополнительные поля по необходимости
|
|
101
|
+
|
|
102
|
+
### SearchResult
|
|
103
|
+
- url: str - URL на тайтл
|
|
104
|
+
- title: str - имя найденного тайтла
|
|
105
|
+
- thumbnail: str - изображение
|
|
106
|
+
|
|
107
|
+
### Ongoing
|
|
108
|
+
- url: str - URL на тайтл
|
|
109
|
+
- title: str - имя найденного тайтла
|
|
110
|
+
- thumbnail: str - изображение
|
|
111
|
+
|
|
112
|
+
### Anime
|
|
113
|
+
В некоторых источниках поля могут возвращать None
|
|
114
|
+
- title: str - имя тайтла (на русском)
|
|
115
|
+
- alt_titles: List[str] - альтернативные названия (английский, японский...)
|
|
116
|
+
- thumbnail: str - изображение
|
|
117
|
+
- description: Optional[str] - описание тайтла
|
|
118
|
+
- genres: List[str] - жанры
|
|
119
|
+
- episodes_available: Optional[int] - доступных эпизодов
|
|
120
|
+
- episodes_total: Optional[int] - максимальное число эпизодов
|
|
121
|
+
- aired: Optional[str] - дата выхода (или год)
|
|
122
|
+
|
|
123
|
+
### Episode
|
|
124
|
+
- title: str - имя эпизода
|
|
125
|
+
- num: str - номер эпизода
|
|
126
|
+
|
|
127
|
+
### Source
|
|
128
|
+
- url: str - ссылка на источник
|
|
129
|
+
- name: str - даббер или имя источника
|
|
130
|
+
|
|
131
|
+
### Video
|
|
132
|
+
|
|
133
|
+
Объект `Video` (полученный из `Source.get_video` или `Source.a_get_video`) имеет следующую структуру:
|
|
134
|
+
|
|
135
|
+
* type - тип видео
|
|
136
|
+
* quality - разрешение видео
|
|
137
|
+
* url - прямая ссылка на видео
|
|
138
|
+
* headers - заголовки требуемые для воспроизведения видео.
|
|
139
|
+
Если возвращает пустой словарь - заголовки не нужны
|
|
140
|
+
|
|
141
|
+
# Примечания
|
|
142
|
+
|
|
143
|
+
- Проект разработан преимущественно на личное, некоммерческое использование с client-side
|
|
144
|
+
стороны.
|
|
145
|
+
Автор проекта не несет ответственности за поломки, убытки в высоко нагруженных проектах и решение
|
|
146
|
+
предоставляется "Как есть" в соответствии с [MIT](LIENSE) лицензией.
|
|
147
|
+
|
|
148
|
+
- Основная цель этого проекта — связать автоматизацию и эффективность извлечения того,
|
|
149
|
+
что предоставляется пользователю в Интернете.
|
|
150
|
+
Весь контент, доступный в рамках проекта, размещается на внешних неаффилированных источниках.
|
|
151
|
+
|
|
152
|
+
**Этот проект не включает инструменты кеширования и сохранения всех полученных данных,
|
|
153
|
+
только готовые реализации парсеров**
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains httpx.Client and httpx.AsyncClient classes with the following settings:
|
|
3
|
+
|
|
4
|
+
1. User-agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N)
|
|
5
|
+
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.114
|
|
6
|
+
|
|
7
|
+
2. x-requested-with: XMLHttpRequest
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
from typing import Dict
|
|
11
|
+
|
|
12
|
+
from httpx import AsyncClient, Client, Response
|
|
13
|
+
|
|
14
|
+
from anicli_api._logger import logger
|
|
15
|
+
|
|
16
|
+
HEADERS: Dict[str, str] = {
|
|
17
|
+
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) "
|
|
18
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36",
|
|
19
|
+
# XMLHttpRequest required
|
|
20
|
+
"x-requested-with": "XMLHttpRequest",
|
|
21
|
+
}
|
|
22
|
+
# DDoS protection check by "Server" key header
|
|
23
|
+
DDOS_SERVICES = ("cloudflare", "ddos-guard")
|
|
24
|
+
|
|
25
|
+
__all__ = ("BaseHTTPSync", "BaseHTTPAsync", "HTTPSync", "HTTPAsync")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HttpxSingleton:
|
|
29
|
+
_client_instance = None
|
|
30
|
+
_client_instance_init = False
|
|
31
|
+
|
|
32
|
+
_async_client_instance = None
|
|
33
|
+
_async_client_instance_init = False
|
|
34
|
+
|
|
35
|
+
def __new__(cls, *args, **kwargs):
|
|
36
|
+
if issubclass(cls, HTTPSync):
|
|
37
|
+
if not cls._client_instance:
|
|
38
|
+
cls._client_instance = super().__new__(cls)
|
|
39
|
+
return cls._client_instance
|
|
40
|
+
|
|
41
|
+
elif issubclass(cls, HTTPAsync):
|
|
42
|
+
if not cls._async_client_instance:
|
|
43
|
+
cls._async_client_instance = super().__new__(cls)
|
|
44
|
+
return cls._async_client_instance
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BaseHTTPSync(Client):
|
|
48
|
+
"""httpx.Client class with configured user agent and enabled redirects"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, **kwargs):
|
|
51
|
+
super().__init__(http2=kwargs.pop("http2", True), **kwargs)
|
|
52
|
+
self.headers.update(HEADERS.copy())
|
|
53
|
+
self.headers.update(kwargs.pop("headers", {}))
|
|
54
|
+
self.follow_redirects = kwargs.pop("follow_redirects", True)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BaseHTTPAsync(AsyncClient):
|
|
58
|
+
"""httpx.AsyncClient class with configured user agent and enabled redirects"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, **kwargs):
|
|
61
|
+
http2 = kwargs.pop("http2", True)
|
|
62
|
+
super().__init__(http2=http2, **kwargs)
|
|
63
|
+
self._headers.update(HEADERS.copy())
|
|
64
|
+
self._headers.update(kwargs.pop("headers", {}))
|
|
65
|
+
self.follow_redirects = kwargs.pop("follow_redirects", True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def check_ddos_protect_hook(resp: Response):
|
|
69
|
+
"""
|
|
70
|
+
Simple ddos protect check hook.
|
|
71
|
+
|
|
72
|
+
If response return 403 code or server headers contains *cloudflare* or *ddos-guard* strings and
|
|
73
|
+
**Connection = close,** throw ConnectionError traceback
|
|
74
|
+
"""
|
|
75
|
+
logger.debug("%s check DDOS protect :\nstatus [%s] %s", resp.url, resp.status_code, resp.headers)
|
|
76
|
+
if (
|
|
77
|
+
resp.headers.get("Server") in DDOS_SERVICES
|
|
78
|
+
and resp.headers.get("Connection", None) == "close"
|
|
79
|
+
or resp.status_code == 403
|
|
80
|
+
):
|
|
81
|
+
logger.error("Ooops, %s have ddos protect :(", resp.url)
|
|
82
|
+
raise ConnectionError(f"{resp.url} have '{resp.headers.get('Server', 'unknown')}' and return 403 code.")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class HTTPSync(HttpxSingleton, BaseHTTPSync):
|
|
86
|
+
"""
|
|
87
|
+
Base singleton **sync** HTTP class with recommended config.
|
|
88
|
+
|
|
89
|
+
Used in extractors and can configure at any point in the program"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, **kwargs):
|
|
92
|
+
if not self._client_instance_init: # dirty hack for update arguments
|
|
93
|
+
super().__init__(**kwargs)
|
|
94
|
+
self.event_hooks.update({"response": [check_ddos_protect_hook]})
|
|
95
|
+
self._client_instance_init = True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class HTTPAsync(HttpxSingleton, BaseHTTPAsync):
|
|
99
|
+
"""
|
|
100
|
+
Base singleton **async** HTTP class with recommended config
|
|
101
|
+
|
|
102
|
+
Used in extractors and can configure at any point in the program
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, **kwargs):
|
|
106
|
+
if not self._async_client_instance_init: # dirty hack for update arguments
|
|
107
|
+
super().__init__(**kwargs)
|
|
108
|
+
self.event_hooks.update({"response": [check_ddos_protect_hook]})
|
|
109
|
+
self._async_client_instance_init = True
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import colorlog
|
|
4
|
+
|
|
5
|
+
__all__ = ["logger"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
handler = colorlog.StreamHandler()
|
|
9
|
+
_formatter = colorlog.ColoredFormatter(fmt="%(log_color)s %(asctime)s [%(levelname)-8s] %(name)s: %(message)s'")
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("anicli-api") # type: ignore
|
|
12
|
+
logger.addHandler(handler)
|
|
13
|
+
logger.setLevel(logging.INFO)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_logger_cast = logging.getLogger("type_caster")
|
|
17
|
+
_logger_cast.setLevel(logging.ERROR) # type: ignore
|
|
18
|
+
|
|
19
|
+
_sc_schema_logger = logging.getLogger("scrape_schema")
|
|
20
|
+
_sc_schema_logger.setLevel(logging.ERROR)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
4
|
+
|
|
5
|
+
from scrape_schema import BaseSchema
|
|
6
|
+
|
|
7
|
+
from anicli_api._http import HTTPAsync, HTTPSync
|
|
8
|
+
from anicli_api.player import ALL_DECODERS
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from anicli_api.player.base import Video
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MainSchema(BaseSchema):
|
|
15
|
+
HTTP = HTTPSync
|
|
16
|
+
HTTP_ASYNC = HTTPAsync
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_kwargs(cls, **kwargs):
|
|
20
|
+
"""ignore fields parse and set attrs directly"""
|
|
21
|
+
cls_ = cls("")
|
|
22
|
+
for k, v in kwargs.items():
|
|
23
|
+
setattr(cls_, k, v)
|
|
24
|
+
return cls_
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BaseExtractor(ABC):
|
|
28
|
+
HTTP = HTTPSync
|
|
29
|
+
HTTP_ASYNC = HTTPAsync
|
|
30
|
+
BASE_URL: str = NotImplemented
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def search(self, query: str):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
async def a_search(self, query: str):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def ongoing(self):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
async def a_ongoing(self):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class BaseSearch(MainSchema):
|
|
50
|
+
url: str = NotImplemented
|
|
51
|
+
title: str = NotImplemented
|
|
52
|
+
thumbnail: str = NotImplemented
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def get_anime(self):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def a_get_anime(self):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BaseOngoing(BaseSearch):
|
|
64
|
+
url: str = NotImplemented
|
|
65
|
+
title: str = NotImplemented
|
|
66
|
+
thumbnail: str = NotImplemented
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def get_anime(self):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
async def a_get_anime(self):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class BaseAnime(MainSchema):
|
|
78
|
+
title: str = NotImplemented
|
|
79
|
+
alt_titles: List[str] = NotImplemented
|
|
80
|
+
thumbnail: str = NotImplemented
|
|
81
|
+
description: Optional[str] = NotImplemented
|
|
82
|
+
genres: List[str] = NotImplemented
|
|
83
|
+
episodes_available: Optional[int] = NotImplemented
|
|
84
|
+
episodes_total: Optional[int] = NotImplemented
|
|
85
|
+
aired: Optional[str] = NotImplemented
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def get_episodes(self):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
async def a_get_episodes(self):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BaseEpisode(MainSchema):
|
|
97
|
+
title: str = NotImplemented
|
|
98
|
+
num: str = NotImplemented
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def get_sources(self):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
async def a_get_sources(self):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class BaseSource(MainSchema):
|
|
110
|
+
ALL_VIDEO_EXTRACTORS = ALL_DECODERS
|
|
111
|
+
url: str = NotImplemented
|
|
112
|
+
name: str = NotImplemented
|
|
113
|
+
|
|
114
|
+
def _pre_validate_url_attr(self) -> None:
|
|
115
|
+
if self.url is NotImplemented:
|
|
116
|
+
raise AttributeError(f"{self.__class__.__name__} missing url attribute.")
|
|
117
|
+
|
|
118
|
+
def get_videos(self) -> List["Video"]:
|
|
119
|
+
self._pre_validate_url_attr()
|
|
120
|
+
for extractor in self.ALL_VIDEO_EXTRACTORS:
|
|
121
|
+
if self.url == extractor():
|
|
122
|
+
return extractor().parse(self.url)
|
|
123
|
+
warnings.warn(f"Failed extractor videos from {self.url}", stacklevel=4)
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
async def a_get_videos(self) -> List["Video"]:
|
|
127
|
+
self._pre_validate_url_attr()
|
|
128
|
+
for extractor in self.ALL_VIDEO_EXTRACTORS:
|
|
129
|
+
if self.url == extractor():
|
|
130
|
+
return await extractor().a_parse(self.url)
|
|
131
|
+
warnings.warn(
|
|
132
|
+
f"Failed extractor videos from {self.url}. " f"Maybe needed video extractor not implemented?",
|
|
133
|
+
stacklevel=4,
|
|
134
|
+
)
|
|
135
|
+
return []
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from anicli_api.player.aniboom import Aniboom
|
|
2
|
+
from anicli_api.player.animejoy import AnimeJoy
|
|
3
|
+
from anicli_api.player.csst import CsstOnline
|
|
4
|
+
from anicli_api.player.dzen import Dzen
|
|
5
|
+
from anicli_api.player.kodik import Kodik
|
|
6
|
+
from anicli_api.player.mailru import MailRu
|
|
7
|
+
from anicli_api.player.okru import OkRu
|
|
8
|
+
from anicli_api.player.sibnet import SibNet
|
|
9
|
+
from anicli_api.player.sovetromantica import SovietRomanticaPlayer
|
|
10
|
+
from anicli_api.player.vkcom import VkCom
|
|
11
|
+
|
|
12
|
+
ALL_DECODERS = (
|
|
13
|
+
Kodik,
|
|
14
|
+
Aniboom,
|
|
15
|
+
SibNet,
|
|
16
|
+
AnimeJoy,
|
|
17
|
+
CsstOnline,
|
|
18
|
+
MailRu,
|
|
19
|
+
OkRu,
|
|
20
|
+
VkCom,
|
|
21
|
+
Dzen,
|
|
22
|
+
SovietRomanticaPlayer,
|
|
23
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from anicli_api.player.base import BaseVideoExtractor, Video, url_validator
|
|
5
|
+
|
|
6
|
+
__all__ = ["PlayerExtractor"]
|
|
7
|
+
# url validator pattern
|
|
8
|
+
_URL_EQ = re.compile(r"https?://(www\.)?.")
|
|
9
|
+
# url validate decorator
|
|
10
|
+
player_validator = url_validator(_URL_EQ)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PlayerExtractor(BaseVideoExtractor):
|
|
14
|
+
URL_RULE = _URL_EQ
|
|
15
|
+
|
|
16
|
+
@player_validator
|
|
17
|
+
def parse(self, url: str, **kwargs) -> List[Video]:
|
|
18
|
+
response = self.http.get(url)
|
|
19
|
+
return self._extract(response)
|
|
20
|
+
|
|
21
|
+
@player_validator
|
|
22
|
+
async def a_parse(self, url: str, **kwargs) -> List[Video]:
|
|
23
|
+
async with self.a_http as client:
|
|
24
|
+
response = await client.get(url)
|
|
25
|
+
return self._extract(response)
|
|
26
|
+
|
|
27
|
+
def _extract(self, response) -> List[Video]:
|
|
28
|
+
# any extract logic
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
PlayerExtractor().parse("")
|