mirror of
https://github.com/cstaelen/tidarr.git
synced 2026-10-19 01:30:57 -05:00
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 <unthoiz@protonmail.com> --------- Co-authored-by: unthoiz <unthoiz@protonmail.com>
This commit is contained in:
1 parent
56405d2bce
commit
7ac3d4bfc7
45 files changed
+1387
-3080
No files matched your search
@@ -22,3 +22,5 @@ APPRISE_API_TAG=
|
||||
M3U_BASEPATH_FILE=
|
||||
SYNC_CRON_EXPRESSION=
|
||||
PUSH_OVER_URL=
|
||||
NO_DOWNLOAD=
|
||||
ENABLE_HISTORY=
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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.
|
||||
|
||||
+2
-3
@@ -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)
|
||||
|
||||
+12
-2
@@ -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<string, Response[]>();
|
||||
app.locals.history = [];
|
||||
app.locals.historySet = new Set<string>();
|
||||
|
||||
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) => {
|
||||
|
||||
+4
-4
@@ -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"
|
||||
|
||||
@@ -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<string, ProcessingItemType> | null = null;
|
||||
|
||||
/**
|
||||
* Load queue from file (only on first call, then uses cache)
|
||||
*/
|
||||
export async function loadQueueFromFile(): Promise<ProcessingItemType[]> {
|
||||
// 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);
|
||||
};
|
||||
@@ -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<string, Response[]> =
|
||||
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<string, ProcessingItemType> = new Map(); // O(1) item lookups
|
||||
const outputs: Map<string, string[]> = 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<void> {
|
||||
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<string | undefined> {
|
||||
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);
|
||||
|
||||
|
||||
+32
-12
@@ -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<void> {
|
||||
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)) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
@@ -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<void> {
|
||||
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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { getAppInstance } from "../app-instance";
|
||||
import { historyDb } from "../services/db-json";
|
||||
|
||||
const QUEUE_PATH = "/";
|
||||
|
||||
export async function loadHistoryFromFile(): Promise<string[]> {
|
||||
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.");
|
||||
}
|
||||
@@ -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<SyncItemType[]> => {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
-780
@@ -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==
|
||||
@@ -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 = ({
|
||||
<Button
|
||||
variant="outlined"
|
||||
data-testid="btn-dl"
|
||||
color={status === "finished" ? "success" : "primary"}
|
||||
endIcon={
|
||||
status === "queue" ? (
|
||||
<AccessTimeIcon />
|
||||
@@ -83,11 +116,13 @@ export const DownloadButton = ({
|
||||
<CheckIcon color="success" />
|
||||
) : status === "processing" ? (
|
||||
<CircularProgress size={20} />
|
||||
) : status === "no_download" ? (
|
||||
<Block color="disabled" />
|
||||
) : (
|
||||
<DownloadIcon />
|
||||
)
|
||||
}
|
||||
disabled={!!status && !force}
|
||||
disabled={!!status && status !== "finished" && !force}
|
||||
onClick={() => downloadItem()}
|
||||
size="small"
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ReactNode, useEffect, useState } from "react";
|
||||
import styled from "@emotion/styled";
|
||||
import AlbumIcon from "@mui/icons-material/Album";
|
||||
import { AppBar, CircularProgress } from "@mui/material";
|
||||
import { HistoryProvider } from "src/provider/HistoryProvider";
|
||||
import { SearchProvider } from "src/provider/SearchProvider";
|
||||
import { SyncProvider } from "src/provider/SyncProvider";
|
||||
|
||||
@@ -46,32 +47,34 @@ function MainLayout({ children }: { children: ReactNode }) {
|
||||
style={{ paddingBottom: "1rem" }}
|
||||
>
|
||||
<SearchProvider>
|
||||
<ProcessingProvider>
|
||||
<SyncProvider>
|
||||
<Content>
|
||||
<AppBar id="app-bar" position="sticky">
|
||||
<HeaderSearch />
|
||||
</AppBar>
|
||||
{!appLoaded || !config ? (
|
||||
<Loader>
|
||||
<Title>
|
||||
<AlbumIcon />
|
||||
Tidarr
|
||||
</Title>
|
||||
<CircularProgress />
|
||||
</Loader>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Content>
|
||||
<ProcessingList />
|
||||
<DialogToken />
|
||||
<DialogNoAPI />
|
||||
<DialogConfigError />
|
||||
<DialogConfig />
|
||||
<DialogUpdateWarning />
|
||||
</SyncProvider>
|
||||
</ProcessingProvider>
|
||||
<HistoryProvider>
|
||||
<ProcessingProvider>
|
||||
<SyncProvider>
|
||||
<Content>
|
||||
<AppBar id="app-bar" position="sticky">
|
||||
<HeaderSearch />
|
||||
</AppBar>
|
||||
{!appLoaded || !config ? (
|
||||
<Loader>
|
||||
<Title>
|
||||
<AlbumIcon />
|
||||
Tidarr
|
||||
</Title>
|
||||
<CircularProgress />
|
||||
</Loader>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Content>
|
||||
<ProcessingList />
|
||||
<DialogToken />
|
||||
<DialogNoAPI />
|
||||
<DialogConfigError />
|
||||
<DialogConfig />
|
||||
<DialogUpdateWarning />
|
||||
</SyncProvider>
|
||||
</ProcessingProvider>
|
||||
</HistoryProvider>
|
||||
</SearchProvider>
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { EditDocument, KeyOff, Warning } from "@mui/icons-material";
|
||||
import {
|
||||
Cached,
|
||||
Check,
|
||||
EditDocument,
|
||||
KeyOff,
|
||||
Warning,
|
||||
} from "@mui/icons-material";
|
||||
import { Alert, Box, Button } from "@mui/material";
|
||||
import { useApiFetcher } from "src/provider/ApiFetcherProvider";
|
||||
import { useConfigProvider } from "src/provider/ConfigProvider";
|
||||
import { useHistoryProvider } from "src/provider/HistoryProvider";
|
||||
|
||||
import TiddlConfigEdit from "./tidal/TiddlConfigEdit";
|
||||
import TiddlConfigList from "./tidal/TiddlConfigList";
|
||||
@@ -10,14 +17,21 @@ import TiddlConfigList from "./tidal/TiddlConfigList";
|
||||
export default function TidalPanel() {
|
||||
const { tokenMissing, actions } = useConfigProvider();
|
||||
const [showEditor, setShowEditor] = useState<boolean>();
|
||||
const [historyFlushed, setHistoryFlushed] = useState<boolean>();
|
||||
|
||||
const {
|
||||
config,
|
||||
actions: { checkAPI },
|
||||
} = useConfigProvider();
|
||||
|
||||
const {
|
||||
actions: { delete_token },
|
||||
} = useApiFetcher();
|
||||
|
||||
const {
|
||||
actions: { emptyHistory },
|
||||
} = useHistoryProvider();
|
||||
|
||||
return (
|
||||
<>
|
||||
{tokenMissing && (
|
||||
@@ -32,34 +46,52 @@ export default function TidalPanel() {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box component="h2" display="flex" flexWrap="wrap">
|
||||
<Box flex="1 1 0" component="span">
|
||||
Tiddl configuration
|
||||
</Box>
|
||||
<Box flex="0 0 auto" component="span" display="flex" gap={2}>
|
||||
<Box component="h2">Tiddl configuration</Box>
|
||||
<Box
|
||||
flex="0 0 auto"
|
||||
component="span"
|
||||
display="flex"
|
||||
gap={2}
|
||||
mb={3}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<EditDocument />}
|
||||
onClick={() => setShowEditor(!showEditor)}
|
||||
>
|
||||
Toggle editor
|
||||
</Button>
|
||||
{!tokenMissing && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<EditDocument />}
|
||||
onClick={() => setShowEditor(!showEditor)}
|
||||
color="warning"
|
||||
startIcon={<KeyOff />}
|
||||
onClick={async () => {
|
||||
actions.toggleModal(false);
|
||||
await delete_token();
|
||||
checkAPI();
|
||||
}}
|
||||
>
|
||||
Toggle editor
|
||||
Revoke Tidal token
|
||||
</Button>
|
||||
{!tokenMissing && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="warning"
|
||||
startIcon={<KeyOff />}
|
||||
onClick={async () => {
|
||||
actions.toggleModal(false);
|
||||
await delete_token();
|
||||
checkAPI();
|
||||
}}
|
||||
>
|
||||
Revoke Tidal token
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{config?.ENABLE_HISTORY && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={!!historyFlushed}
|
||||
startIcon={historyFlushed ? <Check /> : <Cached />}
|
||||
onClick={async () => {
|
||||
setHistoryFlushed(true);
|
||||
await emptyHistory();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
Empty history
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
{showEditor ? <TiddlConfigEdit /> : <TiddlConfigList />}
|
||||
</>
|
||||
|
||||
@@ -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 }) => {
|
||||
<RemoveButton onClick={() => actions.removeItem(item.id)}>
|
||||
<ClearIcon />
|
||||
</RemoveButton>
|
||||
{step === "finished" ? (
|
||||
{status === "finished" ? (
|
||||
<CheckIcon color="success" />
|
||||
) : step === "error" ? (
|
||||
) : status === "error" ? (
|
||||
<WarningIcon color="error" />
|
||||
) : step === "queue" ? (
|
||||
) : status === "queue" ? (
|
||||
<AccessTimeIcon />
|
||||
) : status === "no_download" ? (
|
||||
<Block color="disabled" />
|
||||
) : (
|
||||
<CircularProgress size={20} />
|
||||
)}
|
||||
{step === "error" ? (
|
||||
{status === "error" ? (
|
||||
<>
|
||||
|
||||
<Button variant="outlined" size="small" onClick={() => run()}>
|
||||
@@ -51,8 +54,10 @@ export const ProcessingItem = ({ item }: { item: ProcessingItemType }) => {
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{item.status !== "queue" && <DialogTerminal item={item} />}
|
||||
{step}
|
||||
{item.status !== "queue" && item.status !== "no_download" && (
|
||||
<DialogTerminal item={item} />
|
||||
)}
|
||||
{status}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell scope="row">
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { blue } from "@mui/material/colors";
|
||||
import { useApiFetcher } from "src/provider/ApiFetcherProvider";
|
||||
@@ -24,6 +25,7 @@ import { ProcessingPauseButton } from "./ProcessingPauseButton";
|
||||
|
||||
export const ProcessingList = () => {
|
||||
const { processingList, isPaused } = useProcessingProvider();
|
||||
const { config } = useConfigProvider();
|
||||
const { actions } = useConfigProvider();
|
||||
const { actions: apiActions } = useApiFetcher();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -77,7 +79,7 @@ export const ProcessingList = () => {
|
||||
case hasError:
|
||||
buttonColor = "error";
|
||||
break;
|
||||
case isLoading:
|
||||
case isLoading || config?.NO_DOWNLOAD === "true":
|
||||
buttonColor = "primary";
|
||||
break;
|
||||
default:
|
||||
@@ -173,7 +175,15 @@ export const ProcessingList = () => {
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<ProcessingPauseButton />
|
||||
<div>
|
||||
{config?.NO_DOWNLOAD !== "true" ? (
|
||||
<ProcessingPauseButton />
|
||||
) : (
|
||||
<Typography color="warning">
|
||||
No download mode is active
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<Box display="flex" gap={2}>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -183,7 +193,7 @@ export const ProcessingList = () => {
|
||||
disabled={
|
||||
isRemoving ||
|
||||
!processingList?.some((item) =>
|
||||
["finished", "downloaded", "error"].includes(item.status),
|
||||
["finished", "error"].includes(item.status),
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -63,6 +63,8 @@ type ApiFetcherContextType = {
|
||||
pause_queue: () => Promise<void>;
|
||||
resume_queue: () => Promise<void>;
|
||||
get_queue_status: () => Promise<{ isPaused: boolean } | undefined>;
|
||||
get_list_history: () => Promise<string[] | undefined>;
|
||||
flush_history: () => Promise<unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -210,17 +212,11 @@ export function APIFetcherProvider({ children }: { children: ReactNode }) {
|
||||
async function remove_all() {
|
||||
return await queryExpressJS(`${apiUrl}/remove-all`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
async function remove_finished() {
|
||||
return await queryExpressJS(`${apiUrl}/remove-finished`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,10 +225,10 @@ export function APIFetcherProvider({ children }: { children: ReactNode }) {
|
||||
async function auth(body?: string): Promise<AuthType | undefined> {
|
||||
return await queryExpressJS<AuthType>(`${apiUrl}/auth`, {
|
||||
method: "POST",
|
||||
body: body,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -432,6 +428,26 @@ export function APIFetcherProvider({ children }: { children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Queue History
|
||||
|
||||
async function get_list_history() {
|
||||
return await queryExpressJS<string[]>(`${apiUrl}/history/list`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function flush_history() {
|
||||
return await queryExpressJS(`${apiUrl}/history/list`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const value = {
|
||||
apiUrl,
|
||||
error: {
|
||||
@@ -462,6 +478,8 @@ export function APIFetcherProvider({ children }: { children: ReactNode }) {
|
||||
pause_queue,
|
||||
resume_queue,
|
||||
get_queue_status,
|
||||
get_list_history,
|
||||
flush_history,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { ReactNode, useContext, useEffect, useState } from "react";
|
||||
|
||||
import { useApiFetcher } from "./ApiFetcherProvider";
|
||||
import { useConfigProvider } from "./ConfigProvider";
|
||||
|
||||
type HistoryContextType = {
|
||||
history: string[];
|
||||
actions: {
|
||||
addToHistory: (id: string) => void;
|
||||
emptyHistory: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
const HistoryContext = React.createContext<HistoryContextType>(
|
||||
{} as HistoryContextType,
|
||||
);
|
||||
|
||||
export function HistoryProvider({ children }: { children: ReactNode }) {
|
||||
const [history, setHistory] = useState<string[]>([]);
|
||||
const { config } = useConfigProvider();
|
||||
|
||||
const {
|
||||
actions: { get_list_history, flush_history },
|
||||
} = useApiFetcher();
|
||||
|
||||
const addToHistory = (id: string) => {
|
||||
const formatId = id.toString();
|
||||
if (history.includes(formatId)) return;
|
||||
setHistory([...history, formatId]);
|
||||
};
|
||||
|
||||
const emptyHistory = async () => {
|
||||
await flush_history();
|
||||
setHistory([]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.ENABLE_HISTORY !== "true") return;
|
||||
async function get_list() {
|
||||
const data = await get_list_history();
|
||||
setHistory(data || []);
|
||||
}
|
||||
|
||||
get_list();
|
||||
}, [config, get_list_history]);
|
||||
|
||||
const value = {
|
||||
history,
|
||||
actions: {
|
||||
addToHistory,
|
||||
emptyHistory,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<HistoryContext.Provider value={value}>{children}</HistoryContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useHistoryProvider = () => {
|
||||
return useContext(HistoryContext);
|
||||
};
|
||||
+7
-1
@@ -353,7 +353,13 @@ export type ProcessingItemType = {
|
||||
title: string;
|
||||
quality: QualityType;
|
||||
type: ContentType;
|
||||
status: "queue" | "finished" | "beet" | "processing" | "error";
|
||||
status:
|
||||
| "queue"
|
||||
| "finished"
|
||||
| "beet"
|
||||
| "processing"
|
||||
| "error"
|
||||
| "no_download";
|
||||
url: string;
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
|
||||
-1461
File diff suppressed because it is too large.
Load diff
+5
-3
@@ -9,9 +9,9 @@ services:
|
||||
- 8484:8484
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- ./docker/mnt/config:/home/app/standalone/shared
|
||||
- ./docker/mnt/library:/home/app/standalone/library
|
||||
- .:/home/app/standalone # only used in dev environment
|
||||
- ./docker/mnt/config:/shared
|
||||
- ./docker/mnt/library:/music
|
||||
- .:/tidarr # only used in dev environment
|
||||
restart: 'unless-stopped'
|
||||
environment:
|
||||
- PUID=
|
||||
@@ -34,4 +34,6 @@ services:
|
||||
- PUSH_OVER_URL=
|
||||
- M3U_BASEPATH_FILE=
|
||||
- SYNC_CRON_EXPRESSION=
|
||||
- NO_DOWNLOAD=
|
||||
- ENABLE_HISTORY=
|
||||
|
||||
+14
-14
@@ -7,11 +7,11 @@ ARG TARGETPLATFORM
|
||||
|
||||
FROM python:3.13-alpine3.21 AS base
|
||||
|
||||
WORKDIR /home/app/standalone
|
||||
WORKDIR /tidarr
|
||||
|
||||
ENV SHELL=bash \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
HOME=/home/app/standalone/shared
|
||||
HOME=/shared
|
||||
|
||||
# Install system dependencies (shared by dev and prod)
|
||||
RUN apk update && apk upgrade && \
|
||||
@@ -36,8 +36,8 @@ RUN apk add --no-cache --virtual .build-deps \
|
||||
apk del .build-deps
|
||||
|
||||
# Create beets directory structure
|
||||
RUN mkdir -p /home/app/standalone/shared/beets && \
|
||||
touch /home/app/standalone/shared/beets/beets-library.blb
|
||||
RUN mkdir -p /shared/beets && \
|
||||
touch /shared/beets/beets-library.blb
|
||||
|
||||
###############################################################
|
||||
## DEVELOPMENT IMAGE #
|
||||
@@ -62,7 +62,7 @@ EXPOSE 8484 3000
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD curl -f http://localhost:8484/api/is-auth-active || exit 1
|
||||
|
||||
ENTRYPOINT ["sh", "/home/app/standalone/docker/entrypoint.sh"]
|
||||
ENTRYPOINT ["sh", "/tidarr/docker/entrypoint.sh"]
|
||||
|
||||
###############################################################
|
||||
## BUILD APP (production only) #
|
||||
@@ -70,7 +70,7 @@ ENTRYPOINT ["sh", "/home/app/standalone/docker/entrypoint.sh"]
|
||||
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/linuxserver/baseimage-alpine:3.21 AS app_builder
|
||||
|
||||
WORKDIR /home/app/standalone
|
||||
WORKDIR /tidarr
|
||||
|
||||
ENV SHELL=bash
|
||||
ARG NODE_ENV
|
||||
@@ -101,12 +101,12 @@ ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
# Copy built artifacts from builder stage
|
||||
COPY --from=app_builder /home/app/standalone/api/dist /home/app/standalone/api/dist
|
||||
COPY --from=app_builder /home/app/standalone/api/scripts /home/app/standalone/api/scripts
|
||||
COPY --from=app_builder /home/app/standalone/app/dist /home/app/standalone/app/build
|
||||
COPY --from=app_builder /home/app/standalone/settings /home/app/standalone/settings
|
||||
COPY --from=app_builder /home/app/standalone/.env /home/app/standalone/.env
|
||||
COPY --from=app_builder /home/app/standalone/docker/entrypoint.sh /home/app/standalone/docker/entrypoint.sh
|
||||
COPY --from=app_builder /tidarr/api/dist /tidarr/api/dist
|
||||
COPY --from=app_builder /tidarr/api/scripts /tidarr/api/scripts
|
||||
COPY --from=app_builder /tidarr/app/dist /tidarr/app/build
|
||||
COPY --from=app_builder /tidarr/settings /tidarr/settings
|
||||
COPY --from=app_builder /tidarr/.env /tidarr/.env
|
||||
COPY --from=app_builder /tidarr/docker/entrypoint.sh /tidarr/docker/entrypoint.sh
|
||||
|
||||
# Copy workspace configuration and API package files
|
||||
COPY package.json yarn.lock* ./
|
||||
@@ -114,7 +114,7 @@ COPY api/package.json ./api/package.json
|
||||
|
||||
# Install production dependencies and clean up aggressively
|
||||
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn/v6,sharing=locked \
|
||||
mkdir -p /home/app/standalone/shared/.processing && \
|
||||
mkdir -p /shared/.processing && \
|
||||
yarn install --frozen-lockfile --production && \
|
||||
npm uninstall -g npm && \
|
||||
rm -rf /root/.npm /tmp/* /var/tmp/* && \
|
||||
@@ -126,4 +126,4 @@ EXPOSE 8484
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8484/api/is-auth-active || exit 1
|
||||
|
||||
ENTRYPOINT ["sh", "/home/app/standalone/docker/entrypoint.sh"]
|
||||
ENTRYPOINT ["sh", "/tidarr/docker/entrypoint.sh"]
|
||||
+12
-12
@@ -42,31 +42,31 @@ if [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||
echo "🔑 [TIDARR] Using UMASK=$EFFECTIVE_UMASK"
|
||||
|
||||
# Create required directories if they don't exist
|
||||
mkdir -p /home/app/standalone/shared/.tiddl 2>/dev/null || true
|
||||
mkdir -p /home/app/standalone/shared/beets 2>/dev/null || true
|
||||
mkdir -p /shared/.tiddl 2>/dev/null || true
|
||||
mkdir -p /shared/beets 2>/dev/null || true
|
||||
|
||||
# Change ownership only of specific Tidarr-related files/directories
|
||||
# Avoid traversing cache directories (.cache, .yarn, .pki, node_modules)
|
||||
chown $PUID:$PGID /home/app/standalone/shared 2>/dev/null || true
|
||||
chown -R $PUID:$PGID /home/app/standalone/shared/.tiddl 2>/dev/null || true
|
||||
chown -R $PUID:$PGID /home/app/standalone/shared/beets 2>/dev/null || true
|
||||
chown $PUID:$PGID /home/app/standalone/shared/*.json 2>/dev/null || true
|
||||
chown $PUID:$PGID /home/app/standalone/shared/*.css 2>/dev/null || true
|
||||
chown $PUID:$PGID /home/app/standalone/shared/*.yml 2>/dev/null || true
|
||||
chown $PUID:$PGID /shared 2>/dev/null || true
|
||||
chown -R $PUID:$PGID /shared/.tiddl 2>/dev/null || true
|
||||
chown -R $PUID:$PGID /shared/beets 2>/dev/null || true
|
||||
chown $PUID:$PGID /shared/*.json 2>/dev/null || true
|
||||
chown $PUID:$PGID /shared/*.css 2>/dev/null || true
|
||||
chown $PUID:$PGID /shared/*.yml 2>/dev/null || true
|
||||
|
||||
# In production, allow the user to write custom.css in the app/build directory
|
||||
if [ "$ENVIRONMENT" != "development" ]; then
|
||||
chown -R $PUID:$PGID /home/app/standalone/app/build 2>/dev/null || true
|
||||
chown -R $PUID:$PGID /tidarr/app/build 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Run as specified UID/GID using su-exec
|
||||
# Set HOME to /home/app/standalone/shared for tiddl config access
|
||||
# Set HOME to /shared for tiddl config access
|
||||
# Export UMASK as environment variable so Node.js can access it for setPermissions()
|
||||
# Set umask for any processes that respect it (tiddl/Python ignores shell umask)
|
||||
if [ "$ENVIRONMENT" = "development" ]; then
|
||||
exec su-exec $PUID:$PGID sh -c "export HOME=/home/app/standalone/shared && export UMASK=$EFFECTIVE_UMASK && umask $EFFECTIVE_UMASK && exec sh -c 'yarn install && yarn dev'"
|
||||
exec su-exec $PUID:$PGID sh -c "export HOME=/shared && export UMASK=$EFFECTIVE_UMASK && umask $EFFECTIVE_UMASK && exec sh -c 'yarn install && yarn dev'"
|
||||
else
|
||||
exec su-exec $PUID:$PGID sh -c "export HOME=/home/app/standalone/shared && export UMASK=$EFFECTIVE_UMASK && umask $EFFECTIVE_UMASK && exec yarn --cwd ./api prod"
|
||||
exec su-exec $PUID:$PGID sh -c "export HOME=/shared && export UMASK=$EFFECTIVE_UMASK && umask $EFFECTIVE_UMASK && exec yarn --cwd ./api prod"
|
||||
fi
|
||||
else
|
||||
# Run as root (default)
|
||||
|
||||
@@ -7,6 +7,7 @@ This documentation describes how to use the Tidarr REST API with curl to automat
|
||||
- [Basic Configuration](#basic-configuration)
|
||||
- [Authentication](#authentication)
|
||||
- [Download Endpoints](#download-endpoints)
|
||||
- [History Endpoints](#history-endpoints)
|
||||
- [Configuration Endpoints](#configuration-endpoints)
|
||||
- [Synchronization Endpoints](#synchronization-endpoints)
|
||||
- [Custom CSS Endpoints](#custom-css-endpoints)
|
||||
@@ -315,6 +316,52 @@ curl http://localhost:8484/api/queue/status \
|
||||
|
||||
---
|
||||
|
||||
## History Endpoints
|
||||
|
||||
The history feature tracks downloaded items. It can be enabled by setting `ENABLE_HISTORY=true` in your Docker configuration.
|
||||
|
||||
### Get download history
|
||||
|
||||
Returns the list of all downloaded items.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8484/api/history/list \
|
||||
-H "Authorization: Bearer $TIDARR_TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "251082404",
|
||||
"type": "album",
|
||||
"url": "https://listen.tidal.com/album/251082404",
|
||||
"title": "Album Name",
|
||||
"downloadedAt": 1234567890
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Note:** History is only available when `ENABLE_HISTORY=true` is set in your environment variables.
|
||||
|
||||
### Clear download history
|
||||
|
||||
Removes all items from the download history.
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8484/api/history/list \
|
||||
-H "Authorization: Bearer $TIDARR_TOKEN"
|
||||
```
|
||||
|
||||
**Response:** Status `204 No Content`
|
||||
|
||||
**Behavior:**
|
||||
- Deletes the entire download history
|
||||
- Does not affect the current download queue
|
||||
- Only clears the history tracking, does not delete downloaded files
|
||||
|
||||
---
|
||||
|
||||
## Configuration Endpoints
|
||||
|
||||
### Get Tidarr configuration
|
||||
@@ -424,7 +471,7 @@ curl -X POST http://localhost:8484/api/sync/trigger \
|
||||
|
||||
**Response:** Status `202 Accepted`
|
||||
|
||||
This endpoint will immediately queue all items from the watch list for download. Items already in the queue with status "processing" will be skipped. Items with status "finished" or "downloaded" will be removed from the queue and re-added.
|
||||
This endpoint will immediately queue all items from the watch list for download. Items already in the queue with status "processing" will be skipped. Items with status "finished" will be removed from the queue and re-added.
|
||||
|
||||
**Note:** The synchronization cron is configured via the `SYNC_CRON_EXPRESSION` environment variable (default: `0 3 * * *` = every day at 3 AM).
|
||||
|
||||
|
||||
@@ -244,9 +244,8 @@ services:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- ./shared:/app/shared
|
||||
- ./library:/library
|
||||
- ./incomplete:/incomplete
|
||||
- ./shared:/shared
|
||||
- ./library:/music
|
||||
networks:
|
||||
- proxy_network
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ ADMIN_PASSWORD=
|
||||
LOCK_QUALITY=
|
||||
VERSION=0.0.0-testing
|
||||
|
||||
# Active download history
|
||||
ENABLE_HISTORY=true
|
||||
|
||||
# User/Group IDs (use host user for permissions)
|
||||
PUID=1000
|
||||
PGID=1000
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Enable Tidarr authentication
|
||||
NO_DOWNLOAD=true
|
||||
@@ -83,6 +83,8 @@ test("Tidarr config : Should see configuration dialog", async ({ page }) => {
|
||||
["LOCK_QUALITY", ""],
|
||||
["ENABLE_TIDAL_PROXY", "true"],
|
||||
["SYNC_CRON_EXPRESSION", "0 3 * * *"],
|
||||
["NO_DOWNLOAD", ""],
|
||||
["ENABLE_HISTORY", "true"],
|
||||
];
|
||||
const tableAPIRows = await page
|
||||
.locator("#alert-dialog-description div")
|
||||
|
||||
@@ -118,7 +118,7 @@ test("Edit Config: Should display error dialog when config has errors", async ({
|
||||
await mockConfigAPI(page, {
|
||||
configErrors: [
|
||||
"Config file error: Cannot redefine existing key 'templates'.",
|
||||
"Config path: /home/app/standalone/shared/.tiddl/config.toml",
|
||||
"Config path: /shared/.tiddl/config.toml",
|
||||
],
|
||||
});
|
||||
|
||||
@@ -145,9 +145,7 @@ test("Edit Config: Should display error dialog when config has errors", async ({
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
"Config path: /home/app/standalone/shared/.tiddl/config.toml",
|
||||
),
|
||||
page.getByText("Config path: /shared/.tiddl/config.toml"),
|
||||
).toBeVisible();
|
||||
|
||||
// Close the dialog
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "../test-isolation";
|
||||
|
||||
import { mockConfigAPI } from "./utils/mock";
|
||||
import { runSearch } from "./utils/search";
|
||||
|
||||
test("History: Should see and clear all history when flush is called", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Enable history feature
|
||||
await mockConfigAPI(page, { ENABLE_HISTORY: "true" });
|
||||
|
||||
let historyDeleteCalled = false;
|
||||
const historyAfterDelete: string[] = [];
|
||||
|
||||
// Mock initial history list with items
|
||||
await page.route("**/history/list", async (route) => {
|
||||
if (route.request().method() === "DELETE") {
|
||||
historyDeleteCalled = true;
|
||||
await route.fulfill({ status: 204 });
|
||||
return;
|
||||
}
|
||||
|
||||
// GET request - return history based on whether delete was called
|
||||
const mockHistory = historyDeleteCalled
|
||||
? historyAfterDelete
|
||||
: ["77610756", "77610844"];
|
||||
await route.fulfill({ status: 200, json: mockHistory });
|
||||
});
|
||||
|
||||
await runSearch("Nirvana", page);
|
||||
|
||||
await expect(page.locator(".MuiButton-colorSuccess")).toHaveCount(7);
|
||||
|
||||
// Navigate to settings page (where flush history button might be)
|
||||
await page.getByRole("button", { name: "Tidarr settings" }).click();
|
||||
|
||||
// Wait for settings to load
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByRole("tab", { name: "Tidal configuration" }).click();
|
||||
|
||||
// Look for a button or action to clear history
|
||||
// Note: This assumes there's a UI element for clearing history in settings
|
||||
// Adjust the selector based on your actual implementation
|
||||
const clearHistoryButton = page.getByRole("button", {
|
||||
name: /Empty history/i,
|
||||
});
|
||||
|
||||
// If the button exists, click it
|
||||
await clearHistoryButton.isVisible();
|
||||
await clearHistoryButton.click();
|
||||
|
||||
// Wait for the API call
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify the DELETE API was called
|
||||
expect(historyDeleteCalled).toBe(true);
|
||||
|
||||
await expect(page.locator(".MuiButton-colorSuccess")).toHaveCount(0);
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "../test-isolation";
|
||||
|
||||
import { runSearch } from "./utils/search";
|
||||
|
||||
test.use({ envFile: ".env.e2e.nodownload" });
|
||||
|
||||
test("NO_DOWNLOAD: Should set items to 'no_download' status when NO_DOWNLOAD is enabled", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Add an item to the queue
|
||||
await runSearch("Nirvana", page);
|
||||
await page.getByRole("tab", { name: "Albums" }).first().click();
|
||||
await page
|
||||
.locator("div:nth-child(2) > .MuiPaper-root > div:nth-child(2)")
|
||||
.getByTestId("btn-dl")
|
||||
.click();
|
||||
|
||||
// Open processing list
|
||||
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
|
||||
await page.locator("button.MuiFab-circular").click();
|
||||
|
||||
// Verify item is in the list with "no_download" status
|
||||
await expect(page.getByLabel("Processing table")).toContainText("In Utero");
|
||||
await expect(page.getByLabel("Processing table")).toContainText(
|
||||
"no_download",
|
||||
);
|
||||
});
|
||||
|
||||
test("NO_DOWNLOAD: Should not display pause button when NO_DOWNLOAD is enabled", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Add an item to the queue
|
||||
await runSearch("Nirvana", page);
|
||||
await page.getByRole("tab", { name: "Albums" }).first().click();
|
||||
await page.getByRole("button", { name: "Get album" }).nth(2).click();
|
||||
|
||||
// Open processing list
|
||||
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
|
||||
await page.locator("button.MuiFab-circular").click();
|
||||
|
||||
// Wait for dialog to open
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify pause button is NOT visible
|
||||
const pauseButton = page.getByRole("button", { name: "Pause" });
|
||||
await expect(pauseButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("NO_DOWNLOAD: Should not display terminal button for no_download items", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Add an item to the queue
|
||||
await runSearch("Nirvana", page);
|
||||
await page.getByRole("tab", { name: "Albums" }).first().click();
|
||||
await page
|
||||
.locator("div:nth-child(2) > .MuiPaper-root > div:nth-child(2)")
|
||||
.getByTestId("btn-dl")
|
||||
.click();
|
||||
|
||||
// Open processing list
|
||||
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
|
||||
await page.locator("button.MuiFab-circular").click();
|
||||
|
||||
// Wait for the table to be visible
|
||||
await page.waitForSelector('[aria-label="Processing table"]', {
|
||||
state: "visible",
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
// Verify terminal button is NOT visible
|
||||
const terminalButton = page.getByTestId("btn-console");
|
||||
await expect(terminalButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("NO_DOWNLOAD: Should display Block icon for no_download items in download button", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Add an item to the queue
|
||||
await runSearch("Nirvana", page);
|
||||
await page.getByRole("tab", { name: "Albums" }).first().click();
|
||||
|
||||
// Find the download button and click it
|
||||
const downloadButton = page
|
||||
.locator("div:nth-child(2) > .MuiPaper-root > div:nth-child(2)")
|
||||
.getByTestId("btn-dl");
|
||||
await downloadButton.click();
|
||||
|
||||
// Wait a bit for the status to update
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify the download button is disabled
|
||||
await expect(downloadButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("NO_DOWNLOAD: Should display primary color FAB when NO_DOWNLOAD is enabled", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Add an item to the queue
|
||||
await runSearch("Nirvana", page);
|
||||
await page.getByRole("tab", { name: "Albums" }).first().click();
|
||||
await page.getByRole("button", { name: "Get album" }).nth(2).click();
|
||||
|
||||
// Wait for FAB to appear
|
||||
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
|
||||
|
||||
// Check if the FAB has primary color
|
||||
const fab = page.locator("button.MuiFab-circular");
|
||||
await expect(fab).toHaveClass(/MuiFab-primary/);
|
||||
});
|
||||
|
||||
test("NO_DOWNLOAD: Should allow clearing items even in no_download mode", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Add an item to the queue
|
||||
await runSearch("Nirvana", page);
|
||||
await page.getByRole("tab", { name: "Albums" }).first().click();
|
||||
await page.getByRole("button", { name: "Get album" }).nth(2).click();
|
||||
|
||||
// Open processing list
|
||||
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
|
||||
await page.locator("button.MuiFab-circular").click();
|
||||
|
||||
// Verify item is in the list
|
||||
await expect(page.getByLabel("Processing table")).toContainText(
|
||||
"MTV Unplugged In New York",
|
||||
);
|
||||
|
||||
// Setup confirmation dialog handler
|
||||
page.once("dialog", (dialog) => {
|
||||
expect(dialog.message()).toContain(
|
||||
"Are you sure you want to clear all items from the queue?",
|
||||
);
|
||||
dialog.accept();
|
||||
});
|
||||
|
||||
// Click "Clear all" button
|
||||
await page.waitForTimeout(500);
|
||||
await page.getByRole("button", { name: "Clear all" }).click();
|
||||
|
||||
// Verify the API was called
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.locator("button.MuiFab-circular")).not.toBeVisible();
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@playwright/test@^1.56.1":
|
||||
version "1.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f"
|
||||
integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==
|
||||
dependencies:
|
||||
playwright "1.56.1"
|
||||
|
||||
"@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"
|
||||
|
||||
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==
|
||||
|
||||
fsevents@2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
|
||||
playwright-core@1.56.1:
|
||||
version "1.56.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d"
|
||||
integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==
|
||||
|
||||
playwright@1.56.1, playwright@^1.54.2:
|
||||
version "1.56.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf"
|
||||
integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==
|
||||
dependencies:
|
||||
playwright-core "1.56.1"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
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==
|
||||
@@ -24,7 +24,7 @@ import:
|
||||
quiet_fallback: asis
|
||||
timid: no
|
||||
duplicate_action: asis
|
||||
log: /home/app/standalone/shared/beets/beet.log
|
||||
log: /shared/beets/beet.log
|
||||
languages: ['en']
|
||||
group_albums: no
|
||||
|
||||
|
||||
@@ -53,13 +53,16 @@ skip_existing = true
|
||||
threads_count = 4
|
||||
|
||||
# base download directory, by default it is set to your home directory / Music / tiddl
|
||||
download_path = "/home/app/standalone/shared/.processing"
|
||||
download_path = "/shared/.processing"
|
||||
|
||||
# if you moved the downloaded files to other directory,
|
||||
# then you should specify the destination directory there.
|
||||
# otherwise `tiddl` will not detect them and `skip_existing` will not skip
|
||||
# already downloaded files. by default scan path is set to your download path.
|
||||
scan_path = "/home/app/standalone/library"
|
||||
#
|
||||
# /!\ Should match your mounted library docker volume.
|
||||
#
|
||||
scan_path = "/music"
|
||||
|
||||
# this option is used to determine if you want to include downloading singles from an artist.
|
||||
# "none" download only full albums
|
||||
|
||||
Reference in new issue
Block a user