From 7ac3d4bfc74bdde81cf84c9ba86bfd217bcaa681 Mon Sep 17 00:00:00 2001 From: "cl.st" Date: Wed, 3 Dec 2025 17:35:30 +0100 Subject: [PATCH] merge 1.0.7 (#525) * feat: simplify docker path and make toml path dynamic (#516) * feat(api) no download mode (#523) * feat(api): add no_download flag * feat(api) no download mode * Fix tests * feat(api): separate history from queue file list (#514) * feat(api): separate history from queue file list * feat(front): add front history * feat: add docs and test * Cache files reading * Use lookups for performance * fix(api): return correct array from object * Fix loading state --------- Co-authored-by: unthoiz --------- Co-authored-by: unthoiz --- .env | 2 + .github/workflows/playwright.yml | 8 +- CHANGELOG.md | 13 + Makefile | 27 +- README.md | 35 +- api/constants.ts | 5 +- api/index.ts | 14 +- api/scripts/init.sh | 8 +- api/src/helpers/queue_save_file.ts | 58 +- api/src/processing/ProcessingStack.ts | 214 ++- api/src/processing/jobs.ts | 44 +- api/src/processing/mix-to-playlist.ts | 39 +- api/src/routes/history.ts | 44 + api/src/services/beets.ts | 6 +- api/src/services/config.ts | 2 + api/src/services/db-json.ts | 8 +- api/src/services/history.ts | 57 + api/src/services/sync.ts | 44 +- api/src/services/tiddl.ts | 10 +- api/src/types.ts | 3 +- api/yarn.lock | 780 --------- app/src/components/Buttons/DownloadButton.tsx | 51 +- app/src/components/MainLayout.tsx | 55 +- app/src/components/Parameters/TidalPanel.tsx | 82 +- .../components/Processing/ProcessingItem.tsx | 19 +- .../components/Processing/ProcessingList.tsx | 16 +- app/src/provider/ApiFetcherProvider.tsx | 32 +- app/src/provider/HistoryProvider.tsx | 62 + app/src/types.ts | 8 +- app/yarn.lock | 1461 ----------------- compose.yml | 8 +- docker/Dockerfile | 28 +- docker/entrypoint.sh | 24 +- docs/API_DOCUMENTATION.md | 49 +- docs/NGINX_PROXY_MANAGER.md | 5 +- e2e/.env.e2e | 3 + e2e/.env.e2e.nodownload | 2 + e2e/tests/config.spec.ts | 2 + e2e/tests/editConfig.spec.ts | 6 +- e2e/tests/history.spec.ts | 62 + e2e/tests/noDownload.spec.ts | 145 ++ e2e/yarn.lock | 46 - settings/beets-config.yml | 2 +- settings/config.toml | 7 +- yarn.lock | 871 +++++----- 45 files changed, 1387 insertions(+), 3080 deletions(-) create mode 100644 api/src/routes/history.ts create mode 100644 api/src/services/history.ts delete mode 100644 api/yarn.lock create mode 100644 app/src/provider/HistoryProvider.tsx delete mode 100644 app/yarn.lock create mode 100644 e2e/.env.e2e.nodownload create mode 100644 e2e/tests/history.spec.ts create mode 100644 e2e/tests/noDownload.spec.ts delete mode 100644 e2e/yarn.lock diff --git a/.env b/.env index fac2143..c4fcf55 100644 --- a/.env +++ b/.env @@ -22,3 +22,5 @@ APPRISE_API_TAG= M3U_BASEPATH_FILE= SYNC_CRON_EXPRESSION= PUSH_OVER_URL= +NO_DOWNLOAD= +ENABLE_HISTORY= diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index fa95d8e..a79c87f 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -20,21 +20,17 @@ jobs: with: node-version: 20 cache: 'yarn' - cache-dependency-path: e2e/yarn.lock - name: Install dependencies - working-directory: ./e2e run: yarn install --frozen-lockfile - name: Install Playwright Browsers - working-directory: ./e2e - run: npx playwright install --with-deps chromium + run: yarn workspace tidarr-testing exec playwright install --with-deps chromium - name: Run Playwright tests - working-directory: ./e2e env: CI: true - run: npx playwright test + run: yarn workspace tidarr-testing playwright:test - uses: actions/upload-artifact@v4 if: failure() diff --git a/CHANGELOG.md b/CHANGELOG.md index c5c5346..181dfcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ Tidarr notable changes. [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) format. +## 📦 1.0.7 + +⚠️ Docker volume path as changed. Beware to check your path in `docker-compose.yml` and `config.toml` tiddl congif files. + +### 🚀 Added +- [API] Add `NO_DOWNLOAD` env var to use Tidarr UI only and process download with custom scripts. #517 +- [API] Add persistant history separated from queue #512 +### 🖍 Changed +- [Tiddl] Tidarr use now toml `scan_path` and `download_path` parameters +- [Docker] Update docker volume path to `/shared` and `/music` +- [Docker] Move `/home/app/standalone`to `/tidarr` +- [API] json-db: add cache for file reading + ## 📦 1.0.6 ### 🚀 Added - [API] Add NTFY.sh notification service diff --git a/Makefile b/Makefile index 4aad76a..da9bf22 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,7 @@ dev: ## Boot dev environnement $(DOCKER_COMPOSE) up tidarr --build --remove-orphans install: ## Install deps - $(DOCKER_COMPOSE) exec -w /home/app/standalone/api tidarr yarn install - $(DOCKER_COMPOSE) exec -w /home/app/standalone/app tidarr yarn install - $(DOCKER_COMPOSE) exec -w /home/app/standalone/e2e tidarr yarn install + $(DOCKER_COMPOSE) exec tidarr yarn install ## ## Playwright 🚨 ##-------------- @@ -24,6 +22,9 @@ install: ## Install deps testing-build: ## Build Tidarr production Docker image for E2E tests docker build -t tidarr-prod --target production -f docker/Dockerfile . +testing-install: ## Install Playwright browsers + cd e2e && npx playwright install + testing-run: ## Run Playwright E2E tests (each test gets isolated container on random port) (arg: f=filter) cd e2e && npx playwright test $(f) @@ -44,20 +45,20 @@ testing-ui: ## Run local Playwright UI ##---------------- quality-deadcode: ## Fin deadcode with `ts-prune` - $(DOCKER_COMPOSE) exec -w /home/app/standalone/api tidarr yarn find-deadcode - $(DOCKER_COMPOSE) exec -w /home/app/standalone/app tidarr yarn find-deadcode - $(DOCKER_COMPOSE) exec -w /home/app/standalone/e2e tidarr yarn find-deadcode + $(DOCKER_COMPOSE) exec -w /tidarr/api tidarr yarn find-deadcode + $(DOCKER_COMPOSE) exec -w /tidarr/app tidarr yarn find-deadcode + $(DOCKER_COMPOSE) exec -w /tidarr/e2e tidarr yarn find-deadcode quality-depcheck: ## Check dependencies - $(DOCKER_COMPOSE) exec -w /home/app/standalone/api tidarr yarn depcheck - $(DOCKER_COMPOSE) exec -w /home/app/standalone/app tidarr yarn depcheck - $(DOCKER_COMPOSE) exec -w /home/app/standalone/e2e tidarr yarn depcheck + $(DOCKER_COMPOSE) exec -w /tidarr/api tidarr yarn depcheck + $(DOCKER_COMPOSE) exec -w /tidarr/app tidarr yarn depcheck + $(DOCKER_COMPOSE) exec -w /tidarr/e2e tidarr yarn depcheck quality-lint: ## Check dependencies - $(DOCKER_COMPOSE) exec -w /home/app/standalone tidarr yarn eslint + $(DOCKER_COMPOSE) exec -w /tidarr tidarr yarn eslint quality-lint-fix: ## Check dependencies - $(DOCKER_COMPOSE) exec -w /home/app/standalone tidarr yarn eslint-fix + $(DOCKER_COMPOSE) exec -w /tidarr tidarr yarn eslint-fix ## ## Builder 🚀 @@ -71,8 +72,8 @@ docker-run: ## Run tidarr docker image --rm \ --name tidarr \ -p 8484:8484 \ - -v ${PWD}/docker/mnt/config:/home/app/standalone/shared \ - -v ${PWD}/docker/mnt/library:/home/app/standalone/library \ + -v ${PWD}/docker/mnt/config:/shared \ + -v ${PWD}/docker/mnt/library:/music \ -e ENABLE_BEETS=true \ -e ENABLE_TIDAL_PROXY=true \ -e PUID=501 \ diff --git a/README.md b/README.md index 1e48cbc..c1dd342 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Tidarr is a Docker image that provides a web interface to download up to **24-bi - [M3U track base path](#m3u-track-base-path) - [Sync playlists and mixes](#sync-playlists-and-mixes) - [Custom CSS](#custom-css) + - [Download History](#download-history) - [Services](#services): - [Beets](#beets) - [Plex/Plexamp](#plex-integration) @@ -37,6 +38,7 @@ Tidarr is a Docker image that provides a web interface to download up to **24-bi - [Webhook push over](#webhook-push-over) - [Advanced](#advanced) - [Custom Processing Script](#custom-processing-script) + - [No-download flag](#no-download) - [API Documentation](#api-documentation) - [User requests](#user-requests) - [Donate](#donate) @@ -105,8 +107,8 @@ services: ports: - 8484:8484 volumes: - - /any/folder/to/tidarr/config:/home/app/standalone/shared - - /any/folder/to/library:/home/app/standalone/library + - /any/folder/to/tidarr/config:/shared + - /any/folder/to/library:/music restart: 'unless-stopped' ``` @@ -117,8 +119,8 @@ docker run \ --rm \ --name tidarr \ -p 8484:8484 \ - -v /any/folder/to/tidarr/config:/home/app/standalone/shared \ - -v /any/folder/to/library:/home/app/standalone/library \ + -v /any/folder/to/tidarr/config:/shared \ + -v /any/folder/to/library:/music \ cstaelen/tidarr:latest ``` @@ -232,6 +234,21 @@ You can customize Tidarr's appearance using the UI in settings dialog, or by edi **File location**: `/your/docker/path/to/tidarr/config/custom.css` +### Download History + +Track your downloaded items with the history feature. When enabled, Tidarr will maintain a list of all downloaded content and mark items as already downloaded in the UI. + +```yaml +environment: + - ... + - ENABLE_HISTORY=true +``` + +**Features:** +- Persistent download tracking across restarts +- Visual indicators for already downloaded items (green checkmark) +- Manual history clearing available in settings dialog + ## SERVICES ### Beets @@ -375,10 +392,18 @@ Tidarr supports executing a custom shell script during the post-processing pipel > > 1. Create a shell script named `custom-script.sh` in your config folder (the mounted `shared/` volume) > 2. The script will be automatically detected and executed during post-processing -> 3. The script runs **after** permissions are set but **before** files are moved to the library +> 3. The script runs **after** the tiddl download process (if not deactivated) +> +> To keep the benefits of post processing, all your files must be in the download folder using `PROCESSING_PATH` var available in `custom-script.sh`. > > 📖 [View complete API documentation](docs/CUSTOM_SCRIPT_DOCUMENTATION.md) +### NO DOWNLOAD + +If you want to use Tidarr only as UI and not download files, you can set `NO_DOWNLOAD=true` in the environment variables. + +This way you can use Tidarr to manage your download history, watchlist, and keep benefits of json DB (`sync_list.json`, `queue.json`) to manage download via custom scripts. + ### API Documentation If you want to interact with Tidarr from other applications (scripts, external services, automations), you can use the Express API. diff --git a/api/constants.ts b/api/constants.ts index 0045f62..22e74ad 100644 --- a/api/constants.ts +++ b/api/constants.ts @@ -1,6 +1,5 @@ -export const ROOT_PATH = "/home/app/standalone"; -export const CONFIG_PATH = "/home/app/standalone/shared"; -export const PROCESSING_PATH = "/home/app/standalone/shared/.processing"; +export const ROOT_PATH = "/tidarr"; +export const CONFIG_PATH = "/shared"; export const TIDAL_API_URL = "https://api.tidal.com"; export const SYNC_DEFAULT_CRON = "0 3 * * *"; export const TOKEN_REFRESH_THRESHOLD = 30 * 60; // Refresh token if expires in less than 30 minutes (1800 seconds) diff --git a/api/index.ts b/api/index.ts index 9376243..6867b6e 100644 --- a/api/index.ts +++ b/api/index.ts @@ -13,11 +13,13 @@ import { setupProxies } from "./src/proxies"; // Import routers import authRouter from "./src/routes/auth"; import configRouter from "./src/routes/config"; +import historyRouter from "./src/routes/history"; import processingRouter from "./src/routes/processing"; import sseRouter from "./src/routes/sse"; import syncRouter from "./src/routes/sync"; import tiddlTomlRouter from "./src/routes/tiddl-toml"; import { configureServer } from "./src/services/config"; +import { loadHistoryFromFile } from "./src/services/history"; import { createCronJob } from "./src/services/sync"; import { startTokenRefreshInterval } from "./src/services/token-refresh"; @@ -43,11 +45,13 @@ app.use(cors()); // Setup all API proxies (Tidal, Plex, Navidrome) setupProxies(app); -const processingList = ProcessingStack(app); +const processingList = ProcessingStack(); app.locals.processingStack = processingList; app.locals.addOutputLog = processingList.actions.addOutputLog; app.locals.activeListConnections = []; app.locals.activeItemOutputConnections = new Map(); +app.locals.history = []; +app.locals.historySet = new Set(); app.all("/{*any}", function (_req, res, next) { res.header("Access-Control-Allow-Origin", "*"); @@ -67,6 +71,7 @@ app.use("/api", configRouter); app.use("/api", syncRouter); app.use("/api", customCssRouter); app.use("/api", tiddlTomlRouter); +app.use("/api", historyRouter); // Run @@ -78,6 +83,11 @@ const server = app.listen(port, async () => { const { config: tiddlConfig } = get_tiddl_config(); app.locals.tiddlConfig = tiddlConfig; + // Load download history + const history = await loadHistoryFromFile(); + app.locals.history = history; + app.locals.historySet = new Set(history); + // Start token refresh interval (checks every 15 minutes) startTokenRefreshInterval(app); @@ -100,7 +110,7 @@ process.on("SIGINT", () => gracefulShutdown("SIGINT", app, server)); // fallback load app -const frontendFiles = "/home/app/standalone/app/build"; +const frontendFiles = "/tidarr/app/build"; if (fs.existsSync(frontendFiles)) { app.use(express.static(frontendFiles)); app.get("/{*any}", (_, res) => { diff --git a/api/scripts/init.sh b/api/scripts/init.sh index 2665dd8..942e0ee 100644 --- a/api/scripts/init.sh +++ b/api/scripts/init.sh @@ -2,10 +2,10 @@ echo "🕖 [TIDARR] Application loading ... " -SETTINGS_URL="/home/app/standalone/settings" -PUBLIC_URL="/home/app/standalone/app/build" -DEV_PUBLIC_URL="/home/app/standalone/app/public" -SHARED_URL="/home/app/standalone/shared" +SETTINGS_URL="/tidarr/settings" +PUBLIC_URL="/tidarr/build" +DEV_PUBLIC_URL="/tidarr/app/public" +SHARED_URL="/shared" mkdir -p $SHARED_URL/.tiddl/ 2>&1 || { echo "❌ [TIDDL] Failed to create .tiddl directory" diff --git a/api/src/helpers/queue_save_file.ts b/api/src/helpers/queue_save_file.ts index 05ec164..3b1d86e 100644 --- a/api/src/helpers/queue_save_file.ts +++ b/api/src/helpers/queue_save_file.ts @@ -1,16 +1,35 @@ import { queueDb } from "../services/db-json"; +import { addItemToHistory } from "../services/history"; import { ProcessingItemType } from "../types"; const QUEUE_PATH = "/"; +// In-memory cache to avoid disk reads +let queueCache: ProcessingItemType[] | null = null; +let queueCacheMap: Map | null = null; + +/** + * Load queue from file (only on first call, then uses cache) + */ export async function loadQueueFromFile(): Promise { + // Return cache if available + if (queueCache !== null) { + return queueCache; + } + + // First load: read from disk try { const data = await queueDb.getData(QUEUE_PATH); - return Array.isArray(data) ? data : []; + queueCache = Array.isArray(data) ? data : []; + // Build Map for O(1) lookups + queueCacheMap = new Map(queueCache.map((item) => [item.id, item])); + return queueCache; } catch { // Database doesn't exist yet or path not found, initialize with empty array - await queueDb.push(QUEUE_PATH, []); - return []; + await queueDb.push(QUEUE_PATH, []); // auto-saves with saveOnPush=true + queueCache = []; + queueCacheMap = new Map(); + return queueCache; } } @@ -18,20 +37,32 @@ export const addItemToFile = async (item: ProcessingItemType) => { const saveList = await loadQueueFromFile(); delete item.process; saveList.push(item); + + // Update cache + queueCache = saveList; + queueCacheMap?.set(item.id, item); + + // Write to disk (auto-saves with saveOnPush=true) await queueDb.push(QUEUE_PATH, saveList); }; export const removeItemFromFile = async (id: string) => { const saveList = await loadQueueFromFile(); const filteredList = saveList.filter((item) => item.id !== id); + + // Update cache + queueCache = filteredList; + queueCacheMap?.delete(id); + + // Write to disk (auto-saves with saveOnPush=true) await queueDb.push(QUEUE_PATH, filteredList); }; export const updateItemInQueueFile = async (item: ProcessingItemType) => { const saveList = await loadQueueFromFile(); - const itemIndex = saveList.findIndex((current) => current.id === item.id); - if (itemIndex === -1) { + // O(1) lookup using Map instead of O(n) findIndex + if (!queueCacheMap?.has(item.id)) { // Item not found - it may have been removed already (e.g., auto-remove finished items) // This is not an error, just skip the update console.log( @@ -40,7 +71,24 @@ export const updateItemInQueueFile = async (item: ProcessingItemType) => { return; } + const itemIndex = saveList.findIndex((current) => current.id === item.id); + delete item.process; + + // If item is finished, move it to history + if (item.status === "finished" && process.env.ENABLE_HISTORY === "true") { + // Add to history + await addItemToHistory(item.id); + console.log(`[QUEUE] Item ${item.id} moved to history`); + } + + // Keep in queue, just update saveList[itemIndex] = { ...item }; + + // Update cache + queueCache = saveList; + queueCacheMap?.set(item.id, item); + + // Write to disk (auto-saves with saveOnPush=true) await queueDb.push(QUEUE_PATH, saveList); }; diff --git a/api/src/processing/ProcessingStack.ts b/api/src/processing/ProcessingStack.ts index fde67d7..0692edc 100644 --- a/api/src/processing/ProcessingStack.ts +++ b/api/src/processing/ProcessingStack.ts @@ -1,6 +1,6 @@ import { Express, Response } from "express"; -import { PROCESSING_PATH } from "../../constants"; +import { getAppInstance } from "../app-instance"; import { logs } from "../helpers/logs"; import { addItemToFile, @@ -16,12 +16,13 @@ import { ntfyPush } from "../services/ntfy"; import { plexUpdate } from "../services/plex"; import { hookPushOver } from "../services/pushover"; import { tidalDL } from "../services/tiddl"; -import { ProcessingItemType, TiddlConfig } from "../types"; +import { ProcessingItemType } from "../types"; import { cleanFolder, executeCustomScript, getFolderToScan, + getProcessingPath, hasFileToMove, killProcess, moveAndClean, @@ -35,8 +36,8 @@ import { getTracksByMixId, } from "./mix-to-playlist"; -export function notifySSEConnections(expressApp: Express) { - const { processingStack, activeListConnections } = expressApp.locals; +export function notifySSEConnections(app: Express) { + const { processingStack, activeListConnections } = app.locals; // Data no longer contains output/output_history, send directly const data = JSON.stringify(processingStack.data); @@ -46,13 +47,9 @@ export function notifySSEConnections(expressApp: Express) { }); } -export function notifyItemOutput( - expressApp: Express, - itemId: string, - output: string, -) { +export function notifyItemOutput(app: Express, itemId: string, output: string) { const connections: Map = - expressApp.locals.activeItemOutputConnections; + app.locals.activeItemOutputConnections; // Ensure itemId is a string for Map lookup const itemIdString = String(itemId); const itemConnections = connections.get(itemIdString); @@ -73,8 +70,10 @@ export function notifyItemOutput( } } -export const ProcessingStack = (expressApp: Express) => { +export const ProcessingStack = () => { + const app: Express = getAppInstance(); const data: ProcessingItemType[] = []; + const dataMap: Map = new Map(); // O(1) item lookups const outputs: Map = new Map(); // Store terminal output as array of lines let isPaused = false; @@ -89,6 +88,7 @@ export const ProcessingStack = (expressApp: Express) => { // Initialize empty output history for each loaded item (ensure string key) outputs.set(String(record.id), []); data.push(record); + dataMap.set(record.id, record); }); if (!isPaused) { @@ -123,39 +123,42 @@ export const ProcessingStack = (expressApp: Express) => { // Notify connected clients about the output update const currentOutput = getItemOutput(idString); - notifyItemOutput(expressApp, id, currentOutput); + notifyItemOutput(app, id, currentOutput); } async function addItem(item: ProcessingItemType) { - const foundIndex = data.findIndex( - (listItem: ProcessingItemType) => listItem?.id === item?.id, - ); - if (foundIndex !== -1) return; + // O(1) lookup using Map instead of O(n) findIndex + if (dataMap.has(item.id)) { + await removeItem(item.id); + } data.push(item); + dataMap.set(item.id, item); await addItemToFile(item); processQueue(); - notifySSEConnections(expressApp); + notifySSEConnections(app); } async function removeItem(id: string) { - const item = getItem(id); + // O(1) lookup using Map + const item = dataMap.get(id); + + if (!item) { + console.warn(`removeItem: Item ${id} not found in processing list`); + return; + } const foundIndex = data.findIndex( (listItem: ProcessingItemType) => listItem?.id === item?.id, ); - if (foundIndex === -1) { - console.warn(`removeItem: Item ${id} not found in processing list`); - return; - } - // Kill the process if it exists and is running killProcess(item?.process, id); delete data[foundIndex]; data.splice(foundIndex, 1); + dataMap.delete(id); // Clean up output history for this item (ensure string key) outputs.delete(String(id)); @@ -164,7 +167,7 @@ export const ProcessingStack = (expressApp: Express) => { await removeItemFromFile(id); processQueue(); - notifySSEConnections(expressApp); + notifySSEConnections(app); } async function removeAllItems() { @@ -180,7 +183,7 @@ export const ProcessingStack = (expressApp: Express) => { async function removeFinishedItems() { const itemsToRemove = data.filter((item) => - ["finished", "downloaded", "error"].includes(item.status), + ["finished", "error"].includes(item.status), ); for (const item of itemsToRemove) { try { @@ -192,53 +195,36 @@ export const ProcessingStack = (expressApp: Express) => { } function updateItem(item: ProcessingItemType) { - if (item?.status === "downloaded") { - try { - postProcessing(item); - } catch (error) { - // Catch any unhandled errors in postProcessing to prevent silent failures - const errorMessage = - error instanceof Error ? error.message : String(error); - logs( - item.id, - `❌ [TIDARR] Unexpected error during post-processing: ${errorMessage}`, - ); - console.error( - `❌ [TIDARR] Post-processing error for item ${item.id}:`, - error, - ); - item["status"] = "error"; - // Update the item in the queue file to reflect error status - updateItemInQueueFile(item).catch((err) => - console.error( - `❌ [TIDARR] Failed to update queue file for item ${item.id}:`, - err, - ), - ); - } - } if (item?.status === "finished" || item?.status === "error") { processQueue(); } // Notify list connections about status changes (without output data) - notifySSEConnections(expressApp); + notifySSEConnections(app); // Notify item-specific output connections with the latest output const currentOutput = getItemOutput(item.id); if (currentOutput) { - notifyItemOutput(expressApp, item.id, currentOutput); + notifyItemOutput(app, item.id, currentOutput); } } function getItem(id: string): ProcessingItemType { - const foundIndex = data.findIndex( - (listItem: ProcessingItemType) => listItem?.id === id, - ); - return data[foundIndex]; + // O(1) lookup using Map instead of O(n) findIndex + return dataMap.get(id) as ProcessingItemType; } - function processQueue(): void { + async function prepareProcessing(item: ProcessingItemType) { + item["status"] = "processing"; + updateItem(item); + + // Initialize empty output history in the Map (ensure string key) + outputs.set(String(item.id), []); + + await cleanFolder(item.id); + } + + async function processQueue(): Promise { if (isPaused) return; const indexCurrent = data.findIndex( @@ -250,7 +236,19 @@ export const ProcessingStack = (expressApp: Express) => { if (indexCurrent !== -1) return; if (indexNext !== -1) { - processItem(data[indexNext]); + const item = data[indexNext]; + await prepareProcessing(item); + + // If no_download=true skip processing. + if (app.locals.config.parameters?.NO_DOWNLOAD === "true") { + item["status"] = "no_download"; + item["loading"] = false; + updateItem(item); + + return; + } + + processItem(item); } } @@ -284,102 +282,90 @@ export const ProcessingStack = (expressApp: Express) => { await cleanFolder(currentItem.id); } - notifySSEConnections(expressApp); + notifySSEConnections(app); } function resumeQueue() { isPaused = false; processQueue(); // Restart the queue - notifySSEConnections(expressApp); + notifySSEConnections(app); } function getQueueStatus() { return { isPaused }; } - async function processingMix(item: ProcessingItemType) { - const config = expressApp.locals.tiddlConfig as TiddlConfig; - - const tracks = await getTracksByMixId(item, config); - const playlistId = await createNewPlaylist(item, config); + async function prepareMixToPlaylist( + item: ProcessingItemType, + ): Promise { + const tracks = await getTracksByMixId(item); + const playlistId = await createNewPlaylist(item); if (tracks) { - await addTracksToPlaylist(playlistId, tracks, config, item.id); - - item["url"] = `playlist/${playlistId}`; - logs(item.id, `🕖 [MIX]: Download temporary playlist`); - - const child = tidalDL(item.id, expressApp, () => { - deletePlaylist(playlistId, config, item.id); - }); - if (child) { - item["process"] = child; - } - - return; - } else { - logs(item.id, `⚠️ [MIX]: No track found.`); - item["status"] = "error"; - updateItem(item); + await addTracksToPlaylist(playlistId, tracks, item.id); + return playlistId; } - deletePlaylist(playlistId, config, item.id); + logs(item.id, `⚠️ [MIX]: No track found.`); + item["status"] = "error"; + updateItem(item); + deletePlaylist(playlistId, item.id); } async function processItem(item: ProcessingItemType) { - item["status"] = "processing"; - // Initialize empty output history in the Map (ensure string key) - outputs.set(String(item.id), []); - updateItem(item); - - await cleanFolder(item.id); - + let playlistId; if (item.type === "mix") { - processingMix(item); - } else { - const child = tidalDL(item.id, expressApp); - if (child) { - item["process"] = child; - } + const playlistId = await prepareMixToPlaylist(item); + item["url"] = `playlist/${playlistId}`; + } + + const child = tidalDL(item.id, app, async () => { + if (playlistId) deletePlaylist(playlistId, item.id); + postProcessing(item); + return; + }); + + if (child) { + item["process"] = child; } } async function postProcessing(item: ProcessingItemType) { - const shouldPostProcess = hasFileToMove(`${PROCESSING_PATH}/${item.id}`); - logs(item.id, "---------------------"); logs(item.id, "⚙️ POST PROCESSING "); logs(item.id, "---------------------"); - if (!shouldPostProcess) { - item["status"] = "finished"; - updateItem(item); - logs(item.id, "✅ [TIDARR] No file to process."); - - // Remove item from persistant queue file - await updateItemInQueueFile(item); - - return; - } - if (item["status"] === "error") { logs(item.id, "⚠️ [TIDDL] An error occured while downloading."); return; } - if (item["type"] === "playlist" || item["type"] === "mix") { - replacePathInM3U(item); + // Stop if there is no file to process (maybe existing) + const processingPath = getProcessingPath(); + const shouldPostProcess = hasFileToMove(`${processingPath}/${item.id}`); + + if (!shouldPostProcess) { + item["status"] = "finished"; + updateItem(item); + logs(item.id, "✅ [TIDARR] No file to process."); + + await updateItemInQueueFile(item); + + return; } + // Execute custom script if exists + await executeCustomScript(item); + + // Update m3u item path + replacePathInM3U(item); + // Beets process await beets(item.id); // Set permissions await setPermissions(item); - // Execute custom script if exists - await executeCustomScript(item); - // Keep trace of folders processed const foldersToScan = getFolderToScan(item.id); diff --git a/api/src/processing/jobs.ts b/api/src/processing/jobs.ts index a90a245..cc8b210 100644 --- a/api/src/processing/jobs.ts +++ b/api/src/processing/jobs.ts @@ -2,23 +2,31 @@ import { ChildProcess, execSync, spawn } from "child_process"; import fs from "fs"; import path from "path"; -import { CONFIG_PATH, PROCESSING_PATH, ROOT_PATH } from "../../constants"; +import { CONFIG_PATH } from "../../constants"; import { getAppInstance } from "../app-instance"; import { logs } from "../helpers/logs"; import { ProcessingItemType } from "../types"; +export function getProcessingPath(): string { + const app = getAppInstance(); + const downloadPath = app.locals.tiddlConfig?.download.download_path; + // Remove trailing slash if present to avoid double slashes + return downloadPath?.replace(/\/+$/, ""); +} + export async function moveAndClean(id: string): Promise<{ status: "finished" | "error" | undefined; }> { const app = getAppInstance(); const item: ProcessingItemType = app.locals.processingStack.actions.getItem(id); - + const processingPath = getProcessingPath(); let status: "finished" | "error" | undefined; if (!item) return { status: "finished" }; - const itemProcessingPath = `${PROCESSING_PATH}/${item.id}`; + const itemProcessingPath = `${processingPath}/${item.id}`; + const libraryPath = app.locals.tiddlConfig.download.scan_path; try { logs(item.id, "🕖 [TIDARR] Move processed items ..."); @@ -35,7 +43,7 @@ export async function moveAndClean(id: string): Promise<{ args = "-rfp"; } - const cmd = `cp ${args} "${itemProcessingPath}"/* "${ROOT_PATH}/library" >/dev/null`; + const cmd = `cp ${args} "${itemProcessingPath}"/* "${libraryPath}" >/dev/null`; console.log(`🕖 [TIDARR] Command: ${cmd}`); execSync(cmd, { encoding: "utf-8", shell: "/bin/sh" }); logs(item.id, `✅ [TIDARR] Move complete (${item.type})`); @@ -58,9 +66,11 @@ export async function moveAndClean(id: string): Promise<{ export async function cleanFolder( itemId?: string, ): Promise<"finished" | "error"> { + const processingPath = getProcessingPath(); + const targetPath = itemId - ? `${PROCESSING_PATH}/${itemId}` - : `${PROCESSING_PATH}/*`; + ? `${processingPath}/${itemId}` + : `${processingPath}/*`; // Check if target exists before attempting to remove if (itemId) { @@ -70,7 +80,7 @@ export async function cleanFolder( } } else { // For wildcard cleanup, check if processing folder exists - if (!fs.existsSync(PROCESSING_PATH)) { + if (!fs.existsSync(processingPath)) { return "finished"; } } @@ -91,7 +101,9 @@ export async function cleanFolder( } export function hasFileToMove(pathArg?: string): boolean { - const targetPath = pathArg || PROCESSING_PATH; + const processingPath = getProcessingPath(); + + const targetPath = pathArg || processingPath; // Check if path exists first if (!fs.existsSync(targetPath)) { @@ -117,8 +129,11 @@ export function hasFileToMove(pathArg?: string): boolean { } export function replacePathInM3U(item: ProcessingItemType): void { + if (item["type"] !== "playlist" && item["type"] !== "mix") return; + + const processingPath = getProcessingPath(); const basePath = process.env.M3U_BASEPATH_FILE || "./"; - const downloadDir = `${PROCESSING_PATH}/${item.id}`; + const downloadDir = `${processingPath}/${item.id}`; logs(item.id, `🕖 [TIDARR] Update track path in M3U file ...`); @@ -151,7 +166,9 @@ export function replacePathInM3U(item: ProcessingItemType): void { } export async function setPermissions(item: ProcessingItemType) { - const itemProcessingPath = `${PROCESSING_PATH}/${item.id}`; + const processingPath = getProcessingPath(); + + const itemProcessingPath = `${processingPath}/${item.id}`; if (process.env.PUID && process.env.PGID) { try { @@ -225,8 +242,10 @@ export async function setPermissions(item: ProcessingItemType) { * @returns Array of parent folder paths relative to item's processing path that contain files to scan */ export function getFolderToScan(itemId: string): string[] { + const processingPath = getProcessingPath(); + const foldersToScan: string[] = []; - const itemProcessingPath = `${PROCESSING_PATH}/${itemId}`; + const itemProcessingPath = `${processingPath}/${itemId}`; try { // Find all files (not directories) in the item's processing directory @@ -325,8 +344,9 @@ export async function killProcess( export async function executeCustomScript( item: ProcessingItemType, ): Promise { + const processingPath = getProcessingPath(); const customScriptPath = path.join(CONFIG_PATH, "custom-script.sh"); - const itemProcessingPath = `${PROCESSING_PATH}/${item.id}`; + const itemProcessingPath = `${processingPath}/${item.id}`; // Check if the custom script exists if (!fs.existsSync(customScriptPath)) { diff --git a/api/src/processing/mix-to-playlist.ts b/api/src/processing/mix-to-playlist.ts index 4d5873d..abe26f2 100644 --- a/api/src/processing/mix-to-playlist.ts +++ b/api/src/processing/mix-to-playlist.ts @@ -1,10 +1,11 @@ +import { getAppInstance } from "../app-instance"; import { logs } from "../helpers/logs"; -import { ProcessingItemType, TiddlConfig } from "../types"; +import { ProcessingItemType } from "../types"; + +export async function getTracksByMixId(item: ProcessingItemType) { + const app = getAppInstance(); + const config = app.locals.config; -export async function getTracksByMixId( - item: ProcessingItemType, - config: TiddlConfig, -) { if (!config?.auth) return; logs(item.id, `🕖 [MIX]: Get track from mix id`); @@ -38,10 +39,10 @@ export async function getTracksByMixId( return ids; } -export async function createNewPlaylist( - item: ProcessingItemType, - config: TiddlConfig, -) { +export async function createNewPlaylist(item: ProcessingItemType) { + const app = getAppInstance(); + const config = app.locals.config; + if (!config?.auth) return; logs(item.id, `🕖 [MIX]: Create new playlist`); @@ -80,11 +81,10 @@ export async function createNewPlaylist( return json.data.id; } -export async function deletePlaylist( - playlistId: number, - config: TiddlConfig, - itemId: string, -) { +export async function deletePlaylist(playlistId: number, itemId: string) { + const app = getAppInstance(); + const config = app.locals.config; + if (!config?.auth) return; logs(itemId, `🕖 [MIX]: Delete temporary playlist`); @@ -108,7 +108,10 @@ export async function deletePlaylist( return response; } -export async function getPlaylistEtag(playlistId: number, config: TiddlConfig) { +export async function getPlaylistEtag(playlistId: number) { + const app = getAppInstance(); + const config = app.locals.config; + if (!config) return; const playlistRes = await fetch( @@ -137,14 +140,16 @@ export async function getPlaylistEtag(playlistId: number, config: TiddlConfig) { export async function addTracksToPlaylist( playlistId: number, trackIds: number[], - config: TiddlConfig, itemId: string, ) { + const app = getAppInstance(); + const config = app.locals.config; + if (!config) return; logs(itemId, `🕖 [MIX]: Add track ids to new playlist`); - const etag = await getPlaylistEtag(playlistId, config); + const etag = await getPlaylistEtag(playlistId); const url = `https://tidal.com/v1/playlists/${playlistId}/items?countryCode=FR&locale=fr_FR&deviceType=BROWSER`; const options: RequestInit = { diff --git a/api/src/routes/history.ts b/api/src/routes/history.ts new file mode 100644 index 0000000..cc175a3 --- /dev/null +++ b/api/src/routes/history.ts @@ -0,0 +1,44 @@ +import { Request, Response, Router } from "express"; + +import { getAppInstance } from "../app-instance"; +import { ensureAccessIsGranted } from "../helpers/auth"; +import { handleRouteError } from "../helpers/error-handler"; +import { flushHistory } from "../services/history"; + +const router = Router(); + +/** + * GET /api/history/list + * Get the download history + */ +router.get( + "/history/list", + ensureAccessIsGranted, + (_req: Request, res: Response) => { + try { + const app = getAppInstance(); + res.json(app.locals.history); + } catch (error) { + handleRouteError(error, res, "get history"); + } + }, +); + +/** + * DELETE /api/remove-all + * Clear the entire download queue + */ +router.delete( + "/history/list", + ensureAccessIsGranted, + (_req: Request, res: Response) => { + try { + flushHistory(); + res.sendStatus(204); + } catch (error) { + handleRouteError(error, res, "remove all items from history"); + } + }, +); + +export default router; diff --git a/api/src/services/beets.ts b/api/src/services/beets.ts index 2981f68..f7c3054 100644 --- a/api/src/services/beets.ts +++ b/api/src/services/beets.ts @@ -1,8 +1,9 @@ import { spawnSync } from "child_process"; -import { CONFIG_PATH, PROCESSING_PATH } from "../../constants"; +import { CONFIG_PATH } from "../../constants"; import { getAppInstance } from "../app-instance"; import { logs } from "../helpers/logs"; +import { getProcessingPath } from "../processing/jobs"; import { ProcessingItemType } from "../types"; function spawnBeet( @@ -46,11 +47,12 @@ export async function beets(id: string): Promise { const app = getAppInstance(); const item: ProcessingItemType = app.locals.processingStack.actions.getItem(id); + const processingPath = getProcessingPath(); if (!item || !["album", "artist", "favorite_albums"].includes(item.type)) return; - const itemProcessingPath = `${PROCESSING_PATH}/${item.id}`; + const itemProcessingPath = `${processingPath}/${item.id}`; try { // BEETS diff --git a/api/src/services/config.ts b/api/src/services/config.ts index f8d434a..cace78c 100644 --- a/api/src/services/config.ts +++ b/api/src/services/config.ts @@ -51,6 +51,8 @@ export async function configureServer() { ENABLE_TIDAL_PROXY: process.env.ENABLE_TIDAL_PROXY || "", SYNC_CRON_EXPRESSION: process.env.SYNC_CRON_EXPRESSION || SYNC_DEFAULT_CRON || "", + NO_DOWNLOAD: process.env.NO_DOWNLOAD || "", + ENABLE_HISTORY: process.env.ENABLE_HISTORY || "", }, }; } catch (error: unknown) { diff --git a/api/src/services/db-json.ts b/api/src/services/db-json.ts index c1aabd3..91d5ab5 100644 --- a/api/src/services/db-json.ts +++ b/api/src/services/db-json.ts @@ -3,10 +3,16 @@ import path from "path"; import { CONFIG_PATH } from "../../constants"; -// Queue database +// Queue database (saveOnPush enabled, cache used to avoid disk reads) const queueDbPath = path.join(CONFIG_PATH, "queue"); export const queueDb = new JsonDB(new Config(queueDbPath, true, false, "/")); +// History database (indexing disabled to preserve array type at root) +const historyDbPath = path.join(CONFIG_PATH, "history"); +export const historyDb = new JsonDB( + new Config(historyDbPath, true, false, "/"), +); + // Sync list database const syncListDbPath = path.join(CONFIG_PATH, "sync_list"); export const syncListDb = new JsonDB( diff --git a/api/src/services/history.ts b/api/src/services/history.ts new file mode 100644 index 0000000..8b4d5e1 --- /dev/null +++ b/api/src/services/history.ts @@ -0,0 +1,57 @@ +import { getAppInstance } from "../app-instance"; +import { historyDb } from "../services/db-json"; + +const QUEUE_PATH = "/"; + +export async function loadHistoryFromFile(): Promise { + if (process.env.ENABLE_HISTORY !== "true") { + return []; + } + + try { + const data = await historyDb.getData(QUEUE_PATH); + console.log("✅ [HISTORY] Processing history loaded."); + // Convert numeric-keyed object to array of values + return Array.isArray(data) ? data : Object.values(data); + } catch { + // Database doesn't exist yet or path not found, initialize with empty array + await historyDb.push(QUEUE_PATH, []); + return []; + } +} + +export async function addItemToHistory(itemId: string) { + if (process.env.ENABLE_HISTORY !== "true") { + return; + } + + const app = getAppInstance(); + const id = itemId.toString(); + + // O(1) lookup using Set instead of O(n) array.includes() + if (app.locals.historySet.has(id)) { + return; + } + + app.locals.history.push(id); + app.locals.historySet.add(id); + + // Write only the new item at the end of the array (more efficient than rewriting entire file) + const newIndex = app.locals.history.length - 1; + await historyDb.push(`/${newIndex}`, id, false); + console.log(`✅ [HISTORY] Item "${id}" added to history.`); +} + +export async function flushHistory() { + if (process.env.ENABLE_HISTORY !== "true") { + return; + } + + const app = getAppInstance(); + app.locals.history = []; + app.locals.historySet.clear(); + + await historyDb.push(QUEUE_PATH, []); + + console.log("🚽 [HISTORY] History has been flushed."); +} diff --git a/api/src/services/sync.ts b/api/src/services/sync.ts index d8380a9..6755c60 100644 --- a/api/src/services/sync.ts +++ b/api/src/services/sync.ts @@ -8,14 +8,28 @@ import { syncListDb } from "./db-json"; const SYNC_LIST_PATH = "/"; +// In-memory cache to avoid disk reads +let syncListCache: SyncItemType[] | null = null; + +/** + * Load sync list from file (only on first call, then uses cache) + */ const loadSyncList = async (): Promise => { + // Return cache if available + if (syncListCache !== null) { + return syncListCache; + } + + // First load: read from disk try { const data = await syncListDb.getData(SYNC_LIST_PATH); - return Array.isArray(data) ? data : []; + syncListCache = Array.isArray(data) ? data : []; + return syncListCache; } catch { // Database doesn't exist yet or path not found, initialize with empty array - await syncListDb.push(SYNC_LIST_PATH, []); - return []; + await syncListDb.push(SYNC_LIST_PATH, []); // auto-saves with saveOnPush=true + syncListCache = []; + return syncListCache; } }; @@ -30,6 +44,11 @@ export const addItemToSyncList = async (item: SyncItemType) => { if (itemExists) return; syncList.push(item); + + // Update cache + syncListCache = syncList; + + // Write to disk (auto-saves with saveOnPush=true) await syncListDb.push(SYNC_LIST_PATH, syncList); }; @@ -40,10 +59,18 @@ export const removeItemFromSyncList = async (id: number | string) => { (item) => item.id.toString() !== idString, ); + // Update cache + syncListCache = filteredList; + + // Write to disk (auto-saves with saveOnPush=true) await syncListDb.push(SYNC_LIST_PATH, filteredList); }; export const removeAllFromSyncList = async () => { + // Update cache + syncListCache = []; + + // Write to disk (auto-saves with saveOnPush=true) await syncListDb.push(SYNC_LIST_PATH, []); }; @@ -56,6 +83,11 @@ export const updateSyncItem = async ( if (itemIndex !== -1) { syncList[itemIndex] = { ...syncList[itemIndex], ...update }; + + // Update cache + syncListCache = syncList; + + // Write to disk (auto-saves with saveOnPush=true) await syncListDb.push(SYNC_LIST_PATH, syncList); } }; @@ -75,7 +107,7 @@ export const process_sync_list = async (app: Express) => { element.id, ); if (item && ["processing"].includes(item?.status)) continue; - if (item && ["finished", "downloaded"].includes(item?.status)) { + if (item && ["finished", "no_download"].includes(item?.status)) { await app.locals.processingStack.actions.removeItem(element.id); } @@ -127,10 +159,10 @@ export const createCronJob = async (app: Express) => { try { cron.schedule( finalExpression, - () => { + async () => { try { // Read the sync list fresh each time the cron runs - process_sync_list(app); + await process_sync_list(app); } catch (callbackError) { console.error("❌ [SYNC] Error in cron callback:", callbackError); } diff --git a/api/src/services/tiddl.ts b/api/src/services/tiddl.ts index a1c8021..9b232fc 100644 --- a/api/src/services/tiddl.ts +++ b/api/src/services/tiddl.ts @@ -5,6 +5,7 @@ import { CONFIG_PATH, TOKEN_REFRESH_THRESHOLD } from "../../constants"; import { extractFirstLineClean } from "../helpers/ansi_parse"; import { get_tiddl_config } from "../helpers/get_tiddl_config"; import { logs } from "../helpers/logs"; +import { getProcessingPath } from "../processing/jobs"; import { ProcessingItemType, TiddlConfig } from "../types"; // Constants @@ -44,8 +45,10 @@ export function tidalDL(id: string, app: Express, onFinish?: () => void) { const args: string[] = []; args.push("download"); - args.push("--path", `${CONFIG_PATH}/.processing/${item.id}`); - args.push("--scan-path", "/home/app/standalone/library"); + + // Use getProcessingPath() to ensure trailing slashes are handled + const processingPath = getProcessingPath(); + args.push("--path", `${processingPath}/${item.id}`); if (item.type === "mix" && config?.templates?.mix) { args.push("-o", config.templates.mix); @@ -149,7 +152,8 @@ export function tidalDL(id: string, app: Express, onFinish?: () => void) { } item["status"] = - isDownloaded && !hasProcessingError ? "downloaded" : "error"; + !isDownloaded || hasProcessingError ? "error" : item["status"]; + item["loading"] = false; app.locals.processingStack.actions.updateItem(item); if (onFinish) onFinish(); diff --git a/api/src/types.ts b/api/src/types.ts index 59b41a6..82f61b3 100644 --- a/api/src/types.ts +++ b/api/src/types.ts @@ -20,7 +20,7 @@ export type ProcessingItemType = { artist: string; title: string; type: ContentType; - status: "queue" | "finished" | "downloaded" | "processing" | "error"; + status: "queue" | "finished" | "processing" | "error" | "no_download"; quality: QualityType; url: string; loading: boolean; @@ -158,6 +158,7 @@ export interface SettingsResponse { LOCK_QUALITY?: string; ENABLE_TIDAL_PROXY?: string; SYNC_CRON_EXPRESSION?: string; + ENABLE_HISTORY?: string; }; noToken: boolean; tiddl_config?: TiddlConfig; diff --git a/api/yarn.lock b/api/yarn.lock deleted file mode 100644 index 587ad51..0000000 --- a/api/yarn.lock +++ /dev/null @@ -1,780 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@types/apicache@^1.6.8": - version "1.6.8" - resolved "https://registry.yarnpkg.com/@types/apicache/-/apicache-1.6.8.tgz#d2dbb0eefc019407dcb629e5637efb7dd9925dda" - integrity sha512-PDBHDA9p/YEYRLL2KN2kr2UXRAWvZq/WM+GBN94MwPQRZORUnoazd1A1plZXWDOvCvGTgcKriRIIECnRpHhC/g== - dependencies: - "@types/express" "*" - "@types/redis" "^2.8.0" - -"@types/body-parser@*": - version "1.19.5" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.38" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" - integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== - dependencies: - "@types/node" "*" - -"@types/cors@^2.8.19": - version "2.8.19" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" - integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg== - dependencies: - "@types/node" "*" - -"@types/express-http-proxy@^1.6.7": - version "1.6.7" - resolved "https://registry.yarnpkg.com/@types/express-http-proxy/-/express-http-proxy-1.6.7.tgz#7536a8dbaa3bbbfc4c5d3dc519a2295822e6150b" - integrity sha512-CEp9pbnwVI1RzN9PXc+KESMxwUW5r1O7tkWb5h7Wg/YAIf+KulD/zKev8fbbn+Ljt0Yvs8MXwV2W6Id+cKxe2Q== - dependencies: - "@types/express" "*" - -"@types/express-serve-static-core@^5.0.0": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz#812d2871e5eea17fb0bd5214dda7a7b748c0e12a" - integrity sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/send" "*" - -"@types/express@*", "@types/express@^5.0.5": - version "5.0.5" - resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.5.tgz#3ba069177caa34ab96585ca23b3984d752300cdc" - integrity sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^5.0.0" - "@types/serve-static" "^1" - -"@types/http-errors@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== - -"@types/jsonwebtoken@^9.0.10": - version "9.0.10" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz#a7932a47177dcd4283b6146f3bd5c26d82647f09" - integrity sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA== - dependencies: - "@types/ms" "*" - "@types/node" "*" - -"@types/mime@^1": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" - integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== - -"@types/ms@*": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" - integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== - -"@types/node@*", "@types/node@^24.10.0": - version "24.10.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.0.tgz#6b79086b0dfc54e775a34ba8114dcc4e0221f31f" - integrity sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A== - dependencies: - undici-types "~7.16.0" - -"@types/qs@*": - version "6.9.17" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.17.tgz#fc560f60946d0aeff2f914eb41679659d3310e1a" - integrity sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ== - -"@types/range-parser@*": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" - integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== - -"@types/redis@^2.8.0": - version "2.8.32" - resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11" - integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w== - dependencies: - "@types/node" "*" - -"@types/send@*": - version "0.17.4" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -"@types/send@<1": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.6.tgz#aeb5385be62ff58a52cd5459daa509ae91651d25" - integrity sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -"@types/serve-static@^1": - version "1.15.10" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.10.tgz#768169145a778f8f5dfcb6360aead414a3994fee" - integrity sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw== - dependencies: - "@types/http-errors" "*" - "@types/node" "*" - "@types/send" "<1" - -accepts@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" - integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== - dependencies: - mime-types "^3.0.0" - negotiator "^1.0.0" - -apicache@^1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/apicache/-/apicache-1.6.3.tgz#4441aa40f2a48c9dd6f11c73be937d563405355b" - integrity sha512-jS3VfUFpQ9BesFQZcdd1vVYg3ZsO2kGPmTJHqycIYPAQs54r74CRiyj8DuzJpwzLwIfCBYzh4dy9Jt8xYbo27w== - -body-parser@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa" - integrity sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg== - dependencies: - bytes "^3.1.2" - content-type "^1.0.5" - debug "^4.4.0" - http-errors "^2.0.0" - iconv-lite "^0.6.3" - on-finished "^2.4.1" - qs "^6.14.0" - raw-body "^3.0.0" - type-is "^2.0.0" - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== - -bytes@3.1.2, bytes@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bound@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" - integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== - dependencies: - call-bind-apply-helpers "^1.0.2" - get-intrinsic "^1.3.0" - -content-disposition@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" - integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== - dependencies: - safe-buffer "5.2.1" - -content-type@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -cookie-signature@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" - integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== - -cookie@^0.7.1: - version "0.7.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" - integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== - -cors@^2.8.5: - version "2.8.5" - resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== - dependencies: - object-assign "^4" - vary "^1" - -debug@^3.0.1: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^4.3.5, debug@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" - integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== - dependencies: - ms "^2.1.3" - -depd@2.0.0, depd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -dotenv@^17.2.3: - version "17.2.3" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.2.3.tgz#ad995d6997f639b11065f419a22fabf567cdb9a2" - integrity sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w== - -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -encodeurl@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== - -es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-object-atoms@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" - integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== - dependencies: - es-errors "^1.3.0" - -es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -es6-promise@^4.1.1: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -escape-html@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -etag@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -express-http-proxy@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/express-http-proxy/-/express-http-proxy-2.1.2.tgz#ec577539aa23c9354bfa4ae43e8a318923263c55" - integrity sha512-FXcAcs7Nf/hF73Mzh0WDWPwaOlsEUL/fCHW3L4wU6DH79dypsaxmbnAildCLniFs7HQuuvoiR6bjNVUvGuTb5g== - dependencies: - debug "^3.0.1" - es6-promise "^4.1.1" - raw-body "^2.3.0" - -express@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9" - integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA== - dependencies: - accepts "^2.0.0" - body-parser "^2.2.0" - content-disposition "^1.0.0" - content-type "^1.0.5" - cookie "^0.7.1" - cookie-signature "^1.2.1" - debug "^4.4.0" - encodeurl "^2.0.0" - escape-html "^1.0.3" - etag "^1.8.1" - finalhandler "^2.1.0" - fresh "^2.0.0" - http-errors "^2.0.0" - merge-descriptors "^2.0.0" - mime-types "^3.0.0" - on-finished "^2.4.1" - once "^1.4.0" - parseurl "^1.3.3" - proxy-addr "^2.0.7" - qs "^6.14.0" - range-parser "^1.2.1" - router "^2.2.0" - send "^1.1.0" - serve-static "^2.2.0" - statuses "^2.0.1" - type-is "^2.0.1" - vary "^1.1.2" - -finalhandler@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f" - integrity sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q== - dependencies: - debug "^4.4.0" - encodeurl "^2.0.0" - escape-html "^1.0.3" - on-finished "^2.4.1" - parseurl "^1.3.3" - statuses "^2.0.1" - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" - integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -http-errors@2.0.0, http-errors@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@0.6.3, iconv-lite@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -inherits@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-promise@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" - integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== - -jsonwebtoken@^9.0.2: - version "9.0.2" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" - integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^7.5.4" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -media-typer@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" - integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== - -merge-descriptors@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" - integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== - -mime-db@^1.54.0: - version "1.54.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" - integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== - -mime-types@^3.0.0, mime-types@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" - integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== - dependencies: - mime-db "^1.54.0" - -ms@^2.1.1, ms@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -negotiator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" - integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== - -node-cron@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-4.2.1.tgz#6979be4aee4702f06322d21220df8de252c8e265" - integrity sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg== - -object-assign@^4: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.13.3: - version "1.13.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" - integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== - -on-finished@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -parseurl@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -path-to-regexp@^8.0.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" - integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== - -proxy-addr@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -qs@^6.14.0: - version "6.14.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" - integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== - dependencies: - side-channel "^1.1.0" - -range-parser@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@^2.3.0: - version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -raw-body@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" - integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.6.3" - unpipe "1.0.0" - -router@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" - integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== - dependencies: - debug "^4.4.0" - depd "^2.0.0" - is-promise "^4.0.0" - parseurl "^1.3.3" - path-to-regexp "^8.0.0" - -safe-buffer@5.2.1, safe-buffer@^5.0.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -semver@^7.5.4: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - -send@^1.1.0, send@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212" - integrity sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw== - dependencies: - debug "^4.3.5" - encodeurl "^2.0.0" - escape-html "^1.0.3" - etag "^1.8.1" - fresh "^2.0.0" - http-errors "^2.0.0" - mime-types "^3.0.1" - ms "^2.1.3" - on-finished "^2.4.1" - range-parser "^1.2.1" - statuses "^2.0.1" - -serve-static@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9" - integrity sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ== - dependencies: - encodeurl "^2.0.0" - escape-html "^1.0.3" - parseurl "^1.3.3" - send "^1.2.0" - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -side-channel-list@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" - integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - -side-channel-map@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" - integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - -side-channel-weakmap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" - integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - side-channel-map "^1.0.1" - -side-channel@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" - integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - side-channel-list "^1.0.0" - side-channel-map "^1.0.1" - side-channel-weakmap "^1.0.2" - -statuses@2.0.1, statuses@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -toml@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" - integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== - -type-is@^2.0.0, type-is@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" - integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== - dependencies: - content-type "^1.0.5" - media-typer "^1.1.0" - mime-types "^3.0.0" - -undici-types@~7.16.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" - integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== - -unpipe@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -vary@^1, vary@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== diff --git a/app/src/components/Buttons/DownloadButton.tsx b/app/src/components/Buttons/DownloadButton.tsx index 1d52306..83efcd9 100644 --- a/app/src/components/Buttons/DownloadButton.tsx +++ b/app/src/components/Buttons/DownloadButton.tsx @@ -1,10 +1,13 @@ import { useMemo } from "react"; +import { Block } from "@mui/icons-material"; import AccessTimeIcon from "@mui/icons-material/AccessTime"; import CheckIcon from "@mui/icons-material/Check"; import DownloadIcon from "@mui/icons-material/Download"; import WarningIcon from "@mui/icons-material/Warning"; import { Button, CircularProgress } from "@mui/material"; import { TIDAL_ALBUM_URL, TIDAL_MIX_URL } from "src/contants"; +import { useConfigProvider } from "src/provider/ConfigProvider"; +import { useHistoryProvider } from "src/provider/HistoryProvider"; import { useProcessingProvider } from "../../provider/ProcessingProvider"; import { @@ -29,18 +32,47 @@ export const DownloadButton = ({ force?: boolean; }) => { const { processingList, actions } = useProcessingProvider(); + const { config } = useConfigProvider(); + const { + history, + actions: { addToHistory }, + } = useHistoryProvider(); const status = useMemo(() => { - if (!processingList || processingList?.length === 0 || !id) { - return undefined; - } - - const index = processingList.findIndex( + // Check if item is currently in the processing queue + const processingItem = processingList?.find( (x) => x.id?.toString() === id?.toString() && x.type === type, ); - return index > -1 ? processingList?.[index]?.status : undefined; - }, [processingList, id, type]); + if (processingItem) { + const itemStatus = processingItem.status; + + // When item finishes and history is enabled, add to local history + if (itemStatus === "finished" && config?.ENABLE_HISTORY === "true") { + addToHistory(id.toString()); + } + + return itemStatus; + } + + // If history is enabled, check local history + if (config?.ENABLE_HISTORY === "true" && !force) { + const isInHistory = history?.some( + (x) => x?.toString() === id?.toString(), + ); + return isInHistory ? "finished" : undefined; + } + + return undefined; + }, [ + processingList, + force, + config?.ENABLE_HISTORY, + history, + id, + type, + addToHistory, + ]); const downloadItem = async () => { if (force) await actions.removeItem(id); @@ -74,6 +106,7 @@ export const DownloadButton = ({ + {!tokenMissing && ( - {!tokenMissing && ( - - )} - + )} + {config?.ENABLE_HISTORY && ( + + )} {showEditor ? : } diff --git a/app/src/components/Processing/ProcessingItem.tsx b/app/src/components/Processing/ProcessingItem.tsx index 75e5451..86fed48 100644 --- a/app/src/components/Processing/ProcessingItem.tsx +++ b/app/src/components/Processing/ProcessingItem.tsx @@ -1,5 +1,6 @@ import { Link } from "react-router-dom"; import styled from "@emotion/styled"; +import { Block } from "@mui/icons-material"; import AccessTimeIcon from "@mui/icons-material/AccessTime"; import CheckIcon from "@mui/icons-material/Check"; import ClearIcon from "@mui/icons-material/Clear"; @@ -17,7 +18,7 @@ import { ProcessingItemType } from "src/types"; import { DialogTerminal } from "../Dialog/DialogTerminal"; export const ProcessingItem = ({ item }: { item: ProcessingItemType }) => { - const step = item?.status; + const status = item?.status; const { actions } = useProcessingProvider(); if (!item?.status) return null; @@ -33,16 +34,18 @@ export const ProcessingItem = ({ item }: { item: ProcessingItemType }) => { actions.removeItem(item.id)}> - {step === "finished" ? ( + {status === "finished" ? ( - ) : step === "error" ? ( + ) : status === "error" ? ( - ) : step === "queue" ? ( + ) : status === "queue" ? ( + ) : status === "no_download" ? ( + ) : ( )} - {step === "error" ? ( + {status === "error" ? ( <>