1 parent
0fc0024cb6
commit
73d4f6af42
9 files changed
+678
-2
No files matched your search
+2
-1
@@ -6,7 +6,8 @@ ADMIN_USER=admin
|
|||||||
ADMIN_PASSWORD=admin
|
ADMIN_PASSWORD=admin
|
||||||
ADMIN_COOKIE=muelleel
|
ADMIN_COOKIE=muelleel
|
||||||
|
|
||||||
UPLOAD_PATH=/tmp
|
UPLOAD_PATH=/tmp/uploads
|
||||||
|
VIDEO_PATH=/tmp/videos
|
||||||
|
|
||||||
TEAMSPEAK_LINK=http://example.com
|
TEAMSPEAK_LINK=http://example.com
|
||||||
DISCORD_LINK=http://example.com
|
DISCORD_LINK=http://example.com
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export default defineConfig({
|
|||||||
ADMIN_COOKIE: envField.string({ context: 'server', access: 'secret', default: 'muelleel' }),
|
ADMIN_COOKIE: envField.string({ context: 'server', access: 'secret', default: 'muelleel' }),
|
||||||
|
|
||||||
UPLOAD_PATH: envField.string({ context: 'server', access: 'secret', optional: true }),
|
UPLOAD_PATH: envField.string({ context: 'server', access: 'secret', optional: true }),
|
||||||
|
VIDEO_PATH: envField.string({ context: 'server', access: 'secret', optional: true }),
|
||||||
|
|
||||||
MAX_UPLOAD_BYTES: envField.number({ context: 'server', access: 'secret', default: 20 * 1024 * 1024 }),
|
MAX_UPLOAD_BYTES: envField.number({ context: 'server', access: 'secret', default: 20 * 1024 * 1024 }),
|
||||||
|
|
||||||
START_DATE: envField.string({ context: 'server', access: 'secret', default: '1970-01-01' }),
|
START_DATE: envField.string({ context: 'server', access: 'secret', default: '1970-01-01' }),
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
ADMIN_COOKIE = "muelleel";
|
ADMIN_COOKIE = "muelleel";
|
||||||
|
|
||||||
UPLOAD_PATH = "./.data/uploads";
|
UPLOAD_PATH = "./.data/uploads";
|
||||||
|
VIDEO_PATH = "./.data/videos";
|
||||||
|
|
||||||
TEAMSPEAK_LINK = "http://example.com";
|
TEAMSPEAK_LINK = "http://example.com";
|
||||||
DISCORD_LINK = "http://example.com";
|
DISCORD_LINK = "http://example.com";
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
process-compose.dev = {
|
process-compose.dev = {
|
||||||
imports = [ inputs.services-flake.processComposeModules.default ];
|
imports = [ inputs.services-flake.processComposeModules.default ];
|
||||||
|
|
||||||
cli.preHook = "mkdir -p .data/uploads .data/logs";
|
cli.preHook = "mkdir -p .data/uploads .data/videos .data/logs";
|
||||||
|
|
||||||
defaults.processSettings = { name, ... }: {
|
defaults.processSettings = { name, ... }: {
|
||||||
availability.restart = lib.mkDefault "on_failure";
|
availability.restart = lib.mkDefault "on_failure";
|
||||||
|
|||||||
Generated
+397
@@ -14,10 +14,12 @@
|
|||||||
"@iconify-json/heroicons": "^1.2.3",
|
"@iconify-json/heroicons": "^1.2.3",
|
||||||
"@iconify/tailwind4": "^1.2.3",
|
"@iconify/tailwind4": "^1.2.3",
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
|
"@videojs/html": "^10.2.0",
|
||||||
"astro": "^7.4.4",
|
"astro": "^7.4.4",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"daisyui": "^5.7.50",
|
"daisyui": "^5.7.50",
|
||||||
"drizzle-orm": "^0.45.4",
|
"drizzle-orm": "^0.45.4",
|
||||||
|
"music-metadata": "^12.3.2",
|
||||||
"mysql2": "^3.24.6",
|
"mysql2": "^3.24.6",
|
||||||
"nanostores": "^1.5.5",
|
"nanostores": "^1.5.5",
|
||||||
"pino": "^10.4.0",
|
"pino": "^10.4.0",
|
||||||
@@ -425,6 +427,16 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@borewit/text-codec": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.3.tgz",
|
||||||
|
"integrity": "sha512-49CycTz1aAZmvFcwkeFyhud1HUmRIB5Tabf7ALV+VA4Iu85MTfz2cUwzPagMFAdVBvrWdkCxG67I30tUwwpKsw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Borewit"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@bruits/satteri-darwin-arm64": {
|
"node_modules/@bruits/satteri-darwin-arm64": {
|
||||||
"version": "0.10.5",
|
"version": "0.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz",
|
"resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz",
|
||||||
@@ -1525,6 +1537,30 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@lit-labs/ssr-dom-shim": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@lit/context": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-M26qDE6UkQbZA2mQ3RjJ3Gzd8TxP+/0obMgE5HfkfLhEEyYE3Bui4A5XHiGPjy0MUGAyxB3QgVuw2ciS0kHn6A==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@lit/reactive-element": "^1.6.2 || ^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lit/reactive-element": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@lit-labs/ssr-dom-shim": "^1.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz",
|
||||||
@@ -2764,6 +2800,30 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tokenizer/inflate": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-+0RDxhBkyT9xMnGdJAwmats9yxRLR8ypJrQgrnLqOu4Gv/aHlnxYgdGVy2QTwvBl6IF/w2Hr+ZNirH+iMUbVZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"strtok3": "^10.6.2",
|
||||||
|
"token-types": "^6.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Borewit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tokenizer/token": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.4",
|
"version": "0.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
|
||||||
@@ -3115,6 +3175,165 @@
|
|||||||
"integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
|
"integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/@videojs/core": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/core/-/core-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-xYF4aWKq4Us0SzxwAZmpbeONgTSogH3gF0yfDlZEusaPrpx1rsXwAELas1PjcwzUC+jwMAQ54C6q3RAZ/xbHwA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@videojs/media": "10.2.0",
|
||||||
|
"@videojs/store": "10.2.0",
|
||||||
|
"@videojs/utils": "10.2.0",
|
||||||
|
"video.js": "10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/element": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/element/-/element-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-eGsRhK7nxUkPbjrXGHZDlNJsVTovJCzg6w1+B3aOOWOpfxvLSEUwR8KJ+VPtzqAcbS3lZ5LQjxFyomv1dirh+Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@lit/context": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/html": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/html/-/html-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-Bas9JqHlbiLx9oLhLvCWJr+UoUQ9Xb/Z156O7d/DQ8ujxuV8I7f6vWgkcLOjUjp78rBZjPIy/O+wMPBNbIJpSw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@videojs/core": "10.2.0",
|
||||||
|
"@videojs/element": "10.2.0",
|
||||||
|
"@videojs/media": "10.2.0",
|
||||||
|
"@videojs/native-hls-video": "10.2.0",
|
||||||
|
"@videojs/spf": "10.2.0",
|
||||||
|
"@videojs/store": "10.2.0",
|
||||||
|
"@videojs/utils": "10.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@videojs/cloudflare-video": "10.2.0",
|
||||||
|
"@videojs/dash-video": "10.2.0",
|
||||||
|
"@videojs/google-cast": "10.2.0",
|
||||||
|
"@videojs/hlsjs-video": "10.2.0",
|
||||||
|
"@videojs/mux-audio": "10.2.0",
|
||||||
|
"@videojs/mux-data": "10.2.0",
|
||||||
|
"@videojs/mux-video": "10.2.0",
|
||||||
|
"@videojs/playerjs-video": "10.2.0",
|
||||||
|
"@videojs/shaka-video": "10.2.0",
|
||||||
|
"@videojs/spotify-audio": "10.2.0",
|
||||||
|
"@videojs/tiktok-video": "10.2.0",
|
||||||
|
"@videojs/twitch-video": "10.2.0",
|
||||||
|
"@videojs/vimeo-video": "10.2.0",
|
||||||
|
"@videojs/wistia-video": "10.2.0",
|
||||||
|
"@videojs/youtube-video": "10.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@videojs/cloudflare-video": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/dash-video": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/google-cast": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/hlsjs-video": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/mux-audio": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/mux-data": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/mux-video": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/playerjs-video": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/shaka-video": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/spotify-audio": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/tiktok-video": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/twitch-video": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/vimeo-video": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/wistia-video": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@videojs/youtube-video": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/media": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/media/-/media-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-bdlq1nav3xeuWsmxN1eWEdJcbS1QxHoMZq2+4DJ19yWAGSheXDt52xDNTgNhoKY0FoJFTdNEsv/4qxR/eW+IjA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@videojs/utils": "10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/native-hls-video": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/native-hls-video/-/native-hls-video-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-+0ISYTxbPtXc530CvTqCkoKrOPzxpAqvdgw79cRTOAG/7sR4xgQqt341O+P0HesH5kYeMwkLfWcSM964FAcMTw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@videojs/media": "10.2.0",
|
||||||
|
"@videojs/spf": "10.2.0",
|
||||||
|
"@videojs/utils": "10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/spf": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/spf/-/spf-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-ovoHDqR5td0nfeZeCu2GWbdb3SVqP9udEAZr/4/VNUKYKstOg21rlGYS/We/our17kkxcp6Dvg7H98xLzn83pQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@videojs/media": "10.2.0",
|
||||||
|
"@videojs/utils": "10.2.0",
|
||||||
|
"signal-polyfill": "^0.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/store": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/store/-/store-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-rKV2tCigF1ifVFk1n+svGMpWLdEsBl3LHYbD9hrHQZtSI2SAwgwvD5W5vA0hcc0W3f9cblsrS0zgT/JxQlKfrw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@videojs/utils": "10.2.0",
|
||||||
|
"react-compiler-runtime": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@videojs/element": "10.2.0",
|
||||||
|
"react": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@videojs/element": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/utils": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/utils/-/utils-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-+uV+rg5Dtq0OpPT3OQsybA+grWyDaluFIQ6f5X8Jqb98gy0momQBo7yPA92TlVL7eiUrPpYb4eX0xeTXLKL73Q==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.19.0",
|
"version": "8.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz",
|
||||||
@@ -3530,6 +3749,19 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/content-type": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-GW4qUsfFo59d0HbUibDlWv5wPz+vAAcaTWbKIuKCf0JkC7wWkSyf8f13IpXn5JkeMlB2P8iTSSIjwXljorg2vA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz",
|
||||||
@@ -4409,6 +4641,24 @@
|
|||||||
"flat-cache": "^6.1.23"
|
"flat-cache": "^6.1.23"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-type": {
|
||||||
|
"version": "22.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-type/-/file-type-22.3.0.tgz",
|
||||||
|
"integrity": "sha512-PrPedgnnWl5bWz3JHgar0QSzRvzeyFh7dZ9JpYOgcPZu4KALS9zw4MMa6qXRYyjeBsAlpaWB2boLuG991+0HIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tokenizer/inflate": "^0.6.0",
|
||||||
|
"strtok3": "^10.6.3",
|
||||||
|
"token-types": "^6.1.2",
|
||||||
|
"uint8array-extras": "^1.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -4719,6 +4969,26 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -5270,6 +5540,19 @@
|
|||||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/media-typer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/meshoptimizer": {
|
"node_modules/meshoptimizer": {
|
||||||
"version": "0.18.1",
|
"version": "0.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
|
||||||
@@ -5457,6 +5740,37 @@
|
|||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/music-metadata": {
|
||||||
|
"version": "12.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-12.3.2.tgz",
|
||||||
|
"integrity": "sha512-U+Wu8c/7xa8wP331oVuRTAJSHcEWQIkLjpuyRJG+CAIszGYhpr5CRDv6PqkkVHDoX3ndjmdJlB6S49AaLq8SbQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Borewit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "buymeacoffee",
|
||||||
|
"url": "https://buymeacoffee.com/borewit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@borewit/text-codec": "^0.2.2",
|
||||||
|
"@tokenizer/token": "^0.3.0",
|
||||||
|
"content-type": "^3.1.1",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"file-type": "^22.2.0",
|
||||||
|
"media-typer": "^2.0.0",
|
||||||
|
"strtok3": "^10.6.2",
|
||||||
|
"token-types": "^6.1.2",
|
||||||
|
"uint8array-extras": "^1.6.0",
|
||||||
|
"win-guid": "^0.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mysql2": {
|
"node_modules/mysql2": {
|
||||||
"version": "3.24.6",
|
"version": "3.24.6",
|
||||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.6.tgz",
|
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.6.tgz",
|
||||||
@@ -6163,6 +6477,25 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "19.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz",
|
||||||
|
"integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-compiler-runtime": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
|
||||||
@@ -6838,6 +7171,12 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/signal-polyfill": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-polyfill/-/signal-polyfill-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-p63Y4Er5/eMQ9RHg0M0Y64NlsQKpiu6MDdhBXpyywRuWiPywhJTpKJ1iB5K2hJEbFZ0BnDS7ZkJ+0AfTuL37Rg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/sisteransi": {
|
"node_modules/sisteransi": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||||
@@ -6954,6 +7293,22 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strtok3": {
|
||||||
|
"version": "10.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.6.3.tgz",
|
||||||
|
"integrity": "sha512-MD8mENgyXMr2mDaDfzP3RWYDSyYQxIIou7HhCiCJiBekrsoDtUG0id3vYnjDfY9ndOEbazFXe13Qw6u/1AB8ew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tokenizer/token": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Borewit"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/suf-log": {
|
"node_modules/suf-log": {
|
||||||
"version": "2.5.3",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz",
|
||||||
@@ -7286,6 +7641,24 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/token-types": {
|
||||||
|
"version": "6.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
|
||||||
|
"integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@borewit/text-codec": "^0.2.1",
|
||||||
|
"@tokenizer/token": "^0.3.0",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Borewit"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/trim-lines": {
|
"node_modules/trim-lines": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||||
@@ -7381,6 +7754,18 @@
|
|||||||
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
|
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uint8array-extras": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ultrahtml": {
|
"node_modules/ultrahtml": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz",
|
||||||
@@ -7666,6 +8051,12 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/video.js": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/video.js/-/video.js-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-YoezDYXY6iDkuhLGnyOz96XBKjwB0cQbPNq5ck1FAf2JcqfLnnwkNQ4ofbwhlpAM55RFp6VDhSvKe5wVcFV11Q==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.3.5",
|
"version": "8.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
|
||||||
@@ -7778,6 +8169,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/win-guid": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
|
|||||||
@@ -17,10 +17,12 @@
|
|||||||
"@iconify-json/heroicons": "^1.2.3",
|
"@iconify-json/heroicons": "^1.2.3",
|
||||||
"@iconify/tailwind4": "^1.2.3",
|
"@iconify/tailwind4": "^1.2.3",
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
|
"@videojs/html": "^10.2.0",
|
||||||
"astro": "^7.4.4",
|
"astro": "^7.4.4",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"daisyui": "^5.7.50",
|
"daisyui": "^5.7.50",
|
||||||
"drizzle-orm": "^0.45.4",
|
"drizzle-orm": "^0.45.4",
|
||||||
|
"music-metadata": "^12.3.2",
|
||||||
"mysql2": "^3.24.6",
|
"mysql2": "^3.24.6",
|
||||||
"nanostores": "^1.5.5",
|
"nanostores": "^1.5.5",
|
||||||
"pino": "^10.4.0",
|
"pino": "^10.4.0",
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import '@videojs/html/video/player';
|
||||||
|
import '@videojs/html/video/skin';
|
||||||
|
import '@videojs/html/i18n';
|
||||||
|
import '@videojs/html/i18n/locales/de/register';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
src: string;
|
||||||
|
type?: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { src, type, title }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<media-i18n lang="de">
|
||||||
|
<video-player content-title={title}>
|
||||||
|
<video-skin>
|
||||||
|
<!-- svelte-ignore a11y_media_has_caption -->
|
||||||
|
<video {src} {type} playsinline preload="metadata">
|
||||||
|
<a href={src}>Video kann nicht abgespielt werden</a>
|
||||||
|
</video>
|
||||||
|
</video-skin>
|
||||||
|
</video-player>
|
||||||
|
</media-i18n>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
media-i18n {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
video-player {
|
||||||
|
--media-accent-color: var(--color-primary);
|
||||||
|
--media-border-radius: var(--radius-box);
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
video-skin {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
|
||||||
|
import VideoPlayer from '@app/website/video/VideoPlayer.svelte';
|
||||||
|
import { getVideoMetadata, resolveVideo } from '@util/video.ts';
|
||||||
|
|
||||||
|
const { name } = Astro.params;
|
||||||
|
|
||||||
|
const video = name ? await resolveVideo(name) : null;
|
||||||
|
if (!video) return new Response(null, { status: 404 });
|
||||||
|
|
||||||
|
const metadata = await getVideoMetadata(video);
|
||||||
|
if (!metadata) return new Response(null, { status: 404 });
|
||||||
|
|
||||||
|
const { title, description } = metadata;
|
||||||
|
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||||
|
const streamUrl = `${basePath}/video/${encodeURIComponent(video.name)}/stream`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WebsiteLayout title={`${title} – Video`}>
|
||||||
|
<section class="mx-auto flex w-full max-w-6xl flex-col gap-5 px-4 py-8 lg:py-12">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm tracking-widest text-base-content/60 uppercase">Video</p>
|
||||||
|
<h1 class="mt-1 text-2xl font-semibold break-words lg:text-4xl">{title}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VideoPlayer client:load src={streamUrl} type={video.contentType} title={title} />
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<p class="max-w-3xl text-base leading-relaxed whitespace-pre-line text-base-content/70">{description}</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</WebsiteLayout>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { APIContext, APIRoute } from 'astro';
|
||||||
|
import { createVideoResponse, resolveVideo } from '@util/video.ts';
|
||||||
|
|
||||||
|
async function handle(context: APIContext, headersOnly = false) {
|
||||||
|
const video = await resolveVideo(context.params.name ?? '');
|
||||||
|
if (!video) return new Response(null, { status: 404 });
|
||||||
|
|
||||||
|
return createVideoResponse(video, context.request, headersOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HEAD: APIRoute = (context) => handle(context, true);
|
||||||
|
export const GET: APIRoute = (context) => handle(context);
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { parseFile } from 'music-metadata';
|
||||||
|
import { VIDEO_PATH } from 'astro:env/server';
|
||||||
|
import { logger } from './log';
|
||||||
|
|
||||||
|
const videoTypes = [
|
||||||
|
['mp4', 'video/mp4'],
|
||||||
|
['webm', 'video/webm']
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface ResolvedVideo {
|
||||||
|
name: string;
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
contentType: string;
|
||||||
|
size: number;
|
||||||
|
modifiedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoRange {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoMetadata {
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoMetadataCacheEntry {
|
||||||
|
modifiedAtMs: number;
|
||||||
|
size: number;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoMetadataCache = new Map<string, VideoMetadataCacheEntry>();
|
||||||
|
|
||||||
|
export async function resolveVideo(name: string): Promise<ResolvedVideo | null> {
|
||||||
|
if (!VIDEO_PATH || !name) return null;
|
||||||
|
|
||||||
|
const directory = path.resolve(VIDEO_PATH);
|
||||||
|
const safeName = path.basename(name);
|
||||||
|
|
||||||
|
if (!safeName) return null;
|
||||||
|
|
||||||
|
for (const [extension, contentType] of videoTypes) {
|
||||||
|
const filename = `${safeName}.${extension}`;
|
||||||
|
const filePath = path.join(directory, filename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await fs.promises.stat(filePath);
|
||||||
|
if (!stats.isFile()) continue;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: safeName,
|
||||||
|
filename,
|
||||||
|
path: filePath,
|
||||||
|
contentType,
|
||||||
|
size: stats.size,
|
||||||
|
modifiedAt: stats.mtime
|
||||||
|
};
|
||||||
|
} catch (e: NodeJS.ErrnoException) {
|
||||||
|
// check if file doesn't exist
|
||||||
|
if (e.code === 'ENOENT') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logger.warn(
|
||||||
|
{
|
||||||
|
error: e.message
|
||||||
|
},
|
||||||
|
'error while reading video file'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVideoMetadata(video: ResolvedVideo): Promise<VideoMetadata | null> {
|
||||||
|
const modifiedAtMs = video.modifiedAt.getTime();
|
||||||
|
const cached = videoMetadataCache.get(video.path);
|
||||||
|
|
||||||
|
if (cached && cached.modifiedAtMs === modifiedAtMs && cached.size === video.size) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata;
|
||||||
|
try {
|
||||||
|
const parsed = await parseFile(video.path, {
|
||||||
|
duration: false,
|
||||||
|
skipCovers: true
|
||||||
|
});
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
title: parsed.common.title?.trim() || video.name,
|
||||||
|
description:
|
||||||
|
parsed.common.longDescription?.trim() ||
|
||||||
|
parsed.common.description?.map((value) => value.trim()).find(Boolean) ||
|
||||||
|
null
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// couldn't read metadata
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
videoMetadataCache.set(video.path, {
|
||||||
|
modifiedAtMs,
|
||||||
|
size: video.size,
|
||||||
|
...metadata
|
||||||
|
});
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseVideoRange(rangeHeader: string | null, size: number): VideoRange | 'invalid' | null {
|
||||||
|
if (!rangeHeader) return null;
|
||||||
|
if (size <= 0) return 'invalid';
|
||||||
|
|
||||||
|
const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
|
||||||
|
if (!match) return 'invalid';
|
||||||
|
|
||||||
|
const [, startValue, endValue] = match;
|
||||||
|
if (!startValue && !endValue) return 'invalid';
|
||||||
|
|
||||||
|
let start: number;
|
||||||
|
let end: number;
|
||||||
|
|
||||||
|
if (!startValue) {
|
||||||
|
const suffixLength = Number(endValue);
|
||||||
|
if (!Number.isSafeInteger(suffixLength) || suffixLength <= 0) return 'invalid';
|
||||||
|
|
||||||
|
start = Math.max(size - suffixLength, 0);
|
||||||
|
end = size - 1;
|
||||||
|
} else {
|
||||||
|
start = Number(startValue);
|
||||||
|
if (!Number.isSafeInteger(start) || start < 0 || start >= size) return 'invalid';
|
||||||
|
|
||||||
|
end = endValue ? Number(endValue) : size - 1;
|
||||||
|
if (!Number.isSafeInteger(end) || end < start) return 'invalid';
|
||||||
|
|
||||||
|
end = Math.min(end, size - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createVideoResponse(video: ResolvedVideo, request: Request, headersOnly = false): Response {
|
||||||
|
const range = parseVideoRange(request.headers.get('range'), video.size);
|
||||||
|
|
||||||
|
if (range === 'invalid') {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 416,
|
||||||
|
headers: {
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'Content-Range': `bytes */${video.size}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = range?.start ?? 0;
|
||||||
|
const end = range?.end ?? video.size - 1;
|
||||||
|
const contentLength = video.size === 0 ? 0 : end - start + 1;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'Content-Length': contentLength.toString(),
|
||||||
|
'Content-Type': video.contentType,
|
||||||
|
'Cache-Control': 'public, max-age=3600',
|
||||||
|
'Last-Modified': video.modifiedAt.toUTCString()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (range) {
|
||||||
|
headers['Content-Range'] = `bytes ${start}-${end}/${video.size}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headersOnly) {
|
||||||
|
return new Response(null, { status: range ? 206 : 200, headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = fs.createReadStream(video.path, video.size === 0 ? undefined : { start, end });
|
||||||
|
|
||||||
|
return new Response(stream as any, { status: range ? 206 : 200, headers });
|
||||||
|
}
|
||||||
Reference in new issue
Block a user