POT handling changes, again, #build
Changed: - switch to forked pot provider plugin repo - avoid layoutshift with fixed thumb placeholder - add multiselect bulk reindex - move state update out of effects - fix google cast loading
This commit is contained in:
commit
92af2f586f
|
|
@ -21,7 +21,7 @@ jobs:
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '23'
|
node-version: '24'
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 25.9.0
|
rev: 26.3.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
alias: python
|
alias: python
|
||||||
files: ^backend/
|
files: ^backend/
|
||||||
args: ["--line-length=79"]
|
args: ["--line-length=79"]
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: 6.0.1
|
rev: 8.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
|
|
@ -24,14 +24,14 @@ repos:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
alias: python
|
alias: python
|
||||||
files: ^backend/
|
files: ^backend/
|
||||||
args: ["--max-complexity=10", "--max-line-length=79"]
|
args: ["--jobs=1", "--max-complexity=10", "--max-line-length=79"]
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.4.1
|
rev: v2.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
exclude: ^frontend/package-lock.json
|
exclude: ^frontend/package-lock.json
|
||||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
rev: v9.36.0
|
rev: v10.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: eslint
|
- id: eslint
|
||||||
name: eslint
|
name: eslint
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,9 @@ Some of you might have created useful scripts or API integrations around this pr
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Improve to the Documentation
|
## Improving the Documentation
|
||||||
|
|
||||||
The documentation available at [docs.tubearchivist.com](https://docs.tubearchivist.com/) and is build from a separate repo [tubearchivist/docs](https://github.com/tubearchivist/docs). The Readme there has additional instructions on how to make changes.
|
The documentation is available at [docs.tubearchivist.com](https://docs.tubearchivist.com/), and is built from a separate repo: [tubearchivist/docs](https://github.com/tubearchivist/docs). The Readme there has additional instructions on how to make changes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
# multi stage to build tube archivist
|
# multi stage to build tube archivist
|
||||||
# build python wheel, download and extract ffmpeg, copy into final image
|
# build python wheel, download and extract ffmpeg, copy into final image
|
||||||
|
|
||||||
FROM node:22.13.0-alpine AS npm-builder
|
FROM node:24.14.1-alpine AS npm-builder
|
||||||
COPY frontend/package.json frontend/package-lock.json /
|
COPY frontend/package.json frontend/package-lock.json /
|
||||||
RUN npm i
|
RUN npm i
|
||||||
|
|
||||||
FROM node:22.13.0-alpine AS node-builder
|
FROM node:24.14.1-alpine AS node-builder
|
||||||
|
|
||||||
# RUN npm config set registry https://registry.npmjs.org/
|
# RUN npm config set registry https://registry.npmjs.org/
|
||||||
|
|
||||||
|
|
@ -25,9 +25,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
|
||||||
# install requirements
|
# install requirements
|
||||||
COPY ./backend/requirements.txt /requirements.txt
|
COPY ./backend/requirements.txt /requirements.txt
|
||||||
COPY ./backend/requirements.plugins.txt /requirements.plugins.txt
|
RUN pip install --user -r requirements.txt
|
||||||
RUN pip install --user -r /requirements.txt \
|
|
||||||
&& python -m pip install --target /opt/yt_plugins/bgutil -r /requirements.plugins.txt
|
|
||||||
|
|
||||||
# build ffmpeg
|
# build ffmpeg
|
||||||
FROM python:3.13.11-slim-trixie AS ffmpeg-builder
|
FROM python:3.13.11-slim-trixie AS ffmpeg-builder
|
||||||
|
|
@ -48,7 +46,6 @@ COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
|
||||||
|
|
||||||
# copy build requirements
|
# copy build requirements
|
||||||
COPY --from=builder /root/.local /root/.local
|
COPY --from=builder /root/.local /root/.local
|
||||||
COPY --from=builder /opt/yt_plugins /opt/yt_plugins
|
|
||||||
ENV PATH=/root/.local/bin:$PATH
|
ENV PATH=/root/.local/bin:$PATH
|
||||||
|
|
||||||
# copy ffmpeg
|
# copy ffmpeg
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ Take a look at the example [docker-compose.yml](https://github.com/tubearchivist
|
||||||
|
|
||||||
All environment variables are explained in detail in the docs [here](https://docs.tubearchivist.com/installation/env-vars/).
|
All environment variables are explained in detail in the docs [here](https://docs.tubearchivist.com/installation/env-vars/).
|
||||||
|
|
||||||
Both `TA_PASSWORD` and `ELASTIC_PASSWORD` can be suffixed with `_FILE` to allow passing in passwords as secretes. `_FILE` is a convention used by some images including [ElasticSearch](https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-elasticsearch-docker-configure)
|
Both `TA_PASSWORD` and `ELASTIC_PASSWORD` can be suffixed with `_FILE` to allow passing in passwords as secrets. `_FILE` is a convention used by some images including [ElasticSearch](https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-elasticsearch-docker-configure)
|
||||||
|
|
||||||
### TubeArchivist
|
### TubeArchivist
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,12 @@ class ChannelApiSearchView(ApiBaseView):
|
||||||
return Response(error.data, status=400)
|
return Response(error.data, status=400)
|
||||||
|
|
||||||
self.get_document(parsed["url"])
|
self.get_document(parsed["url"])
|
||||||
|
if not self.response:
|
||||||
|
error = ErrorResponseSerializer(
|
||||||
|
{"error": f"channel not found: {query}"}
|
||||||
|
)
|
||||||
|
return Response(error.data, status=404)
|
||||||
|
|
||||||
serializer = ChannelSerializer(self.response)
|
serializer = ChannelSerializer(self.response)
|
||||||
|
|
||||||
return Response(serializer.data, status=self.status_code)
|
return Response(serializer.data, status=self.status_code)
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,7 @@ class EnvironmentSettings:
|
||||||
|
|
||||||
def print_generic(self):
|
def print_generic(self):
|
||||||
"""print generic env vars"""
|
"""print generic env vars"""
|
||||||
print(
|
print(f"""
|
||||||
f"""
|
|
||||||
HOST_UID: {self.HOST_UID}
|
HOST_UID: {self.HOST_UID}
|
||||||
HOST_GID: {self.HOST_GID}
|
HOST_GID: {self.HOST_GID}
|
||||||
TZ: {self.TZ}
|
TZ: {self.TZ}
|
||||||
|
|
@ -102,36 +101,29 @@ class EnvironmentSettings:
|
||||||
TA_PORT: {self.TA_PORT}
|
TA_PORT: {self.TA_PORT}
|
||||||
TA_BACKEND_PORT: {self.TA_BACKEND_PORT}
|
TA_BACKEND_PORT: {self.TA_BACKEND_PORT}
|
||||||
TA_USERNAME: {self.TA_USERNAME}
|
TA_USERNAME: {self.TA_USERNAME}
|
||||||
TA_PASSWORD: *****"""
|
TA_PASSWORD: *****""")
|
||||||
)
|
|
||||||
|
|
||||||
def print_paths(self):
|
def print_paths(self):
|
||||||
"""debug paths set"""
|
"""debug paths set"""
|
||||||
print(
|
print(f"""
|
||||||
f"""
|
|
||||||
MEDIA_DIR: {self.MEDIA_DIR}
|
MEDIA_DIR: {self.MEDIA_DIR}
|
||||||
APP_DIR: {self.APP_DIR}
|
APP_DIR: {self.APP_DIR}
|
||||||
CACHE_DIR: {self.CACHE_DIR}"""
|
CACHE_DIR: {self.CACHE_DIR}""")
|
||||||
)
|
|
||||||
|
|
||||||
def print_redis_conf(self):
|
def print_redis_conf(self):
|
||||||
"""debug redis conf paths"""
|
"""debug redis conf paths"""
|
||||||
print(
|
print(f"""
|
||||||
f"""
|
|
||||||
REDIS_CON: {self.REDIS_CON}
|
REDIS_CON: {self.REDIS_CON}
|
||||||
REDIS_NAME_SPACE: {self.REDIS_NAME_SPACE}"""
|
REDIS_NAME_SPACE: {self.REDIS_NAME_SPACE}""")
|
||||||
)
|
|
||||||
|
|
||||||
def print_es_paths(self):
|
def print_es_paths(self):
|
||||||
"""debug es conf"""
|
"""debug es conf"""
|
||||||
print(
|
print(f"""
|
||||||
f"""
|
|
||||||
ES_URL: {self.ES_URL}
|
ES_URL: {self.ES_URL}
|
||||||
ES_PASS: *****
|
ES_PASS: *****
|
||||||
ES_USER: {self.ES_USER}
|
ES_USER: {self.ES_USER}
|
||||||
ES_SNAPSHOT_DIR: {self.ES_SNAPSHOT_DIR}
|
ES_SNAPSHOT_DIR: {self.ES_SNAPSHOT_DIR}
|
||||||
ES_DISABLE_VERIFY_SSL: {self.ES_DISABLE_VERIFY_SSL}"""
|
ES_DISABLE_VERIFY_SSL: {self.ES_DISABLE_VERIFY_SSL}""")
|
||||||
)
|
|
||||||
|
|
||||||
def print_all(self):
|
def print_all(self):
|
||||||
"""print all"""
|
"""print all"""
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,17 @@ class YtWrap:
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if EnvironmentSettings.APP_DIR == "/app":
|
return
|
||||||
# container internal only
|
|
||||||
self.obs["plugin_dirs"].append("/opt/yt_plugins/bgutil")
|
# from fork: https://github.com/bbilly1/bgutil-ytdlp-pot-provider
|
||||||
|
deep_merge(
|
||||||
|
self.obs,
|
||||||
|
{
|
||||||
|
"extractor_args": {
|
||||||
|
"youtubepot-bgutilhttp": {"disable": ["True"]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def download(self, url):
|
def download(self, url):
|
||||||
"""make download request"""
|
"""make download request"""
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""Django's command-line utility for administrative tasks."""
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
# install plugins in separate folder for runtime whitelisting
|
|
||||||
bgutil-ytdlp-pot-provider==1.3.1
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
apprise==1.9.9
|
apprise==1.9.9
|
||||||
|
bgutil-ytdlp-pot-provider @ git+https://github.com/bbilly1/bgutil-ytdlp-pot-provider@68578674650bade31cd77fb80ce84f7045191ba7#subdirectory=plugin
|
||||||
celery==5.6.3
|
celery==5.6.3
|
||||||
deepdiff==8.6.2
|
deepdiff==9.0.0
|
||||||
django-auth-ldap==5.3.0
|
django-auth-ldap==5.3.0
|
||||||
django-celery-beat==2.9.0
|
django-celery-beat==2.9.0
|
||||||
django-cors-headers==4.9.0
|
django-cors-headers==4.9.0
|
||||||
Django==6.0.3
|
Django==6.0.4
|
||||||
djangorestframework==3.17.1
|
djangorestframework==3.17.1
|
||||||
drf-spectacular==0.28.0 # rc:ignore
|
drf-spectacular==0.28.0 # rc:ignore
|
||||||
Pillow==12.1.1
|
Pillow==12.2.0
|
||||||
redis==7.4.0
|
redis==7.4.0
|
||||||
requests==2.33.0
|
requests==2.33.1
|
||||||
ryd-client==0.0.6
|
ryd-client==0.0.6
|
||||||
uvicorn==0.42.0
|
uvicorn==0.44.0
|
||||||
yt-dlp[default]==2026.3.17
|
yt-dlp[default]==2026.3.17
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
22.13.0
|
24.14.1
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -11,27 +11,27 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dompurify": "^3.3.1",
|
"dompurify": "^3.3.3",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.4",
|
||||||
"react-router-dom": "^7.11.0",
|
"react-router-dom": "^7.14.0",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
||||||
"@typescript-eslint/parser": "^8.51.0",
|
"@typescript-eslint/parser": "^8.58.0",
|
||||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
"@vitejs/plugin-react-swc": "^4.3.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.4",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.4.0",
|
||||||
"prettier": "3.7.4",
|
"prettier": "3.8.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^6.0.2",
|
||||||
"typescript-eslint": "^8.51.0",
|
"typescript-eslint": "^8.58.0",
|
||||||
"vite": "^7.3.0",
|
"vite": "^8.0.5",
|
||||||
"vite-plugin-checker": "^0.12.0"
|
"vite-plugin-checker": "^0.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export const ReindexTypeEnum = {
|
||||||
playlist: 'playlist',
|
playlist: 'playlist',
|
||||||
};
|
};
|
||||||
|
|
||||||
const queueReindex = async (id: string, type: ReindexType, reindexVideos = false) => {
|
const queueReindex = async (id: string[], type: ReindexType, reindexVideos = false) => {
|
||||||
let params = '';
|
let params = '';
|
||||||
if (reindexVideos) {
|
if (reindexVideos) {
|
||||||
params = '?extract_videos=true';
|
params = '?extract_videos=true';
|
||||||
|
|
@ -16,7 +16,7 @@ const queueReindex = async (id: string, type: ReindexType, reindexVideos = false
|
||||||
|
|
||||||
return APIClient(`/api/refresh/${params}`, {
|
return APIClient(`/api/refresh/${params}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { [type]: [id] },
|
body: { [type]: id },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import VideoThumbnail from './VideoThumbail';
|
||||||
|
|
||||||
type DownloadListItemProps = {
|
type DownloadListItemProps = {
|
||||||
download: Download;
|
download: Download;
|
||||||
setRefresh: (status: boolean) => void;
|
setRefresh: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
||||||
|
|
@ -62,7 +62,11 @@ const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
||||||
<span>{download.youtube_id}</span>
|
<span>{download.youtube_id}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{download.message && <p className="danger-zone">{download.message}</p>}
|
{download.message && (
|
||||||
|
<div>
|
||||||
|
<p className="danger-zone">{download.message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{showIgnored && (
|
{showIgnored && (
|
||||||
|
|
@ -72,7 +76,7 @@ const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
||||||
label="Forget"
|
label="Forget"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteDownloadById(download.youtube_id);
|
await deleteDownloadById(download.youtube_id);
|
||||||
setRefresh(true);
|
setRefresh();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -82,7 +86,7 @@ const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
||||||
label="Add to queue"
|
label="Add to queue"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await updateDownloadQueueStatusById(download.youtube_id, 'pending');
|
await updateDownloadQueueStatusById(download.youtube_id, 'pending');
|
||||||
setRefresh(true);
|
setRefresh();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -96,7 +100,7 @@ const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await updateDownloadQueueStatusById(download.youtube_id, 'ignore');
|
await updateDownloadQueueStatusById(download.youtube_id, 'ignore');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -110,7 +114,7 @@ const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
||||||
|
|
||||||
await updateDownloadQueueStatusById(download.youtube_id, 'priority');
|
await updateDownloadQueueStatusById(download.youtube_id, 'priority');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -125,7 +129,7 @@ const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
||||||
className="danger-button"
|
className="danger-button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteDownloadById(download.youtube_id);
|
await deleteDownloadById(download.youtube_id);
|
||||||
setRefresh(true);
|
setRefresh();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ import { useVideoSelectionStore } from '../stores/VideoSelectionStore';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import updateDownloadQueue from '../api/actions/updateDownloadQueue';
|
import updateDownloadQueue from '../api/actions/updateDownloadQueue';
|
||||||
import { HideWatchedType } from '../configuration/constants/HideWatched';
|
import { HideWatchedType } from '../configuration/constants/HideWatched';
|
||||||
|
import queueReindex from '../api/actions/queueReindex';
|
||||||
|
import { useOutletContext } from 'react-router-dom';
|
||||||
|
import { ChannelBaseOutletContextType } from '../pages/ChannelAbout';
|
||||||
|
|
||||||
type FilterbarProps = {
|
type FilterbarProps = {
|
||||||
viewStyle: ViewStyleNamesType;
|
viewStyle: ViewStyleNamesType;
|
||||||
|
|
@ -49,6 +52,7 @@ const Filterbar = ({
|
||||||
const [showHidden, setShowHidden] = useState(false);
|
const [showHidden, setShowHidden] = useState(false);
|
||||||
const { filterHeight, setFilterHeight, showFilterItems, setShowFilterItems } =
|
const { filterHeight, setFilterHeight, showFilterItems, setShowFilterItems } =
|
||||||
useFilterBarTempConf();
|
useFilterBarTempConf();
|
||||||
|
const { setStartNotification } = useOutletContext() as ChannelBaseOutletContextType;
|
||||||
|
|
||||||
const currentViewStyle = userConfig[viewStyle];
|
const currentViewStyle = userConfig[viewStyle];
|
||||||
const currentHideWatched = userConfig[hideWatched];
|
const currentHideWatched = userConfig[hideWatched];
|
||||||
|
|
@ -84,11 +88,20 @@ const Filterbar = ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reindexSelected = async (ids: string[]) => {
|
||||||
|
queueReindex(ids, 'video');
|
||||||
|
if (setStartNotification !== undefined) setStartNotification(true);
|
||||||
|
};
|
||||||
|
|
||||||
const actionList = [
|
const actionList = [
|
||||||
{
|
{
|
||||||
label: 'Redownload',
|
label: 'Redownload',
|
||||||
handler: redownloadSelected,
|
handler: redownloadSelected,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Reindex',
|
||||||
|
handler: reindexSelected,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleActionSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleActionSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
|
|
||||||
|
|
@ -216,17 +216,16 @@ const GoogleCast = ({ video, setRefresh, onWatchStateChanged }: GoogleCastProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<>
|
<script
|
||||||
<script
|
async
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
|
src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
|
||||||
></script>
|
/>
|
||||||
|
|
||||||
{/* @ts-expect-error React does not know what to do with the google-cast-launcher, but it works. */}
|
{/* @ts-expect-error React does not know what to do with the google-cast-launcher, but it works. */}
|
||||||
<google-cast-launcher id="castbutton"></google-cast-launcher>
|
<google-cast-launcher id="castbutton"></google-cast-launcher>
|
||||||
</>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,6 @@ export default function MembershipAppsettings({ show_help_text }: { show_help_te
|
||||||
const [isLoadingSync, setIsLoadingSync] = useState(false);
|
const [isLoadingSync, setIsLoadingSync] = useState(false);
|
||||||
const [subSyncMessage, setSubSyncMessage] = useState('');
|
const [subSyncMessage, setSubSyncMessage] = useState('');
|
||||||
|
|
||||||
const fetchMembershipToken = async () => {
|
|
||||||
const apiTokenResponse = await APIClient<ApiTokenResponse>(
|
|
||||||
'/api/appsettings/membership/token/',
|
|
||||||
);
|
|
||||||
setMembershipApiToken(apiTokenResponse.data?.token || null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteMembershipToken = async () => {
|
const deleteMembershipToken = async () => {
|
||||||
await APIClient('/api/appsettings/membership/token/', { method: 'DELETE' });
|
await APIClient('/api/appsettings/membership/token/', { method: 'DELETE' });
|
||||||
setMembershipApiToken(null);
|
setMembershipApiToken(null);
|
||||||
|
|
@ -64,6 +57,12 @@ export default function MembershipAppsettings({ show_help_text }: { show_help_te
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const fetchMembershipToken = async () => {
|
||||||
|
const apiTokenResponse = await APIClient<ApiTokenResponse>(
|
||||||
|
'/api/appsettings/membership/token/',
|
||||||
|
);
|
||||||
|
setMembershipApiToken(apiTokenResponse.data?.token || null);
|
||||||
|
};
|
||||||
fetchMembershipToken();
|
fetchMembershipToken();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
import { useVideoSelectionStore } from '../stores/VideoSelectionStore';
|
import { useVideoSelectionStore } from '../stores/VideoSelectionStore';
|
||||||
import iconChecked from '/img/icon-seen.svg';
|
import iconChecked from '/img/icon-seen.svg';
|
||||||
import iconUnchecked from '/img/icon-unseen.svg';
|
import iconUnchecked from '/img/icon-unseen.svg';
|
||||||
|
import bitsToBytes from '../functions/bitsToBytes';
|
||||||
|
|
||||||
const StreamsTypeEmun = {
|
const StreamsTypeEmun = {
|
||||||
Video: 'video',
|
Video: 'video',
|
||||||
|
|
@ -97,9 +98,9 @@ const VideoListItemTable = ({ videoList, viewStyle }: VideoListItemProps) => {
|
||||||
<td>{`${videoStream?.width || '-'}x${videoStream?.height || '-'}`}</td>
|
<td>{`${videoStream?.width || '-'}x${videoStream?.height || '-'}`}</td>
|
||||||
<td>{humanFileSize(media_size, useSiUnits)}</td>
|
<td>{humanFileSize(media_size, useSiUnits)}</td>
|
||||||
<td>{videoStream?.codec || '-'}</td>
|
<td>{videoStream?.codec || '-'}</td>
|
||||||
<td>{humanFileSize(videoStream?.bitrate || 0, useSiUnits)}</td>
|
<td>{humanFileSize(bitsToBytes(videoStream?.bitrate || 0), useSiUnits)}</td>
|
||||||
<td>{audioStream?.codec || '-'}</td>
|
<td>{audioStream?.codec || '-'}</td>
|
||||||
<td>{humanFileSize(audioStream?.bitrate || 0, useSiUnits)}</td>
|
<td>{humanFileSize(bitsToBytes(audioStream?.bitrate || 0), useSiUnits)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -167,20 +167,6 @@ const VideoPlayer = ({
|
||||||
const [showInfoDialog, setShowInfoDialog] = useState(false);
|
const [showInfoDialog, setShowInfoDialog] = useState(false);
|
||||||
const [infoDialogContent, setInfoDialogContent] = useState('');
|
const [infoDialogContent, setInfoDialogContent] = useState('');
|
||||||
const [isTheaterMode, setIsTheaterMode] = useState(false);
|
const [isTheaterMode, setIsTheaterMode] = useState(false);
|
||||||
const [theaterModeKeyPressed, setTheaterModeKeyPressed] = useState(false);
|
|
||||||
|
|
||||||
const questionmarkPressed = useKeyPress('?');
|
|
||||||
const mutePressed = useKeyPress('m');
|
|
||||||
const fullscreenPressed = useKeyPress('f');
|
|
||||||
const subtitlesPressed = useKeyPress('c');
|
|
||||||
const increasePlaybackSpeedPressed = useKeyPress('>');
|
|
||||||
const decreasePlaybackSpeedPressed = useKeyPress('<');
|
|
||||||
const resetPlaybackSpeedPressed = useKeyPress('=');
|
|
||||||
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 videoId = video.youtube_id;
|
||||||
const videoUrl = video.media_url;
|
const videoUrl = video.media_url;
|
||||||
|
|
@ -205,6 +191,152 @@ const VideoPlayer = ({
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useKeyPress('m', () => {
|
||||||
|
setIsMuted(current => !current);
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPress('p', () => {
|
||||||
|
if (videoRef.current?.paused) {
|
||||||
|
videoRef.current.play();
|
||||||
|
} else {
|
||||||
|
videoRef.current?.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPress('>', () => {
|
||||||
|
const newSpeed = playbackSpeedIndex + 1;
|
||||||
|
|
||||||
|
if (videoRef.current && VIDEO_PLAYBACK_SPEEDS[newSpeed]) {
|
||||||
|
const speed = VIDEO_PLAYBACK_SPEEDS[newSpeed];
|
||||||
|
videoRef.current.playbackRate = speed;
|
||||||
|
|
||||||
|
setPlaybackSpeedIndex(newSpeed);
|
||||||
|
infoDialog(`${speed}x`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPress('<', () => {
|
||||||
|
const newSpeedIndex = playbackSpeedIndex - 1;
|
||||||
|
|
||||||
|
if (videoRef.current && VIDEO_PLAYBACK_SPEEDS[newSpeedIndex]) {
|
||||||
|
const speed = VIDEO_PLAYBACK_SPEEDS[newSpeedIndex];
|
||||||
|
videoRef.current.playbackRate = speed;
|
||||||
|
|
||||||
|
setPlaybackSpeedIndex(newSpeedIndex);
|
||||||
|
infoDialog(`${speed}x`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPress('=', () => {
|
||||||
|
const newSpeedIndex = 3;
|
||||||
|
|
||||||
|
if (videoRef.current && VIDEO_PLAYBACK_SPEEDS[newSpeedIndex]) {
|
||||||
|
const speed = VIDEO_PLAYBACK_SPEEDS[newSpeedIndex];
|
||||||
|
videoRef.current.playbackRate = speed;
|
||||||
|
|
||||||
|
setPlaybackSpeedIndex(newSpeedIndex);
|
||||||
|
infoDialog(`${speed}x`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPress('f', () => {
|
||||||
|
if (videoRef.current && videoRef.current.requestFullscreen && !document.fullscreenElement) {
|
||||||
|
videoRef.current.requestFullscreen().catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
infoDialog('Unable to enter fullscreen');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen().catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
infoDialog('Unable to exit fullscreen');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPress('c', () => {
|
||||||
|
if (!videoRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracks = [...videoRef.current.textTracks];
|
||||||
|
|
||||||
|
if (tracks.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastIndex = tracks.findIndex(x => x.mode === 'showing');
|
||||||
|
const active = tracks[lastIndex];
|
||||||
|
|
||||||
|
if (!active && lastSubtitleTack !== 0) {
|
||||||
|
tracks[lastSubtitleTack - 1].mode = 'showing';
|
||||||
|
} else if (active) {
|
||||||
|
active.mode = 'hidden';
|
||||||
|
|
||||||
|
setLastSubtitleTack(lastIndex + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPress('ArrowLeft', () => {
|
||||||
|
const currentCurrentTime = videoRef.current?.currentTime;
|
||||||
|
|
||||||
|
if (currentCurrentTime !== undefined && videoRef.current) {
|
||||||
|
infoDialog('- 5 seconds');
|
||||||
|
videoRef.current.currentTime = currentCurrentTime - 5;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPress('ArrowRight', () => {
|
||||||
|
const currentCurrentTime = videoRef.current?.currentTime;
|
||||||
|
|
||||||
|
if (currentCurrentTime !== undefined && videoRef.current) {
|
||||||
|
infoDialog('+ 5 seconds');
|
||||||
|
videoRef.current.currentTime = currentCurrentTime + 5;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPress('?', () => {
|
||||||
|
setShowHelpDialog(current => {
|
||||||
|
const next = !current;
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowHelpDialog(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPress('t', () => {
|
||||||
|
if (embed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsTheaterMode(current => {
|
||||||
|
const next = !current;
|
||||||
|
infoDialog(next ? 'Theater mode' : 'Normal mode');
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPress('Escape', () => {
|
||||||
|
if (embed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsTheaterMode(current => {
|
||||||
|
if (!current) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
infoDialog('Normal mode');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const handleVideoEnd =
|
const handleVideoEnd =
|
||||||
(
|
(
|
||||||
youtubeId: string,
|
youtubeId: string,
|
||||||
|
|
@ -238,174 +370,6 @@ const VideoPlayer = ({
|
||||||
onVideoEnd?.();
|
onVideoEnd?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mutePressed) {
|
|
||||||
setIsMuted(!isMuted);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [mutePressed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (pPausedPressed) {
|
|
||||||
if (videoRef.current?.paused) {
|
|
||||||
videoRef.current.play();
|
|
||||||
} else {
|
|
||||||
videoRef.current?.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [pPausedPressed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (increasePlaybackSpeedPressed) {
|
|
||||||
const newSpeed = playbackSpeedIndex + 1;
|
|
||||||
|
|
||||||
if (videoRef.current && VIDEO_PLAYBACK_SPEEDS[newSpeed]) {
|
|
||||||
const speed = VIDEO_PLAYBACK_SPEEDS[newSpeed];
|
|
||||||
videoRef.current.playbackRate = speed;
|
|
||||||
|
|
||||||
setPlaybackSpeedIndex(newSpeed);
|
|
||||||
infoDialog(`${speed}x`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [increasePlaybackSpeedPressed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (decreasePlaybackSpeedPressed) {
|
|
||||||
const newSpeedIndex = playbackSpeedIndex - 1;
|
|
||||||
|
|
||||||
if (videoRef.current && VIDEO_PLAYBACK_SPEEDS[newSpeedIndex]) {
|
|
||||||
const speed = VIDEO_PLAYBACK_SPEEDS[newSpeedIndex];
|
|
||||||
videoRef.current.playbackRate = speed;
|
|
||||||
|
|
||||||
setPlaybackSpeedIndex(newSpeedIndex);
|
|
||||||
infoDialog(`${speed}x`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [decreasePlaybackSpeedPressed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (resetPlaybackSpeedPressed) {
|
|
||||||
const newSpeedIndex = 3;
|
|
||||||
|
|
||||||
if (videoRef.current && VIDEO_PLAYBACK_SPEEDS[newSpeedIndex]) {
|
|
||||||
const speed = VIDEO_PLAYBACK_SPEEDS[newSpeedIndex];
|
|
||||||
videoRef.current.playbackRate = speed;
|
|
||||||
|
|
||||||
setPlaybackSpeedIndex(newSpeedIndex);
|
|
||||||
infoDialog(`${speed}x`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [resetPlaybackSpeedPressed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (fullscreenPressed) {
|
|
||||||
if (videoRef.current && videoRef.current.requestFullscreen && !document.fullscreenElement) {
|
|
||||||
videoRef.current.requestFullscreen().catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
infoDialog('Unable to enter fullscreen');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
document.exitFullscreen().catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
infoDialog('Unable to exit fullscreen');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [fullscreenPressed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (subtitlesPressed) {
|
|
||||||
if (videoRef.current) {
|
|
||||||
const tracks = [...videoRef.current.textTracks];
|
|
||||||
|
|
||||||
if (tracks.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastIndex = tracks.findIndex(x => x.mode === 'showing');
|
|
||||||
const active = tracks[lastIndex];
|
|
||||||
|
|
||||||
if (!active && lastSubtitleTack !== 0) {
|
|
||||||
tracks[lastSubtitleTack - 1].mode = 'showing';
|
|
||||||
} else {
|
|
||||||
if (active) {
|
|
||||||
active.mode = 'hidden';
|
|
||||||
|
|
||||||
setLastSubtitleTack(lastIndex + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [subtitlesPressed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (arrowLeftPressed || arrowRightPressed) {
|
|
||||||
let timeStep = 5;
|
|
||||||
|
|
||||||
if (arrowLeftPressed) {
|
|
||||||
infoDialog('- 5 seconds');
|
|
||||||
timeStep *= -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arrowRightPressed) {
|
|
||||||
infoDialog('+ 5 seconds');
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentCurrentTime = videoRef.current?.currentTime;
|
|
||||||
|
|
||||||
if (currentCurrentTime !== undefined && videoRef.current) {
|
|
||||||
videoRef.current.currentTime = currentCurrentTime + timeStep;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [arrowLeftPressed, arrowRightPressed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (questionmarkPressed) {
|
|
||||||
if (!showHelpDialog) {
|
|
||||||
setTimeout(() => {
|
|
||||||
setShowHelpDialog(false);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowHelpDialog(!showHelpDialog);
|
|
||||||
}
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [theaterModePressed, isTheaterMode, theaterModeKeyPressed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (embed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (escapePressed && isTheaterMode) {
|
|
||||||
setIsTheaterMode(false);
|
|
||||||
|
|
||||||
infoDialog('Normal mode');
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [escapePressed, isTheaterMode]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
function bitsToBytes(bits: number) {
|
||||||
|
return bits / 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default bitsToBytes;
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useEffectEvent, useRef, useState } from 'react';
|
||||||
|
|
||||||
// source: https://thibault.sh/react-hooks/use-key-press
|
// source: https://thibault.sh/react-hooks/use-key-press
|
||||||
export function useKeyPress(targetKey: string) {
|
export function useKeyPress(targetKey: string, onKeyDown?: () => void, onKeyUp?: () => void) {
|
||||||
const [isKeyPressed, setIsKeyPressed] = useState(false);
|
const [isKeyPressed, setIsKeyPressed] = useState(false);
|
||||||
|
const isKeyPressedRef = useRef(false);
|
||||||
|
const handleKeyDownEvent = useEffectEvent(() => {
|
||||||
|
onKeyDown?.();
|
||||||
|
});
|
||||||
|
const handleKeyUpEvent = useEffectEvent(() => {
|
||||||
|
onKeyUp?.();
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
|
@ -13,13 +20,21 @@ export function useKeyPress(targetKey: string) {
|
||||||
!event.altKey &&
|
!event.altKey &&
|
||||||
!event.altKey
|
!event.altKey
|
||||||
) {
|
) {
|
||||||
|
if (isKeyPressedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isKeyPressedRef.current = true;
|
||||||
setIsKeyPressed(true);
|
setIsKeyPressed(true);
|
||||||
|
handleKeyDownEvent();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyUp = (event: KeyboardEvent) => {
|
const handleKeyUp = (event: KeyboardEvent) => {
|
||||||
if (event.key === targetKey) {
|
if (event.key === targetKey) {
|
||||||
|
isKeyPressedRef.current = false;
|
||||||
setIsKeyPressed(false);
|
setIsKeyPressed(false);
|
||||||
|
handleKeyUpEvent();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Outlet, useLoaderData, useLocation, useSearchParams } from 'react-router-dom';
|
import { Outlet, useLoaderData, useSearchParams } from 'react-router-dom';
|
||||||
import Footer from '../components/Footer';
|
import Footer from '../components/Footer';
|
||||||
import Colours from '../configuration/colours/Colours';
|
import Colours from '../configuration/colours/Colours';
|
||||||
import { UserConfigType } from '../api/actions/updateUserConfig';
|
import { UserConfigType } from '../api/actions/updateUserConfig';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import Navigation from '../components/Navigation';
|
import Navigation from '../components/Navigation';
|
||||||
import { useAuthStore } from '../stores/AuthDataStore';
|
import { useAuthStore } from '../stores/AuthDataStore';
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
|
|
@ -43,12 +43,8 @@ const Base = () => {
|
||||||
const { setAppSettingsConfig } = useAppSettingsStore();
|
const { setAppSettingsConfig } = useAppSettingsStore();
|
||||||
|
|
||||||
const { userConfig, userAccount, appSettings, auth } = useLoaderData() as BaseLoaderData;
|
const { userConfig, userAccount, appSettings, auth } = useLoaderData() as BaseLoaderData;
|
||||||
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const currentPageFromUrl = Number(searchParams.get('page'));
|
const currentPageFromUrl = Number(searchParams.get('page'));
|
||||||
|
const currentPage = Number.isNaN(currentPageFromUrl) ? 0 : currentPageFromUrl;
|
||||||
const [currentPage, setCurrentPage] = useState(currentPageFromUrl);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAuth(auth);
|
setAuth(auth);
|
||||||
|
|
@ -59,40 +55,20 @@ const Base = () => {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const setCurrentPage = useCallback(
|
||||||
if (currentPageFromUrl !== currentPage) {
|
(page: number) => {
|
||||||
setCurrentPage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should only be executed when location.pathname changes.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentPageFromUrl !== currentPage) {
|
|
||||||
setCurrentPage(currentPageFromUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should only be executed when currentPageFromUrl changes.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [currentPageFromUrl]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentPageFromUrl !== currentPage) {
|
|
||||||
setSearchParams(params => {
|
setSearchParams(params => {
|
||||||
if (currentPage == 0) {
|
if (page === 0) {
|
||||||
params.delete('page');
|
params.delete('page');
|
||||||
} else {
|
} else {
|
||||||
params.set('page', currentPage.toString());
|
params.set('page', page.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
[setSearchParams],
|
||||||
// This should only be executed when currentPage changes.
|
);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [currentPage]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ const ChannelAbout = () => {
|
||||||
label="Reindex"
|
label="Reindex"
|
||||||
title={`Reindex Channel ${channel.channel_name}`}
|
title={`Reindex Channel ${channel.channel_name}`}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await queueReindex(channelId, ReindexTypeEnum.channel as ReindexType);
|
await queueReindex([channelId], ReindexTypeEnum.channel as ReindexType);
|
||||||
|
|
||||||
setReindex(true);
|
setReindex(true);
|
||||||
setStartNotification(true);
|
setStartNotification(true);
|
||||||
|
|
@ -194,7 +194,7 @@ const ChannelAbout = () => {
|
||||||
title={`Reindex Videos of ${channel.channel_name}`}
|
title={`Reindex Videos of ${channel.channel_name}`}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await queueReindex(
|
await queueReindex(
|
||||||
channelId,
|
[channelId],
|
||||||
ReindexTypeEnum.channel as ReindexType,
|
ReindexTypeEnum.channel as ReindexType,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ const Download = () => {
|
||||||
const vidTypeFilterFromUrl = searchParams.get('vid-type');
|
const vidTypeFilterFromUrl = searchParams.get('vid-type');
|
||||||
const errorFilterFromUrl = searchParams.get('error');
|
const errorFilterFromUrl = searchParams.get('error');
|
||||||
|
|
||||||
const [refresh, setRefresh] = useState(false);
|
const [refreshNonce, setRefreshNonce] = useState(0);
|
||||||
const [showHiddenForm, setShowHiddenForm] = useState(false);
|
const [showHiddenForm, setShowHiddenForm] = useState(false);
|
||||||
const [addAsAutoStart, setAddAsAutoStart] = useState(false);
|
const [addAsAutoStart, setAddAsAutoStart] = useState(false);
|
||||||
const [addAsFlat, setAddAsFlat] = useState(false);
|
const [addAsFlat, setAddAsFlat] = useState(false);
|
||||||
|
|
@ -113,34 +113,31 @@ const Download = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshDownloadQueue = () => {
|
||||||
|
setRefreshNonce(current => current + 1);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (refresh) {
|
const videosResponse = await loadDownloadQueue(
|
||||||
const videosResponse = await loadDownloadQueue(
|
currentPage,
|
||||||
currentPage,
|
channelFilterFromUrl,
|
||||||
channelFilterFromUrl,
|
vidTypeFilterFromUrl,
|
||||||
vidTypeFilterFromUrl,
|
errorFilterFromUrl,
|
||||||
errorFilterFromUrl,
|
showIgnored,
|
||||||
showIgnored,
|
searchInput,
|
||||||
searchInput,
|
);
|
||||||
);
|
const { data: channelResponseData } = videosResponse ?? {};
|
||||||
const { data: channelResponseData } = videosResponse ?? {};
|
const videoCount = channelResponseData?.paginate?.total_hits;
|
||||||
const videoCount = channelResponseData?.paginate?.total_hits;
|
|
||||||
|
|
||||||
if (videoCount && lastVideoCount !== videoCount) {
|
if (videoCount && lastVideoCount !== videoCount) {
|
||||||
setLastVideoCount(videoCount);
|
setLastVideoCount(videoCount);
|
||||||
}
|
|
||||||
|
|
||||||
setDownloadResponse(videosResponse);
|
|
||||||
setRefresh(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDownloadResponse(videosResponse);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setRefresh(true);
|
|
||||||
}, [
|
}, [
|
||||||
channelFilterFromUrl,
|
channelFilterFromUrl,
|
||||||
vidTypeFilterFromUrl,
|
vidTypeFilterFromUrl,
|
||||||
|
|
@ -148,6 +145,7 @@ const Download = () => {
|
||||||
currentPage,
|
currentPage,
|
||||||
showIgnored,
|
showIgnored,
|
||||||
searchInput,
|
searchInput,
|
||||||
|
refreshNonce,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -166,7 +164,7 @@ const Download = () => {
|
||||||
errorFilterFromUrl,
|
errorFilterFromUrl,
|
||||||
status,
|
status,
|
||||||
);
|
);
|
||||||
setRefresh(true);
|
refreshDownloadQueue();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -184,7 +182,7 @@ const Download = () => {
|
||||||
if (!isDone) {
|
if (!isDone) {
|
||||||
setRescanPending(false);
|
setRescanPending(false);
|
||||||
setDownloadPending(false);
|
setDownloadPending(false);
|
||||||
setRefresh(true);
|
refreshDownloadQueue();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -308,7 +306,7 @@ const Download = () => {
|
||||||
force: addAsForce,
|
force: addAsForce,
|
||||||
});
|
});
|
||||||
setDownloadQueueText('');
|
setDownloadQueueText('');
|
||||||
setRefresh(true);
|
refreshDownloadQueue();
|
||||||
setShowHiddenForm(false);
|
setShowHiddenForm(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
@ -329,7 +327,7 @@ const Download = () => {
|
||||||
const newParams = new URLSearchParams();
|
const newParams = new URLSearchParams();
|
||||||
newParams.set('ignored', String(!showIgnored));
|
newParams.set('ignored', String(!showIgnored));
|
||||||
setSearchParams(newParams);
|
setSearchParams(newParams);
|
||||||
setRefresh(true);
|
refreshDownloadQueue();
|
||||||
}}
|
}}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showIgnored}
|
checked={showIgnored}
|
||||||
|
|
@ -539,7 +537,7 @@ const Download = () => {
|
||||||
channelFilterFromUrl,
|
channelFilterFromUrl,
|
||||||
vidTypeFilterFromUrl,
|
vidTypeFilterFromUrl,
|
||||||
);
|
);
|
||||||
setRefresh(true);
|
refreshDownloadQueue();
|
||||||
setShowDeleteConfirm(false);
|
setShowDeleteConfirm(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -563,7 +561,7 @@ const Download = () => {
|
||||||
<Fragment
|
<Fragment
|
||||||
key={`${download.channel_id}_${download.timestamp}_${download.youtube_id}`}
|
key={`${download.channel_id}_${download.timestamp}_${download.youtube_id}`}
|
||||||
>
|
>
|
||||||
<DownloadListItem download={download} setRefresh={setRefresh} />
|
<DownloadListItem download={download} setRefresh={refreshDownloadQueue} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -284,7 +284,7 @@ const Playlist = () => {
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setReindex(true);
|
setReindex(true);
|
||||||
|
|
||||||
await queueReindex(playlist.playlist_id, 'playlist');
|
await queueReindex([playlist.playlist_id], 'playlist');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
|
|
@ -294,7 +294,7 @@ const Playlist = () => {
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setReindex(true);
|
setReindex(true);
|
||||||
|
|
||||||
await queueReindex(playlist.playlist_id, 'playlist', true);
|
await queueReindex([playlist.playlist_id], 'playlist', true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import NotFound from './NotFound';
|
||||||
import { ApiResponseType } from '../functions/APIClient';
|
import { ApiResponseType } from '../functions/APIClient';
|
||||||
import VideoThumbnail from '../components/VideoThumbail';
|
import VideoThumbnail from '../components/VideoThumbail';
|
||||||
import { ViewStylesEnum, ViewStylesType } from '../configuration/constants/ViewStyle';
|
import { ViewStylesEnum, ViewStylesType } from '../configuration/constants/ViewStyle';
|
||||||
|
import bitsToBytes from '../functions/bitsToBytes';
|
||||||
|
|
||||||
const isInPlaylist = (videoId: string, playlist: PlaylistType) => {
|
const isInPlaylist = (videoId: string, playlist: PlaylistType) => {
|
||||||
return playlist.playlist_entries.some(entry => {
|
return playlist.playlist_entries.some(entry => {
|
||||||
|
|
@ -108,7 +109,6 @@ const Video = () => {
|
||||||
const { appSettingsConfig } = useAppSettingsStore();
|
const { appSettingsConfig } = useAppSettingsStore();
|
||||||
const { userConfig } = useUserConfigStore();
|
const { userConfig } = useUserConfigStore();
|
||||||
|
|
||||||
const [videoEnded, setVideoEnded] = useState(false);
|
|
||||||
const [seekToTimestamp, setSeekToTimestamp] = useState<number>();
|
const [seekToTimestamp, setSeekToTimestamp] = useState<number>();
|
||||||
const [playlistAutoplay, setPlaylistAutoplay] = useState(
|
const [playlistAutoplay, setPlaylistAutoplay] = useState(
|
||||||
localStorage.getItem('playlistAutoplay') === 'true',
|
localStorage.getItem('playlistAutoplay') === 'true',
|
||||||
|
|
@ -179,24 +179,6 @@ const Video = () => {
|
||||||
localStorage.setItem('playlistIdForAutoplay', playlistIdForAutoplay || '');
|
localStorage.setItem('playlistIdForAutoplay', playlistIdForAutoplay || '');
|
||||||
}, [playlistAutoplay, playlistIdForAutoplay]);
|
}, [playlistAutoplay, playlistIdForAutoplay]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (videoEnded && playlistAutoplay) {
|
|
||||||
const playlist = videoPlaylistNavResponseData?.find(playlist => {
|
|
||||||
return playlist.playlist_meta.playlist_id === playlistIdForAutoplay;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (playlist) {
|
|
||||||
const nextYoutubeId = playlist.playlist_next?.youtube_id;
|
|
||||||
|
|
||||||
if (nextYoutubeId) {
|
|
||||||
setVideoEnded(false);
|
|
||||||
navigate(Routes.Video(nextYoutubeId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [videoEnded, playlistAutoplay]);
|
|
||||||
|
|
||||||
const errorMessage = videoResponseError?.error;
|
const errorMessage = videoResponseError?.error;
|
||||||
|
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
|
|
@ -235,7 +217,18 @@ const Video = () => {
|
||||||
setRefreshVideoList(true);
|
setRefreshVideoList(true);
|
||||||
}}
|
}}
|
||||||
onVideoEnd={() => {
|
onVideoEnd={() => {
|
||||||
setVideoEnded(true);
|
if (!playlistAutoplay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playlist = videoPlaylistNavResponseData?.find(playlist => {
|
||||||
|
return playlist.playlist_meta.playlist_id === playlistIdForAutoplay;
|
||||||
|
});
|
||||||
|
const nextYoutubeId = playlist?.playlist_next?.youtube_id;
|
||||||
|
|
||||||
|
if (nextYoutubeId) {
|
||||||
|
navigate(Routes.Video(nextYoutubeId));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -342,7 +335,7 @@ const Video = () => {
|
||||||
label="Reindex"
|
label="Reindex"
|
||||||
title={`Reindex ${video.title}`}
|
title={`Reindex ${video.title}`}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await queueReindex(video.youtube_id, 'video');
|
await queueReindex([video.youtube_id], 'video');
|
||||||
setReindex(true);
|
setReindex(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -468,7 +461,7 @@ const Video = () => {
|
||||||
return (
|
return (
|
||||||
<p key={stream.index}>
|
<p key={stream.index}>
|
||||||
{capitalizeFirstLetter(stream.type)}: {stream.codec}{' '}
|
{capitalizeFirstLetter(stream.type)}: {stream.codec}{' '}
|
||||||
{humanFileSize(stream.bitrate, useSiUnits)}/s
|
{humanFileSize(bitsToBytes(stream.bitrate), useSiUnits)}/s
|
||||||
{stream.width && (
|
{stream.width && (
|
||||||
<>
|
<>
|
||||||
<span className="space-carrot">|</span> {stream.width}x{stream.height}
|
<span className="space-carrot">|</span> {stream.width}x{stream.height}
|
||||||
|
|
|
||||||
|
|
@ -625,6 +625,8 @@ video:-webkit-full-screen {
|
||||||
.video-thumb img {
|
.video-thumb img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
background: var(--highlight-bg-transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-tags {
|
.video-tags {
|
||||||
|
|
@ -1088,6 +1090,8 @@ video:-webkit-full-screen {
|
||||||
|
|
||||||
.channel-banner img {
|
.channel-banner img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
aspect-ratio: 18 / 3;
|
||||||
|
background: var(--highlight-bg-transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-banner.grid {
|
.channel-banner.grid {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
-r backend/requirements.plugins.txt
|
|
||||||
-r backend/requirements.txt
|
-r backend/requirements.txt
|
||||||
ipython==9.12.0
|
ipython==9.12.0
|
||||||
pre-commit==4.5.1
|
pre-commit==4.5.1
|
||||||
pylint-django==2.7.0
|
pylint-django==2.7.0
|
||||||
pylint==4.0.5
|
pylint==4.0.5
|
||||||
pytest-django==4.12.0
|
pytest-django==4.12.0
|
||||||
pytest==9.0.2
|
pytest==9.0.3
|
||||||
python-dotenv==1.2.2
|
python-dotenv==1.2.2
|
||||||
requirementscheck==0.1.0
|
requirementscheck==0.1.0
|
||||||
types-requests==2.33.0.20260327
|
types-requests==2.33.0.20260408
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue