How to Integrate Douban Data with LunaTV: A Complete Guide Using the Core Fetch Client

Integrating Douban data with LunaTV requires importing the fetchDoubanData helper from src/lib/douban.ts and calling it with a valid Douban API URL to retrieve typed movie and TV show information.

The MoonTechLab/LunaTV repository provides a lightweight, reusable client for consuming Douban's public endpoints. This client wraps the native fetch API with proper headers, timeout handling, and TypeScript generics, making it straightforward to pull movie metadata, recommendations, and category listings into your application.

Understanding the Douban Client Architecture

Core Fetch Helper in src/lib/douban.ts

The heart of the integration lives in src/lib/douban.ts, which exports a generic fetchDoubanData<T>() function. This utility configures a 10-second timeout, sets browser-like headers to satisfy Douban's CORS policies, and returns a strongly-typed JSON response.

// src/lib/douban.ts
export async function fetchDoubanData<T>(url: string): Promise<T> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 10_000);
  
  const fetchOptions = {
    signal: controller.signal,
    headers: {
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
      'Referer': 'https://movie.douban.com/',
      'Accept': 'application/json, text/plain, */*',
      'Origin': 'https://movie.douban.com',
    },
  };

  try {
    const response = await fetch(url, fetchOptions);
    clearTimeout(timeoutId);
    
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    clearTimeout(timeoutId);
    throw error;
  }
}

The function accepts a generic type parameter T, allowing callers to specify the expected response shape at compile time. It handles network timeouts via AbortController and clears the timer in both success and error paths to prevent memory leaks.

Built-in API Routes for Douban Integration

LunaTV exposes server-side endpoints under src/app/api/douban/ that act as thin wrappers around the core fetch helper. These routes centralize Douban-specific logic and provide clean URLs for frontend consumption.

Generic Proxy Endpoint (src/app/api/douban/route.ts)

The base route forwards requests to Douban while reusing the shared client configuration. It accepts a target URL or ID parameter, delegates the actual HTTP call to fetchDoubanData, and streams the JSON response back to the caller.

Recommendations Endpoint (src/app/api/douban/recommends/route.ts)

This route implements Douban's recommendation feed (e.g., https://movie.douban.com/j/radio/people). It constructs the query string, invokes fetchDoubanData with an array type, and returns a curated list of recommended movies based on user preferences.

Categories Endpoint (src/app/api/douban/categories/route.ts)

Located at the same directory level, this endpoint fetches Douban's genre taxonomy—such as "剧情" (Drama) or "喜剧" (Comedy)—by calling the appropriate Douban JSON endpoint through the shared helper and returning the parsed category list.

Step-by-Step Integration Guide

Step 1: Import the Fetch Helper

Begin by importing the client into your server-side route or utility file. The helper is framework-agnostic and works in Next.js API routes, React Server Components, or Node.js scripts.

import { fetchDoubanData } from '@/lib/douban';

Step 2: Define TypeScript Interfaces

Create interfaces that match Douban's JSON structure to enable type safety and IntelliSense. Place these in src/lib/types.ts or a dedicated douban.types.ts file.

// src/lib/douban.types.ts
export interface DoubanMovie {
  id: string;
  title: string;
  rating: {
    value: string;
    count: number;
  };
  year: string;
  genres: string[];
  abstracts?: string;
}

export interface DoubanRecommendation {
  id: string;
  title: string;
  image: string;
  recommendation_reason: string;
}

Step 3: Call the Douban API

Use the helper to fetch specific resources by constructing the appropriate Douban URL. Always pass your defined interface as the generic parameter.

// Example: Fetching a specific movie's details
import { fetchDoubanData } from '@/lib/douban';
import type { DoubanMovie } from '@/lib/douban.types';

export async function getMovieById(id: string): Promise<DoubanMovie> {
  const url = `https://movie.douban.com/j/subject_abstract?subject_id=${id}`;
  
  return await fetchDoubanData<DoubanMovie>(url);
}

For recommendations, point to the built-in endpoint or call Douban directly:

import { fetchDoubanData } from '@/lib/douban';
import type { DoubanRecommendation } from '@/lib/douban.types';

export async function getRecommendations(userId: string): Promise<DoubanRecommendation[]> {
  const url = `https://movie.douban.com/j/radio/people?user_id=${userId}&type=rec`;
  
  return await fetchDoubanData<DoubanRecommendation[]>(url);
}

Step 4: Consume from the Frontend

Call your internal API routes from React components using standard fetch or data-fetching libraries like SWR.

// components/RecommendationList.tsx
import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then(res => res.json());

export default function RecommendationList({ userId }: { userId: string }) {
  const { data, error } = useSWR(
    `/api/douban/recommends?uid=${userId}`,
    fetcher
  );

  if (error) return <div>Failed to load recommendations</div>;
  if (!data) return <div>Loading...</div>;

  return (
    <ul>
      {data.map((movie: any) => (
        <li key={movie.id}>{movie.title}</li>
      ))}
    </ul>
  );
}

Extending the Douban Integration

Adding Custom Endpoints

To integrate additional Douban features (such as search or reviews), create a new folder under src/app/api/douban/ with a route.ts file. Import fetchDoubanData, construct the target URL, and return the processed result.

// src/app/api/douban/search/route.ts
import { fetchDoubanData } from '@/lib/douban';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const query = searchParams.get('q');
  
  if (!query) {
    return NextResponse.json({ error: 'Missing query' }, { status: 400 });
  }

  const doubanUrl = `https://movie.douban.com/j/subject_suggest?q=${encodeURIComponent(query)}`;
  const results = await fetchDoubanData<any[]>(doubanUrl);
  
  return NextResponse.json(results);
}

Implementing Response Caching

For high-traffic scenarios, wrap fetchDoubanData with the existing cache utility in src/lib/search-cache.ts. This memoizes frequent calls (such as popular movie details) to reduce latency and respect Douban's rate limits.

import { fetchDoubanData } from '@/lib/douban';
import { cache } from '@/lib/search-cache';

export const getCachedMovie = cache(
  async (id: string) => {
    return await fetchDoubanData(`https://movie.douban.com/j/subject_abstract?subject_id=${id}`);
  },
  { ttl: 3600 } // Cache for 1 hour
);

Summary

  • The fetchDoubanData function in src/lib/douban.ts provides the foundational HTTP client with timeout handling and browser headers required to communicate with Douban's endpoints.
  • Built-in API routes under src/app/api/douban/ offer ready-to-use endpoints for recommendations and categories, all consuming the core helper.
  • Type-safe integration is achieved by passing generic type parameters when calling the fetch helper, ensuring compile-time validation of Douban's JSON responses.
  • Extension points exist for adding custom search endpoints or implementing caching layers using existing utilities like src/lib/search-cache.ts.

Frequently Asked Questions

How do I handle Douban API rate limits in LunaTV?

The fetchDoubanData client does not implement built-in rate limiting, but you can mitigate throttling by wrapping calls with the cache utility from src/lib/search-cache.ts to memoize responses. For production deployments, implement a request queue or proxy middleware that adds exponential backoff between retries.

Can I use the Douban client on the client side (browser)?

While fetchDoubanData relies on standard fetch and can technically run in browsers, Douban's CORS policies typically block direct cross-origin requests from frontend code. As implemented in LunaTV, you should route requests through the server-side API endpoints under src/app/api/douban/ to avoid CORS errors.

What data format does fetchDoubanData return?

The function returns a Promise resolving to the parsed JSON object. By supplying a generic type parameter (e.g., fetchDoubanData<DoubanMovie>(url)), you instruct TypeScript to treat the response as that specific interface, though the actual runtime value depends on Douban's live API response structure.

Where should I define TypeScript interfaces for Douban responses?

Define interfaces in a dedicated file such as src/lib/douban.types.ts or extend the existing src/lib/types.ts. Import these types wherever you invoke fetchDoubanData to maintain type safety across your integration points.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →