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.
Files changed (29) hide show
  1. anicli_api-0.4.11/PKG-INFO +180 -0
  2. anicli_api-0.4.11/README.MD +153 -0
  3. anicli_api-0.4.11/anicli_api/_http.py +109 -0
  4. anicli_api-0.4.11/anicli_api/_logger.py +20 -0
  5. anicli_api-0.4.11/anicli_api/base.py +135 -0
  6. anicli_api-0.4.11/anicli_api/player/__init__.py +23 -0
  7. anicli_api-0.4.11/anicli_api/player/__template__.py +33 -0
  8. anicli_api-0.4.11/anicli_api/player/aniboom.py +60 -0
  9. anicli_api-0.4.11/anicli_api/player/animejoy.py +27 -0
  10. anicli_api-0.4.11/anicli_api/player/base.py +119 -0
  11. anicli_api-0.4.11/anicli_api/player/csst.py +31 -0
  12. anicli_api-0.4.11/anicli_api/player/dzen.py +40 -0
  13. anicli_api-0.4.11/anicli_api/player/kodik.py +96 -0
  14. anicli_api-0.4.11/anicli_api/player/mailru.py +49 -0
  15. anicli_api-0.4.11/anicli_api/player/okru.py +51 -0
  16. anicli_api-0.4.11/anicli_api/player/sibnet.py +30 -0
  17. anicli_api-0.4.11/anicli_api/player/sovetromantica.py +29 -0
  18. anicli_api-0.4.11/anicli_api/player/vkcom.py +36 -0
  19. anicli_api-0.4.11/anicli_api/source/__init__.py +0 -0
  20. anicli_api-0.4.11/anicli_api/source/__template__.py +105 -0
  21. anicli_api-0.4.11/anicli_api/source/anilibria.py +266 -0
  22. anicli_api-0.4.11/anicli_api/source/animego.py +278 -0
  23. anicli_api-0.4.11/anicli_api/source/animejoy.py +237 -0
  24. anicli_api-0.4.11/anicli_api/source/animevost.py +217 -0
  25. anicli_api-0.4.11/anicli_api/source/sovetromantica.py +188 -0
  26. anicli_api-0.4.11/anicli_api/tools/__init__.py +1 -0
  27. anicli_api-0.4.11/anicli_api/tools/random_useragent.py +109 -0
  28. anicli_api-0.4.11/anicli_api/tools/utils.py +8 -0
  29. 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("")