ddys-nextjs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +196 -0
  4. package/README.zh-CN.md +196 -0
  5. package/examples/app-router/app/api/ddys/[route]/route.ts +1 -0
  6. package/examples/app-router/app/api/ddys/diagnostics/route.ts +1 -0
  7. package/examples/app-router/app/api/ddys/request/route.ts +1 -0
  8. package/examples/app-router/app/api/ddys/revalidate/route.ts +1 -0
  9. package/examples/app-router/app/ddys/calendar/page.tsx +5 -0
  10. package/examples/app-router/app/ddys/collections/page.tsx +5 -0
  11. package/examples/app-router/app/ddys/diagnostics/page.tsx +5 -0
  12. package/examples/app-router/app/ddys/genres/page.tsx +5 -0
  13. package/examples/app-router/app/ddys/hot/page.tsx +5 -0
  14. package/examples/app-router/app/ddys/latest/page.tsx +5 -0
  15. package/examples/app-router/app/ddys/layout.tsx +13 -0
  16. package/examples/app-router/app/ddys/movie/[slug]/page.tsx +15 -0
  17. package/examples/app-router/app/ddys/movie/[slug]/sources/page.tsx +12 -0
  18. package/examples/app-router/app/ddys/movies/page.tsx +5 -0
  19. package/examples/app-router/app/ddys/regions/page.tsx +5 -0
  20. package/examples/app-router/app/ddys/request/page.tsx +8 -0
  21. package/examples/app-router/app/ddys/search/page.tsx +5 -0
  22. package/examples/app-router/app/ddys/shares/page.tsx +5 -0
  23. package/examples/app-router/app/ddys/types/page.tsx +5 -0
  24. package/examples/app-router/app/manifest.ts +9 -0
  25. package/examples/app-router/app/robots.ts +5 -0
  26. package/examples/app-router/app/sitemap.ts +7 -0
  27. package/next.config.example.mjs +9 -0
  28. package/package.json +105 -0
  29. package/public/images/icon-16.png +0 -0
  30. package/public/images/icon-192.png +0 -0
  31. package/public/images/icon-32.png +0 -0
  32. package/public/images/icon-512.png +0 -0
  33. package/public/images/logo.png +0 -0
  34. package/src/actions/index.ts +2 -0
  35. package/src/actions/request.ts +26 -0
  36. package/src/actions/revalidate.ts +15 -0
  37. package/src/client/client.ts +194 -0
  38. package/src/client/config.ts +111 -0
  39. package/src/client/error.ts +15 -0
  40. package/src/client/index.ts +13 -0
  41. package/src/components/card.tsx +38 -0
  42. package/src/components/client.ts +3 -0
  43. package/src/components/diagnostics.tsx +35 -0
  44. package/src/components/grid.tsx +27 -0
  45. package/src/components/index.ts +8 -0
  46. package/src/components/movie-detail.tsx +40 -0
  47. package/src/components/request-form.tsx +50 -0
  48. package/src/components/search.tsx +41 -0
  49. package/src/components/sources.tsx +27 -0
  50. package/src/components/utils.ts +50 -0
  51. package/src/components/view.tsx +50 -0
  52. package/src/index.ts +2 -0
  53. package/src/metadata/index.ts +273 -0
  54. package/src/route-handlers/diagnostics.ts +61 -0
  55. package/src/route-handlers/index.ts +10 -0
  56. package/src/route-handlers/proxy.ts +42 -0
  57. package/src/route-handlers/request.ts +46 -0
  58. package/src/route-handlers/revalidate.ts +31 -0
  59. package/src/server/cache.ts +45 -0
  60. package/src/server/client.ts +15 -0
  61. package/src/server/config.ts +49 -0
  62. package/src/server/index.ts +11 -0
  63. package/src/server/request-service.ts +105 -0
  64. package/src/styles/ddys.css +228 -0
  65. package/src/types/ddys.ts +99 -0
  66. package/src/utils/security.ts +125 -0
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "ddys-nextjs",
3
+ "version": "0.1.0",
4
+ "description": "Official Next.js integration for the DDYS API with App Router pages, Server Components, Route Handlers, metadata, caching, diagnostics, and request form.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://ddys.io",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/ddysiodev/ddys-nextjs.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/ddysiodev/ddys-nextjs/issues"
17
+ },
18
+ "keywords": [
19
+ "ddys",
20
+ "nextjs",
21
+ "next",
22
+ "react",
23
+ "app-router",
24
+ "server-components",
25
+ "route-handlers",
26
+ "metadata",
27
+ "seo",
28
+ "movies",
29
+ "video",
30
+ "api"
31
+ ],
32
+ "sideEffects": [
33
+ "./src/styles/ddys.css"
34
+ ],
35
+ "exports": {
36
+ ".": {
37
+ "types": "./src/index.ts",
38
+ "import": "./src/index.ts"
39
+ },
40
+ "./client": {
41
+ "types": "./src/client/index.ts",
42
+ "import": "./src/client/index.ts"
43
+ },
44
+ "./server": {
45
+ "types": "./src/server/index.ts",
46
+ "import": "./src/server/index.ts"
47
+ },
48
+ "./components": {
49
+ "types": "./src/components/index.ts",
50
+ "import": "./src/components/index.ts"
51
+ },
52
+ "./components/client": {
53
+ "types": "./src/components/client.ts",
54
+ "import": "./src/components/client.ts"
55
+ },
56
+ "./actions": {
57
+ "types": "./src/actions/index.ts",
58
+ "import": "./src/actions/index.ts"
59
+ },
60
+ "./route-handlers": {
61
+ "types": "./src/route-handlers/index.ts",
62
+ "import": "./src/route-handlers/index.ts"
63
+ },
64
+ "./metadata": {
65
+ "types": "./src/metadata/index.ts",
66
+ "import": "./src/metadata/index.ts"
67
+ },
68
+ "./styles.css": "./src/styles/ddys.css"
69
+ },
70
+ "files": [
71
+ "src",
72
+ "examples",
73
+ "public",
74
+ "README.md",
75
+ "README.zh-CN.md",
76
+ "LICENSE",
77
+ ".env.example",
78
+ "next.config.example.mjs"
79
+ ],
80
+ "peerDependencies": {
81
+ "next": ">=14.2.0",
82
+ "react": ">=18.2.0 || >=19.0.0",
83
+ "react-dom": ">=18.2.0 || >=19.0.0"
84
+ },
85
+ "dependencies": {
86
+ "server-only": "^0.0.1"
87
+ },
88
+ "devDependencies": {
89
+ "@types/node": "^22.10.0",
90
+ "@types/react": "^19.0.0",
91
+ "@types/react-dom": "^19.0.0",
92
+ "next": "^16.0.0",
93
+ "react": "^19.0.0",
94
+ "react-dom": "^19.0.0",
95
+ "typescript": "^5.7.0"
96
+ },
97
+ "engines": {
98
+ "node": ">=20.0.0"
99
+ },
100
+ "scripts": {
101
+ "check": "node tools/check.mjs",
102
+ "test": "node tools/check.mjs && node --test tests/*.test.mjs",
103
+ "typecheck": "tsc --noEmit"
104
+ }
105
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,2 @@
1
+ export { submitDdysRequestAction, type DdysActionState } from './request';
2
+ export { revalidateDdysAction } from './revalidate';
@@ -0,0 +1,26 @@
1
+ 'use server';
2
+
3
+ import type { DdysConfigInput } from '../client/config';
4
+ import { getDdysConfigFromEnv } from '../server/config';
5
+ import { submitDdysRequest } from '../server/request-service';
6
+ import { formDataToObject } from '../utils/security';
7
+
8
+ export interface DdysActionState {
9
+ success: boolean;
10
+ message: string;
11
+ data?: unknown;
12
+ }
13
+
14
+ export async function submitDdysRequestAction(_state: DdysActionState, formData: FormData, configInput: DdysConfigInput = {}): Promise<DdysActionState> {
15
+ const config = getDdysConfigFromEnv(configInput);
16
+ const input = formDataToObject(formData);
17
+ try {
18
+ const data = await submitDdysRequest(input, config, {
19
+ identity: input.ddys_identity || 'anonymous',
20
+ token: input.ddys_token
21
+ });
22
+ return { success: true, message: 'Request submitted.', data };
23
+ } catch (error) {
24
+ return { success: false, message: error instanceof Error ? error.message : 'Request failed.' };
25
+ }
26
+ }
@@ -0,0 +1,15 @@
1
+ 'use server';
2
+
3
+ import { revalidateDdys } from '../server/cache';
4
+
5
+ export async function revalidateDdysAction(input: { tag?: string; tagProfile?: string; path?: string; pathType?: 'layout' | 'page'; token?: string }) {
6
+ const expected = process.env.DDYS_REVALIDATE_TOKEN ?? '';
7
+ if (!expected || input.token !== expected) {
8
+ return { success: false, message: 'Invalid revalidation token.' };
9
+ }
10
+ if (!input.tag && !input.path) {
11
+ return { success: false, message: 'Missing tag or path.' };
12
+ }
13
+ revalidateDdys({ tag: input.tag, tagProfile: input.tagProfile, path: input.path, pathType: input.pathType });
14
+ return { success: true };
15
+ }
@@ -0,0 +1,194 @@
1
+ import { DdysError } from './error';
2
+ import { DEFAULT_DDYS_CONFIG, type DdysConfig, type DdysConfigInput, mergeDdysConfig } from './config';
3
+ import type { DdysApiResponse, DdysPaginated, DdysQuery, DdysRequestInput } from '../types/ddys';
4
+ import { buildQuery, cleanQuery, normalizeBaseUrl, positiveId, routeSegment, toSearchParams } from '../utils/security';
5
+
6
+ export interface DdysRequestOptions {
7
+ auth?: boolean;
8
+ noCache?: boolean;
9
+ cacheTtl?: number;
10
+ next?: {
11
+ revalidate?: number | false;
12
+ tags?: string[];
13
+ };
14
+ cache?: RequestCache;
15
+ signal?: AbortSignal;
16
+ }
17
+
18
+ export type DdysFetch = (input: string | URL | Request, init?: RequestInit & { next?: DdysRequestOptions['next'] }) => Promise<Response>;
19
+
20
+ export class DdysClient {
21
+ readonly config: DdysConfig;
22
+ private readonly fetcher: DdysFetch;
23
+
24
+ constructor(config: DdysConfigInput = {}, fetcher: DdysFetch = fetch) {
25
+ const merged = mergeDdysConfig(config);
26
+ this.config = {
27
+ ...merged,
28
+ apiBaseUrl: normalizeBaseUrl(merged.apiBaseUrl, DEFAULT_DDYS_CONFIG.apiBaseUrl),
29
+ siteBaseUrl: normalizeBaseUrl(merged.siteBaseUrl, DEFAULT_DDYS_CONFIG.siteBaseUrl)
30
+ };
31
+ this.fetcher = fetcher;
32
+ }
33
+
34
+ async request<T = unknown>(method: string, path: string, query: DdysQuery = {}, body?: unknown, options: DdysRequestOptions = {}): Promise<DdysApiResponse<T>> {
35
+ method = method.toUpperCase();
36
+ path = `/${path.replace(/^\/+/, '')}`;
37
+ const clean = cleanQuery(query);
38
+ const auth = Boolean(options.auth);
39
+ if (auth && !this.config.apiKey) {
40
+ throw new DdysError('DDYS API Key is not configured.', 401, method, path);
41
+ }
42
+
43
+ return this.sendWithRetry<T>(method, path, clean, body, auth, options);
44
+ }
45
+
46
+ async get<T = unknown>(path: string, query: DdysQuery = {}, options: DdysRequestOptions = {}): Promise<DdysApiResponse<T>> {
47
+ return this.request<T>('GET', path, query, undefined, options);
48
+ }
49
+
50
+ async post<T = unknown>(path: string, body: unknown = {}, options: DdysRequestOptions = {}): Promise<DdysApiResponse<T>> {
51
+ return this.request<T>('POST', path, {}, body, options);
52
+ }
53
+
54
+ async delete<T = unknown>(path: string, options: DdysRequestOptions = {}): Promise<DdysApiResponse<T>> {
55
+ return this.request<T>('DELETE', path, {}, undefined, options);
56
+ }
57
+
58
+ async data<T = unknown>(path: string, query: DdysQuery = {}, options: DdysRequestOptions = {}): Promise<T> {
59
+ const payload = await this.get<T>(path, query, options);
60
+ return (payload.data ?? payload) as T;
61
+ }
62
+
63
+ async paginated<T = unknown>(path: string, query: DdysQuery = {}, options: DdysRequestOptions = {}): Promise<DdysPaginated<T>> {
64
+ const payload = await this.get<T[]>(path, query, options);
65
+ return {
66
+ data: Array.isArray(payload.data) ? payload.data : [],
67
+ meta: payload.meta ?? {}
68
+ };
69
+ }
70
+
71
+ movies(params: DdysQuery = {}, options?: DdysRequestOptions) { return this.paginated('/movies', this.query(params, ['type', 'genre', 'region', 'year', 'sort', 'page', 'per_page']), options); }
72
+ latest(params: DdysQuery = {}, options?: DdysRequestOptions) { return this.data('/latest', this.query(params, ['type', 'genre', 'region', 'year', 'limit']), options); }
73
+ hot(params: DdysQuery = {}, options?: DdysRequestOptions) { return this.data('/hot', this.query(params, ['type', 'genre', 'region', 'limit']), options); }
74
+ search(params: DdysQuery = {}, options?: DdysRequestOptions) { return this.paginated('/search', this.query(params, ['q', 'type', 'page', 'per_page']), options); }
75
+ suggest(q: string, params: DdysQuery = {}, options?: DdysRequestOptions) { return this.data('/suggest', this.query({ ...params, q }, ['q', 'limit']), options); }
76
+ calendar(params: DdysQuery = {}, options?: DdysRequestOptions) { return this.data('/calendar', this.query(params, ['year', 'month']), options); }
77
+ movie(slug: string, options?: DdysRequestOptions) { return this.data(`/movies/${routeSegment(slug, 'movie slug')}`, {}, options); }
78
+ sources(slug: string, options?: DdysRequestOptions) { return this.data(`/movies/${routeSegment(slug, 'movie slug')}/sources`, {}, options); }
79
+ related(slug: string, options?: DdysRequestOptions) { return this.data(`/movies/${routeSegment(slug, 'movie slug')}/related`, {}, options); }
80
+ comments(slug: string, params: DdysQuery = {}, options?: DdysRequestOptions) { return this.paginated(`/movies/${routeSegment(slug, 'movie slug')}/comments`, this.query(params, ['page', 'per_page']), options); }
81
+ collections(params: DdysQuery = {}, options?: DdysRequestOptions) { return this.paginated('/collections', this.query(params, ['page', 'per_page']), options); }
82
+ collection(slug: string, params: DdysQuery = {}, options?: DdysRequestOptions) { return this.data(`/collections/${routeSegment(slug, 'collection slug')}`, this.query(params, ['page', 'per_page']), options); }
83
+ shares(params: DdysQuery = {}, options?: DdysRequestOptions) { return this.paginated('/shares', this.query(params, ['page', 'per_page']), options); }
84
+ share(id: string | number, options?: DdysRequestOptions) { return this.data(`/shares/${positiveId(id, 'share ID')}`, {}, options); }
85
+ requests(params: DdysQuery = {}, options?: DdysRequestOptions) { return this.paginated('/requests', this.query(params, ['page', 'per_page']), options); }
86
+ activities(params: DdysQuery = {}, options?: DdysRequestOptions) { return this.paginated('/activities', this.query(params, ['type', 'page', 'per_page']), options); }
87
+ user(username: string, options?: DdysRequestOptions) { return this.data(`/user/${routeSegment(username, 'username')}`, {}, options); }
88
+ types(options?: DdysRequestOptions) { return this.data('/types', {}, options); }
89
+ genres(options?: DdysRequestOptions) { return this.data('/genres', {}, options); }
90
+ regions(options?: DdysRequestOptions) { return this.data('/regions', {}, options); }
91
+ me(options?: DdysRequestOptions) { return this.unwrap(this.get('/me', {}, { ...options, auth: true, noCache: true })); }
92
+ createRequest(input: DdysRequestInput, options?: DdysRequestOptions) { return this.unwrap(this.post('/requests', input, { ...options, auth: true, noCache: true })); }
93
+ createComment(input: Record<string, unknown>, options?: DdysRequestOptions) { return this.unwrap(this.post('/comments', input, { ...options, auth: true, noCache: true })); }
94
+ deleteComment(id: string | number, options?: DdysRequestOptions) { return this.unwrap(this.delete(`/comments/${positiveId(id, 'comment ID')}`, { ...options, auth: true, noCache: true })); }
95
+ reportInvalidResource(input: Record<string, unknown>, options?: DdysRequestOptions) { return this.unwrap(this.post('/report', input, { ...options, auth: true, noCache: true })); }
96
+ follow(username: string, options?: DdysRequestOptions) { return this.setFollow(username, 'follow', options); }
97
+ unfollow(username: string, options?: DdysRequestOptions) { return this.setFollow(username, 'unfollow', options); }
98
+
99
+ async setFollow(username: string, action: 'follow' | 'unfollow', options?: DdysRequestOptions) {
100
+ return this.unwrap(this.post('/follow', { username, action }, { ...options, auth: true, noCache: true }));
101
+ }
102
+
103
+ async proxy(route: string, query: DdysQuery = {}, options?: DdysRequestOptions): Promise<DdysApiResponse> {
104
+ route = String(route || 'latest').toLowerCase();
105
+ if (!this.config.proxy.allowRoutes.includes(route)) {
106
+ throw new DdysError('Route is not allowed.', 403, 'GET', '/proxy');
107
+ }
108
+ const path = this.proxyPath(route, query);
109
+ if (!path) throw new DdysError('Invalid route parameters.', 400, 'GET', '/proxy');
110
+ return this.get(path, this.query(query, ['type', 'genre', 'region', 'year', 'sort', 'page', 'per_page', 'limit', 'q', 'month']), options);
111
+ }
112
+
113
+ private async unwrap<T>(promise: Promise<DdysApiResponse<T>>): Promise<T | DdysApiResponse<T>> {
114
+ const payload = await promise;
115
+ return payload.data ?? payload;
116
+ }
117
+
118
+ private query(params: DdysQuery, keys: string[]) {
119
+ return buildQuery(params, keys, this.config.security);
120
+ }
121
+
122
+ private proxyPath(route: string, query: DdysQuery): string {
123
+ const slug = String(query.slug ?? '');
124
+ const id = String(query.id ?? '');
125
+ const username = String(query.username ?? '');
126
+ switch (route) {
127
+ case 'movies': return '/movies';
128
+ case 'latest': return '/latest';
129
+ case 'hot': return '/hot';
130
+ case 'search': return '/search';
131
+ case 'suggest': return '/suggest';
132
+ case 'calendar': return '/calendar';
133
+ case 'movie': return slug ? `/movies/${routeSegment(slug, 'movie slug')}` : '';
134
+ case 'sources': return slug ? `/movies/${routeSegment(slug, 'movie slug')}/sources` : '';
135
+ case 'related': return slug ? `/movies/${routeSegment(slug, 'movie slug')}/related` : '';
136
+ case 'comments': return slug ? `/movies/${routeSegment(slug, 'movie slug')}/comments` : '';
137
+ case 'collections': return '/collections';
138
+ case 'collection': return slug ? `/collections/${routeSegment(slug, 'collection slug')}` : '';
139
+ case 'shares': return '/shares';
140
+ case 'share': return id ? `/shares/${positiveId(id, 'share ID')}` : '';
141
+ case 'requests': return '/requests';
142
+ case 'activities': return '/activities';
143
+ case 'user': return username ? `/user/${routeSegment(username, 'username')}` : '';
144
+ case 'types': return '/types';
145
+ case 'genres': return '/genres';
146
+ case 'regions': return '/regions';
147
+ default: return '';
148
+ }
149
+ }
150
+
151
+ private async sendWithRetry<T>(method: string, path: string, query: Record<string, string>, body: unknown, auth: boolean, options: DdysRequestOptions): Promise<DdysApiResponse<T>> {
152
+ const attempts = Math.max(1, this.config.retryTimes + 1);
153
+ let last: unknown;
154
+ for (let attempt = 1; attempt <= attempts; attempt++) {
155
+ try {
156
+ const qs = toSearchParams(query);
157
+ const url = `${this.config.apiBaseUrl}${path}${qs ? `?${qs}` : ''}`;
158
+ const controller = options.signal ? undefined : new AbortController();
159
+ const timeout = controller ? setTimeout(() => controller.abort(), this.config.timeout * 1000) : undefined;
160
+ const headers = {
161
+ Accept: 'application/json',
162
+ 'Content-Type': 'application/json',
163
+ ...(typeof window === 'undefined' ? { 'User-Agent': this.config.userAgent } : {}),
164
+ ...(auth ? { Authorization: `Bearer ${this.config.apiKey}` } : {})
165
+ };
166
+ let response: Response;
167
+ try {
168
+ response = await this.fetcher(url, {
169
+ method,
170
+ headers,
171
+ body: method === 'GET' || body === undefined ? undefined : JSON.stringify(body),
172
+ cache: options.noCache ? 'no-store' : options.cache,
173
+ next: options.noCache ? undefined : options.next,
174
+ signal: options.signal ?? controller?.signal
175
+ });
176
+ } finally {
177
+ if (timeout) clearTimeout(timeout);
178
+ }
179
+ const json = await response.json().catch(() => null);
180
+ if (!json || typeof json !== 'object') throw new DdysError('DDYS API returned invalid JSON.', 502, method, path);
181
+ if (!response.ok) throw new DdysError(`DDYS API HTTP ${response.status}.`, response.status, method, path, json);
182
+ if ('success' in json && json.success === false) throw new DdysError(String(json.message || 'DDYS API request failed.'), 502, method, path, json);
183
+ return json as DdysApiResponse<T>;
184
+ } catch (error) {
185
+ last = error;
186
+ if (error instanceof DdysError && error.status >= 400 && error.status < 500) throw error;
187
+ if (method !== 'GET' || attempt >= attempts) break;
188
+ await new Promise((resolve) => setTimeout(resolve, this.config.retrySleep));
189
+ }
190
+ }
191
+ if (last instanceof DdysError) throw last;
192
+ throw new DdysError(`DDYS API request failed: ${last instanceof Error ? last.message : 'unknown error'}`, 0, method, path, last);
193
+ }
194
+ }
@@ -0,0 +1,111 @@
1
+ export interface DdysCacheConfig {
2
+ defaultTtl: number;
3
+ dictionaryTtl: number;
4
+ freshTtl: number;
5
+ listTtl: number;
6
+ detailTtl: number;
7
+ communityTtl: number;
8
+ }
9
+
10
+ export interface DdysRequestFormConfig {
11
+ enabled: boolean;
12
+ csrf: boolean;
13
+ honeypotField: string;
14
+ rateLimitSeconds: number;
15
+ tokenTtlSeconds: number;
16
+ secret?: string;
17
+ }
18
+
19
+ export interface DdysProxyConfig {
20
+ enabled: boolean;
21
+ allowRoutes: string[];
22
+ }
23
+
24
+ export interface DdysSecurityConfig {
25
+ maxLimit: number;
26
+ maxPerPage: number;
27
+ maxPage: number;
28
+ allowedResourceProtocols: string[];
29
+ }
30
+
31
+ export interface DdysConfig {
32
+ apiBaseUrl: string;
33
+ siteBaseUrl: string;
34
+ apiKey?: string;
35
+ timeout: number;
36
+ retryTimes: number;
37
+ retrySleep: number;
38
+ userAgent: string;
39
+ cache: DdysCacheConfig;
40
+ proxy: DdysProxyConfig;
41
+ requestForm: DdysRequestFormConfig;
42
+ diagnostics: {
43
+ enabled: boolean;
44
+ };
45
+ security: DdysSecurityConfig;
46
+ }
47
+
48
+ export const DDYS_VERSION = '0.1.0';
49
+
50
+ export const DEFAULT_DDYS_CONFIG: DdysConfig = {
51
+ apiBaseUrl: 'https://ddys.io/api/v1',
52
+ siteBaseUrl: 'https://ddys.io',
53
+ apiKey: '',
54
+ timeout: 12,
55
+ retryTimes: 1,
56
+ retrySleep: 150,
57
+ userAgent: `ddys-nextjs/${DDYS_VERSION}`,
58
+ cache: {
59
+ defaultTtl: 300,
60
+ dictionaryTtl: 86400,
61
+ freshTtl: 300,
62
+ listTtl: 600,
63
+ detailTtl: 1800,
64
+ communityTtl: 120
65
+ },
66
+ proxy: {
67
+ enabled: true,
68
+ allowRoutes: [
69
+ 'movies', 'latest', 'hot', 'search', 'suggest', 'calendar',
70
+ 'movie', 'sources', 'related', 'comments',
71
+ 'collections', 'collection', 'shares', 'share',
72
+ 'requests', 'activities', 'user', 'types', 'genres', 'regions'
73
+ ]
74
+ },
75
+ requestForm: {
76
+ enabled: false,
77
+ csrf: true,
78
+ honeypotField: 'ddys_website',
79
+ rateLimitSeconds: 60,
80
+ tokenTtlSeconds: 1800
81
+ },
82
+ diagnostics: {
83
+ enabled: false
84
+ },
85
+ security: {
86
+ maxLimit: 50,
87
+ maxPerPage: 50,
88
+ maxPage: 999,
89
+ allowedResourceProtocols: ['http:', 'https:', 'magnet:', 'ed2k:', 'thunder:']
90
+ }
91
+ };
92
+
93
+ export type DdysConfigInput = Partial<Omit<DdysConfig, 'cache' | 'proxy' | 'requestForm' | 'diagnostics' | 'security'>> & {
94
+ cache?: Partial<DdysCacheConfig>;
95
+ proxy?: Partial<DdysProxyConfig>;
96
+ requestForm?: Partial<DdysRequestFormConfig>;
97
+ diagnostics?: Partial<DdysConfig['diagnostics']>;
98
+ security?: Partial<DdysSecurityConfig>;
99
+ };
100
+
101
+ export function mergeDdysConfig(input: DdysConfigInput = {}): DdysConfig {
102
+ return {
103
+ ...DEFAULT_DDYS_CONFIG,
104
+ ...input,
105
+ cache: { ...DEFAULT_DDYS_CONFIG.cache, ...input.cache },
106
+ proxy: { ...DEFAULT_DDYS_CONFIG.proxy, ...input.proxy },
107
+ requestForm: { ...DEFAULT_DDYS_CONFIG.requestForm, ...input.requestForm },
108
+ diagnostics: { ...DEFAULT_DDYS_CONFIG.diagnostics, ...input.diagnostics },
109
+ security: { ...DEFAULT_DDYS_CONFIG.security, ...input.security }
110
+ };
111
+ }
@@ -0,0 +1,15 @@
1
+ export class DdysError extends Error {
2
+ readonly status: number;
3
+ readonly method: string;
4
+ readonly path: string;
5
+ readonly context: unknown;
6
+
7
+ constructor(message: string, status = 0, method = '', path = '', context?: unknown) {
8
+ super(message);
9
+ this.name = 'DdysError';
10
+ this.status = status;
11
+ this.method = method;
12
+ this.path = path;
13
+ this.context = context;
14
+ }
15
+ }
@@ -0,0 +1,13 @@
1
+ export { DdysClient } from './client';
2
+ export { DdysError } from './error';
3
+ export {
4
+ DDYS_VERSION,
5
+ DEFAULT_DDYS_CONFIG,
6
+ mergeDdysConfig,
7
+ type DdysCacheConfig,
8
+ type DdysConfig,
9
+ type DdysConfigInput,
10
+ type DdysProxyConfig,
11
+ type DdysRequestFormConfig,
12
+ type DdysSecurityConfig
13
+ } from './config';
@@ -0,0 +1,38 @@
1
+ import type { DdysDisplayOptions, DdysItem } from '../types/ddys';
2
+ import { itemMeta, itemPoster, itemSummary, itemTitle, itemUrl } from './utils';
3
+
4
+ export interface DdysCardProps {
5
+ item: DdysItem;
6
+ display?: DdysDisplayOptions;
7
+ siteBaseUrl?: string;
8
+ }
9
+
10
+ export function DdysCard({ item, display = {}, siteBaseUrl }: DdysCardProps) {
11
+ const title = itemTitle(item);
12
+ const poster = itemPoster(item);
13
+ const url = itemUrl(item, siteBaseUrl);
14
+ const meta = itemMeta(item);
15
+ const summary = itemSummary(item);
16
+ const showPoster = display.showPoster ?? true;
17
+ const showSummary = display.showSummary ?? true;
18
+ const showSourceLink = display.showSourceLink ?? true;
19
+ const target = display.target ?? '_blank';
20
+
21
+ return (
22
+ <article className="ddys-next-card">
23
+ {showPoster && poster ? (
24
+ <div className="ddys-next-poster">
25
+ {/* eslint-disable-next-line @next/next/no-img-element */}
26
+ <img src={poster} alt={title} loading="lazy" />
27
+ </div>
28
+ ) : null}
29
+ <div className="ddys-next-card-body">
30
+ <h3 className="ddys-next-title">
31
+ {url && showSourceLink ? <a href={url} target={target} rel="noopener noreferrer">{title}</a> : title}
32
+ </h3>
33
+ {meta.length ? <div className="ddys-next-meta">{meta.join(' / ')}</div> : null}
34
+ {showSummary && summary ? <div className="ddys-next-summary">{summary}</div> : null}
35
+ </div>
36
+ </article>
37
+ );
38
+ }
@@ -0,0 +1,3 @@
1
+ export { DdysDiagnostics } from './diagnostics';
2
+ export { DdysRequestForm, type DdysRequestFormProps } from './request-form';
3
+ export { DdysSearch, type DdysSearchProps } from './search';
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ export function DdysDiagnostics({ endpoint = '/api/ddys/diagnostics' }: { endpoint?: string }) {
6
+ const [status, setStatus] = useState('Ready.');
7
+ const [data, setData] = useState<unknown>(null);
8
+
9
+ async function load() {
10
+ setStatus('Loading...');
11
+ const response = await fetch(endpoint, { credentials: 'same-origin' });
12
+ const json = await response.json().catch(() => ({ success: false, message: 'Invalid JSON response.' }));
13
+ setData(json);
14
+ setStatus(json.success ? 'Diagnostics loaded.' : json.message || `HTTP ${response.status}`);
15
+ }
16
+
17
+ async function test() {
18
+ setStatus('Testing...');
19
+ const response = await fetch(endpoint, { method: 'POST', credentials: 'same-origin' });
20
+ const json = await response.json().catch(() => ({ success: false, message: 'Invalid JSON response.' }));
21
+ setData(json);
22
+ setStatus(json.success ? 'Connection OK.' : json.message || `HTTP ${response.status}`);
23
+ }
24
+
25
+ return (
26
+ <section className="ddys-next-diagnostics">
27
+ <div className="ddys-next-actions">
28
+ <button type="button" onClick={load}>Load diagnostics</button>
29
+ <button type="button" onClick={test}>Test API</button>
30
+ </div>
31
+ <p className="ddys-next-status" role="status">{status}</p>
32
+ {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : null}
33
+ </section>
34
+ );
35
+ }
@@ -0,0 +1,27 @@
1
+ import type { DdysDisplayOptions, DdysItem } from '../types/ddys';
2
+ import type { CSSProperties } from 'react';
3
+ import { DdysCard } from './card';
4
+
5
+ export interface DdysGridProps {
6
+ items: DdysItem[];
7
+ display?: DdysDisplayOptions;
8
+ siteBaseUrl?: string;
9
+ emptyText?: string;
10
+ }
11
+
12
+ export function DdysGrid({ items, display = {}, siteBaseUrl, emptyText = 'No DDYS content found.' }: DdysGridProps) {
13
+ const columns = Math.max(1, Math.min(6, display.columns ?? 4));
14
+ const layout = display.layout ?? 'grid';
15
+ const theme = display.theme ?? 'auto';
16
+ if (!items.length) {
17
+ return <div className={`ddys-next ddys-next-theme-${theme}`}><div className="ddys-next-empty">{emptyText}</div></div>;
18
+ }
19
+
20
+ return (
21
+ <div className={`ddys-next ddys-next-theme-${theme} ddys-next-layout-${layout}`} style={{ '--ddys-next-columns': columns } as CSSProperties}>
22
+ <div className="ddys-next-items">
23
+ {items.map((item, index) => <DdysCard key={String(item.id ?? item.slug ?? index)} item={item} display={display} siteBaseUrl={siteBaseUrl} />)}
24
+ </div>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,8 @@
1
+ export { DdysCard, type DdysCardProps } from './card';
2
+ export { DdysDiagnostics } from './diagnostics';
3
+ export { DdysGrid, type DdysGridProps } from './grid';
4
+ export { DdysMovieDetail, type DdysMovieDetailProps } from './movie-detail';
5
+ export { DdysRequestForm, type DdysRequestFormProps } from './request-form';
6
+ export { DdysSearch, type DdysSearchProps } from './search';
7
+ export { DdysSources, type DdysSourcesProps } from './sources';
8
+ export { DdysView, type DdysViewProps } from './view';
@@ -0,0 +1,40 @@
1
+ import type { DdysDisplayOptions, DdysItem, DdysResource } from '../types/ddys';
2
+ import { DdysCard } from './card';
3
+ import { DdysGrid } from './grid';
4
+ import { DdysSources } from './sources';
5
+ import { itemSummary } from './utils';
6
+
7
+ export interface DdysMovieDetailProps {
8
+ movie: DdysItem;
9
+ display?: DdysDisplayOptions;
10
+ siteBaseUrl?: string;
11
+ }
12
+
13
+ export function DdysMovieDetail({ movie, display, siteBaseUrl }: DdysMovieDetailProps) {
14
+ const summary = itemSummary(movie);
15
+ const related = Array.isArray(movie.movies) ? movie.movies as DdysItem[] : [];
16
+ const groups = sourceGroups(movie);
17
+
18
+ return (
19
+ <div className="ddys-next ddys-next-detail">
20
+ <DdysCard item={movie} display={display} siteBaseUrl={siteBaseUrl} />
21
+ {summary ? <div className="ddys-next-description">{summary}</div> : null}
22
+ {Object.keys(groups).length ? <DdysSources groups={groups} /> : null}
23
+ {related.length ? (
24
+ <section className="ddys-next-related">
25
+ <h3>Related</h3>
26
+ <DdysGrid items={related} display={display} siteBaseUrl={siteBaseUrl} />
27
+ </section>
28
+ ) : null}
29
+ </div>
30
+ );
31
+ }
32
+
33
+ function sourceGroups(movie: DdysItem): Record<string, DdysResource[]> {
34
+ const groups: Record<string, DdysResource[]> = {};
35
+ if (Array.isArray(movie.resources)) groups.Resources = movie.resources as DdysResource[];
36
+ if (Array.isArray(movie.sources)) groups.Sources = movie.sources as DdysResource[];
37
+ if (Array.isArray(movie.online)) groups.Online = movie.online as DdysResource[];
38
+ if (Array.isArray(movie.download)) groups.Download = movie.download as DdysResource[];
39
+ return groups;
40
+ }