TypeScript/ESM-клиент для API anime365 (домены
smotret-anime.app / .online / .org, anime365.ru, anime-365.ru): каталог аниме, эпизоды,
переводы (озвучки и субтитры), ссылки на видео/субтитры, авторизация и загрузка видео (tus).
Полностью покрывает официальную OpenAPI-спеку (продублирована в openapi.yaml),
без сторонних зависимостей.
- Node.js 20 или новее (используется встроенный
fetch) - ESM (
"type": "module")
npm install anime365wrapperimport { Anime365 } from 'anime365wrapper';
const api = new Anime365({ userAgent: 'MyApp/1.0' });
const series = await api.getSeries({ query: 'gate', limit: 10 });
const translations = await api.getTranslations({ feed: 'recent' });API просит всегда указывать userAgent — название сайта или программы, отправляемое в
заголовке User-Agent.
- Ошибки приходят с HTTP 200. Тело ответа при ошибке —
{ "error": { "code", "message", "fields"? } }, статус при этом не меняется. Библиотека разбирает это сама и бросаетAnime365Error— проверятьresponse.okбессмысленно, полагайтесь на try/catch. - Для полного сканирования используйте
afterId, а неoffset. При счёте на сотни тысяч записейoffsetработает медленно. Для этого есть готовые итераторы —iterateSeries/iterateEpisodes/iterateTranslations, см. ниже. subtitlesUrlизgetTranslationEmbed()бывает относительным (например/episodeTranslations/123.ass?willcache). Библиотека сама приводит его к абсолютному URL относительно текущего зеркала.- У
access_tokenнет срока действия — он действителен, пока не сменится пароль. Храните его как секрет (переменная окружения, секрет-хранилище), не коммитьте в репозиторий. - Домен API может меняться и блокироваться регионально — у сервиса несколько официальных зеркал. По умолчанию библиотека пробует их все по очереди при сетевых сбоях — см. «Зеркала и fallback» ниже.
new Anime365(options?: {
baseUrl?: string | string[]; // по умолчанию DEFAULT_MIRRORS (все известные зеркала)
userAgent?: string; // по умолчанию 'Anime365Wrapper/3.0'
accessToken?: string; // если токен уже известен
app?: string; // идентификатор API-клиента для login/accessToken, по умолчанию 'universal'
timeoutMs?: number; // таймаут одной попытки запроса, по умолчанию 30000
retries?: number; // повторы на одном зеркале при сетевых сбоях/5xx, по умолчанию 2
retryDelayMs?: number; // базовая задержка перед повтором (экспоненциально растёт), по умолчанию 300
fetch?: typeof fetch; // своя реализация fetch — для тестов/прокси
})По умолчанию (baseUrl не указан) используется список DEFAULT_MIRRORS — все известные
домены anime365 в порядке приоритета. При сетевом сбое (недоступный домен, DNS, таймаут, 5xx)
запрос сначала повторяется на том же зеркале (retries раз), а затем библиотека пробует
следующее зеркало из списка. Ошибки самого API (Anime365Error, например 404 или неверный
пароль) ни повтор, ни fallback не запускают — домен ответил, значит проблема не в нём. Зеркало,
на котором прошёл последний успешный запрос, запоминается и используется первым в следующий раз.
import { Anime365, DEFAULT_MIRRORS } from 'anime365wrapper';
console.log(DEFAULT_MIRRORS); // ['https://smotret-anime.app/api', 'https://smotret-anime.online/api', ...]
const api = new Anime365(); // fallback по всем зеркалам сразу из коробки
console.log(api.activeBaseUrl); // текущее рабочее зеркало
// baseUrl строкой — фиксированный домен без fallback
const pinned = new Anime365({ baseUrl: 'https://smotret-anime.app/api' });
// свой список зеркал
const custom = new Anime365({ baseUrl: ['https://smotret-anime.online/api', 'https://anime365.ru/api'] });import { Anime365 } from 'anime365wrapper';
const api = new Anime365({ userAgent: 'MyApp/1.0' });
const token = await api.login('user@example.com', 'password'); // сохраняется в this
const me = await api.getMe(); // { isLogined: true, id, name, isPremium, premiumUntil }
// либо переиспользовать уже полученный токен
api.setAccessToken(token);Получить access_token можно и вручную на сайте: api.socialLoginUrl (вход по
email/паролю или через соцсети). Если пользователь уже вошёл на сайте (например, в браузере),
токен для вашего app выдаёт fetchAccessToken():
const token = await api.fetchAccessToken(); // требует активной сессии в куках браузераapp — идентификатор зарегистрированного API-клиента (не секрет, можно публиковать в открытом
коде). По умолчанию используется 'universal'; свой можно зарегистрировать на странице
создания API-клиента и передать через
new Anime365({ app: 'мой-клиент' }).
getSeries(query?: {
fields?: readonly (keyof Series)[]; // ограничивает набор полей — влияет и на тип результата
query?: string; // поиск по названию
chips?: string | Chip[]; // расширенный фильтр каталога, см. buildChips() ниже
afterId?: number;
order?: 'id';
myAnimeListId?: number;
isActive?: 0 | 1;
isAiring?: 0 | 1;
type?: string; // tv, movie, ova, ona, special, music
year?: number;
season?: string;
limit?: number;
offset?: number;
}): Promise<Series[]>
getSeriesById(id: number, fields?: readonly (keyof Series)[]): Promise<Series>
getEpisodes(query?: {
fields?: readonly (keyof Episode)[];
seriesId?: number;
episodeInt?: number | string;
episodeType?: 'tv' | 'movie' | 'ova' | 'ona' | 'special';
isActive?: 0 | 1;
isFirstUploaded?: 0 | 1;
afterId?: number;
limit?: number;
offset?: number;
}): Promise<Episode[]>
getEpisodeById(id: number, fields?: readonly (keyof Episode)[]): Promise<Episode>
getVideoById(id: number): Promise<Video>getTranslations(query?: {
fields?: readonly (keyof Translation)[];
seriesId?: number;
episodeId?: number;
feed?: 'recent' | 'id' | 'all' | 'updatedDateTime' | 'addedDateTime';
afterId?: number;
type?: string; // voiceRu, subRu, voiceEn, subEn, raw...
qualityType?: string;
isActive?: 0 | 1;
limit?: number;
offset?: number;
}): Promise<Translation[]>
getTranslationById(id: number, fields?: readonly (keyof Translation)[]): Promise<Translation>
getTranslationEmbed(id: number): Promise<EmbedTranslation> // ссылки на видео/субтитры, требует авторизацииgetMe(): Promise<User> // требует access_token
login(email: string, password: string): Promise<string> // возвращает и сохраняет access_token
fetchAccessToken(): Promise<string> // токен для текущей сессии на сайтеgetUploadEndpoints(): Promise<UploadEndpoints> // доступные tus-каналы и рекомендуемый
createTranslation(payload: TranslationCreateRequest): Promise<Translation>request<T>(path: string, params?: object, init?: RequestInit): Promise<T>Escape hatch для эндпоинтов, которых ещё нет в типизированных методах — работает через тот же
транспорт (fallback по зеркалам, повторы, разбор {error}).
Передайте fields как литеральный массив ключей — TypeScript сузит тип элементов результата
до Pick<T, ...>, без as const:
const list = await api.getSeries({ fields: ['id', 'titles', 'year'], limit: 10 });
// list: Pick<Series, 'id' | 'titles' | 'year'>[]
list[0].titles.ru; // ок
list[0].posterUrl; // ошибка типов — поле не запрашивалосьДля полного сканирования больших списков — генераторы, которые сами проходят страницы через
afterId (как требует документация вместо offset):
for await (const series of api.iterateSeries({ fields: ['id', 'title'] })) {
console.log(series.id, series.title);
}
for await (const translation of api.iterateTranslations({ feed: 'id' })) {
// ...
}
for await (const episode of api.iterateEpisodes({ seriesId: 30414 })) {
// ...
}Список допустимых полей и операторов сам API не публикует — их видно только на сайте (вкладка
фильтров каталога) или в site.ccsData исходного кода страницы /catalog. buildChips() лишь
механически собирает строку в формате, который использует сайт, ничего не проверяя:
import { Anime365, buildChips } from 'anime365wrapper';
const chips = buildChips([
{ field: 'genre', operator: '@=', value: [8, 35] },
'genre_op=and',
]);
// chips === 'genre@=8,35;genre_op=and'
const api = new Anime365();
const results = await api.getSeries({ chips });Протокол — tus 1.0.0. Библиотека включает минимальный tus-клиент (без
сторонних зависимостей) и высокоуровневый флоу addTranslation():
import { Anime365, addTranslation, fileSource } from 'anime365wrapper';
const api = new Anime365({ userAgent: 'MyApp/1.0' });
await api.login('user@example.com', 'password');
const translation = await addTranslation(api, {
seriesId: 8245,
episodeNumber: 3,
episodeType: 'tv',
type: 'voiceRu',
authors: 'Author1 & Author2',
video: await fileSource('./episode-03.mp4'),
subtitles: await fileSource('./episode-03.ass'), // необязательно
computeSha256: true, // посчитать и передать sha256 для проверки целостности
onVideoProgress: (uploaded, total) => console.log(`${((uploaded / total) * 100).toFixed(1)}%`),
});
console.log(translation.url);addTranslation() сам получает точки загрузки (getUploadEndpoints()), выбирает
рекомендованный канал (с фолбэком по всем его адресам), заливает видео и субтитры по tus и
создаёт перевод через createTranslation().
Более низкоуровневые примитивы — tusUpload() (одна загрузка на конкретный tusUrl, с
поддержкой докачки через resumeUuid), fileSource() (источник из файла на диске, читается по
смещению без загрузки в память) и blobSource() (источник из Blob/File, для браузера).
import { Anime365, Anime365Error, Anime365NetworkError } from 'anime365wrapper';
const api = new Anime365();
try {
await api.getSeriesById(999999999);
} catch (error) {
if (error instanceof Anime365Error) {
console.error(`API вернул ошибку ${error.code}: ${error.message}`); // 404: Series not found.
if (error.fields) console.error(error.fields); // ошибки валидации createTranslation()
} else if (error instanceof Anime365NetworkError) {
console.error('Сеть недоступна или ответ не удалось разобрать:', error.message);
} else {
throw error;
}
}Экспортируются все модели ответов API и типы запросов:
import type {
Series, Episode, Translation, Video, EmbedTranslation, User,
UploadEndpoint, UploadEndpoints, TranslationCreateRequest,
SeriesQuery, EpisodesQuery, TranslationsQuery,
DownloadOption, StreamOption, ApiResponse, AccessTokenResponse,
} from 'anime365wrapper';В каталоге examples/ — рабочие скрипты: поиск аниме, лента последних
переводов, полное сканирование переводов и каталога через итераторы, авторизация + получение
embed-данных, расширенный фильтр через buildChips(), загрузка видео и создание перевода.
Запуск после сборки:
npm run build
npx tsx examples/search-series.ts gateВерсия 3.0 — чистый релиз без легаси-алиасов, часть API переименована под официальную документацию:
| 2.x | 3.0 |
|---|---|
Anime365API / SmotretAnimeAPI |
Anime365 |
AnimeApiError |
Anime365Error |
AnimeApiNetworkError |
Anime365NetworkError |
getSeriesList() |
getSeries() |
getCurrentUser() |
getMe() |
UserSession (обёртка над клиентом) |
не нужна — Anime365 сам хранит токен, используйте его напрямую |
Новое в 3.0: getEpisodes(), getVideoById(), fetchAccessToken(), getUploadEndpoints(),
createTranslation()/addTranslation() (загрузка видео по tus), итераторы iterateSeries /
iterateEpisodes / iterateTranslations, типизированный fields, ретраи с бэкоффом,
error.fields в Anime365Error, инъекция fetch.
git clone https://github.com/thedvxch/anime365wrapper.git
cd anime365wrapper
npm install
npm run build
npm test # юнит-тесты (vitest, мок fetch)
npm run typecheck:examples
npm run smoke # живой прогон по реальному API; ANIME365_TOKEN — опционально