add video player
deploy / build-and-deploy (push) Successful in 27s

This commit is contained in:
bytedream committed 2026-10-31 01:16:07 +01:00
1 parent 0fc0024cb6
commit 73d4f6af42
9 files changed
+678 -2

No files matched your search

+2 -1
View File
@@ -6,7 +6,8 @@ ADMIN_USER=admin
ADMIN_PASSWORD=admin
ADMIN_COOKIE=muelleel
UPLOAD_PATH=/tmp
UPLOAD_PATH=/tmp/uploads
VIDEO_PATH=/tmp/videos
TEAMSPEAK_LINK=http://example.com
DISCORD_LINK=http://example.com
+2
View File
@@ -36,6 +36,8 @@ export default defineConfig({
ADMIN_COOKIE: envField.string({ context: 'server', access: 'secret', default: 'muelleel' }),
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 }),
START_DATE: envField.string({ context: 'server', access: 'secret', default: '1970-01-01' }),
+2 -1
View File
@@ -42,6 +42,7 @@
ADMIN_COOKIE = "muelleel";
UPLOAD_PATH = "./.data/uploads";
VIDEO_PATH = "./.data/videos";
TEAMSPEAK_LINK = "http://example.com";
DISCORD_LINK = "http://example.com";
@@ -71,7 +72,7 @@
process-compose.dev = {
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, ... }: {
availability.restart = lib.mkDefault "on_failure";
+397
View File
@@ -14,10 +14,12 @@
"@iconify-json/heroicons": "^1.2.3",
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/vite": "^4.3.3",
"@videojs/html": "^10.2.0",
"astro": "^7.4.4",
"bcrypt": "^6.0.0",
"daisyui": "^5.7.50",
"drizzle-orm": "^0.45.4",
"music-metadata": "^12.3.2",
"mysql2": "^3.24.6",
"nanostores": "^1.5.5",
"pino": "^10.4.0",
@@ -425,6 +427,16 @@
"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": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz",
@@ -1525,6 +1537,30 @@
"dev": true,
"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": {
"version": "1.2.6",
"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"
}
},
"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": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
@@ -3115,6 +3175,165 @@
"integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
"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": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz",
@@ -3530,6 +3749,19 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz",
@@ -4409,6 +4641,24 @@
"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": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -4719,6 +4969,26 @@
"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": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5270,6 +5540,19 @@
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
"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": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
@@ -5457,6 +5740,37 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"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": {
"version": "3.24.6",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.6.tgz",
@@ -6163,6 +6477,25 @@
"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": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
@@ -6838,6 +7171,12 @@
"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": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -6954,6 +7293,22 @@
"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": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz",
@@ -7286,6 +7641,24 @@
"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": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -7381,6 +7754,18 @@
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
"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": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz",
@@ -7666,6 +8051,12 @@
"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": {
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
@@ -7778,6 +8169,12 @@
"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": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+2
View File
@@ -17,10 +17,12 @@
"@iconify-json/heroicons": "^1.2.3",
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/vite": "^4.3.3",
"@videojs/html": "^10.2.0",
"astro": "^7.4.4",
"bcrypt": "^6.0.0",
"daisyui": "^5.7.50",
"drizzle-orm": "^0.45.4",
"music-metadata": "^12.3.2",
"mysql2": "^3.24.6",
"nanostores": "^1.5.5",
"pino": "^10.4.0",
+44
View File
@@ -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>
+32
View File
@@ -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>
+12
View File
@@ -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);
+185
View File
@@ -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 });
}