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
@@ -0,0 +1,125 @@
1
+ import type { DdysQuery, DdysScalar } from '../types/ddys';
2
+
3
+ interface QueryLimits {
4
+ maxLimit?: number;
5
+ maxPerPage?: number;
6
+ maxPage?: number;
7
+ }
8
+
9
+ export function scalar(value: unknown, fallback = ''): string {
10
+ if (Array.isArray(value) || value === null || typeof value === 'object' || typeof value === 'function') {
11
+ return fallback;
12
+ }
13
+
14
+ return String(value ?? fallback).replace(/\0/g, '').trim();
15
+ }
16
+
17
+ export function boolValue(value: unknown): boolean {
18
+ if (typeof value === 'boolean') return value;
19
+ return ['1', 'true', 'yes', 'on'].includes(scalar(value).toLowerCase());
20
+ }
21
+
22
+ export function intRange(value: unknown, fallback: number, min: number, max: number): number {
23
+ const parsed = Number.parseInt(scalar(value), 10);
24
+ if (!Number.isFinite(parsed)) return fallback;
25
+ return Math.max(min, Math.min(max, parsed));
26
+ }
27
+
28
+ export function choice<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
29
+ const text = scalar(value).toLowerCase();
30
+ return allowed.includes(text as T) ? (text as T) : fallback;
31
+ }
32
+
33
+ export function normalizeBaseUrl(value: unknown, fallback: string): string {
34
+ const text = scalar(value);
35
+ try {
36
+ const url = new URL(text);
37
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
38
+ return fallback;
39
+ }
40
+ return text.replace(/\/+$/, '');
41
+ } catch {
42
+ return fallback;
43
+ }
44
+ }
45
+
46
+ export function cleanQuery(query: DdysQuery = {}): Record<string, string> {
47
+ const out: Record<string, string> = {};
48
+ for (const [key, value] of Object.entries(query)) {
49
+ const raw = Array.isArray(value) ? value[0] : value;
50
+ const text = scalar(raw as DdysScalar);
51
+ if (text !== '') out[key] = text;
52
+ }
53
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
54
+ }
55
+
56
+ export function normalizeQueryValue(key: string, value: unknown, limits: QueryLimits = {}): string | number {
57
+ const text = scalar(value);
58
+ if (text === '') return '';
59
+ if (key === 'limit') return intRange(text, 12, 1, limits.maxLimit ?? 50);
60
+ if (key === 'per_page') return intRange(text, 12, 1, limits.maxPerPage ?? 50);
61
+ if (key === 'page') return intRange(text, 1, 1, limits.maxPage ?? 999);
62
+ if (key === 'year') return /^\d{4}$/.test(text) && Number(text) >= 1900 && Number(text) <= 2099 ? Number(text) : '';
63
+ if (key === 'month') return /^\d{1,2}$/.test(text) && Number(text) >= 1 && Number(text) <= 12 ? Number(text) : '';
64
+ if (key === 'q') return text.slice(0, 120);
65
+ if (['type', 'genre', 'region', 'sort'].includes(key)) return text.slice(0, 64);
66
+ return text.slice(0, 255);
67
+ }
68
+
69
+ export function buildQuery(source: DdysQuery = {}, keys: string[], limits: QueryLimits = {}): Record<string, string | number> {
70
+ const out: Record<string, string | number> = {};
71
+ for (const key of keys) {
72
+ if (!(key in source)) continue;
73
+ const value = normalizeQueryValue(key, source[key], limits);
74
+ if (value !== '') out[key] = value;
75
+ }
76
+ return out;
77
+ }
78
+
79
+ export function toSearchParams(query: Record<string, string | number> = {}): string {
80
+ const params = new URLSearchParams();
81
+ for (const [key, value] of Object.entries(query)) params.set(key, String(value));
82
+ return params.toString();
83
+ }
84
+
85
+ export function routeSegment(value: unknown, label: string): string {
86
+ const text = scalar(value);
87
+ if (text === '' || text.includes('/') || /[\x00-\x1F\x7F]/.test(text)) {
88
+ throw new Error(`Invalid ${label}.`);
89
+ }
90
+ return encodeURIComponent(text);
91
+ }
92
+
93
+ export function positiveId(value: unknown, label: string): number {
94
+ const text = scalar(value);
95
+ if (!/^[1-9][0-9]*$/.test(text)) throw new Error(`Invalid ${label}.`);
96
+ return Number(text);
97
+ }
98
+
99
+ export function safeMediaUrl(value: unknown): string {
100
+ const text = scalar(value);
101
+ if (!text) return '';
102
+ try {
103
+ const url = new URL(text);
104
+ return ['http:', 'https:'].includes(url.protocol) ? text : '';
105
+ } catch {
106
+ return '';
107
+ }
108
+ }
109
+
110
+ export function isAllowedResourceUrl(href: string, protocols: readonly string[]): boolean {
111
+ const text = href.trim();
112
+ return protocols.some((protocol) => {
113
+ const lower = protocol.toLowerCase().trim();
114
+ if (lower === 'http:' || lower === 'https:') return text.toLowerCase().startsWith(`${lower}//`);
115
+ return text.toLowerCase().startsWith(lower);
116
+ });
117
+ }
118
+
119
+ export function formDataToObject(formData: FormData): Record<string, string> {
120
+ const out: Record<string, string> = {};
121
+ for (const [key, value] of formData.entries()) {
122
+ out[key] = typeof value === 'string' ? value : value.name;
123
+ }
124
+ return out;
125
+ }