Daily MaverickSouth Africans need to wake up to the many crippling costs of corruptionESPNChapman notches 400th save as Red Sox rollThe Jerusalem PostAmazon-owned Boeing 767 cargo plane crashes after overshooting runway in MiamiRTP DesportoAlcaraz nos quartos de final do US OpenESPN DeportesArsenal liga tercer triunfo consecutivo a costa del Chelseaוואלהאיראן: נכריז בימים הקרובים על אזור מוגבל מחוץ למצר הורמוזInquirerSuper Lotto 6/49 ticket wins ₱25.3M jackpot Sept. 6Hindustan Times SportWant to surpass Gukesh, become youngest world champion: ErdogmusCollider2024's Horror Cult Favorite Is Officially Rising From the Dead on StreamingSCMP ChinaHeading to America to bridge the gap in US-China relationsCNews«Базис» купил российского «убийцу» DynatraceRai NewsSassonia-Anhalt, stravince la Afd con il 44%, Cdu 17,4%, Spd 9,2%, Linke 8,6%, Verdi 8,9%, Bsw 5,1%
The Daily Newsstand · Free, Always
Sunday, September 6, 2026

Восемь аниме-плееров рунета изнутри: шифр Цезаря, пустой параметр и домен, который увели

Translate

Я взял за основу популярную опенсорсную библиотеку парсеров аниме-плееров, запустил её — и получил от Kodik 500 Internal Server Error. Не «токен протух», не «IP заблокирован», а именно 500 на каждый запрос. Разбор занял вечер, а причина оказалась в одном символе: библиотека отправляла параметр ref пустым, а сервер требует его заполненным и раскодированным.

Дальше выяснилось, что это не единичная поломка, а норма жанра. Один плеер полгода назад переехал на другой формат страницы. Другой отвечает бесконечным редиректом на самого себя. У третьего домен просто увели — сейчас там индонезийский интернет-магазин, а не аниме. Четвёртый прячет ссылки в WebSocket из обфусцированного бандла на 600 КБ, и его я честно не осилил.

Под катом — разбор восьми плееров: как каждый отдаёт видео, где именно ломаются существующие парсеры и что из этого следует, если вы пишете что-то похожее. Библиотека получилась побочным продуктом и лежит в открытом доступе.

Что получилось

anime-dl-core — библиотека, которая превращает ссылку на embed в прямые ссылки на видео (HLS/DASH/MP4) с заголовками, без которых CDN не отдаст файл.

pip install anime-dl-core
import anime_dl_core as ap

result = ap.extract("https://aniboom.one/embed/9G1MJ6NMV8z?episode=1&translation=30")

print(result.qualities)                   # [360, 480, 720, 1080]
stream = result.best(kind="hls")
print(stream.url)                         # https://.../media\\_6.m3u8
print(stream.headers)                     # {'Referer': 'https://aniboom.one/', ...}
print(stream.ffmpeg_command("ep1.mp4"))   # готовая команда ffmpeg

Плееры

Aniboom, CVH, Kodik, Sibnet, Animedia, AniLibria, VK Video, SovetRomantica

Зависимости

одна — requests

Python

3.9+

Тесты

86 офлайн на настоящих сохранённых ответах + 18 живых

Код

github.com/ialakey/anime-dl-core, MIT

Библиотека сознательно занимается только плеерами. Она не ищет аниме, не ведёт каталог, ничего не скачивает сама и не обходит платные ограничения — читает ровно то же, что читает обычный веб-плеер в браузере.

Зачем это вообще

Аниме-сайты рунета устроены одинаково: сам сайт — это витрина (описание, оценки, список серий), а видео живёт в чужом плеере, который встраивается в <iframe>. Один и тот же тайтл на разных сайтах крутится через одни и те же четыре-пять плееров.

Значит, если нужен свой клиент — телевизор, торрент-качалка на NAS, оффлайн для поездки в поезде, экран без рекламы — задача сводится не к «спарсить сайт», а к «понять плеер». Плееров конечное число, и они, в отличие от витрин, живут годами.

Дальше — по одному.

Kodik: три шага, шифр Цезаря и тот самый пустой параметр

Kodik — самый распространённый плеер и самый защищённый из встреченных. Достать из него ссылку — это три запроса.

Шаг первый. Страница embed (https://kodikplayer.com/seria/<id>/<hash>/720p) содержит блок с подписями:

var urlParams = '{"d":"animego.org",
                  "d_sign":"45f22a7face01f12...:2609080656",
                  "pd":"kodikplayer.com",
                  "pd_sign":"31aa713fde164488...:2609080656",
                  "ref":"https%3A%2F%2Fanimego.org%2F",
                  "ref_sign":"f99556604d27d0ff...:2609080656"}';
var vInfo = {};
vInfo.type = 'seria';
vInfo.hash = '932d5da818729ec5ccc9be7968ee3717';
vInfo.id   = '1304528';

Подписи одноразовые и с временной меткой — страницу и следующий запрос нужно делать подряд.

Шаг второй. Адрес ручки спрятан в скрипте плеера:

$.ajax({type:"POST", url:atob("L2Z0b3I="), cache:!1, dataType:"json", data:e, ...})

atob("L2Z0b3I=")/ftor. Обфускация уровня «чтобы не нагуглили», но искать её нужно регуляркой по вызову atob, а не по смещению в файле: в исходной библиотеке адрес вырезался как js[js.find("$.ajax")+30 : js.find("cache:!1")-3], и любая правка минификатора это сломает.

Шаг третий — и вот здесь была поломка. POST на /ftor с подписями. Исходная библиотека отправляла:

params = {..., 'ref': '', 'ref_sign': urlParams['ref_sign'], ...}

Пустой ref — и сервер отвечает 500. Правильно так:

from urllib.parse import unquote
params = {..., 'ref': unquote(url_params['ref']),   # 'https://animego.org/'
               'ref_sign': url_params['ref_sign'], ...}

То есть ref нужно взять из urlParams и раскодировать из процентной кодировки: подпись ref_sign считается от раскодированного значения. Одна строка — и вместо 500 приходит json со ссылками.

Ссылки, правда, зашифрованы:

Tg9rjO91HK5hj2fdHOVsjq5rj20dlFVtkvDejO9pHPUdVLQeUhIgV2GbGENtUg00HODqTERtU2Qb...

Это base64, к которому применён шифр Цезаря по латинице. Сдвиг меняется — на момент разбора он был 18. Подбирается перебором 26 вариантов с проверкой результата:

def decode_kodik_url(value, known_shift=None):
    for shift in ([known_shift] if known_shift is not None else []) + list(range(26)):
        try:
            decoded = b64decode_padded(caesar_shift(value, shift)).decode("utf-8")
        except Exception:
            continue
        if decoded.startswith("//") or decoded.startswith("http"):
            return decoded, shift
    raise DecryptionError(...)

Найденный сдвиг кэшируется в объекте плеера, так что перебор происходит один раз за сессию. На выходе:

//cloud.solodcdn.com/useruploads/4203027f-.../858b661430c3...:2026090706/720.mp4:hls:manifest.m3u8

Отсюда же берётся прямой mp4 — достаточно отрезать :hls:manifest.m3u8. Проверил: отдаётся честный файл на 202 МБ.

Бонусом на странице лежат таймкоды опенинга и эндинга:

playerSettings.skipButton = parseSkipButton("0:30-1:50,22:55-24:05", "anime");

Что важно: токен API Kodik для всего этого не нужен. Токен нужен только чтобы искать по базе Kodik; чтобы достать видео по готовой ссылке на embed — нет.

Aniboom: всё в одном атрибуте

После Kodik — отдых. Страница embed содержит:

<video id="video" data-parameters="{&quot;id&quot;:&quot;QK8d1LbNX6l&quot;,
  &quot;dash&quot;:&quot;{\&quot;src\&quot;:\&quot;https:\/\/.../o9hwpavtcccz7.mpd\&quot;}&quot;,
  &quot;hls&quot;:&quot;{\&quot;src\&quot;:\&quot;https:\/\/.../master.m3u8\&quot;}&quot;,
  &quot;duration&quot;:&quot;2971&quot;, &quot;qualityVideo&quot;:1080, ...}">

Разэкранировать html-сущности, распарсить json, внутри hls и dash — снова json-строки. Дальше — мастер-плейлист, который я разворачиваю в отдельные качества:

#EXT-X-STREAM-INF:BANDWIDTH=593867,RESOLUTION=640x360,CODECS="avc1.640028,mp4a.40.2"
media_0.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1593867,RESOLUTION=1280x720,...
media_4.m3u8

Единственная тонкость — заголовки: без Referer: https://aniboom.one/ и Origin CDN отвечает отказом. Это общее место для всех плееров, и поэтому в моей библиотеке заголовки — не деталь реализации, а часть результата:

stream.headers   # {'Referer': ..., 'Origin': ..., 'User-Agent': ...}
stream.ffmpeg_args("ep.mp4")   # уже с -headers

CVH: открытое API и ключи, о которых не догадаешься

CVH (CdnVideoHub) — два открытых эндпоинта без всякой авторизации:

GET /api/v1/player/sv/playlist?pub=747&aggr=mali&id=51019   → все серии и озвучки
GET /api/v1/player/sv/video/<vkId>                          → ссылки на потоки

Плейлист приятно устроен: каждая запись — это {season, episode, voiceStudio, voiceType, vkId}, то есть сразу видно, у какой серии какие озвучки.

А вот ответ со ссылками содержит сюрприз:

"sources": {
  "hlsUrl": "https://vd576.okcdn.ru/video.m3u8?...",
  "dashUrl": "https://vd576.okcdn.ru/?...",
  "mpegHighUrl": "...", "mpegFullHdUrl": "...", "mpegMediumUrl": "...",
  "mpegLowUrl": "...", "mpegLowestUrl": "...", "mpegTinyUrl": "..."
}

Раздаёт видео CDN Одноклассников, отсюда и нейминг. Соответствие такое:

Ключ

Качество

mpegTinyUrl

144p

mpegLowestUrl

240p

mpegLowUrl

360p

mpegMediumUrl

480p

mpegHighUrl

720p

mpegFullHdUrl

1080p

Исходная библиотека искала ключи, начинающиеся с url (url360, url720 — старый формат ответа), и поэтому теряла все прямые mp4, оставляя только HLS. Классическая тихая поломка: ошибки нет, данные есть, а половины результата нет.

Sibnet: одна строчка и обязательный Referer

player.src([{src: "/v/2305c05076f006222ee9e551c08c7e03/2589828.mp4", type: "video/mp4"}]);

Относительный путь → https://video.sibnet.ru/v/...mp4, дальше 302 на реальный CDN:

Location: //dv97.sibnet.ru/25/89/82/2589828.mp4?st=I3P2Jhxp...&e=1788750000&noip=1

Подписанный адрес живёт недолго и привязан к IP, поэтому по умолчанию я возвращаю стабильную ссылку video.sibnet.ru/v/..., которая сама редиректит куда надо, а разворачивание редиректа сделал опцией.

VK Video: страница переехала, и парсеры об этом не знают

Все известные мне парсеры VK ищут на странице video_ext.php вот это:

var playerParams = {"params":[{"url240":"...","url720":"...","hls":"..."}]};

Такого там больше нет. Сейчас данные лежат в предзагруженном ответе внутреннего API:

window.cur = Object.assign(window.cur || {}, {
  "apiPrefetchCache":[{
    "method":"video.get",
    "response":{"items":[{
      "files":{
        "mp4_144":"https://vkvd419.okcdn.ru/?...",
        "mp4_720":"https://vkvd419.okcdn.ru/?...",
        "hls_ondemand":"https://vkvd419.okcdn.ru/.../hls4\\_....m3u8",
        "dash_ondemand":"https://vkvd419.okcdn.ru/.../dash4\\_....mpd"
      },
      "title":"...", "duration":1440, "image":[...]
    }]}
  }]
});

Нашёл я это тупым способом, который рекомендую: сохранить страницу в файл и посчитать, какие ключи вообще стоят рядом с https://:

keys = re.findall(r'"([a-zA-Z0-9_]+)":"https:\\/\\/[^"]{20,}"', text)
print(Counter(keys).most_common(30))

Достать json из середины страницы удобно через raw_decode — он читает ровно один объект и говорит, где остановился:

start = text.find("[", text.find('"apiPrefetchCache"'))
entries, _ = json.JSONDecoder().raw_decode(text[start:])

Полезные детали:

  • параметр hash из кода вставки нужен только для видео «доступно по ссылке»; для публичных достаточно oid и id;

  • у трансляции в files только hls — по этому и определяется live;

  • страница видео (vkvideo.ru/video-…) ничего полезного не предзагружает, работает именно video_ext.php.

Старый формат я оставил как запасной вариант: он ещё встречается на зеркалах.

SovetRomantica: сайта нет, домен увели

Здесь начинается детектив. Плеер SovetRomantica живёт по адресу sovetromantica.com/embed/episode_<id>_<серия>-<dubbed|subtitles>. Запрашиваю — приходит 394 КБ HTML, в которых нет ни аниме, ни плеера, зато есть интернет-магазин детских товаров на индонезийском.

Первая мысль — подмена DNS у провайдера. Проверяю сертификат:

cert = ssl_socket.getpeercert()
# subject: www.qris.sovetromantica.com
# SAN: *.sovetromantica.com, sovetromantica.com,
#      www.panduan.sovetromantica.com, www.qris.sovetromantica.com

Сертификат настоящий, Let’s Encrypt, выписан на домен — вместе с поддоменами qris (это индонезийская платёжная система) и panduan («руководство»). Это не подмена: домен просто сменил владельца и ушёл под SEO-схему. CDN команды (scu*.sovetromantica.com) не резолвится вообще. На Boosty у команды висит сбор на перезапуск проекта.

Казалось бы — вычеркнуть плеер. Но разметку-то можно восстановить: у Wayback Machine есть CDX API, который умеет отдавать список сохранённых страниц по маске.

requests.get("https://web.archive.org/cdx/search/cdx", params={
    "url": "sovetromantica.com/embed/*", "output": "json",
    "filter": "statuscode:200", "from": "2023", "collapse": "urlkey"})

Восемьдесят пять сохранённых embed-страниц. Беру свежую (суффикс id_ в адресе отдаёт оригинал без обвязки архива) — и вот она, настоящая разметка:

var nextEpisode = 'https://sovetromantica.com/embed/episode\\_1073\\_2-dubbed';
var skips = [
  {'start': 174.7996, 'end': 184.7996, 'skip_to': 262.9293},
  {'start': 1316.7738, 'end': 1326.7738, 'skip_to': 1398.5638},
];
var config = {
  "id":"sovetromantica_player",
  "file":"https://scu2.sovetromantica.com/anime/1073_gekkan-shoujo-nozaki-kun/episodes/dubbed/episode_1/episode\\_1.m3u8",
  "poster":"...", "thumbnails":"....vtt",
  "title":"Gekkan Shoujo Nozaki-kun - Озвучка - Эпизод 1",
  "points": points
};

Обратите внимание на config: это не валидный json — в конце стоит js-переменная points. Поэтому поля достаются по отдельности регулярками, а не одним json.loads.

Структура skips тоже небанальная: startend — это когда показывать кнопку «пропустить», а skip_to — куда она перематывает. То есть пропускаемый фрагмент — это startskip_to, и именно так я его и записываю.

Плеер в итоге в библиотеке есть, разбор проверен на настоящих архивных страницах, а домен вынесен в параметр:

with SovetRomanticaPlayer(base_url="https://новый-домен") as player:
    result = player.extract("episode_1073_1-dubbed")

Когда команда вернётся — заработает без правок кода. А свежие релизы SovetRomantica сейчас выкладывает во ВКонтакте, и их разбирает плеер vk.

Мораль, которая мне дорого досталась: прежде чем чинить парсер, проверьте, что вы вообще на том сайте. Двести строк регулярок могут «не работать» просто потому, что за доменом теперь другой человек.

Animedia: бесконечный редирект и переезд

Ссылки вида online.animedia.tv/embed/14938/1/1 до сих пор раздают агрегаторы. Запрашиваю — requests падает с TooManyRedirects. Смотрю без автоперехода:

GET https://online.animedia.tv/embed/14938/1/1
301 → https://online.animedia.tv/embed/14938/1/1

Редирект на самого себя. Ни куки, ни User-Agent не помогают, с другой сети — то же самое. То есть это сломано у всех, а не у меня.

Живой сайт нашёлся перебором доменов: animedia.online редиректит на amd.online, и это работающая «Animedia Online». Плеер там теперь свой — aser.pro/vod/<id>, и внутри всё в одну строку:

var player = new Playerjs({
  id: "videoplayer20182",
  file: "https://aser.pro/content/stream/raskolotaya_bitvoj_sineva_nebes_5/001_20182/hls/index.m3u8"
});

Playerjs — популярный плеер, и его параметр file бывает трёх видов: одна ссылка, [720]url1,[360]url2 и json-плейлист со списком серий. Поддержал все три, потому что стоит это десять строк, а ломается иначе молча.

А ссылки на серии лежат на странице тайтла, по одной на эпизод:

<a data-vid="1" data-vlnk="https://aser.pro/vod/1067" href="...">1</a>
<a data-vid="2" data-vlnk="https://aser.pro/vod/1068" href="...">2</a>

Alloha: место, где я остановился

Alloha — крупный агрегатор с открытым API. По id Кинопоиска/IMDb/TMDb или по названию он отдаёт всё, что нужно:

{"status":"success","data":{
  "name":"Атака титанов", "year":2013, "id_kp":749374,
  "seasons":{"1":{"episodes":{"1":{"iframe":"https://...","translation":{...}}}}},
  "iframe":"https://miyagi-as.stravers.live/?token\\_movie=...&token=..."}}

Открываю iframe — в html есть fileList с id файлов, качествами и озвучками, но ни одной ссылки на видео. Ищу дальше:

  • m3u8 в html — нет;

  • /api/..., /playlist?..., /get?... — все 404;

  • в бандле плеера (600 КБ, обфускация со строковым массивом и hex-константами) находится вот это:

window.__ws_factory(''.concat(aS, ...).concat(encodeURIComponent(aO), '&v=2.1&t=').concat(Date.now()))

Ссылки приходят по WebSocket, адрес которого собирается внутри обфусцированного кода. Это не «сложно распарсить», это «нужно исполнять их js». Headless-браузер решил бы задачу, но добавил бы в библиотеку с одной зависимостью — Chromium.

Поэтому я остановился и написал это в README прямым текстом. Alloha в библиотеке есть, но как источник, а не плеер: она отдаёт метаданные, список сезонов, озвучек и готовую ссылку на iframe — вставить к себе на страницу или открыть в webview.

Отдельно скажу, почему считаю это правильным решением. Соблазн был написать «поддерживается» и отдавать iframe под видом потока. Но у библиотеки один контракт: Stream.url — это то, что можно скормить ffmpeg. Строчка «не смог, вот почему» в README честнее, чем метод, который возвращает не то, что обещает.

Ещё один невключённый — jut.su. Там в <source> теперь лежат заглушки:

<source src="https://gen.jut.su/templates/school/images/pixel.png?1080" res="1080"/>

Настоящие ссылки подгружаются динамически. Та же история: без исполнения js не обойтись.

Что в сухом остатке по защитам

Плеер

Как отдаёт ссылку

Барьер

Aniboom

json в атрибуте тега

Referer/Origin

CVH

открытое API

нет

Kodik

POST с одноразовыми подписями

base64 + Цезарь, подписи, ref

Sibnet

js-строка на странице

Referer, подписанный редирект

Animedia

new Playerjs({file})

нет

AniLibria

открытое API

нет

VK

предзагруженный ответ API в html

формат меняется

Alloha

WebSocket из обфусцированного бандла

не пройден

Общая закономерность: чем крупнее сервис, тем меньше он полагается на обфускацию и тем больше — на подписи, время жизни и привязку к IP. Kodik шифрует ссылку шифром Цезаря (это не защита, а знак «сюда не ходи»), но подписи в urlParams живут минуты — и вот это уже настоящая защита, потому что делает бессмысленным кэширование.

Отсюда три вывода для всех, кто такое пишет:

  1. Заголовки — часть результата. Ссылка без Referer — это не ссылка. Возвращайте их вместе с url, иначе пользователь вашей библиотеки будет ловить 403 и думать, что вы что-то сломали.

  2. Ссылки живут минуты. Кэшировать их бессмысленно, получать нужно перед самым скачиванием.

  3. Ошибка должна называть, чего не нашлось. «Не удалось найти urlParams на странице Kodik» чинится за минуту, AttributeError: 'NoneType' object has no attribute 'group' — за вечер.

Как это устроено внутри

Библиотека собрана вокруг реестра плееров: extract() определяет плеер по домену ссылки и возвращает единый результат.

src/anime_dl_core/
    registry.py   реестр, extract() / extract_async()
    base.py       BasePlayer: клиенты, сопоставление ссылок, жизненный цикл
    models.py     Stream, PlayerResult, SkipSegment, StreamKind
    http.py       HttpClient (requests) и AsyncHttpClient (aiohttp) с общим Response
    utils.py      разбор html/js, m3u8, дешифровка ссылок Kodik
    players/      по файлу на плеер
    sources/      необязательные помощники (AnimeGO, Animedia, Alloha)

Свой плеер добавляется двадцатью строками:

@register
class MyPlayer(BasePlayer):
    name = "myplayer"
    domains = ("myplayer.example",)
    playback_headers = {"Referer": "https://myplayer.example/"}

    def extract(self, url, **kwargs):
        page = self.client.get(url).raise_for_status()
        playlist = search(r'file:"([^"]+\.m3u8)"', page.text, what="ссылку на m3u8")
        return PlayerResult(player=self.name, source_url=url,
                            streams=[Stream(playlist, StreamKind.HLS, None,
                                            dict(self.playback_headers))])

Синхронный и асинхронный режимы делают одни и те же шаги и вызывают одни и те же функции разбора — поэтому не расходятся в поведении. HTML разбирается регулярками: для десятка конкретных страниц это надёжнее, чем зависимость от bs4/lxml, и оставляет библиотеке ровно одну зависимость.

Тесты: фикстуры из настоящих ответов

Восемьдесят шесть офлайн-тестов гоняют разбор на реальных ответах плееров, сохранённых в tests/fixtures/. Не на выдуманных, а на тех самых страницах, с которых я снимал разбор, — включая настоящий зашифрованный ответ Kodik, так что дешифровка проверяется по-настоящему, а не на строке, которую я сам же и зашифровал.

Восемнадцать живых тестов ходят в интернет. В CI они не гоняются намеренно: живой тест падает, когда конкретный тайтл убрали с сайта или сервис недоступен из дата-центра GitHub, — это не регрессия библиотеки, а красный прогон на ровном месте. Запускаются отдельно:

ANIME_DL_CORE_LIVE=1 pytest -m live

Такое разделение я советую всем, кто пишет парсеры: офлайн-тесты отвечают на вопрос «не сломал ли я разбор», живые — на вопрос «не сломался ли сайт». Это разные вопросы, и смешивать их в одном прогоне — верный способ приучить себя игнорировать красный CI.

CI: sha256 в релизе и доверенная публикация

Раз уж библиотека публичная, релизы делает CI по тегу: собирает sdist и wheel, ставит собранное колесо в чистое окружение (пакет, который импортируется только из исходников, — это сломанный пакет), считает SHA-256 каждого файла и кладёт хеши вместе с коммитом в текст релиза.

Публикация на PyPI — через Trusted Publishing: PyPI принимает короткоживущий OIDC-токен, выданный конкретному репозиторию и конкретному workflow. Долгоживущего секрета не существует вовсе, а к файлам прикладываются attestations (PEP 740) — подтверждение, что колесо собрано этим workflow из этого коммита:

{"kind": "GitHub", "repository": "ialakey/anime-dl-core",
 "workflow": "publish-pypi.yml", "environment": null}

Грабли, на которые я наступил: pending publisher на PyPI работает только для проектов, которых ещё нет. Если проект уже создан загрузкой по токену, доверенного издателя нужно добавлять на странице самого проекта (Manage → Publishing), иначе обмен токена падает с invalid-publisher: valid token, but no corresponding publisher — при абсолютно корректных claim’ах, что сбивает с толку.

Что дальше

Чего не хватает и что я, возможно, сделаю (или сделает кто-то — MIT):

  • Alloha через headless-браузер отдельным опциональным пакетом, чтобы ядро осталось с одной зависимостью.

  • Больше плееров. Формат добавления — один файл, десяток регулярок и фикстура из настоящего ответа.

  • Скачивание. Сейчас библиотека отдаёт ссылку и команду ffmpeg; загрузчик с докачкой и склейкой HLS — отдельный слой.

Если у вас есть публичный пример плеера, который я не поддержал (или поддержал, но он у вас не работает) — это самый полезный вид обратной связи: почти всегда это либо новый формат страницы, либо ещё один переехавший домен.

Дисклеймер

Библиотека не обходит платные ограничения и не ломает DRM: она читает ровно те данные, которые отдаёт браузеру обычный веб-плеер. Ответственность за использование — на пользователе; уважайте авторские права и правила сайтов.

Код: github.com/ialakey/anime-dl-core · Пакет: pypi.org/project/anime-dl-core · Лицензия MIT.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.