mirror of
https://github.com/Radiquum/AniX.git
synced 2025-09-04 13:35:36 +05:00
api proxy service for AniX [ Closes #8 ]
This commit is contained in:
commit
6599861eef
24 changed files with 3701 additions and 15 deletions
|
@ -1,3 +1,4 @@
|
|||
# пример заполнения: https://example.com, http://0.0.0.0:80
|
||||
NEXT_PUBLIC_PLAYER_PARSER_URL= # Домен сервиса player-parsers, требуется для работы встроенного плеера
|
||||
NEXT_PUBLIC_API_URL= # Домен сервиса api-prox, для использования своего сервера API вместо встроенного middleware
|
||||
# ---
|
25
.github/workflows/docker-anix-api-prox.yml
vendored
Normal file
25
.github/workflows/docker-anix-api-prox.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
name: Build and Publish 'anix-api-prox' to Docker Hub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- V3
|
||||
paths:
|
||||
- "api-prox/*.ts"
|
||||
- "!api-prox/hooks/*"
|
||||
- "!api-prox/episode/*"
|
||||
- "api-prox/Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Build Image
|
||||
run: docker build . -t radiquum/anix-api-prox:dev
|
||||
- name: Publish Image
|
||||
run: |
|
||||
docker login -u radiquum -p ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker push radiquum/anix-api-prox:dev
|
|
@ -26,11 +26,12 @@
|
|||
|
||||

|
||||
|
||||
5. (опционально) добавьте переменную для использования своего плеера:
|
||||
5. (опционально) добавьте переменные для использования своего плеера и\или API прокси:
|
||||
|
||||
- NEXT_PUBLIC_PLAYER_PARSER_URL
|
||||
- NEXT_PUBLIC_API_URL
|
||||
|
||||
на те которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md)
|
||||
на те которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md) и/или [anix-api-prox](./api-prox/README.RU.md)
|
||||
|
||||

|
||||
|
||||
|
@ -73,11 +74,12 @@
|
|||
|
||||

|
||||
|
||||
7. (опционально) добавьте переменную для использования своего плеера:
|
||||
7. (опционально) добавьте переменную для использования своего плеера и\или API прокси::
|
||||
|
||||
- NEXT_PUBLIC_PLAYER_PARSER_URL
|
||||
- NEXT_PUBLIC_API_URL
|
||||
|
||||
на те которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md)
|
||||
на те которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md) и/или [anix-api-prox](./api-prox/README.RU.md)
|
||||
|
||||
1. 
|
||||
|
||||
|
@ -120,7 +122,7 @@
|
|||
- -p - порт контейнера который будет доступен извне. ПОРТ:3000
|
||||
|
||||
> [!NOTE]
|
||||
> для переменных которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md), необходимо использовать `-e ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ` до последнего слова anix
|
||||
> для переменных которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md) и/или [anix-api-prox](./api-prox/README.RU.md), необходимо использовать `-e ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ` до последнего слова anix
|
||||
|
||||
[команда docker run](https://docs.docker.com/reference/cli/docker/container/run/)
|
||||
|
||||
|
@ -150,7 +152,7 @@
|
|||
1. Клонируйте репозиторий `git clone https://github.com/Radiquum/AniX`
|
||||
2. Переместитесь в директорию репозитория `cd AniX`
|
||||
3. Выполните команду `npm install`
|
||||
4. (опционально) скопируйте .env.sample как .env и заполните его переменными которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md)
|
||||
4. (опционально) скопируйте .env.sample как .env и заполните его переменными которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md) и/или [anix-api-prox](./api-prox/README.RU.md)
|
||||
5. Выполните команду `npm run build`
|
||||
6. создайте новую директорию (далее будем использовать `<имя_новой_директории>` как её имя)
|
||||
7. переместите в созданную директорию (`<имя_новой_директории>`)
|
||||
|
|
|
@ -26,11 +26,12 @@ Requirements:
|
|||
|
||||

|
||||
|
||||
5. (optional) Add variable to use your own player:
|
||||
5. (optional) Add variable to use your own player and/or api-proxy:
|
||||
|
||||
- NEXT_PUBLIC_PLAYER_PARSER_URL
|
||||
- NEXT_PUBLIC_API_URL
|
||||
|
||||
Use the ones you received if you deployed [anix-player-parsers](./player-parsers/README.md)
|
||||
Use the ones you received if you deployed [anix-player-parsers](./player-parsers/README.md) and/or [anix-api-prox](./api-prox/README.md)
|
||||
|
||||

|
||||
|
||||
|
@ -73,11 +74,12 @@ Requirements:
|
|||
|
||||

|
||||
|
||||
7. (optional) Add variables to use your own player:
|
||||
7. (optional) Add variables to use your own player and/or api-proxy:
|
||||
|
||||
- NEXT_PUBLIC_PLAYER_PARSER_URL
|
||||
- NEXT_PUBLIC_API_URL
|
||||
|
||||
Use the ones you received if you deployed [anix-player-parsers](./player-parsers/README.md)
|
||||
Use the ones you received if you deployed [anix-player-parsers](./player-parsers/README.md) and/or [anix-api-prox](./api-prox/README.md)
|
||||
|
||||
1. 
|
||||
|
||||
|
@ -120,7 +122,7 @@ Additional Requirements:
|
|||
- -p - container port to be exposed externally. PORT:3000
|
||||
|
||||
> [!NOTE]
|
||||
> For variables you received if you deployed [anix-player-parsers](./player-parsers/README.md), you need to use `-e VARIABLE=VALUE` before the last word anix
|
||||
> For variables you received if you deployed [anix-player-parsers](./player-parsers/README.md) and/or [anix-api-prox](./api-prox/README.md), you need to use `-e VARIABLE=VALUE` before the last word anix
|
||||
|
||||
[docker run command](https://docs.docker.com/reference/cli/docker/container/run/)
|
||||
|
||||
|
@ -150,7 +152,7 @@ Instructions:
|
|||
1. Clone the repository `git clone https://github.com/Radiquum/AniX`
|
||||
2. Navigate to the repository directory `cd AniX`
|
||||
3. Run the command `npm install`
|
||||
4. (optional) copy `.env.sample` as `.env` and fill it with variables you received if you deployed [anix-player-parsers](./player-parsers/README.md)
|
||||
4. (optional) copy `.env.sample` as `.env` and fill it with variables you received if you deployed [anix-player-parsers](./player-parsers/README.md) and/or [anix-api-prox](./api-prox/README.md)
|
||||
5. Run the command `npm run build`
|
||||
6. Create a new directory (next we will be refer to its name as `<new_dir>`)
|
||||
7. Move the following files into the new directory (`<new_dir>`):
|
||||
|
|
5
api-prox/.dockerignore
Normal file
5
api-prox/.dockerignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
episode
|
||||
hooks
|
||||
node_modules
|
||||
README.md
|
||||
README.RU.md
|
17
api-prox/Dockerfile
Normal file
17
api-prox/Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
|||
FROM node:23-alpine
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/radiquum/anix
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY *.ts ./
|
||||
COPY tsconfig.json ./
|
||||
|
||||
RUN mkdir -p /app/hooks
|
||||
|
||||
EXPOSE 7001
|
||||
|
||||
CMD ["npm", "run", "serve"]
|
111
api-prox/README.RU.md
Normal file
111
api-prox/README.RU.md
Normal file
|
@ -0,0 +1,111 @@
|
|||
# AniX - Api Proxy
|
||||
|
||||
Данный под-проект позволяет проксировать запросы к API Anixart и изменять их ответы с помощью хуков
|
||||
|
||||
Он может использоваться как для основного проекта AniX, так и как отдельный сервис для андроид приложения с изменённой ссылкой на API.
|
||||
|
||||
Лицензия: [MIT](../LICENSE)
|
||||
|
||||
## Доступные хуки
|
||||
|
||||
- release.ts: добавляет рейтинг с shikimori в строку доп. информации
|
||||
- profile.example.ts: меняет ник официального аккаунта anixart (пример работы с хуком)
|
||||
- profile.sponsor.ts: включает спонсорку (отключает рекламу) после входа в аккаунт в android приложении
|
||||
- toggles.ts: заменяет ответ конфигурации, если присутствует переменная окружения `HOST_URL` заменяет ссылку веб плеера, на встроенную, при совместном использовании с переменной окружения `PLAYER_PARSER_URL`, включает собственный веб-плеер, как в веб клиенте AniX
|
||||
- episode.disabled.ts: позволяет изменять/добавлять озвучки, источники и эпизоды с помощью файла json, в папке `episode`.
|
||||
|
||||
## Использование
|
||||
|
||||
В строке веб-браузера необходимо ввести:
|
||||
|
||||
`<http|https>://<ip|domain><:port>/<ENDPOINT>[?<QUERY_PARAMS>]`
|
||||
|
||||
Ответ:
|
||||
|
||||
- 500: произошла ошибка, подробнее в строке `reason` в теле ответа
|
||||
- 200: запрос прошёл успешно (если произошла ошибка на стороне API Anixart, смотри строку `code`)
|
||||
|
||||
## Развёртывание
|
||||
|
||||
### Docker
|
||||
|
||||
Требования:
|
||||
|
||||
- [docker](https://docs.docker.com/engine/install/)
|
||||
|
||||
### Пре-билд
|
||||
|
||||
1. выполните команду:
|
||||
|
||||
`docker run -d --name anix-api -p 7001:7001 radiquum/anix-api-prox:latest`
|
||||
|
||||
для использования хуков необходимо создать папку `hooks` и добавить флаг `-v ./hooks:/app/hooks` перед флагом `-p`.
|
||||
(так-же и для папки `episode`, если требуется)
|
||||
|
||||
### Ручной билд
|
||||
|
||||
Доп. Требования:
|
||||
|
||||
- [git](https://git-scm.com/)
|
||||
|
||||
1. Клонируйте репозиторий `git clone https://github.com/Radiquum/AniX`
|
||||
2. Переместитесь в директорию репозитория `cd AniX`
|
||||
3. Переместитесь в директорию сервиса `cd api-prox`
|
||||
4. Выполните команду `docker build -t anix-api-prox .`
|
||||
5. После окончания, выполните команду: `docker run -d --restart always --name anix-player -p 7001:7001 anix-api-prox`
|
||||
|
||||
для использования хуков необходимо добавить флаг `-v ./hooks:/app/hooks` перед флагом `-p`.
|
||||
(так-же и для папки `episode`, если требуется)
|
||||
|
||||
### docker/Обозначения
|
||||
|
||||
- -d - запустить контейнер в фоне
|
||||
- --restart always - всегда запускать после перезагрузки сервера
|
||||
- --name - название контейнера
|
||||
- -p - порт контейнера который будет доступен извне. ПОРТ:7000
|
||||
- -v - добавить папку с хоста в контейнер
|
||||
|
||||
### docker/После развёртывания
|
||||
|
||||
Сервис будет доступен по адресу: `http://<ВАШ IP><:ВАШ ПОРТ>/`
|
||||
|
||||
### docker/Примечание
|
||||
|
||||
Для использования своего домена и поддержки протокола HTTPS, вы можете использовать Traefik или другой reverse-proxy, с сертификатом SSL.
|
||||
|
||||
Полезные ссылки:
|
||||
|
||||
- [Конвертер из команды docker run в синтакс для docker compose](https://it-tools.tech/docker-run-to-docker-compose-converter)
|
||||
- [Как настроить Traefik + свой домен + SSL](https://letmegooglethat.com/?q=how+to+setup+traefik+with+custom+domain+and+ssl+certificate+from+lets+encrypt%3F)
|
||||
|
||||
### pm2
|
||||
|
||||
Требования:
|
||||
|
||||
- [git](https://git-scm.com/)
|
||||
- [nodejs 23+ с npm](http://nodejs.org/)
|
||||
- [pm2](https://pm2.keymetrics.io/)
|
||||
|
||||
Инструкция:
|
||||
|
||||
1. Клонируйте репозиторий `git clone https://github.com/Radiquum/AniX`
|
||||
2. Переместитесь в директорию репозитория `cd AniX`
|
||||
3. Переместитесь в директорию сервиса `cd api-prox`
|
||||
4. Выполните команду `npm install`
|
||||
5. После окончания и выполните команду `pm2 start index.ts -n anix-api-prox`
|
||||
|
||||
### pm2/Обозначения
|
||||
|
||||
- -n - название сервиса в pm2
|
||||
|
||||
### pm2/После развёртывания
|
||||
|
||||
Сервис будет доступен по адресу: `http://<ВАШ IP>:7001/`
|
||||
|
||||
### pm2/Примечание
|
||||
|
||||
Для автоматического запуска приложения, рекомендуется настроить pm2 на автозапуск, с помощью команды: `pm2 startup`
|
||||
|
||||
Полезные ссылки:
|
||||
|
||||
- [PM2: подходим к вопросу процесс-менеджмента с умом @ Habr](https://habr.com/ru/articles/480670/)
|
112
api-prox/README.md
Normal file
112
api-prox/README.md
Normal file
|
@ -0,0 +1,112 @@
|
|||
|
||||
# AniX - Api Proxy
|
||||
|
||||
This sub-project allows proxying requests to the Anixart API and modifying their responses using hooks.
|
||||
|
||||
It can be used both for the main AniX project and as a standalone service for the Android app with a modified API link.
|
||||
|
||||
License: [MIT](../LICENSE)
|
||||
|
||||
## Available Hooks
|
||||
|
||||
- release.ts: adds a rating from Shikimori to the additional info line
|
||||
- profile.example.ts: changes the nickname of the official Anixart account (an example of using a hook)
|
||||
- profile.sponsor.ts: enables sponsorship (disables ads) after logging into the account in the Android app
|
||||
- toggles.ts: replaces the configuration response; if the `HOST_URL` environment variable is present, it replaces the web player link with an embedded one; when used together with the `PLAYER_PARSER_URL` variable, enables the custom web player, as in the AniX web client
|
||||
- episode.disabled.ts: allows modifying/adding voiceovers, sources, and episodes using a JSON file in the `episode` folder.
|
||||
|
||||
## Usage
|
||||
|
||||
In the web browser address bar, enter:
|
||||
|
||||
`<http|https>://<ip|domain><:port>/<ENDPOINT>[?<QUERY_PARAMS>]`
|
||||
|
||||
Response:
|
||||
|
||||
- 500: an error occurred, see the `reason` field in the response body for more details
|
||||
- 200: request was successful (if there was an error on the Anixart API side, see the `code` field)
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker
|
||||
|
||||
Requirements:
|
||||
|
||||
- [docker](https://docs.docker.com/engine/install/)
|
||||
|
||||
### Pre-built
|
||||
|
||||
1. Run the command:
|
||||
|
||||
`docker run -d --name anix-api -p 7001:7001 radiquum/anix-api-prox:latest`
|
||||
|
||||
To use hooks, create a `hooks` folder and add the flag `-v ./hooks:/app/hooks` before the `-p` flag.
|
||||
(The same applies to the `episode` folder if needed)
|
||||
|
||||
### Manual Build
|
||||
|
||||
Additional Requirements:
|
||||
|
||||
- [git](https://git-scm.com/)
|
||||
|
||||
1. Clone the repository: `git clone https://github.com/Radiquum/AniX`
|
||||
2. Navigate to the repository directory: `cd AniX`
|
||||
3. Navigate to the service directory: `cd api-prox`
|
||||
4. Run the command: `docker build -t anix-api-prox .`
|
||||
5. After completion, run: `docker run -d --restart always --name anix-player -p 7001:7001 anix-api-prox`
|
||||
|
||||
To use hooks, add the flag `-v ./hooks:/app/hooks` before the `-p` flag.
|
||||
(The same applies to the `episode` folder if needed)
|
||||
|
||||
### docker/Flags
|
||||
|
||||
- -d - run the container in background
|
||||
- --restart always - always start after server reboot
|
||||
- --name - container name
|
||||
- -p - container port that will be accessible from outside. PORT:7000
|
||||
- -v - mount a folder from host into the container
|
||||
|
||||
### docker/After Deployment
|
||||
|
||||
The service will be available at: `http://<YOUR IP><:YOUR PORT>/`
|
||||
|
||||
### docker/Note
|
||||
|
||||
To use your own domain and support HTTPS, you can use Traefik or another reverse proxy with an SSL certificate.
|
||||
|
||||
Useful links:
|
||||
|
||||
- [Converter from docker run command to docker compose syntax](https://it-tools.tech/docker-run-to-docker-compose-converter)
|
||||
- [How to setup Traefik + custom domain + SSL](https://letmegooglethat.com/?q=how+to+setup+traefik+with+custom+domain+and+ssl+certificate+from+lets+encrypt%3F)
|
||||
|
||||
### pm2
|
||||
|
||||
Requirements:
|
||||
|
||||
- [git](https://git-scm.com/)
|
||||
- [nodejs 23+ with npm](http://nodejs.org/)
|
||||
- [pm2](https://pm2.keymetrics.io/)
|
||||
|
||||
Instructions:
|
||||
|
||||
1. Clone the repository: `git clone https://github.com/Radiquum/AniX`
|
||||
2. Navigate to the repository directory: `cd AniX`
|
||||
3. Navigate to the service directory: `cd api-prox`
|
||||
4. Run: `npm install`
|
||||
5. After completion, run: `pm2 start index.ts -n anix-api-prox`
|
||||
|
||||
### pm2/Flags
|
||||
|
||||
- -n - service name in pm2
|
||||
|
||||
### pm2/After Deployment
|
||||
|
||||
The service will be available at: `http://<YOUR IP>:7001/`
|
||||
|
||||
### pm2/Note
|
||||
|
||||
For automatic app startup, it is recommended to set up pm2 autostart using the command: `pm2 startup`
|
||||
|
||||
Useful links:
|
||||
|
||||
- [PM2: a smart approach to process management @ Habr](https://habr.com/ru/articles/480670/)
|
53
api-prox/episode/841.example.json
Normal file
53
api-prox/episode/841.example.json
Normal file
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"$schema": "https://gist.githubusercontent.com/Radiquum/7f33c09be293233b831c841b69f24608/raw/anix-api-prox-custom-episode-schema.json",
|
||||
"comment": [
|
||||
"пример для добавления и изменения озвучек, источников и эпизодов",
|
||||
"для аниме Initial D - First Stage, ID: 841",
|
||||
"кстати раздел 'comment' совсем не нужен, так что это буквально коммент"
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "NyaniDUB (Заменено с AniDub)",
|
||||
"sources": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Sibnet",
|
||||
"episodes": [
|
||||
{
|
||||
"position": 0,
|
||||
"name": "Первая серия"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 999,
|
||||
"name": "By Blender Foundation",
|
||||
"icon": "https://download.blender.org/branding/community/blender_community_badge_white.png",
|
||||
"episodes_count": 2,
|
||||
"sources": [
|
||||
{
|
||||
"id": 998,
|
||||
"name": "Public Test Videos",
|
||||
"episodes_count": 2,
|
||||
"episodes": [
|
||||
{
|
||||
"position": 1,
|
||||
"name": "Big Buck Bunny",
|
||||
"url": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
||||
"is_filler": true
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"name": "Elephant Dream",
|
||||
"url": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
|
||||
"is_filler": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
297
api-prox/hooks/episode.disabled.ts
Normal file
297
api-prox/hooks/episode.disabled.ts
Normal file
|
@ -0,0 +1,297 @@
|
|||
// хук добавляет ссылки на кастомные источники
|
||||
// а так-же позволяет добавлять собственные озвучки
|
||||
// с помощью json (<api-prox>/episode/<id релиза>.json)
|
||||
// пример находится в файле 841.example.json в папке episode
|
||||
//
|
||||
// сами видео файлы эпизодов необходимо хостить отдельно (например с помощью nginx),
|
||||
// хуку требуется переменная среды HOST_URL, которая ведёт на сервис api-prox
|
||||
|
||||
import { logger } from "../shared";
|
||||
import fs from "fs/promises";
|
||||
|
||||
let HOSTNAME: null | string = null;
|
||||
if (process.env.HOST_URL) {
|
||||
HOSTNAME = process.env.HOST_URL;
|
||||
}
|
||||
|
||||
export function match(path: string): boolean {
|
||||
// если не установлен хост, не запускаем хук
|
||||
if (!HOSTNAME) return false;
|
||||
// используем только страницы с путём /episode/*
|
||||
const pathRe = /^\/episode\/\d+/;
|
||||
if (pathRe.test(path)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface VoiceoverInfo {
|
||||
"@id": number;
|
||||
id: number;
|
||||
name: string;
|
||||
icon: null | string;
|
||||
workers: null | string;
|
||||
is_sub: boolean;
|
||||
episodes_count: number;
|
||||
view_count: number;
|
||||
pinned: boolean;
|
||||
}
|
||||
|
||||
export interface SourceInfo {
|
||||
"@id": number;
|
||||
id: number;
|
||||
type: number | VoiceoverInfo;
|
||||
name: string;
|
||||
episodes_count: number;
|
||||
}
|
||||
|
||||
export interface EpisodeInfo {
|
||||
"@id": number;
|
||||
position: number;
|
||||
release: number | any;
|
||||
source: number | SourceInfo;
|
||||
name: null | string;
|
||||
url: string;
|
||||
iframe: boolean;
|
||||
addedDate: number;
|
||||
is_filler: boolean;
|
||||
is_watched: boolean;
|
||||
}
|
||||
|
||||
export async function get(data: any, url: URL) {
|
||||
const base = "./episode";
|
||||
|
||||
let releaseId = null;
|
||||
let voiceOverId = null;
|
||||
let sourceId = null;
|
||||
let info: any = null;
|
||||
|
||||
const path = url.pathname.split("/").filter((item) => {
|
||||
return !["", "episode"].includes(item);
|
||||
});
|
||||
|
||||
logger.consoleHook("debug", `Received request for:`, url.pathname);
|
||||
logger.consoleHook("debug", `Decoded pathname:`, path);
|
||||
|
||||
releaseId = Number(path[0]);
|
||||
voiceOverId = Number(path[1]);
|
||||
sourceId = Number(path[2]);
|
||||
logger.consoleHook("debug", `Release ID:`, releaseId);
|
||||
logger.consoleHook("debug", `Voiceover ID:`, voiceOverId);
|
||||
logger.consoleHook("debug", `Source ID:`, sourceId);
|
||||
|
||||
try {
|
||||
info = JSON.parse(
|
||||
await fs.readFile(`${base}/${releaseId}.json`, {
|
||||
encoding: "utf8",
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (path.length == 1) {
|
||||
if (!info || !data.hasOwnProperty("types")) {
|
||||
return data;
|
||||
}
|
||||
|
||||
for (let i = 0; i < info.types.length; i++) {
|
||||
const type: VoiceoverInfo = info.types[i];
|
||||
const existingType: VoiceoverInfo = data.types.find(
|
||||
(item: VoiceoverInfo) => item.id == type.id
|
||||
);
|
||||
|
||||
if (existingType) {
|
||||
type.name ? (existingType.name = type.name) : null;
|
||||
type.icon ? (existingType.icon = type.icon) : null;
|
||||
type.workers ? (existingType.workers = type.workers) : "";
|
||||
type.is_sub ? (existingType.is_sub = type.is_sub) : null;
|
||||
type.episodes_count ?
|
||||
(existingType.episodes_count = type.episodes_count)
|
||||
: null;
|
||||
} else {
|
||||
data.types = [
|
||||
...data.types,
|
||||
{
|
||||
"@id": data.types.length + 1,
|
||||
id: type.id,
|
||||
name: type.name || "Неизвестная Озвучка",
|
||||
icon: type.icon || "",
|
||||
workers: type.workers || "",
|
||||
is_sub: type.is_sub || false,
|
||||
episodes_count: type.episodes_count || 0,
|
||||
view_count: 0,
|
||||
pinned: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path.length == 2) {
|
||||
if (!info || !data.hasOwnProperty("sources")) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const apiResponse = await fetch(`${HOSTNAME}/episode/${releaseId}`);
|
||||
if (!apiResponse.ok) {
|
||||
return data;
|
||||
}
|
||||
const types = await apiResponse.json();
|
||||
const type: VoiceoverInfo = types.types.find(
|
||||
(item: VoiceoverInfo) => item.id == voiceOverId
|
||||
);
|
||||
type["@id"] = 2;
|
||||
type.episodes_count = 0;
|
||||
type.view_count = 0;
|
||||
|
||||
if (data.sources.length > 0) {
|
||||
data.sources[0].type = type;
|
||||
}
|
||||
|
||||
const sources = info.types.find(
|
||||
(item: VoiceoverInfo) => item.id == type.id
|
||||
);
|
||||
if (!sources || !sources.sources || sources.sources.length == 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
for (let i = 0; i < sources.sources.length; i++) {
|
||||
const source: SourceInfo = sources.sources[i];
|
||||
const existingSource: SourceInfo = data.sources.find(
|
||||
(item: SourceInfo) => item.id == source.id
|
||||
);
|
||||
|
||||
if (existingSource) {
|
||||
source.name ? (existingSource.name = source.name) : null;
|
||||
source.episodes_count ?
|
||||
(existingSource.episodes_count = source.episodes_count)
|
||||
: null;
|
||||
} else {
|
||||
data.sources = [
|
||||
...data.sources,
|
||||
{
|
||||
"@id": data.sources.length == 0 ? 1 : 2 + data.sources.length,
|
||||
id: source.id,
|
||||
type: data.sources.length > 0 ? 2 : type,
|
||||
name: source.name || "Неизвестный Источник",
|
||||
episodes_count: source.episodes_count || 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path.length == 3) {
|
||||
if (!info || !data.hasOwnProperty("episodes")) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const apiSourceResponse = await fetch(
|
||||
`${HOSTNAME}/episode/${releaseId}/${voiceOverId}`
|
||||
);
|
||||
if (!apiSourceResponse.ok) {
|
||||
return data;
|
||||
}
|
||||
const sources = await apiSourceResponse.json();
|
||||
const source = sources.sources.find(
|
||||
(item: SourceInfo) => item.id == sourceId
|
||||
);
|
||||
|
||||
source["@id"] = 3;
|
||||
if (isNaN(source.type["@id"])) {
|
||||
source.type = sources.sources[0].type;
|
||||
}
|
||||
source.type["@id"] = 4;
|
||||
|
||||
const apiReleaseResponse = await fetch(`${HOSTNAME}/release/${releaseId}`);
|
||||
if (!apiReleaseResponse.ok) {
|
||||
return data;
|
||||
}
|
||||
const release = await apiReleaseResponse.json();
|
||||
release.release["@id"] = 2;
|
||||
release.release.screenshots = [];
|
||||
release.release.comments = [];
|
||||
release.release.screenshot_images = [];
|
||||
release.release.related_releases = [];
|
||||
release.release.recommended_releases = [];
|
||||
release.release.video_banners = [];
|
||||
release.release.your_vote = 0;
|
||||
release.release.related_count = 0;
|
||||
release.release.comment_count = 0;
|
||||
release.release.comments_count = 0;
|
||||
release.release.collection_count = 0;
|
||||
release.release.profile_list_status = 0;
|
||||
|
||||
if (data.episodes.length > 0) {
|
||||
data.episodes[0].release = release.release;
|
||||
data.episodes[0].source = source;
|
||||
data.episodes[0].source.episodes_count = 0;
|
||||
data.episodes[0].source.type.workers ?
|
||||
null
|
||||
: (data.episodes[0].source.type.workers = "");
|
||||
}
|
||||
|
||||
const ctypes = info.types;
|
||||
if (!ctypes || ctypes.length == 0) return data;
|
||||
const ctype = info.types.find(
|
||||
(item: VoiceoverInfo) => item.id == voiceOverId
|
||||
);
|
||||
if (!ctype) return data;
|
||||
const csource = ctype.sources.find(
|
||||
(item: SourceInfo) => item.id == sourceId
|
||||
);
|
||||
if (!csource || !csource.episodes) return data;
|
||||
const episodes = csource.episodes;
|
||||
if (!episodes || episodes.length == 0) return data;
|
||||
|
||||
if (
|
||||
data.episodes &&
|
||||
data.episodes.length > 0 &&
|
||||
data.episodes[0].source &&
|
||||
data.episodes[0].source.name == "Sibnet"
|
||||
) {
|
||||
data.episodes.forEach((item: EpisodeInfo, index: number) => {
|
||||
item.name ? null : (
|
||||
(data.episodes[index].name = `${item.position + 1} серия`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < episodes.length; i++) {
|
||||
const episode: EpisodeInfo = episodes[i];
|
||||
const existingEpisode: EpisodeInfo = data.episodes.find(
|
||||
(item: EpisodeInfo) => item.position == episode.position
|
||||
);
|
||||
|
||||
if (existingEpisode) {
|
||||
episode.position ? (existingEpisode.position = episode.position) : null;
|
||||
episode.name ? (existingEpisode.name = episode.name) : null;
|
||||
episode.url ? (existingEpisode.url = episode.url) : null;
|
||||
episode.iframe !== undefined ?
|
||||
(existingEpisode.iframe = episode.iframe)
|
||||
: null;
|
||||
episode.is_filler !== undefined ?
|
||||
(existingEpisode.is_filler = episode.is_filler)
|
||||
: null;
|
||||
} else {
|
||||
data.episodes = [
|
||||
...data.episodes,
|
||||
{
|
||||
"@id": data.episodes.length == 0 ? 1 : 4 + data.episodes.length,
|
||||
position: episode.position || data.episodes.length,
|
||||
release: data.episodes.length > 0 ? 2 : release.release,
|
||||
source: data.episodes.length > 0 ? 3 : source,
|
||||
name: episode.name || "Неизвестная Серия",
|
||||
url: episode.url || "",
|
||||
iframe: episode.iframe !== undefined ? episode.iframe : true,
|
||||
addedDate: 0,
|
||||
is_filler:
|
||||
episode.is_filler !== undefined ? episode.is_filler : false,
|
||||
is_watched: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
28
api-prox/hooks/profile.example.ts
Normal file
28
api-prox/hooks/profile.example.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
// хук меняет юзернейм 'Anixart' на 'Anixartiki'
|
||||
|
||||
import { logger } from "../shared";
|
||||
|
||||
export function match(path: string): boolean {
|
||||
// id профиля 1, это профиль Anixart (разработчиков)
|
||||
if (path == "/profile/1") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function get(data: any, url: URL) {
|
||||
const newUname = "Anixartiki";
|
||||
|
||||
// проверяем что есть поле 'profile' и оно не равно 'null', что значит что мы получили данные с апи и можно двигаться дальше
|
||||
// иначе возвращаем оригинальные данные
|
||||
if (!data.hasOwnProperty("profile") || !data.profile) return data;
|
||||
|
||||
// выводим сообщение в лог, если уровень логгера 'debug'
|
||||
logger.debugHook(
|
||||
`Changed username of '${data["profile"]["login"]}' (${data["profile"]["id"]}) to ${newUname}`
|
||||
);
|
||||
|
||||
// меняем поле на новый юзернейм
|
||||
data["profile"]["login"] = newUname;
|
||||
|
||||
// возвращаем изменённые данные
|
||||
return data;
|
||||
}
|
12
api-prox/hooks/profile.sponsor.ts
Normal file
12
api-prox/hooks/profile.sponsor.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
// хук включает "вечную" спонсорку, отключая рекламу после входа в профиль, в официальном приложении
|
||||
|
||||
export function match(path: string): boolean {
|
||||
if (path == "/profile/info") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function get(data: any, url: URL) {
|
||||
data["is_sponsor"] = true;
|
||||
data["sponsorship_expires"] = 2147483647;
|
||||
return data;
|
||||
}
|
41
api-prox/hooks/release.ts
Normal file
41
api-prox/hooks/release.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
// хук добавляет рейтинг шикимори в поле note релиза
|
||||
|
||||
export function match(path: string): boolean {
|
||||
// используем только страницы с путём /release/<id>
|
||||
const pathRe = /\/release\/\d+/
|
||||
if (pathRe.test(path)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function get(data: any, url: URL) {
|
||||
// проверяем что есть поле 'release'
|
||||
// иначе возвращаем оригинальные данные
|
||||
if (!data.hasOwnProperty("release")) return data;
|
||||
|
||||
// ищём аниме на шикимори по названию, т.к. ид аниме аниксарт и шикимори разные и нет никакого референса друг на друга
|
||||
const shikiIdRes = await fetch(
|
||||
`https://shikimori.one/api/animes?search=${data["release"]["title_original"]}`
|
||||
);
|
||||
if (!shikiIdRes.ok) return data; // если при поиске произошла ошибка, то возвращаем оригинальные данные
|
||||
const shikiIdJson = await shikiIdRes.json();
|
||||
if (shikiIdJson.length == 0) return data; // если нет результатов, то возвращаем оригинальные данные
|
||||
|
||||
const shikiId = shikiIdJson[0]["id"]; // берём ид от первого результата
|
||||
|
||||
// повторяем процесс, уже с ид от шикимори
|
||||
const shikiAnimRes = await fetch(
|
||||
`https://shikimori.one/api/animes/${shikiId}`
|
||||
);
|
||||
if (!shikiAnimRes.ok) return data;
|
||||
const shikiAnimJson = await shikiAnimRes.json();
|
||||
|
||||
// пушим строки в список, что-бы было легче их объединить
|
||||
const noteBuilder = [];
|
||||
if (data["release"]["note"] != null) noteBuilder.push(`${data.release.note}<br/>---<br/>`); // если в поле note уже что-то есть, разделяем значение и рейтинг
|
||||
noteBuilder.push(`<b>Рейтинг Shikimori:</b> ${Number(shikiAnimJson.score)}★`); // добавляем рейтинг от шикимори
|
||||
data["release"]["note"] = noteBuilder.toString(); // заменяем оригинальное поле нашей строкой
|
||||
data["release"]["id_shikimori"] = shikiId; // добавляем айди шикимори в ответ, потому что почему нет
|
||||
|
||||
// возвращаем изменённые данные
|
||||
return data;
|
||||
}
|
76
api-prox/hooks/toggles.ts
Normal file
76
api-prox/hooks/toggles.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
// хук изменяет ответ config/toggles
|
||||
|
||||
export interface Toggles {
|
||||
minVersionCode: number;
|
||||
lastVersionCode: number;
|
||||
whatsNew: string;
|
||||
downloadLink: string;
|
||||
minGPVersionCode: number;
|
||||
lastGPVersionCode: number;
|
||||
gpWhatsNew: string;
|
||||
gpDownloadLink: string;
|
||||
overrideGPVersion: boolean;
|
||||
inAppUpdates: boolean;
|
||||
inAppUpdatesImmediate: boolean;
|
||||
inAppUpdatesFlexibleDelay: number;
|
||||
impMessageEnabled: boolean;
|
||||
impMessageText: string;
|
||||
impMessageBackgroundColor: string;
|
||||
impMessageTextColor: string;
|
||||
impMessageLink: string;
|
||||
adBannerBlockId: string;
|
||||
adBannerSizeType: number;
|
||||
adInterstitialBlockId: string;
|
||||
adBannerDelay: number;
|
||||
adInterstitialDelay: number;
|
||||
kodikVideoLinksUrl: string;
|
||||
kodikIframeAd: boolean;
|
||||
sibnetRandUserAgent: boolean;
|
||||
sibnetUserAgent: string;
|
||||
torlookUrl: string;
|
||||
baseUrl: string;
|
||||
apiUrl: string;
|
||||
apiAltUrl: string;
|
||||
apiAltAvailable: boolean;
|
||||
iframeEmbedUrl: string;
|
||||
kodikAdIframeUrl: string;
|
||||
sponsorshipPromotion: boolean;
|
||||
sponsorshipText: string;
|
||||
sponsorshipAvailable: boolean;
|
||||
pageNoConnectionUrl: string;
|
||||
snowfall: boolean;
|
||||
searchBarIconUrl: string;
|
||||
searchBarIconTint: string;
|
||||
searchBarIconAction: string;
|
||||
searchBarIconValue: string;
|
||||
min_blog_create_rating_score: number;
|
||||
}
|
||||
|
||||
export function match(path: string): boolean {
|
||||
if (path == "/config/toggles") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function get(data: Toggles, url: URL) {
|
||||
data.lastVersionCode = 25062200;
|
||||
|
||||
data.impMessageEnabled = true;
|
||||
data.impMessageText = "разработчик AniX / Api-Prox-Service";
|
||||
data.impMessageLink = "https://wah.su/radiquum";
|
||||
data.impMessageBackgroundColor = "ffb3d0";
|
||||
data.impMessageTextColor = "ffffff";
|
||||
|
||||
data.apiAltAvailable = false;
|
||||
data.apiAltUrl = "";
|
||||
|
||||
data.sponsorshipAvailable = false;
|
||||
data.sponsorshipPromotion = false;
|
||||
data.kodikIframeAd = false;
|
||||
data.kodikAdIframeUrl = "";
|
||||
|
||||
if (process.env.HOST_URL) {
|
||||
data.iframeEmbedUrl = `${process.env.HOST_URL}/player?url=`;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
14
api-prox/iframe.ts
Normal file
14
api-prox/iframe.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export const Iframe = (url: string) => {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Веб-плеер</title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=yes' />
|
||||
<style>body, html {height: 100%; width: 100%; margin: 0px;padding: 0px;border: 0px;}</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="${url}" width='100%' height='100%' frameborder='0' AllowFullScreen allow="autoplay"></iframe>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
362
api-prox/index.ts
Normal file
362
api-prox/index.ts
Normal file
|
@ -0,0 +1,362 @@
|
|||
import {
|
||||
ANIXART_API,
|
||||
ANIXART_HEADERS,
|
||||
ANIXART_HEADERST,
|
||||
asJSON,
|
||||
GetHook,
|
||||
LoadedHook,
|
||||
logger,
|
||||
PostHook,
|
||||
} from "./shared";
|
||||
import express from "express";
|
||||
import fs from "fs/promises";
|
||||
import { MediaChromeTheme } from "./media-chrome";
|
||||
import { Iframe } from "./iframe";
|
||||
|
||||
const app = express();
|
||||
app.use(
|
||||
express.raw({ inflate: true, limit: "50mb", type: "multipart/form-data" })
|
||||
);
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
const HOST = process.env.HOST || "0.0.0.0";
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 7001;
|
||||
|
||||
let hooks: string[] = [];
|
||||
|
||||
async function loadHooks() {
|
||||
let hooksDir: string[] = [];
|
||||
try {
|
||||
hooksDir = await fs.readdir("./hooks");
|
||||
} catch (err) {
|
||||
logger.error("'hooks' directory not found");
|
||||
}
|
||||
|
||||
for (let i = 0; i < hooksDir.length; i++) {
|
||||
const name = hooksDir[i];
|
||||
if (
|
||||
!name.endsWith(".ts") ||
|
||||
name.includes("example") ||
|
||||
name.includes("disabled")
|
||||
)
|
||||
continue;
|
||||
|
||||
require(`./hooks/${name}`);
|
||||
logger.infoHook(`Loaded "./hooks/${name}"`);
|
||||
hooks.push(name);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const watcher = fs.watch(`./hooks/${name}`);
|
||||
for await (const event of watcher) {
|
||||
if (event.eventType === "change") {
|
||||
logger.infoHook(`Updated "./hooks/${event.filename}"`);
|
||||
delete require.cache[require.resolve(`./hooks/${event.filename}`)];
|
||||
require(`./hooks/${event.filename}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
app.get("/player", async (req, res) => {
|
||||
let url = req.query.url || null;
|
||||
|
||||
res.status(200);
|
||||
res.set({
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
|
||||
"Cache-Control": "no-cache",
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
});
|
||||
if (!url) {
|
||||
res.send("<h1>No url query found!</h1>");
|
||||
return;
|
||||
}
|
||||
|
||||
let player = "";
|
||||
let poster = "";
|
||||
|
||||
const CUSTOM_PLAYER_DOMAINS = [
|
||||
"video.sibnet.ru",
|
||||
"anixart.libria.fun",
|
||||
"kodik.info",
|
||||
"aniqit.com",
|
||||
"kodik.cc",
|
||||
"kodik.biz",
|
||||
];
|
||||
const urlDomain = new URL(url.toString());
|
||||
const PLAYER_PARSER_URL = process.env.PLAYER_PARSER_URL || null;
|
||||
|
||||
if (CUSTOM_PLAYER_DOMAINS.includes(urlDomain.hostname)) {
|
||||
try {
|
||||
if (!PLAYER_PARSER_URL) throw new Error();
|
||||
|
||||
if (
|
||||
["kodik.info", "aniqit.com", "kodik.cc", "kodik.biz"].includes(
|
||||
urlDomain.hostname
|
||||
)
|
||||
) {
|
||||
player = "kodik";
|
||||
}
|
||||
if ("anixart.libria.fun" == urlDomain.hostname) {
|
||||
player = "libria";
|
||||
}
|
||||
if ("video.sibnet.ru" == urlDomain.hostname) {
|
||||
player = "sibnet";
|
||||
}
|
||||
|
||||
const playerParserRes = await fetch(
|
||||
`${PLAYER_PARSER_URL}?url=${encodeURIComponent(url.toString())}&player=${player}`
|
||||
);
|
||||
|
||||
if (!playerParserRes.ok) throw new Error();
|
||||
|
||||
const playerParserData: { manifest: string; poster: string } =
|
||||
await playerParserRes.json();
|
||||
|
||||
poster = playerParserData.poster;
|
||||
if (playerParserData.manifest.startsWith("#EXTM3U")) {
|
||||
const playerUrlArray = playerParserData.manifest.split("\n");
|
||||
url = playerUrlArray.join("\\n");
|
||||
} else {
|
||||
url = playerParserData.manifest;
|
||||
}
|
||||
} catch {
|
||||
res.send(Iframe(url.toString()));
|
||||
return;
|
||||
}
|
||||
} else if (url.toString().toLowerCase().endsWith("mp4")) {
|
||||
player = "mp4";
|
||||
} else if (url.toString().toLowerCase().endsWith(".m3u8")) {
|
||||
player = "hls";
|
||||
} else {
|
||||
res.send(Iframe(url.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Веб-плеер</title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=yes' />
|
||||
<style>body, html {height: 100%; width: 100%; margin: 0px;padding: 0px;border: 0px;}</style>
|
||||
${["kodik", "libria", "hls"].includes(player) ? '<script type="module" src="https://cdn.jsdelivr.net/npm/hls-video-element@1.2/+esm"></script>' : ""}
|
||||
<script>
|
||||
window.onload = () => {
|
||||
const url = "${url}";
|
||||
const poster = "${poster}";
|
||||
const element = document.querySelector("#video-element");
|
||||
|
||||
element.poster = poster;
|
||||
if (url.startsWith("http")) {
|
||||
element.src = url;
|
||||
} else {
|
||||
let file = new File([url], "manifest.m3u8", {
|
||||
type: "application/x-mpegURL",
|
||||
});
|
||||
element.src = URL.createObjectURL(file);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
${MediaChromeTheme()}
|
||||
|
||||
<media-theme
|
||||
template="media-theme-sutro"
|
||||
style="width:100%;height:100%;">
|
||||
${
|
||||
["kodik", "libria", "hls"].includes(player) ?
|
||||
`<hls-video slot="media" playsinline id="video-element"></hls-video>`
|
||||
: `<video slot="media" playsinline id="video-element"></video>`
|
||||
}
|
||||
</media-theme>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
app.get("/*path", async (req, res) => {
|
||||
if (req.path == "/favicon.ico") return asJSON(res, {}, 404);
|
||||
|
||||
const url = new URL(`${ANIXART_API}${req.url}`);
|
||||
logger.debug(
|
||||
`[${req.method}] ${url.protocol}//${url.hostname}${url.pathname}`
|
||||
);
|
||||
// logger.debug(` ↳ [QUERY] ${url.searchParams.toString()}`);
|
||||
|
||||
if (
|
||||
url.searchParams.get("API-Version") == "v2" ||
|
||||
req.headers["api-version"] == "v2"
|
||||
) {
|
||||
// logger.debug(` ↳ Force API V2`);
|
||||
ANIXART_HEADERS["Api-Version"] = "v2";
|
||||
url.searchParams.delete("API-Version");
|
||||
}
|
||||
|
||||
const apiResponse = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers: ANIXART_HEADERS,
|
||||
});
|
||||
|
||||
if (
|
||||
!apiResponse ||
|
||||
!apiResponse.ok ||
|
||||
apiResponse.headers.get("content-type") != "application/json"
|
||||
) {
|
||||
logger.error(
|
||||
`Failed to fetch: '${url.protocol}//${url.hostname}${url.pathname}', Path probably doesn't exist`
|
||||
);
|
||||
asJSON(
|
||||
res,
|
||||
{
|
||||
code: 99,
|
||||
returned_value: {
|
||||
request_status: apiResponse ? apiResponse.status : null,
|
||||
request_content_type:
|
||||
apiResponse ? apiResponse.headers.get("content-type") : null,
|
||||
},
|
||||
reason: "Path probably doesn't exist",
|
||||
},
|
||||
500
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let data = await apiResponse.json();
|
||||
|
||||
for (let i = 0; i < hooks.length; i++) {
|
||||
const name = hooks[i];
|
||||
const hook: GetHook = require(`./hooks/${name}`);
|
||||
if (!hook.hasOwnProperty("match") || !hook.hasOwnProperty("get")) continue;
|
||||
if (!hook.match(req.path)) continue;
|
||||
data = await hook.get(data, url);
|
||||
}
|
||||
|
||||
asJSON(res, data, 200);
|
||||
return;
|
||||
});
|
||||
|
||||
app.post("/*path", async (req, res) => {
|
||||
const url = new URL(`${ANIXART_API}${req.url}`);
|
||||
logger.debug(
|
||||
`[${req.method}] ${url.protocol}//${url.hostname}${url.pathname}`
|
||||
);
|
||||
// logger.debug(` ↳ [QUERY] ${url.searchParams.toString()}`);
|
||||
|
||||
let apiResponse: null | Response = null;
|
||||
const apiHeaders: ANIXART_HEADERST = {
|
||||
"User-Agent": ANIXART_HEADERS["User-Agent"],
|
||||
"Content-Type": req.headers["content-type"] || "application/json",
|
||||
};
|
||||
|
||||
if (
|
||||
url.searchParams.get("API-Version") == "v2" ||
|
||||
req.headers["api-version"] == "v2"
|
||||
) {
|
||||
// logger.debug(` ↳ Force API V2`);
|
||||
apiHeaders["Api-Version"] = "v2";
|
||||
url.searchParams.delete("API-Version");
|
||||
}
|
||||
|
||||
const reqContentType =
|
||||
req.headers["content-type"] ?
|
||||
req.headers["content-type"].split(";")[0]
|
||||
: "application/json";
|
||||
|
||||
const supportedContentTypes = [
|
||||
"application/json",
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data",
|
||||
];
|
||||
const isSupported = supportedContentTypes.some((type) =>
|
||||
reqContentType.toLowerCase().startsWith(type)
|
||||
);
|
||||
if (!isSupported) {
|
||||
res.status(500).json({
|
||||
code: 99,
|
||||
error: "Unsupported Media Type",
|
||||
reason: `Content-Type '${reqContentType}' is not supported.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (reqContentType) {
|
||||
case "multipart/form-data":
|
||||
apiResponse = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: apiHeaders,
|
||||
body: req.body,
|
||||
});
|
||||
break;
|
||||
case "application/x-www-form-urlencoded":
|
||||
apiResponse = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: apiHeaders,
|
||||
body: new URLSearchParams(req.body),
|
||||
});
|
||||
break;
|
||||
case "application/json":
|
||||
apiResponse = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: apiHeaders,
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
!apiResponse ||
|
||||
!apiResponse.ok ||
|
||||
apiResponse.headers.get("content-type") != "application/json"
|
||||
) {
|
||||
logger.error(
|
||||
`Failed to post: '${url.protocol}//${url.hostname}${url.pathname}', Path probably doesn't exist`
|
||||
);
|
||||
asJSON(
|
||||
res,
|
||||
{
|
||||
code: 99,
|
||||
returned_value: {
|
||||
request_status: apiResponse ? apiResponse.status : null,
|
||||
request_content_type:
|
||||
apiResponse ? apiResponse.headers.get("content-type") : null,
|
||||
},
|
||||
reason: "Path probably doesn't exist",
|
||||
},
|
||||
500
|
||||
);
|
||||
return;
|
||||
}
|
||||
let data = await apiResponse.json();
|
||||
let hooks: string[] = [];
|
||||
|
||||
try {
|
||||
hooks = await fs.readdir("./hooks");
|
||||
} catch (err) {
|
||||
logger.error("'hooks' directory not found");
|
||||
}
|
||||
|
||||
for (let i = 0; i < hooks.length; i++) {
|
||||
const name = hooks[i];
|
||||
const hook: PostHook = require(`./hooks/${name}`);
|
||||
if (!hook.hasOwnProperty("match") || !hook.hasOwnProperty("post")) continue;
|
||||
if (!hook.match(req.path)) continue;
|
||||
data = await hook.post(data, url);
|
||||
}
|
||||
|
||||
asJSON(res, data, 200);
|
||||
return;
|
||||
});
|
||||
|
||||
app.listen(PORT, HOST, function () {
|
||||
loadHooks();
|
||||
logger.info(`Server listen: http://${HOST}:${PORT}`);
|
||||
});
|
813
api-prox/media-chrome.ts
Normal file
813
api-prox/media-chrome.ts
Normal file
|
@ -0,0 +1,813 @@
|
|||
export const MediaChromeTheme = () => {
|
||||
return `
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/media-chrome/+esm"></script>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/media-chrome/menu/+esm"></script>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/media-chrome/media-theme-element/+esm"></script>
|
||||
|
||||
<template id="media-theme-sutro">
|
||||
<!-- Sutro -->
|
||||
<style>
|
||||
:host {
|
||||
--_primary-color: var(--media-primary-color, #fff);
|
||||
--_secondary-color: var(--media-secondary-color, transparent);
|
||||
--_accent-color: var(--media-accent-color, #fff);
|
||||
}
|
||||
|
||||
media-controller {
|
||||
--base: 18px;
|
||||
|
||||
font-size: calc(0.75 * var(--base));
|
||||
font-family: Roboto, Arial, sans-serif;
|
||||
--media-font-family: Roboto, helvetica neue, segoe ui, arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
--media-primary-color: #fff;
|
||||
--media-secondary-color: transparent;
|
||||
--media-menu-background: rgba(28, 28, 28, 0.6);
|
||||
--media-text-color: var(--_primary-color);
|
||||
--media-control-hover-background: var(--media-secondary-color);
|
||||
|
||||
--media-range-track-height: calc(0.125 * var(--base));
|
||||
--media-range-thumb-height: var(--base);
|
||||
--media-range-thumb-width: var(--base);
|
||||
--media-range-thumb-border-radius: var(--base);
|
||||
|
||||
--media-control-height: calc(2 * var(--base));
|
||||
}
|
||||
|
||||
media-controller[breakpointmd] {
|
||||
--base: 20px;
|
||||
}
|
||||
|
||||
/* The biggest size controller is tied to going fullscreen
|
||||
instead of a player width */
|
||||
media-controller[mediaisfullscreen] {
|
||||
--base: 24px;
|
||||
}
|
||||
|
||||
.media-button {
|
||||
--media-control-hover-background: var(--_secondary-color);
|
||||
--media-tooltip-background: rgb(28 28 28 / .24);
|
||||
--media-text-content-height: 1.2;
|
||||
--media-tooltip-padding: .7em 1em;
|
||||
--media-tooltip-distance: 8px;
|
||||
--media-tooltip-container-margin: 18px;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.1s cubic-bezier(0.4, 0, 1, 1);
|
||||
}
|
||||
|
||||
.media-button svg {
|
||||
fill: none;
|
||||
stroke: var(--_primary-color);
|
||||
stroke-width: 1;
|
||||
stroke-linecap: 'round';
|
||||
stroke-linejoin: 'round';
|
||||
}
|
||||
|
||||
svg .svg-shadow {
|
||||
stroke: #000;
|
||||
stroke-opacity: 0.15;
|
||||
stroke-width: 2px;
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<media-controller
|
||||
breakpoints="md:480"
|
||||
defaultsubtitles="{{defaultsubtitles}}"
|
||||
defaultduration="{{defaultduration}}"
|
||||
gesturesdisabled="{{disabled}}"
|
||||
hotkeys="{{hotkeys}}"
|
||||
nohotkeys="{{nohotkeys}}"
|
||||
defaultstreamtype="on-demand"
|
||||
>
|
||||
<slot name="media" slot="media"></slot>
|
||||
<slot name="poster" slot="poster"></slot>
|
||||
<media-error-dialog slot="dialog"></media-error-dialog>
|
||||
|
||||
<!-- Controls Gradient -->
|
||||
<style>
|
||||
.media-gradient-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: calc(8 * var(--base));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.media-gradient-bottom::before {
|
||||
content: '';
|
||||
--gradient-steps: hsl(0 0% 0% / 0) 0%, hsl(0 0% 0% / 0.013) 8.1%, hsl(0 0% 0% / 0.049) 15.5%,
|
||||
hsl(0 0% 0% / 0.104) 22.5%, hsl(0 0% 0% / 0.175) 29%, hsl(0 0% 0% / 0.259) 35.3%, hsl(0 0% 0% / 0.352) 41.2%,
|
||||
hsl(0 0% 0% / 0.45) 47.1%, hsl(0 0% 0% / 0.55) 52.9%, hsl(0 0% 0% / 0.648) 58.8%, hsl(0 0% 0% / 0.741) 64.7%,
|
||||
hsl(0 0% 0% / 0.825) 71%, hsl(0 0% 0% / 0.896) 77.5%, hsl(0 0% 0% / 0.951) 84.5%, hsl(0 0% 0% / 0.987) 91.9%,
|
||||
hsl(0 0% 0%) 100%;
|
||||
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.7;
|
||||
background: linear-gradient(to bottom, var(--gradient-steps));
|
||||
}
|
||||
</style>
|
||||
<div class="media-gradient-bottom"></div>
|
||||
|
||||
<!-- Settings Menu -->
|
||||
<style>
|
||||
media-settings-menu {
|
||||
--media-menu-icon-height: 20px;
|
||||
--media-menu-item-icon-height: 20px;
|
||||
--media-settings-menu-min-width: calc(10 * var(--base));
|
||||
--media-menu-transform-in: translateY(0) scale(1);
|
||||
--media-menu-transform-out: translateY(20px) rotate(3deg) scale(1);
|
||||
padding-block: calc(0.15 * var(--base));
|
||||
margin-right: 10px;
|
||||
margin-bottom: 17px;
|
||||
border-radius: 8px;
|
||||
z-index: 2;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
media-settings-menu-item,
|
||||
[role='menu']::part(menu-item) {
|
||||
--media-icon-color: var(--_primary-color);
|
||||
margin-inline: calc(0.45 * var(--base));
|
||||
height: calc(1.6 * var(--base));
|
||||
font-size: calc(0.7 * var(--base));
|
||||
font-weight: 400;
|
||||
padding: 0;
|
||||
padding-left: calc(0.4 * var(--base));
|
||||
padding-right: calc(0.1 * var(--base));
|
||||
border-radius: 6px;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
[slot='submenu']::part(back button) {
|
||||
font-size: calc(0.7 * var(--base));
|
||||
}
|
||||
|
||||
media-settings-menu-item:hover {
|
||||
--media-icon-color: #000;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
media-settings-menu-item:hover [slot='submenu']::part(menu-item),
|
||||
[slot='submenu']::part(back indicator) {
|
||||
--media-icon-color: var(--_primary-color);
|
||||
}
|
||||
|
||||
media-settings-menu-item:hover [slot='submenu']::part(menu-item):hover {
|
||||
--media-icon-color: #000;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
media-settings-menu-item[submenusize='0'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Also hide if only Auto is added. */
|
||||
.quality-settings[submenusize='1'] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<media-settings-menu hidden anchor="auto">
|
||||
<media-settings-menu-item>
|
||||
Playback Speed
|
||||
<media-playback-rate-menu slot="submenu" hidden rates="0.5 0.75 1 1.25 1.5 1.75 2">
|
||||
<div slot="title">Playback Speed</div>
|
||||
</media-playback-rate-menu>
|
||||
</media-settings-menu-item>
|
||||
<media-settings-menu-item class="quality-settings">
|
||||
Quality
|
||||
<media-rendition-menu slot="submenu" hidden>
|
||||
<div slot="title">Quality</div>
|
||||
</media-rendition-menu>
|
||||
</media-settings-menu-item>
|
||||
<media-settings-menu-item>
|
||||
Subtitles/CC
|
||||
<media-captions-menu slot="submenu" hidden>
|
||||
<div slot="title">Subtitles/CC</div>
|
||||
</media-captions-menu>
|
||||
</media-settings-menu-item>
|
||||
</media-settings-menu>
|
||||
|
||||
<!-- Control Bar -->
|
||||
<style>
|
||||
media-control-bar {
|
||||
position: absolute;
|
||||
height: calc(2 * var(--base));
|
||||
line-height: calc(2 * var(--base));
|
||||
bottom: var(--base);
|
||||
left: var(--base);
|
||||
right: var(--base);
|
||||
}
|
||||
</style>
|
||||
|
||||
<media-control-bar>
|
||||
<!-- Play/Pause -->
|
||||
<style>
|
||||
@keyframes bounce-scale-play {
|
||||
0% {
|
||||
transform: scale(0.75, 0.75);
|
||||
}
|
||||
50% {
|
||||
transform: scale(115%, 115%);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.media-button {
|
||||
border-radius: 25%;
|
||||
backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(0);
|
||||
-webkit-backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(0);
|
||||
transition: backdrop-filter 0.3s, -webkit-backdrop-filter 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.media-button:hover {
|
||||
/* background-color: rgba(0, 0, 0, 0.05); */
|
||||
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px;
|
||||
/* hue-rotate(120deg) */
|
||||
backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(1);
|
||||
-webkit-backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(1);
|
||||
transition: backdrop-filter 0.3s, -webkit-backdrop-filter 0.3s;
|
||||
}
|
||||
|
||||
media-play-button #icon-play {
|
||||
opacity: 0;
|
||||
transform-box: view-box;
|
||||
transform-origin: center center;
|
||||
transform: scale(0.5, 0.5);
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
media-play-button[mediapaused] #icon-play {
|
||||
opacity: 1;
|
||||
transform: scale(1, 1);
|
||||
animation: 0.35s bounce-scale-play ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes bounce-pause-left {
|
||||
0% {
|
||||
font-size: 10px;
|
||||
}
|
||||
50% {
|
||||
font-size: 3px;
|
||||
}
|
||||
100% {
|
||||
font-size: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce-pause-right {
|
||||
0% {
|
||||
font-size: 10px;
|
||||
transform: translateX(-8px);
|
||||
}
|
||||
50% {
|
||||
font-size: 3px;
|
||||
transform: translateX(1px);
|
||||
}
|
||||
100% {
|
||||
font-size: 4px;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
media-play-button #pause-left,
|
||||
media-play-button #pause-right {
|
||||
/* Using font-size to animate height because using scale was resulting in unexpected positioning */
|
||||
font-size: 4px;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transform-box: view-box;
|
||||
}
|
||||
|
||||
media-play-button:not([mediapaused]) #pause-left {
|
||||
animation: 0.3s bounce-pause-left ease-out;
|
||||
}
|
||||
|
||||
media-play-button:not([mediapaused]) #pause-right {
|
||||
animation: 0.3s bounce-pause-right ease-out;
|
||||
}
|
||||
|
||||
media-play-button[mediapaused] #pause-left,
|
||||
media-play-button[mediapaused] #pause-right {
|
||||
opacity: 0;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
media-play-button[mediapaused] #pause-right {
|
||||
transform-origin: right center;
|
||||
transform: translateX(-8px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<media-play-button mediapaused class="media-button">
|
||||
<svg slot="icon" viewBox="0 0 32 32">
|
||||
<!-- <use class="svg-shadow" xlink:href="#icon-play"></use> -->
|
||||
<g>
|
||||
<path
|
||||
id="icon-play"
|
||||
d="M20.7131 14.6976C21.7208 15.2735 21.7208 16.7265 20.7131 17.3024L12.7442 21.856C11.7442 22.4274 10.5 21.7054 10.5 20.5536L10.5 11.4464C10.5 10.2946 11.7442 9.57257 12.7442 10.144L20.7131 14.6976Z"
|
||||
/>
|
||||
</g>
|
||||
<!-- <use class="svg-shadow" xlink:href="#icon-pause"></use> -->
|
||||
<g id="icon-pause">
|
||||
<rect id="pause-left" x="10.5" width="1em" y="10.5" height="11" rx="0.5" />
|
||||
<rect id="pause-right" x="17.5" width="1em" y="10.5" height="11" rx="0.5" />
|
||||
</g>
|
||||
</svg>
|
||||
</media-play-button>
|
||||
|
||||
<!-- Volume/Mute -->
|
||||
<style>
|
||||
media-mute-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
media-mute-button .muted-path {
|
||||
transition: clip-path 0.2s ease-out;
|
||||
}
|
||||
|
||||
media-mute-button #muted-path-2 {
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
|
||||
media-mute-button .muted-path {
|
||||
clip-path: inset(0);
|
||||
}
|
||||
|
||||
media-mute-button:not([mediavolumelevel='off']) #muted-path-1 {
|
||||
clip-path: inset(0 0 100% 0);
|
||||
}
|
||||
|
||||
media-mute-button:not([mediavolumelevel='off']) #muted-path-2 {
|
||||
clip-path: inset(0 0 100% 0);
|
||||
}
|
||||
|
||||
media-mute-button .muted-path {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
media-mute-button[mediavolumelevel='off'] .muted-path {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
media-mute-button .vol-path {
|
||||
opacity: 1;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
media-mute-button[mediavolumelevel='off'] .vol-path {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
media-mute-button[mediavolumelevel='low'] #vol-high-path,
|
||||
media-mute-button[mediavolumelevel='medium'] #vol-high-path {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
media-volume-range {
|
||||
--media-range-track-background: rgba(255, 255, 255, 0.2);
|
||||
--media-range-thumb-opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes volume-in {
|
||||
0% {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(50%) rotate(1deg);
|
||||
}
|
||||
50% {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: rotate(-2deg);
|
||||
}
|
||||
100% {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes volume-out {
|
||||
0% {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(50%) rotate(1deg);
|
||||
}
|
||||
}
|
||||
|
||||
.media-volume-range-wrapper {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
position: absolute;
|
||||
top: -100%;
|
||||
left: calc(2 * var(--base));
|
||||
|
||||
width: calc(10 * var(--base));
|
||||
height: calc(2.5 * var(--base));
|
||||
transform-origin: center left;
|
||||
}
|
||||
|
||||
media-volume-range {
|
||||
/*
|
||||
Hide range and animation until mediavolume attribute is set.
|
||||
visibility didn't work, hovering over media-volume-range-wrapper
|
||||
caused it to show. Should require mute-button:hover.
|
||||
*/
|
||||
opacity: 0;
|
||||
transition: opacity 0s 1s;
|
||||
|
||||
width: calc(10 * var(--base));
|
||||
height: var(--base);
|
||||
padding: 0;
|
||||
border-radius: calc(0.25 * var(--base));
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
|
||||
--media-range-bar-color: var(--media-accent-color);
|
||||
|
||||
--media-range-padding-left: 0;
|
||||
--media-range-padding-right: 0;
|
||||
|
||||
--media-range-track-width: calc(10 * var(--base));
|
||||
--media-range-track-height: var(--base);
|
||||
--media-range-track-border-radius: calc(0.25 * var(--base));
|
||||
--media-range-track-backdrop-filter: blur(10px) brightness(80%);
|
||||
|
||||
/* This makes zero volume still show some of the bar.
|
||||
I can't make the bar have curved corners otherwise though. */
|
||||
--media-range-thumb-width: var(--base);
|
||||
--media-range-thumb-border-radius: calc(0.25 * var(--base));
|
||||
|
||||
/* The Sutro design has a gradient like this, but not sure I like it */
|
||||
/* --media-range-thumb-box-shadow: 10px 0px 20px rgba(255, 255, 255, 0.5); */
|
||||
}
|
||||
|
||||
media-volume-range[mediavolume] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[keyboardcontrol] media-volume-range:focus {
|
||||
/* TODO: This appears to be creating a think outline */
|
||||
outline: 1px solid rgba(27, 127, 204, 0.9);
|
||||
}
|
||||
|
||||
media-mute-button:hover + .media-volume-range-wrapper,
|
||||
media-mute-button:focus + .media-volume-range-wrapper,
|
||||
media-mute-button:focus-within + .media-volume-range-wrapper,
|
||||
.media-volume-range-wrapper:hover,
|
||||
.media-volume-range-wrapper:focus,
|
||||
.media-volume-range-wrapper:focus-within {
|
||||
animation: 0.3s volume-in forwards ease-out;
|
||||
}
|
||||
|
||||
.media-volume-range-wrapper:not(:hover, :focus-within) {
|
||||
animation: 0.3s volume-out ease-out;
|
||||
}
|
||||
|
||||
/* When keyboard navigating the volume range and wrapper need to always be visible
|
||||
otherwise focus state can't land on it. This is ok when keyboard navigating because
|
||||
the hovering issues aren't a concern, unless you happen to be keyboard AND mouse navigating.
|
||||
*/
|
||||
[keyboardcontrol] .media-volume-range-wrapper,
|
||||
[keyboardcontrol] .media-volume-range-wrapper:focus-within,
|
||||
[keyboardcontrol] .media-volume-range-wrapper:focus-within media-volume-range {
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
<media-mute-button class="media-button" notooltip>
|
||||
<use class="svg-shadow" xlink:href="#vol-paths"></use>
|
||||
<svg slot="icon" viewBox="0 0 32 32">
|
||||
<g id="vol-paths">
|
||||
<path
|
||||
id="speaker-path"
|
||||
d="M16.5 20.486v-8.972c0-1.537-2.037-2.08-2.802-.745l-1.026 1.79a2.5 2.5 0 0 1-.8.85l-1.194.78A1.5 1.5 0 0 0 10 15.446v1.11c0 .506.255.978.678 1.255l1.194.782a2.5 2.5 0 0 1 .8.849l1.026 1.79c.765 1.334 2.802.792 2.802-.745Z"
|
||||
/>
|
||||
<path
|
||||
id="vol-low-path"
|
||||
class="vol-path"
|
||||
d="M18.5 18C19.6046 18 20.5 17.1046 20.5 16C20.5 14.8954 19.6046 14 18.5 14"
|
||||
/>
|
||||
<path
|
||||
id="vol-high-path"
|
||||
class="vol-path"
|
||||
d="M18 21C20.7614 21 23 18.7614 23 16C23 13.2386 20.7614 11 18 11"
|
||||
/>
|
||||
<path id="muted-path-1" class="muted-path" d="M23 18L19 14" />
|
||||
<path id="muted-path-2" class="muted-path" d="M23 14L19 18" />
|
||||
</g>
|
||||
</svg>
|
||||
</media-mute-button>
|
||||
<div class="media-volume-range-wrapper">
|
||||
<media-volume-range></media-volume-range>
|
||||
</div>
|
||||
|
||||
<!-- Time Display -->
|
||||
<style>
|
||||
media-time-display {
|
||||
position: relative;
|
||||
padding: calc(0.5 * var(--base));
|
||||
font-size: calc(0.7 * var(--base));
|
||||
border-radius: calc(0.5 * var(--base));
|
||||
}
|
||||
|
||||
media-controller[breakpointmd] media-time-display:not([showduration]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
media-controller:not([breakpointmd]) media-time-display[showduration] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<media-time-display></media-time-display>
|
||||
<media-time-display showduration></media-time-display>
|
||||
|
||||
<!-- Time Range / Progress Bar -->
|
||||
<style>
|
||||
media-time-range {
|
||||
height: calc(2 * var(--base));
|
||||
border-radius: calc(0.25 * var(--base));
|
||||
|
||||
--media-range-track-backdrop-filter: invert(10%) blur(5px) brightness(110%);
|
||||
--media-range-track-background: rgba(255, 255, 255, 0.2);
|
||||
--media-range-track-pointer-background: rgba(255, 255, 255, 0.5);
|
||||
--media-range-track-border-radius: calc(0.25 * var(--base));
|
||||
|
||||
--media-time-range-buffered-color: rgba(255, 255, 255, 0.4);
|
||||
--media-range-bar-color: var(--media-accent-color);
|
||||
|
||||
--media-range-thumb-background: var(--media-accent-color);
|
||||
--media-range-thumb-transition: opacity 0.1s linear;
|
||||
--media-range-thumb-opacity: 0;
|
||||
|
||||
--media-preview-thumbnail-border: calc(0.125 * var(--base)) solid #fff;
|
||||
--media-preview-thumbnail-border-radius: calc(0.5 * var(--base));
|
||||
--media-preview-thumbnail-min-width: calc(8 * var(--base));
|
||||
--media-preview-thumbnail-max-width: calc(10 * var(--base));
|
||||
--media-preview-thumbnail-min-height: calc(5 * var(--base));
|
||||
--media-preview-thumbnail-max-height: calc(7 * var(--base));
|
||||
--media-preview-box-margin: 0 0 -10px;
|
||||
}
|
||||
media-time-range:hover {
|
||||
--media-range-thumb-opacity: 1;
|
||||
--media-range-track-height: calc(0.25 * var(--base));
|
||||
}
|
||||
|
||||
media-preview-thumbnail {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
media-preview-chapter-display {
|
||||
font-size: calc(0.6 * var(--base));
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
media-preview-time-display {
|
||||
font-size: calc(0.65 * var(--base));
|
||||
padding-top: 0;
|
||||
}
|
||||
</style>
|
||||
<media-time-range>
|
||||
<media-preview-thumbnail slot="preview"></media-preview-thumbnail>
|
||||
<media-preview-chapter-display slot="preview"></media-preview-chapter-display>
|
||||
<media-preview-time-display slot="preview"></media-preview-time-display>
|
||||
</media-time-range>
|
||||
|
||||
<media-seek-backward-button class="media-button" seekoffset="10">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
slot="icon"
|
||||
class="svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="-8 -8 40 40"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path
|
||||
strokeLinejoin="round"
|
||||
d="M14 4.5L12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12c0-4.1 2.468-7.625 6-9.168"
|
||||
/>
|
||||
<path strokeLinejoin="round" d="m7.5 10.5l2.5-2v7" />
|
||||
<path d="M12.5 13.75v-3.5a1.75 1.75 0 1 1 3.5 0v3.5a1.75 1.75 0 1 1-3.5 0Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</media-seek-backward-button>
|
||||
|
||||
<media-seek-forward-button class="media-button" seekoffset="10">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
slot="icon"
|
||||
class="svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="-8 -8 40 40"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path
|
||||
strokeLinejoin="round"
|
||||
d="M10 4.5L12 2C6.477 2 2 6.477 2 12s4.477 10 10 10s10-4.477 10-10c0-4.1-2.468-7.625-6-9.168"
|
||||
/>
|
||||
<path strokeLinejoin="round" d="m7.5 10.5l2.5-2v7" />
|
||||
<path d="M12.5 13.75v-3.5a1.75 1.75 0 1 1 3.5 0v3.5a1.75 1.75 0 1 1-3.5 0Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</media-seek-forward-button>
|
||||
|
||||
<media-seek-forward-button class="media-button" seekoffset="90">
|
||||
<svg
|
||||
slot="icon"
|
||||
class="svg"
|
||||
width="256"
|
||||
height="256"
|
||||
viewBox="-65 -75 400 400"
|
||||
>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="m246.52 118l-88.19-56.13a12 12 0 0 0-12.18-.39A11.66 11.66 0 0 0 140 71.84v44.59L54.33 61.87a12 12 0 0 0-12.18-.39A11.66 11.66 0 0 0 36 71.84v112.32a11.66 11.66 0 0 0 6.15 10.36a12 12 0 0 0 12.18-.39L140 139.57v44.59a11.66 11.66 0 0 0 6.15 10.36a12 12 0 0 0 12.18-.39L246.52 138a11.81 11.81 0 0 0 0-19.94Zm-108.3 13.19L50 187.38a3.91 3.91 0 0 1-4 .13a3.76 3.76 0 0 1-2-3.35V71.84a3.76 3.76 0 0 1 2-3.35a4 4 0 0 1 1.91-.5a3.94 3.94 0 0 1 2.13.63l88.18 56.16a3.8 3.8 0 0 1 0 6.44Zm104 0L154 187.38a3.91 3.91 0 0 1-4 .13a3.76 3.76 0 0 1-2-3.35V71.84a3.76 3.76 0 0 1 2-3.35a4 4 0 0 1 1.91-.5a3.94 3.94 0 0 1 2.13.63l88.18 56.16a3.8 3.8 0 0 1 0 6.44Z"
|
||||
/>
|
||||
</svg>
|
||||
</media-seek-forward-button>
|
||||
|
||||
<!-- Settings Menu Button -->
|
||||
<style>
|
||||
media-settings-menu-button svg {
|
||||
transition: transform 0.1s cubic-bezier(0.4, 0, 1, 1);
|
||||
transform: rotateZ(0deg);
|
||||
}
|
||||
media-settings-menu-button[aria-expanded='true'] svg {
|
||||
transform: rotateZ(30deg);
|
||||
}
|
||||
</style>
|
||||
<media-settings-menu-button class="media-button">
|
||||
<svg slot="icon" viewBox="0 0 32 32">
|
||||
<use class="svg-shadow" xlink:href="#settings-icon"></use>
|
||||
<g id="settings-icon">
|
||||
<path
|
||||
d="M16 18C17.1046 18 18 17.1046 18 16C18 14.8954 17.1046 14 16 14C14.8954 14 14 14.8954 14 16C14 17.1046 14.8954 18 16 18Z"
|
||||
/>
|
||||
<path
|
||||
d="M21.0176 13.0362L20.9715 12.9531C20.8445 12.7239 20.7797 12.4629 20.784 12.1982L20.8049 10.8997C20.8092 10.6343 20.675 10.3874 20.4545 10.2549L18.5385 9.10362C18.3186 8.97143 18.0472 8.9738 17.8293 9.10981L16.7658 9.77382C16.5485 9.90953 16.2999 9.98121 16.0465 9.98121H15.9543C15.7004 9.98121 15.4513 9.90922 15.2336 9.77295L14.1652 9.10413C13.9467 8.96728 13.674 8.96518 13.4535 9.09864L11.5436 10.2545C11.3242 10.3873 11.1908 10.6336 11.1951 10.8981L11.216 12.1982C11.2203 12.4629 11.1555 12.7239 11.0285 12.9531L10.9831 13.0351C10.856 13.2645 10.6715 13.4535 10.4493 13.5819L9.36075 14.2109C9.13763 14.3398 8.99942 14.5851 9 14.8511L9.00501 17.152C9.00559 17.4163 9.1432 17.6597 9.36476 17.7883L10.4481 18.4167C10.671 18.546 10.8559 18.7364 10.9826 18.9673L11.0313 19.0559C11.1565 19.284 11.2203 19.5431 11.2161 19.8059L11.1951 21.1003C11.1908 21.3657 11.325 21.6126 11.5456 21.7452L13.4615 22.8964C13.6814 23.0286 13.9528 23.0262 14.1707 22.8902L15.2342 22.2262C15.4515 22.0905 15.7001 22.0188 15.9535 22.0188H16.0457C16.2996 22.0188 16.5487 22.0908 16.7664 22.227L17.8348 22.8959C18.0534 23.0327 18.326 23.0348 18.5465 22.9014L20.4564 21.7455C20.6758 21.6127 20.8092 21.3664 20.8049 21.1019L20.784 19.8018C20.7797 19.5371 20.8445 19.2761 20.9715 19.0469L21.0169 18.9649C21.144 18.7355 21.3285 18.5465 21.5507 18.4181L22.6393 17.7891C22.8624 17.6602 23.0006 17.4149 23 17.1489L22.995 14.848C22.9944 14.5837 22.8568 14.3403 22.6352 14.2117L21.5493 13.5818C21.328 13.4534 21.1442 13.2649 21.0176 13.0362Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</media-settings-menu-button>
|
||||
|
||||
<!-- PIP/Mini Player Button -->
|
||||
<style>
|
||||
media-controller:not([breakpointmd]) media-pip-button {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<media-pip-button class="media-button">
|
||||
<svg slot="icon" viewBox="0 0 32 32">
|
||||
<use class="svg-shadow" xlink:href="#pip-icon"></use>
|
||||
<g id="pip-icon">
|
||||
<path
|
||||
d="M12 22H9.77778C9.34822 22 9 21.6162 9 21.1429V10.8571C9 10.3838 9.34822 10 9.77778 10L22.2222 10C22.6518 10 23 10.3838 23 10.8571V12.5714"
|
||||
/>
|
||||
<path
|
||||
d="M15 21.5714V16.4286C15 16.1919 15.199 16 15.4444 16H22.5556C22.801 16 23 16.1919 23 16.4286V17V21.5714C23 21.8081 22.801 22 22.5556 22H20.3333H17.6667H15.4444C15.199 22 15 21.8081 15 21.5714Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</media-pip-button>
|
||||
|
||||
<!-- Airplay Button -->
|
||||
<media-airplay-button class="media-button">
|
||||
<svg viewBox="0 0 32 32" aria-hidden="true" slot="icon">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20.5 20h1.722c.43 0 .778-.32.778-.714v-8.572c0-.394-.348-.714-.778-.714H9.778c-.43 0-.778.32-.778.714v1.429"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.5 20H9.778c-.43 0-.778-.32-.778-.714v-8.572c0-.394.348-.714.778-.714h12.444c.43 0 .778.32.778.714v1.429"/>
|
||||
<path stroke-linejoin="round" d="m16 19 3.464 3.75h-6.928L16 19Z"/>
|
||||
</svg>
|
||||
</media-airplay-button>
|
||||
|
||||
<!-- Cast Button -->
|
||||
<media-cast-button class="media-button">
|
||||
<svg slot="icon" viewBox="0 0 32 32">
|
||||
<use class="svg-shadow" xlink:href="#cast-icon"></use>
|
||||
<g id="cast-icon">
|
||||
<path
|
||||
d="M18.5 21.833h4.167c.46 0 .833-.373.833-.833V11a.833.833 0 0 0-.833-.833H9.333A.833.833 0 0 0 8.5 11v1.111m0 8.056c.92 0 1.667.746 1.667 1.666M8.5 17.667a4.167 4.167 0 0 1 4.167 4.166"
|
||||
/>
|
||||
<path d="M8.5 15.167a6.667 6.667 0 0 1 6.667 6.666" />
|
||||
</g>
|
||||
</svg>
|
||||
</media-cast-button>
|
||||
|
||||
<!-- Fullscreen Button -->
|
||||
<style>
|
||||
/* Having trouble getting @property to work in the shadow dom
|
||||
to clean this up. Like https://codepen.io/luwes/pen/oNRyZyx */
|
||||
|
||||
media-fullscreen-button .fs-arrow {
|
||||
translate: 0% 0%;
|
||||
}
|
||||
media-fullscreen-button:hover .fs-arrow {
|
||||
animation: 0.35s up-left-bounce cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
media-fullscreen-button:hover #fs-enter-top,
|
||||
media-fullscreen-button:hover #fs-exit-bottom {
|
||||
animation-name: up-right-bounce;
|
||||
}
|
||||
|
||||
media-fullscreen-button:hover #fs-enter-bottom,
|
||||
media-fullscreen-button:hover #fs-exit-top {
|
||||
animation-name: down-left-bounce;
|
||||
}
|
||||
|
||||
@keyframes up-left-bounce {
|
||||
0% {
|
||||
translate: 0 0;
|
||||
}
|
||||
50% {
|
||||
translate: -4% -4%;
|
||||
}
|
||||
}
|
||||
@keyframes up-right-bounce {
|
||||
0% {
|
||||
translate: 0 0;
|
||||
}
|
||||
50% {
|
||||
translate: 4% -4%;
|
||||
}
|
||||
}
|
||||
@keyframes down-left-bounce {
|
||||
0% {
|
||||
translate: 0 0;
|
||||
}
|
||||
50% {
|
||||
translate: -4% 4%;
|
||||
}
|
||||
}
|
||||
@keyframes down-right-bounce {
|
||||
0% {
|
||||
translate: 0 0;
|
||||
}
|
||||
50% {
|
||||
translate: 4% 4%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<media-fullscreen-button class="media-button">
|
||||
<svg slot="enter" viewBox="0 0 32 32">
|
||||
<use class="svg-shadow" xlink:href="#fs-enter-paths"></use>
|
||||
<g id="fs-enter-paths">
|
||||
<g id="fs-enter-top" class="fs-arrow">
|
||||
<path d="M18 10H22V14" />
|
||||
<path d="M22 10L18 14" />
|
||||
</g>
|
||||
<g id="fs-enter-bottom" class="fs-arrow">
|
||||
<path d="M14 22L10 22V18" />
|
||||
<path d="M10 22L14 18" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg slot="exit" viewBox="0 0 32 32">
|
||||
<use class="svg-shadow" xlink:href="#fs-exit-paths"></use>
|
||||
<g id="fs-exit-paths">
|
||||
<g id="fs-exit-top" class="fs-arrow">
|
||||
<path d="M22 14H18V10" />
|
||||
<path d="M22 10L18 14" />
|
||||
</g>
|
||||
<g id="fs-exit-bottom" class="fs-arrow">
|
||||
<path d="M10 18L14 18V22" />
|
||||
<path d="M14 18L10 22" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</media-fullscreen-button>
|
||||
</media-control-bar>
|
||||
</media-controller>
|
||||
</template>
|
||||
`
|
||||
}
|
1453
api-prox/package-lock.json
generated
Normal file
1453
api-prox/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
20
api-prox/package.json
Normal file
20
api-prox/package.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "anix-api-proxy",
|
||||
"version": "1.0.0",
|
||||
"description": "Proxy and Hook requests from anix to anixart api",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"serve": "npx tsx ./index.ts"
|
||||
},
|
||||
"author": "Radiquum",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^24.0.4",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"tsx": "^4.20.3"
|
||||
}
|
||||
}
|
126
api-prox/shared.ts
Normal file
126
api-prox/shared.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
export const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
|
||||
"Cache-Control": "no-cache",
|
||||
};
|
||||
|
||||
export const resHeaders = {
|
||||
...corsHeaders,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
import { Request, Response } from "express";
|
||||
export function asJSON(res: Response, object: any, status: number) {
|
||||
res.status(status);
|
||||
res.set(resHeaders);
|
||||
res.send(JSON.stringify(object));
|
||||
}
|
||||
|
||||
export const ANIXART_UA =
|
||||
"AnixartApp/9.0 BETA 5-25062213 (Android 9; SDK 28; arm64-v8a; samsung SM-G975N; en)";
|
||||
export const ANIXART_API = "https://api.anixart.app";
|
||||
|
||||
export type ANIXART_HEADERST = {
|
||||
"User-Agent": string;
|
||||
"Content-Type": string;
|
||||
"Api-Version"?: string;
|
||||
};
|
||||
export const ANIXART_HEADERS: ANIXART_HEADERST = {
|
||||
"User-Agent": ANIXART_UA,
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
};
|
||||
|
||||
type LogLevel = "debug" | "info" | "warn" | "error" | "disable";
|
||||
export class Log {
|
||||
level: LogLevel;
|
||||
levelInt = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
disable: 4,
|
||||
};
|
||||
|
||||
constructor(level: LogLevel = "info") {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
getString(...args: string[]): string {
|
||||
return args.toString();
|
||||
}
|
||||
|
||||
getTime(): string {
|
||||
const datetime = new Date();
|
||||
return `${datetime.getHours().toString().padStart(2, "0")}:${datetime.getMinutes().toString().padStart(2, "0")}:${datetime.getSeconds().toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
console(logLevel: LogLevel = "info", ...msg: any[]) {
|
||||
if (this.levelInt[this.level] <= this.levelInt[logLevel])
|
||||
console.log(`[${logLevel.toUpperCase()}](${this.getTime()}) -> `, ...msg);
|
||||
}
|
||||
debug(...msg: string[]) {
|
||||
if (this.levelInt[this.level] <= 0)
|
||||
console.log(`[DEBUG](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||
}
|
||||
info(...msg: string[]) {
|
||||
if (this.levelInt[this.level] <= 1)
|
||||
console.log(`[INFO](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||
}
|
||||
warn(...msg: string[]) {
|
||||
if (this.levelInt[this.level] <= 2)
|
||||
console.log(`[WARN](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||
}
|
||||
error(...msg: string[]) {
|
||||
if (this.levelInt[this.level] <= 3)
|
||||
console.log(`[ERROR](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||
}
|
||||
|
||||
consoleHook(logLevel: LogLevel = "info", ...msg: any[]) {
|
||||
if (this.levelInt[this.level] <= this.levelInt[logLevel])
|
||||
console.log(
|
||||
`[${logLevel.toUpperCase()}|HOOK](${this.getTime()}) -> `,
|
||||
...msg
|
||||
);
|
||||
}
|
||||
debugHook(...msg: string[]) {
|
||||
if (this.levelInt[this.level] <= 0)
|
||||
console.log(
|
||||
`[DEBUG|HOOK](${this.getTime()}) -> ${this.getString(...msg)}`
|
||||
);
|
||||
}
|
||||
infoHook(...msg: string[]) {
|
||||
if (this.levelInt[this.level] <= 1)
|
||||
console.log(
|
||||
`[INFO|HOOK](${this.getTime()}) -> ${this.getString(...msg)}`
|
||||
);
|
||||
}
|
||||
warnHook(...msg: string[]) {
|
||||
if (this.levelInt[this.level] <= 2)
|
||||
console.log(
|
||||
`[WARN|HOOK](${this.getTime()}) -> ${this.getString(...msg)}`
|
||||
);
|
||||
}
|
||||
errorHook(...msg: string[]) {
|
||||
if (this.levelInt[this.level] <= 3)
|
||||
console.log(
|
||||
`[ERROR|HOOK](${this.getTime()}) -> ${this.getString(...msg)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Log((process.env.LOG_LEVEL as LogLevel) || "info");
|
||||
|
||||
export interface GetHook {
|
||||
match: (path: string) => boolean;
|
||||
get: (data: any, url: URL) => any;
|
||||
}
|
||||
|
||||
export interface PostHook {
|
||||
match: (path: string) => boolean;
|
||||
post: (body: any, url: URL) => any;
|
||||
}
|
||||
|
||||
export interface LoadedHook {
|
||||
path: string;
|
||||
mtime: string;
|
||||
}
|
113
api-prox/tsconfig.json
Normal file
113
api-prox/tsconfig.json
Normal file
|
@ -0,0 +1,113 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "libReplacement": true, /* Enable lib replacement. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
export const CURRENT_APP_VERSION = "3.7.0";
|
||||
import { env } from "next-runtime-env";
|
||||
|
||||
const NEXT_PUBLIC_API_URL = env("NEXT_PUBLIC_API_URL") || null;
|
||||
export const API_URL = "https://api.anixart.app";
|
||||
export const API_PREFIX = "/api/proxy";
|
||||
export const API_PREFIX = NEXT_PUBLIC_API_URL || "/api/proxy";
|
||||
export const USER_AGENT =
|
||||
"AnixartApp/9.0 BETA 5-25062213 (Android 9; SDK 28; arm64-v8a; samsung SM-G975N; en)";
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
node_modules
|
||||
README.md
|
||||
README.md
|
||||
README.RU.md
|
|
@ -33,5 +33,5 @@
|
|||
"**/*.tsx",
|
||||
"next.config.js"
|
||||
],
|
||||
"exclude": ["node_modules", "player-parsers"]
|
||||
"exclude": ["node_modules", "player-parsers", "api-prox"]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue