From 63ef322ccf37acb54d90f346ad30780d012dfcd4 Mon Sep 17 00:00:00 2001 From: ngthanhvu Date: Thu, 30 Jul 2026 22:32:44 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20th=C3=AAm=20c=C3=A1i=20proxy=20h=C3=ACn?= =?UTF-8?q?h=20=E1=BA=A3nh=20d=C3=B9ng=20webp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pages/phim/[slug].vue | 2 +- package-lock.json | 566 +++++++++++++++++++++++++++++++- package.json | 1 + server/api/home/sections.get.ts | 13 +- server/api/image-proxy.get.ts | 112 +++++++ server/api/movies.get.ts | 5 +- server/api/movies/[slug].get.ts | 5 +- server/utils/movies.ts | 25 +- server/utils/proxy-image.ts | 20 ++ 9 files changed, 727 insertions(+), 22 deletions(-) create mode 100644 server/api/image-proxy.get.ts create mode 100644 server/utils/proxy-image.ts diff --git a/app/pages/phim/[slug].vue b/app/pages/phim/[slug].vue index af8a700..73ba8e4 100644 --- a/app/pages/phim/[slug].vue +++ b/app/pages/phim/[slug].vue @@ -226,7 +226,7 @@ useHead(() => ({ -
+

{{ movie.name }} diff --git a/package-lock.json b/package-lock.json index 5af2e9c..db5cfc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "jsonwebtoken": "^9.0.3", "mysql2": "^3.23.1", "nuxt": "^4.4.5", + "sharp": "^0.35.3", "swiper": "^14.0.6", "tailwindcss": "^4.3.0", "vue": "^3.5.34", @@ -1480,6 +1481,516 @@ "vue": ">= 3.0.0 < 4" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-wasm32/node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@ioredis/commands": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", @@ -9920,9 +10431,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10009,6 +10520,55 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 393b482..8fcf96b 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "jsonwebtoken": "^9.0.3", "mysql2": "^3.23.1", "nuxt": "^4.4.5", + "sharp": "^0.35.3", "swiper": "^14.0.6", "tailwindcss": "^4.3.0", "vue": "^3.5.34", diff --git a/server/api/home/sections.get.ts b/server/api/home/sections.get.ts index 653b5d0..05d2809 100644 --- a/server/api/home/sections.get.ts +++ b/server/api/home/sections.get.ts @@ -1,5 +1,6 @@ import { eq, and, isNull, desc, inArray, count } from 'drizzle-orm' import { comments, users, movies } from '../../database/schema' +import { proxyImageUrl } from '../../utils/proxy-image' export default defineEventHandler(async () => { const db = useDb() @@ -132,8 +133,8 @@ export default defineEventHandler(async () => { source: c.source, slug: c.slug, name: c.movieName || '', - thumb: c.movieThumb, - poster: c.moviePoster, + thumb: proxyImageUrl(c.movieThumb || ''), + poster: proxyImageUrl(c.moviePoster || ''), } : null, })), @@ -142,8 +143,8 @@ export default defineEventHandler(async () => { slug: m.slug, name: m.name, originName: m.originName, - thumb: m.thumb, - poster: m.poster, + thumb: proxyImageUrl(m.thumb || ''), + poster: proxyImageUrl(m.poster || ''), views: m.views, rating: m.rating, })), @@ -152,8 +153,8 @@ export default defineEventHandler(async () => { slug: m.slug, name: m.name, originName: m.originName, - thumb: m.thumb, - poster: m.poster, + thumb: proxyImageUrl(m.thumb || ''), + poster: proxyImageUrl(m.poster || ''), views: m.views, rating: m.rating, })), diff --git a/server/api/image-proxy.get.ts b/server/api/image-proxy.get.ts new file mode 100644 index 0000000..4a72295 --- /dev/null +++ b/server/api/image-proxy.get.ts @@ -0,0 +1,112 @@ +import { createHash } from 'crypto' +import { useRedis } from '../utils/redis' + +const CACHE_TTL = 604800 // 7 ngày +const CACHE_PREFIX = 'img:' +const DEFAULT_WIDTH = null +const DEFAULT_QUALITY = 85 + +async function getCachedWebP(redis: any, key: string): Promise { + try { + const cached = await redis.getBuffer(key) + return cached || null + } catch { + return null + } +} + +async function setCachedWebP(redis: any, key: string, data: Buffer): Promise { + try { + await redis.set(key, data, 'EX', CACHE_TTL) + } catch { + // Ignore cache write errors + } +} + +export default defineEventHandler(async (event) => { + const url = getQuery(event).url as string + + if (!url) { + throw createError({ statusCode: 400, message: 'Missing URL parameter' }) + } + + if (!url.startsWith('http://') && !url.startsWith('https://')) { + throw createError({ statusCode: 400, message: 'Invalid URL protocol' }) + } + + const width = getQuery(event).w ? parseInt(getQuery(event).w as string) : DEFAULT_WIDTH + const quality = getQuery(event).q ? parseInt(getQuery(event).q as string) : DEFAULT_QUALITY + const cacheKey = CACHE_PREFIX + createHash('md5').update(`${url}|${width}|${quality}`).digest('hex') + + const redis = useRedis() + + // Check cache + const cached = await getCachedWebP(redis, cacheKey) + if (cached) { + setResponseHeader(event, 'cache-control', 'public, max-age=604800, immutable') + setResponseHeader(event, 'content-type', 'image/webp') + setResponseHeader(event, 'x-cache', 'HIT') + setResponseHeader(event, 'access-control-allow-origin', '*') + return cached + } + + // Fetch original image + let response: Response + try { + response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', + 'Referer': new URL(url).origin, + 'Origin': new URL(url).origin, + }, + signal: AbortSignal.timeout(15000), + }) + } catch (error: any) { + throw createError({ statusCode: 502, message: `Failed to fetch image: ${error.message}` }) + } + + if (!response.ok) { + throw createError({ statusCode: response.status, message: `Upstream error: ${response.statusText}` }) + } + + const contentType = response.headers.get('content-type') || '' + + // Skip non-image content types + if (!contentType.startsWith('image/')) { + throw createError({ statusCode: 400, message: 'Not an image' }) + } + + const buffer = Buffer.from(await response.arrayBuffer()) + + // Convert to WebP with optional resizing (dynamic import to avoid ESM resolution in dev) + let webpBuffer: Buffer + try { + const { default: sharp } = await import('sharp') + let transform = sharp(buffer) .webp({ quality, effort: 4 }) + + if (width) { + transform = transform.resize(width, null, { withoutEnlargement: true }) + } + + webpBuffer = await transform.toBuffer() + } catch (error: any) { + // If conversion fails, return original + setResponseHeader(event, 'content-type', contentType) + setResponseHeader(event, 'cache-control', 'public, max-age=3600') + setResponseHeader(event, 'x-cache', 'MISS') + setResponseHeader(event, 'access-control-allow-origin', '*') + return buffer + } + + // Cache in Redis + await setCachedWebP(redis, cacheKey, webpBuffer) + + setResponseHeader(event, 'cache-control', 'public, max-age=604800, immutable') + setResponseHeader(event, 'content-type', 'image/webp') + setResponseHeader(event, 'content-length', webpBuffer.length.toString()) + setResponseHeader(event, 'x-cache', 'MISS') + setResponseHeader(event, 'access-control-allow-origin', '*') + + return webpBuffer +}) diff --git a/server/api/movies.get.ts b/server/api/movies.get.ts index a45638b..daa3ca4 100644 --- a/server/api/movies.get.ts +++ b/server/api/movies.get.ts @@ -1,5 +1,6 @@ import { movies } from '../database/schema' import { desc, like, and, eq, sql } from 'drizzle-orm' +import { proxyImageUrl } from '../utils/proxy-image' export default defineEventHandler(async (event) => { const db = useDb() @@ -67,8 +68,8 @@ function mapMovieToResponse(movie: any) { slug: movie.slug, name: movie.name, originName: movie.originName || '', - thumb: movie.customThumb || movie.thumb || '', - poster: movie.customPoster || movie.poster || '', + thumb: movie.customThumb ? proxyImageUrl(movie.customThumb) : proxyImageUrl(movie.thumb || ''), + poster: movie.customPoster ? proxyImageUrl(movie.customPoster) : proxyImageUrl(movie.poster || ''), year: movie.year || undefined, time: movie.time || undefined, episode: movie.episode || undefined, diff --git a/server/api/movies/[slug].get.ts b/server/api/movies/[slug].get.ts index 728c413..10e9c16 100644 --- a/server/api/movies/[slug].get.ts +++ b/server/api/movies/[slug].get.ts @@ -1,6 +1,7 @@ import { movies } from '../../database/schema' import { eq, and } from 'drizzle-orm' import type { MovieDetail, NormalizedServer } from '../../utils/movies' +import { proxyImageUrl } from '../../utils/proxy-image' function mapCustomServers(movie: any): NormalizedServer[] { let raw = movie.customServers ?? movie.custom_servers @@ -30,8 +31,8 @@ function mapMovieToDetail(movie: any): MovieDetail { name: movie.name, originName: movie.originName || '', slug: movie.slug, - thumb: movie.customThumb || movie.thumb || '', - poster: movie.customPoster || movie.poster || '', + thumb: movie.customThumb ? proxyImageUrl(movie.customThumb) : proxyImageUrl(movie.thumb || ''), + poster: movie.customPoster ? proxyImageUrl(movie.customPoster) : proxyImageUrl(movie.poster || ''), year: movie.year || undefined, time: movie.time || undefined, episode: movie.episode || undefined, diff --git a/server/utils/movies.ts b/server/utils/movies.ts index d6deb79..7205c85 100644 --- a/server/utils/movies.ts +++ b/server/utils/movies.ts @@ -1,3 +1,5 @@ +import { proxyImageUrl } from './proxy-image' + type Source = 'ophim' | 'nguonc' | 'kkphim' type SourceFilter = Source | 'all' @@ -304,23 +306,26 @@ function normalizeActor(actor: any): NormalizedActor | undefined { const name = text(actor?.name || actor?.actor_name || actor?.title).trim() if (!name) return undefined + const avatarUrl = joinImage('', actor?.avatar || actor?.image || actor?.thumb_url || actor?.poster_url) return { name, originalName: text(actor?.original_name || actor?.origin_name || actor?.real_name) || undefined, role: text(actor?.role || actor?.character || actor?.as || actor?.cast_name) || undefined, - avatar: joinImage('', actor?.avatar || actor?.image || actor?.thumb_url || actor?.poster_url) || undefined, + avatar: proxyImageUrl(avatarUrl), } } export function normalizeOphimMovie(movie: any, pathImage = OPHIM_IMAGE): NormalizedMovie { + const thumbUrl = joinOphimImage(pathImage, movie?.thumb_url) + const posterUrl = joinOphimImage(pathImage, movie?.poster_url) return { id: `ophim:${movie?._id ?? movie?.slug}`, source: 'ophim', name: text(movie?.name, 'Chưa có tên'), originName: text(movie?.origin_name), slug: text(movie?.slug), - thumb: joinOphimImage(pathImage, movie?.thumb_url), - poster: joinOphimImage(pathImage, movie?.poster_url), + thumb: proxyImageUrl(thumbUrl), + poster: proxyImageUrl(posterUrl), year: Number(movie?.year) || undefined, time: text(movie?.time), episode: text(movie?.episode_current), @@ -336,14 +341,16 @@ export function normalizeOphimMovie(movie: any, pathImage = OPHIM_IMAGE): Normal } export function normalizeKkphimMovie(movie: any, pathImage = KKPHIM_IMAGE): NormalizedMovie { + const thumbUrl = joinKkphimImage(pathImage, movie?.thumb_url) + const posterUrl = joinKkphimImage(pathImage, movie?.poster_url) return { id: `kkphim:${movie?._id ?? movie?.slug}`, source: 'kkphim', - name: text(movie?.name, 'Chưa có tên'), + name: text(movie?.name, 'Chưa có tên'), originName: text(movie?.origin_name), slug: text(movie?.slug), - thumb: joinKkphimImage(pathImage, movie?.thumb_url), - poster: joinKkphimImage(pathImage, movie?.poster_url), + thumb: proxyImageUrl(thumbUrl), + poster: proxyImageUrl(posterUrl), year: Number(movie?.year) || undefined, time: text(movie?.time), episode: text(movie?.episode_current), @@ -360,6 +367,8 @@ export function normalizeKkphimMovie(movie: any, pathImage = KKPHIM_IMAGE): Norm function normalizeNguoncMovie(movie: any): NormalizedMovie { const image = movie?.thumb_url || movie?.poster_url || movie?.image || movie?.thumbnail + const thumbUrl = joinImage('', image) + const posterUrl = joinImage('', movie?.poster_url) return { id: `nguonc:${movie?._id ?? movie?.slug}`, @@ -367,8 +376,8 @@ function normalizeNguoncMovie(movie: any): NormalizedMovie { name: text(movie?.name || movie?.title, 'Chưa có tên'), originName: text(movie?.original_name || movie?.origin_name), slug: text(movie?.slug), - thumb: joinImage('', image), - poster: joinImage('', movie?.poster_url), + thumb: proxyImageUrl(thumbUrl), + poster: proxyImageUrl(posterUrl), year: Number(movie?.year || movie?.release_year) || undefined, time: text(movie?.time || movie?.duration), episode: text(movie?.episode_current || movie?.current_episode), diff --git a/server/utils/proxy-image.ts b/server/utils/proxy-image.ts new file mode 100644 index 0000000..16ebea8 --- /dev/null +++ b/server/utils/proxy-image.ts @@ -0,0 +1,20 @@ +/** + * Transform image URLs to use the internal image proxy. + * This converts remote images to WebP format for faster loading. + */ +export function proxyImageUrl(url: string): string { + if (!url) return '' + if (url.startsWith('/api/image-proxy')) return url + + // Nếu đã có sẵn query params + const separator = url.includes('?') ? '&' : '?' + return `/api/image-proxy?url=${encodeURIComponent(url)}` +} + +export function proxyThumbUrl(url: string): string { + return proxyImageUrl(url) +} + +export function proxyPosterUrl(url: string): string { + return proxyImageUrl(url) +}