From e4948d955429af1c9d52167d5b53b122791d5ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Matz=20Andr=C3=A9n?= Date: Thu, 14 May 2026 16:08:54 +0200 Subject: [PATCH 01/30] Add basic hardening for auth and download paths --- src/backend/.env.default | 6 ++- src/backend/.env.docker | 6 ++- src/backend/src/app.module.ts | 9 +++- src/backend/src/environmentEnum.ts | 2 + src/backend/src/main.ts | 43 +++++++++------- src/backend/src/playlist/playlist.service.ts | 2 +- src/backend/src/shared/auth.guard.ts | 50 +++++++++++++++++++ src/backend/src/shared/utils.service.ts | 21 ++++++-- src/backend/src/track/track.service.ts | 32 ++++++------ src/frontend/src/app/app.config.ts | 15 ++++-- .../app/services/auth-token.interceptor.ts | 19 +++++++ 11 files changed, 158 insertions(+), 47 deletions(-) create mode 100644 src/backend/src/shared/auth.guard.ts create mode 100644 src/frontend/src/app/services/auth-token.interceptor.ts diff --git a/src/backend/.env.default b/src/backend/.env.default index 1aa8fb9..bb3fbac 100644 --- a/src/backend/.env.default +++ b/src/backend/.env.default @@ -14,4 +14,8 @@ SPOTIFY_CLIENT_SECRET=your_client_secret YT_COOKIES= YT_COOKIES_FILE=./config/cookies.txt -YT_DOWNLOADS_PER_MINUTE=3 \ No newline at end of file +YT_DOWNLOADS_PER_MINUTE=3 + +# Optional local hardening. Set AUTH_ENABLED=true and choose a strong token. +AUTH_ENABLED=false +SPOOTY_AUTH_TOKEN= diff --git a/src/backend/.env.docker b/src/backend/.env.docker index 74f06ae..39e0da7 100644 --- a/src/backend/.env.docker +++ b/src/backend/.env.docker @@ -12,4 +12,8 @@ SPOTIFY_CLIENT_ID=your_client_id SPOTIFY_CLIENT_SECRET=your_client_secret YT_COOKIES= -YT_DOWNLOADS_PER_MINUTE=3 \ No newline at end of file +YT_DOWNLOADS_PER_MINUTE=3 + +# Optional local hardening. Set AUTH_ENABLED=true and choose a strong token. +AUTH_ENABLED=false +SPOOTY_AUTH_TOKEN= diff --git a/src/backend/src/app.module.ts b/src/backend/src/app.module.ts index 166e489..ed33f2b 100644 --- a/src/backend/src/app.module.ts +++ b/src/backend/src/app.module.ts @@ -11,6 +11,8 @@ import { PlaylistEntity } from './playlist/playlist.entity'; import { resolve } from 'path'; import { EnvironmentEnum } from './environmentEnum'; import { BullModule } from '@nestjs/bullmq'; +import { APP_GUARD } from '@nestjs/core'; +import { AuthGuard } from './shared/auth.guard'; @Module({ imports: [ @@ -59,6 +61,11 @@ import { BullModule } from '@nestjs/bullmq'; PlaylistModule, ], controllers: [AppController], - providers: [], + providers: [ + { + provide: APP_GUARD, + useClass: AuthGuard, + }, + ], }) export class AppModule {} diff --git a/src/backend/src/environmentEnum.ts b/src/backend/src/environmentEnum.ts index 627bc6c..fb2d9b3 100644 --- a/src/backend/src/environmentEnum.ts +++ b/src/backend/src/environmentEnum.ts @@ -8,4 +8,6 @@ export enum EnvironmentEnum { REDIS_RUN = 'REDIS_RUN', YT_COOKIES = 'YT_COOKIES', YT_COOKIES_FILE = 'YT_COOKIES_FILE', + AUTH_ENABLED = 'AUTH_ENABLED', + SPOOTY_AUTH_TOKEN = 'SPOOTY_AUTH_TOKEN', } diff --git a/src/backend/src/main.ts b/src/backend/src/main.ts index e58ca65..26e9c41 100644 --- a/src/backend/src/main.ts +++ b/src/backend/src/main.ts @@ -5,27 +5,34 @@ import { resolve } from 'path'; import { exec } from 'child_process'; import { EnvironmentEnum } from './environmentEnum'; +function envFlag(name: string): boolean { + return String(process.env[name] || '').toLowerCase() === 'true'; +} + async function bootstrap() { + if (!process.env[EnvironmentEnum.DOWNLOADS_PATH]) { + throw new Error('DOWNLOADS_PATH environment variable is missing'); + } + + const folderName = resolve( + __dirname, + process.env[EnvironmentEnum.DOWNLOADS_PATH], + ); + fs.mkdirSync(folderName, { recursive: true }); + + if (envFlag(EnvironmentEnum.REDIS_RUN)) { + try { + // Convenience mode for the single-container Docker image. + // Hardened deployments should prefer an external Redis service. + exec(`redis-server --port ${process.env.REDIS_PORT || 6379}`); + } catch (e) { + console.log('Unable to run redis server from app'); + console.log(e); + } + } + const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); await app.listen(process.env.PORT || 3000); } bootstrap(); - -if (!process.env[EnvironmentEnum.DOWNLOADS_PATH]) { - throw new Error('DOWNLOADS_PATH environment variable is missing'); -} -const folderName = resolve( - __dirname, - process.env[EnvironmentEnum.DOWNLOADS_PATH], -); -!fs.existsSync(folderName) && fs.mkdirSync(folderName); - -try { - // not good idea, but I want to keep simple Dockerfile, I know ideally should be in another container and used docker compose - Boolean(process.env[EnvironmentEnum.REDIS_RUN]) && - exec(`redis-server --port ${process.env.REDIS_PORT}`); -} catch (e) { - console.log('Unable to run redis server form app'); - console.log(e); -} diff --git a/src/backend/src/playlist/playlist.service.ts b/src/backend/src/playlist/playlist.service.ts index 5a612b7..8239df2 100644 --- a/src/backend/src/playlist/playlist.service.ts +++ b/src/backend/src/playlist/playlist.service.ts @@ -207,7 +207,7 @@ export class PlaylistService { private createPlaylistFolderStructure(playlistName: string): void { const playlistPath = this.utilsService.getPlaylistFolderPath(playlistName); - !fs.existsSync(playlistPath) && fs.mkdirSync(playlistPath); + !fs.existsSync(playlistPath) && fs.mkdirSync(playlistPath, { recursive: true }); } @Interval(3_600_000) diff --git a/src/backend/src/shared/auth.guard.ts b/src/backend/src/shared/auth.guard.ts new file mode 100644 index 0000000..09a1ddf --- /dev/null +++ b/src/backend/src/shared/auth.guard.ts @@ -0,0 +1,50 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import type { Request } from 'express'; + +@Injectable() +export class AuthGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + if (!this.authEnabled()) { + return true; + } + + const expectedToken = process.env.SPOOTY_AUTH_TOKEN; + if (!expectedToken) { + throw new UnauthorizedException( + 'AUTH_ENABLED=true but SPOOTY_AUTH_TOKEN is missing', + ); + } + + const request = context.switchToHttp().getRequest(); + const providedToken = this.extractToken(request); + + if (providedToken !== expectedToken) { + throw new UnauthorizedException('Invalid or missing Spooty auth token'); + } + + return true; + } + + private authEnabled(): boolean { + return String(process.env.AUTH_ENABLED || '').toLowerCase() === 'true'; + } + + private extractToken(request: Request): string | undefined { + const headerToken = request.header('x-spooty-token'); + if (headerToken) { + return headerToken; + } + + const authorization = request.header('authorization'); + if (authorization?.toLowerCase().startsWith('bearer ')) { + return authorization.slice('bearer '.length).trim(); + } + + return undefined; + } +} diff --git a/src/backend/src/shared/utils.service.ts b/src/backend/src/shared/utils.service.ts index e4993fb..73ea1cc 100644 --- a/src/backend/src/shared/utils.service.ts +++ b/src/backend/src/shared/utils.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { resolve } from 'path'; +import { relative, resolve } from 'path'; import { EnvironmentEnum } from '../environmentEnum'; @Injectable() @@ -16,13 +16,24 @@ export class UtilsService { } getPlaylistFolderPath(name: string): string { - return resolve( - this.getRootDownloadsPath(), - this.stripFileIllegalChars(name), + return this.ensureInsideDownloadsRoot( + resolve(this.getRootDownloadsPath(), this.stripFileIllegalChars(name)), ); } + ensureInsideDownloadsRoot(candidatePath: string): string { + const root = this.getRootDownloadsPath(); + const resolvedCandidate = resolve(candidatePath); + const rel = relative(root, resolvedCandidate); + + if (rel === '' || (!rel.startsWith('..') && !resolve(rel).startsWith('/'))) { + return resolvedCandidate; + } + + throw new Error(`Unsafe path outside downloads root: ${resolvedCandidate}`); + } + stripFileIllegalChars(text: string): string { - return text.replace(/[/\\?%*:|"<>]/g, '-'); + return text.replace(/[/\\?%*:|"<>]/g, '-').trim() || 'untitled'; } } diff --git a/src/backend/src/track/track.service.ts b/src/backend/src/track/track.service.ts index d5141a2..8a9b57f 100644 --- a/src/backend/src/track/track.service.ts +++ b/src/backend/src/track/track.service.ts @@ -110,27 +110,25 @@ export class TrackService { if (!(await this.get(track.id))) { return; } - if ( - !track.name || - !track.artist || - !track.playlist - ) { + if (!track.name || !track.artist || !track.playlist) { this.logger.error( `Track or playlist field is null or undefined: name=${track.name}, artist=${track.artist}, playlist=${track.playlist ? 'ok' : 'null'}`, ); return; } - // Use track's own coverUrl if available, otherwise fall back to playlist coverUrl + const coverUrl = track.coverUrl || track.playlist.coverUrl; if (!coverUrl) { this.logger.warn( `No cover art available for track: ${track.artist} - ${track.name}`, ); } + await this.update(track.id, { ...track, status: TrackStatusEnum.Downloading, }); + let error: string; try { const folderName = this.getFolderName(track, track.playlist); @@ -147,6 +145,7 @@ export class TrackService { this.logger.error(err); error = String(err); } + const updatedTrack = { ...track, status: error ? TrackStatusEnum.Error : TrackStatusEnum.Completed, @@ -157,24 +156,27 @@ export class TrackService { getTrackFileName(track: TrackEntity): string { const safeArtist = track.artist || 'unknown_artist'; - const safeName = (track.name || 'unknown_track').replace('/', ''); + const safeName = track.name || 'unknown_track'; const fileName = `${safeArtist} - ${safeName}`; return `${this.utilsService.stripFileIllegalChars(fileName)}.${this.configService.get(EnvironmentEnum.FORMAT)}`; } getFolderName(track: TrackEntity, playlist: PlaylistEntity): string { - // Individual tracks (isTrack=true) go in root downloads folder, playlists in subfolders if (playlist?.isTrack) { - return resolve( - this.utilsService.getRootDownloadsPath(), - this.getTrackFileName(track), + return this.utilsService.ensureInsideDownloadsRoot( + resolve( + this.utilsService.getRootDownloadsPath(), + this.getTrackFileName(track), + ), ); } - + const safePlaylistName = playlist?.name || 'unknown_playlist'; - return resolve( - this.utilsService.getPlaylistFolderPath(safePlaylistName), - this.getTrackFileName(track), + return this.utilsService.ensureInsideDownloadsRoot( + resolve( + this.utilsService.getPlaylistFolderPath(safePlaylistName), + this.getTrackFileName(track), + ), ); } } diff --git a/src/frontend/src/app/app.config.ts b/src/frontend/src/app/app.config.ts index 73ea233..2547396 100644 --- a/src/frontend/src/app/app.config.ts +++ b/src/frontend/src/app/app.config.ts @@ -1,15 +1,20 @@ -import {ApplicationConfig, importProvidersFrom, provideZoneChangeDetection} from '@angular/core'; +import { + ApplicationConfig, + importProvidersFrom, + provideZoneChangeDetection, +} from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; -import {provideHttpClient} from "@angular/common/http"; -import {SocketIoModule} from "ngx-socket-io"; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { SocketIoModule } from 'ngx-socket-io'; +import { authTokenInterceptor } from './services/auth-token.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), - provideHttpClient(), - importProvidersFrom(SocketIoModule.forRoot({url: ''})) + provideHttpClient(withInterceptors([authTokenInterceptor])), + importProvidersFrom(SocketIoModule.forRoot({ url: '' })), ], }; diff --git a/src/frontend/src/app/services/auth-token.interceptor.ts b/src/frontend/src/app/services/auth-token.interceptor.ts new file mode 100644 index 0000000..cd9356f --- /dev/null +++ b/src/frontend/src/app/services/auth-token.interceptor.ts @@ -0,0 +1,19 @@ +import { HttpInterceptorFn } from '@angular/common/http'; + +const TOKEN_STORAGE_KEY = 'spooty_auth_token'; + +export const authTokenInterceptor: HttpInterceptorFn = (req, next) => { + const token = localStorage.getItem(TOKEN_STORAGE_KEY); + + if (!token) { + return next(req); + } + + return next( + req.clone({ + setHeaders: { + 'x-spooty-token': token, + }, + }), + ); +}; From 0f9b1f9477bd46f4bcb7e54e57302646fb4574c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Matz=20Andr=C3=A9n?= Date: Thu, 14 May 2026 16:15:20 +0200 Subject: [PATCH 02/30] Harden YouTube search and download handling --- src/backend/src/shared/youtube.service.ts | 93 ++++++++++++++----- .../src/track/track-download.processor.ts | 12 ++- src/backend/src/track/track.service.ts | 11 ++- 3 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/backend/src/shared/youtube.service.ts b/src/backend/src/shared/youtube.service.ts index 27bfc30..e62ee5a 100644 --- a/src/backend/src/shared/youtube.service.ts +++ b/src/backend/src/shared/youtube.service.ts @@ -13,6 +13,14 @@ const HEADERS = { 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', }; +const ALLOWED_YOUTUBE_HOSTS = new Set([ + 'youtube.com', + 'www.youtube.com', + 'm.youtube.com', + 'music.youtube.com', + 'youtu.be', +]); + @Injectable() export class YoutubeService { private readonly logger = new Logger(TrackService.name); @@ -20,10 +28,37 @@ export class YoutubeService { constructor(private readonly configService: ConfigService) {} async findOnYoutubeOne(artist: string, name: string): Promise { - this.logger.debug(`Searching ${artist} - ${name} on YT`); - const url = (await yts(`${artist} - ${name}`)).videos[0].url; - this.logger.debug(`Found ${artist} - ${name} on ${url}`); - return url; + const query = `${artist} - ${name}`; + this.logger.debug(`Searching ${query} on YT`); + + const result = await yts(query); + const firstVideo = result.videos?.[0]; + + if (!firstVideo?.url) { + throw new Error(`No YouTube result found for: ${query}`); + } + + this.assertValidYoutubeUrl(firstVideo.url); + this.logger.debug(`Found ${query} on ${firstVideo.url}`); + return firstVideo.url; + } + + assertValidYoutubeUrl(url: string): void { + let parsed: URL; + + try { + parsed = new URL(url); + } catch { + throw new Error('Invalid YouTube URL'); + } + + if (!['http:', 'https:'].includes(parsed.protocol)) { + throw new Error('Rejected non-HTTP YouTube URL'); + } + + if (!ALLOWED_YOUTUBE_HOSTS.has(parsed.hostname.toLowerCase())) { + throw new Error(`Rejected non-YouTube URL: ${parsed.hostname}`); + } } private getCookiesOptions(): { @@ -51,10 +86,13 @@ export class YoutubeService { this.logger.debug( `Downloading ${track.artist} - ${track.name} (${track.youtubeUrl}) from YT`, ); + if (!track.youtubeUrl) { - this.logger.error('youtubeUrl is null or undefined'); - throw Error('youtubeUrl is null or undefined'); + throw new Error('youtubeUrl is null or undefined'); } + + this.assertValidYoutubeUrl(track.youtubeUrl); + const ytdlp = new YtDlp(); await ytdlp.downloadAudio( track.youtubeUrl, @@ -67,6 +105,7 @@ export class YoutubeService { audioQuality: this.configService.get('QUALITY'), }, ); + this.logger.debug( `Downloaded ${track.artist} - ${track.name} to ${output}`, ); @@ -78,24 +117,30 @@ export class YoutubeService { title: string, artist: string, ): Promise { - if (coverUrl) { - const res = await fetch(coverUrl); - const arrayBuf = await res.arrayBuffer(); - const imageBuffer = Buffer.from(arrayBuf); - - NodeID3.write( - { - title, - artist, - APIC: { - mime: 'image/jpeg', - type: { id: 3, name: 'front cover' }, - description: 'cover', - imageBuffer, - }, - }, - folderName, - ); + if (!coverUrl) { + return; } + + const res = await fetch(coverUrl); + if (!res.ok) { + throw new Error(`Failed to fetch cover art: ${res.status}`); + } + + const arrayBuf = await res.arrayBuffer(); + const imageBuffer = Buffer.from(arrayBuf); + + NodeID3.write( + { + title, + artist, + APIC: { + mime: 'image/jpeg', + type: { id: 3, name: 'front cover' }, + description: 'cover', + imageBuffer, + }, + }, + folderName, + ); } } diff --git a/src/backend/src/track/track-download.processor.ts b/src/backend/src/track/track-download.processor.ts index 8cdf7b1..de1b7d9 100644 --- a/src/backend/src/track/track-download.processor.ts +++ b/src/backend/src/track/track-download.processor.ts @@ -3,6 +3,16 @@ import { Job } from 'bullmq'; import { TrackService } from './track.service'; import { TrackEntity } from './track.entity'; +function getDownloadsPerMinute(): number { + const parsed = Number(process.env.YT_DOWNLOADS_PER_MINUTE || 3); + + if (!Number.isFinite(parsed) || parsed <= 0) { + return 3; + } + + return Math.max(1, Math.min(30, Math.floor(parsed))); +} + @Processor('track-download-processor') export class TrackDownloadProcessor extends WorkerHost { constructor(private readonly trackService: TrackService) { @@ -10,7 +20,7 @@ export class TrackDownloadProcessor extends WorkerHost { } async process(job: Job): Promise { - const maxPerMinute = Number(process.env.YT_DOWNLOADS_PER_MINUTE || 3); + const maxPerMinute = getDownloadsPerMinute(); const sleepMs = Math.floor(60000 / maxPerMinute); await new Promise((res) => setTimeout(res, sleepMs)); await this.trackService.downloadFromYoutube(job.data); diff --git a/src/backend/src/track/track.service.ts b/src/backend/src/track/track.service.ts index 8a9b57f..3365625 100644 --- a/src/backend/src/track/track.service.ts +++ b/src/backend/src/track/track.service.ts @@ -99,17 +99,24 @@ export class TrackService { error: String(err), status: TrackStatusEnum.Error, }; + await this.update(track.id, updatedTrack); + return; } + + await this.update(track.id, updatedTrack); await this.trackDownloadQueue.add('', updatedTrack, { jobId: `id-${updatedTrack.id}`, }); - await this.update(track.id, updatedTrack); } async downloadFromYoutube(track: TrackEntity): Promise { - if (!(await this.get(track.id))) { + const dbTrack = await this.get(track.id); + if (!dbTrack) { return; } + + track = dbTrack; + if (!track.name || !track.artist || !track.playlist) { this.logger.error( `Track or playlist field is null or undefined: name=${track.name}, artist=${track.artist}, playlist=${track.playlist ? 'ok' : 'null'}`, From 41f5a80ffb1ea2460474b6bae60f4e55e5c8b7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Matz=20Andr=C3=A9n?= Date: Thu, 14 May 2026 16:25:52 +0200 Subject: [PATCH 03/30] Harden API validation throttling and error handling --- package-lock.json | 52 ++++++++++++++++++- package.json | 5 ++ src/backend/src/app.module.ts | 11 ++++ src/backend/src/main.ts | 16 +++++- .../src/playlist/dto/create-playlist.dto.ts | 11 ++++ .../src/playlist/dto/update-playlist.dto.ts | 7 +++ .../src/playlist/playlist.controller.ts | 17 +++--- src/backend/src/playlist/playlist.service.ts | 17 ++++-- src/backend/src/shared/errors/safe-error.ts | 20 +++++++ .../shared/filters/http-exception.filter.ts | 42 +++++++++++++++ src/backend/src/track/track.controller.ts | 41 +++++++++++---- src/backend/src/track/track.service.ts | 16 ++++-- 12 files changed, 231 insertions(+), 24 deletions(-) create mode 100644 src/backend/src/playlist/dto/create-playlist.dto.ts create mode 100644 src/backend/src/playlist/dto/update-playlist.dto.ts create mode 100644 src/backend/src/shared/errors/safe-error.ts create mode 100644 src/backend/src/shared/filters/http-exception.filter.ts diff --git a/package-lock.json b/package-lock.json index 89ca489..0d374fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,11 @@ "src/backend", "src/frontend" ], + "dependencies": { + "@nestjs/throttler": "^6.5.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1" + }, "devDependencies": { "@release-it/bumper": "^7.0.1", "auto-changelog": "^2.5.0", @@ -6054,6 +6059,16 @@ } } }, + "node_modules/@nestjs/throttler": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz", + "integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0" + } + }, "node_modules/@nestjs/typeorm": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", @@ -8241,6 +8256,11 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -10472,6 +10492,21 @@ "node": ">= 0.3.0" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", + "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -16929,6 +16964,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.1.tgz", + "integrity": "sha512-GEw0GLL7YUUA6nv21IsCvVjtI5Ejn84sjbdfQ9KxdbqEVOk1PZh7xejn01EEiniKw+dBeCfim+8MGeuvVuE2BA==" + }, "node_modules/license-webpack-plugin": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", @@ -24476,6 +24516,14 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -25704,7 +25752,7 @@ "license": "MIT" }, "src/backend": { - "version": "2.3.4", + "version": "2.4.2", "license": "UNLICENSED", "dependencies": { "@nestjs/bullmq": "^10.2.3", @@ -25772,7 +25820,7 @@ "license": "MIT" }, "src/frontend": { - "version": "2.3.4", + "version": "2.4.2", "dependencies": { "@angular/animations": "^19.0.6", "@angular/common": "^19.0.6", diff --git a/package.json b/package.json index 2782fb6..c4f6c20 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,10 @@ "release-it": "^18.1.2", "release-it-docker-plugin": "^2.0.0", "rimraf": "^6.0.1" + }, + "dependencies": { + "@nestjs/throttler": "^6.5.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1" } } diff --git a/src/backend/src/app.module.ts b/src/backend/src/app.module.ts index ed33f2b..8b352cf 100644 --- a/src/backend/src/app.module.ts +++ b/src/backend/src/app.module.ts @@ -13,6 +13,7 @@ import { EnvironmentEnum } from './environmentEnum'; import { BullModule } from '@nestjs/bullmq'; import { APP_GUARD } from '@nestjs/core'; import { AuthGuard } from './shared/auth.guard'; +import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; @Module({ imports: [ @@ -44,6 +45,12 @@ import { AuthGuard } from './shared/auth.guard'; ], inject: [ConfigService], }), + ThrottlerModule.forRoot([ + { + ttl: 60000, + limit: 60, + }, + ]), BullModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ @@ -66,6 +73,10 @@ import { AuthGuard } from './shared/auth.guard'; provide: APP_GUARD, useClass: AuthGuard, }, + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, ], }) export class AppModule {} diff --git a/src/backend/src/main.ts b/src/backend/src/main.ts index 26e9c41..fd890c6 100644 --- a/src/backend/src/main.ts +++ b/src/backend/src/main.ts @@ -1,4 +1,6 @@ import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { HttpExceptionFilter } from './shared/filters/http-exception.filter'; import { AppModule } from './app.module'; import * as fs from 'fs'; import { resolve } from 'path'; @@ -31,7 +33,19 @@ async function bootstrap() { } } - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { + bodyParser: true, + }); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); + + app.useGlobalFilters(new HttpExceptionFilter()); app.setGlobalPrefix('api'); await app.listen(process.env.PORT || 3000); } diff --git a/src/backend/src/playlist/dto/create-playlist.dto.ts b/src/backend/src/playlist/dto/create-playlist.dto.ts new file mode 100644 index 0000000..cfeebf4 --- /dev/null +++ b/src/backend/src/playlist/dto/create-playlist.dto.ts @@ -0,0 +1,11 @@ +import { IsString, IsUrl, MaxLength } from 'class-validator'; + +export class CreatePlaylistDto { + @IsString() + @MaxLength(2048) + @IsUrl({ + require_protocol: true, + protocols: ['https'], + }) + spotifyUrl: string; +} diff --git a/src/backend/src/playlist/dto/update-playlist.dto.ts b/src/backend/src/playlist/dto/update-playlist.dto.ts new file mode 100644 index 0000000..bd6e0dd --- /dev/null +++ b/src/backend/src/playlist/dto/update-playlist.dto.ts @@ -0,0 +1,7 @@ +import { IsBoolean, IsOptional } from 'class-validator'; + +export class UpdatePlaylistDto { + @IsOptional() + @IsBoolean() + active?: boolean; +} diff --git a/src/backend/src/playlist/playlist.controller.ts b/src/backend/src/playlist/playlist.controller.ts index 5e5d784..f270f51 100644 --- a/src/backend/src/playlist/playlist.controller.ts +++ b/src/backend/src/playlist/playlist.controller.ts @@ -4,11 +4,14 @@ import { Delete, Get, Param, + ParseIntPipe, Post, Put, } from '@nestjs/common'; import { PlaylistService } from './playlist.service'; import { PlaylistEntity } from './playlist.entity'; +import { CreatePlaylistDto } from './dto/create-playlist.dto'; +import { UpdatePlaylistDto } from './dto/update-playlist.dto'; @Controller('playlist') export class PlaylistController { @@ -20,25 +23,27 @@ export class PlaylistController { } @Post() - async create(@Body() playlist: PlaylistEntity): Promise { - await this.service.create(playlist); + async create(@Body() playlist: CreatePlaylistDto): Promise { + await this.service.create(playlist as PlaylistEntity); } @Put(':id') update( - @Param('id') id: number, - @Body() playlist: Partial, + @Param('id', ParseIntPipe) id: number, + @Body() playlist: UpdatePlaylistDto, ): Promise { return this.service.update(id, playlist); } @Delete(':id') - remove(@Param('id') id: number): Promise { + remove(@Param('id', ParseIntPipe) id: number): Promise { return this.service.remove(id); } @Get('retry/:id') - retryFailedOfPlaylist(@Param('id') id: number): Promise { + retryFailedOfPlaylist( + @Param('id', ParseIntPipe) id: number, + ): Promise { return this.service.retryFailedOfPlaylist(id); } } diff --git a/src/backend/src/playlist/playlist.service.ts b/src/backend/src/playlist/playlist.service.ts index 8239df2..35942f2 100644 --- a/src/backend/src/playlist/playlist.service.ts +++ b/src/backend/src/playlist/playlist.service.ts @@ -10,6 +10,7 @@ import { Interval } from '@nestjs/schedule'; import { TrackStatusEnum } from '../track/track.entity'; import { UtilsService } from '../shared/utils.service'; import { SpotifyService } from '../shared/spotify.service'; +import { toSafeErrorMessage } from '../shared/errors/safe-error'; enum WsPlaylistOperation { New = 'playlistNew', @@ -77,7 +78,11 @@ export class PlaylistService { // Don't create folder structure for individual tracks - they go in root } catch (err) { this.logger.error(`Error getting track details: ${err}`); - playlist2Save = { ...playlist, error: String(err), isTrack: true }; + playlist2Save = { + ...playlist, + error: toSafeErrorMessage(err), + isTrack: true, + }; } const savedPlaylist = await this.save(playlist2Save); @@ -117,7 +122,10 @@ export class PlaylistService { this.createPlaylistFolderStructure(playlist2Save.name); } catch (err) { this.logger.error(`Error getting playlist details: ${err}`); - playlist2Save = { ...playlist, error: String(err) }; + playlist2Save = { + ...playlist, + error: toSafeErrorMessage(err), + }; } const savedPlaylist = await this.save(playlist2Save); @@ -224,7 +232,10 @@ export class PlaylistService { ); this.createPlaylistFolderStructure(playlist.name); } catch (err) { - await this.update(playlist.id, { ...playlist, error: String(err) }); + await this.update(playlist.id, { + ...playlist, + error: toSafeErrorMessage(err), + }); } for (const track of tracks ?? []) { const track2Save = { diff --git a/src/backend/src/shared/errors/safe-error.ts b/src/backend/src/shared/errors/safe-error.ts new file mode 100644 index 0000000..72b5f2e --- /dev/null +++ b/src/backend/src/shared/errors/safe-error.ts @@ -0,0 +1,20 @@ +export function toSafeErrorMessage(error: unknown): string { + if (error instanceof Error) { + return sanitize(error.message); + } + + if (typeof error === 'string') { + return sanitize(error); + } + + return 'Unknown error'; +} + +function sanitize(text: string): string { + return text + .replace(/\/home\/[^\s]+/g, '[path]') + .replace(/\/root\/[^\s]+/g, '[path]') + .replace(/token=[^\s&]+/gi, 'token=[redacted]') + .replace(/authorization:[^\n]+/gi, 'authorization:[redacted]') + .slice(0, 500); +} diff --git a/src/backend/src/shared/filters/http-exception.filter.ts b/src/backend/src/shared/filters/http-exception.filter.ts new file mode 100644 index 0000000..23195da --- /dev/null +++ b/src/backend/src/shared/filters/http-exception.filter.ts @@ -0,0 +1,42 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import type { Request, Response } from 'express'; + +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(HttpExceptionFilter.name); + + catch(exception: unknown, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.message + : 'Internal server error'; + + this.logger.error( + `${request.method} ${request.url} -> ${status}: ${String(exception)}`, + ); + + response.status(status).json({ + statusCode: status, + message, + timestamp: new Date().toISOString(), + path: request.url, + }); + } +} diff --git a/src/backend/src/track/track.controller.ts b/src/backend/src/track/track.controller.ts index bbf7065..4dd58cb 100644 --- a/src/backend/src/track/track.controller.ts +++ b/src/backend/src/track/track.controller.ts @@ -2,12 +2,14 @@ import { Controller, Delete, Get, + NotFoundException, Param, + ParseIntPipe, Res, StreamableFile, } from '@nestjs/common'; import { TrackService } from './track.service'; -import { createReadStream } from 'fs'; +import { createReadStream, existsSync } from 'fs'; import type { Response } from 'express'; import { ConfigService } from '@nestjs/config'; import { TrackEntity } from './track.entity'; @@ -20,33 +22,54 @@ export class TrackController { ) {} @Get('playlist/:id') - getAllByPlaylist(@Param('id') playlistId: number): Promise { + getAllByPlaylist( + @Param('id', ParseIntPipe) playlistId: number, + ): Promise { return this.service.getAllByPlaylist(playlistId); } @Get('download/:id') async getFile( @Res({ passthrough: true }) res: Response, - @Param('id') id: number, + @Param('id', ParseIntPipe) id: number, ): Promise { const track = await this.service.get(id); + + if (!track || !track.playlist) { + throw new NotFoundException('Track not found'); + } + + const filePath = this.service.getFolderName(track, track.playlist); + + if (!existsSync(filePath)) { + throw new NotFoundException('Track file not found'); + } + const fileName = this.service.getTrackFileName(track); - const readStream = createReadStream( - this.service.getFolderName(track, track.playlist), - ); + + const readStream = createReadStream(filePath); + res.set({ - 'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}`, + 'Content-Disposition': + `attachment; filename="${encodeURIComponent(fileName)}"`, + 'Content-Type': 'application/octet-stream', + 'X-Content-Type-Options': 'nosniff', }); + return new StreamableFile(readStream); } @Delete(':id') - remove(@Param('id') id: number): Promise { + remove( + @Param('id', ParseIntPipe) id: number, + ): Promise { return this.service.remove(id); } @Get('retry/:id') - retry(@Param('id') id: number): Promise { + retry( + @Param('id', ParseIntPipe) id: number, + ): Promise { return this.service.retry(id); } } diff --git a/src/backend/src/track/track.service.ts b/src/backend/src/track/track.service.ts index 3365625..6a43006 100644 --- a/src/backend/src/track/track.service.ts +++ b/src/backend/src/track/track.service.ts @@ -12,6 +12,7 @@ import { UtilsService } from '../shared/utils.service'; import { Queue } from 'bullmq'; import { InjectQueue } from '@nestjs/bullmq'; import { YoutubeService } from '../shared/youtube.service'; +import { toSafeErrorMessage } from '../shared/errors/safe-error'; enum WsTrackOperation { New = 'trackNew', @@ -73,7 +74,16 @@ export class TrackService { async retry(id: number): Promise { const track = await this.get(id); - await this.trackSearchQueue.add('', track, { jobId: `id-${id}` }); + const existingJob = await this.trackSearchQueue.getJob(`id-${id}`); + + if (existingJob) { + this.logger.warn(`Search job already exists for track ${id}`); + return; + } + + await this.trackSearchQueue.add('', track, { + jobId: `id-${id}`, + }); await this.update(id, { ...track, status: TrackStatusEnum.New }); } @@ -96,7 +106,7 @@ export class TrackService { this.logger.error(err); updatedTrack = { ...track, - error: String(err), + error: toSafeErrorMessage(err), status: TrackStatusEnum.Error, }; await this.update(track.id, updatedTrack); @@ -150,7 +160,7 @@ export class TrackService { } } catch (err) { this.logger.error(err); - error = String(err); + error = toSafeErrorMessage(err); } const updatedTrack = { From f9e4cf5d0784b53fcee4266b42885908be5a5690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Matz=20Andr=C3=A9n?= Date: Thu, 14 May 2026 16:38:42 +0200 Subject: [PATCH 04/30] Add HTTP security headers and CORS defaults --- package-lock.json | 11 ++++++++++- package.json | 3 ++- src/backend/.env.default | 6 ++++++ src/backend/.env.docker | 6 ++++++ src/backend/src/environmentEnum.ts | 2 ++ src/backend/src/main.ts | 23 +++++++++++++++++++++++ 6 files changed, 49 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d374fe..d1f5e76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "dependencies": { "@nestjs/throttler": "^6.5.0", "class-transformer": "^0.5.1", - "class-validator": "^0.15.1" + "class-validator": "^0.15.1", + "helmet": "^8.1.0" }, "devDependencies": { "@release-it/bumper": "^7.0.1", @@ -14434,6 +14435,14 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/himalaya": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/himalaya/-/himalaya-1.1.1.tgz", diff --git a/package.json b/package.json index c4f6c20..4c2acab 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "dependencies": { "@nestjs/throttler": "^6.5.0", "class-transformer": "^0.5.1", - "class-validator": "^0.15.1" + "class-validator": "^0.15.1", + "helmet": "^8.1.0" } } diff --git a/src/backend/.env.default b/src/backend/.env.default index bb3fbac..e5ff276 100644 --- a/src/backend/.env.default +++ b/src/backend/.env.default @@ -19,3 +19,9 @@ YT_DOWNLOADS_PER_MINUTE=3 # Optional local hardening. Set AUTH_ENABLED=true and choose a strong token. AUTH_ENABLED=false SPOOTY_AUTH_TOKEN= + + +# Optional frontend origin restriction +CORS_ORIGIN=http://localhost:4200 + +NODE_ENV=development diff --git a/src/backend/.env.docker b/src/backend/.env.docker index 39e0da7..4459351 100644 --- a/src/backend/.env.docker +++ b/src/backend/.env.docker @@ -17,3 +17,9 @@ YT_DOWNLOADS_PER_MINUTE=3 # Optional local hardening. Set AUTH_ENABLED=true and choose a strong token. AUTH_ENABLED=false SPOOTY_AUTH_TOKEN= + + +# Optional frontend origin restriction +CORS_ORIGIN=http://localhost:4200 + +NODE_ENV=production diff --git a/src/backend/src/environmentEnum.ts b/src/backend/src/environmentEnum.ts index fb2d9b3..8208771 100644 --- a/src/backend/src/environmentEnum.ts +++ b/src/backend/src/environmentEnum.ts @@ -10,4 +10,6 @@ export enum EnvironmentEnum { YT_COOKIES_FILE = 'YT_COOKIES_FILE', AUTH_ENABLED = 'AUTH_ENABLED', SPOOTY_AUTH_TOKEN = 'SPOOTY_AUTH_TOKEN', + CORS_ORIGIN = 'CORS_ORIGIN', + NODE_ENV = 'NODE_ENV', } diff --git a/src/backend/src/main.ts b/src/backend/src/main.ts index fd890c6..58cb61a 100644 --- a/src/backend/src/main.ts +++ b/src/backend/src/main.ts @@ -5,6 +5,7 @@ import { AppModule } from './app.module'; import * as fs from 'fs'; import { resolve } from 'path'; import { exec } from 'child_process'; +import helmet from 'helmet'; import { EnvironmentEnum } from './environmentEnum'; function envFlag(name: string): boolean { @@ -46,6 +47,28 @@ async function bootstrap() { ); app.useGlobalFilters(new HttpExceptionFilter()); + + app.use( + helmet({ + crossOriginResourcePolicy: false, + }), + ); + + const corsOrigin = + process.env[EnvironmentEnum.CORS_ORIGIN] || 'http://localhost:4200'; + + app.enableCors({ + origin: corsOrigin, + credentials: false, + }); + + if ( + process.env[EnvironmentEnum.NODE_ENV] === 'production' && + !envFlag(EnvironmentEnum.AUTH_ENABLED) + ) { + console.warn('[SECURITY] AUTH_ENABLED is false while NODE_ENV=production'); + } + app.setGlobalPrefix('api'); await app.listen(process.env.PORT || 3000); } From 5fb84af6359ac4b81404c089ea8d07eb3393350d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Matz=20Andr=C3=A9n?= Date: Thu, 14 May 2026 16:45:16 +0200 Subject: [PATCH 05/30] Harden queue execution and job lifecycle --- src/backend/src/app.module.ts | 15 ++++- .../src/track/track-download.processor.ts | 17 ++++- .../src/track/track-search.processor.ts | 13 +++- src/backend/src/track/track.service.ts | 67 +++++++++++++++---- 4 files changed, 96 insertions(+), 16 deletions(-) diff --git a/src/backend/src/app.module.ts b/src/backend/src/app.module.ts index 8b352cf..12caa6e 100644 --- a/src/backend/src/app.module.ts +++ b/src/backend/src/app.module.ts @@ -55,7 +55,20 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ defaultJobOptions: { - removeOnComplete: true, + attempts: 2, + backoff: { + type: 'exponential', + delay: 30_000, + }, + removeOnComplete: { + age: 3_600, + count: 1_000, + }, + removeOnFail: { + age: 86_400, + count: 1_000, + }, + timeout: 10 * 60_000, }, connection: { host: configService.get(EnvironmentEnum.REDIS_HOST), diff --git a/src/backend/src/track/track-download.processor.ts b/src/backend/src/track/track-download.processor.ts index de1b7d9..3f88d56 100644 --- a/src/backend/src/track/track-download.processor.ts +++ b/src/backend/src/track/track-download.processor.ts @@ -1,4 +1,5 @@ import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Logger } from '@nestjs/common'; import { Job } from 'bullmq'; import { TrackService } from './track.service'; import { TrackEntity } from './track.entity'; @@ -13,15 +14,29 @@ function getDownloadsPerMinute(): number { return Math.max(1, Math.min(30, Math.floor(parsed))); } -@Processor('track-download-processor') +@Processor('track-download-processor', { + concurrency: 1, +}) export class TrackDownloadProcessor extends WorkerHost { + private readonly logger = new Logger(TrackDownloadProcessor.name); + constructor(private readonly trackService: TrackService) { super(); } async process(job: Job): Promise { + if (!job.data?.id) { + this.logger.warn(`Skipping malformed download job ${job.id}`); + return; + } + const maxPerMinute = getDownloadsPerMinute(); const sleepMs = Math.floor(60000 / maxPerMinute); + + this.logger.debug( + `Processing download job ${job.id} for track ${job.data.id}; pacing ${sleepMs}ms`, + ); + await new Promise((res) => setTimeout(res, sleepMs)); await this.trackService.downloadFromYoutube(job.data); } diff --git a/src/backend/src/track/track-search.processor.ts b/src/backend/src/track/track-search.processor.ts index 0f16a26..0336193 100644 --- a/src/backend/src/track/track-search.processor.ts +++ b/src/backend/src/track/track-search.processor.ts @@ -1,15 +1,26 @@ import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Logger } from '@nestjs/common'; import { Job } from 'bullmq'; import { TrackService } from './track.service'; import { TrackEntity } from './track.entity'; -@Processor('track-search-processor') +@Processor('track-search-processor', { + concurrency: 2, +}) export class TrackSearchProcessor extends WorkerHost { + private readonly logger = new Logger(TrackSearchProcessor.name); + constructor(private readonly trackService: TrackService) { super(); } async process(job: Job): Promise { + if (!job.data?.id) { + this.logger.warn(`Skipping malformed search job ${job.id}`); + return; + } + + this.logger.debug(`Processing search job ${job.id} for track ${job.data.id}`); await this.trackService.findOnYoutube(job.data); } } diff --git a/src/backend/src/track/track.service.ts b/src/backend/src/track/track.service.ts index 6a43006..8780989 100644 --- a/src/backend/src/track/track.service.ts +++ b/src/backend/src/track/track.service.ts @@ -58,9 +58,7 @@ export class TrackService { async create(track: TrackEntity, playlist?: PlaylistEntity): Promise { const savedTrack = await this.repository.save({ ...track, playlist }); - await this.trackSearchQueue.add('', savedTrack, { - jobId: `id-${savedTrack.id}`, - }); + await this.enqueueSearch(savedTrack.id); this.io.emit(WsTrackOperation.New, { track: savedTrack, playlistId: playlist.id, @@ -74,23 +72,30 @@ export class TrackService { async retry(id: number): Promise { const track = await this.get(id); - const existingJob = await this.trackSearchQueue.getJob(`id-${id}`); - if (existingJob) { - this.logger.warn(`Search job already exists for track ${id}`); + if (!track) { + this.logger.warn(`Cannot retry missing track ${id}`); return; } - await this.trackSearchQueue.add('', track, { - jobId: `id-${id}`, + await this.enqueueSearch(id); + await this.update(id, { + ...track, + error: null, + status: TrackStatusEnum.New, }); - await this.update(id, { ...track, status: TrackStatusEnum.New }); } async findOnYoutube(track: TrackEntity): Promise { - if (!(await this.get(track.id))) { + const dbTrack = await this.get(track.id); + + if (!dbTrack) { + this.logger.warn(`Skipping search for missing track ${track.id}`); return; } + + track = dbTrack; + await this.update(track.id, { ...track, status: TrackStatusEnum.Searching, @@ -114,9 +119,45 @@ export class TrackService { } await this.update(track.id, updatedTrack); - await this.trackDownloadQueue.add('', updatedTrack, { - jobId: `id-${updatedTrack.id}`, - }); + await this.enqueueDownload(updatedTrack.id); + } + + private async enqueueSearch(id: number): Promise { + const track = await this.get(id); + + if (!track) { + this.logger.warn(`Cannot enqueue search for missing track ${id}`); + return; + } + + const jobId = `search-${id}`; + const existingJob = await this.trackSearchQueue.getJob(jobId); + + if (existingJob) { + this.logger.warn(`Search job already exists for track ${id}`); + return; + } + + await this.trackSearchQueue.add('search-track', { id }, { jobId }); + } + + private async enqueueDownload(id: number): Promise { + const track = await this.get(id); + + if (!track) { + this.logger.warn(`Cannot enqueue download for missing track ${id}`); + return; + } + + const jobId = `download-${id}`; + const existingJob = await this.trackDownloadQueue.getJob(jobId); + + if (existingJob) { + this.logger.warn(`Download job already exists for track ${id}`); + return; + } + + await this.trackDownloadQueue.add('download-track', { id }, { jobId }); } async downloadFromYoutube(track: TrackEntity): Promise { From 33f077ed1e9379f0368ed4d8500c31e5feff42f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Matz=20Andr=C3=A9n?= Date: Thu, 14 May 2026 17:07:22 +0200 Subject: [PATCH 06/30] Reduce Docker build context and harden runtime image --- .dockerignore | 13 +++++++++++++ Dockerfile | 34 +++++++++++++++++++++++----------- 2 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..db9b566 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +dist +.git +.github +coverage +.vscode +*.log +downloads +config +.env +.env.* +README.md +assets diff --git a/Dockerfile b/Dockerfile index 61f3fd0..85fdcbe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,15 +6,27 @@ RUN npm run build FROM node:20.20.0-alpine WORKDIR /spooty -COPY --from=builder /spooty/dist . -COPY --from=builder /spooty/src ./src -COPY --from=builder /spooty/package.json ./package.json -COPY --from=builder /spooty/package-lock.json ./package-lock.json -COPY --from=builder /spooty/src/backend/.env.docker ./.env -RUN npm prune --production -RUN rm -rf src package.json package-lock.json -RUN apk add --no-cache ffmpeg -RUN apk add --no-cache redis -RUN apk add --no-cache python3 py3-pip + +RUN apk add --no-cache ffmpeg redis python3 py3-pip curl \ + && addgroup -S spooty \ + && adduser -S spooty -G spooty + +COPY --from=builder --chown=spooty:spooty /spooty/dist . +COPY --from=builder --chown=spooty:spooty /spooty/src ./src +COPY --from=builder --chown=spooty:spooty /spooty/package.json ./package.json +COPY --from=builder --chown=spooty:spooty /spooty/package-lock.json ./package-lock.json +COPY --from=builder --chown=spooty:spooty /spooty/src/backend/.env.docker ./.env + +RUN npm prune --production \ + && rm -rf src package.json package-lock.json \ + && mkdir -p /spooty/backend/downloads /spooty/backend/config /spooty/config \ + && chown -R spooty:spooty /spooty + +USER spooty + EXPOSE 3000 -CMD ["node", "backend/main.js"] \ No newline at end of file + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -fsS http://127.0.0.1:3000/api || exit 1 + +CMD ["node", "backend/main.js"] From abc63c0cec74f22fc37fd13d77031028e89478e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Matz=20Andr=C3=A9n?= Date: Thu, 14 May 2026 17:17:51 +0200 Subject: [PATCH 07/30] Add public health endpoint for Docker healthcheck --- Dockerfile | 2 +- src/backend/src/app.controller.ts | 7 +++++-- src/backend/src/shared/auth.guard.ts | 7 ++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 85fdcbe..b896b27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,6 @@ USER spooty EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD curl -fsS http://127.0.0.1:3000/api || exit 1 + CMD curl -fsS http://127.0.0.1:3000/api/health || exit 1 CMD ["node", "backend/main.js"] diff --git a/src/backend/src/app.controller.ts b/src/backend/src/app.controller.ts index 6ac9162..b25065e 100644 --- a/src/backend/src/app.controller.ts +++ b/src/backend/src/app.controller.ts @@ -2,10 +2,13 @@ import { Controller, Get } from '@nestjs/common'; @Controller() export class AppController { - constructor() {} - @Get() getHello(): string { return 'ONLINE'; } + + @Get('health') + getHealth(): { status: string } { + return { status: 'ok' }; + } } diff --git a/src/backend/src/shared/auth.guard.ts b/src/backend/src/shared/auth.guard.ts index 09a1ddf..50fd671 100644 --- a/src/backend/src/shared/auth.guard.ts +++ b/src/backend/src/shared/auth.guard.ts @@ -9,6 +9,12 @@ import type { Request } from 'express'; @Injectable() export class AuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + + if (request.path === '/api/health') { + return true; + } + if (!this.authEnabled()) { return true; } @@ -20,7 +26,6 @@ export class AuthGuard implements CanActivate { ); } - const request = context.switchToHttp().getRequest(); const providedToken = this.extractToken(request); if (providedToken !== expectedToken) { From d1b040a8b14f4a8e3aaaeb7910c2b6b8386c6958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Matz=20Andr=C3=A9n?= Date: Thu, 14 May 2026 17:36:44 +0200 Subject: [PATCH 08/30] Allow frontend auth token bootstrap from URL --- src/frontend/src/app/app.component.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/frontend/src/app/app.component.ts b/src/frontend/src/app/app.component.ts index cd72f00..8a9d0d7 100644 --- a/src/frontend/src/app/app.component.ts +++ b/src/frontend/src/app/app.component.ts @@ -30,9 +30,23 @@ export class AppComponent { private readonly playlistService: PlaylistService, private readonly versionService: VersionService, ) { + this.bootstrapAuthTokenFromUrl(); this.fetchPlaylists(); } + private bootstrapAuthTokenFromUrl(): void { + const url = new URL(window.location.href); + const token = url.searchParams.get('token'); + + if (!token) { + return; + } + + localStorage.setItem('spooty_auth_token', token); + url.searchParams.delete('token'); + window.history.replaceState({}, document.title, url.toString()); + } + fetchPlaylists(): void { this.playlistService.fetch(); } From 4bb61f5ebec67108be03a0fc2434c13183ce6fcf Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Thu, 14 May 2026 18:00:53 +0200 Subject: [PATCH 09/30] Add deterministic YouTube source scoring --- src/backend/src/shared/youtube.service.ts | 135 +++++++++++++++++++++- src/backend/src/track/track.service.ts | 11 +- 2 files changed, 138 insertions(+), 8 deletions(-) diff --git a/src/backend/src/shared/youtube.service.ts b/src/backend/src/shared/youtube.service.ts index e62ee5a..80199ca 100644 --- a/src/backend/src/shared/youtube.service.ts +++ b/src/backend/src/shared/youtube.service.ts @@ -21,26 +21,114 @@ const ALLOWED_YOUTUBE_HOSTS = new Set([ 'youtu.be', ]); +const BAD_MATCH_TERMS = [ + 'live', + 'cover', + 'karaoke', + 'remix', + 'sped up', + 'slowed', + 'nightcore', + 'reaction', + 'tutorial', + 'instrumental', +]; + +export interface YoutubeMatch { + url: string; + title: string; + author: string; + score: number; + reason: string; +} + @Injectable() export class YoutubeService { private readonly logger = new Logger(TrackService.name); constructor(private readonly configService: ConfigService) {} - async findOnYoutubeOne(artist: string, name: string): Promise { + async findBestYoutubeMatch(artist: string, name: string): Promise { const query = `${artist} - ${name}`; this.logger.debug(`Searching ${query} on YT`); const result = await yts(query); - const firstVideo = result.videos?.[0]; + const candidates = (result.videos || []) + .filter((video: any) => !!video?.url && !!video?.title) + .slice(0, 10) + .map((video: any) => this.scoreCandidate(video, artist, name)) + .filter((match: YoutubeMatch) => { + try { + this.assertValidYoutubeUrl(match.url); + return true; + } catch { + return false; + } + }) + .sort((a: YoutubeMatch, b: YoutubeMatch) => { + if (b.score !== a.score) { + return b.score - a.score; + } + return a.url.localeCompare(b.url); + }); - if (!firstVideo?.url) { + const best = candidates[0]; + + if (!best) { throw new Error(`No YouTube result found for: ${query}`); } - this.assertValidYoutubeUrl(firstVideo.url); - this.logger.debug(`Found ${query} on ${firstVideo.url}`); - return firstVideo.url; + if (best.score < 40) { + throw new Error( + `No confident YouTube match for "${query}". Best score: ${best.score} (${best.title})`, + ); + } + + this.logger.debug( + `Selected YouTube match for "${query}": ${best.title} by ${best.author} (${best.score})`, + ); + + return best; + } + + async findOnYoutubeOne(artist: string, name: string): Promise { + return (await this.findBestYoutubeMatch(artist, name)).url; + } + + private scoreCandidate(video: any, artist: string, name: string): YoutubeMatch { + const title = String(video.title || ''); + const author = String(video.author?.name || video.author || ''); + const haystack = normalize(`${title} ${author}`); + const titleScore = tokenOverlap(name, title) * 55; + const artistScore = tokenOverlap(artist, `${title} ${author}`) * 35; + + let score = titleScore + artistScore; + const reasons = [ + `title=${Math.round(titleScore)}`, + `artist=${Math.round(artistScore)}`, + ]; + + for (const term of BAD_MATCH_TERMS) { + if (haystack.includes(term)) { + score -= 15; + reasons.push(`penalty:${term}`); + } + } + + if (haystack.includes('official audio') || haystack.includes('topic')) { + score += 10; + reasons.push('bonus:officialish'); + } + + score = Math.max(0, Math.min(100, Math.round(score))); + + return { + url: String(video.url), + title, + author, + score, + reason: reasons.join(', '), + }; } assertValidYoutubeUrl(url: string): void { @@ -144,3 +232,38 @@ export class YoutubeService { ); } } + +function normalize(value: string): string { + return value + .toLowerCase() + .replace(/&/g, ' and ') + .replace(/[^a-z0-9åäöüéèàáíóúñ]+/gi, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +function tokens(value: string): Set { + return new Set( + normalize(value) + .split(' ') + .filter((token) => token.length > 1), + ); +} + +function tokenOverlap(needle: string, haystack: string): number { + const needleTokens = tokens(needle); + const haystackTokens = tokens(haystack); + + if (needleTokens.size === 0) { + return 0; + } + + let matches = 0; + for (const token of needleTokens) { + if (haystackTokens.has(token)) { + matches++; + } + } + + return matches / needleTokens.size; +} diff --git a/src/backend/src/track/track.service.ts b/src/backend/src/track/track.service.ts index 8780989..26c9636 100644 --- a/src/backend/src/track/track.service.ts +++ b/src/backend/src/track/track.service.ts @@ -102,11 +102,18 @@ export class TrackService { }); let updatedTrack: TrackEntity; try { - const youtubeUrl = await this.youtubeService.findOnYoutubeOne( + const youtubeMatch = await this.youtubeService.findBestYoutubeMatch( track.artist, track.name, ); - updatedTrack = { ...track, youtubeUrl, status: TrackStatusEnum.Queued }; + updatedTrack = { + ...track, + youtubeUrl: youtubeMatch.url, + status: TrackStatusEnum.Queued, + }; + this.logger.debug( + `YouTube match for track ${track.id}: ${youtubeMatch.title} by ${youtubeMatch.author} score=${youtubeMatch.score} reason=${youtubeMatch.reason}`, + ); } catch (err) { this.logger.error(err); updatedTrack = { From 20632c6f9f228672735400b26ad83270af45994e Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Thu, 14 May 2026 18:18:37 +0200 Subject: [PATCH 10/30] Add deterministic YouTube matching and fix download path guard --- src/backend/src/shared/utils.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/src/shared/utils.service.ts b/src/backend/src/shared/utils.service.ts index 73ea1cc..0c3ba66 100644 --- a/src/backend/src/shared/utils.service.ts +++ b/src/backend/src/shared/utils.service.ts @@ -26,7 +26,7 @@ export class UtilsService { const resolvedCandidate = resolve(candidatePath); const rel = relative(root, resolvedCandidate); - if (rel === '' || (!rel.startsWith('..') && !resolve(rel).startsWith('/'))) { + if (rel === '' || (!rel.startsWith('..') && !relative('', rel).startsWith('..'))) { return resolvedCandidate; } From 64160fe21a91cf4ce1376b18e0d15ef7662a89d2 Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Thu, 14 May 2026 18:20:58 +0200 Subject: [PATCH 11/30] Ignore local test downloads --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index f646312..555f5e6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ **/*.sqlite # Local Netlify folder .netlify + +test-downloads/ From a1772c76707268e0d70a1a19a9d13188c3f51dc6 Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Thu, 14 May 2026 18:37:15 +0200 Subject: [PATCH 12/30] Make cover art non-fatal and disable critical CSS inlining --- src/backend/src/track/track.service.ts | 18 ++++++++++++------ src/frontend/angular.json | 10 +++++++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/backend/src/track/track.service.ts b/src/backend/src/track/track.service.ts index 26c9636..cb63820 100644 --- a/src/backend/src/track/track.service.ts +++ b/src/backend/src/track/track.service.ts @@ -199,12 +199,18 @@ export class TrackService { const folderName = this.getFolderName(track, track.playlist); await this.youtubeService.downloadAndFormat(track, folderName); if (coverUrl) { - await this.youtubeService.addImage( - folderName, - coverUrl, - track.name, - track.artist, - ); + try { + await this.youtubeService.addImage( + folderName, + coverUrl, + track.name, + track.artist, + ); + } catch (imageError) { + this.logger.warn( + `Cover art embed failed for track ${track.id}: ${toSafeErrorMessage(imageError)}`, + ); + } } } catch (err) { this.logger.error(err); diff --git a/src/frontend/angular.json b/src/frontend/angular.json index e39aab0..fc6b073 100644 --- a/src/frontend/angular.json +++ b/src/frontend/angular.json @@ -50,7 +50,15 @@ "maximumError": "4kB" } ], - "outputHashing": "all" + "outputHashing": "all", + "optimization": { + "scripts": true, + "styles": { + "minify": true, + "inlineCritical": false + }, + "fonts": true + } }, "development": { "optimization": false, From 0034b28f1a14e8f74e3b527836f593554f974f62 Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Thu, 14 May 2026 19:07:53 +0200 Subject: [PATCH 13/30] Improve large playlist stability and Spotify item endpoint --- src/backend/src/shared/spotify-api.service.ts | 2 +- .../src/track/track-search.processor.ts | 27 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/backend/src/shared/spotify-api.service.ts b/src/backend/src/shared/spotify-api.service.ts index 0d21b3b..c28e731 100644 --- a/src/backend/src/shared/spotify-api.service.ts +++ b/src/backend/src/shared/spotify-api.service.ts @@ -168,7 +168,7 @@ export class SpotifyApiService { ); const response = await fetch( - `https://api.spotify.com/v1/playlists/${playlistId}/tracks?offset=${offset}&limit=100&fields=items(track(id,name,artists,preview_url,album(images))),next`, + `https://api.spotify.com/v1/playlists/${playlistId}/items?offset=${offset}&limit=100&fields=items(track(id,name,artists,preview_url,album(images))),next`, { headers: { Authorization: `Bearer ${accessToken}`, diff --git a/src/backend/src/track/track-search.processor.ts b/src/backend/src/track/track-search.processor.ts index 0336193..53ebd07 100644 --- a/src/backend/src/track/track-search.processor.ts +++ b/src/backend/src/track/track-search.processor.ts @@ -4,8 +4,18 @@ import { Job } from 'bullmq'; import { TrackService } from './track.service'; import { TrackEntity } from './track.entity'; +function getSearchDelayMs(): number { + const parsed = Number(process.env.YT_SEARCH_DELAY_MS || 5000); + + if (!Number.isFinite(parsed) || parsed < 0) { + return 5000; + } + + return Math.max(1000, Math.min(30000, Math.floor(parsed))); +} + @Processor('track-search-processor', { - concurrency: 2, + concurrency: 1, }) export class TrackSearchProcessor extends WorkerHost { private readonly logger = new Logger(TrackSearchProcessor.name); @@ -14,13 +24,14 @@ export class TrackSearchProcessor extends WorkerHost { super(); } - async process(job: Job): Promise { - if (!job.data?.id) { - this.logger.warn(`Skipping malformed search job ${job.id}`); - return; - } + async process(job: Job<{ id: number }, void, string>): Promise { + const delayMs = getSearchDelayMs(); - this.logger.debug(`Processing search job ${job.id} for track ${job.data.id}`); - await this.trackService.findOnYoutube(job.data); + this.logger.debug( + `Processing search job ${job.id} for track ${job.data.id}; pacing ${delayMs}ms`, + ); + + await new Promise((res) => setTimeout(res, delayMs)); + await this.trackService.findOnYoutube({ id: job.data.id } as TrackEntity); } } From c1fd39c3773ba7b0aecf34e45a04a042100c4bd4 Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Thu, 14 May 2026 19:40:27 +0200 Subject: [PATCH 14/30] Add Spotify OAuth and large playlist support --- CHANGELOG.md | 199 +++----- README.md | 447 +++++++++++++----- src/backend/src/environmentEnum.ts | 2 + src/backend/src/shared/auth.guard.ts | 7 +- .../controllers/spotify-auth.controller.ts | 38 ++ src/backend/src/shared/shared.module.ts | 3 +- src/backend/src/shared/spotify-api.service.ts | 337 +++++++++---- src/frontend/src/app/app.component.html | 7 + src/frontend/src/app/app.component.ts | 14 + 9 files changed, 727 insertions(+), 327 deletions(-) create mode 100644 src/backend/src/shared/controllers/spotify-auth.controller.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2188d0a..e714779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,171 +1,122 @@ -### Changelog +# Changelog -All notable changes to this project will be documented in this file. Dates are displayed in UTC. +All notable changes to the hardened Spooty branch are documented here. -#### [2.4.2](https://github.com/Raiper34/spooty/compare/2.4.1...2.4.2) +This project follows a practical chronological changelog rather than autogenerated dependency noise. -- fix(spotify): use data.next for playlist pagination instead of filtered track count [`#52`](https://github.com/Raiper34/spooty/issues/52) [`#35`](https://github.com/Raiper34/spooty/issues/35) +--- -#### [2.4.1](https://github.com/Raiper34/spooty/compare/2.4.0...2.4.1) +# Unreleased -> 7 February 2026 +## Added -- fix(docker): fix docker node version [`a14f5ea`](https://github.com/Raiper34/spooty/commit/a14f5eab6805acdf4755dbd8f37a289a0d675c12) +### Spotify OAuth Integration -#### [2.4.0](https://github.com/Raiper34/spooty/compare/2.3.4...2.4.0) +- Added Spotify OAuth login flow +- Added `/api/spotify/login` +- Added `/api/spotify/callback` +- Added `/api/spotify/status` +- Added persistent Spotify user access token storage +- Added authenticated Spotify playlist retrieval +- Added OAuth frontend bootstrap flow -> 7 February 2026 +### Large Playlist Support -- feat(files): add possibility to set the audio quality of downloaded files [`#39`](https://github.com/Raiper34/spooty/issues/39) -- fix(downloading): fix downloading songs from browser with special characters [`#41`](https://github.com/Raiper34/spooty/issues/41) -- fix(downloadin): fix ejs runtime problem [`#38`](https://github.com/Raiper34/spooty/issues/38) +- Fixed Spotify playlist truncation at 100 tracks +- Added full Spotify pagination support +- Verified support for playlists exceeding 300 tracks +- Improved playlist queue handling stability -#### [2.3.4](https://github.com/Raiper34/spooty/compare/2.3.3...2.3.4) +### Queue Hardening -> 4 February 2026 +- Added configurable YouTube search pacing +- Added configurable YouTube download pacing +- Added sequential search queue behavior +- Reduced aggressive request bursts toward YouTube +- Improved queue logging visibility -- fix(ytdl): Upgrade ytdl package (automated) [`ef32347`](https://github.com/Raiper34/spooty/commit/ef32347903d7caa2cc72c9c54341195b2ac0bb96) +### Security -#### [2.3.3](https://github.com/Raiper34/spooty/compare/2.3.2...2.3.3) +- Added token-based frontend access mode +- Added external secret handling recommendations +- Added isolated Spotify credential storage +- Improved Docker credential handling documentation -> 30 January 2026 +### Frontend -- fix(ytdl): Upgrade ytdl package (automated) [`cc552d1`](https://github.com/Raiper34/spooty/commit/cc552d1def309a3633aa67ca7f3659eb44ef47b2) +- Added Spotify login integration +- Added auth token bootstrap from URL +- Improved playlist rendering behavior +- Improved large playlist progress tracking -#### [2.3.2](https://github.com/Raiper34/spooty/compare/2.3.1...2.3.2) +### Docker -> 26 January 2026 +- Improved hardened Docker runtime flow +- Added explicit environment variable examples +- Added safer default pacing recommendations +- Improved deployment documentation -- fix(ytdl): Upgrade ytdl package (automated) [`531730b`](https://github.com/Raiper34/spooty/commit/531730bc57f9ab1339277ad995f37611a9f55aa8) +--- -#### [2.3.1](https://github.com/Raiper34/spooty/compare/2.3.0...2.3.1) +# 2.4.2 -> 23 January 2026 +## Fixed -- fix(ytdl): Upgrade ytdl package (automated) [`08b83bf`](https://github.com/Raiper34/spooty/commit/08b83bf9de70999bddb47cd1d64d63d1ab550d88) -- docs(readme): change redirect URI [`8162a2a`](https://github.com/Raiper34/spooty/commit/8162a2a21f762310f857dfc42cad53831769ed66) +- Fixed Spotify playlist pagination using `data.next` +- Fixed incomplete playlist retrieval behavior -#### [2.3.0](https://github.com/Raiper34/spooty/compare/2.2.1...2.3.0) +--- -> 29 December 2025 +# 2.4.1 -- feat(track): allow download of individual track and fix album art [`#29`](https://github.com/Raiper34/spooty/issues/29) -- docs(readme): remove demo image and gif from readme [`a2ac579`](https://github.com/Raiper34/spooty/commit/a2ac579854a6cbc95729d434c514fb2309f7a3bb) +## Fixed -#### [2.2.1](https://github.com/Raiper34/spooty/compare/2.2.0...2.2.1) +- Fixed Docker Node.js version compatibility -> 15 November 2025 +--- -- fix(downloading): migrate from @distube/ytdl-core to ytdlp-nodejs yt downloading library [`5795f7c`](https://github.com/Raiper34/spooty/commit/5795f7cc178ab4d8a8d3d8a7e94f33743ff6e9e0) -- fix(docker): add python3 dependency into docker image for yt download library [`46d7c66`](https://github.com/Raiper34/spooty/commit/46d7c6699bc8698896b22837e8507885677a71bd) -- docs(readme): fix yt cookies section heading [`285578f`](https://github.com/Raiper34/spooty/commit/285578ff1ed1d7fe14d7ddd0d95a4b8079276e4e) -- docs(readme): add docker versio badge into readme [`4be3743`](https://github.com/Raiper34/spooty/commit/4be3743f7b5a8019bd6ed0d2248f23a254a82603) +# 2.4.0 -#### [2.2.0](https://github.com/Raiper34/spooty/compare/2.1.1...2.2.0) +## Added -> 17 October 2025 +- Added configurable audio quality selection -- feat(spotify): integrate Spotify API for playlist metadata and track retrieval [`de55e42`](https://github.com/Raiper34/spooty/commit/de55e42fcd246d86e8d3156d8e0ab4898b5755d6) -- feat(youtube): use youtube cookies to bypass limitation & add timeout between each downloads [`4c05178`](https://github.com/Raiper34/spooty/commit/4c051782fd8808a808fe49fe1509ec0b0db9d05e) +## Fixed -#### [2.1.1](https://github.com/Raiper34/spooty/compare/2.1.0...2.1.1) +- Fixed special character download issues +- Fixed EJS runtime issue -> 13 August 2025 +--- -- feat(gui): add version to gui header [`#25`](https://github.com/Raiper34/spooty/issues/25) -- fix(track): fix track missing artist and title tags [`b93993c`](https://github.com/Raiper34/spooty/commit/b93993c6cf034dccad3df08485d2126614874743) -- style(gui): change gui primary to match spotify primary color [`5068fa8`](https://github.com/Raiper34/spooty/commit/5068fa8a4b724ef4a1060dc4d83cc80afee08bad) +# 2.3.0 -#### [2.1.0](https://github.com/Raiper34/spooty/compare/2.0.11...2.1.0) +## Added -> 12 August 2025 +- Added individual track downloads +- Added cover art support -- feat(track): add track cover art into downloaded file [`#24`](https://github.com/Raiper34/spooty/issues/24) +--- -#### [2.0.11](https://github.com/Raiper34/spooty/compare/2.0.10...2.0.11) +# 2.2.0 -> 14 June 2025 +## Added -- fix(ytdl): Upgrade ytdl package (automated) [`fe79302`](https://github.com/Raiper34/spooty/commit/fe79302c2a747093df39405e42c06c08957acc3c) +- Added Spotify API integration +- Added YouTube cookie support +- Added download timeout handling -#### [2.0.10](https://github.com/Raiper34/spooty/compare/2.0.9...2.0.10) +--- -> 4 June 2025 +# 2.0.0 -- fix(ytdl): Upgrade ytdl package (automated) [`f74a4d2`](https://github.com/Raiper34/spooty/commit/f74a4d2ed9da50e29f4e44b0027b2891c7473ec6) +## Added -#### [2.0.9](https://github.com/Raiper34/spooty/compare/2.0.8...2.0.9) +- Introduced queue-based backend architecture -> 8 May 2025 +--- -- fix(ytdl): Upgrade ytdl package (automated) [`cc6a201`](https://github.com/Raiper34/spooty/commit/cc6a20196d474152eecaeb9862edfb3505ed3106) +# 1.0.0 -#### [2.0.8](https://github.com/Raiper34/spooty/compare/2.0.7...2.0.8) +## Added -> 27 April 2025 - -- docs(readme): remove docs, netlify and docsify, keep only readme docs for now [`bc9482c`](https://github.com/Raiper34/spooty/commit/bc9482c6cb9a47aa0d96fa34866546ec552d1e3c) -- fix(ytdl): Upgrade ytdl package (automated) [`17b875e`](https://github.com/Raiper34/spooty/commit/17b875e2825cb739767565be5d86aba3a48a9208) - -#### [2.0.7](https://github.com/Raiper34/spooty/compare/2.0.6...2.0.7) - -> 5 April 2025 - -- fix(ytdl): Upgrade ytdl package (automated) [`9653db5`](https://github.com/Raiper34/spooty/commit/9653db54ea48e93526f2124b11b8012d987b7bcc) - -#### [2.0.6](https://github.com/Raiper34/spooty/compare/2.0.5...2.0.6) - -> 1 April 2025 - -- ci(netlify): remove netlify cli from deps [`e4cde85`](https://github.com/Raiper34/spooty/commit/e4cde8585ba0dfc422cec89360612947ebe92ff1) -- fix(ytdl): Upgrade ytdl package (automated) [`5f30f31`](https://github.com/Raiper34/spooty/commit/5f30f3116f34ceb48d00e3abd5c606e8f9b7caba) -- feat(names): strip special characters from file and folder names [`6f39c1e`](https://github.com/Raiper34/spooty/commit/6f39c1e5dc4b8f87917a18d2a23cec6b5cd9ca60) -- ci(github-actions): fix github actions update ytdl script [`04d8987`](https://github.com/Raiper34/spooty/commit/04d8987aa51a8887bf1b8b41e8680c2db9530bef) - -#### [2.0.5](https://github.com/Raiper34/spooty/compare/2.0.4...2.0.5) - -> 20 March 2025 - -- ci(docker): fix docker buildx in release-it for github actions [`db65823`](https://github.com/Raiper34/spooty/commit/db65823ce0794280cb373a4381e24ad704a4db56) -- fix(ytdl): Upgrade ytdl package (automated) [`16c57aa`](https://github.com/Raiper34/spooty/commit/16c57aaefd599f04b0a88723d55751f4ba27383a) - -#### [2.0.4](https://github.com/Raiper34/spooty/compare/2.0.3...2.0.4) - -> 15 March 2025 - -- fix(ytdl): Upgrade ytdl package (automated) [`b960dc8`](https://github.com/Raiper34/spooty/commit/b960dc88db2a44c5865cb5c6f5964e4b4ffedc3f) - -#### [2.0.3](https://github.com/Raiper34/spooty/compare/2.0.2...2.0.3) - -> 15 March 2025 - -- fix(ytdl): Upgrade ytdl package (automated) [`844d026`](https://github.com/Raiper34/spooty/commit/844d02668512cb8446f27f5629412fb5331b1298) - -#### [2.0.2](https://github.com/Raiper34/spooty/compare/2.0.1...2.0.2) - -> 15 March 2025 - -- fix(ytdl): Upgrade ytdl package (automated) [`cf559eb`](https://github.com/Raiper34/spooty/commit/cf559eb41a2eb84cd29d4a732d68885f4ebcf348) - -#### [2.0.1](https://github.com/Raiper34/spooty/compare/2.0.0...2.0.1) - -> 23 February 2025 - -- build(ytdl): Upgrade ytdl package (automated) [`#20`](https://github.com/Raiper34/spooty/pull/20) -- refactor(backend): queue system presented [`#5`](https://github.com/Raiper34/spooty/issues/5) -- fix(utdl): upgrade ytdl package [`66b55b3`](https://github.com/Raiper34/spooty/commit/66b55b39432f2da71bd401fd65af6c0a207dd6c2) -- ci(github-actions): add automatic ytdl update into github actions [`2ca165b`](https://github.com/Raiper34/spooty/commit/2ca165b59a76147439d4d00055b84102cf7a7317) -- ci(github-actions): add automatic ytdl update into github actions [`9baf3b9`](https://github.com/Raiper34/spooty/commit/9baf3b9eb7b093029300614bc9c1a913a73fb003) -- ci(github-actions): add automatic ytdl update into github actions [`b2fbb01`](https://github.com/Raiper34/spooty/commit/b2fbb01a7415c4027aa50dd2a1773d0b1e0dde3a) -- docs(website): change default environment variable for REDIS_RUN var [`f539a22`](https://github.com/Raiper34/spooty/commit/f539a227575569ce5641f5dfec6fcf50598a491d) -- ci(github-actions): add automatic ytdl update into github actions [`cbe2c9f`](https://github.com/Raiper34/spooty/commit/cbe2c9fbdc8322881b6ae3fd90586b7656d155ec) -- ci(github-actions): add automatic ytdl update into github actions [`a55ebf6`](https://github.com/Raiper34/spooty/commit/a55ebf659974137f96a465b74a8a0c8b649b3399) -- ci(github-actions): add automatic ytdl update into github actions [`66516e6`](https://github.com/Raiper34/spooty/commit/66516e6916af1b1e29df13d870f775f5c7ce43c2) - - - -### 2.0.0 -- refactor(backend): queue system presented - -### 1.0.0 -- initial release \ No newline at end of file +- Initial release diff --git a/README.md b/README.md index b0543d5..92bb5bb 100644 --- a/README.md +++ b/README.md @@ -1,157 +1,370 @@ -[![npm version](https://img.shields.io/docker/pulls/raiper34/spooty)](https://hub.docker.com/r/raiper34/spooty) -[![npm version](https://img.shields.io/docker/image-size/raiper34/spooty)](https://hub.docker.com/r/raiper34/spooty) -![Docker Image Version](https://img.shields.io/docker/v/raiper34/spooty) -[![npm version](https://img.shields.io/docker/stars/raiper34/spooty)](https://hub.docker.com/r/raiper34/spooty) -[![GitHub License](https://img.shields.io/github/license/raiper34/spooty)](https://github.com/Raiper34/spooty) -[![GitHub Repo stars](https://img.shields.io/github/stars/raiper34/spooty)](https://github.com/Raiper34/spooty) +# Spooty Hardened -![spooty logo](assets/logo.svg) -# Spooty - selfhosted Spotify downloader -Spooty is a self-hosted Spotify downloader. -It allows download track/playlist/album from the Spotify url. -It can also subscribe to a playlist or author page and download new songs upon release. -Spooty basically downloads nothing from Spotify, it only gets information from spotify and then finds relevant and downloadeds music on Youtube. -The project is based on NestJS and Angular. +Self-hosted Spotify playlist and track downloader built with NestJS + Angular. -> [!IMPORTANT] -> Please do not use this tool for piracy! Download only music you own rights! Use this tool only on your responsibility. +Spooty does not download audio from Spotify itself. +It retrieves metadata from Spotify and locates matching audio on YouTube. -### Content -- [🚀 Installation](#-installation) - - [Spotify App Configuration](#spotify-app-configuration) - - [Docker](#docker) - - [Docker command](#docker-command) - - [Docker compose](#docker-compose) - - [Build from source](#build-from-source) - - [Process](#requirements) - - [Requirements](#process) - - [Environment variables](#environment-variables) - - [YouTube cookies](#youtube-cookies) -- [⚖️ License](#-license) +This hardened branch focuses on: -## 🚀 Installation -Recommended and the easiest way how to start to use of Spooty is using docker. +- Large playlist support (>100 tracks) +- Spotify OAuth login flow +- Better YouTube pacing and throttling resistance +- Improved Docker deployment +- Safer credential handling +- More resilient cover-art embedding +- Better queue stability -### Spotify App Configuration +--- -To fully use Spooty, you need to create an application in the Spotify Developer Dashboard: +# Features -1. Go to [Spotify Developer Dashboard](https://developer.spotify.com/dashboard) -2. Sign in with your Spotify account -3. Create a new application -4. Note your `Client ID` and `Client Secret` -5. Configure the redirect URI to `http://127.0.0.1:3000/api/callback` (or the corresponding URL of your instance) +- Download Spotify playlists +- Download individual Spotify tracks +- Playlist auto-subscription support +- Automatic YouTube matching +- MP3 tagging and embedded cover art +- Docker deployment +- Queue-based download system +- Spotify OAuth integration +- Large playlist pagination support +- Download pacing controls +- YouTube cookie support -These credentials will be used by Spooty to access the Spotify API. +--- -### Docker +# Important Notice -Just run docker command or use docker compose configuration. -For detailed configuration, see available [environment variables](#environment-variables). +Use this software responsibly. -#### Docker command -```shell -docker run -d -p 3000:3000 \ - -v /path/to/downloads:/spooty/backend/downloads \ - -v /path/to/cookies.txt:/spooty/config/cookies.txt \ - -e SPOTIFY_CLIENT_ID=your_client_id \ - -e SPOTIFY_CLIENT_SECRET=your_client_secret \ - raiper34/spooty:latest -``` +Only download music you legally own or are permitted to access. -#### Docker compose -```yaml +The maintainers are not responsible for misuse. + +--- + +# Supported URLs + +- Spotify playlists +- Spotify tracks + +Example: + + https://open.spotify.com/playlist/... + https://open.spotify.com/track/... + +--- + +# Quick Start (Recommended) + +## 1. Create Spotify Developer App + +Go to: + + https://developer.spotify.com/dashboard + +Create an application. + +Add this Redirect URI: + + http://127.0.0.1:3000/api/spotify/callback + +Copy: + +- Client ID +- Client Secret + +--- + +## 2. Store Credentials Outside Repository + +Create a secure env file: + +~~~bash +sudo mkdir -p /etc/tokens + +sudo tee /etc/tokens/spotify.env > /dev/null <<'EOF' +SPOTIFY_CLIENT_ID=your_client_id +SPOTIFY_CLIENT_SECRET=your_client_secret +EOF + +sudo chown root:$USER /etc/tokens/spotify.env +sudo chmod 640 /etc/tokens/spotify.env +~~~ + +Never commit this file. + +--- + +# Docker Run + +~~~bash +docker run --rm -p 3000:3000 \ + --env-file /etc/tokens/spotify.env \ + -e SPOTIFY_REDIRECT_URI='http://127.0.0.1:3000/api/spotify/callback' \ + -e AUTH_ENABLED=true \ + -e SPOOTY_AUTH_TOKEN=change_this_token \ + -e YT_SEARCH_DELAY_MS=7000 \ + -e YT_DOWNLOADS_PER_MINUTE=3 \ + -v "$PWD/downloads:/spooty/backend/downloads" \ + spootyfy-hardened:local +~~~ + +Open: + + http://127.0.0.1:3000/?token=change_this_token + +Then: + +1. Click "Connect Spotify" +2. Login to Spotify +3. Approve access +4. Paste playlist URL +5. Download + +--- + +# Docker Compose + +~~~yaml services: spooty: - image: raiper34/spooty:latest + image: spootyfy-hardened:local container_name: spooty restart: unless-stopped + ports: - "3000:3000" - volumes: - - /path/to/downloads:/spooty/backend/downloads - - /path/to/cookies.txt:/spooty/config/cookies.txt + + env_file: + - /etc/tokens/spotify.env + environment: - - SPOTIFY_CLIENT_ID=your_client_id - - SPOTIFY_CLIENT_SECRET=your_client_secret - # Configure other environment variables if needed -``` + SPOTIFY_REDIRECT_URI: "http://127.0.0.1:3000/api/spotify/callback" + AUTH_ENABLED: "true" + SPOOTY_AUTH_TOKEN: "change_this_token" -### Build from source + YT_SEARCH_DELAY_MS: "7000" + YT_DOWNLOADS_PER_MINUTE: "3" -Spooty can be also build from source files on your own. + volumes: + - ./downloads:/spooty/backend/downloads +~~~ -#### Requirements -- Node v20.20.0 (it is recommended to use `nvm` node version manager to install proper version of node) -- Redis in memory cache -- Ffmpeg +--- + +# Build From Source + +## Requirements + +- Node.js 20.20.0 +- Docker +- ffmpeg - Python3 +- Redis -#### Process -- install Node v20.20.0 using `nvm install` and use that node version `nvm use` -- from project root install all dependencies using `npm install` -- copy `.env.default` as `.env` in `src/backend` folder and modify desired environment properties (see [environment variables](#environment-variables)) -- add your Spotify application credentials to the `.env` file: - ``` - SPOTIFY_CLIENT_ID=your_client_id - SPOTIFY_CLIENT_SECRET=your_client_secret - ``` -- build source files `npm run build` - - built project will be stored in `dist` folder -- start server `npm run start` +--- -### Environment variables +## Build -Some behaviour and settings of Spooty can be configured using environment variables and `.env` file. +~~~bash +npm install +npm run build +~~~ - Name | Default | Description | --------------------------|---------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - DB_PATH | `./config/db.sqlite` (relative to backend) | Path where Spooty database will be stored | - FE_PATH | `../frontend/browser` (relative to backend) | Path to frontend part of application | - DOWNLOADS_PATH | `./downloads` (relative to backend) | Path where downaloded files will be stored | - FORMAT | `mp3` | Format of downloaded files ('aac', 'flac', 'mp3', 'm4a', 'opus', 'vorbis', 'wav', 'alac') | - QUALITY | undefined | Audio quality (0-9 VBR or specific bitrate) of downloaded files | - PORT | 3000 | Port of Spooty server | - REDIS_PORT | 6379 | Port of Redis server | - REDIS_HOST | localhost | Host of Redis server | - RUN_REDIS | false | Whenever Redis server should be started from backend (recommended for Docker environment) | - SPOTIFY_CLIENT_ID | your_client_id | Client ID of your Spotify application (required) | - SPOTIFY_CLIENT_SECRET | your_client_secret | Client Secret of your Spotify application (required) | - YT_DOWNLOADS_PER_MINUTE | 3 | Set the maximum number of YouTube downloads started per minute | - YT_COOKIES | | Browser name to automatically extract YouTube cookies from (e.g. `chrome`, `firefox`). Only works when running Spooty natively (not in Docker). See [below](#yt_cookies---browser-based-cookies-non-docker). | - YT_COOKIES_FILE | `./config/cookies.txt` | Path to a Netscape-format `cookies.txt` file. Recommended for Docker deployments. See [below](#yt_cookies_file---cookies-file-recommended-for-docker). | +--- -### YouTube cookies +## Run -YouTube may block or throttle downloads without authentication cookies. Spooty supports two ways to provide them — use the one that fits your setup. +~~~bash +npm run start +~~~ -#### `YT_COOKIES` — browser-based cookies (non-Docker) +--- -Set `YT_COOKIES` to the name of your browser and yt-dlp will automatically read cookies directly from it. -Supported values: `chrome`, `firefox`, `edge`, `safari`, `brave`, `opera`, `chromium`. +# Environment Variables -``` -YT_COOKIES=chrome -``` +| Variable | Default | Description | +|---|---|---| +| DB_PATH | `./config/db.sqlite` | SQLite database path | +| DOWNLOADS_PATH | `./downloads` | Download directory | +| FORMAT | `mp3` | Output format | +| REDIS_HOST | `localhost` | Redis host | +| REDIS_PORT | `6379` | Redis port | +| SPOTIFY_CLIENT_ID | unset | Spotify client ID | +| SPOTIFY_CLIENT_SECRET | unset | Spotify client secret | +| SPOTIFY_REDIRECT_URI | unset | OAuth callback URI | +| AUTH_ENABLED | `false` | Enable frontend token auth | +| SPOOTY_AUTH_TOKEN | unset | Frontend auth token | +| YT_SEARCH_DELAY_MS | `5000` | Delay between YouTube searches | +| YT_DOWNLOADS_PER_MINUTE | `3` | Download pacing | +| YT_COOKIES | unset | Browser cookie extraction | +| YT_COOKIES_FILE | unset | Netscape cookies.txt path | -> [!NOTE] -> This only works when Spooty runs on the same machine as your browser (i.e. not in Docker, where no browser is present). +--- -#### `YT_COOKIES_FILE` — cookies file (recommended for Docker) +# YouTube Cookies -Export your YouTube cookies as a Netscape `cookies.txt` file and provide its path. This is the recommended approach for Docker deployments. +YouTube may throttle or block requests without cookies. -**How to get your `cookies.txt` file:** -1. Install a browser extension that exports cookies in Netscape format, e.g. [Get cookies.txt LOCALLY](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc) for Chrome or [cookies.txt](https://addons.mozilla.org/en-US/firefox/addon/cookies-txt/) for Firefox. -2. Go to https://www.youtube.com and log in. -3. Use the extension to export cookies for `youtube.com` and save the file as `cookies.txt`. +Two supported methods exist. -**Docker usage:** +--- -Bind mount the `cookies.txt` file into the container and set `YT_COOKIES_FILE` to its path inside the container. See the [Environment variables](#environment-variables) section for details. +## Browser Cookies (Native Install Only) -> [!NOTE] -> `YT_COOKIES` takes priority over `YT_COOKIES_FILE` if both are set. +~~~bash +YT_COOKIES=firefox +~~~ -# ⚖️ License -[MIT](https://choosealicense.com/licenses/mit/) +Supported: + +- firefox +- chrome +- chromium +- edge +- brave +- opera + +This does NOT work reliably in Docker. + +--- + +## Cookies File (Recommended) + +Export a Netscape-format `cookies.txt`. + +Mount into container: + +~~~bash +-v /path/to/cookies.txt:/spooty/config/cookies.txt +~~~ + +Then: + +~~~bash +-e YT_COOKIES_FILE=/spooty/config/cookies.txt +~~~ + +--- + +# Large Playlist Support + +The hardened branch fixes several Spotify API limitations: + +- Proper playlist pagination +- >100 track playlists +- OAuth-authenticated playlist access +- Queue stability improvements + +Verified working with playlists containing 300+ tracks. + +--- + +# Queue Pacing + +Aggressive YouTube access can trigger: + +- HTTP 302 loops +- CAPTCHA +- temporary throttling +- incomplete downloads + +Recommended safe pacing: + +~~~bash +-e YT_SEARCH_DELAY_MS=7000 +-e YT_DOWNLOADS_PER_MINUTE=3 +~~~ + +--- + +# Security Notes + +Never commit: + +- Spotify secrets +- OAuth tokens +- cookies.txt +- downloaded music +- local databases + +Recommended `.gitignore` additions: + +~~~gitignore +downloads/ +config/ +*.sqlite +cookies.txt +.env +.env.local +~~~ + +--- + +# Hardened Branch Changelog + +## Unreleased Hardened Changes + +### Spotify + +- Added Spotify OAuth login flow +- Added persistent Spotify user token storage +- Added authenticated playlist retrieval +- Fixed large playlist pagination +- Fixed playlist truncation at 100 tracks +- Added playlist retrieval debugging +- Added OAuth status endpoint + +### YouTube + +- Added configurable search pacing +- Added configurable download pacing +- Reduced aggressive YouTube request bursts +- Improved queue throttling behavior + +### Backend + +- Added safer queue pacing logic +- Added explicit search processor throttling +- Improved Docker runtime stability +- Improved logging around Spotify retrieval +- Added auth bootstrap flow support + +### Frontend + +- Added Spotify login integration +- Added frontend auth token bootstrap +- Fixed playlist progress handling for large playlists +- Improved playlist rendering stability + +### Security + +- Moved secrets to external env files +- Improved Docker secret handling recommendations +- Added security documentation +- Prevented accidental secret inclusion in repository + +--- + +# Development Notes + +Recommended local test run: + +~~~bash +docker run --rm -p 3000:3000 \ + --env-file /etc/tokens/spotify.env \ + -e SPOTIFY_REDIRECT_URI='http://127.0.0.1:3000/api/spotify/callback' \ + -e AUTH_ENABLED=true \ + -e SPOOTY_AUTH_TOKEN=test-token \ + -e YT_SEARCH_DELAY_MS=7000 \ + -e YT_DOWNLOADS_PER_MINUTE=3 \ + -v "$PWD/downloads:/spooty/backend/downloads" \ + spootyfy-hardened:local +~~~ + +--- + +# License + +MIT diff --git a/src/backend/src/environmentEnum.ts b/src/backend/src/environmentEnum.ts index 8208771..61caf30 100644 --- a/src/backend/src/environmentEnum.ts +++ b/src/backend/src/environmentEnum.ts @@ -12,4 +12,6 @@ export enum EnvironmentEnum { SPOOTY_AUTH_TOKEN = 'SPOOTY_AUTH_TOKEN', CORS_ORIGIN = 'CORS_ORIGIN', NODE_ENV = 'NODE_ENV', + SPOTIFY_REDIRECT_URI = 'SPOTIFY_REDIRECT_URI', + SPOTIFY_TOKEN_PATH = 'SPOTIFY_TOKEN_PATH', } diff --git a/src/backend/src/shared/auth.guard.ts b/src/backend/src/shared/auth.guard.ts index 50fd671..9e7f9c8 100644 --- a/src/backend/src/shared/auth.guard.ts +++ b/src/backend/src/shared/auth.guard.ts @@ -11,7 +11,12 @@ export class AuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); - if (request.path === '/api/health') { + if ( + request.path === '/api/health' || + request.path === '/api/spotify/login' || + request.path === '/api/spotify/callback' || + request.path === '/api/spotify/status' + ) { return true; } diff --git a/src/backend/src/shared/controllers/spotify-auth.controller.ts b/src/backend/src/shared/controllers/spotify-auth.controller.ts new file mode 100644 index 0000000..cf4014b --- /dev/null +++ b/src/backend/src/shared/controllers/spotify-auth.controller.ts @@ -0,0 +1,38 @@ +import { Controller, Get, Query, Res } from '@nestjs/common'; +import type { Response } from 'express'; +import { SpotifyApiService } from '../spotify-api.service'; + +@Controller('spotify') +export class SpotifyAuthController { + constructor(private readonly spotifyApiService: SpotifyApiService) {} + + @Get('status') + async status(): Promise<{ connected: boolean }> { + return { connected: await this.spotifyApiService.hasUserToken() }; + } + + @Get('login') + login(@Res() res: Response): void { + res.redirect(this.spotifyApiService.getAuthorizationUrl()); + } + + @Get('callback') + async callback( + @Query('code') code: string | undefined, + @Query('error') error: string | undefined, + @Res() res: Response, + ): Promise { + if (error) { + res.redirect(`/?spotify_error=${encodeURIComponent(error)}`); + return; + } + + if (!code) { + res.redirect('/?spotify_error=missing_code'); + return; + } + + await this.spotifyApiService.exchangeAuthorizationCode(code); + res.redirect('/?spotify=connected'); + } +} diff --git a/src/backend/src/shared/shared.module.ts b/src/backend/src/shared/shared.module.ts index 752e8e9..a19186f 100644 --- a/src/backend/src/shared/shared.module.ts +++ b/src/backend/src/shared/shared.module.ts @@ -4,11 +4,12 @@ import { ConfigModule } from '@nestjs/config'; import { SpotifyService } from './spotify.service'; import { YoutubeService } from './youtube.service'; import { SpotifyApiService } from './spotify-api.service'; +import { SpotifyAuthController } from './controllers/spotify-auth.controller'; @Module({ imports: [ConfigModule], providers: [UtilsService, SpotifyService, YoutubeService, SpotifyApiService], - controllers: [], + controllers: [SpotifyAuthController], exports: [UtilsService, SpotifyService, YoutubeService, SpotifyApiService], }) export class SharedModule {} diff --git a/src/backend/src/shared/spotify-api.service.ts b/src/backend/src/shared/spotify-api.service.ts index c28e731..120f15f 100644 --- a/src/backend/src/shared/spotify-api.service.ts +++ b/src/backend/src/shared/spotify-api.service.ts @@ -1,16 +1,25 @@ import { Injectable, Logger } from '@nestjs/common'; +import { resolve, dirname } from 'path'; +import * as fs from 'fs'; +import { EnvironmentEnum } from '../environmentEnum'; // eslint-disable-next-line @typescript-eslint/no-var-requires const fetch = require('isomorphic-unfetch'); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getDetails } = require('spotify-url-info')(fetch); +interface StoredSpotifyUserToken { + access_token: string; + refresh_token: string; + expires_at: number; + scope?: string; + token_type?: string; +} + @Injectable() export class SpotifyApiService { private readonly logger = new Logger(SpotifyApiService.name); private accessToken: string | null = null; - private tokenExpiry: number = 0; - - constructor() {} + private tokenExpiry = 0; private getPlaylistId(url: string): string { try { @@ -51,13 +60,179 @@ export class SpotifyApiService { } } + private getClientId(): string { + const clientId = process.env.SPOTIFY_CLIENT_ID; + if (!clientId) { + throw new Error('Missing SPOTIFY_CLIENT_ID'); + } + return clientId; + } + + private getClientSecret(): string { + const clientSecret = process.env.SPOTIFY_CLIENT_SECRET; + if (!clientSecret) { + throw new Error('Missing SPOTIFY_CLIENT_SECRET'); + } + return clientSecret; + } + + private getRedirectUri(): string { + return ( + process.env[EnvironmentEnum.SPOTIFY_REDIRECT_URI] || + 'http://127.0.0.1:3000/api/spotify/callback' + ); + } + + private getTokenPath(): string { + if (process.env[EnvironmentEnum.SPOTIFY_TOKEN_PATH]) { + return process.env[EnvironmentEnum.SPOTIFY_TOKEN_PATH]; + } + + const dbPath = process.env[EnvironmentEnum.DB_PATH] || './config/db.sqlite'; + return resolve(dirname(resolve(__dirname, dbPath)), 'spotify-user-token.json'); + } + + private getBasicAuthHeader(): string { + const credentials = Buffer.from( + `${this.getClientId()}:${this.getClientSecret()}`, + ).toString('base64'); + return `Basic ${credentials}`; + } + + private readUserToken(): StoredSpotifyUserToken | null { + const tokenPath = this.getTokenPath(); + + if (!fs.existsSync(tokenPath)) { + return null; + } + + try { + return JSON.parse(fs.readFileSync(tokenPath, 'utf-8')); + } catch (error) { + this.logger.error(`Failed to read Spotify user token: ${error.message}`); + return null; + } + } + + private writeUserToken(token: StoredSpotifyUserToken): void { + const tokenPath = this.getTokenPath(); + fs.mkdirSync(dirname(tokenPath), { recursive: true }); + fs.writeFileSync(tokenPath, JSON.stringify(token, null, 2), { + encoding: 'utf-8', + mode: 0o600, + }); + } + + async hasUserToken(): Promise { + return !!this.readUserToken(); + } + + getAuthorizationUrl(): string { + const url = new URL('https://accounts.spotify.com/authorize'); + + url.searchParams.set('response_type', 'code'); + url.searchParams.set('client_id', this.getClientId()); + url.searchParams.set( + 'scope', + [ + 'playlist-read-private', + 'playlist-read-collaborative', + 'user-read-private', + ].join(' '), + ); + url.searchParams.set('redirect_uri', this.getRedirectUri()); + url.searchParams.set('show_dialog', 'true'); + + return url.toString(); + } + + async exchangeAuthorizationCode(code: string): Promise { + const response = await fetch('https://accounts.spotify.com/api/token', { + method: 'POST', + headers: { + Authorization: this.getBasicAuthHeader(), + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: this.getRedirectUri(), + }).toString(), + }); + + if (!response.ok) { + const errorData = await response.text(); + throw new Error(`Failed to exchange Spotify authorization code: ${errorData}`); + } + + const data = await response.json(); + + this.writeUserToken({ + access_token: data.access_token, + refresh_token: data.refresh_token, + expires_at: Date.now() + data.expires_in * 1000 - 60_000, + scope: data.scope, + token_type: data.token_type, + }); + + this.logger.debug('Stored Spotify user access token'); + } + + private async refreshUserAccessToken( + token: StoredSpotifyUserToken, + ): Promise { + const response = await fetch('https://accounts.spotify.com/api/token', { + method: 'POST', + headers: { + Authorization: this.getBasicAuthHeader(), + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: token.refresh_token, + }).toString(), + }); + + if (!response.ok) { + const errorData = await response.text(); + throw new Error(`Failed to refresh Spotify user token: ${errorData}`); + } + + const data = await response.json(); + const refreshed = { + ...token, + access_token: data.access_token, + refresh_token: data.refresh_token || token.refresh_token, + expires_at: Date.now() + data.expires_in * 1000 - 60_000, + scope: data.scope || token.scope, + token_type: data.token_type || token.token_type, + }; + + this.writeUserToken(refreshed); + return refreshed; + } + + private async getUserAccessToken(): Promise { + const token = this.readUserToken(); + + if (!token) { + throw new Error('Spotify user is not connected. Open /api/spotify/login first.'); + } + + if (Date.now() < token.expires_at) { + return token.access_token; + } + + return (await this.refreshUserAccessToken(token)).access_token; + } + async getTrackMetadata( spotifyUrl: string, ): Promise<{ name: string; artist: string; image: string }> { try { this.logger.debug(`Getting track metadata for ${spotifyUrl}`); const trackId = this.getTrackId(spotifyUrl); - const accessToken = await this.getAccessToken(); + const accessToken = await this.getClientCredentialsAccessToken(); const response = await fetch( `https://api.spotify.com/v1/tracks/${trackId}`, @@ -102,31 +277,18 @@ export class SpotifyApiService { } } - private async getAccessToken(): Promise { + private async getClientCredentialsAccessToken(): Promise { if (this.accessToken && Date.now() < this.tokenExpiry) { return this.accessToken; } try { - this.logger.debug('Getting new Spotify access token'); - - const clientId = process.env.SPOTIFY_CLIENT_ID; - const clientSecret = process.env.SPOTIFY_CLIENT_SECRET; - - if (!clientId || !clientSecret) { - throw new Error( - 'Missing Spotify credentials. Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET in .env file', - ); - } - - const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString( - 'base64', - ); + this.logger.debug('Getting new Spotify client credentials access token'); const response = await fetch('https://accounts.spotify.com/api/token', { method: 'POST', headers: { - Authorization: `Basic ${credentials}`, + Authorization: this.getBasicAuthHeader(), 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'grant_type=client_credentials', @@ -141,7 +303,7 @@ export class SpotifyApiService { this.accessToken = data.access_token; this.tokenExpiry = Date.now() + data.expires_in * 1000 - 60000; - this.logger.debug('Successfully obtained Spotify access token'); + this.logger.debug('Successfully obtained Spotify client credentials access token'); return this.accessToken; } catch (error) { this.logger.error(`Error getting Spotify access token: ${error.message}`); @@ -155,86 +317,93 @@ export class SpotifyApiService { const playlistId = this.getPlaylistId(spotifyUrl); this.logger.debug(`Extracted playlist ID: ${playlistId}`); - - const accessToken = await this.getAccessToken(); + + const accessToken = await this.getUserAccessToken(); const allTracks = []; let offset = 0; let hasMoreTracks = true; while (hasMoreTracks) { - this.logger.debug( - `Fetching tracks from Spotify API with offset ${offset}`, - ); + this.logger.debug( + `Fetching tracks from Spotify API with offset ${offset}`, + ); - const response = await fetch( - `https://api.spotify.com/v1/playlists/${playlistId}/items?offset=${offset}&limit=100&fields=items(track(id,name,artists,preview_url,album(images))),next`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, + const response = await fetch( + `https://api.spotify.com/v1/playlists/${playlistId}/items?offset=${offset}&limit=100`, + { + headers: { + Authorization: `Bearer ${accessToken}`, }, + }, + ); + + if (!response.ok) { + const errorText = await response.text(); + this.logger.error( + `Spotify API error: ${response.status} ${errorText}`, ); - if (!response.ok) { - const errorText = await response.text(); - this.logger.error( - `Spotify API error: ${response.status} ${errorText}`, + if (response.status === 403) { + throw new Error( + 'Spotify API refused playlist item access with 403 Forbidden. Connect Spotify through /api/spotify/login and ensure the authenticated user can access this playlist.', ); - throw new Error(`Failed to fetch tracks: ${response.status}`); } - const data = await response.json(); - - if (!data.items || data.items.length === 0) { - this.logger.debug('No more tracks to fetch from Spotify API'); - hasMoreTracks = false; - continue; - } - - const pageTracks = data.items - .map( - (item: { - track: { - id: string; - name: any; - artists: any[]; - preview_url: any; - album: { images: any[] }; - }; - }) => { - if (!item.track) return null; - - return { - id: item.track.id, - name: item.track.name, - artist: item.track.artists.map((a) => a.name).join(', '), - previewUrl: item.track.preview_url, - coverUrl: item.track.album?.images?.[0]?.url || null, - }; - }, - ) - .filter((track) => track !== null); - - this.logger.debug( - `Retrieved ${pageTracks.length} tracks from Spotify API at offset ${offset}`, - ); - - if (pageTracks.length > 0) { - allTracks.push(...pageTracks); - } - - if (!data.next) { - hasMoreTracks = false; - } else { - offset += 100; - } + throw new Error(`Failed to fetch tracks: ${response.status}`); } + const data = await response.json(); + + if (!data.items || data.items.length === 0) { + this.logger.debug('No more tracks to fetch from Spotify API'); + hasMoreTracks = false; + continue; + } + + const pageTracks = data.items + .map( + (item: { + item: { + id: string; + name: any; + artists: any[]; + preview_url: any; + album: { images: any[] }; + }; + }) => { + if (!item.item) return null; + + return { + id: item.item.id, + name: item.item.name, + artist: item.item.artists.map((a) => a.name).join(', '), + previewUrl: item.item.preview_url, + coverUrl: item.item.album?.images?.[0]?.url || null, + }; + }, + ) + .filter((track) => track !== null); + this.logger.debug( - `Total tracks retrieved from Spotify API: ${allTracks.length}`, + `Retrieved ${pageTracks.length} tracks from Spotify API at offset ${offset}`, ); - return allTracks; + + if (pageTracks.length > 0) { + allTracks.push(...pageTracks); + } + + if (!data.next) { + hasMoreTracks = false; + } else { + offset += 100; + } + } + + this.logger.debug( + `Total tracks retrieved from Spotify API: ${allTracks.length}`, + ); + return allTracks; } catch (error) { this.logger.error(`Failed to get all playlist tracks: ${error.message}`); throw error; diff --git a/src/frontend/src/app/app.component.html b/src/frontend/src/app/app.component.html index 97d2ea3..3dc769c 100644 --- a/src/frontend/src/app/app.component.html +++ b/src/frontend/src/app/app.component.html @@ -6,6 +6,13 @@ v{{version}}

Self-hosted spotify downloader

+ diff --git a/src/frontend/src/app/app.component.ts b/src/frontend/src/app/app.component.ts index 8a9d0d7..95e7b3c 100644 --- a/src/frontend/src/app/app.component.ts +++ b/src/frontend/src/app/app.component.ts @@ -5,6 +5,7 @@ import {PlaylistService, PlaylistStatusEnum} from "./services/playlist.service"; import {PlaylistBoxComponent} from "./components/playlist-box/playlist-box.component"; import {VersionService} from "./services/version.service"; import {map} from "rxjs"; +import {HttpClient} from "@angular/common/http"; @Component({ selector: 'app-root', @@ -25,12 +26,15 @@ export class AppComponent { playlists$ = this.playlistService.all$.pipe(map(items => items.filter(item => !item.isTrack))); songs$ = this.playlistService.all$.pipe(map(items => items.filter(item => item.isTrack))); version = this.versionService.getVersion(); + spotifyConnected = false; constructor( private readonly playlistService: PlaylistService, private readonly versionService: VersionService, + private readonly http: HttpClient, ) { this.bootstrapAuthTokenFromUrl(); + this.checkSpotifyStatus(); this.fetchPlaylists(); } @@ -47,6 +51,16 @@ export class AppComponent { window.history.replaceState({}, document.title, url.toString()); } + checkSpotifyStatus(): void { + this.http + .get<{ connected: boolean }>('/api/spotify/status') + .subscribe((status) => (this.spotifyConnected = status.connected)); + } + + connectSpotify(): void { + window.location.href = '/api/spotify/login'; + } + fetchPlaylists(): void { this.playlistService.fetch(); } From ca388a8fc8dc2f31d74744bcbcf8e6bf9922cca7 Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Thu, 14 May 2026 21:52:58 +0200 Subject: [PATCH 15/30] Improve deterministic YouTube matching and playlist handling --- CHANGELOG.md | 21 ++ src/backend/src/shared/spotify-api.service.ts | 5 +- src/backend/src/shared/youtube.service.ts | 232 ++++++++++++------ src/backend/src/track/track.entity.ts | 3 + 4 files changed, 183 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e714779..0cbd812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,27 @@ This project follows a practical chronological changelog rather than autogenerat - Added safer default pacing recommendations - Improved deployment documentation +## Unreleased + +### Added +- Spotify OAuth login flow +- Persistent Spotify user token handling +- Large playlist pagination support (>100 tracks) +- Deterministic YouTube candidate scoring +- Duration-aware YouTube matching +- Detailed YouTube candidate debug logging +- Improved Docker deployment guidance + +### Improved +- YouTube pacing and throttling resistance +- Cover art embedding resilience +- Queue stability during large playlist imports + +### Fixed +- Spotify playlist truncation at 100 tracks +- Incorrect live/acoustic YouTube selections +- Playlist retrieval failures on large public playlists + --- # 2.4.2 diff --git a/src/backend/src/shared/spotify-api.service.ts b/src/backend/src/shared/spotify-api.service.ts index 120f15f..3ff818e 100644 --- a/src/backend/src/shared/spotify-api.service.ts +++ b/src/backend/src/shared/spotify-api.service.ts @@ -228,7 +228,7 @@ export class SpotifyApiService { async getTrackMetadata( spotifyUrl: string, - ): Promise<{ name: string; artist: string; image: string }> { + ): Promise<{ name: string; artist: string; image: string; durationMs?: number }> { try { this.logger.debug(`Getting track metadata for ${spotifyUrl}`); const trackId = this.getTrackId(spotifyUrl); @@ -253,6 +253,7 @@ export class SpotifyApiService { name: data.name, artist: data.artists.map((a) => a.name).join(', '), image: data.album.images[0]?.url || '', + durationMs: data.duration_ms, }; } catch (error) { this.logger.error(`Failed to get track metadata: ${error.message}`); @@ -370,6 +371,7 @@ export class SpotifyApiService { artists: any[]; preview_url: any; album: { images: any[] }; + duration_ms?: number; }; }) => { if (!item.item) return null; @@ -380,6 +382,7 @@ export class SpotifyApiService { artist: item.item.artists.map((a) => a.name).join(', '), previewUrl: item.item.preview_url, coverUrl: item.item.album?.images?.[0]?.url || null, + durationMs: item.item.duration_ms, }; }, ) diff --git a/src/backend/src/shared/youtube.service.ts b/src/backend/src/shared/youtube.service.ts index 80199ca..5d6a1dd 100644 --- a/src/backend/src/shared/youtube.service.ts +++ b/src/backend/src/shared/youtube.service.ts @@ -23,6 +23,13 @@ const ALLOWED_YOUTUBE_HOSTS = new Set([ const BAD_MATCH_TERMS = [ 'live', + 'ao vivo', + 'acústico', + 'acustico', + 'acoustic', + 'concert', + 'festival', + 'session', 'cover', 'karaoke', 'remix', @@ -34,6 +41,16 @@ const BAD_MATCH_TERMS = [ 'instrumental', ]; +const OFFICIALISH_TERMS = [ + 'official audio', + 'official video', + 'official music video', + 'lyric video', + 'lyrics', + 'vevo', + '- topic', +]; + export interface YoutubeMatch { url: string; title: string; @@ -42,22 +59,30 @@ export interface YoutubeMatch { reason: string; } +interface CandidateScore extends YoutubeMatch { + rejected: boolean; +} + @Injectable() export class YoutubeService { private readonly logger = new Logger(TrackService.name); constructor(private readonly configService: ConfigService) {} - async findBestYoutubeMatch(artist: string, name: string): Promise { + async findBestYoutubeMatch( + artist: string, + name: string, + durationMs?: number, + ): Promise { const query = `${artist} - ${name}`; this.logger.debug(`Searching ${query} on YT`); const result = await yts(query); const candidates = (result.videos || []) .filter((video: any) => !!video?.url && !!video?.title) - .slice(0, 10) - .map((video: any) => this.scoreCandidate(video, artist, name)) - .filter((match: YoutubeMatch) => { + .slice(0, 15) + .map((video: any) => this.scoreCandidate(video, artist, name, durationMs)) + .filter((match: CandidateScore) => { try { this.assertValidYoutubeUrl(match.url); return true; @@ -65,72 +90,160 @@ export class YoutubeService { return false; } }) - .sort((a: YoutubeMatch, b: YoutubeMatch) => { - if (b.score !== a.score) { - return b.score - a.score; - } - return a.url.localeCompare(b.url); - }); + .sort((a, b) => b.score - a.score); - const best = candidates[0]; + this.logger.debug( + `YouTube candidates for "${query}": ` + + candidates + .slice(0, 5) + .map( + (c) => + `[${c.score}${c.rejected ? ' rejected' : ''}] ${c.title} by ${c.author} (${c.reason})`, + ) + .join(' | '), + ); - if (!best) { - throw new Error(`No YouTube result found for: ${query}`); - } + const accepted = candidates.find((candidate) => !candidate.rejected); - if (best.score < 40) { - throw new Error( - `No confident YouTube match for "${query}". Best score: ${best.score} (${best.title})`, - ); + if (!accepted) { + throw new Error(`No acceptable YouTube result found for: ${query}`); } this.logger.debug( - `Selected YouTube match for "${query}": ${best.title} by ${best.author} (${best.score})`, + `Selected YouTube match for "${query}": ${accepted.title} by ${accepted.author} (${accepted.score})`, ); - return best; + return accepted; } - async findOnYoutubeOne(artist: string, name: string): Promise { - return (await this.findBestYoutubeMatch(artist, name)).url; - } - - private scoreCandidate(video: any, artist: string, name: string): YoutubeMatch { + private scoreCandidate( + video: any, + artist: string, + name: string, + durationMs?: number, + ): CandidateScore { const title = String(video.title || ''); const author = String(video.author?.name || video.author || ''); - const haystack = normalize(`${title} ${author}`); - const titleScore = tokenOverlap(name, title) * 55; - const artistScore = tokenOverlap(artist, `${title} ${author}`) * 35; + const url = String(video.url || ''); + const seconds = this.getVideoSeconds(video); - let score = titleScore + artistScore; - const reasons = [ - `title=${Math.round(titleScore)}`, - `artist=${Math.round(artistScore)}`, - ]; + const normalizedTitle = this.normalize(title); + const normalizedAuthor = this.normalize(author); + const normalizedArtist = this.normalize(artist); + const normalizedName = this.normalize(name); - for (const term of BAD_MATCH_TERMS) { - if (haystack.includes(term)) { - score -= 15; - reasons.push(`penalty:${term}`); + let score = 0; + const reasons: string[] = []; + let rejected = false; + + if (normalizedTitle.includes(normalizedName)) { + score += 45; + reasons.push('title=45'); + } + + for (const artistPart of this.artistParts(normalizedArtist)) { + if (artistPart.length >= 3 && (normalizedTitle.includes(artistPart) || normalizedAuthor.includes(artistPart))) { + score += 20; + reasons.push(`artist=${artistPart}:20`); + break; } } - if (haystack.includes('official audio') || haystack.includes('topic')) { - score += 10; - reasons.push('bonus:officialish'); + const officialish = OFFICIALISH_TERMS.find((term) => + `${normalizedTitle} ${normalizedAuthor}`.includes(this.normalize(term)), + ); + if (officialish) { + score += 12; + reasons.push(`officialish=${officialish}:12`); } - score = Math.max(0, Math.min(100, Math.round(score))); + const badTerm = BAD_MATCH_TERMS.find((term) => + normalizedTitle.includes(this.normalize(term)), + ); + if (badTerm) { + score -= 25; + reasons.push(`bad=${badTerm}:-25`); + } + + if (durationMs && seconds) { + const spotifySeconds = Math.round(durationMs / 1000); + const diff = Math.abs(seconds - spotifySeconds); + const ratio = diff / Math.max(spotifySeconds, 1); + + if (diff <= 8 || ratio <= 0.05) { + score += 40; + reasons.push(`duration=excellent:${diff}s:+40`); + } else if (diff <= 15 || ratio <= 0.10) { + score += 30; + reasons.push(`duration=good:${diff}s:+30`); + } else if (diff <= 30 || ratio <= 0.15) { + score += 18; + reasons.push(`duration=ok:${diff}s:+18`); + } else if (diff <= 45 || ratio <= 0.25) { + score += 5; + reasons.push(`duration=weak:${diff}s:+5`); + } else if (diff > 60 && ratio > 0.40) { + score -= 80; + rejected = true; + reasons.push(`duration=reject:${diff}s:-80`); + } else { + score -= 35; + reasons.push(`duration=bad:${diff}s:-35`); + } + } else { + reasons.push('duration=unknown'); + } return { - url: String(video.url), + url, title, author, score, reason: reasons.join(', '), + rejected, }; } + private normalize(value: string): string { + return value + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/&/g, ' and ') + .replace(/[^a-z0-9]+/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + } + + private artistParts(normalizedArtist: string): string[] { + return normalizedArtist + .split(/\s+(and|feat|featuring|ft|x|,|with)\s+|,/) + .map((part) => part.trim()) + .filter((part) => part && !['and', 'feat', 'featuring', 'ft', 'x', 'with'].includes(part)); + } + + private getVideoSeconds(video: any): number | undefined { + if (typeof video.seconds === 'number' && Number.isFinite(video.seconds)) { + return video.seconds; + } + + const timestamp = String(video.timestamp || video.duration || ''); + const parts = timestamp + .split(':') + .map((part) => Number(part)) + .filter((part) => Number.isFinite(part)); + + if (parts.length === 2) { + return parts[0] * 60 + parts[1]; + } + + if (parts.length === 3) { + return parts[0] * 3600 + parts[1] * 60 + parts[2]; + } + + return undefined; + } + assertValidYoutubeUrl(url: string): void { let parsed: URL; @@ -232,38 +345,3 @@ export class YoutubeService { ); } } - -function normalize(value: string): string { - return value - .toLowerCase() - .replace(/&/g, ' and ') - .replace(/[^a-z0-9åäöüéèàáíóúñ]+/gi, ' ') - .replace(/\s+/g, ' ') - .trim(); -} - -function tokens(value: string): Set { - return new Set( - normalize(value) - .split(' ') - .filter((token) => token.length > 1), - ); -} - -function tokenOverlap(needle: string, haystack: string): number { - const needleTokens = tokens(needle); - const haystackTokens = tokens(haystack); - - if (needleTokens.size === 0) { - return 0; - } - - let matches = 0; - for (const token of needleTokens) { - if (haystackTokens.has(token)) { - matches++; - } - } - - return matches / needleTokens.size; -} diff --git a/src/backend/src/track/track.entity.ts b/src/backend/src/track/track.entity.ts index 4c327e3..ab17da9 100644 --- a/src/backend/src/track/track.entity.ts +++ b/src/backend/src/track/track.entity.ts @@ -36,6 +36,9 @@ export class TrackEntity { @Column({ nullable: true }) coverUrl?: string; // Track-specific album art (overrides playlist coverUrl) + @Column({ nullable: true }) + durationMs?: number; + @Column({ default: Date.now() }) createdAt?: number; From 48517a5a851aeed14fe6310427c967daa1630bfd Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Fri, 15 May 2026 20:47:25 +0200 Subject: [PATCH 16/30] Use direct yt-dlp CLI with deterministic fallback handling --- .dockerignore | 13 ++ .gitignore | 1 + Dockerfile | 2 +- src/backend/src/playlist/playlist.service.ts | 105 +++++++++ src/backend/src/shared/spotify-api.service.ts | 216 ++++++++++++++++++ src/backend/src/shared/spotify.service.ts | 11 + src/backend/src/shared/youtube.service.ts | 164 ++++++++++--- src/backend/src/track/track.entity.ts | 6 + src/backend/src/track/track.service.ts | 82 ++++++- src/frontend/src/app/app.component.html | 144 ++++++------ src/frontend/src/app/app.component.scss | 129 +++++++++++ .../playlist-box/playlist-box.component.html | 80 +++++-- .../playlist-box/playlist-box.component.scss | 72 +++++- .../playlist-box/playlist-box.component.ts | 31 ++- 14 files changed, 913 insertions(+), 143 deletions(-) diff --git a/.dockerignore b/.dockerignore index db9b566..7617d12 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,3 +11,16 @@ config .env.* README.md assets + +# Local runtime/download artifacts +test-downloads +downloads +config +*.sqlite +*.sqlite3 +*.db +*.mp3 +*.m4a +*.webm +*.part +spooty-config diff --git a/.gitignore b/.gitignore index 555f5e6..a0c1e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ .netlify test-downloads/ +spooty-config diff --git a/Dockerfile b/Dockerfile index b896b27..09868ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN npm run build FROM node:20.20.0-alpine WORKDIR /spooty -RUN apk add --no-cache ffmpeg redis python3 py3-pip curl \ +RUN apk add --no-cache ffmpeg redis python3 py3-pip curl yt-dlp \ && addgroup -S spooty \ && adduser -S spooty -G spooty diff --git a/src/backend/src/playlist/playlist.service.ts b/src/backend/src/playlist/playlist.service.ts index 35942f2..ac973c3 100644 --- a/src/backend/src/playlist/playlist.service.ts +++ b/src/backend/src/playlist/playlist.service.ts @@ -51,9 +51,12 @@ export class PlaylistService { async create(playlist: PlaylistEntity): Promise { // Detect if URL is for a single track or a playlist and route accordingly const isTrack = this.spotifyService.isTrackUrl(playlist.spotifyUrl); + const isArtist = this.spotifyService.isArtistUrl(playlist.spotifyUrl); if (isTrack) { await this.createSingleTrack(playlist); + } else if (isArtist) { + await this.createArtistLibrary(playlist); } else { await this.createPlaylist(playlist); } @@ -105,6 +108,107 @@ export class PlaylistService { } } + + private async createArtistLibrary(playlist: PlaylistEntity): Promise { + let detail: { tracks: any[]; name: string; image: string }; + let playlist2Save: PlaylistEntity; + + try { + detail = await this.spotifyService.getArtistLibrary(playlist.spotifyUrl); + this.logger.debug( + `Artist library retrieved with ${detail.tracks?.length || 0} tracks`, + ); + + playlist2Save = { + ...playlist, + name: detail.name, + coverUrl: detail.image, + active: false, + }; + this.createPlaylistFolderStructure(playlist2Save.name); + } catch (err) { + this.logger.error(`Error getting artist library details: ${err}`); + playlist2Save = { + ...playlist, + name: 'Failed artist library import', + error: toSafeErrorMessage(err), + active: false, + }; + } + + const savedPlaylist = await this.save(playlist2Save); + + if (detail?.tracks && detail.tracks.length > 0) { + await this.createTracksForSavedPlaylist(detail.tracks, savedPlaylist); + } else { + this.logger.warn(`No tracks found for artist library ${savedPlaylist.name}`); + } + } + + private async createTracksForSavedPlaylist( + tracks: any[], + savedPlaylist: PlaylistEntity, + ): Promise { + this.logger.debug( + `Starting to process ${tracks.length} tracks for ${savedPlaylist.name}`, + ); + + let processedCount = 0; + let skippedCount = 0; + let errorCount = 0; + + for (const track of tracks) { + try { + if (!track.artist || !track.name) { + this.logger.warn( + `Skipping track ${processedCount + skippedCount + 1}: Missing artist or name information`, + ); + skippedCount++; + continue; + } + + if (track.unavailable === true) { + this.logger.warn( + `Skipping unavailable track ${processedCount + skippedCount + 1}: ${track.artist} - ${track.name}`, + ); + skippedCount++; + continue; + } + + await this.trackService.create( + { + artist: track.artist, + name: track.name, + spotifyUrl: track.previewUrl || null, + coverUrl: track.coverUrl || savedPlaylist.coverUrl, + durationMs: track.durationMs, + }, + savedPlaylist, + ); + + processedCount++; + + if (processedCount % 100 === 0) { + this.logger.debug( + `Processed ${processedCount} tracks so far for ${savedPlaylist.name}`, + ); + } + } catch (error) { + this.logger.error( + `Error creating track "${ + track?.artist || 'Unknown' + } - ${track?.name || 'Unknown'}": ${error.message}`, + ); + errorCount++; + } + } + + this.logger.debug( + `Finished processing ${savedPlaylist.name}: ` + + `${processedCount} tracks processed, ${skippedCount} skipped, ${errorCount} errors`, + ); + } + private async createPlaylist(playlist: PlaylistEntity): Promise { let detail: { tracks: any; name: any; image: any }; let playlist2Save: PlaylistEntity; @@ -162,6 +266,7 @@ export class PlaylistService { name: track.name, spotifyUrl: track.previewUrl || null, coverUrl: track.coverUrl || savedPlaylist.coverUrl, // Use track's album art, fallback to playlist cover + durationMs: track.durationMs, }, savedPlaylist, ); diff --git a/src/backend/src/shared/spotify-api.service.ts b/src/backend/src/shared/spotify-api.service.ts index 3ff818e..cfd50f3 100644 --- a/src/backend/src/shared/spotify-api.service.ts +++ b/src/backend/src/shared/spotify-api.service.ts @@ -60,6 +60,30 @@ export class SpotifyApiService { } } + isArtistUrl(url: string): boolean { + try { + const urlObj = new URL(url); + return urlObj.pathname.includes('/artist/'); + } catch { + return false; + } + } + + private getArtistId(url: string): string { + try { + const urlObj = new URL(url); + const pathParts = urlObj.pathname.split('/'); + const artistIndex = pathParts.findIndex((part) => part === 'artist'); + if (artistIndex >= 0 && pathParts.length > artistIndex + 1) { + return pathParts[artistIndex + 1].split('?')[0]; + } + throw new Error('Invalid Spotify artist URL'); + } catch (error) { + this.logger.error(`Failed to extract artist ID: ${error.message}`); + throw error; + } + } + private getClientId(): string { const clientId = process.env.SPOTIFY_CLIENT_ID; if (!clientId) { @@ -412,4 +436,196 @@ export class SpotifyApiService { throw error; } } + + async getArtistLibrary( + spotifyUrl: string, + ): Promise<{ name: string; image: string; tracks: any[] }> { + try { + this.logger.debug(`Getting artist library for ${spotifyUrl}`); + const artistId = this.getArtistId(spotifyUrl); + const accessToken = await this.getUserAccessToken(); + + const artistResponse = await fetch( + `https://api.spotify.com/v1/artists/${artistId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + if (!artistResponse.ok) { + const errorText = await artistResponse.text(); + throw new Error(`Failed to fetch artist metadata: ${artistResponse.status} ${errorText}`); + } + + const artist = await artistResponse.json(); + const albums = await this.getAllArtistAlbums(artistId, accessToken); + const tracks = await this.getTracksForAlbums(albums, accessToken, artist.name); + + return { + name: `Artist Library: ${artist.name}`, + image: artist.images?.[0]?.url || '', + tracks, + }; + } catch (error) { + this.logger.error(`Failed to get artist library: ${error.message}`); + throw error; + } + } + + private async getAllArtistAlbums(artistId: string, accessToken: string): Promise { + const includeGroups = process.env.SPOTIFY_ARTIST_INCLUDE_GROUPS || 'album,single'; + const albumByKey = new Map(); + const artistAlbumPageLimit = 20; + let offset = 0; + let hasMoreAlbums = true; + + while (hasMoreAlbums) { + this.logger.debug( + `Fetching artist albums from Spotify API with offset ${offset}`, + ); + + const albumUrl = new URL( + `https://api.spotify.com/v1/artists/${artistId}/albums`, + ); + albumUrl.searchParams.set('include_groups', includeGroups); + albumUrl.searchParams.set('offset', String(offset)); + + this.logger.debug(`Spotify artist albums URL: ${albumUrl.toString()}`); + + const response = await fetch(albumUrl.toString(), { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to fetch artist albums: ${response.status} ${errorText}`); + } + + const data = await response.json(); + + for (const album of data.items || []) { + const key = this.normalizeLibraryKey(`${album.name}|${album.release_date}|${album.total_tracks}`); + if (!albumByKey.has(key)) { + albumByKey.set(key, album); + } + } + + if (!data.next) { + hasMoreAlbums = false; + } else { + offset += artistAlbumPageLimit; + } + } + + const albums = [...albumByKey.values()]; + this.logger.debug(`Retrieved ${albums.length} deduplicated artist albums/singles`); + return albums; + } + + private async getTracksForAlbums( + albums: any[], + accessToken: string, + artistName: string, + ): Promise { + const trackByKey = new Map(); + const maxTracks = Number(process.env.SPOTIFY_ARTIST_LIBRARY_MAX_TRACKS || 1000); + + for (const album of albums) { + let offset = 0; + let hasMoreTracks = true; + + while (hasMoreTracks) { + const response = await fetch( + `https://api.spotify.com/v1/albums/${album.id}/tracks?limit=50&offset=${offset}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to fetch album tracks: ${response.status} ${errorText}`); + } + + const data = await response.json(); + + for (const item of data.items || []) { + if (!item?.name || !item?.artists?.length) { + continue; + } + + const fullTrack = await this.getTrackById(item.id, accessToken); + const isrc = fullTrack?.external_ids?.isrc; + const durationMs = fullTrack?.duration_ms || item.duration_ms; + const artist = item.artists.map((a) => a.name).join(', '); + const key = isrc + ? `isrc:${isrc}` + : this.normalizeLibraryKey(`${artist}|${item.name}|${Math.round((durationMs || 0) / 2000)}`); + + if (!trackByKey.has(key)) { + trackByKey.set(key, { + id: item.id, + name: item.name, + artist, + previewUrl: item.external_urls?.spotify || null, + coverUrl: album.images?.[0]?.url || null, + durationMs, + albumName: album.name, + primaryArtist: artistName, + }); + } + + if (trackByKey.size >= maxTracks) { + this.logger.warn(`Artist library max track limit reached: ${maxTracks}`); + return [...trackByKey.values()]; + } + } + + if (!data.next) { + hasMoreTracks = false; + } else { + offset += 50; + } + } + } + + const tracks = [...trackByKey.values()]; + this.logger.debug(`Retrieved ${tracks.length} deduplicated artist library tracks`); + return tracks; + } + + private async getTrackById(trackId: string, accessToken: string): Promise { + const response = await fetch( + `https://api.spotify.com/v1/tracks/${trackId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + if (!response.ok) { + return null; + } + + return response.json(); + } + + private normalizeLibraryKey(value: string): string { + return value + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/&/g, ' and ') + .replace(/[^a-z0-9]+/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + } + } diff --git a/src/backend/src/shared/spotify.service.ts b/src/backend/src/shared/spotify.service.ts index c029f0e..b1a1b14 100644 --- a/src/backend/src/shared/spotify.service.ts +++ b/src/backend/src/shared/spotify.service.ts @@ -16,6 +16,10 @@ export class SpotifyService { return this.spotifyApiService.isTrackUrl(url); } + isArtistUrl(url: string): boolean { + return this.spotifyApiService.isArtistUrl(url); + } + async getTrackDetail( spotifyUrl: string, ): Promise<{ name: string; artist: string; image: string }> { @@ -61,6 +65,13 @@ export class SpotifyService { } } + async getArtistLibrary( + spotifyUrl: string, + ): Promise<{ name: string; tracks: any[]; image: string }> { + this.logger.debug(`Get artist library ${spotifyUrl} on Spotify`); + return this.spotifyApiService.getArtistLibrary(spotifyUrl); + } + async getPlaylistTracks(spotifyUrl: string): Promise { this.logger.debug(`Get playlist ${spotifyUrl} on Spotify`); try { diff --git a/src/backend/src/shared/youtube.service.ts b/src/backend/src/shared/youtube.service.ts index 5d6a1dd..69d83c2 100644 --- a/src/backend/src/shared/youtube.service.ts +++ b/src/backend/src/shared/youtube.service.ts @@ -3,9 +3,9 @@ import { TrackEntity } from '../track/track.entity'; import { EnvironmentEnum } from '../environmentEnum'; import { TrackService } from '../track/track.service'; import { ConfigService } from '@nestjs/config'; -import { YtDlp } from 'ytdlp-nodejs'; import * as yts from 'yt-search'; import * as fs from 'fs'; +import { spawn } from 'child_process'; const NodeID3 = require('node-id3'); const HEADERS = { @@ -73,13 +73,24 @@ export class YoutubeService { artist: string, name: string, durationMs?: number, + excludedUrls: string[] = [], ): Promise { const query = `${artist} - ${name}`; this.logger.debug(`Searching ${query} on YT`); const result = await yts(query); + const excludedVideoIds = new Set( + excludedUrls + .map((url) => this.getYoutubeVideoId(url)) + .filter((id): id is string => !!id), + ); + const candidates = (result.videos || []) .filter((video: any) => !!video?.url && !!video?.title) + .filter((video: any) => { + const videoId = this.getYoutubeVideoId(String(video.url)); + return !videoId || !excludedVideoIds.has(videoId); + }) .slice(0, 15) .map((video: any) => this.scoreCandidate(video, artist, name, durationMs)) .filter((match: CandidateScore) => { @@ -106,7 +117,10 @@ export class YoutubeService { const accepted = candidates.find((candidate) => !candidate.rejected); if (!accepted) { - throw new Error(`No acceptable YouTube result found for: ${query}`); + throw new Error( + `No acceptable YouTube result found for: ${query}` + + (excludedUrls.length ? ` after excluding ${excludedUrls.length} failed candidate(s)` : ''), + ); } this.logger.debug( @@ -244,6 +258,20 @@ export class YoutubeService { return undefined; } + private getYoutubeVideoId(url: string): string | null { + try { + const parsed = new URL(url); + + if (parsed.hostname === 'youtu.be') { + return parsed.pathname.replace(/^\//, '') || null; + } + + return parsed.searchParams.get('v'); + } catch { + return null; + } + } + assertValidYoutubeUrl(url: string): void { let parsed: URL; @@ -262,25 +290,93 @@ export class YoutubeService { } } - private getCookiesOptions(): { - cookiesFromBrowser?: string; - cookies?: string; - } { - const cookiesBrowser = this.configService.get( - EnvironmentEnum.YT_COOKIES, - ); - if (cookiesBrowser) { - this.logger.debug(`Using cookies from browser: ${cookiesBrowser}`); - return { cookiesFromBrowser: cookiesBrowser }; - } + private getCookiesFile(): string | null { const cookiesFile = this.configService.get( EnvironmentEnum.YT_COOKIES_FILE, ); + if (cookiesFile && fs.existsSync(cookiesFile)) { this.logger.debug(`Using cookies file: ${cookiesFile}`); - return { cookies: cookiesFile }; + return cookiesFile; } - return {}; + + return null; + } + + private classifyYtDlpError(output: string): string { + const text = output || ''; + + if (text.includes('Sign in to confirm your age')) { + return 'YouTube age-gated: cookies required or invalid'; + } + + if (text.includes('Only images are available')) { + return 'YouTube unavailable: no downloadable audio/video formats'; + } + + if (text.includes('Requested format is not available')) { + return 'YouTube unavailable: requested audio format is not available'; + } + + if (text.includes('Private video')) { + return 'YouTube unavailable: private video'; + } + + if (text.includes('Video unavailable')) { + return 'YouTube unavailable'; + } + + if (text.includes('Unable to download webpage')) { + return 'YouTube unavailable: unable to download webpage'; + } + + return text.trim().slice(0, 1200) || 'Unknown yt-dlp error'; + } + + private prepareWritableCookiesFile(cookiesFile: string | null): string | null { + if (!cookiesFile) { + return null; + } + + const tempCookiesFile = `/tmp/jarri-spooty-youtube-${process.pid}.cookies.txt`; + fs.copyFileSync(cookiesFile, tempCookiesFile); + return tempCookiesFile; + } + + private runYtDlp(args: string[]): Promise { + return new Promise((resolvePromise, rejectPromise) => { + const child = spawn('yt-dlp', args, { + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + + child.on('error', (error) => { + rejectPromise(new Error(`Failed to start yt-dlp: ${error.message}`)); + }); + + child.on('close', (code) => { + if (code === 0) { + resolvePromise(); + return; + } + + rejectPromise( + new Error( + `yt-dlp exited with code ${code}: ${this.classifyYtDlpError(stderr || stdout)}`, + ), + ); + }); + }); } async downloadAndFormat(track: TrackEntity, output: string): Promise { @@ -294,18 +390,32 @@ export class YoutubeService { this.assertValidYoutubeUrl(track.youtubeUrl); - const ytdlp = new YtDlp(); - await ytdlp.downloadAudio( - track.youtubeUrl, - this.configService.get<'m4a'>(EnvironmentEnum.FORMAT), - { - output, - ...this.getCookiesOptions(), - headers: HEADERS, - jsRuntime: 'node', - audioQuality: this.configService.get('QUALITY'), - }, - ); + const format = this.configService.get(EnvironmentEnum.FORMAT) || 'mp3'; + const quality = this.configService.get('QUALITY') || '0'; + const cookiesFile = this.prepareWritableCookiesFile(this.getCookiesFile()); + + const args = [ + '--no-playlist', + '--no-cache-dir', + '--no-cookies-from-browser', + '--extract-audio', + '--audio-format', + format, + '--audio-quality', + quality, + '--add-header', + `User-Agent:${HEADERS['User-Agent']}`, + '-o', + output, + ]; + + if (cookiesFile) { + args.push('--cookies', cookiesFile); + } + + args.push(track.youtubeUrl); + + await this.runYtDlp(args); this.logger.debug( `Downloaded ${track.artist} - ${track.name} to ${output}`, diff --git a/src/backend/src/track/track.entity.ts b/src/backend/src/track/track.entity.ts index ab17da9..62bb437 100644 --- a/src/backend/src/track/track.entity.ts +++ b/src/backend/src/track/track.entity.ts @@ -27,6 +27,12 @@ export class TrackEntity { @Column({ nullable: true }) youtubeUrl?: string; + @Column({ nullable: true, type: 'text' }) + rejectedYoutubeUrls?: string; + + @Column({ default: 0 }) + downloadAttemptCount?: number; + @Column({ default: TrackStatusEnum.New }) status?: TrackStatusEnum; diff --git a/src/backend/src/track/track.service.ts b/src/backend/src/track/track.service.ts index cb63820..45cbe8f 100644 --- a/src/backend/src/track/track.service.ts +++ b/src/backend/src/track/track.service.ts @@ -70,6 +70,23 @@ export class TrackService { this.io.emit(WsTrackOperation.Update, track); } + private parseRejectedYoutubeUrls(track: TrackEntity): string[] { + if (!track.rejectedYoutubeUrls) { + return []; + } + + try { + const parsed = JSON.parse(track.rejectedYoutubeUrls); + return Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : []; + } catch { + return []; + } + } + + private stringifyRejectedYoutubeUrls(urls: string[]): string { + return JSON.stringify([...new Set(urls)].slice(0, 20)); + } + async retry(id: number): Promise { const track = await this.get(id); @@ -78,12 +95,22 @@ export class TrackService { return; } - await this.enqueueSearch(id); + const rejectedUrls = this.parseRejectedYoutubeUrls(track); + + if (track.youtubeUrl && !rejectedUrls.includes(track.youtubeUrl)) { + rejectedUrls.push(track.youtubeUrl); + } + await this.update(id, { ...track, + youtubeUrl: null, + rejectedYoutubeUrls: this.stringifyRejectedYoutubeUrls(rejectedUrls), + downloadAttemptCount: 0, error: null, status: TrackStatusEnum.New, }); + + await this.enqueueSearch(id); } async findOnYoutube(track: TrackEntity): Promise { @@ -102,9 +129,13 @@ export class TrackService { }); let updatedTrack: TrackEntity; try { + const rejectedUrls = this.parseRejectedYoutubeUrls(track); + const youtubeMatch = await this.youtubeService.findBestYoutubeMatch( track.artist, track.name, + track.durationMs, + rejectedUrls, ); updatedTrack = { ...track, @@ -141,8 +172,8 @@ export class TrackService { const existingJob = await this.trackSearchQueue.getJob(jobId); if (existingJob) { - this.logger.warn(`Search job already exists for track ${id}`); - return; + this.logger.warn(`Removing existing search job for track ${id} before enqueue`); + await existingJob.remove(); } await this.trackSearchQueue.add('search-track', { id }, { jobId }); @@ -160,13 +191,23 @@ export class TrackService { const existingJob = await this.trackDownloadQueue.getJob(jobId); if (existingJob) { - this.logger.warn(`Download job already exists for track ${id}`); - return; + this.logger.warn(`Removing existing download job for track ${id} before enqueue`); + await existingJob.remove(); } await this.trackDownloadQueue.add('download-track', { id }, { jobId }); } + private getMaxDownloadAttempts(): number { + const parsed = Number(process.env.YT_DOWNLOAD_FALLBACK_ATTEMPTS || 3); + + if (!Number.isFinite(parsed) || parsed < 1) { + return 3; + } + + return Math.max(1, Math.min(10, Math.floor(parsed))); + } + async downloadFromYoutube(track: TrackEntity): Promise { const dbTrack = await this.get(track.id); if (!dbTrack) { @@ -217,10 +258,39 @@ export class TrackService { error = toSafeErrorMessage(err); } + const rejectedUrls = this.parseRejectedYoutubeUrls(track); + const nextAttemptCount = (track.downloadAttemptCount || 0) + 1; + const maxAttempts = this.getMaxDownloadAttempts(); + + if (error && track.youtubeUrl && !rejectedUrls.includes(track.youtubeUrl)) { + rejectedUrls.push(track.youtubeUrl); + } + + if (error && nextAttemptCount < maxAttempts) { + this.logger.warn( + `Download failed for track ${track.id}; rejecting current YouTube candidate and retrying search ` + + `(${nextAttemptCount}/${maxAttempts})`, + ); + + await this.update(track.id, { + ...track, + youtubeUrl: null, + rejectedYoutubeUrls: this.stringifyRejectedYoutubeUrls(rejectedUrls), + downloadAttemptCount: nextAttemptCount, + error: null, + status: TrackStatusEnum.New, + }); + + await this.enqueueSearch(track.id); + return; + } + const updatedTrack = { ...track, status: error ? TrackStatusEnum.Error : TrackStatusEnum.Completed, - ...(error ? { error } : {}), + rejectedYoutubeUrls: this.stringifyRejectedYoutubeUrls(rejectedUrls), + downloadAttemptCount: error ? nextAttemptCount : 0, + ...(error ? { error } : { error: null }), }; await this.update(track.id, updatedTrack); } diff --git a/src/frontend/src/app/app.component.html b/src/frontend/src/app/app.component.html index 3dc769c..2e77ef7 100644 --- a/src/frontend/src/app/app.component.html +++ b/src/frontend/src/app/app.component.html @@ -1,80 +1,94 @@ -
-
-

+

+
+

Jarri subsystem · deterministic media ingestion

+

- Spooty - v{{version}} -

-

Self-hosted spotify downloader

+ Jarri Spooty + v{{version}} +

+

Spotify metadata → YouTube candidate scoring → Local archive

+
+ +
-
-
-
-

Download

-
-
- - -
-

Please enter a valid Spotify URL (e.g. https://open.spotify.com/...)

-
+
+
+
+ Spotify + {{ spotifyConnected ? 'Connected' : 'Disconnected' }}
-
-
-
-

Playlists

-
- - -
+
+ Matching + Duration-aware
- - -

No playlists

-
+
+ Queue + Paced
-
-
-
-

Songs

-
- - -
-
- - -

No songs

-
+
+ Mode + Archive run
+ +
+

Acquire music

+
+ + +
+

+ Please enter a valid Spotify URL. +

+
+ +
+
+
+

Playlist History

+

One collapsed row per playlist run.

+
+
+ + +
+
+ + + +

No playlists

+
+
+ +
+

Single Songs

+ + +

No songs

+
+
- - diff --git a/src/frontend/src/app/app.component.scss b/src/frontend/src/app/app.component.scss index e69de29..0ab9ed7 100644 --- a/src/frontend/src/app/app.component.scss +++ b/src/frontend/src/app/app.component.scss @@ -0,0 +1,129 @@ +:host { + display: block; + min-height: 100vh; + background: + radial-gradient(circle at top left, rgba(29, 185, 84, 0.18), transparent 30rem), + linear-gradient(135deg, #06110c 0%, #0a1210 45%, #020403 100%); + color: #eafff0; +} + +.jarri-hero { + display: flex; + justify-content: space-between; + gap: 24px; + align-items: center; + padding: 34px 44px; + background: + linear-gradient(90deg, rgba(3, 12, 7, 0.95), rgba(29, 185, 84, 0.92)); + border-bottom: 1px solid rgba(126, 255, 180, 0.25); +} + +.eyebrow { + margin: 0 0 6px; + color: #9fffc3; + text-transform: uppercase; + letter-spacing: 0.18em; + font-size: 0.72rem; + font-weight: 800; +} + +h1 { + margin: 0; + color: #f3fff7; + font-size: clamp(2.2rem, 4vw, 4.6rem); + font-weight: 900; + letter-spacing: -0.06em; + text-shadow: 0 2px 24px rgba(0, 0, 0, 0.45); +} + +.hero-subtitle { + margin-top: 8px; + color: #d8ffe5; + font-size: 1.05rem; +} + +.version-tag { + background: #07110c; + color: #9fffc3; + vertical-align: middle; +} + +.auth-button { + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.28); + background: rgba(255, 255, 255, 0.92); + color: #07110c; + font-weight: 800; +} + +.auth-button.connected { + background: #9fffc3; +} + +.jarri-body { + padding: 34px 44px; +} + +.intelligence-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 22px; +} + +.intel-card { + border: 1px solid rgba(126, 255, 180, 0.18); + border-radius: 16px; + padding: 14px 16px; + background: rgba(255, 255, 255, 0.06); + box-shadow: 0 16px 50px rgba(0, 0, 0, 0.22); +} + +.intel-label { + display: block; + color: #8ea99a; + text-transform: uppercase; + letter-spacing: 0.13em; + font-size: 0.68rem; + font-weight: 800; +} + +.intel-card strong { + display: block; + margin-top: 4px; + color: #f1fff5; + font-size: 1.1rem; +} + +.intel-card strong.online { + color: #9fffc3; +} + +.jarri-box { + border-radius: 18px; + margin-bottom: 20px; + box-shadow: 0 18px 60px rgba(0, 0, 0, 0.24); +} + +.button { + border-radius: 10px; +} + +@media (max-width: 900px) { + .jarri-hero, + .jarri-body { + padding: 18px; + } + + .jarri-hero { + display: block; + } + + .hero-right { + margin-top: 14px; + } + + .intelligence-grid { + grid-template-columns: 1fr 1fr; + } +} diff --git a/src/frontend/src/app/components/playlist-box/playlist-box.component.html b/src/frontend/src/app/components/playlist-box/playlist-box.component.html index 6b8e2b8..79db7ac 100644 --- a/src/frontend/src/app/components/playlist-box/playlist-box.component.html +++ b/src/frontend/src/app/components/playlist-box/playlist-box.component.html @@ -1,35 +1,65 @@ -
-

- -   - {{playlist.error}} - - {{playlist.name}} -   - - - - - +

+

+ +   + + {{ playlist.isTrack ? 'track' : 'playlist' }}  + + + {{playlist.error}} + + + + {{playlist.name}} + + + + + + {{trackCompletedCount$ | async}}/{{trackCount$ | async}} + + + + {{errors}} failed + + + + + + -   + (click)="toggleActive(playlist.id, playlist.active)"> + -   -   -   - {{trackCompletedCount$ | async}}/{{trackCount$ | async}}  + + + + + +

+ +
+
+
+ +

+ {{progressPercent$ | async}}% complete + subscribed +

+ diff --git a/src/frontend/src/app/components/playlist-box/playlist-box.component.scss b/src/frontend/src/app/components/playlist-box/playlist-box.component.scss index 9272c18..bfccf69 100644 --- a/src/frontend/src/app/components/playlist-box/playlist-box.component.scss +++ b/src/frontend/src/app/components/playlist-box/playlist-box.component.scss @@ -2,19 +2,73 @@ display: none; } -.panel-heading { - padding: 5px 10px; - border-radius: 10px; +.playlist-card { + margin: 8px 0; + border-radius: 14px; + overflow: hidden; + background: rgba(255, 255, 255, 0.96); +} + +.playlist-heading { + padding: 8px 12px; + border-radius: 0; + cursor: pointer; &:hover .hover-icon { display: inline; } - - &:hover .hover-icon-reversed { - display: none; - } } -.panel { - margin: 5px 0; +.playlist-title { + min-width: 0; + overflow-wrap: anywhere; +} + +.history-label { + border-radius: 999px; + background: rgba(0, 0, 0, 0.16); + padding: 2px 7px; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.playlist-actions { + display: inline-flex; + align-items: center; + gap: 9px; + white-space: nowrap; +} + +.progress-text { + font-weight: 700; +} + +.error-count { + border-radius: 999px; + padding: 2px 7px; + background: #ff3860; + color: white; + font-size: 0.72rem; + font-weight: 700; +} + +.progress-shell { + height: 7px; + background: rgba(0, 0, 0, 0.08); +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #1db954, #48f59b); + transition: width 180ms ease-out; +} + +.progress-caption { + display: flex; + justify-content: space-between; + margin: 0; + padding: 5px 12px 8px; + color: #667; + font-size: 0.78rem; } diff --git a/src/frontend/src/app/components/playlist-box/playlist-box.component.ts b/src/frontend/src/app/components/playlist-box/playlist-box.component.ts index 50a072c..d25673e 100644 --- a/src/frontend/src/app/components/playlist-box/playlist-box.component.ts +++ b/src/frontend/src/app/components/playlist-box/playlist-box.component.ts @@ -2,7 +2,7 @@ import {Component, Input} from '@angular/core'; import {AsyncPipe, CommonModule, NgIf} from "@angular/common"; import {TrackListComponent} from "../track-list/track-list.component"; import {PlaylistService, PlaylistStatusEnum, PlaylistUi} from "../../services/playlist.service"; -import {Observable, map} from "rxjs"; +import {Observable, combineLatest, map} from "rxjs"; import {Playlist} from "../../models/playlist"; const STATUS2CLASS = { @@ -14,15 +14,15 @@ const STATUS2CLASS = { } @Component({ - selector: 'app-playlist-box', - imports: [ - CommonModule, - AsyncPipe, - NgIf, - TrackListComponent - ], - templateUrl: './playlist-box.component.html', - styleUrl: './playlist-box.component.scss', + selector: 'app-playlist-box', + imports: [ + CommonModule, + AsyncPipe, + NgIf, + TrackListComponent + ], + templateUrl: './playlist-box.component.html', + styleUrl: './playlist-box.component.scss', standalone: true }) export class PlaylistBoxComponent { @@ -31,16 +31,27 @@ export class PlaylistBoxComponent { this._playlist = val; this.trackCount$ = this.service.getTrackCount(val.id); this.trackCompletedCount$ = this.service.getCompletedTrackCount(val.id); + this.trackErrorCount$ = this.service.getErrorTrackCount(val.id); + this.progressPercent$ = combineLatest([ + this.trackCompletedCount$, + this.trackCount$, + ]).pipe( + map(([completed, total]) => total ? Math.round((completed / total) * 100) : 0) + ); this.statusClass$ = this.service.getStatus$(val.id).pipe( map(status => STATUS2CLASS[status]) ); } + get playlist(): Playlist & PlaylistUi { return this._playlist; } + _playlist!: Playlist & PlaylistUi; trackCount$!: Observable; trackCompletedCount$!: Observable; + trackErrorCount$!: Observable; + progressPercent$!: Observable; statusClass$!: Observable; constructor(private readonly service: PlaylistService) { } From 5f4d7c9c4e3c68896189f94bc47f4bf5f3401ed7 Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Fri, 15 May 2026 21:07:01 +0200 Subject: [PATCH 17/30] Document deterministic yt-dlp hardening and Docker persistence --- CHANGELOG.md | 89 ++++++++++++++--------- README.md | 201 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 221 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cbd812..8097af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,37 +10,60 @@ This project follows a practical chronological changelog rather than autogenerat ## Added -### Spotify OAuth Integration +### Spotify - Added Spotify OAuth login flow - Added `/api/spotify/login` - Added `/api/spotify/callback` - Added `/api/spotify/status` -- Added persistent Spotify user access token storage +- Added persistent Spotify user token storage - Added authenticated Spotify playlist retrieval - Added OAuth frontend bootstrap flow +- Added support for playlists exceeding 300 tracks +- Added deterministic playlist pagination handling -### Large Playlist Support +### YouTube -- Fixed Spotify playlist truncation at 100 tracks -- Added full Spotify pagination support -- Verified support for playlists exceeding 300 tracks -- Improved playlist queue handling stability +- Added deterministic YouTube candidate scoring +- Added duration-aware YouTube matching +- Added failed-candidate rejection memory +- Added automatic retry/fallback handling +- Added explicit YouTube candidate exclusion support +- Added detailed YouTube candidate debug logging +- Added explicit yt-dlp CLI execution +- Added deterministic yt-dlp stderr capture +- Added age-gated video detection +- Added invalid-format detection +- Added Docker-compatible cookie handling +- Added temporary cookie-copy isolation +- Added retry-aware queue coordination ### Queue Hardening - Added configurable YouTube search pacing - Added configurable YouTube download pacing -- Added sequential search queue behavior +- Added sequential queue coordination +- Added retry collision prevention +- Added deterministic queue replacement handling +- Added explicit retry-state transitions - Reduced aggressive request bursts toward YouTube -- Improved queue logging visibility + +### Docker + +- Added persistent SQLite bind-mount recommendations +- Added persistent runtime config recommendations +- Added yt-dlp installation to runtime container +- Improved Docker runtime observability +- Improved hardened Docker startup flow +- Improved deployment documentation ### Security - Added token-based frontend access mode - Added external secret handling recommendations - Added isolated Spotify credential storage -- Improved Docker credential handling documentation +- Added safer cookie handling flow +- Improved Docker secret isolation documentation ### Frontend @@ -49,37 +72,33 @@ This project follows a practical chronological changelog rather than autogenerat - Improved playlist rendering behavior - Improved large playlist progress tracking -### Docker +## Improved -- Improved hardened Docker runtime flow -- Added explicit environment variable examples -- Added safer default pacing recommendations -- Improved deployment documentation +- Improved YouTube throttling resistance +- Improved age-gated video recovery behavior +- Improved operational observability +- Improved cover-art embedding resilience +- Improved queue stability during large playlist imports +- Improved deterministic runtime behavior +- Improved Docker persistence handling -## Unreleased +## Fixed -### Added -- Spotify OAuth login flow -- Persistent Spotify user token handling -- Large playlist pagination support (>100 tracks) -- Deterministic YouTube candidate scoring -- Duration-aware YouTube matching -- Detailed YouTube candidate debug logging -- Improved Docker deployment guidance - -### Improved -- YouTube pacing and throttling resistance -- Cover art embedding resilience -- Queue stability during large playlist imports - -### Fixed -- Spotify playlist truncation at 100 tracks -- Incorrect live/acoustic YouTube selections -- Playlist retrieval failures on large public playlists +- Fixed Spotify playlist truncation at 100 tracks +- Fixed incomplete playlist retrieval behavior +- Fixed repeated retry loops against failed YouTube videos +- Fixed queue deadlocks during retries +- Fixed retry-button queue collisions +- Fixed container database loss on restart +- Fixed yt-dlp wrapper opacity issues +- Fixed hidden yt-dlp stderr failures +- Fixed multiple age-gated fallback failure paths +- Fixed invalid YouTube candidate reuse +- Fixed several Docker persistence edge cases --- -# 2.4.2 +# 2.4.2 (Original Spooty Endpoint) ## Fixed diff --git a/README.md b/README.md index 92bb5bb..4379add 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,13 @@ This hardened branch focuses on: - Safer credential handling - More resilient cover-art embedding - Better queue stability +- Deterministic YouTube fallback handling +- Explicit yt-dlp CLI execution +- Automatic failed-candidate rejection +- Improved age-gated video handling +- Persistent SQLite state across container restarts +- Deterministic Docker config persistence +- Improved operational observability --- @@ -30,6 +37,9 @@ This hardened branch focuses on: - Large playlist pagination support - Download pacing controls - YouTube cookie support +- Automatic YouTube retry/fallback handling +- Failed YouTube candidate rejection memory +- Deterministic yt-dlp error classification --- @@ -80,7 +90,7 @@ Copy: Create a secure env file: -~~~bash +```bash sudo mkdir -p /etc/tokens sudo tee /etc/tokens/spotify.env > /dev/null <<'EOF' @@ -90,25 +100,44 @@ EOF sudo chown root:$USER /etc/tokens/spotify.env sudo chmod 640 /etc/tokens/spotify.env -~~~ +``` Never commit this file. --- +## 3. Export YouTube Cookies (Recommended) + +YouTube increasingly rate-limits or age-gates anonymous downloads. + +Export a Netscape-format `cookies.txt` from a logged-in browser session. + +Recommended storage: + +```bash +sudo cp cookies.txt /etc/tokens/youtube.cookies.txt +sudo chown root:$USER /etc/tokens/youtube.cookies.txt +sudo chmod 640 /etc/tokens/youtube.cookies.txt +``` + +--- + # Docker Run -~~~bash +```bash docker run --rm -p 3000:3000 \ --env-file /etc/tokens/spotify.env \ -e SPOTIFY_REDIRECT_URI='http://127.0.0.1:3000/api/spotify/callback' \ -e AUTH_ENABLED=true \ -e SPOOTY_AUTH_TOKEN=change_this_token \ -e YT_SEARCH_DELAY_MS=7000 \ - -e YT_DOWNLOADS_PER_MINUTE=3 \ + -e YT_DOWNLOADS_PER_MINUTE=6 \ + -e YT_COOKIES_FILE=/spooty/config/youtube.cookies.txt \ -v "$PWD/downloads:/spooty/backend/downloads" \ + -v "$PWD/spooty-config:/spooty/backend/config" \ + -v "/etc/tokens/youtube.cookies.txt:/spooty/config/youtube.cookies.txt:ro" \ spootyfy-hardened:local -~~~ +``` Open: @@ -126,7 +155,7 @@ Then: # Docker Compose -~~~yaml +```yaml services: spooty: image: spootyfy-hardened:local @@ -145,11 +174,15 @@ services: SPOOTY_AUTH_TOKEN: "change_this_token" YT_SEARCH_DELAY_MS: "7000" - YT_DOWNLOADS_PER_MINUTE: "3" + YT_DOWNLOADS_PER_MINUTE: "6" + + YT_COOKIES_FILE: "/spooty/config/youtube.cookies.txt" volumes: - ./downloads:/spooty/backend/downloads -~~~ + - ./spooty-config:/spooty/backend/config + - /etc/tokens/youtube.cookies.txt:/spooty/config/youtube.cookies.txt:ro +``` --- @@ -162,23 +195,25 @@ services: - ffmpeg - Python3 - Redis +- yt-dlp --- ## Build -~~~bash +```bash npm install npm run build -~~~ +docker build -t spootyfy-hardened:local . +``` --- ## Run -~~~bash +```bash npm run start -~~~ +``` --- @@ -196,8 +231,8 @@ npm run start | SPOTIFY_REDIRECT_URI | unset | OAuth callback URI | | AUTH_ENABLED | `false` | Enable frontend token auth | | SPOOTY_AUTH_TOKEN | unset | Frontend auth token | -| YT_SEARCH_DELAY_MS | `5000` | Delay between YouTube searches | -| YT_DOWNLOADS_PER_MINUTE | `3` | Download pacing | +| YT_SEARCH_DELAY_MS | `7000` | Delay between YouTube searches | +| YT_DOWNLOADS_PER_MINUTE | `6` | Download pacing | | YT_COOKIES | unset | Browser cookie extraction | | YT_COOKIES_FILE | unset | Netscape cookies.txt path | @@ -213,9 +248,9 @@ Two supported methods exist. ## Browser Cookies (Native Install Only) -~~~bash +```bash YT_COOKIES=firefox -~~~ +``` Supported: @@ -236,15 +271,63 @@ Export a Netscape-format `cookies.txt`. Mount into container: -~~~bash --v /path/to/cookies.txt:/spooty/config/cookies.txt -~~~ +```bash +-v /path/to/cookies.txt:/spooty/config/youtube.cookies.txt:ro +``` Then: -~~~bash --e YT_COOKIES_FILE=/spooty/config/cookies.txt -~~~ +```bash +-e YT_COOKIES_FILE=/spooty/config/youtube.cookies.txt +``` + +--- + +# Deterministic YouTube Fallback Handling + +The hardened branch now uses direct `yt-dlp` CLI execution rather than relying entirely on wrapper abstractions. + +This provides: + +- Explicit stderr visibility +- Better Docker compatibility +- Deterministic retry handling +- Automatic failed-candidate rejection +- Better age-gated video handling +- Improved operational observability + +If a YouTube candidate fails: + +1. The failed URL is recorded +2. The candidate is rejected +3. A new YouTube search is performed +4. The next-best valid candidate is attempted automatically + +This prevents infinite retry loops against dead or restricted videos. + +--- + +# Persistent Docker State + +The hardened Docker flow strongly recommends persistent bind mounts for: + +- downloads +- SQLite database +- runtime config + +Recommended: + +```bash +-v "$PWD/downloads:/spooty/backend/downloads" \ +-v "$PWD/spooty-config:/spooty/backend/config" +``` + +This prevents: + +- database resets on container recreation +- queue state loss +- retry-state loss +- playlist metadata loss --- @@ -272,10 +355,45 @@ Aggressive YouTube access can trigger: Recommended safe pacing: -~~~bash +```bash -e YT_SEARCH_DELAY_MS=7000 --e YT_DOWNLOADS_PER_MINUTE=3 -~~~ +-e YT_DOWNLOADS_PER_MINUTE=6 +``` + +The hardened branch also includes additional internal pacing and retry coordination to reduce: + +- repeated failed candidate loops +- aggressive retry bursts +- queue collisions +- YouTube anti-bot triggers + +--- + +# yt-dlp Notes + +Modern YouTube behavior changes frequently. + +The hardened branch now: + +- Uses explicit yt-dlp CLI execution +- Captures stderr deterministically +- Detects age-gated failures +- Detects missing downloadable formats +- Handles retry/fallback selection explicitly + +Some videos may still fail due to: + +- regional restrictions +- removed videos +- YouTube anti-bot measures +- invalid cookies +- unavailable formats + +Recommended: + +- fresh cookies.txt exports +- authenticated YouTube sessions +- moderate pacing settings --- @@ -288,17 +406,19 @@ Never commit: - cookies.txt - downloaded music - local databases +- spooty-config/ Recommended `.gitignore` additions: -~~~gitignore +```gitignore downloads/ config/ +spooty-config/ *.sqlite cookies.txt .env .env.local -~~~ +``` --- @@ -318,10 +438,19 @@ cookies.txt ### YouTube -- Added configurable search pacing -- Added configurable download pacing -- Reduced aggressive YouTube request bursts -- Improved queue throttling behavior +- Added deterministic YouTube candidate scoring +- Added duration-aware YouTube matching +- Added failed-candidate rejection memory +- Added automatic retry/fallback handling +- Added explicit YouTube candidate exclusion support +- Added detailed YouTube candidate debug logging +- Added explicit yt-dlp CLI execution +- Added deterministic yt-dlp stderr capture +- Added age-gated video detection +- Added invalid-format detection +- Added Docker-compatible cookie handling +- Added temporary cookie-copy isolation +- Added retry-aware queue coordination ### Backend @@ -330,6 +459,7 @@ cookies.txt - Improved Docker runtime stability - Improved logging around Spotify retrieval - Added auth bootstrap flow support +- Added deterministic retry-state handling ### Frontend @@ -351,17 +481,20 @@ cookies.txt Recommended local test run: -~~~bash +```bash docker run --rm -p 3000:3000 \ --env-file /etc/tokens/spotify.env \ -e SPOTIFY_REDIRECT_URI='http://127.0.0.1:3000/api/spotify/callback' \ -e AUTH_ENABLED=true \ -e SPOOTY_AUTH_TOKEN=test-token \ -e YT_SEARCH_DELAY_MS=7000 \ - -e YT_DOWNLOADS_PER_MINUTE=3 \ + -e YT_DOWNLOADS_PER_MINUTE=6 \ + -e YT_COOKIES_FILE=/spooty/config/youtube.cookies.txt \ -v "$PWD/downloads:/spooty/backend/downloads" \ + -v "$PWD/spooty-config:/spooty/backend/config" \ + -v "/etc/tokens/youtube.cookies.txt:/spooty/config/youtube.cookies.txt:ro" \ spootyfy-hardened:local -~~~ +``` --- From 9e83001b73a3458beecc46641fd0c26a494d7857 Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Fri, 15 May 2026 22:11:01 +0200 Subject: [PATCH 18/30] Update Angular toolchain patch versions --- package-lock.json | 2486 ++++++++++++++++++++++--------------- src/frontend/package.json | 22 +- 2 files changed, 1477 insertions(+), 1031 deletions(-) diff --git a/package-lock.json b/package-lock.json index d1f5e76..f665304 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,13 +44,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1902.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.19.tgz", - "integrity": "sha512-iexYDIYpGAeAU7T60bGcfrGwtq1bxpZixYxWuHYiaD1b5baQgNSfd1isGEOh37GgDNsf4In9i2LOLPm0wBdtgQ==", + "version": "0.1902.26", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.26.tgz", + "integrity": "sha512-YL0SE4B+zLkPJa7glWBqNJzZS2qMMq8GzuGACJvHnRETL3jad34q3ByIM+wOTXbAVbKef3Poa/sPPunjMLdZUA==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", + "@angular-devkit/core": "19.2.26", "rxjs": "7.8.1" }, "engines": { @@ -59,57 +58,16 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/architect/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { + "version": "19.2.26", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.26.tgz", + "integrity": "sha512-50HrZQ2S/t9bWDEg30qqkwHUCH+k7pmslPqHLMFe0q+oUNaL5Ru+fi/L84bDzqRP/0mkeIIY2rOuUWrw6ZpGvQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.1902.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.19.tgz", - "integrity": "sha512-x2tlGg5CsUveFzuRuqeHknSbGirSAoRynEh+KqPRGK0G3WpMViW/M8SuVurecasegfIrDWtYZ4FnVxKqNbKwXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/architect": "0.1902.19", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^5.0.2" - } - }, - "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@angular-devkit/core": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", - "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.17.1", + "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", + "picomatch": "4.0.4", "rxjs": "7.8.1", "source-map": "0.7.4" }, @@ -127,24 +85,82 @@ } } }, - "node_modules/@angular-devkit/core/node_modules/rxjs": { + "node_modules/@angular-devkit/architect/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular-devkit/architect/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1902.26", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.26.tgz", + "integrity": "sha512-xHVMOd/DU4Mew05W95t2j0VFRmOxdCTghBZD2ZQygX2PTyXZKSAZtQjKwKP3CSs6nSDKy0/eHwplf9wI++TvpQ==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1902.26", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@angular-devkit/schematics": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.19.tgz", - "integrity": "sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==", + "version": "19.2.26", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.26.tgz", + "integrity": "sha512-3PmlXbegeMsCNhK26jI068Rr8wArGGMWjvWd9IVHaFhXxkcc3xHjCiqYaz8uRj9Rz+Ra4ToC7qU/Rwgy72qJ9A==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", + "@angular-devkit/core": "19.2.26", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -364,45 +380,86 @@ "tslib": "^2.1.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.26", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.26.tgz", + "integrity": "sha512-50HrZQ2S/t9bWDEg30qqkwHUCH+k7pmslPqHLMFe0q+oUNaL5Ru+fi/L84bDzqRP/0mkeIIY2rOuUWrw6ZpGvQ==", + "dev": true, + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-devkit/schematics/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } }, - "node_modules/@angular/animations": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.18.tgz", - "integrity": "sha512-c76x1t+OiSstPsvJdHmV8Q4taF+8SxWKqiY750fOjpd01it4jJbU6YQqIroC6Xie7154zZIxOTHH2uTj+nm5qA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/common": "19.2.18", - "@angular/core": "19.2.18" - } - }, "node_modules/@angular/cli": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.19.tgz", - "integrity": "sha512-e9tAzFNOL4mMWfMnpC9Up83OCTOp2siIj8W41FCp8jfoEnw79AXDDLh3d70kOayiObchksTJVShslTogLUyhMw==", + "version": "19.2.26", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.26.tgz", + "integrity": "sha512-WOwwim0ETWRoQRklwGn8g2l9UmgcG+jDCe5w0QX+wcsFjVCHh5ZYZpXDQcHZEHqRmBB9GEGHiXzITs7mB60qBw==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1902.19", - "@angular-devkit/core": "19.2.19", - "@angular-devkit/schematics": "19.2.19", + "@angular-devkit/architect": "0.1902.26", + "@angular-devkit/core": "19.2.26", + "@angular-devkit/schematics": "19.2.26", "@inquirer/prompts": "7.3.2", "@listr2/prompt-adapter-inquirer": "2.0.18", - "@schematics/angular": "19.2.19", + "@schematics/angular": "19.2.26", "@yarnpkg/lockfile": "1.1.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", @@ -424,6 +481,74 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "19.2.26", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.26.tgz", + "integrity": "sha512-50HrZQ2S/t9bWDEg30qqkwHUCH+k7pmslPqHLMFe0q+oUNaL5Ru+fi/L84bDzqRP/0mkeIIY2rOuUWrw6ZpGvQ==", + "dev": true, + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/cli/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/cli/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@angular/cli/node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -442,6 +567,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.18.tgz", "integrity": "sha512-CrV02Omzw/QtfjlEVXVPJVXipdx83NuA+qSASZYrxrhKFusUZyK3P/Zznqg+wiAeNDbedQwMUVqoAARHf0xQrw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -454,10 +580,9 @@ } }, "node_modules/@angular/compiler": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.18.tgz", - "integrity": "sha512-3MscvODxRVxc3Cs0ZlHI5Pk5rEvE80otfvxZTMksOZuPlv1B+S8MjWfc3X3jk9SbyUEzODBEH55iCaBHD48V3g==", - "license": "MIT", + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.22.tgz", + "integrity": "sha512-NO3q39TMq88MRyBM9E/cSn4KFEItPrp79izX3nRkWlGQ0D9nJPdzm2cHtZoB9dCZ3N6EOazTgBK+2jKylQrUFQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -466,11 +591,10 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.18.tgz", - "integrity": "sha512-N4TMtLfImJIoMaRL6mx7885UBeQidywptHH6ACZj71Ar6++DBc1mMlcwuvbeJCd3r3y8MQ5nLv5PZSN/tHr13w==", + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.22.tgz", + "integrity": "sha512-zwl4gh5smX95AduH60RpeToKRpRtj2oVEJ3KsgCb6KUfLbe/PK3lCQaLVW4tp1BbiCvCC05lhYC7ARtMbFFHrA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -490,7 +614,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "19.2.18", + "@angular/compiler": "19.2.22", "typescript": ">=5.5 <5.9" } }, @@ -499,7 +623,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -529,15 +652,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -547,7 +668,6 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, - "license": "MIT", "dependencies": { "readdirp": "^4.0.1" }, @@ -562,15 +682,13 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@angular/compiler-cli/node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 14.18.0" }, @@ -584,6 +702,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.18.tgz", "integrity": "sha512-+QRrf0Igt8ccUWXHA+7doK5W6ODyhHdqVyblSlcQ8OciwkzIIGGEYNZom5OZyWMh+oI54lcSeyV2O3xaDepSrQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -595,82 +714,6 @@ "zone.js": "~0.15.0" } }, - "node_modules/@angular/forms": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.18.tgz", - "integrity": "sha512-pe40934jWhoS7DyGl7jyZdoj1gvBgur2t1zrJD+csEkTitYnW14+La2Pv6SW1pNX5nIzFsgsS9Nex1KcH5S6Tw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/common": "19.2.18", - "@angular/core": "19.2.18", - "@angular/platform-browser": "19.2.18", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/platform-browser": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.18.tgz", - "integrity": "sha512-eahtsHPyXTYLARs9YOlXhnXGgzw0wcyOcDkBvNWK/3lA0NHIgIHmQgXAmBo+cJ+g9skiEQTD2OmSrrwbFKWJkw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/animations": "19.2.18", - "@angular/common": "19.2.18", - "@angular/core": "19.2.18" - }, - "peerDependenciesMeta": { - "@angular/animations": { - "optional": true - } - } - }, - "node_modules/@angular/platform-browser-dynamic": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.18.tgz", - "integrity": "sha512-wqDtK2yVN5VDqVeOSOfqELdu40fyoIDknBGSxA27CEXzFVdMWJyIpuvUi+GMa+9eGjlS+1uVVBaRwxmnuvHj+A==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/common": "19.2.18", - "@angular/compiler": "19.2.18", - "@angular/core": "19.2.18", - "@angular/platform-browser": "19.2.18" - } - }, - "node_modules/@angular/router": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.18.tgz", - "integrity": "sha512-7cimxtPODSwokFQ0TRYzX0ad8Yjrl0MJfzaDCJejd1n/q7RZ7KZmHd0DS/LkDNXVMEh4swr00fK+3YWG/Szsrg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/common": "19.2.18", - "@angular/core": "19.2.18", - "@angular/platform-browser": "19.2.18", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -759,7 +802,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.25.9" }, @@ -1065,7 +1107,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.24.7" }, @@ -2748,14 +2789,13 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "aix" @@ -2765,14 +2805,13 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -2782,14 +2821,13 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -2799,14 +2837,13 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -2816,14 +2853,13 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -2833,14 +2869,13 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -2850,14 +2885,13 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -2867,14 +2901,13 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -2884,14 +2917,13 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -2901,14 +2933,13 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -2918,14 +2949,13 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -2935,14 +2965,13 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -2952,14 +2981,13 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -2969,14 +2997,13 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -2986,14 +3013,13 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3003,14 +3029,13 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3020,14 +3045,13 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3037,14 +3061,13 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "netbsd" @@ -3054,14 +3077,13 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "netbsd" @@ -3071,14 +3093,13 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" @@ -3088,14 +3109,13 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" @@ -3104,15 +3124,30 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "sunos" @@ -3122,14 +3157,13 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -3139,14 +3173,13 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -3156,14 +3189,13 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -4345,7 +4377,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -4362,7 +4393,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -4379,7 +4409,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -4392,14 +4421,13 @@ } }, "node_modules/@jsonjoy.com/fs-core": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.10.tgz", - "integrity": "sha512-PyAEA/3cnHhsGcdY+AmIU+ZPqTuZkDhCXQ2wkXypdLitSpd6d5Ivxhnq4wa2ETRWFVJGabYynBWxIijOswSmOw==", + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz", + "integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", "thingies": "^2.5.0" }, "engines": { @@ -4414,15 +4442,14 @@ } }, "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.10.tgz", - "integrity": "sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q==", + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz", + "integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", "thingies": "^2.5.0" }, "engines": { @@ -4437,17 +4464,16 @@ } }, "node_modules/@jsonjoy.com/fs-node": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.10.tgz", - "integrity": "sha512-7R4Gv3tkUdW3dXfXiOkqxkElxKNVdd8BDOWC0/dbERd0pXpPY+s2s1Mino+aTvkGrFPiY+mmVxA7zhskm4Ue4Q==", + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz", + "integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "@jsonjoy.com/fs-print": "4.56.10", - "@jsonjoy.com/fs-snapshot": "4.56.10", + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, @@ -4463,11 +4489,10 @@ } }, "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.10.tgz", - "integrity": "sha512-uUnKz8R0YJyKq5jXpZtkGV9U0pJDt8hmYcLRrPjROheIfjMXsz82kXMgAA/qNg0wrZ1Kv+hrg7azqEZx6XZCVw==", + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz", + "integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -4480,15 +4505,14 @@ } }, "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.10.tgz", - "integrity": "sha512-oH+O6Y4lhn9NyG6aEoFwIBNKZeYy66toP5LJcDOMBgL99BKQMUf/zWJspdRhMdn/3hbzQsZ8EHHsuekbFLGUWw==", + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz", + "integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-fsa": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10" + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2" }, "engines": { "node": ">=10.0" @@ -4502,13 +4526,12 @@ } }, "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.10.tgz", - "integrity": "sha512-8EuPBgVI2aDPwFdaNQeNpHsyqPi3rr+85tMNG/lHvQLiVjzoZsvxA//Xd8aB567LUhy4QS03ptT+unkD/DIsNg==", + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz", + "integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.56.10" + "@jsonjoy.com/fs-node-builtins": "4.57.2" }, "engines": { "node": ">=10.0" @@ -4522,13 +4545,12 @@ } }, "node_modules/@jsonjoy.com/fs-print": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.10.tgz", - "integrity": "sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw==", + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz", + "integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.57.2", "tree-dump": "^1.1.0" }, "engines": { @@ -4543,14 +4565,13 @@ } }, "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.10.tgz", - "integrity": "sha512-DkR6l5fj7+qj0+fVKm/OOXMGfDFCGXLfyHkORH3DF8hxkpDgIHbhf/DwncBMs2igu/ST7OEkexn1gIqoU6Y+9g==", + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz", + "integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.57.2", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, @@ -4570,7 +4591,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -4587,7 +4607,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -4604,7 +4623,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/base64": "17.67.0", "@jsonjoy.com/buffers": "17.67.0", @@ -4631,7 +4649,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/util": "17.67.0" }, @@ -4651,7 +4668,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/buffers": "17.67.0", "@jsonjoy.com/codegen": "17.67.0" @@ -4672,7 +4688,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/base64": "^1.1.2", "@jsonjoy.com/buffers": "^1.2.0", @@ -4699,7 +4714,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -4716,7 +4730,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/codegen": "^1.0.0", "@jsonjoy.com/util": "^1.9.0" @@ -4737,7 +4750,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/buffers": "^1.0.0", "@jsonjoy.com/codegen": "^1.0.0" @@ -4758,7 +4770,6 @@ "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -4774,8 +4785,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@listr2/prompt-adapter-inquirer": { "version": "2.0.18", @@ -4837,7 +4847,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -4851,7 +4860,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -4865,7 +4873,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -4879,7 +4886,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -4893,7 +4899,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -4907,7 +4912,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -5005,7 +5009,6 @@ "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", "dev": true, - "license": "MIT", "optional": true, "engines": { "node": ">= 10" @@ -5042,7 +5045,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -5059,7 +5061,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -5076,7 +5077,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -5093,7 +5093,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -5110,7 +5109,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -5127,7 +5125,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -5144,7 +5141,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -5161,7 +5157,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -5178,7 +5173,6 @@ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -5195,7 +5189,6 @@ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -5212,7 +5205,6 @@ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -5229,7 +5221,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -5246,7 +5237,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -5263,7 +5253,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openharmony" @@ -5280,7 +5269,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -5297,7 +5285,6 @@ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -5314,7 +5301,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -6187,11 +6173,10 @@ } }, "node_modules/@ngtools/webpack": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.19.tgz", - "integrity": "sha512-R9aeTrOBiRVl8I698JWPniUAAEpSvzc8SUGWSM5UXWMcHnWqd92cOnJJ1aXDGJZKXrbhMhCBx9Dglmcks5IDpg==", + "version": "19.2.26", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.26.tgz", + "integrity": "sha512-N5jElC86vFXrTCTwxhrJK6rgvX8uVFtnt4sPrRiCatGz9iNzVcz0JfI1BEoGAB6U8GMICWTH9DHE0qmRnElKqw==", "dev": true, - "license": "MIT", "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", @@ -7104,20 +7089,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -7210,392 +7181,338 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", "cpu": [ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", "cpu": [ "loong64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", - "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", - "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openharmony" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@schematics/angular": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.19.tgz", - "integrity": "sha512-6/0pvbPCY4UHeB4lnM/5r250QX5gcLgOYbR5FdhFu+22mOPHfWpRc5tNuY9kCephDHzAHjo6fTW1vefOOmA4jw==", + "version": "19.2.26", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.26.tgz", + "integrity": "sha512-UF3Db1EaqX2/HjeyCBtUgY6PGXko87hfUG1yA1x4Zbybz0sTKBtn6YPOfwyxXgIXosAMkEA2jAhaU4EHLBpVlw==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", - "@angular-devkit/schematics": "19.2.19", + "@angular-devkit/core": "19.2.26", + "@angular-devkit/schematics": "19.2.26", "jsonc-parser": "3.3.1" }, "engines": { @@ -7604,6 +7521,74 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "19.2.26", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.26.tgz", + "integrity": "sha512-50HrZQ2S/t9bWDEg30qqkwHUCH+k7pmslPqHLMFe0q+oUNaL5Ru+fi/L84bDzqRP/0mkeIIY2rOuUWrw6ZpGvQ==", + "dev": true, + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@schematics/angular/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@schematics/angular/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -7914,7 +7899,6 @@ "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -7934,7 +7918,6 @@ "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, - "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" @@ -8133,7 +8116,6 @@ "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -8163,8 +8145,7 @@ "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/semver": { "version": "7.7.1", @@ -8188,7 +8169,6 @@ "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, - "license": "MIT", "dependencies": { "@types/express": "*" } @@ -8221,7 +8201,6 @@ "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -8267,7 +8246,6 @@ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -8505,7 +8483,6 @@ "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.21.3" }, @@ -8737,7 +8714,6 @@ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -8834,11 +8810,10 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8855,7 +8830,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -8938,7 +8912,6 @@ "engines": [ "node >= 0.8.0" ], - "license": "Apache-2.0", "bin": { "ansi-html": "bin/ansi-html" } @@ -9626,15 +9599,13 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/beasties": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.2.tgz", "integrity": "sha512-p4AF8uYzm9Fwu8m/hSVTCPXrRBPmB34hQpHsec2KOaR9CZmgoU8IOv4Cvwq4hgz2p4hLMNbsdNl5XeA6XbAQwA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "css-select": "^5.1.0", "css-what": "^6.1.0", @@ -9755,7 +9726,6 @@ "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" @@ -10888,7 +10858,6 @@ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, - "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -10901,7 +10870,6 @@ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, - "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", @@ -10920,7 +10888,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -10929,15 +10896,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/compression/node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -11021,7 +10986,6 @@ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.8" } @@ -11968,8 +11932,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/dezalgo": { "version": "1.0.4", @@ -12027,7 +11990,6 @@ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, - "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, @@ -12455,8 +12417,7 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -12487,12 +12448,11 @@ } }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -12500,39 +12460,39 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/esbuild-wasm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.4.tgz", - "integrity": "sha512-2HlCS6rNvKWaSKhWaG/YIyRsTsL3gUrMP2ToZMBIjw9LM7vVcIs+rz8kE2vExvTJgvM8OKPqNpcHawY/BQc/qQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.28.0.tgz", + "integrity": "sha512-5TRVKExcEmeMkccIZMzUq+Az6X2RoMAJyfl6SMMO1dMVhmvt0I2mx7gAb6zYi42n4d1ETcatFXazGKzA+aW7fg==", "dev": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -13205,7 +13165,6 @@ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dev": true, - "license": "Apache-2.0", "dependencies": { "websocket-driver": ">=0.5.1" }, @@ -14135,7 +14094,6 @@ "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -14323,8 +14281,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/handlebars": { "version": "4.7.8", @@ -14487,7 +14444,6 @@ "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dev": true, - "license": "MIT", "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", @@ -14499,15 +14455,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/hpack.js/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -14522,15 +14476,13 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/hpack.js/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -14608,8 +14560,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/http-errors": { "version": "2.0.1", @@ -14635,8 +14586,7 @@ "version": "0.5.10", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/http-proxy": { "version": "1.18.1", @@ -14729,7 +14679,6 @@ "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.18" } @@ -15284,11 +15233,10 @@ "optional": true }, "node_modules/is-network-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", - "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", "dev": true, - "license": "MIT", "engines": { "node": ">=16" }, @@ -16834,11 +16782,10 @@ } }, "node_modules/launch-editor": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", - "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", + "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", "dev": true, - "license": "MIT", "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.3" @@ -17119,7 +17066,6 @@ "integrity": "sha512-SuHqzPl7mYStna8WRotY8XX/EUZBjjv3QyKIByeCLFfC9uXT/OIHByEcA07PzbMfQAM0KYJtLgtpMRlIe5dErQ==", "dev": true, "hasInstallScript": true, - "license": "MIT", "optional": true, "dependencies": { "msgpackr": "^1.11.2", @@ -17145,7 +17091,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/loader-runner": { @@ -17581,7 +17526,6 @@ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } @@ -17918,8 +17862,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/minimatch": { "version": "9.0.3", @@ -18125,7 +18068,6 @@ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" } @@ -18202,7 +18144,6 @@ "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dev": true, - "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" @@ -18434,11 +18375,10 @@ } }, "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } @@ -18883,8 +18823,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/on-finished": { "version": "2.4.1", @@ -18903,7 +18842,6 @@ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -18998,7 +18936,6 @@ "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/os-name": { @@ -19078,7 +19015,6 @@ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/retry": "0.12.2", "is-network-error": "^1.0.0", @@ -19096,7 +19032,6 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } @@ -19328,7 +19263,6 @@ "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", "dev": true, - "license": "MIT", "dependencies": { "entities": "^4.3.0", "parse5": "^7.0.0", @@ -19368,7 +19302,6 @@ "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", "dev": true, - "license": "MIT", "dependencies": { "parse5": "^7.0.0" }, @@ -19478,11 +19411,10 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -19516,7 +19448,6 @@ "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==", "dev": true, - "license": "MIT", "optionalDependencies": { "@napi-rs/nice": "^1.0.1" } @@ -19611,9 +19542,9 @@ } }, "node_modules/postcss": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", - "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "dev": true, "funding": [ { @@ -19629,9 +19560,8 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -19685,8 +19615,7 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/postcss-modules-extract-imports": { "version": "3.1.0", @@ -19895,8 +19824,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/promise-inflight": { "version": "1.0.1", @@ -21230,11 +21158,10 @@ } }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "1.0.8" }, @@ -21246,31 +21173,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" } }, @@ -21532,15 +21459,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/selfsigned": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, - "license": "MIT", "dependencies": { "@types/node-forge": "^1.3.0", "node-forge": "^1" @@ -21627,7 +21552,6 @@ "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", "dev": true, - "license": "MIT", "dependencies": { "accepts": "~1.3.8", "batch": "0.6.1", @@ -21650,7 +21574,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -21660,7 +21583,6 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -21670,7 +21592,6 @@ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, - "license": "MIT", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.4", @@ -21686,15 +21607,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -21804,7 +21723,6 @@ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -22113,7 +22031,6 @@ "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", "dev": true, - "license": "MIT", "dependencies": { "faye-websocket": "^0.11.3", "uuid": "^8.3.2", @@ -22124,8 +22041,8 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "dev": true, - "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } @@ -22276,7 +22193,6 @@ "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.1.0", "handle-thing": "^2.0.0", @@ -22293,7 +22209,6 @@ "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.1.0", "detect-node": "^2.0.4", @@ -23408,11 +23323,10 @@ "license": "MIT" }, "node_modules/thingies": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", - "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", + "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.18" }, @@ -23435,8 +23349,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -23455,19 +23368,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -23584,7 +23484,6 @@ "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -24543,11 +24442,10 @@ } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -24617,33 +24515,461 @@ } } }, - "node_modules/vite/node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/void-elements": { @@ -24685,7 +25011,6 @@ "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "dev": true, - "license": "MIT", "dependencies": { "minimalistic-assert": "^1.0.0" } @@ -24705,7 +25030,6 @@ "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/web-streams-polyfill": { @@ -24729,7 +25053,6 @@ "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -24778,7 +25101,6 @@ "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, - "license": "MIT", "dependencies": { "colorette": "^2.0.10", "memfs": "^4.6.0", @@ -24808,7 +25130,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -24822,20 +25143,19 @@ } }, "node_modules/webpack-dev-middleware/node_modules/memfs": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.10.tgz", - "integrity": "sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w==", + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.2.tgz", + "integrity": "sha512-2nWzSsJzrukurSDna4Z0WywuScK4Id3tSKejgu74u8KCdW4uNrseKRSIDg75C6Yw5ZRqBe0F0EtMNlTbUq8bAQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-fsa": "4.56.10", - "@jsonjoy.com/fs-node": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-to-fsa": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "@jsonjoy.com/fs-print": "4.56.10", - "@jsonjoy.com/fs-snapshot": "4.56.10", + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-to-fsa": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", @@ -24856,7 +25176,6 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -24876,7 +25195,6 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "dev": true, - "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -24934,7 +25252,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -24952,7 +25269,6 @@ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, - "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -24973,11 +25289,10 @@ } }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 10" } @@ -24987,7 +25302,6 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -25000,7 +25314,6 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -25078,7 +25391,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -25097,7 +25409,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -25112,7 +25423,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -25122,8 +25432,7 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -25131,7 +25440,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -25151,7 +25459,6 @@ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", @@ -25166,7 +25473,6 @@ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=0.8.0" } @@ -25831,14 +26137,14 @@ "src/frontend": { "version": "2.4.2", "dependencies": { - "@angular/animations": "^19.0.6", - "@angular/common": "^19.0.6", - "@angular/compiler": "^19.0.6", - "@angular/core": "^19.0.6", - "@angular/forms": "^19.0.6", - "@angular/platform-browser": "^19.0.6", - "@angular/platform-browser-dynamic": "^19.0.6", - "@angular/router": "^19.0.6", + "@angular/animations": "^19.2.22", + "@angular/common": "^19.2.22", + "@angular/compiler": "^19.2.22", + "@angular/core": "^19.2.22", + "@angular/forms": "^19.2.22", + "@angular/platform-browser": "^19.2.22", + "@angular/platform-browser-dynamic": "^19.2.22", + "@angular/router": "^19.2.22", "@distube/ytdl-core": "^4.15.9", "@fortawesome/fontawesome-free": "^6.5.2", "@ngneat/elf": "^2.5.1", @@ -25856,9 +26162,9 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^19.0.7", - "@angular/cli": "^19.0.7", - "@angular/compiler-cli": "^19.0.6", + "@angular-devkit/build-angular": "^19.2.26", + "@angular/cli": "^19.2.26", + "@angular/compiler-cli": "^19.2.22", "@types/jasmine": "~5.1.0", "jasmine-core": "~5.1.0", "karma": "~6.4.0", @@ -25870,17 +26176,16 @@ } }, "src/frontend/node_modules/@angular-devkit/build-angular": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.19.tgz", - "integrity": "sha512-uIxi6Vzss6+ycljVhkyPUPWa20w8qxJL9lEn0h6+sX/fhM8Djt0FHIuTQjoX58EoMaQ/1jrXaRaGimkbaFcG9A==", + "version": "19.2.26", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.26.tgz", + "integrity": "sha512-7uyeZ6uDHAAvOM7v20XtF9ZTd6YNDdjadRRxnz0GgxoIfF3w92KI6vBVGZOr3UCCIjmYs9oaPtkCr72OseBQxw==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1902.19", - "@angular-devkit/build-webpack": "0.1902.19", - "@angular-devkit/core": "19.2.19", - "@angular/build": "19.2.19", + "@angular-devkit/architect": "0.1902.26", + "@angular-devkit/build-webpack": "0.1902.26", + "@angular-devkit/core": "19.2.26", + "@angular/build": "19.2.26", "@babel/core": "7.26.10", "@babel/generator": "7.26.10", "@babel/helper-annotate-as-pure": "7.25.9", @@ -25891,7 +26196,7 @@ "@babel/preset-env": "7.26.9", "@babel/runtime": "7.26.10", "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "19.2.19", + "@ngtools/webpack": "19.2.26", "@vitejs/plugin-basic-ssl": "1.2.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -25899,7 +26204,7 @@ "browserslist": "^4.21.5", "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", - "esbuild-wasm": "0.25.4", + "esbuild-wasm": "0.28.0", "fast-glob": "3.3.3", "http-proxy-middleware": "3.0.5", "istanbul-lib-instrument": "6.0.3", @@ -25912,9 +26217,9 @@ "mini-css-extract-plugin": "2.9.2", "open": "10.1.0", "ora": "5.4.1", - "picomatch": "4.0.2", + "picomatch": "4.0.4", "piscina": "4.8.0", - "postcss": "8.5.2", + "postcss": "8.5.12", "postcss-loader": "8.1.1", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", @@ -25926,7 +26231,7 @@ "terser": "5.39.0", "tree-kill": "1.2.2", "tslib": "2.8.1", - "webpack": "5.98.0", + "webpack": "5.105.0", "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.2.2", "webpack-merge": "6.0.1", @@ -25938,14 +26243,14 @@ "yarn": ">= 1.13.0" }, "optionalDependencies": { - "esbuild": "0.25.4" + "esbuild": "0.28.0" }, "peerDependencies": { "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", "@angular/localize": "^19.0.0 || ^19.2.0-next.0", "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", - "@angular/ssr": "^19.2.19", + "@angular/ssr": "^19.2.26", "@web/test-runner": "^0.20.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", @@ -25996,14 +26301,13 @@ } }, "src/frontend/node_modules/@angular-devkit/build-angular/node_modules/@angular/build": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.19.tgz", - "integrity": "sha512-SFzQ1bRkNFiOVu+aaz+9INmts7tDUrsHLEr9HmARXr9qk5UmR8prlw39p2u+Bvi6/lCiJ18TZMQQl9mGyr63lg==", + "version": "19.2.26", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.26.tgz", + "integrity": "sha512-pcV/m+hm5dD05d2xAOjr5dIdSJoSZDmM1OKbD551196OXiXb88F52hw0Rmn+TWk1+ksX3ivjgH0lSe7ww65mPA==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1902.19", + "@angular-devkit/architect": "0.1902.26", "@babel/core": "7.26.10", "@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-split-export-declaration": "7.24.7", @@ -26012,7 +26316,7 @@ "@vitejs/plugin-basic-ssl": "1.2.0", "beasties": "0.3.2", "browserslist": "^4.23.0", - "esbuild": "0.25.4", + "esbuild": "0.28.0", "fast-glob": "3.3.3", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", @@ -26020,13 +26324,13 @@ "magic-string": "0.30.17", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "4.0.2", + "picomatch": "4.0.4", "piscina": "4.8.0", - "rollup": "4.34.8", + "rollup": "4.59.0", "sass": "1.85.0", "semver": "7.7.1", "source-map-support": "0.5.21", - "vite": "6.4.1", + "vite": "6.4.2", "watchpack": "2.4.2" }, "engines": { @@ -26043,7 +26347,7 @@ "@angular/localize": "^19.0.0 || ^19.2.0-next.0", "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", - "@angular/ssr": "^19.2.19", + "@angular/ssr": "^19.2.26", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^19.0.0 || ^19.2.0-next.0", @@ -26091,12 +26395,164 @@ "tslib": "^2.1.0" } }, + "src/frontend/node_modules/@angular-devkit/core": { + "version": "19.2.26", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.26.tgz", + "integrity": "sha512-50HrZQ2S/t9bWDEg30qqkwHUCH+k7pmslPqHLMFe0q+oUNaL5Ru+fi/L84bDzqRP/0mkeIIY2rOuUWrw6ZpGvQ==", + "dev": true, + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "src/frontend/node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "src/frontend/node_modules/@angular/animations": { + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.22.tgz", + "integrity": "sha512-Sl921GvzVrZ+DRLB0neRQQfcciYi9p8bDdeTSevFO4VhvX0L1ij8EH/NJD4/UfKo6y1E5RZLOulst7zJ5VVSwg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.22", + "@angular/core": "19.2.22" + } + }, + "src/frontend/node_modules/@angular/common": { + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.22.tgz", + "integrity": "sha512-OGvJkK9xvlXF0LsM8T2NTgL6ehr+P4t493xztdIwiO7CzseDzHCBNZ8L1TwaVqqP3Xj0UhGTtWXp7YN4N9+u7Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.2.22", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "src/frontend/node_modules/@angular/core": { + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.22.tgz", + "integrity": "sha512-ImujBPIrV9eetYl2GaEBRw9CetklFtXB11oVaxOjflE2z6juTjugzXhszo/khMLj6UFfS+Anvp7Vv/Ot6DJL7A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0" + } + }, + "src/frontend/node_modules/@angular/forms": { + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.22.tgz", + "integrity": "sha512-NbBGY4JRbmt/na8fRFS1UvkJXac+xuzJbNsZW299S5/PBIHTgbpWuahO6SdErvuBvWXD87eln7+b4uMZeqJM7g==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.22", + "@angular/core": "19.2.22", + "@angular/platform-browser": "19.2.22", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "src/frontend/node_modules/@angular/platform-browser": { + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.22.tgz", + "integrity": "sha512-7OE80uM3DYDXa8D+1c3D231yzfqWz+2YaQElKK5MTPIDobSA9Dsu4YIFX80FRFxaopo3673RepZgxxFx3KzRzA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "19.2.22", + "@angular/common": "19.2.22", + "@angular/core": "19.2.22" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "src/frontend/node_modules/@angular/platform-browser-dynamic": { + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.22.tgz", + "integrity": "sha512-Ez0dbASephvR+ZNxIxvjTCM3PT5t/FHJw8X1DQ2kZiHdbqTO6eOicjk96uicXKDnkS7mBuUV7HP01qEa/XfPig==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.22", + "@angular/compiler": "19.2.22", + "@angular/core": "19.2.22", + "@angular/platform-browser": "19.2.22" + } + }, + "src/frontend/node_modules/@angular/router": { + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.22.tgz", + "integrity": "sha512-FNxNJ2WifZdcOzieJHHugNTZWgZArD35bnmpO3m7V9Bow43Hy42xAOpHvAGMw8wZgVFBQl6C+Wr6GZB3eQzX/w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.22", + "@angular/core": "19.2.22", + "@angular/platform-browser": "19.2.22", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "src/frontend/node_modules/@babel/core": { "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -26127,7 +26583,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -26154,7 +26609,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, @@ -26170,7 +26624,6 @@ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.6.tgz", "integrity": "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==", "dev": true, - "license": "MIT", "dependencies": { "@inquirer/core": "^10.1.7", "@inquirer/type": "^3.0.4" @@ -26188,266 +26641,345 @@ } }, "src/frontend/node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", - "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "src/frontend/node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", - "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "src/frontend/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", - "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "src/frontend/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", - "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "src/frontend/node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", - "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "src/frontend/node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", - "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "src/frontend/node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", - "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "src/frontend/node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", - "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "src/frontend/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", - "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "src/frontend/node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", - "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "src/frontend/node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "src/frontend/node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "src/frontend/node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "src/frontend/node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, "optional": true, "os": [ "linux" ] }, "src/frontend/node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", - "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "src/frontend/node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "src/frontend/node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", - "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "src/frontend/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", - "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "src/frontend/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", - "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, + "src/frontend/node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "src/frontend/node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, "src/frontend/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", - "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "src/frontend/node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", - "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "src/frontend/node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, "optional": true, "os": [ "win32" ] }, "src/frontend/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", - "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, - "src/frontend/node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "src/frontend/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, - "license": "MIT" - }, - "src/frontend/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "ajv": "^8.0.0" + "readdirp": "^4.0.1" }, - "peerDependencies": { - "ajv": "^8.0.0" + "engines": { + "node": ">= 14.16.0" }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "funding": { + "url": "https://paulmillr.com/funding/" } }, "src/frontend/node_modules/commander": { @@ -26457,52 +26989,28 @@ "dev": true, "license": "MIT" }, - "src/frontend/node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "src/frontend/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, - "license": "MIT" - }, - "src/frontend/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" }, - "engines": { - "node": ">=8.0.0" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "src/frontend/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "src/frontend/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "src/frontend/node_modules/rollup": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", - "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, - "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -26512,48 +27020,34 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.8", - "@rollup/rollup-android-arm64": "4.34.8", - "@rollup/rollup-darwin-arm64": "4.34.8", - "@rollup/rollup-darwin-x64": "4.34.8", - "@rollup/rollup-freebsd-arm64": "4.34.8", - "@rollup/rollup-freebsd-x64": "4.34.8", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", - "@rollup/rollup-linux-arm-musleabihf": "4.34.8", - "@rollup/rollup-linux-arm64-gnu": "4.34.8", - "@rollup/rollup-linux-arm64-musl": "4.34.8", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", - "@rollup/rollup-linux-riscv64-gnu": "4.34.8", - "@rollup/rollup-linux-s390x-gnu": "4.34.8", - "@rollup/rollup-linux-x64-gnu": "4.34.8", - "@rollup/rollup-linux-x64-musl": "4.34.8", - "@rollup/rollup-win32-arm64-msvc": "4.34.8", - "@rollup/rollup-win32-ia32-msvc": "4.34.8", - "@rollup/rollup-win32-x64-msvc": "4.34.8", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, - "src/frontend/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "src/frontend/node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -26605,7 +27099,6 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, - "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -26613,53 +27106,6 @@ "engines": { "node": ">=10.13.0" } - }, - "src/frontend/node_modules/webpack": { - "version": "5.98.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", - "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } } } } diff --git a/src/frontend/package.json b/src/frontend/package.json index c5a9433..0d86524 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -11,14 +11,14 @@ }, "private": true, "dependencies": { - "@angular/animations": "^19.0.6", - "@angular/common": "^19.0.6", - "@angular/compiler": "^19.0.6", - "@angular/core": "^19.0.6", - "@angular/forms": "^19.0.6", - "@angular/platform-browser": "^19.0.6", - "@angular/platform-browser-dynamic": "^19.0.6", - "@angular/router": "^19.0.6", + "@angular/animations": "^19.2.22", + "@angular/common": "^19.2.22", + "@angular/compiler": "^19.2.22", + "@angular/core": "^19.2.22", + "@angular/forms": "^19.2.22", + "@angular/platform-browser": "^19.2.22", + "@angular/platform-browser-dynamic": "^19.2.22", + "@angular/router": "^19.2.22", "@distube/ytdl-core": "^4.15.9", "@fortawesome/fontawesome-free": "^6.5.2", "@ngneat/elf": "^2.5.1", @@ -36,9 +36,9 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^19.0.7", - "@angular/cli": "^19.0.7", - "@angular/compiler-cli": "^19.0.6", + "@angular-devkit/build-angular": "^19.2.26", + "@angular/cli": "^19.2.26", + "@angular/compiler-cli": "^19.2.22", "@types/jasmine": "~5.1.0", "jasmine-core": "~5.1.0", "karma": "~6.4.0", From 005e47bf6eff7a56ee07ee03cc2bacb950d5fd30 Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Fri, 15 May 2026 23:17:51 +0200 Subject: [PATCH 19/30] Remove unused fluent-ffmpeg dependency --- package-lock.json | 965 +++++++++++++++------------------------ src/backend/package.json | 2 - 2 files changed, 370 insertions(+), 597 deletions(-) diff --git a/package-lock.json b/package-lock.json index f665304..4c3ccae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,6 +154,93 @@ "tslib": "^2.1.0" } }, + "node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/core/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, + "node_modules/@angular-devkit/core/node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@angular-devkit/schematics": { "version": "19.2.26", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.26.tgz", @@ -195,34 +282,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/core": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", - "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/schematics": { "version": "17.3.11", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", @@ -242,41 +301,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@angular-devkit/schematics-cli/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -347,19 +371,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/picomatch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@angular-devkit/schematics-cli/node_modules/run-async": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", @@ -563,11 +574,9 @@ } }, "node_modules/@angular/common": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.18.tgz", - "integrity": "sha512-CrV02Omzw/QtfjlEVXVPJVXipdx83NuA+qSASZYrxrhKFusUZyK3P/Zznqg+wiAeNDbedQwMUVqoAARHf0xQrw==", - "license": "MIT", - "peer": true, + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.22.tgz", + "integrity": "sha512-OGvJkK9xvlXF0LsM8T2NTgL6ehr+P4t493xztdIwiO7CzseDzHCBNZ8L1TwaVqqP3Xj0UhGTtWXp7YN4N9+u7Q==", "dependencies": { "tslib": "^2.3.0" }, @@ -575,7 +584,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.2.18", + "@angular/core": "19.2.22", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -698,11 +707,9 @@ } }, "node_modules/@angular/core": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.18.tgz", - "integrity": "sha512-+QRrf0Igt8ccUWXHA+7doK5W6ODyhHdqVyblSlcQ8OciwkzIIGGEYNZom5OZyWMh+oI54lcSeyV2O3xaDepSrQ==", - "license": "MIT", - "peer": true, + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.22.tgz", + "integrity": "sha512-ImujBPIrV9eetYl2GaEBRw9CetklFtXB11oVaxOjflE2z6juTjugzXhszo/khMLj6UFfS+Anvp7Vv/Ot6DJL7A==", "dependencies": { "tslib": "^2.3.0" }, @@ -1976,11 +1983,10 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", @@ -3258,11 +3264,10 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3275,11 +3280,10 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3293,11 +3297,10 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3348,22 +3351,20 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3751,33 +3752,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", - "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", - "license": "MIT" - }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -4933,6 +4910,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4946,6 +4924,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4959,6 +4938,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4972,6 +4952,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4985,6 +4966,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4998,6 +4980,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5383,34 +5366,6 @@ } } }, - "node_modules/@nestjs/cli/node_modules/@angular-devkit/core": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", - "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, "node_modules/@nestjs/cli/node_modules/@angular-devkit/schematics": { "version": "17.3.11", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", @@ -5430,41 +5385,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@nestjs/cli/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@nestjs/cli/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/@nestjs/cli/node_modules/cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", @@ -5593,13 +5513,12 @@ } }, "node_modules/@nestjs/cli/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5615,19 +5534,6 @@ "dev": true, "license": "ISC" }, - "node_modules/@nestjs/cli/node_modules/picomatch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@nestjs/cli/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -5853,41 +5759,6 @@ "typescript": ">=4.8.2" } }, - "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", - "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core/node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true, - "license": "MIT" - }, "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { "version": "17.3.11", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", @@ -5914,41 +5785,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@nestjs/schematics/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@nestjs/schematics/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/@nestjs/schematics/node_modules/magic-string": { "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", @@ -5962,19 +5798,6 @@ "node": ">=12" } }, - "node_modules/@nestjs/schematics/node_modules/picomatch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@nestjs/schematics/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -6443,13 +6266,12 @@ } }, "node_modules/@npmcli/package-json/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -7823,13 +7645,12 @@ } }, "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -7994,15 +7815,6 @@ "@types/send": "*" } }, - "node_modules/@types/fluent-ffmpeg": { - "version": "2.1.28", - "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.28.tgz", - "integrity": "sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -8965,11 +8777,10 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -9291,15 +9102,49 @@ } }, "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "dev": true, - "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "engines": { + "node": ">=10" } }, "node_modules/babel-jest": { @@ -9586,11 +9431,10 @@ } }, "node_modules/basic-ftp": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", - "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" } @@ -9891,10 +9735,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dependencies": { "balanced-match": "^1.0.0" } @@ -10000,43 +9843,27 @@ "license": "MIT" }, "node_modules/bullmq": { - "version": "5.67.3", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.67.3.tgz", - "integrity": "sha512-eeQobOJn8M0Rj8tcZCVFLrimZgJQallJH1JpclOoyut2nDNkDwTEPMVcZzLeSR2fGeIVbfJTjU96F563Qkge5A==", - "license": "MIT", + "version": "5.76.8", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.76.8.tgz", + "integrity": "sha512-v3WTwA8diFtsADaJ8eK2ozyi2CYK9rDZCeoKF+dIPF/MUL8HxAOa3H72Gmu1lC4yKlho6t1PwNr/QpDVqaNEZQ==", "dependencies": { "cron-parser": "4.9.0", - "ioredis": "5.9.2", - "msgpackr": "1.11.5", + "ioredis": "5.10.1", + "msgpackr": "2.0.1", "node-abort-controller": "3.1.1", - "semver": "7.7.3", - "tslib": "2.8.1", - "uuid": "11.1.0" - } - }, - "node_modules/bullmq/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "semver": "7.8.0", + "tslib": "2.8.1" }, "engines": { - "node": ">=10" + "node": ">=12.22.0" } }, - "node_modules/bullmq/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" + "node_modules/bullmq/node_modules/msgpackr": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.1.tgz", + "integrity": "sha512-9J+tqTEsbHqY8YohazYgty7LgerFIWxvMLpUjqETSmjHojtJm2WnX2kK/2a1fLI7CO7ERP1YSEUXMucz4j+yBA==", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" } }, "node_modules/bulma": { @@ -10145,13 +9972,12 @@ "license": "ISC" }, "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -10161,11 +9987,10 @@ } }, "node_modules/cacache/node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -12706,11 +12531,10 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -12723,11 +12547,10 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -12741,11 +12564,10 @@ "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -13005,10 +12827,9 @@ "license": "MIT" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==" }, "node_modules/extend": { "version": "3.0.2", @@ -13134,9 +12955,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -13147,8 +12968,7 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ], - "license": "BSD-3-Clause" + ] }, "node_modules/fastq": { "version": "1.20.1", @@ -13553,47 +13373,15 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/fluent-ffmpeg": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", - "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "dependencies": { - "async": "^0.2.9", - "which": "^1.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/fluent-ffmpeg/node_modules/async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" - }, - "node_modules/fluent-ffmpeg/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -13601,7 +13389,6 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -13673,11 +13460,10 @@ } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -13726,11 +13512,10 @@ } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -14113,22 +13898,20 @@ "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "devOptional": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "devOptional": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -14284,11 +14067,10 @@ "dev": true }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, - "license": "MIT", "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -14771,11 +14553,10 @@ } }, "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", - "dev": true, - "license": "MIT" + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true }, "node_modules/import-cwd": { "version": "3.0.0", @@ -14990,12 +14771,11 @@ } }, "node_modules/ioredis": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz", - "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==", - "license": "MIT", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", "dependencies": { - "@ioredis/commands": "1.5.0", + "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -15014,11 +14794,10 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "devOptional": true, - "license": "MIT", "engines": { "node": ">= 12" } @@ -16150,11 +15929,10 @@ } }, "node_modules/jest-util/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -16488,11 +16266,10 @@ } }, "node_modules/karma-coverage/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -16516,11 +16293,10 @@ } }, "node_modules/karma-coverage/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -16584,11 +16360,10 @@ } }, "node_modules/karma/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -16607,11 +16382,10 @@ } }, "node_modules/karma/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -17140,11 +16914,10 @@ "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "dev": true, - "license": "MIT" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true }, "node_modules/lodash.capitalize": { "version": "4.2.1", @@ -17710,11 +17483,10 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -18082,7 +17854,9 @@ "version": "1.11.5", "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "dev": true, "license": "MIT", + "optional": true, "optionalDependencies": { "msgpackr-extract": "^3.0.2" } @@ -18505,11 +18279,10 @@ } }, "node_modules/node-gyp/node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -19994,10 +19767,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dependencies": { "side-channel": "^1.1.0" }, @@ -20140,11 +19912,10 @@ } }, "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -20195,10 +19966,9 @@ } }, "node_modules/redstar/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -21096,6 +20866,27 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/rimraf/node_modules/glob": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", @@ -21125,16 +20916,15 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -21422,11 +21212,10 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -21475,10 +21264,9 @@ } }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "bin": { "semver": "bin/semver.js" }, @@ -21997,10 +21785,9 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", - "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", - "license": "MIT", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" @@ -23166,16 +22953,14 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -23189,12 +22974,39 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, "@swc/core": { "optional": true }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, "esbuild": { "optional": true }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, "uglify-js": { "optional": true } @@ -23292,22 +23104,20 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -23987,13 +23797,12 @@ } }, "node_modules/typeorm/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "peer": true, "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -24003,14 +23812,13 @@ } }, "node_modules/typeorm/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", "peer": true, "bin": { "uuid": "dist/esm/bin/uuid" @@ -24096,10 +23904,9 @@ } }, "node_modules/undici": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.21.0.tgz", - "integrity": "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==", - "license": "MIT", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "engines": { "node": ">=20.18.1" } @@ -26080,10 +25887,8 @@ "@nestjs/serve-static": "^4.0.2", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.10", - "@types/fluent-ffmpeg": "^2.1.24", "@types/yt-search": "^2.10.3", "bullmq": "^5.31.2", - "fluent-ffmpeg": "^2.1.3", "isomorphic-unfetch": "^4.0.2", "node-id3": "^0.2.9", "reflect-metadata": "^0.2.0", @@ -26446,36 +26251,6 @@ "@angular/core": "19.2.22" } }, - "src/frontend/node_modules/@angular/common": { - "version": "19.2.22", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.22.tgz", - "integrity": "sha512-OGvJkK9xvlXF0LsM8T2NTgL6ehr+P4t493xztdIwiO7CzseDzHCBNZ8L1TwaVqqP3Xj0UhGTtWXp7YN4N9+u7Q==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/core": "19.2.22", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "src/frontend/node_modules/@angular/core": { - "version": "19.2.22", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.22.tgz", - "integrity": "sha512-ImujBPIrV9eetYl2GaEBRw9CetklFtXB11oVaxOjflE2z6juTjugzXhszo/khMLj6UFfS+Anvp7Vv/Ot6DJL7A==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.15.0" - } - }, "src/frontend/node_modules/@angular/forms": { "version": "19.2.22", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.22.tgz", diff --git a/src/backend/package.json b/src/backend/package.json index e730fea..fa71ed0 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -31,10 +31,8 @@ "@nestjs/serve-static": "^4.0.2", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.10", - "@types/fluent-ffmpeg": "^2.1.24", "@types/yt-search": "^2.10.3", "bullmq": "^5.31.2", - "fluent-ffmpeg": "^2.1.3", "isomorphic-unfetch": "^4.0.2", "node-id3": "^0.2.9", "reflect-metadata": "^0.2.0", From f64cc9747575a1e56f9c4755049b92ee67bba19a Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Fri, 15 May 2026 23:36:57 +0200 Subject: [PATCH 20/30] Replace yt-search dependency with yt-dlp search --- package-lock.json | 319 ++-------------------- src/backend/package.json | 2 - src/backend/src/shared/youtube.service.ts | 76 +++++- 3 files changed, 97 insertions(+), 300 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c3ccae..a9947b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4325,30 +4325,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsep-plugin/assignment": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", - "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@jsep-plugin/regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", - "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, "node_modules/@jsonjoy.com/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", @@ -8079,12 +8055,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yt-search": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yt-search/-/yt-search-2.10.3.tgz", - "integrity": "sha512-kH4Yzs/edH9/s/Wj58WhV73qpiRedWeNy5V202BqpXPd+xUFM8M3ASLdUMYUbzzlUDoGorDm4JpeN6iNx3sOjw==", - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -8910,94 +8880,6 @@ "node": ">= 4" } }, - "node_modules/async.parallellimit": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/async.parallellimit/-/async.parallellimit-0.5.2.tgz", - "integrity": "sha512-4Di2nFsb3jL7aUIICvRSvtw/oynpMIx0JrwYn5hqJI661Dd+mYBi2ElOukOQgRHihU1SCTapb86Vx/Snva5M1w==", - "license": "MIT", - "dependencies": { - "async.util.eachoflimit": "0.5.2", - "async.util.parallel": "0.5.2" - } - }, - "node_modules/async.util.eachoflimit": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/async.util.eachoflimit/-/async.util.eachoflimit-0.5.2.tgz", - "integrity": "sha512-oZksH0sBW0AEOJKgBCQ79io9DZruoRBLTAea/Ik36pejR7pDpByvtXeuJsoZdPwSVslsrQcsUfucbUaiXYBnAQ==", - "license": "MIT", - "dependencies": { - "async.util.keyiterator": "0.5.2", - "async.util.noop": "0.5.2", - "async.util.once": "0.5.2", - "async.util.onlyonce": "0.5.2" - } - }, - "node_modules/async.util.isarray": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/async.util.isarray/-/async.util.isarray-0.5.2.tgz", - "integrity": "sha512-wbUzlrwON8RUgi+v/rhF0U99Ce8Osjcn+JP/mFNg6ymvShcobAOvE6cvLajSY5dPqKCOE1xfdhefgBif11zZgw==", - "license": "MIT" - }, - "node_modules/async.util.isarraylike": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/async.util.isarraylike/-/async.util.isarraylike-0.5.2.tgz", - "integrity": "sha512-DbFpsz3ZFNkohAW8IpGTlm8gotU32zpqe3Y2XkEA/G3XNO6rmUTKPpo7XgXUruoI+AsGi8+0zWpJHe7t1sLiAg==", - "license": "MIT", - "dependencies": { - "async.util.isarray": "0.5.2" - } - }, - "node_modules/async.util.keyiterator": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/async.util.keyiterator/-/async.util.keyiterator-0.5.2.tgz", - "integrity": "sha512-cktrETawCwgu13y3KZs2uMGFnNHc+IjKPZsavtRaoCjLELkePb2co4zrr+ghPvEqLXZIJPTKqC2HFZgJTssMVw==", - "license": "MIT", - "dependencies": { - "async.util.isarraylike": "0.5.2", - "async.util.keys": "0.5.2" - } - }, - "node_modules/async.util.keys": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/async.util.keys/-/async.util.keys-0.5.2.tgz", - "integrity": "sha512-umCOCRCRYwIC2Ho3fbuhKwIIe7OhQsVoVKGoF5GoQiGJUmjP4TG0Bmmcdpm7yW/znoIGKpnjKzVQz0niH4tfqw==", - "license": "MIT" - }, - "node_modules/async.util.noop": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/async.util.noop/-/async.util.noop-0.5.2.tgz", - "integrity": "sha512-AdwShXwE0KoskgqVJAck8zcM32nIHj3AC8ZN62ZaR5srhrY235Nw18vOJZWxcOfhxdVM0hRVKM8kMx7lcl7cCQ==", - "license": "MIT" - }, - "node_modules/async.util.once": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/async.util.once/-/async.util.once-0.5.2.tgz", - "integrity": "sha512-YQ5WPzDTt2jlblUDkq2I5RV/KiAJErJ4/0cEFhYPaZzqIuF/xDzdGvnEKe7UeuoMszsVPeajzcpKgkbwdb9MUA==", - "license": "MIT" - }, - "node_modules/async.util.onlyonce": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/async.util.onlyonce/-/async.util.onlyonce-0.5.2.tgz", - "integrity": "sha512-UgQvkU9JZ+I0Cm1f56XyGXcII+J3d/5XWUuHpcevlItuA3WFSJcqZrsyAUck2FkRSD8BwYQX1zUTDp3SJMVESg==", - "license": "MIT" - }, - "node_modules/async.util.parallel": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/async.util.parallel/-/async.util.parallel-0.5.2.tgz", - "integrity": "sha512-0bEvwmQ8fxsTYNwacw5iq0i3PvGryRkXxZ01Rvox21izdMdls9IH2rAZjfunbgI8j6nFRyIdCmMINQ9kka99ow==", - "license": "MIT", - "dependencies": { - "async.util.isarraylike": "0.5.2", - "async.util.noop": "0.5.2", - "async.util.restparam": "0.5.2" - } - }, - "node_modules/async.util.restparam": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/async.util.restparam/-/async.util.restparam-0.5.2.tgz", - "integrity": "sha512-Q9Z+zgmtMxFX5i7CnBvNOkgrL5hptztCqwarQluyNudUUk4iCmyjmsQl8MuQEjNh3gGqP5ayvDaextL1VXXgIg==", - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -9579,14 +9461,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, "license": "ISC" }, - "node_modules/boolstring": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/boolstring/-/boolstring-2.0.1.tgz", - "integrity": "sha512-tpNlEZsRdZzIP7KElpv0GRiRsnkh7+Ko+W2ohZt2DcH3/z/Gya1r+0dlSSNc5GWGSt0zG7c4JV02U7ZCk/1lWw==", - "license": "MIT" - }, "node_modules/boxen": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", @@ -10157,6 +10034,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", @@ -10182,6 +10060,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -10736,6 +10615,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -11340,6 +11220,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -11356,6 +11237,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -11483,12 +11365,6 @@ "node": ">=4" } }, - "node_modules/dasu": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/dasu/-/dasu-0.4.3.tgz", - "integrity": "sha512-AFwspl5k7V8MW8H7tyIGJ0gtOauUg7JC+DgiRFUIXvPNNDFXTMtvnCkZY0macN6JLGqBjNP38WVnQN7Iv3RSlg==", - "license": "MIT" - }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -11852,6 +11728,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -11866,6 +11743,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, "funding": [ { "type": "github", @@ -11878,6 +11756,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -11893,6 +11772,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -12038,6 +11918,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, "license": "MIT", "dependencies": { "iconv-lite": "^0.6.3", @@ -12051,6 +11932,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -12157,6 +12039,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -14280,6 +14163,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -14299,6 +14183,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -14440,12 +14325,6 @@ "node": ">=10.17.0" } }, - "node_modules/human-time": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/human-time/-/human-time-0.0.2.tgz", - "integrity": "sha512-sbYI90YhYmstslPTb70BLGjy6mdESa0lxL7uDR4fIVAx9Iobz8fLEqi7FqF4Q/6vblrzZALg//MsYJlIPBU8SA==", - "license": "MIT" - }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -16061,15 +15940,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsep": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", - "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -16167,24 +16037,6 @@ ], "license": "MIT" }, - "node_modules/jsonpath-plus": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", - "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", - "license": "MIT", - "dependencies": { - "@jsep-plugin/assignment": "^1.3.0", - "@jsep-plugin/regex": "^1.0.4", - "jsep": "^1.4.0" - }, - "bin": { - "jsonpath": "bin/jsonpath-cli.js", - "jsonpath-plus": "bin/jsonpath-cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", @@ -16490,12 +16342,6 @@ "node": ">=10" } }, - "node_modules/keypress": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.2.1.tgz", - "integrity": "sha512-HjorDJFNhnM4SicvaUXac0X77NiskggxJdesG72+O5zBKpSqKFCrqmndKVqpu3pFqkla0St6uGk8Ju0sCurrmg==", - "license": "MIT" - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -17532,6 +17378,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -18157,67 +18004,6 @@ "node": ">= 6.13.0" } }, - "node_modules/node-fzf": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/node-fzf/-/node-fzf-0.14.0.tgz", - "integrity": "sha512-VZU+nuj8zzEEtGzeYGGzKdSmj7W9oHm9xK2A1g1kEPmlyK3726T1iT1e9FCxWiaKKjEn3oqR+p3So7EjU94JaA==", - "license": "MIT", - "dependencies": { - "keypress": "~0.2.1", - "minimist": "~1.2.5", - "picocolors": "~1.1.1", - "redstar": "0.0.2", - "restore-cursor": "~3.1.0", - "string-width": "~2.1.1", - "ttys": "0.0.3" - }, - "bin": { - "nfzf": "bin/cli.js" - } - }, - "node_modules/node-fzf/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/node-fzf/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/node-fzf/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "license": "MIT", - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/node-fzf/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/node-gyp": { "version": "11.5.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", @@ -18554,6 +18340,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -18632,6 +18419,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -19023,6 +18811,7 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -19049,6 +18838,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, "license": "MIT", "dependencies": { "domhandler": "^5.0.3", @@ -19062,6 +18852,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, "license": "MIT", "dependencies": { "parse5": "^7.0.0" @@ -19086,6 +18877,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -19181,6 +18973,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -19956,36 +19749,6 @@ "node": ">=4" } }, - "node_modules/redstar": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/redstar/-/redstar-0.0.2.tgz", - "integrity": "sha512-VNvLaLxMJMYiAasJX5Q/GC+Os7FXL0yPWFDuTodhR7Na9wqzrXsePPWC+EtIv4t3q5DyAK00w423xi5mQN2fqg==", - "license": "MIT", - "dependencies": { - "minimatch": "~3.0.4" - } - }, - "node_modules/redstar/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/redstar/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -20803,6 +20566,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, "license": "MIT", "dependencies": { "onetime": "^5.1.0", @@ -20816,6 +20580,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, "license": "ISC" }, "node_modules/retry": { @@ -23506,14 +23271,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/ttys": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/ttys/-/ttys-0.0.3.tgz", - "integrity": "sha512-UCqXRZS2S7U4aVB7Salj3ChPRSsb57ogJpJ1eMCvyowxFOBGsaHKcRU8bovcDwajX1mRbv0IpUnYkoG7Ieo5Zg==", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/tuf-js": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.1.0.tgz", @@ -25289,6 +25046,7 @@ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -25301,6 +25059,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -25313,6 +25072,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -25833,27 +25593,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yt-search": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/yt-search/-/yt-search-2.13.1.tgz", - "integrity": "sha512-iUvUpUYyG1Gk5MxwdxUkSeugXR2DcWX+pEq3rJlsNHHNp4+Lka+fCwNYEdoN5ovxltAzAvx0K3/3MbX7FqDdAw==", - "license": "MIT", - "dependencies": { - "async.parallellimit": "~0.5.2", - "boolstring": "~2.0.1", - "cheerio": "^1.0.0-rc.10", - "dasu": "~0.4.3", - "human-time": "0.0.2", - "jsonpath-plus": "~10.3.0", - "minimist": "~1.2.5", - "node-fzf": "~0.14.0" - }, - "bin": { - "yt-search": "bin/cli.js", - "yt-search-audio": "bin/mpv_audio.sh", - "yt-search-video": "bin/mpv_video.sh" - } - }, "node_modules/ytdlp-nodejs": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/ytdlp-nodejs/-/ytdlp-nodejs-3.4.4.tgz", @@ -25887,7 +25626,6 @@ "@nestjs/serve-static": "^4.0.2", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.10", - "@types/yt-search": "^2.10.3", "bullmq": "^5.31.2", "isomorphic-unfetch": "^4.0.2", "node-id3": "^0.2.9", @@ -25895,7 +25633,6 @@ "rxjs": "7.8.0", "spotify-url-info": "^3.2.18", "sqlite3": "^5.1.7", - "yt-search": "^2.12.1", "ytdlp-nodejs": "^3.4.4" }, "devDependencies": { diff --git a/src/backend/package.json b/src/backend/package.json index fa71ed0..714ea0f 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -31,7 +31,6 @@ "@nestjs/serve-static": "^4.0.2", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.10", - "@types/yt-search": "^2.10.3", "bullmq": "^5.31.2", "isomorphic-unfetch": "^4.0.2", "node-id3": "^0.2.9", @@ -39,7 +38,6 @@ "rxjs": "7.8.0", "spotify-url-info": "^3.2.18", "sqlite3": "^5.1.7", - "yt-search": "^2.12.1", "ytdlp-nodejs": "^3.4.4" }, "devDependencies": { diff --git a/src/backend/src/shared/youtube.service.ts b/src/backend/src/shared/youtube.service.ts index 69d83c2..7b1cf18 100644 --- a/src/backend/src/shared/youtube.service.ts +++ b/src/backend/src/shared/youtube.service.ts @@ -3,7 +3,6 @@ import { TrackEntity } from '../track/track.entity'; import { EnvironmentEnum } from '../environmentEnum'; import { TrackService } from '../track/track.service'; import { ConfigService } from '@nestjs/config'; -import * as yts from 'yt-search'; import * as fs from 'fs'; import { spawn } from 'child_process'; const NodeID3 = require('node-id3'); @@ -63,6 +62,16 @@ interface CandidateScore extends YoutubeMatch { rejected: boolean; } +interface YtDlpSearchVideo { + url?: string; + webpage_url?: string; + id?: string; + title?: string; + uploader?: string; + channel?: string; + duration?: number; +} + @Injectable() export class YoutubeService { private readonly logger = new Logger(TrackService.name); @@ -78,14 +87,14 @@ export class YoutubeService { const query = `${artist} - ${name}`; this.logger.debug(`Searching ${query} on YT`); - const result = await yts(query); + const videos = await this.searchYoutubeVideos(query); const excludedVideoIds = new Set( excludedUrls .map((url) => this.getYoutubeVideoId(url)) .filter((id): id is string => !!id), ); - const candidates = (result.videos || []) + const candidates = videos .filter((video: any) => !!video?.url && !!video?.title) .filter((video: any) => { const videoId = this.getYoutubeVideoId(String(video.url)); @@ -130,6 +139,55 @@ export class YoutubeService { return accepted; } + private async searchYoutubeVideos(query: string): Promise { + const searchTarget = `ytsearch15:${query}`; + const args = [ + '--dump-single-json', + '--flat-playlist', + '--skip-download', + '--no-playlist', + '--no-cache-dir', + '--no-cookies-from-browser', + '--add-header', + `User-Agent:${HEADERS['User-Agent']}`, + searchTarget, + ]; + + const output = await this.runYtDlpForOutput(args); + let parsed: any; + + try { + parsed = JSON.parse(output); + } catch (error) { + throw new Error(`Failed to parse yt-dlp search output: ${(error as Error).message}`); + } + + const entries = Array.isArray(parsed?.entries) ? parsed.entries : []; + return entries + .filter((entry: YtDlpSearchVideo) => !!entry?.title && (!!entry?.url || !!entry?.webpage_url || !!entry?.id)) + .map((entry: YtDlpSearchVideo) => ({ + ...entry, + url: this.normalizeYtDlpVideoUrl(entry), + author: entry.uploader || entry.channel || '', + seconds: typeof entry.duration === 'number' ? entry.duration : undefined, + })); + } + + private normalizeYtDlpVideoUrl(video: YtDlpSearchVideo): string { + const rawUrl = String(video.webpage_url || video.url || ''); + + if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://')) { + return rawUrl; + } + + const id = String(video.id || rawUrl || '').trim(); + if (!id) { + return ''; + } + + return `https://www.youtube.com/watch?v=${id}`; + } + private scoreCandidate( video: any, artist: string, @@ -343,7 +401,7 @@ export class YoutubeService { return tempCookiesFile; } - private runYtDlp(args: string[]): Promise { + private runYtDlpForOutput(args: string[]): Promise { return new Promise((resolvePromise, rejectPromise) => { const child = spawn('yt-dlp', args, { stdio: ['ignore', 'pipe', 'pipe'], @@ -352,11 +410,11 @@ export class YoutubeService { let stdout = ''; let stderr = ''; - child.stdout.on('data', (chunk) => { + child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); }); - child.stderr.on('data', (chunk) => { + child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); }); @@ -366,7 +424,7 @@ export class YoutubeService { child.on('close', (code) => { if (code === 0) { - resolvePromise(); + resolvePromise(stdout); return; } @@ -379,6 +437,10 @@ export class YoutubeService { }); } + private async runYtDlp(args: string[]): Promise { + await this.runYtDlpForOutput(args); + } + async downloadAndFormat(track: TrackEntity, output: string): Promise { this.logger.debug( `Downloading ${track.artist} - ${track.name} (${track.youtubeUrl}) from YT`, From be11bb94d7cc594a96483d3adc5030e62d0ba43f Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Fri, 15 May 2026 23:45:59 +0200 Subject: [PATCH 21/30] Replace Redis shell exec with spawn --- src/backend/src/main.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/backend/src/main.ts b/src/backend/src/main.ts index 58cb61a..d360536 100644 --- a/src/backend/src/main.ts +++ b/src/backend/src/main.ts @@ -4,7 +4,7 @@ import { HttpExceptionFilter } from './shared/filters/http-exception.filter'; import { AppModule } from './app.module'; import * as fs from 'fs'; import { resolve } from 'path'; -import { exec } from 'child_process'; +import { spawn } from 'child_process'; import helmet from 'helmet'; import { EnvironmentEnum } from './environmentEnum'; @@ -27,7 +27,16 @@ async function bootstrap() { try { // Convenience mode for the single-container Docker image. // Hardened deployments should prefer an external Redis service. - exec(`redis-server --port ${process.env.REDIS_PORT || 6379}`); + const redisProcess = spawn( + 'redis-server', + ['--port', String(process.env.REDIS_PORT || 6379)], + { + stdio: 'ignore', + detached: true, + }, + ); + + redisProcess.unref(); } catch (e) { console.log('Unable to run redis server from app'); console.log(e); From 5dc428b14814ec66165187e2aeb575f25a4d7252 Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Fri, 15 May 2026 23:49:54 +0200 Subject: [PATCH 22/30] Use POST for retry state-changing endpoints --- src/backend/src/playlist/playlist.controller.ts | 2 +- src/backend/src/track/track.controller.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/backend/src/playlist/playlist.controller.ts b/src/backend/src/playlist/playlist.controller.ts index f270f51..a713196 100644 --- a/src/backend/src/playlist/playlist.controller.ts +++ b/src/backend/src/playlist/playlist.controller.ts @@ -40,7 +40,7 @@ export class PlaylistController { return this.service.remove(id); } - @Get('retry/:id') + @Post('retry/:id') retryFailedOfPlaylist( @Param('id', ParseIntPipe) id: number, ): Promise { diff --git a/src/backend/src/track/track.controller.ts b/src/backend/src/track/track.controller.ts index 4dd58cb..854cf17 100644 --- a/src/backend/src/track/track.controller.ts +++ b/src/backend/src/track/track.controller.ts @@ -2,6 +2,7 @@ import { Controller, Delete, Get, + Post, NotFoundException, Param, ParseIntPipe, @@ -66,7 +67,7 @@ export class TrackController { return this.service.remove(id); } - @Get('retry/:id') + @Post('retry/:id') retry( @Param('id', ParseIntPipe) id: number, ): Promise { From 5d20972c2d6849fca01d9203a1fe1c2bbf920099 Mon Sep 17 00:00:00 2001 From: Tor Wingalen Date: Fri, 15 May 2026 23:55:10 +0200 Subject: [PATCH 23/30] Update frontend retry calls to POST --- src/frontend/src/app/services/playlist.service.ts | 2 +- src/frontend/src/app/services/track.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/app/services/playlist.service.ts b/src/frontend/src/app/services/playlist.service.ts index d132e91..90863da 100644 --- a/src/frontend/src/app/services/playlist.service.ts +++ b/src/frontend/src/app/services/playlist.service.ts @@ -138,7 +138,7 @@ export class PlaylistService { } retryFailed(id: number): void { - this.http.get(`${ENDPOINT}/retry/${id}`).subscribe(); + this.http.post(`${ENDPOINT}/retry/${id}`, {}).subscribe(); } setActive(id: number, active: boolean): void { diff --git a/src/frontend/src/app/services/track.service.ts b/src/frontend/src/app/services/track.service.ts index ded0ae8..a48d590 100644 --- a/src/frontend/src/app/services/track.service.ts +++ b/src/frontend/src/app/services/track.service.ts @@ -57,7 +57,7 @@ export class TrackService { } retry(id: number): void { - this.http.get(`${ENDPOINT}/retry/${id}`).subscribe(); + this.http.post(`${ENDPOINT}/retry/${id}`, {}).subscribe(); } private initWsConnection(): void { From 0cd52949444a4917fe824bf7ad7f9701a866ea72 Mon Sep 17 00:00:00 2001 From: TorMatzAndren Date: Sat, 16 May 2026 11:04:38 +0200 Subject: [PATCH 24/30] Remove unused downloader dependencies --- package-lock.json | 119 ++------------------------------------ package.json | 3 +- src/backend/package.json | 3 +- src/frontend/package.json | 1 - 4 files changed, 8 insertions(+), 118 deletions(-) diff --git a/package-lock.json b/package-lock.json index a9947b2..23c8109 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2773,27 +2773,6 @@ "node": ">=14.17.0" } }, - "node_modules/@distube/ytdl-core": { - "version": "4.16.12", - "resolved": "https://registry.npmjs.org/@distube/ytdl-core/-/ytdl-core-4.16.12.tgz", - "integrity": "sha512-/NR8Jur1Q4E2oD+DJta7uwWu7SkqdEkhwERt7f4iune70zg7ZlLLTOHs1+jgg3uD2jQjpdk7RGC16FqstG4RsA==", - "license": "MIT", - "dependencies": { - "http-cookie-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "m3u8stream": "^0.8.6", - "miniget": "^4.2.3", - "sax": "^1.4.1", - "tough-cookie": "^5.1.2", - "undici": "^7.8.0" - }, - "engines": { - "node": ">=20.18.1" - }, - "funding": { - "url": "https://github.com/distubejs/ytdl-core?sponsor" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", @@ -8559,6 +8538,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -14199,30 +14179,6 @@ "devOptional": true, "license": "BSD-2-Clause" }, - "node_modules/http-cookie-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-7.0.3.tgz", - "integrity": "sha512-EeZo7CGhfqPW6R006rJa4QtZZUpBygDa2HZH3DJqsTzTjyRE6foDBVQIv/pjVsxHC8z2GIdbB1Hvn9SRorP3WQ==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.4" - }, - "engines": { - "node": ">=20.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/3846masa" - }, - "peerDependencies": { - "tough-cookie": "^4.0.0 || ^5.0.0 || ^6.0.0", - "undici": "^7.0.0" - }, - "peerDependenciesMeta": { - "undici": { - "optional": true - } - } - }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -14306,6 +14262,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -17114,19 +17071,6 @@ "node": ">=12" } }, - "node_modules/m3u8stream": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.6.tgz", - "integrity": "sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==", - "license": "MIT", - "dependencies": { - "miniget": "^4.2.2", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/macos-release": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.4.0.tgz", @@ -17468,15 +17412,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/miniget": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.3.tgz", - "integrity": "sha512-SjbDPDICJ1zT+ZvQwK0hUcRY4wxlhhNpHL9nJOB2MEAXRGagTljsO8MEDzQMTFf0Q8g4QNi8P9lEm/g7e+qgzA==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -20952,7 +20887,9 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, "license": "BlueOak-1.0.0", + "optional": true, "engines": { "node": ">=11.0.0" } @@ -22943,24 +22880,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "license": "MIT" - }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -23036,18 +22955,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -23664,6 +23571,7 @@ "version": "7.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, "engines": { "node": ">=20.18.1" } @@ -25593,19 +25501,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ytdlp-nodejs": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/ytdlp-nodejs/-/ytdlp-nodejs-3.4.4.tgz", - "integrity": "sha512-PR2nQOtyOzSuSQpWf6SHD97d6qlcfY2y6CVCeYnuDacYDc1YzNLXYiqAyIpFWk13UUrtQN+XD35h5DERli3SXQ==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "ytdlp": "dist/cli/index.js" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/zone.js": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", @@ -25632,8 +25527,7 @@ "reflect-metadata": "^0.2.0", "rxjs": "7.8.0", "spotify-url-info": "^3.2.18", - "sqlite3": "^5.1.7", - "ytdlp-nodejs": "^3.4.4" + "sqlite3": "^5.1.7" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -25687,7 +25581,6 @@ "@angular/platform-browser": "^19.2.22", "@angular/platform-browser-dynamic": "^19.2.22", "@angular/router": "^19.2.22", - "@distube/ytdl-core": "^4.15.9", "@fortawesome/fontawesome-free": "^6.5.2", "@ngneat/elf": "^2.5.1", "@ngneat/elf-cli-ng": "^1.0.0", diff --git a/package.json b/package.json index 4c2acab..64e518d 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "clean": "rimraf dist", "changelog": "auto-changelog -p", "release": "release-it", - "commit": "cz", - "check:lib": "npm-check-updates -w backend -f ytdlp-nodejs -u" + "commit": "cz" }, "devDependencies": { "@release-it/bumper": "^7.0.1", diff --git a/src/backend/package.json b/src/backend/package.json index 714ea0f..f96b77e 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -37,8 +37,7 @@ "reflect-metadata": "^0.2.0", "rxjs": "7.8.0", "spotify-url-info": "^3.2.18", - "sqlite3": "^5.1.7", - "ytdlp-nodejs": "^3.4.4" + "sqlite3": "^5.1.7" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/src/frontend/package.json b/src/frontend/package.json index 0d86524..2599fe2 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -19,7 +19,6 @@ "@angular/platform-browser": "^19.2.22", "@angular/platform-browser-dynamic": "^19.2.22", "@angular/router": "^19.2.22", - "@distube/ytdl-core": "^4.15.9", "@fortawesome/fontawesome-free": "^6.5.2", "@ngneat/elf": "^2.5.1", "@ngneat/elf-cli-ng": "^1.0.0", From 38ca4d6c8d6fbb96f0ab23a3efef6e17719daa38 Mon Sep 17 00:00:00 2001 From: TorMatzAndren Date: Sat, 16 May 2026 11:08:26 +0200 Subject: [PATCH 25/30] Harden download filename sanitization --- src/backend/src/shared/utils.service.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/backend/src/shared/utils.service.ts b/src/backend/src/shared/utils.service.ts index 0c3ba66..4b5ab41 100644 --- a/src/backend/src/shared/utils.service.ts +++ b/src/backend/src/shared/utils.service.ts @@ -22,7 +22,7 @@ export class UtilsService { } ensureInsideDownloadsRoot(candidatePath: string): string { - const root = this.getRootDownloadsPath(); + const root = resolve(this.getRootDownloadsPath()); const resolvedCandidate = resolve(candidatePath); const rel = relative(root, resolvedCandidate); @@ -34,6 +34,20 @@ export class UtilsService { } stripFileIllegalChars(text: string): string { - return text.replace(/[/\\?%*:|"<>]/g, '-').trim() || 'untitled'; + const sanitized = String(text || '') + .replace(/[\x00-\x1f\x80-\x9f]/g, '-') + .replace(/[/\\?%*:|"<>]/g, '-') + .replace(/\s+/g, ' ') + .replace(/[. ]+$/g, '') + .trim(); + + const fallback = sanitized || 'untitled'; + const reservedName = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i; + + if (reservedName.test(fallback) || fallback === '.' || fallback === '..') { + return `_${fallback}`; + } + + return fallback.slice(0, 180); } } From 09bbffa6e8a014ab052fb48b2f153f759c9e50ed Mon Sep 17 00:00:00 2001 From: TorMatzAndren Date: Sat, 16 May 2026 11:11:59 +0200 Subject: [PATCH 26/30] Harden cover art fetching --- src/backend/src/shared/youtube.service.ts | 39 +++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/backend/src/shared/youtube.service.ts b/src/backend/src/shared/youtube.service.ts index 7b1cf18..9fa8b95 100644 --- a/src/backend/src/shared/youtube.service.ts +++ b/src/backend/src/shared/youtube.service.ts @@ -494,12 +494,47 @@ export class YoutubeService { return; } - const res = await fetch(coverUrl); + let parsedCoverUrl: URL; + try { + parsedCoverUrl = new URL(coverUrl); + } catch { + throw new Error('Invalid cover art URL'); + } + + if (!['http:', 'https:'].includes(parsedCoverUrl.protocol)) { + throw new Error('Rejected non-HTTP cover art URL'); + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 15_000); + + let res: Response; + try { + res = await fetch(parsedCoverUrl.toString(), { signal: controller.signal }); + } finally { + clearTimeout(timeout); + } + if (!res.ok) { throw new Error(`Failed to fetch cover art: ${res.status}`); } + const contentType = res.headers.get('content-type')?.split(';')[0].toLowerCase() || ''; + if (!['image/jpeg', 'image/png', 'image/webp'].includes(contentType)) { + throw new Error(`Rejected unsupported cover art type: ${contentType || 'unknown'}`); + } + + const maxCoverBytes = 5 * 1024 * 1024; + const contentLength = Number(res.headers.get('content-length') || 0); + if (contentLength > maxCoverBytes) { + throw new Error(`Rejected oversized cover art: ${contentLength} bytes`); + } + const arrayBuf = await res.arrayBuffer(); + if (arrayBuf.byteLength > maxCoverBytes) { + throw new Error(`Rejected oversized cover art: ${arrayBuf.byteLength} bytes`); + } + const imageBuffer = Buffer.from(arrayBuf); NodeID3.write( @@ -507,7 +542,7 @@ export class YoutubeService { title, artist, APIC: { - mime: 'image/jpeg', + mime: contentType, type: { id: 3, name: 'front cover' }, description: 'cover', imageBuffer, From c3d2f39b823da63cfcceb979cc07c4bbc9aa6c35 Mon Sep 17 00:00:00 2001 From: TorMatzAndren Date: Sat, 16 May 2026 11:42:16 +0200 Subject: [PATCH 27/30] Harden yt-dlp process lifecycle --- src/backend/src/shared/youtube.service.ts | 43 ++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/backend/src/shared/youtube.service.ts b/src/backend/src/shared/youtube.service.ts index 9fa8b95..30b0ca3 100644 --- a/src/backend/src/shared/youtube.service.ts +++ b/src/backend/src/shared/youtube.service.ts @@ -407,6 +407,37 @@ export class YoutubeService { stdio: ['ignore', 'pipe', 'pipe'], }); + const timeoutMs = 10 * 60 * 1000; + let settled = false; + + const settleResolve = (value: string) => { + if (settled) { + return; + } + + settled = true; + clearTimeout(timeout); + resolvePromise(value); + }; + + const settleReject = (error: Error) => { + if (settled) { + return; + } + + settled = true; + clearTimeout(timeout); + rejectPromise(error); + }; + + const timeout = setTimeout(() => { + child.kill('SIGKILL'); + + settleReject( + new Error(`yt-dlp exceeded maximum runtime of ${timeoutMs}ms`), + ); + }, timeoutMs); + let stdout = ''; let stderr = ''; @@ -419,16 +450,20 @@ export class YoutubeService { }); child.on('error', (error) => { - rejectPromise(new Error(`Failed to start yt-dlp: ${error.message}`)); + settleReject(new Error(`Failed to start yt-dlp: ${error.message}`)); }); child.on('close', (code) => { - if (code === 0) { - resolvePromise(stdout); + if (settled) { return; } - rejectPromise( + if (code === 0) { + settleResolve(stdout); + return; + } + + settleReject( new Error( `yt-dlp exited with code ${code}: ${this.classifyYtDlpError(stderr || stdout)}`, ), From 1f3131d99abf153b99d97f5d2555d228324acf22 Mon Sep 17 00:00:00 2001 From: TorMatzAndren Date: Sat, 16 May 2026 12:45:02 +0200 Subject: [PATCH 28/30] Shape track websocket payloads --- src/backend/src/track/track.service.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/backend/src/track/track.service.ts b/src/backend/src/track/track.service.ts index 45cbe8f..809153f 100644 --- a/src/backend/src/track/track.service.ts +++ b/src/backend/src/track/track.service.ts @@ -56,18 +56,34 @@ export class TrackService { this.io.emit(WsTrackOperation.Delete, { id }); } + private toClientTrack(track: TrackEntity): Partial { + return { + id: track.id, + artist: track.artist, + name: track.name, + spotifyUrl: track.spotifyUrl, + youtubeUrl: track.youtubeUrl, + downloadAttemptCount: track.downloadAttemptCount, + status: track.status, + error: track.error, + coverUrl: track.coverUrl, + durationMs: track.durationMs, + createdAt: track.createdAt, + }; + } + async create(track: TrackEntity, playlist?: PlaylistEntity): Promise { const savedTrack = await this.repository.save({ ...track, playlist }); await this.enqueueSearch(savedTrack.id); this.io.emit(WsTrackOperation.New, { - track: savedTrack, + track: this.toClientTrack(savedTrack), playlistId: playlist.id, }); } async update(id: number, track: TrackEntity): Promise { await this.repository.update(id, track); - this.io.emit(WsTrackOperation.Update, track); + this.io.emit(WsTrackOperation.Update, this.toClientTrack(track)); } private parseRejectedYoutubeUrls(track: TrackEntity): string[] { From d83b0e6e1e2ad77927e3654405110230a97066ea Mon Sep 17 00:00:00 2001 From: TorMatzAndren Date: Sat, 16 May 2026 13:01:28 +0200 Subject: [PATCH 29/30] Normalize Jarri Spooty naming --- CHANGELOG.md | 2 +- README.md | 4 ++-- src/backend/src/shared/auth.guard.ts | 2 +- src/frontend/README.md | 2 +- src/frontend/public/site.webmanifest | 4 ++-- src/frontend/src/index.html | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8097af8..653dedc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -All notable changes to the hardened Spooty branch are documented here. +All notable changes to Jarri Spooty are documented here. This project follows a practical chronological changelog rather than autogenerated dependency noise. diff --git a/README.md b/README.md index 4379add..bbf5f8d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Spooty Hardened +# Jarri Spooty Self-hosted Spotify playlist and track downloader built with NestJS + Angular. -Spooty does not download audio from Spotify itself. +Jarri Spooty does not download audio from Spotify itself. It retrieves metadata from Spotify and locates matching audio on YouTube. This hardened branch focuses on: diff --git a/src/backend/src/shared/auth.guard.ts b/src/backend/src/shared/auth.guard.ts index 9e7f9c8..4da142f 100644 --- a/src/backend/src/shared/auth.guard.ts +++ b/src/backend/src/shared/auth.guard.ts @@ -34,7 +34,7 @@ export class AuthGuard implements CanActivate { const providedToken = this.extractToken(request); if (providedToken !== expectedToken) { - throw new UnauthorizedException('Invalid or missing Spooty auth token'); + throw new UnauthorizedException('Invalid or missing Jarri Spooty auth token'); } return true; diff --git a/src/frontend/README.md b/src/frontend/README.md index 6d9c621..e384097 100644 --- a/src/frontend/README.md +++ b/src/frontend/README.md @@ -1,4 +1,4 @@ -# SpootyFe +# Jarri Spooty Frontend This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.0.3. diff --git a/src/frontend/public/site.webmanifest b/src/frontend/public/site.webmanifest index 5ee6a4b..308fc80 100644 --- a/src/frontend/public/site.webmanifest +++ b/src/frontend/public/site.webmanifest @@ -1,6 +1,6 @@ { - "name": "SpootyFe", - "short_name": "SpootyFe", + "name": "Jarri Spooty", + "short_name": "Spooty", "description": "Self-hosted Spotify downloader", "icons": [ { diff --git a/src/frontend/src/index.html b/src/frontend/src/index.html index 3ea4d09..091d567 100644 --- a/src/frontend/src/index.html +++ b/src/frontend/src/index.html @@ -2,7 +2,7 @@ - SpootyFe + Jarri Spooty From a71a47f76a9b3c4a7f3a7fb600c052da15755c57 Mon Sep 17 00:00:00 2001 From: TorMatzAndren Date: Sat, 16 May 2026 13:10:00 +0200 Subject: [PATCH 30/30] Document Jarri Spooty hardening updates --- CHANGELOG.md | 6 ++++++ README.md | 14 +++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 653dedc..3868bdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,12 @@ This project follows a practical chronological changelog rather than autogenerat - Added isolated Spotify credential storage - Added safer cookie handling flow - Improved Docker secret isolation documentation +- Removed unused downloader dependencies to reduce transitive attack surface +- Hardened download filename sanitization +- Hardened remote cover-art fetching with protocol, MIME, timeout, and size checks +- Added yt-dlp subprocess timeout and single-settlement handling +- Shaped track websocket payloads to avoid leaking internal entity fields +- Normalized visible project naming to Jarri Spooty ### Frontend diff --git a/README.md b/README.md index bbf5f8d..d2df967 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ This hardened branch focuses on: - Persistent SQLite state across container restarts - Deterministic Docker config persistence - Improved operational observability +- Reduced unused dependency surface +- Hardened filename, cover-art, subprocess, and websocket boundaries --- @@ -40,6 +42,8 @@ This hardened branch focuses on: - Automatic YouTube retry/fallback handling - Failed YouTube candidate rejection memory - Deterministic yt-dlp error classification +- Hardened cover-art validation and embedding +- Explicit client-facing websocket payload shaping --- @@ -136,7 +140,7 @@ docker run --rm -p 3000:3000 \ -v "$PWD/downloads:/spooty/backend/downloads" \ -v "$PWD/spooty-config:/spooty/backend/config" \ -v "/etc/tokens/youtube.cookies.txt:/spooty/config/youtube.cookies.txt:ro" \ - spootyfy-hardened:local + jarri-spooty:local ``` Open: @@ -158,8 +162,8 @@ Then: ```yaml services: spooty: - image: spootyfy-hardened:local - container_name: spooty + image: jarri-spooty:local + container_name: jarri-spooty restart: unless-stopped ports: @@ -204,7 +208,7 @@ services: ```bash npm install npm run build -docker build -t spootyfy-hardened:local . +docker build -t jarri-spooty:local . ``` --- @@ -493,7 +497,7 @@ docker run --rm -p 3000:3000 \ -v "$PWD/downloads:/spooty/backend/downloads" \ -v "$PWD/spooty-config:/spooty/backend/config" \ -v "/etc/tokens/youtube.cookies.txt:/spooty/config/youtube.cookies.txt:ro" \ - spootyfy-hardened:local + jarri-spooty:local ``` ---