f4b404's picture
Upload folder using huggingface_hub
f55c62a verified
Raw
History Blame Contribute Delete
8.94 kB
import express, { Request, Response, Express } from 'express';
import {
userApi,
healthApi,
statusApi,
formatApi,
catalogApi,
postersApi,
gdriveApi,
debridApi,
searchApi,
animeApi,
proxyApi,
templatesApi,
syncApi,
authApi,
dashboardApi,
} from './routes/api/index.js';
import {
configure,
manifest,
stream,
catalog,
meta,
subtitle,
addonCatalog,
alias,
} from './routes/stremio/index.js';
import {
manifest as chillLinkManifest,
streams as chillLinkStreams,
} from './routes/chilllink/index.js';
import seanimeExtensionsRouter from './routes/seanime/extensions.js';
import {
gdrive,
torboxSearch,
torznab,
newznab,
prowlarr,
knaben,
eztv,
torrentGalaxy,
seadex,
easynews,
library,
} from './routes/builtins/index.js';
import {
ipMiddleware,
loggerMiddleware,
userDataMiddleware,
errorMiddleware,
corsMiddleware,
staticRateLimiter,
internalMiddleware,
stremioStreamRateLimiter,
requireSessionIfAuthRequired,
} from './middlewares/index.js';
import {
config as appConfig,
constants,
createLogger,
Env,
} from '@aiostreams/core';
import { StremioTransformer } from '@aiostreams/core';
import { createResponse } from './utils/responses.js';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const app: Express = express();
const logger = createLogger('server');
export enum StaticFiles {
DOWNLOAD_FAILED = 'download_failed.mp4',
DOWNLOADING = 'downloading.mp4',
UNAVAILABLE_FOR_LEGAL_REASONS = 'unavailable_for_legal_reasons.mp4',
STORE_LIMIT_EXCEEDED = 'store_limit_exceeded.mp4',
CONTENT_PROXY_LIMIT_REACHED = 'content_proxy_limit_reached.mp4',
INTERNAL_SERVER_ERROR = '500.mp4',
TOO_MANY_REQUESTS = '429.mp4',
FORBIDDEN = '403.mp4',
UNAUTHORIZED = '401.mp4',
NO_MATCHING_FILE = 'no_matching_file.mp4',
PAYMENT_REQUIRED = 'payment_required.mp4',
OK = '200.mp4',
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const frontendRoot = path.join(__dirname, '../../frontend/dist');
export const staticRoot = path.join(__dirname, './static');
app.use(ipMiddleware);
app.use(loggerMiddleware);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Allow all origins in development for easier testing
if (appConfig.bootstrap.nodeEnv === 'development') {
logger.info('CORS enabled for all origins in development');
app.use(corsMiddleware);
}
// API Routes
const apiRouter = express.Router();
apiRouter.use('/user', userApi);
apiRouter.use('/health', healthApi);
apiRouter.use('/status', statusApi);
apiRouter.use('/format', formatApi);
apiRouter.use('/catalogs', catalogApi);
apiRouter.use('/posters', postersApi);
apiRouter.use('/oauth/exchange/gdrive', gdriveApi);
apiRouter.use('/debrid', debridApi);
apiRouter.use(
'/search',
(req, res, next) => {
if (!appConfig.api.enableSearchApi) {
res.status(403).json({ error: 'Search API is disabled', success: false });
return;
}
next();
},
searchApi
);
apiRouter.use('/anime', animeApi);
apiRouter.use('/proxy', proxyApi);
apiRouter.use('/templates', templatesApi);
apiRouter.use('/sync', syncApi);
apiRouter.use('/auth', authApi);
apiRouter.use('/dashboard', dashboardApi);
apiRouter.use((req, res) => {
res.status(404).json(
createResponse({
success: false,
detail: 'Not Found',
})
);
});
app.use(`/api/v${constants.API_VERSION}`, apiRouter);
// Stremio Routes
const stremioRouter = express.Router({ mergeParams: true });
stremioRouter.use(corsMiddleware);
// Public routes - no auth needed
stremioRouter.use('/manifest.json', manifest);
stremioRouter.use('/stream', stream);
stremioRouter.use('/configure', requireSessionIfAuthRequired, configure);
stremioRouter.use('/u', alias);
// Protected routes with authentication
const stremioAuthRouter = express.Router({ mergeParams: true });
stremioAuthRouter.use(corsMiddleware);
stremioAuthRouter.use(userDataMiddleware);
stremioAuthRouter.use('/manifest.json', manifest);
stremioAuthRouter.use('/stream', stream);
stremioAuthRouter.use('/configure', requireSessionIfAuthRequired, configure);
stremioAuthRouter.use('/meta', meta);
stremioAuthRouter.use('/catalog', catalog);
stremioAuthRouter.use('/subtitles', subtitle);
stremioAuthRouter.use('/addon_catalog', addonCatalog);
app.use('/stremio', stremioRouter); // For public routes
app.use('/stremio/:uuid/:encryptedPassword', stremioAuthRouter); // For authenticated routes
const chillLinkRouter = express.Router({ mergeParams: true });
chillLinkRouter.use(corsMiddleware);
chillLinkRouter.use(userDataMiddleware);
chillLinkRouter.use('/manifest', chillLinkManifest);
chillLinkRouter.use('/streams', chillLinkStreams);
app.use('/chilllink/:uuid/:encryptedPassword', chillLinkRouter);
const seanimeRouter = express.Router({ mergeParams: true });
seanimeRouter.use(corsMiddleware);
seanimeRouter.use(seanimeExtensionsRouter);
app.use('/seanime', seanimeRouter);
const builtinsRouter = express.Router();
builtinsRouter.use(internalMiddleware);
builtinsRouter.use('/gdrive', gdrive);
builtinsRouter.use('/torbox-search', torboxSearch);
builtinsRouter.use('/torznab', torznab);
builtinsRouter.use('/newznab', newznab);
builtinsRouter.use('/prowlarr', prowlarr);
builtinsRouter.use('/knaben', knaben);
builtinsRouter.use('/eztv', eztv);
builtinsRouter.use('/torrent-galaxy', torrentGalaxy);
builtinsRouter.use('/seadex', seadex);
builtinsRouter.use('/easynews', easynews);
builtinsRouter.use('/library', library);
app.use('/builtins', builtinsRouter);
// Content-hashed build assets. These filenames change on every content
// change, so they are immutable and safe to cache aggressively. Deliberately
// NOT behind staticRateLimiter: a single page load pulls many of these and
// rate-limiting them is what caused asset fetch failures + the logo flash.
app.get('/assets/*any', (req, res, next) => {
const filePath = path.resolve(frontendRoot, req.path.replace(/^\//, ''));
if (filePath.startsWith(frontendRoot) && fs.existsSync(filePath)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
res.sendFile(filePath);
return;
}
next();
});
// Root-level static files (not content-hashed). Short cache; kept behind the
// static rate limiter. The logo honours the alternate-design branding flag.
app.get('/logo.png', staticRateLimiter, (req, res, next) => {
const filePath = path.resolve(
frontendRoot,
appConfig.branding.alternateDesign ? 'logo_alt.png' : 'logo.png'
);
if (filePath.startsWith(frontendRoot) && fs.existsSync(filePath)) {
res.setHeader('Cache-Control', 'public, max-age=3600');
res.sendFile(filePath);
return;
}
next();
});
app.get(
[
'/favicon.ico',
'/manifest.json',
'/web-app-manifest-192x192.png',
'/web-app-manifest-512x512.png',
'/apple-icon.png',
'/mini-nightly-white.png',
'/mini-stable-white.png',
'/icon0.svg',
'/icon1.png',
'/logo_alt.png',
],
staticRateLimiter,
(req, res, next) => {
const filePath = path.resolve(frontendRoot, req.path.replace(/^\//, ''));
if (filePath.startsWith(frontendRoot) && fs.existsSync(filePath)) {
res.setHeader('Cache-Control', 'public, max-age=3600');
res.sendFile(filePath);
return;
}
next();
}
);
app.get('/static/*any', corsMiddleware, (req, res, next) => {
const filePath = path.resolve(
staticRoot,
req.path.replace(/^\/static\//, '')
);
logger.debug(`Static file requested: ${filePath}`);
if (filePath.startsWith(staticRoot) && fs.existsSync(filePath)) {
res.sendFile(filePath);
return;
}
next();
});
// legacy route handlers
app.get(
'{/:config}/stream/:type/:id.json',
stremioStreamRateLimiter,
(req, res) => {
const baseUrl =
appConfig.bootstrap.baseUrl ||
`${req.protocol}://${req.hostname}${
req.hostname === 'localhost' ? `:${appConfig.bootstrap.port}` : ''
}`;
res.json({
streams: [
StremioTransformer.createErrorStream({
errorDescription:
'AIOStreams v2 requires you to reconfigure. Please click this stream to reconfigure.',
errorUrl: `${baseUrl}/stremio/configure`,
}),
],
});
}
);
// redirect for legacy paths
app.get('{/:config}/configure', (req, res) => {
res.redirect('/stremio/configure');
});
app.get('/configure', (req, res) => {
res.redirect('/stremio/configure');
});
// SPA fallback.
app.get('*splat', staticRateLimiter, (req, res, next) => {
if (req.method !== 'GET' || !req.accepts('html')) {
next();
return;
}
const indexPath = path.join(frontendRoot, 'index.html');
if (fs.existsSync(indexPath)) {
res.setHeader('Cache-Control', 'no-cache');
res.sendFile(indexPath);
return;
}
next();
});
// Error handling middleware should be last
app.use(errorMiddleware);
export default app;