LunaTV Search Flow Architecture: How Multi-Source Video Search Works
LunaTV processes search queries through a pipelined architecture that authenticates users, queries multiple downstream APIs concurrently with per-site timeouts, caches results in-memory, filters yellow-listed content, and returns aggregated responses with HTTP cache headers—all orchestrated from the Next.js API route handler.
MoonTechLab/LunaTV implements a robust LunaTV search flow architecture designed to aggregate video content from multiple third-party sources efficiently. This Next.js application handles GET /api/search requests by orchestrating authentication, parallel downstream queries, intelligent caching, and content filtering into a seamless pipeline. Understanding this architecture reveals how the system maintains low latency while managing external API dependencies.
Entry Point and Request Validation
The search flow begins at src/app/api/search/route.ts, the Next.js API route handler that serves as the orchestration layer for all search operations. When a client issues GET /api/search?q={keyword}, the route first extracts authentication context using getAuthInfoFromCookie() to validate the requesting user.
The handler performs strict input validation: if the query parameter q is missing or empty, it returns an immediate short-lived empty result to prevent unnecessary processing. For valid requests, the route prepares for parallel execution by fetching the list of enabled downstream sources and initializing a Promise.race for each site call, capped with a 20-second timeout to prevent stalled responses.
// src/app/api/search/route.ts (simplified flow)
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');
if (!query) {
return new Response(JSON.stringify({ results: [] }), {
headers: { 'Cache-Control': 'max-age=60' }
});
}
const authInfo = getAuthInfoFromCookie(request);
const sites = await getAvailableApiSites(authInfo);
// Parallel search with timeout protection
const searches = sites.map(site =>
Promise.race([
searchFromApi(site, query),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 20000)
)
])
);
const results = await Promise.allSettled(searches);
// ... aggregation logic
}
Configuration Loading and Site Resolution
Before dispatching queries, the system loads runtime configuration via src/lib/config.ts. The getAvailableApiSites() function retrieves the cached admin configuration through getConfig(), then filters the SourceConfig array based on user-specific permissions such as enabledApis or tag mappings.
This module also supplies critical cache policies including SiteInterfaceCacheTime, which determines how long downstream API responses remain valid in the local cache. The configuration layer ensures that only authorized and active sources participate in the search, preventing wasted resources on disabled or restricted endpoints.
Parallel Downstream API Execution
The core aggregation logic resides in src/lib/downstream.ts, where searchFromApi() constructs the target API URL and delegates network operations to searchWithCache(). This architecture supports pagination through SearchDownstreamMaxPage, automatically fetching additional result pages when available and flattening them into a single SearchResult array.
Each downstream call operates independently, allowing the system to collect results from fast sources while timing out slow ones. The searchWithCache() function implements a default 8-second timeout for individual HTTP requests, ensuring that a single sluggish provider cannot degrade the overall user experience.
Result Transformation
Raw JSON responses undergo normalization before entering the aggregation pipeline. The system extracts key metadata including title, poster, year, description (processed through cleanHtmlTags() to strip HTML), and type_name. Episodes without playable URLs are automatically discarded during this phase to ensure result quality.
// From src/lib/downstream.ts
function transformToSearchResult(raw: any): SearchResult {
return {
title: raw.name,
poster: raw.pic,
year: parseInt(raw.year),
description: cleanHtmlTags(raw.content),
type_name: raw.type_name,
episodes: raw.episodes?.filter((ep: any) => ep.url?.length > 0) || []
};
}
In-Memory Caching Strategy
LunaTV employs an aggressive caching layer implemented in src/lib/search-cache.ts to minimize redundant network calls. The searchWithCache() function first checks getCachedSearchPage(siteKey, query, page) for a valid entry marked with status === 'ok'. Cache hits return immediately without touching the downstream API.
On cache misses, the system performs the HTTP request, then stores the response using setCachedSearchPage(). The cache implementation features automatic expiration based on TTL values from SiteInterfaceCacheTime and enforces size limits through automatic cleanup of stale entries. This design ensures that popular queries receive sub-millisecond responses while protecting upstream APIs from request spikes.
// src/lib/search-cache.ts usage pattern
const cached = getCachedSearchPage(site.key, keyword, page);
if (cached && cached.status === 'ok') {
return cached.data;
}
// Fetch from upstream
const fresh = await fetchWithTimeout(apiUrl, 8000);
setCachedSearchPage(site.key, keyword, page, fresh);
return fresh;
Content Filtering and Response Headers
After aggregating results via Promise.allSettled(), the route handler in src/app/api/search/route.ts optionally filters the combined dataset. If DisableYellowFilter is false in the configuration, it removes results whose type_name matches entries in the yellowWords list managed via src/lib/yellow.ts.
The final response includes optimized HTTP cache headers. The system calculates max-age values through getCacheTime(), applying them to both standard Cache-Control and CDN-Cache-Control headers. This allows edge networks to cache successful search results, further reducing origin load and improving global response times.
Summary
- Authentication-first design: Every search request validates the user via
getAuthInfoFromCookie()before processing. - Concurrent downstream queries: The architecture uses
Promise.race()with a 20-second timeout to query multiple API sources simultaneously without blocking on slow responses. - Intelligent caching:
search-cache.tsprovides in-memory storage with TTL enforcement, reducing latency for repeated queries and protecting upstream rate limits. - Defensive timeouts: Per-call network timeouts (8 seconds) and overall race timeouts (20 seconds) ensure predictable performance regardless of external API health.
- Configurable filtering: Yellow-word filtering can be toggled via
DisableYellowFilter, with sensitive content removed before the response reaches the client.
Frequently Asked Questions
How does LunaTV handle slow or unresponsive downstream APIs?
LunaTV implements a dual-timeout strategy defined in src/app/api/search/route.ts and src/lib/search-cache.ts. Each individual network call uses an 8-second timeout within searchWithCache(), while the orchestration layer wraps each site query in a Promise.race() with a 20-second ceiling. If a downstream API fails to respond within these windows, the promise rejects and Promise.allSettled() collects the error, allowing successful results from other sources to return to the client without delay.
What caching mechanisms does LunaTV use for search results?
The platform utilizes an in-memory cache system implemented in src/lib/search-cache.ts. The getCachedSearchPage() function checks for valid cached entries before issuing network requests, while setCachedSearchPage() stores successful responses with configurable TTL values from SiteInterfaceCacheTime. The cache automatically expires stale entries and enforces size limits, ensuring that frequently accessed queries return instantly while maintaining memory efficiency.
Can the content filtering be disabled in the LunaTV search flow?
Yes. The yellow-word filtering mechanism can be disabled via the DisableYellowFilter configuration flag in the admin settings. When enabled, the filter iterates through aggregated results in src/app/api/search/route.ts and removes items where type_name contains words from the yellowWords array. Setting this flag to true bypasses the filtering logic entirely, returning all aggregated results regardless of content classification.
How does LunaTV determine which downstream sources to query for a specific user?
Source selection occurs in src/lib/config.ts through the getAvailableApiSites() function. This utility loads the global configuration via getConfig() and filters the SourceConfig array based on the authenticated user's enabledApis preferences or tag mappings. This per-user resolution ensures that administrators can control API access granularly, restricting certain downstream sources to specific user tiers or regions while maintaining a unified search interface.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →