feat: use tiddl folder templates (#317)

* wip

* fix: group constants

* feat: add update message modal

* feat: add tiddl config in config dialog

* fix: preserve cp ownership if puid and pgid are set

* feat: move python deps to requirements.txt file

* feat: enchance update warn dialog
This commit is contained in:
cl.st authored and GitHub committed 2025-09-03 23:37:04 +02:00
1 parent 3348b82cdc
commit cf93f77af6
23 files changed
+228 -91

No files matched your search

+8
View File
@@ -5,10 +5,18 @@ Tidarr notable changes.
## 📦 0.3.1
### 🖍 Changed
* [Docker] ⚠️ **Now there is only one docker volume to mount for the music library destination**
* [API] ⚠️ **Folder management and format is now handle in `tiddl.json` file.**
* [API] Use tiddl `-V` parameters for video DL
* [Front] Hide/Show video lists if tiddl `download_video` parameter is false/true.
### 🚀 Added
* [Front] Add an update warning modal
* [Front] Add tiddl config in config dialog
### 🐛 Fixed
* [Front] Fix skeleton loaders
* [API] If PUID and PGID are set, `cp` uses `-rfp` args
### 🧹 Renovate
* [Chore] Move python deps to `requirements.txt` file
## 📦 0.3.0
### 🚀 Added
+1 -4
View File
@@ -73,10 +73,7 @@ docker-run: ## Run tidarr docker image
--platform=linux/amd64 \
-p 8484:8484 \
-v ${PWD}/docker/mnt/config/:/home/app/standalone/shared \
-v ${PWD}/docker/mnt/download/albums:/home/app/standalone/download/albums \
-v ${PWD}/docker/mnt/download/tracks:/home/app/standalone/download/tracks \
-v ${PWD}/docker/mnt/download/playlists:/home/app/standalone/download/playlists \
-v ${PWD}/docker/mnt/download/videos:/home/app/standalone/download/videos \
-v ${PWD}/docker/mnt/library:/home/app/standalone/library/ \
-e ENABLE_BEETS=true \
-e PUID=501 \
-e PGID=501 \
+12 -14
View File
@@ -18,7 +18,7 @@ Tidarr is a Docker image that provides a web interface to download up to **24-bi
- [Getting Started](#getting-started)
- [Tidal authentication](#tidal-authentication)
- [Options](#options)
- [Download options](#download-settings)
- [Download settings](#download-settings)
- [PUID/PGID](#puid-pgid)
- [Password protection](#password-protection)
- [Services](#services):
@@ -81,10 +81,7 @@ services:
- 8484:8484
volumes:
- /any/folder/to/tidarr/config:/home/app/standalone/shared
- /any/folder/to/download/albums:/home/app/standalone/download/albums
- /any/folder/to/download/tracks:/home/app/standalone/download/tracks
- /any/folder/to/download/playlists:/home/app/standalone/download/playlists
- /any/folder/to/download/videos:/home/app/standalone/download/videos
- /any/folder/to/library:/home/app/standalone/library
restart: 'unless-stopped'
```
@@ -96,10 +93,7 @@ docker run \
--name tidarr \
-p 8484:8484 \
-v /any/folder/to/tidarr/config/:/home/app/standalone/shared \
-v /any/folder/to/download/albums:/home/app/standalone/download/albums \
-v /any/folder/to/download/tracks:/home/app/standalone/download/tracks \
-v /any/folder/to/download/playlists:/home/app/standalone/download/playlists \
-v /any/folder/to/download/videos:/home/app/standalone/download/videos \
-v /any/folder/to/library:/home/app/standalone/library \
cstaelen/tidarr:latest
```
@@ -128,26 +122,30 @@ docker exec tidarr cp -rf /root/tiddl.json /home/app/standalone/shared/tiddl.jso
-> You can set download options in `/your/docker/path/to/tidarr/config/tiddl.json`.
/!\ Be ware to set the right folder templates
See default :
```json
{
// More tiddldetails : https://github.com/oskvr37/wiki/Template-formatting
"template": {
"track": "{artist} - {title}",
"video": "{artist} - {title}",
"album": "{album_artist}/{album}/{number:02d}. {title}",
"playlist": "{playlist}/{playlist_number:02d}. {artist} - {title}"
"track": "{artist}/_tracks/{artist} - {title}",
"video": "_videos/{artist}/{artist} - {title}",
"playlist": "_playlists/{playlist}/{playlist_number:02d}. {artist} - {title}"
},
"download": {
// Default high (16bit 44.1khz), max available: master (24bit 192khz max)
// https://github.com/oskvr37/tiddl?tab=readme-ov-file#download-quality
"quality": "high",
// Should not be changed (otherwise downloads will fail) /!\
// Should not be changed (otherwise downloads will fail) /!\
"path": "/home/app/standalone/download/incomplete",
"threads": 4,
"embed_lyrics": false,
// Include or not singles while downloading "all releases"
"singles_filter": "none", // "none", "only", "include"
// Allow video download
// Show video content
"download_video": true
},
"cover": {
+10 -11
View File
@@ -34,18 +34,17 @@ export async function moveAndClean(
setPermissions();
try {
let folder = item.type;
if (item.type === "artist") {
folder = "album";
}
if (item.type === "mix") {
folder = "playlist";
}
item["output"] = logs(item, `=== Move processed items ===`);
const output_move = execSync(
`cp -rf ${ROOT_PATH}/download/incomplete/* ${ROOT_PATH}/download/${folder}s/ >/dev/null`,
{ encoding: "utf-8" },
);
let args = "-rf";
if (process.env.PUID && process.env.PGID) {
args = "-rfp";
}
const cmd = `cp ${args} ${ROOT_PATH}/download/incomplete/* ${ROOT_PATH}/library >/dev/null`;
item["output"] = logs(item, cmd);
const output_move = execSync(cmd, { encoding: "utf-8" });
item["output"] = logs(
item,
`- Move complete ${item.type}\r\n${output_move}`,
+6 -2
View File
@@ -27,11 +27,15 @@ export function tidalDL(id: number, app: Express, onFinish?: () => void) {
item["output"] = logs(item, `Executing: ${binary} ${args.join(" ")}`);
const child = spawn(binary, args);
const signal = child.signalCode || undefined;
child.stdout?.setEncoding("utf8");
child.stdout?.on("data", (data) => {
child.stdout?.on("data", (data: string) => {
item["output"] = logs(item, data.replace(/[\r\n]+/gm, ""));
item["process"] = child;
if (data.includes("ERROR")) {
child.emit("error", new Error(data));
child.kill(signal);
}
app.settings.processingList.actions.updateItem(item);
});
+50 -27
View File
@@ -24,7 +24,7 @@ import { DialogHandler } from ".";
const TableParameters = ({
rows,
}: {
rows: [string, string | undefined][];
rows: [string, string | number | boolean | undefined][];
}) => {
return (
<TableContainer component={Paper}>
@@ -32,7 +32,7 @@ const TableParameters = ({
<TableHead>
<TableRow>
<TableCell>
<strong>Environment vars</strong>
<strong>Variable</strong>
</TableCell>
<TableCell>
<strong>Value</strong>
@@ -46,7 +46,7 @@ const TableParameters = ({
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell>{row?.[0]}</TableCell>
<TableCell>{row?.[1]}</TableCell>
<TableCell>{row?.[1]?.toString()}</TableCell>
</TableRow>
))}
</TableBody>
@@ -61,6 +61,7 @@ export const DialogConfig = () => {
config,
isUpdateAvailable,
releaseData,
tiddlConfig,
isConfigModalOpen,
actions,
} = useConfigProvider();
@@ -150,30 +151,52 @@ export const DialogConfig = () => {
</>
)}
{currentTab === 2 && (
<Box display="flex" justifyContent="center" my={4}>
{!tokenMissing ? (
<Button
variant="contained"
color="warning"
startIcon={<KeyOff />}
onClick={async () => {
actions.toggleModal(false);
await delete_token();
checkAPI();
}}
>
Revoke Tidal token
</Button>
) : (
<Alert
color="warning"
icon={<Warning sx={{ fontSize: 20 }} />}
variant="outlined"
>
No Tidal token found !
</Alert>
)}
</Box>
<>
<Box display="flex" justifyContent="center" my={4}>
{!tokenMissing ? (
<Button
variant="contained"
color="warning"
startIcon={<KeyOff />}
onClick={async () => {
actions.toggleModal(false);
await delete_token();
checkAPI();
}}
>
Revoke Tidal token
</Button>
) : (
<Alert
color="warning"
icon={<Warning sx={{ fontSize: 20 }} />}
variant="outlined"
>
No Tidal token found !
</Alert>
)}
</Box>
<Box>
<h3>Tiddl template options</h3>
{tiddlConfig?.template ? (
<TableParameters rows={Object.entries(tiddlConfig.template)} />
) : (
"Not found."
)}
<h3>Tiddl download options</h3>
{tiddlConfig?.download ? (
<TableParameters rows={Object.entries(tiddlConfig.download)} />
) : (
"Not found."
)}
<h3>Tiddl cover options</h3>
{tiddlConfig?.cover ? (
<TableParameters rows={Object.entries(tiddlConfig.cover)} />
) : (
"Not found."
)}
</Box>
</>
)}
</DialogHandler>
);
@@ -0,0 +1,66 @@
import { useState } from "react";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import WarningIcon from "@mui/icons-material/Warning";
import { Box, Button, Link } from "@mui/material";
import { LOCALSTORAGE_UPDATE_WARNING } from "src/contants";
import { useConfigProvider } from "src/provider/ConfigProvider";
import { DialogHandler } from ".";
const VERSION_CONCERNED = "0.3.1";
export const DialogUpdateWarning = () => {
const { config } = useConfigProvider();
const [show, setShow] = useState(
localStorage.getItem(LOCALSTORAGE_UPDATE_WARNING) !== VERSION_CONCERNED,
);
function persistClose() {
setShow(false);
localStorage.setItem(LOCALSTORAGE_UPDATE_WARNING, VERSION_CONCERNED);
}
return (
<DialogHandler
open={show && !!config?.TIDARR_VERSION?.includes(VERSION_CONCERNED)}
onClose={() => setShow(false)}
title={"Folder configuration has changed !"}
icon={<WarningIcon color="error" />}
maxWidth="sm"
buttons={
<Button onClick={persistClose} variant="outlined">
Don't show again
</Button>
}
>
<p>
Since the latest update, the folder configuration has changed. There is
now only a single Docker volume for the music library (destination).
</p>
<p>
To define the subfolders and the destination format for tracks, albums,
playlists, etc., you will need to edit the "template" section of the{" "}
<strong>
<i>tiddl.json</i>
</strong>{" "}
file.
</p>
<Box gap={2} display="flex">
<Link
href="https://github.com/cstaelen/tidarr?tab=readme-ov-file#getting-started"
target="_blank"
>
Read more on Github{` `}
<OpenInNewIcon sx={{ fontSize: 15, verticalAlign: "middle" }} />
</Link>
<Link
href="https://github.com/oskvr37/tiddl/wiki/Template-formatting"
target="_blank"
>
Template formatting (tiddl){` `}
<OpenInNewIcon sx={{ fontSize: 15, verticalAlign: "middle" }} />
</Link>
</Box>
</DialogHandler>
);
};
+6 -1
View File
@@ -17,6 +17,7 @@ export const DialogHandler = ({
open,
icon,
maxWidth = "xl",
buttons,
onClose,
}: {
children: ReactNode;
@@ -24,6 +25,7 @@ export const DialogHandler = ({
icon: ReactNode;
open: boolean;
maxWidth?: Breakpoint;
buttons?: ReactNode;
onClose?: () => void;
}) => {
const handleClose = () => {
@@ -51,7 +53,10 @@ export const DialogHandler = ({
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
<Button variant="outlined" onClick={handleClose}>
Close
</Button>
{buttons}
</DialogActions>
</Dialog>
);
+2
View File
@@ -10,6 +10,7 @@ import { ProcessingProvider } from "../provider/ProcessingProvider";
import { DialogConfig } from "./Dialog/DialogConfig";
import { DialogNoAPI } from "./Dialog/DialogNoAPI";
import { DialogToken } from "./Dialog/DialogToken";
import { DialogUpdateWarning } from "./Dialog/DialogUpdateWarning";
import { Footer } from "./Layout/Footer";
import { HeaderSearch } from "./Layout/HeaderSearch";
import { ProcessingList } from "./Processing/ProcessingList";
@@ -58,6 +59,7 @@ function MainLayout({ children }: { children: ReactNode }) {
<DialogToken />
<DialogNoAPI />
<DialogConfig />
<DialogUpdateWarning />
</ProcessingProvider>
</SearchProvider>
</main>
+3 -3
View File
@@ -1,10 +1,10 @@
import { Navigate, Outlet, useLocation } from "react-router-dom";
import {
LOCALSTORAGE_REDIRECT_URL,
LOCALSTORAGE_TOKEN_KEY,
useAuth,
} from "../../provider/AuthProvider";
} from "src/contants";
import { useAuth } from "../../provider/AuthProvider";
import { Loader } from "./Loader";
+7
View File
@@ -1,3 +1,10 @@
export const TIDAL_ITEMS_PER_PAGE = 18;
export const TIDAL_API_URL = "https://api.tidal.com";
export const TIDAL_ALBUM_URL = "http://www.tidal.com/album/";
export const LOCALSTORAGE_TOKEN_KEY = "tidarr-token";
export const LOCALSTORAGE_REDIRECT_URL = "tidarr-redirect";
export const LOCALSTORAGE_UPDATE_WARNING = "tidarr-update-warn";
export const LOCALSTORAGE_QUALITY_DOWNLOAD = "tidarr-quality-download";
export const LOCALSTORAGE_DISPLAY_MODE = "tidarr-display-mode";
+1 -1
View File
@@ -4,7 +4,7 @@ import {
EventSourcePlus,
SseMessage,
} from "event-source-plus";
import { LOCALSTORAGE_TOKEN_KEY } from "src/provider/AuthProvider";
import { LOCALSTORAGE_TOKEN_KEY } from "src/contants";
import {
AuthType,
+4 -3
View File
@@ -7,13 +7,14 @@ import React, {
} from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { ROUTE_LOGIN } from "src/components/Security/PrivateRoute";
import {
LOCALSTORAGE_REDIRECT_URL,
LOCALSTORAGE_TOKEN_KEY,
} from "src/contants";
import { ApiReturnType, AuthType, CheckAuthType } from "src/types";
import { useApiFetcher } from "./ApiFetcherProvider";
export const LOCALSTORAGE_TOKEN_KEY = "tidarr-token";
export const LOCALSTORAGE_REDIRECT_URL = "tidarr-redirect";
type AuthContextType = {
isAuthActive: boolean | undefined;
isAccessGranted: boolean;
+4 -3
View File
@@ -1,4 +1,8 @@
import React, { ReactNode, useContext, useEffect, useState } from "react";
import {
LOCALSTORAGE_DISPLAY_MODE,
LOCALSTORAGE_QUALITY_DOWNLOAD,
} from "src/contants";
import {
ConfigParametersType,
@@ -30,9 +34,6 @@ type ConfigContextType = {
};
};
export const LOCALSTORAGE_QUALITY_DOWNLOAD = "tidarr-quality-download";
export const LOCALSTORAGE_DISPLAY_MODE = "tidarr-display-mode";
const ConfigContext = React.createContext<ConfigContextType>(
{} as ConfigContextType,
);
+1 -4
View File
@@ -10,10 +10,7 @@ services:
- 3000:3000
volumes:
- ./docker/mnt/config:/home/app/standalone/shared
- ./docker/mnt/download/albums:/home/app/standalone/download/albums
- ./docker/mnt/download/tracks:/home/app/standalone/download/tracks
- ./docker/mnt/download/playlists:/home/app/standalone/download/playlists
- ./docker/mnt/download/videos:/home/app/standalone/download/videos
- ./docker/mnt/library:/home/app/standalone/library
- .:/home/app/build # only used in dev environment
restart: 'unless-stopped'
environment:
+4 -10
View File
@@ -1,26 +1,20 @@
# syntax = devthefuture/dockerfile-x
FROM ghcr.io/linuxserver/baseimage-alpine:3.21
WORKDIR /home/app/build
ENV SHELL=bash
ARG NODE_ENV
ENV NODE_ENV="${NODE_ENV}"
ENV PYTHONUNBUFFERED=1
ENV ENVIRONMENT="development"
ENV VERSION="0.0.0-dev"
RUN apk update && apk upgrade
RUN apk add npm nodejs py-pip ffmpeg beets
RUN apk add npm nodejs py-pip
RUN mkdir -p $HOME/.venvs
RUN python3 -m venv $HOME/.venvs
RUN source $HOME/.venvs/bin/activate
ENV PATH="$HOME/.venvs/bin:$PATH"
INCLUDE docker/stages/Dockerfile.python
RUN python -m pip install "tiddl==2.5.1" requests ffmpeg-python mutagen
RUN mkdir -p /home/app/standalone/shared/beets
RUN touch /home/app/standalone/shared/beets/beets-library.blb
RUN mkdir -p /home/app/standalone/download/incomplete/
RUN npm install -g yarn tsx
+3 -1
View File
@@ -17,7 +17,9 @@ ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN python3 -m pip install "tiddl==2.5.1" requests ffmpeg-python mutagen
COPY docker/requirements.txt docker/requirements.txt
RUN python -m pip install -r docker/requirements.txt
INCLUDE docker/stages/Dockerfile.runner
RUN mkdir -p /home/app/standalone/download/incomplete/
+1 -1
View File
@@ -9,7 +9,7 @@ INCLUDE docker/stages/Dockerfile.builder
## BUILD STANDALONE APP
FROM ghcr.io/linuxserver/baseimage-alpine:3.21 AS runner
RUN apk add npm nodejs py3-pip beets ffmpeg
RUN apk add npm nodejs py3-pip
INCLUDE docker/stages/Dockerfile.python
INCLUDE docker/stages/Dockerfile.runner
+5
View File
@@ -0,0 +1,5 @@
requests
ffmpeg-python
mutagen
tiddl == 2.5.1
+4 -1
View File
@@ -8,7 +8,10 @@ RUN python3 -m venv $HOME/.venvs
RUN source $HOME/.venvs/bin/activate
ENV PATH="$HOME/.venvs/bin:$PATH"
RUN python -m pip install "tiddl==2.5.1" requests ffmpeg-python mutagen
RUN apk add ffmpeg beets
COPY docker/requirements.txt docker/requirements.txt
RUN python -m pip install -r docker/requirements.txt
RUN mkdir -p /home/app/standalone/shared/beets
RUN touch /home/app/standalone/shared/beets/beets-library.blb
+9 -1
View File
@@ -91,7 +91,7 @@ test("Tidarr config : Should see configuration dialog", async ({ page }) => {
];
const tableAPIRows = await page
.locator("#alert-dialog-description div")
.filter({ hasText: "Environment" })
.filter({ hasText: "Variable" })
.locator("table tbody tr")
.all();
@@ -107,6 +107,14 @@ test("Tidarr config : Should see configuration dialog", async ({ page }) => {
await expect(
page.getByRole("button", { name: "Revoke Tidal token" }),
).toBeInViewport();
const tableTiddlRows = await page
.locator("#alert-dialog-description div")
.filter({ hasText: "Variable" })
.locator("table tbody tr")
.all();
expect(tableTiddlRows.length).toEqual(13);
});
test("Tidarr config : Should see update button", async ({ page }) => {
+16
View File
@@ -170,8 +170,24 @@ export async function mockConfigAPI(page: Page) {
},
download: {
quality: "high",
path: "/home/app/standalone/download/incomplete",
threads: 4,
singles_filter: "none",
embed_lyrics: false,
download_video: true,
},
cover: {
save: true,
size: 1280,
filename: "cover.jpg",
},
template: {
track: "tracks/{artist}/{artist} - {title}",
video: "videos/{artist}/{artist} - {title}",
album: "albums/{album_artist}/{year} - {album}/{number:02d}. {title}",
playlist:
"playlists/{playlist}/{playlist_number:02d}. {artist} - {title}",
},
},
};
await route.fulfill({ json });
+5 -4
View File
@@ -1,15 +1,16 @@
{
"template": {
"track": "{artist} - {title}",
"video": "{artist} - {title}",
"album": "{album_artist}/{album}/{number:02d}. {title}",
"playlist": "{playlist}/{playlist_number:02d}. {artist} - {title}"
"track": "tracks/{artist}/{artist} - {title}",
"video": "videos/{artist}/{artist} - {title}",
"album": "albums/{album_artist}/{year} - {album}/{number:02d}. {title}",
"playlist": "playlists/{playlist}/{playlist_number:02d}. {artist} - {title}"
},
"download": {
"quality": "high",
"path": "/home/app/standalone/download/incomplete",
"threads": 4,
"singles_filter": "none",
"embed_lyrics": false,
"download_video": true
},
"cover": {