Design Prot Merge #1
@@ -0,0 +1,15 @@
|
|||||||
|
name: musify-share
|
||||||
|
|
||||||
|
# Temporary overlay for exposing the stack outside the LAN, on top of
|
||||||
|
# compose.dev.yml. Only difference from a normal dev run: Zitadel writes its
|
||||||
|
# init PAT to a separate host directory, so it never overwrites
|
||||||
|
# zitadel/.output (used by the musify-dev project's own Zitadel instance).
|
||||||
|
#
|
||||||
|
# docker compose --env-file deploy/.env.share \
|
||||||
|
# -f deploy/compose.yml -f deploy/compose.dev.yml -f deploy/compose.share.yml \
|
||||||
|
# --profile apps up -d --build api gateway web-player
|
||||||
|
|
||||||
|
services:
|
||||||
|
zitadel:
|
||||||
|
volumes:
|
||||||
|
- ./zitadel/.output-share:/output
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
eyJhbGciOiJBMjU2R0NNS1ciLCJlbmMiOiJBMjU2R0NNIiwiaXYiOiJ5Yk1hUFdVUmNqcU44WDNGIiwia2lkIjoib2lkY0tleSIsInRhZyI6Ikw2NjYwN3FRVzZNYVJLaVl3Q0w1N3cifQ.BE_7qqA1Auer5yEoGnyN0v35JlZlK3XgCo0s_CQtG7s.cOf9oudMHRnVTOGj.0DRh9kj7oahl4imvMjxqRLUOZo6NhYlQyslf9_x4XF_TKi1dnQ.Uu36qEL5J1VKSIYcvWlbAg
|
||||||
@@ -15,7 +15,14 @@
|
|||||||
param(
|
param(
|
||||||
[string]$ProjectName = "Musify",
|
[string]$ProjectName = "Musify",
|
||||||
[string]$ApiAppName = "Musify API (Scalar)",
|
[string]$ApiAppName = "Musify API (Scalar)",
|
||||||
[string]$WebAppName = "Musify Web"
|
[string]$WebAppName = "Musify Web",
|
||||||
|
# Alternate .env to read/write (e.g. deploy/.env.share). Defaults to deploy/.env.
|
||||||
|
[string]$EnvFile,
|
||||||
|
# Skip writing Musify.Api user-secrets and web-player/.env: use when the apps
|
||||||
|
# only ever run in Docker for this environment and get their config from compose.
|
||||||
|
[switch]$SkipHostWiring,
|
||||||
|
# Matches the zitadel volume mount for this environment (see compose.share.yml).
|
||||||
|
[string]$OutputDir = ".output"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -23,8 +30,8 @@ $deployDir = Split-Path $PSScriptRoot -Parent
|
|||||||
$repoDir = Split-Path $deployDir -Parent
|
$repoDir = Split-Path $deployDir -Parent
|
||||||
$apiProject = Join-Path $repoDir "backend\Musify.Api"
|
$apiProject = Join-Path $repoDir "backend\Musify.Api"
|
||||||
$webEnvFile = Join-Path $repoDir "web-player\.env"
|
$webEnvFile = Join-Path $repoDir "web-player\.env"
|
||||||
$envFile = Join-Path $deployDir ".env"
|
$envFile = if ($EnvFile) { $EnvFile } else { Join-Path $deployDir ".env" }
|
||||||
$patFile = Join-Path $PSScriptRoot ".output\admin-sa.pat"
|
$patFile = Join-Path $PSScriptRoot "$OutputDir\admin-sa.pat"
|
||||||
|
|
||||||
. (Join-Path $deployDir "scripts\common.ps1")
|
. (Join-Path $deployDir "scripts\common.ps1")
|
||||||
|
|
||||||
@@ -148,8 +155,11 @@ Set-DotEnvValue $envFile "AUTH_CLIENT_ID" $apiClientId
|
|||||||
Set-DotEnvValue $envFile "WEB_CLIENT_ID" $webClientId
|
Set-DotEnvValue $envFile "WEB_CLIENT_ID" $webClientId
|
||||||
Set-DotEnvValue $envFile "WEB_CLIENT_SECRET" $webSecret
|
Set-DotEnvValue $envFile "WEB_CLIENT_SECRET" $webSecret
|
||||||
|
|
||||||
# Musify.Api reads these from user-secrets when it runs outside Docker.
|
if ($SkipHostWiring) {
|
||||||
@{
|
Write-Host " wired into $envFile only (-SkipHostWiring: apps run in Docker)"
|
||||||
|
} else {
|
||||||
|
# Musify.Api reads these from user-secrets when it runs outside Docker.
|
||||||
|
@{
|
||||||
"Authentication:ClientId" = $apiClientId
|
"Authentication:ClientId" = $apiClientId
|
||||||
"Authentication:AudienceAddress" = $apiClientId
|
"Authentication:AudienceAddress" = $apiClientId
|
||||||
"Authentication:IssuerAddress" = $issuer
|
"Authentication:IssuerAddress" = $issuer
|
||||||
@@ -158,16 +168,16 @@ Set-DotEnvValue $envFile "WEB_CLIENT_SECRET" $webSecret
|
|||||||
"Authentication:TokenEndpoint" = "$issuer/oauth/v2/token"
|
"Authentication:TokenEndpoint" = "$issuer/oauth/v2/token"
|
||||||
"Authentication:ScalarRedirectUri" = "$apiUrl/scalar/"
|
"Authentication:ScalarRedirectUri" = "$apiUrl/scalar/"
|
||||||
"Authentication:RequireHttpsMetadata" = "false"
|
"Authentication:RequireHttpsMetadata" = "false"
|
||||||
}.GetEnumerator() | ForEach-Object {
|
}.GetEnumerator() | ForEach-Object {
|
||||||
dotnet user-secrets set $_.Key $_.Value --project $apiProject | Out-Null
|
dotnet user-secrets set $_.Key $_.Value --project $apiProject | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
# web-player/.env is what `npm run dev` reads (the container gets its config
|
# web-player/.env is what `npm run dev` reads (the container gets its config
|
||||||
# from compose instead).
|
# from compose instead).
|
||||||
if (-not (Test-Path $webEnvFile)) {
|
if (-not (Test-Path $webEnvFile)) {
|
||||||
Copy-Item (Join-Path $repoDir "web-player\.env.example") $webEnvFile
|
Copy-Item (Join-Path $repoDir "web-player\.env.example") $webEnvFile
|
||||||
}
|
}
|
||||||
@{
|
@{
|
||||||
ZITADEL_ISSUER = $issuer
|
ZITADEL_ISSUER = $issuer
|
||||||
ZITADEL_CLIENT_ID = $webClientId
|
ZITADEL_CLIENT_ID = $webClientId
|
||||||
ZITADEL_CLIENT_SECRET = $webSecret
|
ZITADEL_CLIENT_SECRET = $webSecret
|
||||||
@@ -175,8 +185,9 @@ if (-not (Test-Path $webEnvFile)) {
|
|||||||
AUTH_POST_LOGOUT_URI = "$viteUrl/"
|
AUTH_POST_LOGOUT_URI = "$viteUrl/"
|
||||||
SESSION_SECRET = $cfg['SESSION_SECRET']
|
SESSION_SECRET = $cfg['SESSION_SECRET']
|
||||||
API_BASE_URL = $apiUrl
|
API_BASE_URL = $apiUrl
|
||||||
}.GetEnumerator() | ForEach-Object {
|
}.GetEnumerator() | ForEach-Object {
|
||||||
Set-DotEnvValue $webEnvFile $_.Key $_.Value
|
Set-DotEnvValue $webEnvFile $_.Key $_.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host " wired into deploy/.env, Musify.Api user-secrets and web-player/.env"
|
Write-Host " wired into deploy/.env, Musify.Api user-secrets and web-player/.env"
|
||||||
|
}
|
||||||
|
|||||||
Generated
+89
-14
@@ -37,6 +37,29 @@
|
|||||||
"vite": "^8.0.16"
|
"vite": "^8.0.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz",
|
||||||
|
"integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
|
||||||
|
"integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||||
@@ -1098,7 +1121,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.69.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.69.1.tgz",
|
||||||
"integrity": "sha512-+nz8Fx/cElzb2ZPHC+6Ll3y3NEVIe4Na75PeplLlyTmd1dBXAjz2KR14y1ZgNjb2ThfAYzulu+PFy1UE3RCUzA==",
|
"integrity": "sha512-+nz8Fx/cElzb2ZPHC+6Ll3y3NEVIe4Na75PeplLlyTmd1dBXAjz2KR14y1ZgNjb2ThfAYzulu+PFy1UE3RCUzA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
"@sveltejs/acorn-typescript": "^1.0.9",
|
"@sveltejs/acorn-typescript": "^1.0.9",
|
||||||
@@ -1150,7 +1172,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz",
|
||||||
"integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==",
|
"integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
@@ -1388,6 +1409,72 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emnapi/core": "^1.7.1",
|
||||||
|
"@emnapi/runtime": "^1.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.2",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz",
|
||||||
@@ -1479,7 +1566,6 @@
|
|||||||
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
|
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -1541,7 +1627,6 @@
|
|||||||
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
|
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.64.0",
|
"@typescript-eslint/scope-manager": "8.64.0",
|
||||||
"@typescript-eslint/types": "8.64.0",
|
"@typescript-eslint/types": "8.64.0",
|
||||||
@@ -1649,7 +1734,6 @@
|
|||||||
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
},
|
},
|
||||||
@@ -1733,7 +1817,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||||
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -1968,7 +2051,6 @@
|
|||||||
"integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==",
|
"integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
@@ -3047,7 +3129,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -3181,7 +3262,6 @@
|
|||||||
"integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==",
|
"integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -3198,7 +3278,6 @@
|
|||||||
"integrity": "sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA==",
|
"integrity": "sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
@@ -3369,7 +3448,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
|
||||||
"integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
|
"integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.9"
|
"@types/estree": "1.0.9"
|
||||||
},
|
},
|
||||||
@@ -3504,7 +3582,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz",
|
||||||
"integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==",
|
"integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.4",
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@@ -3716,7 +3793,6 @@
|
|||||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -3778,7 +3854,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
|
||||||
"integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
|
"integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { toQueueItems, type ApiTrackLike, type QueueItem } from '$lib/player/player.svelte';
|
||||||
|
|
||||||
|
interface YouTubeAlbumTracksResponse {
|
||||||
|
album: { artist: string; thumbnailUrl: string };
|
||||||
|
tracks: { videoId: string; title: string; isExplicit: boolean }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAlbumQueueItems(
|
||||||
|
kind: 'local' | 'youtube',
|
||||||
|
albumId: string
|
||||||
|
): Promise<QueueItem[]> {
|
||||||
|
if (kind === 'local') {
|
||||||
|
const res = await fetch(`/api/albums/${albumId}/tracks`);
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const data = (await res.json()) as { items: ApiTrackLike[] };
|
||||||
|
return toQueueItems(data.items ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`/api/albums/youtube/${albumId}/tracks`);
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const detail = (await res.json()) as YouTubeAlbumTracksResponse;
|
||||||
|
return detail.tracks.map((track) => ({
|
||||||
|
id: track.videoId,
|
||||||
|
title: track.title,
|
||||||
|
artist: detail.album.artist,
|
||||||
|
source: 'youtube' as const,
|
||||||
|
coverUrl: detail.album.thumbnailUrl,
|
||||||
|
explicit: track.isExplicit
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -25,15 +25,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||||
|
import ListPlus from '@lucide/svelte/icons/list-plus';
|
||||||
|
import ListEnd from '@lucide/svelte/icons/list-end';
|
||||||
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
menu: AlbumMenuState;
|
menu: AlbumMenuState;
|
||||||
playlists: { id: string; name: string }[];
|
playlists: { id: string; name: string }[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onPlayNext: () => void;
|
||||||
|
onAddToQueue: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { menu, playlists, onClose }: Props = $props();
|
let { menu, playlists, onClose, onPlayNext, onAddToQueue }: Props = $props();
|
||||||
|
|
||||||
function onWindowKeydown(event: KeyboardEvent) {
|
function onWindowKeydown(event: KeyboardEvent) {
|
||||||
if (event.key === 'Escape') onClose();
|
if (event.key === 'Escape') onClose();
|
||||||
@@ -55,6 +59,22 @@
|
|||||||
class="fixed z-40 w-60 rounded-panel border border-line bg-elevated p-1.5 shadow-menu"
|
class="fixed z-40 w-60 rounded-panel border border-line bg-elevated p-1.5 shadow-menu"
|
||||||
style="left:{menu.x}px; top:{menu.y}px;"
|
style="left:{menu.x}px; top:{menu.y}px;"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={onPlayNext}
|
||||||
|
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm text-fg-2 transition hover:bg-hover"
|
||||||
|
>
|
||||||
|
<ListPlus class="h-5 w-5 shrink-0" strokeWidth={2} />
|
||||||
|
<span class="text-base">Reproducir a continuación</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={onAddToQueue}
|
||||||
|
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm text-fg-2 transition hover:bg-hover"
|
||||||
|
>
|
||||||
|
<ListEnd class="h-5 w-5 shrink-0" strokeWidth={2} />
|
||||||
|
<span class="text-base">Añadir a la cola</span>
|
||||||
|
</button>
|
||||||
{#if playlists.length > 0}
|
{#if playlists.length > 0}
|
||||||
<div class="group/addmenu relative">
|
<div class="group/addmenu relative">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
: ''}"
|
: ''}"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute inset-0 bg-scrim/25 opacity-0 transition-opacity duration-300 group-hover/tile:opacity-100"
|
class="pointer-events-none absolute inset-0 bg-scrim/25 opacity-0 transition-opacity duration-300"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="relative flex h-full flex-col justify-end p-3 sm:p-4 {banner
|
class="relative flex h-full flex-col justify-end p-3 sm:p-4 {banner
|
||||||
|
|||||||
@@ -342,15 +342,29 @@ class PlayerState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addToQueue(item: QueueItem) {
|
addToQueue(item: QueueItem) {
|
||||||
|
this.playNext([item]);
|
||||||
|
}
|
||||||
|
|
||||||
|
playNext(items: QueueItem[]) {
|
||||||
|
if (items.length === 0) return;
|
||||||
if (this.currentId === null) {
|
if (this.currentId === null) {
|
||||||
this.playQueue([item], 0);
|
this.playQueue(items, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tracks = [...this.tracks];
|
const tracks = [...this.tracks];
|
||||||
tracks.splice(this.#index() + 1, 0, toTrack(item));
|
tracks.splice(this.#index() + 1, 0, ...items.map(toTrack));
|
||||||
this.tracks = tracks;
|
this.tracks = tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appendToQueue(items: QueueItem[]) {
|
||||||
|
if (items.length === 0) return;
|
||||||
|
if (this.currentId === null) {
|
||||||
|
this.playQueue(items, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.tracks = [...this.tracks, ...items.map(toTrack)];
|
||||||
|
}
|
||||||
|
|
||||||
playTrack(id: string | number) {
|
playTrack(id: string | number) {
|
||||||
if (this.currentId === id) {
|
if (this.currentId === id) {
|
||||||
this.toggle();
|
this.toggle();
|
||||||
|
|||||||
@@ -174,5 +174,13 @@
|
|||||||
menu={albumMenu}
|
menu={albumMenu}
|
||||||
playlists={data.playlists}
|
playlists={data.playlists}
|
||||||
onClose={() => (albumMenu = null)}
|
onClose={() => (albumMenu = null)}
|
||||||
|
onPlayNext={() => {
|
||||||
|
player.playNext(queue);
|
||||||
|
albumMenu = null;
|
||||||
|
}}
|
||||||
|
onAddToQueue={() => {
|
||||||
|
player.appendToQueue(queue);
|
||||||
|
albumMenu = null;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { player } from '$lib/player/player.svelte';
|
import { player } from '$lib/player/player.svelte';
|
||||||
|
import { fetchAlbumQueueItems } from '$lib/albums';
|
||||||
import { fmtTime } from '$lib/format';
|
import { fmtTime } from '$lib/format';
|
||||||
import Page from '$lib/components/ui/Page.svelte';
|
import Page from '$lib/components/ui/Page.svelte';
|
||||||
import PageHeader from '$lib/components/ui/PageHeader.svelte';
|
import PageHeader from '$lib/components/ui/PageHeader.svelte';
|
||||||
@@ -163,14 +164,24 @@
|
|||||||
contextMenu = contextMenuStateFor(event, target);
|
contextMenu = contextMenuStateFor(event, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isForeignAlbum(album: LocalAlbum) {
|
|
||||||
return String(album.ownerUserId) !== data.user?.sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAlbumMenu(event: MouseEvent, kind: 'local' | 'youtube', albumId: string) {
|
function openAlbumMenu(event: MouseEvent, kind: 'local' | 'youtube', albumId: string) {
|
||||||
albumMenu = albumContextMenuStateFor(event, kind, albumId);
|
albumMenu = albumContextMenuStateFor(event, kind, albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function albumPlayNext() {
|
||||||
|
if (!albumMenu) return;
|
||||||
|
const { kind, albumId } = albumMenu;
|
||||||
|
albumMenu = null;
|
||||||
|
player.playNext(await fetchAlbumQueueItems(kind, albumId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function albumAddToQueue() {
|
||||||
|
if (!albumMenu) return;
|
||||||
|
const { kind, albumId } = albumMenu;
|
||||||
|
albumMenu = null;
|
||||||
|
player.appendToQueue(await fetchAlbumQueueItems(kind, albumId));
|
||||||
|
}
|
||||||
|
|
||||||
function addToQueue() {
|
function addToQueue() {
|
||||||
if (!contextMenu) return;
|
if (!contextMenu) return;
|
||||||
player.addToQueue(queueItemForTarget(contextMenu));
|
player.addToQueue(queueItemForTarget(contextMenu));
|
||||||
@@ -255,9 +266,7 @@
|
|||||||
href="/albums/{entry.album.id}"
|
href="/albums/{entry.album.id}"
|
||||||
class="group/card animate-enter block min-w-0 rounded-art focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
|
class="group/card animate-enter block min-w-0 rounded-art focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
|
||||||
style="animation-delay:{Math.min(i, 10) * 45}ms"
|
style="animation-delay:{Math.min(i, 10) * 45}ms"
|
||||||
oncontextmenu={isForeignAlbum(entry.album)
|
oncontextmenu={(e) => openAlbumMenu(e, 'local', entry.album.id)}
|
||||||
? (e) => openAlbumMenu(e, 'local', entry.album.id)
|
|
||||||
: undefined}
|
|
||||||
>
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<PlaylistArt
|
<PlaylistArt
|
||||||
@@ -395,5 +404,7 @@
|
|||||||
menu={albumMenu}
|
menu={albumMenu}
|
||||||
playlists={data.playlists}
|
playlists={data.playlists}
|
||||||
onClose={() => (albumMenu = null)}
|
onClose={() => (albumMenu = null)}
|
||||||
|
onPlayNext={albumPlayNext}
|
||||||
|
onAddToQueue={albumAddToQueue}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { createApiClient, requireUser } from '$lib/server/api';
|
import { createApiClient, requireUser } from '$lib/server/api';
|
||||||
import { fetchYoutubeFiller } from '$lib/server/youtube';
|
import { fetchYoutubeFiller } from '$lib/server/youtube';
|
||||||
import { addTrackAction, addYouTubeToPlaylistAction } from '$lib/server/playlistActions';
|
import {
|
||||||
|
addAlbumToPlaylistAction,
|
||||||
|
addTrackAction,
|
||||||
|
addYouTubeToPlaylistAction
|
||||||
|
} from '$lib/server/playlistActions';
|
||||||
import {
|
import {
|
||||||
HOME_ALBUMS_LIMIT,
|
HOME_ALBUMS_LIMIT,
|
||||||
HOME_LATEST_PAGE_SIZE,
|
HOME_LATEST_PAGE_SIZE,
|
||||||
@@ -62,5 +66,6 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
|
|||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
addTrack: addTrackAction,
|
addTrack: addTrackAction,
|
||||||
addYouTubeToPlaylist: addYouTubeToPlaylistAction
|
addYouTubeToPlaylist: addYouTubeToPlaylistAction,
|
||||||
|
addAlbumToPlaylist: addAlbumToPlaylistAction
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,11 @@
|
|||||||
contextMenuStateFor,
|
contextMenuStateFor,
|
||||||
type ContextMenuState
|
type ContextMenuState
|
||||||
} from '$lib/components/TrackContextMenu.svelte';
|
} from '$lib/components/TrackContextMenu.svelte';
|
||||||
|
import AlbumContextMenu, {
|
||||||
|
albumContextMenuStateFor,
|
||||||
|
type AlbumMenuState
|
||||||
|
} from '$lib/components/AlbumContextMenu.svelte';
|
||||||
|
import { fetchAlbumQueueItems } from '$lib/albums';
|
||||||
import type { YouTubeSong } from '$lib/types';
|
import type { YouTubeSong } from '$lib/types';
|
||||||
import { appendUnique } from '$lib/collections';
|
import { appendUnique } from '$lib/collections';
|
||||||
import { HOME_LATEST_PAGE_SIZE } from '$lib/config';
|
import { HOME_LATEST_PAGE_SIZE } from '$lib/config';
|
||||||
@@ -33,6 +38,7 @@
|
|||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let contextMenu = $state<ContextMenuState | null>(null);
|
let contextMenu = $state<ContextMenuState | null>(null);
|
||||||
|
let albumMenu = $state<AlbumMenuState | null>(null);
|
||||||
let youtubeFillerItems = $state<YouTubeSong[]>([]);
|
let youtubeFillerItems = $state<YouTubeSong[]>([]);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -109,6 +115,24 @@
|
|||||||
contextMenu = null;
|
contextMenu = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openAlbumMenu(event: MouseEvent, albumId: string) {
|
||||||
|
albumMenu = albumContextMenuStateFor(event, 'local', albumId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function albumPlayNext() {
|
||||||
|
if (!albumMenu) return;
|
||||||
|
const { kind, albumId } = albumMenu;
|
||||||
|
albumMenu = null;
|
||||||
|
player.playNext(await fetchAlbumQueueItems(kind, albumId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function albumAddToQueue() {
|
||||||
|
if (!albumMenu) return;
|
||||||
|
const { kind, albumId } = albumMenu;
|
||||||
|
albumMenu = null;
|
||||||
|
player.appendToQueue(await fetchAlbumQueueItems(kind, albumId));
|
||||||
|
}
|
||||||
|
|
||||||
function albumCaption(album: (typeof albums)[number]) {
|
function albumCaption(album: (typeof albums)[number]) {
|
||||||
return [
|
return [
|
||||||
album.releaseYear ? String(album.releaseYear) : undefined,
|
album.releaseYear ? String(album.releaseYear) : undefined,
|
||||||
@@ -195,6 +219,7 @@
|
|||||||
: `/albums/${album.id}`}
|
: `/albums/${album.id}`}
|
||||||
class="group/card animate-enter block w-42 shrink-0 rounded-art focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
|
class="group/card animate-enter block w-42 shrink-0 rounded-art focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
|
||||||
style="animation-delay:{Math.min(i, 10) * 45}ms"
|
style="animation-delay:{Math.min(i, 10) * 45}ms"
|
||||||
|
oncontextmenu={(e) => openAlbumMenu(e, album.id)}
|
||||||
>
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
{#if album.thumbnailUrl}
|
{#if album.thumbnailUrl}
|
||||||
@@ -266,9 +291,13 @@
|
|||||||
>
|
>
|
||||||
{playlist.name}
|
{playlist.name}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-0.5 truncate text-sm text-muted">
|
||||||
{#if playlist.description}
|
{#if playlist.description}
|
||||||
<div class="mt-0.5 truncate text-sm text-muted">{playlist.description}</div>
|
{playlist.description}
|
||||||
|
{:else}
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
<a
|
<a
|
||||||
@@ -330,7 +359,11 @@
|
|||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
{#if recentlyPlayed.length === 0 && albums.length === 0 && mixes.length === 0 && playlists.length === 0 && newTargets.length === 0}
|
{#if recentlyPlayed.length === 0 && albums.length === 0 && mixes.length === 0 && playlists.length === 0 && newTargets.length === 0}
|
||||||
<EmptyState icon={Music} title="Todavía no hay música" description="Sube tu música o explora nuevas canciones, álbumes y listas de reproducción." />
|
<EmptyState
|
||||||
|
icon={Music}
|
||||||
|
title="Todavía no hay música"
|
||||||
|
description="Sube tu música o explora nuevas canciones, álbumes y listas de reproducción."
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -387,3 +420,13 @@
|
|||||||
onAddToQueue={addToQueue}
|
onAddToQueue={addToQueue}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if albumMenu}
|
||||||
|
<AlbumContextMenu
|
||||||
|
menu={albumMenu}
|
||||||
|
{playlists}
|
||||||
|
onClose={() => (albumMenu = null)}
|
||||||
|
onPlayNext={albumPlayNext}
|
||||||
|
onAddToQueue={albumAddToQueue}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { createApiClient, unwrapOrError } from '$lib/server/api';
|
||||||
|
import { ALBUM_TRACKS_PAGE_SIZE } from '$lib/config';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ params, locals, fetch }) => {
|
||||||
|
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
|
||||||
|
const result = await api.GET('/albums/{albumId}/tracks', {
|
||||||
|
params: {
|
||||||
|
path: { albumId: params.id },
|
||||||
|
query: { pageNumber: 1, pageSize: ALBUM_TRACKS_PAGE_SIZE }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return json(unwrapOrError(result, 'No se pudo cargar el álbum.'));
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { createApiClient, unwrapOrError } from '$lib/server/api';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ params, locals, fetch }) => {
|
||||||
|
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
|
||||||
|
const result = await api.GET('/youtube/albums/{albumId}', {
|
||||||
|
params: { path: { albumId: params.albumId } }
|
||||||
|
});
|
||||||
|
|
||||||
|
return json(unwrapOrError(result, 'No se pudo cargar el álbum de YouTube.'));
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user