diff --git a/.gitignore b/.gitignore
index 812a03ba..474bdc25 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@ backend/.env
# JavaScript stuff
node_modules
+
+.editorconfig
diff --git a/Dockerfile b/Dockerfile
index b8260679..c4319f71 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,20 +1,24 @@
# multi stage to build tube archivist
# build python wheel, download and extract ffmpeg, copy into final image
+FROM node:lts-alpine AS npm-builder
+COPY frontend/package.json frontend/package-lock.json /
+RUN npm i
+
FROM node:lts-alpine AS node-builder
# RUN npm config set registry https://registry.npmjs.org/
+COPY --from=npm-builder ./node_modules /frontend/node_modules
COPY ./frontend /frontend
-
WORKDIR /frontend
-RUN npm i
+
RUN npm run build:deploy
WORKDIR /
# First stage to build python wheel
-FROM python:3.11.8-slim-bookworm AS builder
+FROM python:3.11.13-slim-bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential gcc libldap2-dev libsasl2-dev libssl-dev git
@@ -24,7 +28,7 @@ COPY ./backend/requirements.txt /requirements.txt
RUN pip install --user -r requirements.txt
# build ffmpeg
-FROM python:3.11.8-slim-bookworm AS ffmpeg-builder
+FROM python:3.11.13-slim-bookworm AS ffmpeg-builder
ARG TARGETPLATFORM
@@ -32,7 +36,7 @@ COPY docker_assets/ffmpeg_download.py ffmpeg_download.py
RUN python ffmpeg_download.py $TARGETPLATFORM
# build final image
-FROM python:3.11.8-slim-bookworm AS tubearchivist
+FROM python:3.11.13-slim-bookworm AS tubearchivist
ARG INSTALL_DEBUG
@@ -54,9 +58,9 @@ RUN apt-get clean && apt-get -y update && apt-get -y install --no-install-recomm
# install debug tools for testing environment
RUN if [ "$INSTALL_DEBUG" ] ; then \
- apt-get -y update && apt-get -y install --no-install-recommends \
- vim htop bmon net-tools iputils-ping procps lsof \
- && pip install --user ipython pytest pytest-django \
+ apt-get -y update && apt-get -y install --no-install-recommends \
+ vim htop bmon net-tools iputils-ping procps lsof \
+ && pip install --user ipython pytest pytest-django \
; fi
# make folders
diff --git a/README.md b/README.md
index 7d3b2a96..9c50cd8c 100644
--- a/README.md
+++ b/README.md
@@ -71,6 +71,7 @@ All environment variables are explained in detail in the docs [here](https://doc
| ELASTIC_USER | Change the default ElasticSearch user | Optional |
| TA_LDAP | Configure TA to use LDAP Authentication | [Read more](https://docs.tubearchivist.com/configuration/ldap/) |
| DISABLE_STATIC_AUTH | Remove authentication from media files, (Google Cast...) | [Read more](https://docs.tubearchivist.com/installation/env-vars/#disable_static_auth) |
+| TA_AUTO_UPDATE_YTDLP | Configure TA to automatically install the latest yt-dlp on container start | Optional |
| DJANGO_DEBUG | Return additional error messages, for debug only | Optional |
| TA_LOGIN_AUTH_MODE | Configure the order of login authentication backends (Default: single) | Optional |
diff --git a/backend/config/settings.py b/backend/config/settings.py
index 39d2186e..abcc4aa8 100644
--- a/backend/config/settings.py
+++ b/backend/config/settings.py
@@ -316,7 +316,7 @@ CORS_ALLOW_HEADERS = list(default_headers) + [
# TA application settings
TA_UPSTREAM = "https://github.com/tubearchivist/tubearchivist"
-TA_VERSION = "v0.5.4"
+TA_VERSION = "v0.5.5-unstable"
# API
REST_FRAMEWORK = {
diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt
index a5ee0352..30696ad3 100644
--- a/backend/requirements-dev.txt
+++ b/backend/requirements-dev.txt
@@ -4,7 +4,7 @@ pre-commit==4.2.0
pylint-django==2.6.1
pylint==3.3.7
pytest-django==4.11.1
-pytest==8.4.0
-python-dotenv==1.1.0
+pytest==8.4.1
+python-dotenv==1.1.1
requirementscheck==0.0.6
-types-requests==2.32.0.20250602
+types-requests==2.32.4.20250611
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 9751c2cb..b939b37f 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -3,13 +3,13 @@ celery==5.5.3
django-auth-ldap==5.2.0
django-celery-beat==2.8.1
django-cors-headers==4.7.0
-Django==5.2.2
+Django==5.2.3
djangorestframework==3.16.0
drf-spectacular==0.28.0
Pillow==11.2.1
redis==6.2.0
requests==2.32.4
ryd-client==0.0.6
-uvicorn==0.34.3
+uvicorn==0.35.0
whitenoise==6.9.0
-yt-dlp[default]==2025.6.9
+yt-dlp[default]==2025.6.30
diff --git a/docker_assets/run.sh b/docker_assets/run.sh
index 9f065d7b..b09bee22 100644
--- a/docker_assets/run.sh
+++ b/docker_assets/run.sh
@@ -9,6 +9,15 @@ else
LOGLEVEL="INFO"
fi
+# update yt-dlp if needed
+if [[ "${TA_AUTO_UPDATE_YTDLP,,}" =~ ^(release|nightly)$ ]]; then
+ echo "Updating yt-dlp..."
+ preflag=$([[ "${TA_AUTO_UPDATE_YTDLP,,}" == "nightly" ]] && echo "--pre" || echo "")
+ python -m pip install --target=/root/.local/bin --upgrade $preflag "yt-dlp[default]" || {
+ echo "yt-dlp update failed"
+ }
+fi
+
# stop on pending manual migration
python manage.py ta_stop_on_error
diff --git a/frontend/src/components/DownloadListItem.tsx b/frontend/src/components/DownloadListItem.tsx
index bb0a82e3..ff0273dd 100644
--- a/frontend/src/components/DownloadListItem.tsx
+++ b/frontend/src/components/DownloadListItem.tsx
@@ -47,7 +47,11 @@ const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
{!download.channel_indexed && {download.channel_name}}
-
+
{download.title}
diff --git a/frontend/src/components/InputConfig.tsx b/frontend/src/components/InputConfig.tsx
index a378a5fb..cf1bfa13 100644
--- a/frontend/src/components/InputConfig.tsx
+++ b/frontend/src/components/InputConfig.tsx
@@ -1,4 +1,5 @@
import { useState } from 'react';
+import LoadingIndicator from './LoadingIndicator';
type InputTextProps = {
type: 'text' | 'number';
@@ -51,13 +52,7 @@ const InputConfig = ({ type, name, value, setValue, oldValue, updateCallback }:
>
)}
{oldValue !== null && }
- {loading && (
- <>
-
- >
- )}
+ {loading && }
{success && ✅}
diff --git a/frontend/src/components/LoadingIndicator.tsx b/frontend/src/components/LoadingIndicator.tsx
new file mode 100644
index 00000000..0eaeaeb4
--- /dev/null
+++ b/frontend/src/components/LoadingIndicator.tsx
@@ -0,0 +1,9 @@
+const LoadingIndicator = () => {
+ return (
+
+ );
+};
+
+export default LoadingIndicator;
diff --git a/frontend/src/components/VideoPlayer.tsx b/frontend/src/components/VideoPlayer.tsx
index 7684e24a..de8fc4bb 100644
--- a/frontend/src/components/VideoPlayer.tsx
+++ b/frontend/src/components/VideoPlayer.tsx
@@ -140,6 +140,8 @@ const VideoPlayer = ({
const [showHelpDialog, setShowHelpDialog] = useState(false);
const [showInfoDialog, setShowInfoDialog] = useState(false);
const [infoDialogContent, setInfoDialogContent] = useState('');
+ const [isTheaterMode, setIsTheaterMode] = useState(false);
+ const [theaterModeKeyPressed, setTheaterModeKeyPressed] = useState(false);
const questionmarkPressed = useKeyPress('?');
const mutePressed = useKeyPress('m');
@@ -151,6 +153,8 @@ const VideoPlayer = ({
const arrowRightPressed = useKeyPress('ArrowRight');
const arrowLeftPressed = useKeyPress('ArrowLeft');
const pPausedPressed = useKeyPress('p');
+ const theaterModePressed = useKeyPress('t');
+ const escapePressed = useKeyPress('Escape');
const videoId = video.youtube_id;
const videoUrl = video.media_url;
@@ -345,10 +349,42 @@ const VideoPlayer = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [questionmarkPressed]);
+ useEffect(() => {
+ if (embed) {
+ return;
+ }
+
+ if (theaterModePressed && !theaterModeKeyPressed) {
+ setTheaterModeKeyPressed(true);
+
+ const newTheaterMode = !isTheaterMode;
+ setIsTheaterMode(newTheaterMode);
+
+ infoDialog(newTheaterMode ? 'Theater mode' : 'Normal mode');
+ } else if (!theaterModePressed) {
+ setTheaterModeKeyPressed(false);
+ }
+ }, [theaterModePressed, isTheaterMode, theaterModeKeyPressed]);
+
+ useEffect(() => {
+ if (embed) {
+ return;
+ }
+
+ if (escapePressed && isTheaterMode) {
+ setIsTheaterMode(false);
+
+ infoDialog('Normal mode');
+ }
+ }, [escapePressed, isTheaterMode]);
+
return (
<>
-
-
+
+