ichime-ts-api-client 1.0.0
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.
- package/README.md +105 -0
- package/biome.json +52 -0
- package/package.json +49 -0
- package/src/api/api-client.ts +173 -0
- package/src/api/date-decoder.ts +57 -0
- package/src/api/errors.ts +43 -0
- package/src/api/index.ts +4 -0
- package/src/api/types/episode.ts +16 -0
- package/src/api/types/index.ts +15 -0
- package/src/api/types/series-type.ts +10 -0
- package/src/api/types/series.ts +44 -0
- package/src/api/types/translation.ts +30 -0
- package/src/http-session.ts +99 -0
- package/src/index.ts +53 -0
- package/src/web/errors.ts +43 -0
- package/src/web/helpers.ts +70 -0
- package/src/web/index.ts +4 -0
- package/src/web/types/anime-list-status.ts +70 -0
- package/src/web/types/anime-list.ts +23 -0
- package/src/web/types/index.ts +21 -0
- package/src/web/types/moment.ts +25 -0
- package/src/web/types/personal-episode.ts +19 -0
- package/src/web/types/profile.ts +5 -0
- package/src/web/web-client.ts +810 -0
- package/tests/auth.test.ts +92 -0
- package/tsconfig.json +24 -0
- package/tsdown.config.ts +11 -0
- package/vitest.config.ts +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# ichime-ts-api-client
|
|
2
|
+
|
|
3
|
+
TypeScript API client для работы с сайтом [smotret-anime.com](https://smotret-anime.com).
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add ichime-ts-api-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Использование
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { ApiClient } from "ichime-ts-api-client";
|
|
15
|
+
|
|
16
|
+
const client = new ApiClient();
|
|
17
|
+
|
|
18
|
+
// Получить список серий
|
|
19
|
+
const series = await client.listSeries({
|
|
20
|
+
query: "naruto",
|
|
21
|
+
limit: 10,
|
|
22
|
+
offset: 0,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
console.log(series);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## API
|
|
29
|
+
|
|
30
|
+
### ApiClient
|
|
31
|
+
|
|
32
|
+
Основной класс для работы с API.
|
|
33
|
+
|
|
34
|
+
#### Конструктор
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
new ApiClient(options?: ApiClientOptions)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Параметры:**
|
|
41
|
+
- `options.baseURL` - базовый URL API (по умолчанию: `https://smotret-anime.com`)
|
|
42
|
+
- `options.timeout` - таймаут запросов в миллисекундах (по умолчанию: `10000`)
|
|
43
|
+
|
|
44
|
+
#### Методы
|
|
45
|
+
|
|
46
|
+
##### `listSeries(options?: ListSeriesOptions): Promise<Series[]>`
|
|
47
|
+
|
|
48
|
+
Получить список серий.
|
|
49
|
+
|
|
50
|
+
**Параметры:**
|
|
51
|
+
- `options.query` - поисковый запрос
|
|
52
|
+
- `options.limit` - количество результатов
|
|
53
|
+
- `options.offset` - смещение для пагинации
|
|
54
|
+
- `options.chips` - фильтры в виде объекта ключ-значение
|
|
55
|
+
- `options.myAnimeListId` - ID из MyAnimeList
|
|
56
|
+
|
|
57
|
+
**Пример:**
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const series = await client.listSeries({
|
|
61
|
+
query: "attack on titan",
|
|
62
|
+
limit: 20,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Разработка
|
|
67
|
+
|
|
68
|
+
### Требования
|
|
69
|
+
|
|
70
|
+
- Node.js >= 24.0.0
|
|
71
|
+
- pnpm
|
|
72
|
+
|
|
73
|
+
### Установка зависимостей
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pnpm install
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Сборка
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pnpm build
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Тестирование
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pnpm test
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Линтинг
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pnpm lint
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Форматирование
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pnpm format
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Лицензия
|
|
104
|
+
|
|
105
|
+
MIT
|
package/biome.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false,
|
|
10
|
+
"includes": ["**", "!**/node_modules", "!**/dist", "!**/.build", "!**/*.swift"]
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "space",
|
|
15
|
+
"indentWidth": 2,
|
|
16
|
+
"lineWidth": 100
|
|
17
|
+
},
|
|
18
|
+
"assist": {
|
|
19
|
+
"actions": {
|
|
20
|
+
"source": {
|
|
21
|
+
"organizeImports": "on"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"linter": {
|
|
26
|
+
"enabled": true,
|
|
27
|
+
"rules": {
|
|
28
|
+
"recommended": true,
|
|
29
|
+
"complexity": {
|
|
30
|
+
"noBannedTypes": "error",
|
|
31
|
+
"noUselessTypeConstraint": "error"
|
|
32
|
+
},
|
|
33
|
+
"correctness": {
|
|
34
|
+
"noUnusedVariables": "error"
|
|
35
|
+
},
|
|
36
|
+
"style": {
|
|
37
|
+
"useConst": "error",
|
|
38
|
+
"useImportType": "error"
|
|
39
|
+
},
|
|
40
|
+
"suspicious": {
|
|
41
|
+
"noExplicitAny": "warn"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"javascript": {
|
|
46
|
+
"formatter": {
|
|
47
|
+
"quoteStyle": "double",
|
|
48
|
+
"semicolons": "always",
|
|
49
|
+
"trailingCommas": "es5"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ichime-ts-api-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TypeScript API client for smotret-anime.com",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=24.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsdown",
|
|
19
|
+
"test": "vitest",
|
|
20
|
+
"test:coverage": "vitest --coverage",
|
|
21
|
+
"lint": "biome check .",
|
|
22
|
+
"lint:fix": "biome check --write .",
|
|
23
|
+
"format": "biome format --write .",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"prepublish": "pnpm build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"anime",
|
|
29
|
+
"api",
|
|
30
|
+
"client",
|
|
31
|
+
"smotret-anime"
|
|
32
|
+
],
|
|
33
|
+
"author": "",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@biomejs/biome": "^2.3.13",
|
|
37
|
+
"@types/node": "^25.1.0",
|
|
38
|
+
"@types/tough-cookie": "^4.0.5",
|
|
39
|
+
"tsdown": "^0.20.1",
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"vitest": "^4.0.18"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"cheerio": "^1.2.0",
|
|
45
|
+
"domhandler": "5.0.3",
|
|
46
|
+
"dotenv": "^17.2.3",
|
|
47
|
+
"tough-cookie": "^6.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { HttpSession } from "../http-session.js";
|
|
2
|
+
import { transformDatesInResponse } from "./date-decoder.js";
|
|
3
|
+
import { ApiClientError, ApiError } from "./errors.js";
|
|
4
|
+
import type {
|
|
5
|
+
Episode,
|
|
6
|
+
EpisodeFull,
|
|
7
|
+
Series,
|
|
8
|
+
SeriesFull,
|
|
9
|
+
TranslationEmbed,
|
|
10
|
+
TranslationFull,
|
|
11
|
+
} from "./types/index.js";
|
|
12
|
+
|
|
13
|
+
interface ApiSuccessfulResponse<T> {
|
|
14
|
+
data: T;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ApiErrorResponse {
|
|
18
|
+
error: {
|
|
19
|
+
code: number;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ListSeriesOptions {
|
|
25
|
+
query?: string;
|
|
26
|
+
limit?: number;
|
|
27
|
+
offset?: number;
|
|
28
|
+
chips?: Record<string, string>;
|
|
29
|
+
myAnimeListId?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ListEpisodesOptions {
|
|
33
|
+
seriesId?: number;
|
|
34
|
+
limit?: number;
|
|
35
|
+
offset?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ApiClient {
|
|
39
|
+
constructor(private readonly session: HttpSession) {}
|
|
40
|
+
|
|
41
|
+
async sendRequest<T>(endpoint: string, queryItems: Record<string, string> = {}): Promise<T> {
|
|
42
|
+
const url = new URL(`/api${endpoint}`, this.session.baseUrl);
|
|
43
|
+
|
|
44
|
+
// Сортируем параметры по имени (как в Swift)
|
|
45
|
+
const sortedParams = Object.entries(queryItems).sort(([a], [b]) => a.localeCompare(b));
|
|
46
|
+
for (const [key, value] of sortedParams) {
|
|
47
|
+
url.searchParams.set(key, value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let response: Response;
|
|
51
|
+
try {
|
|
52
|
+
response = await this.session.request(url.pathname + url.search, {
|
|
53
|
+
method: "GET",
|
|
54
|
+
headers: {
|
|
55
|
+
Accept: "application/json",
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw ApiClientError.requestFailed(error instanceof Error ? error : undefined);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let data: unknown;
|
|
63
|
+
try {
|
|
64
|
+
data = await response.json();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw ApiClientError.canNotDecodeResponseJson(error instanceof Error ? error : undefined);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Проверяем на ошибку API
|
|
70
|
+
if (this.isApiErrorResponse(data)) {
|
|
71
|
+
const apiError = data.error;
|
|
72
|
+
|
|
73
|
+
if (apiError.code === 403) {
|
|
74
|
+
throw ApiClientError.apiError(ApiError.authenticationRequired());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (apiError.code === 404) {
|
|
78
|
+
throw ApiClientError.apiError(ApiError.notFound());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
throw ApiClientError.apiError(ApiError.other(apiError.code, apiError.message));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Успешный ответ
|
|
85
|
+
if (this.isApiSuccessfulResponse<T>(data)) {
|
|
86
|
+
return transformDatesInResponse<T>(data.data);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw ApiClientError.canNotDecodeResponseJson();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private isApiErrorResponse(data: unknown): data is ApiErrorResponse {
|
|
93
|
+
return (
|
|
94
|
+
typeof data === "object" &&
|
|
95
|
+
data !== null &&
|
|
96
|
+
"error" in data &&
|
|
97
|
+
typeof (data as ApiErrorResponse).error === "object" &&
|
|
98
|
+
(data as ApiErrorResponse).error !== null &&
|
|
99
|
+
"code" in (data as ApiErrorResponse).error &&
|
|
100
|
+
"message" in (data as ApiErrorResponse).error
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private isApiSuccessfulResponse<T>(data: unknown): data is ApiSuccessfulResponse<T> {
|
|
105
|
+
return typeof data === "object" && data !== null && "data" in data;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// === API Requests ===
|
|
109
|
+
|
|
110
|
+
async getSeries(seriesId: number): Promise<SeriesFull> {
|
|
111
|
+
return this.sendRequest<SeriesFull>(`/series/${seriesId}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async getEpisode(episodeId: number): Promise<EpisodeFull> {
|
|
115
|
+
return this.sendRequest<EpisodeFull>(`/episodes/${episodeId}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async listSeries(options: ListSeriesOptions = {}): Promise<Series[]> {
|
|
119
|
+
const queryItems: Record<string, string> = {};
|
|
120
|
+
|
|
121
|
+
if (options.chips) {
|
|
122
|
+
const chipsValue = Object.entries(options.chips)
|
|
123
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
124
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
125
|
+
.join(";");
|
|
126
|
+
queryItems.chips = chipsValue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (options.query !== undefined) {
|
|
130
|
+
queryItems.query = options.query;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (options.limit !== undefined) {
|
|
134
|
+
queryItems.limit = String(options.limit);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (options.offset !== undefined) {
|
|
138
|
+
queryItems.offset = String(options.offset);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options.myAnimeListId !== undefined) {
|
|
142
|
+
queryItems.myAnimeListId = String(options.myAnimeListId);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return this.sendRequest<Series[]>("/series", queryItems);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async listEpisodes(options: ListEpisodesOptions = {}): Promise<Episode[]> {
|
|
149
|
+
const queryItems: Record<string, string> = {};
|
|
150
|
+
|
|
151
|
+
if (options.seriesId !== undefined) {
|
|
152
|
+
queryItems.seriesId = String(options.seriesId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (options.limit !== undefined) {
|
|
156
|
+
queryItems.limit = String(options.limit);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (options.offset !== undefined) {
|
|
160
|
+
queryItems.offset = String(options.offset);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return this.sendRequest<Episode[]>("/episodes", queryItems);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async getTranslation(translationId: number): Promise<TranslationFull> {
|
|
167
|
+
return this.sendRequest<TranslationFull>(`/translations/${translationId}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async getTranslationEmbed(translationId: number): Promise<TranslationEmbed> {
|
|
171
|
+
return this.sendRequest<TranslationEmbed>(`/translations/embed/${translationId}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Парсит даты в формате API Anime365: "yyyy-MM-dd HH:mm:ss" в московской таймзоне.
|
|
3
|
+
*/
|
|
4
|
+
export function parseApiDate(dateString: string): Date {
|
|
5
|
+
// API возвращает даты в формате "2024-01-15 14:30:00" в московской таймзоне (UTC+3)
|
|
6
|
+
const [datePart, timePart] = dateString.split(" ");
|
|
7
|
+
if (!datePart || !timePart) {
|
|
8
|
+
throw new Error(`Invalid date format: ${dateString}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Добавляем смещение московской таймзоны (+03:00)
|
|
12
|
+
const isoString = `${datePart}T${timePart}+03:00`;
|
|
13
|
+
const date = new Date(isoString);
|
|
14
|
+
|
|
15
|
+
if (Number.isNaN(date.getTime())) {
|
|
16
|
+
throw new Error(`Invalid date: ${dateString}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Проверяет, является ли дата "пустой" датой-заглушкой API (2000-01-01 00:00:00).
|
|
24
|
+
*/
|
|
25
|
+
export function isEmptyDate(date: Date): boolean {
|
|
26
|
+
// API возвращает "2000-01-01 00:00:00" MSK как пустую дату
|
|
27
|
+
// В UTC это будет 1999-12-31 21:00:00
|
|
28
|
+
return date.getTime() === new Date("1999-12-31T21:00:00.000Z").getTime();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Рекурсивно преобразует строки дат в объекты Date в JSON-ответе.
|
|
33
|
+
* Ищет поля, заканчивающиеся на "DateTime".
|
|
34
|
+
*/
|
|
35
|
+
export function transformDatesInResponse<T>(obj: unknown): T {
|
|
36
|
+
if (obj === null || obj === undefined) {
|
|
37
|
+
return obj as T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(obj)) {
|
|
41
|
+
return obj.map((item) => transformDatesInResponse(item)) as T;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof obj === "object") {
|
|
45
|
+
const result: Record<string, unknown> = {};
|
|
46
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
47
|
+
if (key.endsWith("DateTime") && typeof value === "string") {
|
|
48
|
+
result[key] = parseApiDate(value);
|
|
49
|
+
} else {
|
|
50
|
+
result[key] = transformDatesInResponse(value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result as T;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return obj as T;
|
|
57
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
public readonly code: number,
|
|
4
|
+
message: string
|
|
5
|
+
) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ApiError";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static authenticationRequired(): ApiError {
|
|
11
|
+
return new ApiError(403, "Authentication required");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static notFound(): ApiError {
|
|
15
|
+
return new ApiError(404, "Not found");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static other(code: number, message: string): ApiError {
|
|
19
|
+
return new ApiError(code, message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ApiClientError extends Error {
|
|
24
|
+
constructor(
|
|
25
|
+
message: string,
|
|
26
|
+
public readonly cause?: ApiError | Error
|
|
27
|
+
) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "ApiClientError";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static canNotDecodeResponseJson(error?: Error): ApiClientError {
|
|
33
|
+
return new ApiClientError("Cannot decode response JSON", error);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static apiError(error: ApiError): ApiClientError {
|
|
37
|
+
return new ApiClientError(`API error: ${error.message}`, error);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static requestFailed(error?: Error): ApiClientError {
|
|
41
|
+
return new ApiClientError("Request failed", error);
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Translation } from "./translation.js";
|
|
2
|
+
|
|
3
|
+
export interface Episode {
|
|
4
|
+
id: number;
|
|
5
|
+
episodeFull: string;
|
|
6
|
+
episodeInt: string;
|
|
7
|
+
episodeType: string;
|
|
8
|
+
firstUploadedDateTime: Date;
|
|
9
|
+
isActive: number;
|
|
10
|
+
isFirstUploaded: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface EpisodeFull extends Episode {
|
|
14
|
+
seriesId: number;
|
|
15
|
+
translations: Translation[];
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type { Episode, EpisodeFull } from "./episode.js";
|
|
2
|
+
export type {
|
|
3
|
+
Series,
|
|
4
|
+
SeriesFull,
|
|
5
|
+
SeriesFullDescription,
|
|
6
|
+
SeriesFullGenre,
|
|
7
|
+
Titles,
|
|
8
|
+
} from "./series.js";
|
|
9
|
+
export type { SeriesType } from "./series-type.js";
|
|
10
|
+
export type {
|
|
11
|
+
Translation,
|
|
12
|
+
TranslationEmbed,
|
|
13
|
+
TranslationEmbedStream,
|
|
14
|
+
TranslationFull,
|
|
15
|
+
} from "./translation.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Episode } from "./episode.js";
|
|
2
|
+
import type { SeriesType } from "./series-type.js";
|
|
3
|
+
|
|
4
|
+
export interface Titles {
|
|
5
|
+
ru: string | null;
|
|
6
|
+
romaji: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Series {
|
|
10
|
+
id: number;
|
|
11
|
+
title: string;
|
|
12
|
+
titles: Titles;
|
|
13
|
+
posterUrl: string | null;
|
|
14
|
+
myAnimeListScore: string;
|
|
15
|
+
season: string;
|
|
16
|
+
type: SeriesType | null;
|
|
17
|
+
year: number | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SeriesFullGenre {
|
|
21
|
+
id: number;
|
|
22
|
+
title: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SeriesFullDescription {
|
|
26
|
+
source: string;
|
|
27
|
+
value: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SeriesFull {
|
|
31
|
+
id: number;
|
|
32
|
+
title: string;
|
|
33
|
+
posterUrl: string | null;
|
|
34
|
+
myAnimeListScore: string;
|
|
35
|
+
myAnimeListId: number;
|
|
36
|
+
isAiring: number;
|
|
37
|
+
numberOfEpisodes: number;
|
|
38
|
+
season: string;
|
|
39
|
+
type: SeriesType | null;
|
|
40
|
+
titles: Titles;
|
|
41
|
+
genres: SeriesFullGenre[] | null;
|
|
42
|
+
descriptions: SeriesFullDescription[] | null;
|
|
43
|
+
episodes: Episode[] | null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Episode } from "./episode.js";
|
|
2
|
+
import type { Series } from "./series.js";
|
|
3
|
+
|
|
4
|
+
export interface Translation {
|
|
5
|
+
id: number;
|
|
6
|
+
activeDateTime: Date;
|
|
7
|
+
addedDateTime: Date;
|
|
8
|
+
isActive: number;
|
|
9
|
+
qualityType: string;
|
|
10
|
+
typeKind: string;
|
|
11
|
+
typeLang: string;
|
|
12
|
+
authorsSummary: string;
|
|
13
|
+
height: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TranslationFull {
|
|
17
|
+
episode: Episode;
|
|
18
|
+
series: Series;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TranslationEmbedStream {
|
|
22
|
+
height: number;
|
|
23
|
+
urls: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TranslationEmbed {
|
|
27
|
+
stream: TranslationEmbedStream[];
|
|
28
|
+
subtitlesUrl: string | null;
|
|
29
|
+
subtitlesVttUrl: string | null;
|
|
30
|
+
}
|