Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

anime365wrapper

anime365wrapper

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 anime365wrapper

Быстрый старт

import { 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.

Особенности API, которые важно знать

  • Ошибки приходят с 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 — для тестов/прокси
})

Зеркала и fallback

По умолчанию (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>

Прямой доступ к API

request<T>(path: string, params?: object, init?: RequestInit): Promise<T>

Escape hatch для эндпоинтов, которых ещё нет в типизированных методах — работает через тот же транспорт (fallback по зеркалам, повторы, разбор {error}).

Типизированный fields

Передайте 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 })) {
  // ...
}

Расширенный фильтр каталога (chips)

Список допустимых полей и операторов сам 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

Миграция с 2.x

Версия 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 — опционально

About

Wrapper anime365.ru (smotret-anime) for Node.JS

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages