mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
218 lines
6.1 KiB
TypeScript
218 lines
6.1 KiB
TypeScript
import { Router, Request, Response, NextFunction } from 'express';
|
|
import {
|
|
AIOStreams,
|
|
AIOStreamResponse,
|
|
Env,
|
|
UserData,
|
|
UserRepository,
|
|
APIError,
|
|
constants,
|
|
formatZodError,
|
|
validateConfig,
|
|
isEncrypted,
|
|
decryptString,
|
|
createLogger,
|
|
ApiTransformer,
|
|
SearchApiResponseData,
|
|
SearchApiResultField,
|
|
StremioTransformer,
|
|
} from '@aiostreams/core';
|
|
import { streamApiRateLimiter } from '../../middlewares/ratelimit.js';
|
|
import { ApiResponse, createResponse } from '../../utils/responses.js';
|
|
import { z, ZodError } from 'zod';
|
|
const router: Router = Router();
|
|
|
|
const logger = createLogger('server');
|
|
|
|
router.use(streamApiRateLimiter);
|
|
|
|
const SearchApiRequestSchema = z.object({
|
|
type: z.string(),
|
|
id: z.string(),
|
|
format: z.coerce.boolean().optional().default(false),
|
|
requiredFields: z
|
|
.union([z.array(SearchApiResultField), SearchApiResultField])
|
|
.optional()
|
|
.default([])
|
|
.transform((val) => {
|
|
if (Array.isArray(val)) {
|
|
return val;
|
|
}
|
|
return [val];
|
|
}),
|
|
});
|
|
|
|
router.get(
|
|
'/',
|
|
async (
|
|
req: Request,
|
|
res: Response<ApiResponse<SearchApiResponseData>>,
|
|
next: NextFunction
|
|
) => {
|
|
try {
|
|
const { type, id, requiredFields, format } = SearchApiRequestSchema.parse(
|
|
req.query
|
|
);
|
|
let encodedUserData: string | undefined = z
|
|
.string()
|
|
.optional()
|
|
.parse(req.headers['x-aiostreams-user-data']);
|
|
let auth: string | undefined = z
|
|
.string()
|
|
.optional()
|
|
.parse(req.headers['authorization']);
|
|
|
|
if (!encodedUserData && !auth) {
|
|
throw new APIError(
|
|
constants.ErrorCode.UNAUTHORIZED,
|
|
undefined,
|
|
`At least one of AIOStreams-User-Data or Authorization headers must be present`
|
|
);
|
|
}
|
|
|
|
let userData: UserData | null = null;
|
|
|
|
if (encodedUserData) {
|
|
try {
|
|
userData = JSON.parse(
|
|
Buffer.from(encodedUserData, 'base64').toString('utf-8')
|
|
);
|
|
if (userData) {
|
|
userData.trusted = false;
|
|
logger.debug(`Using encodedUserData for Search API request`);
|
|
}
|
|
} catch (error: any) {
|
|
throw new APIError(
|
|
constants.ErrorCode.BAD_REQUEST,
|
|
undefined,
|
|
`Invalid encodedUserData: ${error.message}`
|
|
);
|
|
}
|
|
} else if (auth) {
|
|
let uuid: string;
|
|
let password: string;
|
|
try {
|
|
if (!auth.startsWith('Basic ')) {
|
|
throw new APIError(
|
|
constants.ErrorCode.BAD_REQUEST,
|
|
undefined,
|
|
`Invalid auth: ${auth}. Must start with 'Basic '`
|
|
);
|
|
}
|
|
const base64Credentials = auth.slice('Basic '.length).trim();
|
|
const credentials = Buffer.from(base64Credentials, 'base64').toString(
|
|
'utf-8'
|
|
);
|
|
const sepIndex = credentials.indexOf(':');
|
|
if (sepIndex === -1) {
|
|
throw new APIError(
|
|
constants.ErrorCode.BAD_REQUEST,
|
|
undefined,
|
|
`Invalid basic auth format`
|
|
);
|
|
}
|
|
uuid = credentials.slice(0, sepIndex);
|
|
password = credentials.slice(sepIndex + 1);
|
|
if (!uuid || !password) {
|
|
throw new APIError(
|
|
constants.ErrorCode.BAD_REQUEST,
|
|
undefined,
|
|
`Missing username or password in basic auth`
|
|
);
|
|
}
|
|
if (isEncrypted(password)) {
|
|
const {
|
|
success: successfulDecryption,
|
|
data: decryptedPassword,
|
|
error,
|
|
} = decryptString(password);
|
|
if (!successfulDecryption) {
|
|
next(
|
|
new APIError(
|
|
constants.ErrorCode.ENCRYPTION_ERROR,
|
|
undefined,
|
|
error
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
password = decryptedPassword;
|
|
}
|
|
logger.debug(`Using basic auth for Search API request: ${uuid}`);
|
|
} catch (error: any) {
|
|
throw new APIError(
|
|
constants.ErrorCode.BAD_REQUEST,
|
|
undefined,
|
|
`Invalid auth: ${error.message}`
|
|
);
|
|
}
|
|
const userExists = await UserRepository.checkUserExists(uuid);
|
|
if (!userExists) {
|
|
throw new APIError(constants.ErrorCode.USER_INVALID_DETAILS);
|
|
}
|
|
|
|
userData = await UserRepository.getUser(uuid, password);
|
|
|
|
if (!userData) {
|
|
throw new APIError(constants.ErrorCode.USER_INVALID_DETAILS);
|
|
}
|
|
}
|
|
if (!userData) {
|
|
throw new APIError(constants.ErrorCode.USER_INVALID_DETAILS);
|
|
}
|
|
userData.ip = req.userIp;
|
|
try {
|
|
userData = await validateConfig(userData, {
|
|
skipErrorsFromAddonsOrProxies: true,
|
|
decryptValues: true,
|
|
});
|
|
} catch (error: any) {
|
|
throw new APIError(
|
|
constants.ErrorCode.USER_INVALID_CONFIG,
|
|
undefined,
|
|
error.message
|
|
);
|
|
}
|
|
const transformer = new ApiTransformer(userData);
|
|
const stremioTransformer = format
|
|
? new StremioTransformer(userData)
|
|
: null;
|
|
const response = await (
|
|
await new AIOStreams(userData).initialise()
|
|
).getStreams(id, type);
|
|
|
|
const stremioData = await stremioTransformer?.transformStreams(response);
|
|
const stremioStreams = stremioData?.streams.filter(
|
|
(stream) =>
|
|
!['statistic', 'error'].includes(stream.streamData?.type || '')
|
|
);
|
|
|
|
const apiData = await transformer.transformStreams(
|
|
response,
|
|
requiredFields
|
|
);
|
|
if (stremioStreams && format) {
|
|
apiData.results = apiData.results.map((result, index) => {
|
|
const stream = stremioStreams[index];
|
|
return {
|
|
...result,
|
|
name: stream?.name,
|
|
description: stream?.description,
|
|
};
|
|
});
|
|
}
|
|
|
|
res.status(200).json(
|
|
createResponse<SearchApiResponseData>({
|
|
success: true,
|
|
data: apiData,
|
|
})
|
|
);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
);
|
|
|
|
export default router;
|