Compare commits

...

14 Commits

Author SHA1 Message Date
f09945a51d temporary fix for https://github.com/nodejs/corepack/issues/612 2025-02-03 10:22:58 +01:00
beaa6240b0 updated node builder version 2025-02-03 10:18:00 +01:00
3dd2f0995b fixed postprocessor args 2025-02-03 10:10:58 +01:00
Robi Hahn
2f2eca2bff i18n: hungarian translation (#250) 2025-01-30 18:24:57 +01:00
Patrick Thoelken
5073113568 refac: i18n (#244) 2025-01-16 16:10:00 +01:00
Marco Piovanello
430bfabfb4 i18n system refactor (#243) 2025-01-15 20:47:58 +01:00
160a2721f9 Code refactoring, renabled app title customization.
With React 19 header title API title doesn't need react-helmet anymore :)
2025-01-13 10:49:55 +01:00
Augusto Vasconcelos
6f0187bccc Update settings.ts (enable portuguese-br) (#238)
Enable portuguese-br and sort alphabetically
2025-01-08 08:31:43 +01:00
Augusto Vasconcelos
801c89df5d Update i18n.yaml (Brazilian Translation) (#237)
Added Brazilian Translation
2025-01-07 15:52:08 +01:00
Michael M. Chang
49fdaeb42a update username (#234) 2025-01-01 09:38:59 +01:00
fc07c08c58 fixed default template not selected 2024-12-23 09:35:22 +01:00
f9e829dce6 added status API endpoint 2024-12-19 13:08:25 +01:00
17fb608f45 code refactoring 2024-12-19 12:18:36 +01:00
9d3861ab39 Added better archive functionalty (backend side atm)
Code refactoring
2024-12-18 11:59:17 +01:00
90 changed files with 2898 additions and 1357 deletions

1
.gitignore vendored
View File

@@ -20,6 +20,7 @@ cookies.txt
__debug*
ui/
.idea
.idea/
frontend/.pnp.cjs
frontend/.pnp.loader.mjs
frontend/.yarn/install-state.gz

View File

@@ -1,8 +1,8 @@
# Node (pnpm) ------------------------------------------------------------------
FROM node:20-slim AS ui
FROM node:22-slim AS ui
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@10.0.0 --activate && corepack enable
COPY . /usr/src/yt-dlp-webui
WORKDIR /usr/src/yt-dlp-webui/frontend

View File

@@ -1,6 +1,6 @@
> [!NOTE]
> A poll is up to decide the future of yt-dlp-web-ui frontend! If you're interested you can take part.
> https://github.com/marcopeocchi/yt-dlp-web-ui/discussions/223
> https://github.com/marcopiovanello/yt-dlp-web-ui/discussions/223
# yt-dlp Web UI
@@ -10,14 +10,14 @@ High performance extendeable web ui and RPC server for yt-dlp with low impact on
Created for the only purpose of *fetching* videos from my server/nas and monitor upcoming livestreams.
**Docker images are available on [Docker Hub](https://hub.docker.com/r/marcobaobao/yt-dlp-webui) or [ghcr.io](https://github.com/marcopeocchi/yt-dlp-web-ui/pkgs/container/yt-dlp-web-ui)**.
**Docker images are available on [Docker Hub](https://hub.docker.com/r/marcobaobao/yt-dlp-webui) or [ghcr.io](https://github.com/marcopiovanello/yt-dlp-web-ui/pkgs/container/yt-dlp-web-ui)**.
```sh
docker pull marcobaobao/yt-dlp-webui
```
```sh
# latest dev
docker pull ghcr.io/marcopeocchi/yt-dlp-web-ui:latest
docker pull ghcr.io/marcopiovanello/yt-dlp-web-ui:latest
```
## Donate to yt-dlp-webui development
@@ -31,7 +31,7 @@ docker pull ghcr.io/marcopeocchi/yt-dlp-web-ui:latest
![image](https://github.com/user-attachments/assets/16450a40-cda6-4c8b-9d20-8ec36282f6ed)
## Video showcase
[app.webm](https://github.com/marcopeocchi/yt-dlp-web-ui/assets/35533749/91545bc4-233d-4dde-8504-27422cb26964)
[app.webm](https://github.com/marcopiovanello/yt-dlp-web-ui/assets/35533749/91545bc4-233d-4dde-8504-27422cb26964)
## Settings
@@ -52,7 +52,7 @@ This feature is disabled by default as this intended to be used to retrieve the
To enable it just go to the settings page and enable the **Enable video/audio formats selection** flag!
## [Docker](https://github.com/marcopeocchi/yt-dlp-web-ui/pkgs/container/yt-dlp-web-ui) run
## [Docker](https://github.com/marcopiovanello/yt-dlp-web-ui/pkgs/container/yt-dlp-web-ui) run
```sh
docker pull marcobaobao/yt-dlp-webui
docker run -d -p 3033:3033 -v <your dir>:/downloads marcobaobao/yt-dlp-webui
@@ -110,7 +110,7 @@ services:
restart: unless-stopped
```
## [Prebuilt binaries](https://github.com/marcopeocchi/yt-dlp-web-ui/releases) installation
## [Prebuilt binaries](https://github.com/marcopiovanello/yt-dlp-web-ui/releases) installation
```sh
# download the latest release from the releases page

View File

@@ -6,7 +6,7 @@
"dev": "vite",
"build": "vite build"
},
"author": "marcopeocchi",
"author": "marcopiovanello",
"license": "GPL-3.0-only",
"private": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
import { ThemeProvider } from '@emotion/react'
import ArchiveIcon from '@mui/icons-material/Archive'
import ChevronLeft from '@mui/icons-material/ChevronLeft'
import CloudDownloadIcon from '@mui/icons-material/CloudDownload'
import Dashboard from '@mui/icons-material/Dashboard'
import LiveTvIcon from '@mui/icons-material/LiveTv'
import Menu from '@mui/icons-material/Menu'
@@ -16,7 +16,8 @@ import ListItemIcon from '@mui/material/ListItemIcon'
import ListItemText from '@mui/material/ListItemText'
import Toolbar from '@mui/material/Toolbar'
import Typography from '@mui/material/Typography'
import { grey, red } from '@mui/material/colors'
import { grey } from '@mui/material/colors'
import { useAtomValue } from 'jotai'
import { useMemo, useState } from 'react'
import { Link, Outlet } from 'react-router-dom'
import { settingsState } from './atoms/settings'
@@ -28,7 +29,6 @@ import SocketSubscriber from './components/SocketSubscriber'
import ThemeToggler from './components/ThemeToggler'
import { useI18n } from './hooks/useI18n'
import Toaster from './providers/ToasterProvider'
import { useAtomValue } from 'jotai'
import { getAccentValue } from './utils'
export default function Layout() {
@@ -42,7 +42,7 @@ export default function Layout() {
palette: {
mode: settings.theme,
primary: {
main: getAccentValue(settings.accent)
main: getAccentValue(settings.accent, settings.theme)
},
background: {
default: settings.theme === 'light' ? grey[50] : '#121212'
@@ -57,6 +57,7 @@ export default function Layout() {
return (
<ThemeProvider theme={theme}>
<title>{settings.appTitle}</title>
<SocketSubscriber />
<Box sx={{ display: 'flex' }}>
<CssBaseline />
@@ -113,7 +114,7 @@ export default function Layout() {
<ListItemText primary={i18n.t('homeButtonLabel')} />
</ListItemButton>
</Link>
<Link to={'/archive'} style={
{/* <Link to={'/archive'} style={
{
textDecoration: 'none',
color: mode === 'dark' ? '#ffffff' : '#000000DE'
@@ -125,6 +126,19 @@ export default function Layout() {
</ListItemIcon>
<ListItemText primary={i18n.t('archiveButtonLabel')} />
</ListItemButton>
</Link> */}
<Link to={'/filebrowser'} style={
{
textDecoration: 'none',
color: mode === 'dark' ? '#ffffff' : '#000000DE'
}
}>
<ListItemButton>
<ListItemIcon>
<CloudDownloadIcon />
</ListItemIcon>
<ListItemText primary={i18n.t('archiveButtonLabel')} />
</ListItemButton>
</Link>
<Link to={'/monitor'} style={
{

View File

@@ -1,804 +1,22 @@
---
# Check the i18n src/assets/i18n folder.
#
# This file maps the language name to its translations file
# english -> /src/assets/i18n/en_US.yaml
languages:
english:
urlInput: Video URL (one per line)
statusTitle: Status
statusReady: Ready
selectFormatButton: Select format
startButton: Start
abortAllButton: Abort All
updateBinButton: Update yt-dlp binary
darkThemeButton: Dark theme
lightThemeButton: Light theme
settingsAnchor: Settings
serverAddressTitle: Server address
serverPortTitle: Port
extractAudioCheckbox: Extract audio
noMTimeCheckbox: Don't set file modification time
bgReminder: Once you close this page the download will continue in the background.
toastConnected: 'Connected to '
toastUpdated: Updated yt-dlp binary!
formatSelectionEnabler: Enable video/audio formats selection
themeSelect: 'Theme'
languageSelect: 'Language'
overridesAnchor: Overrides
pathOverrideOption: Enable output path overriding
filenameOverrideOption: Enable output file name overriding
customFilename: Custom filename (leave blank to use default)
customPath: Custom path
customArgs: Enable custom yt-dlp args (great power = great responsibilities)
customArgsInput: Custom yt-dlp arguments
rpcConnErr: Error while conencting to RPC server
splashText: No active downloads
archiveTitle: Archive
clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may close this window)
restartAppMessage: Needs a page reload to take effect
servedFromReverseProxyCheckbox: Is behind a reverse proxy
urlBase: URL base, for reverse proxy support (subdir), defaults to empty
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
rpcPollingTimeTitle: RPC polling time
rpcPollingTimeDescription: A lower interval results in higher CPU usage (server and client side)
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
german:
urlInput: Video URL
statusTitle: Status
statusReady: Bereit
selectFormatButton: Format auswählen
startButton: Start
abortAllButton: Alle Abbrechen
updateBinButton: yt-dlp Binärdatei aktualisieren
darkThemeButton: Dunkel Modus
lightThemeButton: Hell Modus
settingsAnchor: Einstellungen
serverAddressTitle: Server Adresse
serverPortTitle: Port
extractAudioCheckbox: Audio extrahieren
noMTimeCheckbox: Datei-Änderungszeitpunkt nicht festlegen
bgReminder: Sobald Sie diese Seite schließen, wird der Download im Hintergrund fortgesetzt.
toastConnected: 'Verbunden mit '
toastUpdated: yt-dlp Binärdatei aktualisiert!
formatSelectionEnabler: Video/Audio Format auswählbar
themeSelect: 'Modus'
languageSelect: 'Sprache'
overridesAnchor: Überschreibungen
pathOverrideOption: Ausgabe-Pfad Überschreibung aktivieren
filenameOverrideOption: Ausgabe-Dateiname Überschreibung aktivieren
customFilename: Custom filename (leave blank to use default)
customPath: Benutzerdefinierter Pfad
customArgs: Benutzerdefinierte yt-dlp Argumente aktivieren (viel Macht = viel Verantwortung)
customArgsInput: Benutzerdefinierte yt-dlp Argumente
rpcConnErr: Fehler beim Verbinden mit RPC Server
splashText: Keine aktiven Downloads
archiveTitle: Archiv
clipboardAction: URL in Zwischenablage kopiert
playlistCheckbox: Playlist herunterladen (es wird einige Zeit dauern, nach dem Absenden können Sie dieses Fenster schließen)
restartAppMessage: Erfordert ein Neuladen der Seite, um wirksam zu werden
servedFromReverseProxyCheckbox: Ist hinter einem Reverse Proxy Unterordner
newDownloadButton: Neuer Download
homeButtonLabel: Home
archiveButtonLabel: Archiv
settingsButtonLabel: Einstellungen
rpcAuthenticationLabel: RPC Authentifizierung
themeTogglerLabel: Modus Umschalter
loadingLabel: Lädt...
appTitle: App Titel
savedTemplates: Gespeicherte Vorlage
templatesEditor: Vorlagen Bearbeiter
templatesEditorNameLabel: Vorlagen Name
templatesEditorContentLabel: Vorlagen Inhalt
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
french:
urlInput: URL vidéo de YouTube ou d'un autre service pris en charge
statusTitle: Statut
statusReady: Prêt
selectFormatButton: Sélectionner le format
startButton: Démarrer
abortAllButton: Tout arrêter
updateBinButton: Mettre à jour l'exécutable yt-dlp
darkThemeButton: Thème sombre
lightThemeButton: Thème clair
settingsAnchor: Paramètres
serverAddressTitle: Adresse du serveur
serverPortTitle: Port
extractAudioCheckbox: Extraire l'audio
noMTimeCheckbox: Ne pas définir le temps de modification du fichier
bgReminder: Une fois que vous aurez fermé cette page, le téléchargement continuera en arrière-plan.
toastConnected: 'Connecté à '
toastUpdated: L'exécutable yt-dlp a été mis à jour !
formatSelectionEnabler: Activer la sélection des formats vidéo/audio
themeSelect: 'Thème'
languageSelect: 'Langue'
overridesAnchor: Remplacer
pathOverrideOption: Activer le remplacement du chemin de sortie
filenameOverrideOption: Activer le remplacement du nom du fichier de sortie
customFilename: Nom de fichier personnalisé (laisser vide pour utiliser le nom par défaut)
customPath: Chemin personnalisé
customArgs: Activer les args personnalisés yt-dlp (grand pouvoir = grandes responsabilités)
customArgsInput: Arguments yt-dlp personnalisés
rpcConnErr: Erreur lors de la connexion au serveur RPC
splashText: Aucun téléchargement actif
archiveTitle: Archive
clipboardAction: URL copiée dans le presse-papiers
playlistCheckbox: Télécharger la liste de lecture (cela prendra du temps, vous pouvez fermer cette fenêtre après l'avoir validée)
restartAppMessage: Nécessite un rechargement de la page pour prendre effet
servedFromReverseProxyCheckbox: Est derrière un sous-dossier de proxy inverse
notConnectedText: not connected
settingsLabel: Settings
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: Nom de l'application
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
italian:
urlInput: URL Video (uno per linea)
statusTitle: Stato
startButton: Inizia
statusReady: Pronto
abortAllButton: Termina tutto
updateBinButton: Aggiorna yt-dlp
darkThemeButton: Tema scuro
lightThemeButton: Tema chiaro
settingsAnchor: Impostazioni
serverAddressTitle: Indirizzo server
serverPortTitle: Porta
extractAudioCheckbox: Estrai l'audio
noMTimeCheckbox: Non impostare la proprietà "Data ultima modifica"
bgReminder: Chiusa questa UI il download continuerà in background.
toastConnected: 'Connesso a '
toastUpdated: yt-dlp aggiornato con successo!
formatSelectionEnabler: Abilita la selezione dei formati audio/video
themeSelect: 'Tema'
languageSelect: 'Lingua'
overridesAnchor: Sovrascritture
pathOverrideOption: Abilita sovrascrittura percorso di output
filenameOverrideOption: Abilita sovrascrittura del nome del file di output
customFilename: Custom filename (leave blank to use default)
customPath: Custom path
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
customArgsInput: Custom yt-dlp arguments
rpcConnErr: Error nella connessione al server RPC
splashText: Nessun download attivo
archiveTitle: Archivio
clipboardAction: URL copiato negli appunti
playlistCheckbox: Download playlist (richiederà tempo, puoi chiudere la finestra dopo l'inoltro)
restartAppMessage: La finestra deve essere ricaricata perché abbia effetto
servedFromReverseProxyCheckbox: Is behind a reverse proxy
newDownloadButton: Nuovo download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: Titolo applicazione
savedTemplates: Template salvati
templatesEditor: Editor template
templatesEditorNameLabel: Nome template
templatesEditorContentLabel: Contentunto template
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
chinese:
urlInput: 视频 URL
statusTitle: 状态
statusReady: 就绪
selectFormatButton: 选择格式
startButton: 开始
abortAllButton: 全部中止
updateBinButton: 更新 yt-dlp 可执行文件
darkThemeButton: 黑暗主题
lightThemeButton: 明亮主题
settingsAnchor: 设置
serverAddressTitle: 服务器地址
serverPortTitle: 端口
extractAudioCheckbox: 提取音频
noMTimeCheckbox: 不设置文件修改时间
bgReminder: 关闭页面后,下载会继续在后台运行。
toastConnected: '已连接到 '
toastUpdated: 已更新 yt-dlp 可执行文件!
formatSelectionEnabler: 启用视频/音频格式选择
themeSelect: '主题'
languageSelect: '语言'
overridesAnchor: 覆盖
pathOverrideOption: 启用输出路径覆盖
filenameOverrideOption: 启用输出文件名覆盖
customFilename: 自定义文件名(留空使用默认值)
customPath: 自定义路径
customArgs: 启用自定义 yt-dlp 参数(能力越大 = 责任越大)
customArgsInput: 自定义 yt-dlp 参数
rpcConnErr: 连接 RPC 服务器发生错误
splashText: 没有正在进行的下载
archiveTitle: 归档
clipboardAction: 复制 URL 到剪贴板
playlistCheckbox: 下载播放列表(可能需要一段时间,提交后可以关闭页面等待)
restartAppMessage: 需要刷新页面才能生效
servedFromReverseProxyCheckbox: 处于反向代理的子目录后
newDownloadButton: 新下载
homeButtonLabel: 主页
archiveButtonLabel: 归档
settingsButtonLabel: 设置
rpcAuthenticationLabel: RPC 身份验证
themeTogglerLabel: 主题切换
loadingLabel: 正在加载…
appTitle: App 标题
savedTemplates: 保存模板
templatesEditor: 模板编辑器
templatesEditorNameLabel: 模板名称
templatesEditorContentLabel: 模板内容
logsTitle: '日志'
awaitingLogs: '正在等待日志…'
bulkDownload: '下载 zip 压缩包中的文件'
livestreamURLInput: 直播 URL
livestreamStatusWaiting: 等待直播开始
livestreamStatusDownloading: 下载中
livestreamStatusCompleted: 已完成
livestreamStatusErrored: 发生错误
livestreamStatusUnknown: 未知
livestreamDownloadInfo: |
本功能将会监控即将开始的直播流,每个进程都会传入参数:--wait-for-video 10 重试间隔10秒
如果直播已经开始,那么依然可以下载,但是不会记录下载进度。
直播开始后,将会转移到下载页面
livestreamExperimentalWarning: 实验性功能可能存在未知Bug请谨慎使用
accentSelect: 'Accent'
spanish:
urlInput: URL de YouTube u otro servicio compatible
statusTitle: Estado
startButton: Iniciar
statusReady: Listo
abortAllButton: Cancelar Todo
updateBinButton: Actualizar el binario yt-dlp
darkThemeButton: Tema oscuro
lightThemeButton: Tema claro
settingsAnchor: Ajustes
serverAddressTitle: Dirección del servidor
serverPortTitle: Puerto
extractAudioCheckbox: Extraer audio
noMTimeCheckbox: No guardar el tiempo de modificación del archivo
bgReminder: Si cierras esta página, la descarga continuará en segundo plano.
toastConnected: 'Conectado a'
toastUpdated: ¡El binario yt-dlp está actualizado!
formatSelectionEnabler: Habilitar la selección de formatos de video/audio
themeSelect: 'Tema'
languageSelect: 'Idiomas'
overridesAnchor: Anulaciones
pathOverrideOption: Sobreescribir en la ruta de salida
filenameOverrideOption: Sobreescribir el nombre del fichero
customFilename: Nombre de archivo personalizado (en blanco para usar el predeterminado)
customPath: Ruta personalizada
customArgs: Habilitar los argumentos yt-dlp personalizados (un gran poder conlleva una gran responsabilidad)
customArgsInput: Argumentos yt-dlp personalizados
rpcConnErr: Error al conectarse al servidor RPC
splashText: No active downloads
archiveTitle: Archive
clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
russian:
urlInput: URL-адрес YouTube или любого другого поддерживаемого сервиса
statusTitle: Статус
startButton: Начать
statusReady: Готово
abortAllButton: Прервать все
updateBinButton: Обновить бинарный файл yt-dlp
darkThemeButton: Темная тема
lightThemeButton: Светлая тема
settingsAnchor: Настройки
serverAddressTitle: Адрес сервера
serverPortTitle: Порт
extractAudioCheckbox: Извлечь аудио
noMTimeCheckbox: Не устанавливать время модификации файла
bgReminder: Как только вы закроете эту страницу, загрузка продолжится в фоновом режиме.
toastConnected: 'Подключен к '
toastUpdated: Бинарный файл yt-dlp обновлен!
formatSelectionEnabler: Активировать выбор видео/аудио форматов
themeSelect: 'Тема'
languageSelect: 'Язык'
overridesAnchor: Переопределить
pathOverrideOption: Активировать переопределение выходного пути
filenameOverrideOption: Активировать переопределение имени выходного файла
customFilename: Задать имя файла (оставьте пустым, чтобы использовать значение по умолчанию)
customPath: Задать путь
customArgs: Включить настраиваемые аргументы yt-dlp (большая сила = большая ответственность)
customArgsInput: Пользовательские аргументы yt-dlp
rpcConnErr: Ошибка при подключении к серверу RPC
splashText: Нет активных загрузок
archiveTitle: Архив
clipboardAction: URL скопирован в буфер обмена
playlistCheckbox: Скачать плейлист. Это займет время, после отправки вы сможете закрыть окно
servedFromReverseProxyCheckbox: Находится за обратным прокси
newDownloadButton: Новая загрузка
homeButtonLabel: Home
archiveButtonLabel: Архив
settingsButtonLabel: Настройки
rpcAuthenticationLabel: RPC-аутентификация
themeTogglerLabel: Переключить тему
loadingLabel: Загрузка...
appTitle: Название приложения
savedTemplates: Сохраненные шаблоны
templatesEditor: Редактор шаблонов
templatesEditorNameLabel: Имя шаблона
templatesEditorContentLabel: Содержание шаблона
logsTitle: 'Логи'
awaitingLogs: 'Ожидание логов...'
bulkDownload: 'Скачать файлы в zip архиве'
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
korean:
urlInput: YouTube나 다른 지원되는 사이트의 URL
statusTitle: 상태
startButton: 시작
statusReady: 준비됨
abortAllButton: 모두 중단
updateBinButton: yt-dlp 바이너리 업데이트
darkThemeButton: 다크 모드
lightThemeButton: 라이트 모드
settingsAnchor: 설정
serverAddressTitle: 서버 주소
serverPortTitle: Port
extractAudioCheckbox: 오디오 추출
noMTimeCheckbox: 파일 수정 시간을 설정하지 않음
bgReminder: 이 페이지를 닫아도 백그라운드에서 다운로드가 계속됩니다
toastConnected: '다음으로 연결됨 '
toastUpdated: yt-dlp 바이너리를 업데이트 했습니다
formatSelectionEnabler: 비디오/오디오 포멧 옵션 표시
themeSelect: 'Theme'
languageSelect: 'Language'
overridesAnchor: Overrides
pathOverrideOption: Enable output path overriding
filenameOverrideOption: Enable output file name overriding
customFilename: Custom filename (leave blank to use default)
customPath: Custom path
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
customArgsInput: Custom yt-dlp arguments
rpcConnErr: Error while conencting to RPC server
splashText: No active downloads
archiveTitle: Archive
clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
japanese:
urlInput: YouTubeまたはサポート済み動画のURL
statusTitle: 状態
statusReady: 準備
selectFormatButton: フォーマット選択
startButton: 開始
abortAllButton: すべて中止
updateBinButton: yt-dlp更新
darkThemeButton: 黒テーマ
lightThemeButton: 白テーマ
settingsAnchor: 設定
serverAddressTitle: サーバーアドレス
serverPortTitle: ポート番号
extractAudioCheckbox: 音質
noMTimeCheckbox: ファイル時間の修正をしない
bgReminder: このページを閉じてもバックグラウンドでダウンロードを続けます
toastConnected: '接続中 '
toastUpdated: yt-dlpを更新しました!
formatSelectionEnabler: 選択可能な動画/音源
themeSelect: 'テーマ'
languageSelect: '言語'
overridesAnchor: 上書き
pathOverrideOption: 保存するディレクトリ
filenameOverrideOption: ファイル名の上書き
customFilename: (空白の場合は元のファイル名)
customPath: 保存先
customArgs: yt-dlpのオプションの有効化 (最適設定にする場合)
customArgsInput: yt-dlpのオプション
rpcConnErr: RPCサーバーへの接続中にエラーが発生しました
splashText: アクティブなダウンロードはありません
archiveTitle: アーカイブ
clipboardAction: URLをクリップボードにコピーしました
playlistCheckbox: プレイリストをダウンロード (これには時間がかかりますが、処理中はウィンドウを閉じることができます)
servedFromReverseProxyCheckbox: リバースプロキシのサブフォルダにあります
newDownloadButton: 新しくダウンロード
homeButtonLabel: ホーム
archiveButtonLabel: アーカイブ
settingsButtonLabel: 設定
rpcAuthenticationLabel: RPC認証
themeTogglerLabel: テーマ切り替え
loadingLabel: 読み込み中...
appTitle: アプリタイトル
savedTemplates: 保存したテンプレート
templatesEditor: テンプレートエディター
templatesEditorNameLabel: テンプレート名
templatesEditorContentLabel: テンプレート内容
logsTitle: 'ログ'
awaitingLogs: 'ログを待機中...'
bulkDownload: 'ダウンロードしたファイルをZIPで保存'
livestreamURLInput: ライブストリームURL
livestreamStatusWaiting: 開始を待っています
livestreamStatusDownloading: ダウンロード中
livestreamStatusCompleted: 完了
livestreamStatusErrored: エラー
livestreamStatusUnknown: 不明
livestreamDownloadInfo: |
まだ開始されていないライブストリームを監視します。各プロセスは、--wait-for-video 10 で実行されます。
すでに開始されているライブストリームが提供された場合、ダウンロードは継続されますが進行状況は追跡されません。
ライブストリームが開始されると、ダウンロードページに移動されます。
livestreamExperimentalWarning: この機能は実験的なものです。何かが壊れるかもしれません!
accentSelect: 'Accent'
catalan:
urlInput: URL de YouTube o d'un altre servei compatible
statusTitle: Estat
startButton: Iniciar
statusReady: Llest
abortAllButton: Cancel·lar Tot
updateBinButton: Actualitzar el binari yt-dlp
darkThemeButton: Tema fosc
lightThemeButton: Tema clar
settingsAnchor: Configuració
serverAddressTitle: Direcció del servidor
serverPortTitle: Port
extractAudioCheckbox: Extreure àudio
noMTimeCheckbox: No guardar el temps de modificació de l'arxiu
bgReminder: Si tanques aquesta pàgina, la descàrrega continuarà en segon pla.
toastConnected: 'Connectat a'
toastUpdated: El binari yt-dlp està actualitzat!
formatSelectionEnabler: Habilitar la selecció de formats de vídeo/àudio
themeSelect: 'Tema'
languageSelect: 'Idiomes'
overridesAnchor: Anul·lacions
pathOverrideOption: Sobreescriure en la ruta de sortida
filenameOverrideOption: Sobreescriure el nom del fitxer
customFilename: Nom d'arxiu personalitzat (en blanc per utilitzar el predeterminat)
customPath: Ruta personalitzada
customArgs: Habilitar els arguments yt-dlp personalitzats (un gran poder comporta una gran responsabilitat)
customArgsInput: Arguments yt-dlp personalitzats
rpcConnErr: Error en connectar-se al servidor RPC
splashText: No active downloads
archiveTitle: Archive
clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
ukrainian:
urlInput: URL-адреса YouTube або будь-якого іншого підтримуваного сервісу
statusTitle: Статус
startButton: Почати
statusReady: Готово
abortAllButton: Перервати все
updateBinButton: Оновити бінарний файл yt-dlp
darkThemeButton: Темна тема
lightThemeButton: Світла тема
settingsAnchor: Налаштування
serverAddressTitle: Адреса сервера
serverPortTitle: Порт
extractAudioCheckbox: Витягти аудіо
noMTimeCheckbox: Не встановлювати час модифікації файлу
bgReminder: Як тільки ви закриєте цю сторінку, завантаження продовжиться у фоновому режимі.
toastConnected: 'Підключений до '
toastUpdated: Бінарний файл yt-dlp оновлено!
formatSelectionEnabler: Активувати вибір відео/аудіо форматів
themeSelect: 'Тема'
languageSelect: 'Мова'
overridesAnchor: Перевизначити
pathOverrideOption: Активувати перевизначення вихідного шляху
filenameOverrideOption: Активувати перевизначення імені вихідного файлу
customFilename: Введіть ім'я файлу (залишіть порожнім, щоб використовувати значення за замовчуванням)
customPath: Задати шлях
customArgs: Включити аргументи, що настроюються yt-dlp (велика сила = велика відповідальність)
customArgsInput: Користувальницькі аргументи yt-dlp
rpcConnErr: Помилка при підключенні до сервера RPC
splashText: Немає активних завантажень
archiveTitle: Архів
clipboardAction: URL скопійовано в буфер обміну
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
polish:
urlInput: Adres URL YouTube lub innej obsługiwanej usługi
statusTitle: Status
startButton: Początek
statusReady: Gotowy
abortAllButton: Anuluj wszystko
updateBinButton: Zaktualizuj plik binarny yt-dlp
darkThemeButton: Ciemny motyw
lightThemeButton: Światło motyw
settingsAnchor: Ustawienia
serverAddressTitle: Adres serwera
serverPortTitle: Port
extractAudioCheckbox: Wyodrębnij dźwięk
noMTimeCheckbox: Nie ustawiaj czasu modyfikacji pliku
bgReminder: Po zamknięciu tej strony pobieranie będzie kontynuowane w tle.
toastConnected: 'Połączony z '
toastUpdated: Zaktualizowano plik binarny yt-dlp!
formatSelectionEnabler: Aktywuj wybór formatów wideo/audio
themeSelect: 'Motyw'
languageSelect: 'Język'
overridesAnchor: Przedefiniuj
pathOverrideOption: Aktywuj zastąpienie ścieżki źródłowej
filenameOverrideOption: Aktywuj zastępowanie nazwy pliku źródłowego
customFilename: Wprowadź nazwę pliku (pozostaw puste, aby użyć nazwy domyślnej)
customPath: Ustaw ścieżkę
customArgs: Uwzględnij konfigurowalne argumenty yt-dlp (wielka moc = wielka odpowiedzialność)
customArgsInput: Niestandardowe argumenty yt-dlp
rpcConnErr: Wystąpił błąd podczas łączenia z serwerem RPC
splashText: Brak aktywnych pobrań
archiveTitle: Archiwum
clipboardAction: Adres URL zostanie skopiowany do schowka
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
swedish:
urlInput: Videolänk (en per rad)
statusTitle: Status
statusReady: Redo
selectFormatButton: Välj format
startButton: Start
abortAllButton: Avbryt alla
updateBinButton: Uppdatera yt-dlp
darkThemeButton: Mörkt tema
lightThemeButton: Ljust tema
settingsAnchor: Inställningar
serverAddressTitle: Serveraddress
serverPortTitle: Port
extractAudioCheckbox: Extrahera ljud
noMTimeCheckbox: Lägg inte till info om när filen senast modifierades
bgReminder: När du stänger denna sida så kommer nedladdningen att fortsätta i bakgrunden.
toastConnected: 'Ansluten till '
toastUpdated: Uppdaterade yt-dlp!
formatSelectionEnabler: Tillåt val av ljud- och bildformat
themeSelect: 'Tema'
languageSelect: 'Språk'
overridesAnchor: Överskrivningar
pathOverrideOption: Tillåt överskrivning av filsökvägen
filenameOverrideOption: Tillåt överskrivning av filnamn
customFilename: Eget filnamn (lämna blankt för standardnamn)
customPath: Egen filsökväg
customArgs: Tillåt egna yt-dlp-argument (frihet under ansvar!)
customArgsInput: Egna yt-dlp-argument
rpcConnErr: Ett fel inträffade vid anslutning till RPC-server
splashText: Inga pågående nedladdningar
archiveTitle: Arkiv
clipboardAction: Kopierade länken
playlistCheckbox: Ladda ner spellista (detta kommer ta did, efter start så kan du stänga detta fönster)
restartAppMessage: En sidomladdning behövs innan förändringen får effekt
servedFromReverseProxyCheckbox: Servern befinner sig bakom en omvänd proxy
urlBase: "URL-bas, måste anges när en omvänd proxy används. Standardinställning: lämna blank"
newDownloadButton: Ny nedladdning
homeButtonLabel: Hem
archiveButtonLabel: Arkiv
settingsButtonLabel: Inställningar
rpcAuthenticationLabel: RPC-Autentisering
themeTogglerLabel: Tema-knapp
loadingLabel: Laddar...
appTitle: Apptitel
savedTemplates: Sparade mallar
templatesEditor: Mallredigerare
templatesEditorNameLabel: Namn
templatesEditorContentLabel: Innehåll
logsTitle: 'Loggar'
awaitingLogs: 'Väntar på loggar...'
bulkDownload: 'Ladda ner filer i ett zip-arkiv'
rpcPollingTimeTitle: Frekvens av RPC-uppdateringar
rpcPollingTimeDescription: En högre frekvens kräver mer CPU-resurser för både server och klient
templatesReloadInfo: För att registrera en ny mall så kan en sidomladdning krävas.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
catalan: ca.yaml
german: de.yaml
english: en_US.yaml
spanish: es.yaml
french: fr.yaml
italian: it_IT.yaml
japanese: ja.yaml
korean: ko.yaml
polish: pl.yaml
portuguese-br: pt_BR.yaml
russian: ru.yaml
swedish: sv.yaml
ukrainian: uk.yaml
chinese: zh_CN.yaml
hungarian: hu.yaml

View File

@@ -0,0 +1,70 @@
keys:
urlInput: URL de YouTube o d'un altre servei compatible
statusTitle: Estat
startButton: Iniciar
statusReady: Llest
abortAllButton: Cancel·lar Tot
updateBinButton: Actualitzar el binari yt-dlp
darkThemeButton: Tema fosc
lightThemeButton: Tema clar
settingsAnchor: Configuració
serverAddressTitle: Direcció del servidor
serverPortTitle: Port
extractAudioCheckbox: Extreure àudio
noMTimeCheckbox: No guardar el temps de modificació de l'arxiu
bgReminder: Si tanques aquesta pàgina, la descàrrega continuarà en segon pla.
toastConnected: 'Connectat a'
toastUpdated: El binari yt-dlp està actualitzat!
formatSelectionEnabler: Habilitar la selecció de formats de vídeo/àudio
themeSelect: 'Tema'
languageSelect: 'Idiomes'
overridesAnchor: Anul·lacions
pathOverrideOption: Sobreescriure en la ruta de sortida
filenameOverrideOption: Sobreescriure el nom del fitxer
customFilename: Nom d'arxiu personalitzat (en blanc per utilitzar el predeterminat)
customPath: Ruta personalitzada
customArgs: Habilitar els arguments yt-dlp personalitzats (un gran poder comporta una gran responsabilitat)
customArgsInput: Arguments yt-dlp personalitzats
rpcConnErr: Error en connectar-se al servidor RPC
splashText: No active downloads
archiveTitle: Archive
clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
urlBase: URL base, for reverse proxy support (subdir), defaults to empty
rpcPollingTimeTitle: RPC polling time
rpcPollingTimeDescription: A lower interval results in higher CPU usage (server and client side)
generalDownloadSettings: 'Ajustes generales de descarga'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,72 @@
keys:
urlInput: Video URL
statusTitle: Status
statusReady: Bereit
selectFormatButton: Format auswählen
startButton: Start
abortAllButton: Alle Abbrechen
updateBinButton: yt-dlp Binärdatei aktualisieren
darkThemeButton: Dunkler Modus
lightThemeButton: Heller Modus
settingsAnchor: Einstellungen
serverAddressTitle: Adresse des Servers
serverPortTitle: Port
extractAudioCheckbox: Audio extrahieren
noMTimeCheckbox: Datei-Änderungszeitpunkt nicht festlegen
bgReminder: Sobald Sie diese Seite schließen, wird der Download im Hintergrund fortgesetzt.
toastConnected: 'Verbunden mit '
toastUpdated: yt-dlp Binärdatei aktualisiert!
formatSelectionEnabler: Video/Audio Format auswählbar
themeSelect: 'Modus'
languageSelect: 'Sprache'
overridesAnchor: Überschreibungen
pathOverrideOption: Ausgabe-Pfad Überschreibung aktivieren
filenameOverrideOption: Ausgabe-Dateiname Überschreibung aktivieren
customFilename: Benutzerdefinierter Dateiname (Leer lassen um Standardwert zu nutzen)
customPath: Benutzerdefinierter Pfad
customArgs: Benutzerdefinierte yt-dlp Argumente aktivieren (Auf viel Macht folgt große Verantwortung)
customArgsInput: Benutzerdefinierte yt-dlp Argumente
rpcConnErr: Fehler beim Verbinden mit RPC Server
splashText: Keine aktiven Downloads
archiveTitle: Archiv
clipboardAction: URL in Zwischenablage kopiert
playlistCheckbox: Playlist herunterladen (es wird einige Zeit dauern, nach dem Absenden können Sie dieses Fenster schließen)
restartAppMessage: Erfordert ein Neuladen der Seite, um wirksam zu werden
servedFromReverseProxyCheckbox: Ist hinter einem Reverse Proxy Unterordner
newDownloadButton: Neuer Download
homeButtonLabel: Home
archiveButtonLabel: Archiv
settingsButtonLabel: Einstellungen
rpcAuthenticationLabel: RPC Authentifizierung
themeTogglerLabel: Modus Umschalter
loadingLabel: Lädt...
appTitle: App Titel
savedTemplates: Gespeicherte Vorlage
templatesEditor: Vorlageneditor
templatesEditorNameLabel: Vorlagen Name
templatesEditorContentLabel: Vorlagen Inhalt
logsTitle: 'Logausgabe'
awaitingLogs: 'Warte auf Log ...'
bulkDownload: 'Alles in einem ZIP-Archiv herunterladen'
rpcPollingTimeTitle: RPC-Abfragezeit
rpcPollingTimeDescription: Ein kürzerer Intervall führt zu einer höheren CPU-Auslastung (Server- und Clientseite)
templatesReloadInfo: Um eine neue Vorlage zu registrieren, muss die Seite möglicherweise neu geladen werden.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Warte auf Start
livestreamStatusDownloading: Herunterladen
livestreamStatusCompleted: Abgeschlossen
livestreamStatusErrored: Fehlerhaft
livestreamStatusUnknown: Status unbekannt
livestreamNoMonitoring: Aktuell wird kein Livestream überwacht
livestreamDownloadInfo: |
Damit wird der noch nicht gestartete Livestream überwacht. Jeder Prozess wird mit --wait-for-video 10 ausgeführt.
Wenn ein bereits gestarteter Livestream vorhanden ist, wird er zwar heruntergeladen, aber sein Fortschritt wird nicht verfolgt.
Sobald der Livestream gestartet ist, wird er auf der Download-Seite angezeigt.
livestreamExperimentalWarning: Dieses Feature ist aktuell noch experimentell, sei vorsichtig, denn es könnte sein, dass etwas nicht genau funktioniert!
accentSelect: 'Farbtöne'
urlBase: URL-Basis für Reverse-Proxy-Unterstützung (Unterverzeichnis), standardmäßig leer
generalDownloadSettings: 'Allgemeine Download Einstellungen'
deleteCookies: 'Cookies löschen'
noFilesFound: 'Keine Dateien gefunden'
tableView: 'Tabellenansicht'
deleteSelected: 'Ausgewählte löschen'

View File

@@ -0,0 +1,72 @@
keys:
urlInput: Video URL (one per line)
statusTitle: Status
statusReady: Ready
selectFormatButton: Select format
startButton: Start
abortAllButton: Abort All
updateBinButton: Update yt-dlp binary
darkThemeButton: Dark theme
lightThemeButton: Light theme
settingsAnchor: Settings
serverAddressTitle: Server address
serverPortTitle: Port
extractAudioCheckbox: Extract audio
noMTimeCheckbox: Don't set file modification time
bgReminder: Once you close this page the download will continue in the background.
toastConnected: 'Connected to '
toastUpdated: Updated yt-dlp binary!
formatSelectionEnabler: Enable video/audio formats selection
themeSelect: 'Theme'
languageSelect: 'Language'
overridesAnchor: Overrides
pathOverrideOption: Enable output path overriding
filenameOverrideOption: Enable output file name overriding
customFilename: Custom filename (leave blank to use default)
customPath: Custom path
customArgs: Enable custom yt-dlp args (great power = great responsibilities)
customArgsInput: Custom yt-dlp arguments
rpcConnErr: Error while conencting to RPC server
splashText: No active downloads
archiveTitle: Archive
clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may close this window)
restartAppMessage: Needs a page reload to take effect
servedFromReverseProxyCheckbox: Is behind a reverse proxy
urlBase: URL base, for reverse proxy support (subdir), defaults to empty
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
rpcPollingTimeTitle: RPC polling time
rpcPollingTimeDescription: A lower interval results in higher CPU usage (server and client side)
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,70 @@
keys:
urlInput: URL de YouTube u otro servicio compatible
statusTitle: Estado
startButton: Iniciar
statusReady: Listo
abortAllButton: Cancelar Todo
updateBinButton: Actualizar el binario yt-dlp
darkThemeButton: Tema oscuro
lightThemeButton: Tema claro
settingsAnchor: Ajustes
serverAddressTitle: Dirección del servidor
serverPortTitle: Puerto
extractAudioCheckbox: Extraer audio
noMTimeCheckbox: No guardar el tiempo de modificación del archivo
bgReminder: Si cierras esta página, la descarga continuará en segundo plano.
toastConnected: 'Conectado a'
toastUpdated: ¡El binario yt-dlp está actualizado!
formatSelectionEnabler: Habilitar la selección de formatos de video/audio
themeSelect: 'Tema'
languageSelect: 'Idiomas'
overridesAnchor: Anulaciones
pathOverrideOption: Sobreescribir en la ruta de salida
filenameOverrideOption: Sobreescribir el nombre del fichero
customFilename: Nombre de archivo personalizado (en blanco para usar el predeterminado)
customPath: Ruta personalizada
customArgs: Habilitar los argumentos yt-dlp personalizados (un gran poder conlleva una gran responsabilidad)
customArgsInput: Argumentos yt-dlp personalizados
rpcConnErr: Error al conectarse al servidor RPC
splashText: No active downloads
archiveTitle: Archive
clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
urlBase: URL base, for reverse proxy support (subdir), defaults to empty
rpcPollingTimeTitle: RPC polling time
rpcPollingTimeDescription: A lower interval results in higher CPU usage (server and client side)
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,74 @@
keys:
urlInput: URL vidéo de YouTube ou d'un autre service pris en charge
statusTitle: Statut
statusReady: Prêt
selectFormatButton: Sélectionner le format
startButton: Démarrer
abortAllButton: Tout arrêter
updateBinButton: Mettre à jour l'exécutable yt-dlp
darkThemeButton: Thème sombre
lightThemeButton: Thème clair
settingsAnchor: Paramètres
serverAddressTitle: Adresse du serveur
serverPortTitle: Port
extractAudioCheckbox: Extraire l'audio
noMTimeCheckbox: Ne pas définir le temps de modification du fichier
bgReminder: Une fois que vous aurez fermé cette page, le téléchargement continuera en arrière-plan.
toastConnected: 'Connecté à '
toastUpdated: L'exécutable yt-dlp a été mis à jour !
formatSelectionEnabler: Activer la sélection des formats vidéo/audio
themeSelect: 'Thème'
languageSelect: 'Langue'
overridesAnchor: Remplacer
pathOverrideOption: Activer le remplacement du chemin de sortie
filenameOverrideOption: Activer le remplacement du nom du fichier de sortie
customFilename: Nom de fichier personnalisé (laisser vide pour utiliser le nom par défaut)
customPath: Chemin personnalisé
customArgs: Activer les args personnalisés yt-dlp (grand pouvoir = grandes responsabilités)
customArgsInput: Arguments yt-dlp personnalisés
rpcConnErr: Erreur lors de la connexion au serveur RPC
splashText: Aucun téléchargement actif
archiveTitle: Archive
clipboardAction: URL copiée dans le presse-papiers
playlistCheckbox: Télécharger la liste de lecture (cela prendra du temps, vous pouvez fermer cette fenêtre après l'avoir validée)
restartAppMessage: Nécessite un rechargement de la page pour prendre effet
servedFromReverseProxyCheckbox: Est derrière un sous-dossier de proxy inverse
notConnectedText: not connected
settingsLabel: Settings
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: Nom de l'application
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
urlBase: URL base, for reverse proxy support (subdir), defaults to empty
rpcPollingTimeTitle: RPC polling time
rpcPollingTimeDescription: A lower interval results in higher CPU usage (server and client side)
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,72 @@
keys:
urlInput: Video URL (soronként egy)
statusTitle: Állapot
statusReady: Előkészítve
selectFormatButton: Válassz formátumot
startButton: Indítás
abortAllButton: Összes megszakítása
updateBinButton: yt-dlp bináris frissítése
darkThemeButton: Sötét téma
lightThemeButton: Világos téma
settingsAnchor: Beállítások
serverAddressTitle: Szerver címe
serverPortTitle: Port
extractAudioCheckbox: Audio konvertálása
noMTimeCheckbox: Fájl módosítás időpontja ne legyen beállítva
bgReminder: Miután a lap bezárásra kerül, a letöltés folytatódni fog a háttérben.
toastConnected: 'Kapcsolódva: '
toastUpdated: yt-dlp bináris frissítése sikeres volt!
formatSelectionEnabler: Video/audio formátum manuális kiválasztásának engedélyezése
themeSelect: 'Téma'
languageSelect: 'Nyelv'
overridesAnchor: Felülbírálások
pathOverrideOption: Letöltési útvonal felülbírálása
filenameOverrideOption: Letöltési fájlnév felülbírálása
customFilename: Egyedi fájlnév (hagyd üresen, hogy a fájlnév automatikusan generálódjon)
customPath: Egyedi útvonal
customArgs: Egyedi yt-dlp argumentumok (Nagy hatalommal nagy felelősség jár.)
customArgsInput: Egyedi yt-dlp argumentumok
rpcConnErr: Hiba történt az RPC szerver történő kapcsolódáskor
splashText: Nincs aktív letöltés
archiveTitle: Archívum
clipboardAction: URL a vágólapra másolva.
playlistCheckbox: Lejátszási lista letöltése (Több időt vehet igénybe. A letöltés a háttérben történik, a böngészőablak szabadon bezárható.)
restartAppMessage: Az oldal újratöltése lehet szükséges a változtatások megjelenítéséhez.
servedFromReverseProxyCheckbox: Reverse proxy mögötti működés
urlBase: URL base, reverse proxy támogatásához (subdir), alapból üres
newDownloadButton: Új letöltés
homeButtonLabel: Kezdőlap
archiveButtonLabel: Archívum
settingsButtonLabel: Beállítások
rpcAuthenticationLabel: RPC bejelentkezés
themeTogglerLabel: Témaválasztó
loadingLabel: Betöltés...
appTitle: Alkalmazás címe
savedTemplates: Mentett sablonok
templatesEditor: Sablonszerkesztő
templatesEditorNameLabel: Sablon neve
templatesEditorContentLabel: Sablon tartalma
logsTitle: 'Naplók'
awaitingLogs: 'Napló letöltése...'
bulkDownload: 'Fájlok letöltése ZIP archívumként'
rpcPollingTimeTitle: RPC lekérdezési időköz
rpcPollingTimeDescription: Rövidebb időköz nagyobb processzor terheléssel járhat (mind szerver és böngésző oldalon is)
templatesReloadInfo: Az új sablon megjelenéséhez újra kell tölteni az oldalt.
livestreamURLInput: Élő stream URL
livestreamStatusWaiting: Várakozás a kezdésre
livestreamStatusDownloading: Letöltés
livestreamStatusCompleted: Letöltve
livestreamStatusErrored: Hiba
livestreamStatusUnknown: Ismeretlen
livestreamNoMonitoring: Nincsenek figyelt élő adások
livestreamDownloadInfo: |
Ez figyelni fog egy még el nem indított élő közvetítést. Minden folyamat a --wait-for-video 10 paraméterrel lesz végrehajtva.
Ha egy már elindított élő közvetítés van megadva, az továbbra is letöltésre kerül, de a folyamatát nem követi nyomon.
Amint elindul, az élő közvetítés átkerül a letöltések oldalra..
livestreamExperimentalWarning: Ez a funkció még kísérleti. Nem garantált a hibamentes működés.
accentSelect: 'Kiemelt szín'
generalDownloadSettings: 'Általános letöltési beállítások'
deleteCookies: Sütik törlése
noFilesFound: 'Nem található fájlok'
tableView: 'Táblázatos Nézet'
deleteSelected: 'Kiválasztottak törlése'

View File

@@ -0,0 +1,71 @@
keys:
urlInput: URL Video (uno per linea)
statusTitle: Stato
startButton: Inizia
statusReady: Pronto
abortAllButton: Termina tutto
updateBinButton: Aggiorna yt-dlp
darkThemeButton: Tema scuro
lightThemeButton: Tema chiaro
settingsAnchor: Impostazioni
serverAddressTitle: Indirizzo server
serverPortTitle: Porta
extractAudioCheckbox: Estrai l'audio
noMTimeCheckbox: Non impostare la proprietà "Data ultima modifica"
bgReminder: Chiusa questa UI il download continuerà in background.
toastConnected: 'Connesso a '
toastUpdated: yt-dlp aggiornato con successo!
formatSelectionEnabler: Abilita la selezione dei formati audio/video
themeSelect: 'Tema'
languageSelect: 'Lingua'
overridesAnchor: Sovrascritture
pathOverrideOption: Abilita sovrascrittura percorso di output
filenameOverrideOption: Abilita sovrascrittura del nome del file di output
customFilename: Custom filename (leave blank to use default)
customPath: Custom path
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
customArgsInput: Custom yt-dlp arguments
rpcConnErr: Error nella connessione al server RPC
splashText: Nessun download attivo
archiveTitle: Archivio
clipboardAction: URL copiato negli appunti
playlistCheckbox: Download playlist (richiederà tempo, puoi chiudere la finestra dopo l'inoltro)
restartAppMessage: La finestra deve essere ricaricata perché abbia effetto
servedFromReverseProxyCheckbox: Is behind a reverse proxy
newDownloadButton: Nuovo download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: Titolo applicazione
savedTemplates: Template salvati
templatesEditor: Editor template
templatesEditorNameLabel: Nome template
templatesEditorContentLabel: Contentunto template
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
urlBase: base URL, per supporto a reverse proxy (subdir), default vuoto
rpcPollingTimeTitle: Intervallo di polling RPC
rpcPollingTimeDescription: Un intervallo più corto implica un maggior utilizzo di CPU (lato client e server)
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,71 @@
keys:
urlInput: YouTubeまたはサポート済み動画のURL
statusTitle: 状態
statusReady: 準備
selectFormatButton: フォーマット選択
startButton: 開始
abortAllButton: すべて中止
updateBinButton: yt-dlp更新
darkThemeButton: 黒テーマ
lightThemeButton: 白テーマ
settingsAnchor: 設定
serverAddressTitle: サーバーアドレス
serverPortTitle: ポート番号
extractAudioCheckbox: 音質
noMTimeCheckbox: ファイル時間の修正をしない
bgReminder: このページを閉じてもバックグラウンドでダウンロードを続けます
toastConnected: '接続中 '
toastUpdated: yt-dlpを更新しました!
formatSelectionEnabler: 選択可能な動画/音源
themeSelect: 'テーマ'
languageSelect: '言語'
overridesAnchor: 上書き
pathOverrideOption: 保存するディレクトリ
filenameOverrideOption: ファイル名の上書き
customFilename: (空白の場合は元のファイル名)
customPath: 保存先
customArgs: yt-dlpのオプションの有効化 (最適設定にする場合)
customArgsInput: yt-dlpのオプション
rpcConnErr: RPCサーバーへの接続中にエラーが発生しました
splashText: アクティブなダウンロードはありません
archiveTitle: アーカイブ
clipboardAction: URLをクリップボードにコピーしました
playlistCheckbox: プレイリストをダウンロード (これには時間がかかりますが、処理中はウィンドウを閉じることができます)
servedFromReverseProxyCheckbox: リバースプロキシのサブフォルダにあります
newDownloadButton: 新しくダウンロード
homeButtonLabel: ホーム
archiveButtonLabel: アーカイブ
settingsButtonLabel: 設定
rpcAuthenticationLabel: RPC認証
themeTogglerLabel: テーマ切り替え
loadingLabel: 読み込み中...
appTitle: アプリタイトル
savedTemplates: 保存したテンプレート
templatesEditor: テンプレートエディター
templatesEditorNameLabel: テンプレート名
templatesEditorContentLabel: テンプレート内容
logsTitle: 'ログ'
awaitingLogs: 'ログを待機中...'
bulkDownload: 'ダウンロードしたファイルをZIPで保存'
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: ライブストリームURL
livestreamStatusWaiting: 開始を待っています
livestreamStatusDownloading: ダウンロード中
livestreamStatusCompleted: 完了
livestreamStatusErrored: エラー
livestreamStatusUnknown: 不明
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
まだ開始されていないライブストリームを監視します。各プロセスは、--wait-for-video 10 で実行されます。
すでに開始されているライブストリームが提供された場合、ダウンロードは継続されますが進行状況は追跡されません。
ライブストリームが開始されると、ダウンロードページに移動されます。
livestreamExperimentalWarning: この機能は実験的なものです。何かが壊れるかもしれません!
accentSelect: 'Accent'
urlBase: URL base, for reverse proxy support (subdir), defaults to empty
rpcPollingTimeTitle: RPC polling time
rpcPollingTimeDescription: A lower interval results in higher CPU usage (server and client side)
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,70 @@
keys:
urlInput: YouTube나 다른 지원되는 사이트의 URL
statusTitle: 상태
startButton: 시작
statusReady: 준비됨
abortAllButton: 모두 중단
updateBinButton: yt-dlp 바이너리 업데이트
darkThemeButton: 다크 모드
lightThemeButton: 라이트 모드
settingsAnchor: 설정
serverAddressTitle: 서버 주소
serverPortTitle: Port
extractAudioCheckbox: 오디오 추출
noMTimeCheckbox: 파일 수정 시간을 설정하지 않음
bgReminder: 이 페이지를 닫아도 백그라운드에서 다운로드가 계속됩니다
toastConnected: '다음으로 연결됨 '
toastUpdated: yt-dlp 바이너리를 업데이트 했습니다
formatSelectionEnabler: 비디오/오디오 포멧 옵션 표시
themeSelect: 'Theme'
languageSelect: 'Language'
overridesAnchor: Overrides
pathOverrideOption: Enable output path overriding
filenameOverrideOption: Enable output file name overriding
customFilename: Custom filename (leave blank to use default)
customPath: Custom path
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
customArgsInput: Custom yt-dlp arguments
rpcConnErr: Error while conencting to RPC server
splashText: No active downloads
archiveTitle: Archive
clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
urlBase: URL base, for reverse proxy support (subdir), defaults to empty
rpcPollingTimeTitle: RPC polling time
rpcPollingTimeDescription: A lower interval results in higher CPU usage (server and client side)
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,70 @@
keys:
urlInput: Adres URL YouTube lub innej obsługiwanej usługi
statusTitle: Status
startButton: Początek
statusReady: Gotowy
abortAllButton: Anuluj wszystko
updateBinButton: Zaktualizuj plik binarny yt-dlp
darkThemeButton: Ciemny motyw
lightThemeButton: Światło motyw
settingsAnchor: Ustawienia
serverAddressTitle: Adres serwera
serverPortTitle: Port
extractAudioCheckbox: Wyodrębnij dźwięk
noMTimeCheckbox: Nie ustawiaj czasu modyfikacji pliku
bgReminder: Po zamknięciu tej strony pobieranie będzie kontynuowane w tle.
toastConnected: 'Połączony z '
toastUpdated: Zaktualizowano plik binarny yt-dlp!
formatSelectionEnabler: Aktywuj wybór formatów wideo/audio
themeSelect: 'Motyw'
languageSelect: 'Język'
overridesAnchor: Przedefiniuj
pathOverrideOption: Aktywuj zastąpienie ścieżki źródłowej
filenameOverrideOption: Aktywuj zastępowanie nazwy pliku źródłowego
customFilename: Wprowadź nazwę pliku (pozostaw puste, aby użyć nazwy domyślnej)
customPath: Ustaw ścieżkę
customArgs: Uwzględnij konfigurowalne argumenty yt-dlp (wielka moc = wielka odpowiedzialność)
customArgsInput: Niestandardowe argumenty yt-dlp
rpcConnErr: Wystąpił błąd podczas łączenia z serwerem RPC
splashText: Brak aktywnych pobrań
archiveTitle: Archiwum
clipboardAction: Adres URL zostanie skopiowany do schowka
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
urlBase: URL base, for reverse proxy support (subdir), defaults to empty
rpcPollingTimeTitle: RPC polling time
rpcPollingTimeDescription: A lower interval results in higher CPU usage (server and client side)
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,72 @@
keys:
urlInput: URL do vídeo (uma por linha)
statusTitle: Status
statusReady: Pronto
selectFormatButton: Selecionar formato
startButton: Iniciar
abortAllButton: Cancelar tudo
updateBinButton: Atualizar binário yt-dlp
darkThemeButton: Tema escuro
lightThemeButton: Tema claro
settingsAnchor: Configurações
serverAddressTitle: Endereço do servidor
serverPortTitle: Porta
extractAudioCheckbox: Extrair áudio
noMTimeCheckbox: Não definir hora de modificação do arquivo
bgReminder: Uma vez que você feche esta página, o download continuará em segundo plano.
toastConnected: 'Conectado a '
toastUpdated: Binário yt-dlp atualizado!
formatSelectionEnabler: Habilitar seleção de formatos de vídeo/aúdio
themeSelect: 'Tema'
languageSelect: 'Idioma'
overridesAnchor: Substituições
pathOverrideOption: Habilitar substituição do caminho de saída
filenameOverrideOption: Habilitar substituição do nome do arquivo de saída
customFilename: Nome de arquivo personalizado (deixe em branco para usar o padrão)
customPath: Caminho personalizado
customArgs: Habilitar argumentos personalizados do yt-dlp (grandes poderes = grandes responsabilidades)
customArgsInput: Argumentos personalizados do yt-dlp
rpcConnErr: Erro ao conectar ao servidor RPC
splashText: Nenhum download ativo
archiveTitle: Arquivo
clipboardAction: URL copiada para a área de transferência
playlistCheckbox: Baixar playlist (isso pode levar algum tempo, depois de enviar você pode fechar esta janela)
restartAppMessage: Necessário recarregar a página para que a mudança tenha efeito
servedFromReverseProxyCheckbox: Está atrás de um proxy reverso
urlBase: Base da URL, para suporte de proxy reverso (subdiretório), padrão vazio
newDownloadButton: Novo download
homeButtonLabel: Início
archiveButtonLabel: Arquivo
settingsButtonLabel: Configurações
rpcAuthenticationLabel: Autenticação RPC
themeTogglerLabel: Alternador de tema
loadingLabel: Carregando...
appTitle: Título do aplicativo
savedTemplates: Modelos salvos
templatesEditor: Editor de modelos
templatesEditorNameLabel: Nome do modelo
templatesEditorContentLabel: Conteúdo do modelo
logsTitle: 'Logs'
awaitingLogs: 'Aguardando logs...'
bulkDownload: 'Baixar arquivos em um arquivo zip'
rpcPollingTimeTitle: Tempo de polling RPC
rpcPollingTimeDescription: Um intervalo menor resulta em maior uso de CPU (lado do servidor e do cliente)
templatesReloadInfo: Para registrar um novo modelo, pode ser necessário recarregar a página.
livestreamURLInput: URL da transmissão ao vivo
livestreamStatusWaiting: Aguardando/Aguarde o início
livestreamStatusDownloading: Baixando
livestreamStatusCompleted: Concluído
livestreamStatusErrored: Erro
livestreamStatusUnknown: Desconhecido
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
Isso monitorará uma transmissão ao vivo que ainda não começou. Cada processo será executado com --wait-for-video 10.
Se uma transmissão ao vivo já iniciada for fornecida, ela ainda será baixada, mas seu progresso não será rastreado.
Uma vez iniciada, a transmissão ao vivo será migrada para a página de downloads.
livestreamExperimentalWarning: Este recurso ainda é experimental. Algo pode quebrar!
accentSelect: 'Accent'
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,70 @@
keys:
urlInput: URL-адрес YouTube или любого другого поддерживаемого сервиса
statusTitle: Статус
startButton: Начать
statusReady: Готово
abortAllButton: Прервать все
updateBinButton: Обновить бинарный файл yt-dlp
darkThemeButton: Темная тема
lightThemeButton: Светлая тема
settingsAnchor: Настройки
serverAddressTitle: Адрес сервера
serverPortTitle: Порт
extractAudioCheckbox: Извлечь аудио
noMTimeCheckbox: Не устанавливать время модификации файла
bgReminder: Как только вы закроете эту страницу, загрузка продолжится в фоновом режиме.
toastConnected: 'Подключен к '
toastUpdated: Бинарный файл yt-dlp обновлен!
formatSelectionEnabler: Активировать выбор видео/аудио форматов
themeSelect: 'Тема'
languageSelect: 'Язык'
overridesAnchor: Переопределить
pathOverrideOption: Активировать переопределение выходного пути
filenameOverrideOption: Активировать переопределение имени выходного файла
customFilename: Задать имя файла (оставьте пустым, чтобы использовать значение по умолчанию)
customPath: Задать путь
customArgs: Включить настраиваемые аргументы yt-dlp (большая сила = большая ответственность)
customArgsInput: Пользовательские аргументы yt-dlp
rpcConnErr: Ошибка при подключении к серверу RPC
splashText: Нет активных загрузок
archiveTitle: Архив
clipboardAction: URL скопирован в буфер обмена
playlistCheckbox: Скачать плейлист. Это займет время, после отправки вы сможете закрыть окно
servedFromReverseProxyCheckbox: Находится за обратным прокси
newDownloadButton: Новая загрузка
homeButtonLabel: Home
archiveButtonLabel: Архив
settingsButtonLabel: Настройки
rpcAuthenticationLabel: RPC-аутентификация
themeTogglerLabel: Переключить тему
loadingLabel: Загрузка...
appTitle: Название приложения
savedTemplates: Сохраненные шаблоны
templatesEditor: Редактор шаблонов
templatesEditorNameLabel: Имя шаблона
templatesEditorContentLabel: Содержание шаблона
logsTitle: 'Логи'
awaitingLogs: 'Ожидание логов...'
bulkDownload: 'Скачать файлы в zip архиве'
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
urlBase: URL base, for reverse proxy support (subdir), defaults to empty
rpcPollingTimeTitle: RPC polling time
rpcPollingTimeDescription: A lower interval results in higher CPU usage (server and client side)
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,72 @@
keys:
urlInput: Videolänk (en per rad)
statusTitle: Status
statusReady: Redo
selectFormatButton: Välj format
startButton: Start
abortAllButton: Avbryt alla
updateBinButton: Uppdatera yt-dlp
darkThemeButton: Mörkt tema
lightThemeButton: Ljust tema
settingsAnchor: Inställningar
serverAddressTitle: Serveraddress
serverPortTitle: Port
extractAudioCheckbox: Extrahera ljud
noMTimeCheckbox: Lägg inte till info om när filen senast modifierades
bgReminder: När du stänger denna sida så kommer nedladdningen att fortsätta i bakgrunden.
toastConnected: 'Ansluten till '
toastUpdated: Uppdaterade yt-dlp!
formatSelectionEnabler: Tillåt val av ljud- och bildformat
themeSelect: 'Tema'
languageSelect: 'Språk'
overridesAnchor: Överskrivningar
pathOverrideOption: Tillåt överskrivning av filsökvägen
filenameOverrideOption: Tillåt överskrivning av filnamn
customFilename: Eget filnamn (lämna blankt för standardnamn)
customPath: Egen filsökväg
customArgs: Tillåt egna yt-dlp-argument (frihet under ansvar!)
customArgsInput: Egna yt-dlp-argument
rpcConnErr: Ett fel inträffade vid anslutning till RPC-server
splashText: Inga pågående nedladdningar
archiveTitle: Arkiv
clipboardAction: Kopierade länken
playlistCheckbox: Ladda ner spellista (detta kommer ta did, efter start så kan du stänga detta fönster)
restartAppMessage: En sidomladdning behövs innan förändringen får effekt
servedFromReverseProxyCheckbox: Servern befinner sig bakom en omvänd proxy
urlBase: "URL-bas, måste anges när en omvänd proxy används. Standardinställning: lämna blank"
newDownloadButton: Ny nedladdning
homeButtonLabel: Hem
archiveButtonLabel: Arkiv
settingsButtonLabel: Inställningar
rpcAuthenticationLabel: RPC-Autentisering
themeTogglerLabel: Tema-knapp
loadingLabel: Laddar...
appTitle: Apptitel
savedTemplates: Sparade mallar
templatesEditor: Mallredigerare
templatesEditorNameLabel: Namn
templatesEditorContentLabel: Innehåll
logsTitle: 'Loggar'
awaitingLogs: 'Väntar på loggar...'
bulkDownload: 'Ladda ner filer i ett zip-arkiv'
rpcPollingTimeTitle: Frekvens av RPC-uppdateringar
rpcPollingTimeDescription: En högre frekvens kräver mer CPU-resurser för både server och klient
templatesReloadInfo: För att registrera en ny mall så kan en sidomladdning krävas.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,70 @@
keys:
urlInput: URL-адреса YouTube або будь-якого іншого підтримуваного сервісу
statusTitle: Статус
startButton: Почати
statusReady: Готово
abortAllButton: Перервати все
updateBinButton: Оновити бінарний файл yt-dlp
darkThemeButton: Темна тема
lightThemeButton: Світла тема
settingsAnchor: Налаштування
serverAddressTitle: Адреса сервера
serverPortTitle: Порт
extractAudioCheckbox: Витягти аудіо
noMTimeCheckbox: Не встановлювати час модифікації файлу
bgReminder: Як тільки ви закриєте цю сторінку, завантаження продовжиться у фоновому режимі.
toastConnected: 'Підключений до '
toastUpdated: Бінарний файл yt-dlp оновлено!
formatSelectionEnabler: Активувати вибір відео/аудіо форматів
themeSelect: 'Тема'
languageSelect: 'Мова'
overridesAnchor: Перевизначити
pathOverrideOption: Активувати перевизначення вихідного шляху
filenameOverrideOption: Активувати перевизначення імені вихідного файлу
customFilename: Введіть ім'я файлу (залишіть порожнім, щоб використовувати значення за замовчуванням)
customPath: Задати шлях
customArgs: Включити аргументи, що настроюються yt-dlp (велика сила = велика відповідальність)
customArgsInput: Користувальницькі аргументи yt-dlp
rpcConnErr: Помилка при підключенні до сервера RPC
splashText: Немає активних завантажень
archiveTitle: Архів
clipboardAction: URL скопійовано в буфер обміну
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title
savedTemplates: Saved templates
templatesEditor: Templates editor
templatesEditorNameLabel: Template name
templatesEditorContentLabel: Template content
logsTitle: 'Logs'
awaitingLogs: 'Awaiting logs...'
bulkDownload: 'Download files in a zip archive'
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: Livestream URL
livestreamStatusWaiting: Waiting/Wait start
livestreamStatusDownloading: Downloading
livestreamStatusCompleted: Completed
livestreamStatusErrored: Errored
livestreamStatusUnknown: Unknown
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
Once started the livestream will be migrated to the downloads page.
livestreamExperimentalWarning: This feature is still experimental. Something might break!
accentSelect: 'Accent'
urlBase: URL base, for reverse proxy support (subdir), defaults to empty
rpcPollingTimeTitle: RPC polling time
rpcPollingTimeDescription: A lower interval results in higher CPU usage (server and client side)
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -0,0 +1,72 @@
keys:
urlInput: 视频 URL
statusTitle: 状态
statusReady: 就绪
selectFormatButton: 选择格式
startButton: 开始
abortAllButton: 全部中止
updateBinButton: 更新 yt-dlp 可执行文件
darkThemeButton: 黑暗主题
lightThemeButton: 明亮主题
settingsAnchor: 设置
serverAddressTitle: 服务器地址
serverPortTitle: 端口
extractAudioCheckbox: 提取音频
noMTimeCheckbox: 不设置文件修改时间
bgReminder: 关闭页面后,下载会继续在后台运行。
toastConnected: '已连接到 '
toastUpdated: 已更新 yt-dlp 可执行文件!
formatSelectionEnabler: 启用视频/音频格式选择
themeSelect: '主题'
languageSelect: '语言'
overridesAnchor: 覆盖
pathOverrideOption: 启用输出路径覆盖
filenameOverrideOption: 启用输出文件名覆盖
customFilename: 自定义文件名(留空使用默认值)
customPath: 自定义路径
customArgs: 启用自定义 yt-dlp 参数(能力越大 = 责任越大)
customArgsInput: 自定义 yt-dlp 参数
rpcConnErr: 连接 RPC 服务器发生错误
splashText: 没有正在进行的下载
archiveTitle: 归档
clipboardAction: 复制 URL 到剪贴板
playlistCheckbox: 下载播放列表(可能需要一段时间,提交后可以关闭页面等待)
restartAppMessage: 需要刷新页面才能生效
servedFromReverseProxyCheckbox: 处于反向代理的子目录后
newDownloadButton: 新下载
homeButtonLabel: 主页
archiveButtonLabel: 归档
settingsButtonLabel: 设置
rpcAuthenticationLabel: RPC 身份验证
themeTogglerLabel: 主题切换
loadingLabel: 正在加载…
appTitle: App 标题
savedTemplates: 保存模板
templatesEditor: 模板编辑器
templatesEditorNameLabel: 模板名称
templatesEditorContentLabel: 模板内容
logsTitle: '日志'
awaitingLogs: '正在等待日志…'
bulkDownload: '下载 zip 压缩包中的文件'
templatesReloadInfo: To register a new template it might need a page reload.
livestreamURLInput: 直播 URL
livestreamStatusWaiting: 等待直播开始
livestreamStatusDownloading: 下载中
livestreamStatusCompleted: 已完成
livestreamStatusErrored: 发生错误
livestreamStatusUnknown: 未知
livestreamNoMonitoring: No livestreams monitored
livestreamDownloadInfo: |
本功能将会监控即将开始的直播流,每个进程都会传入参数:--wait-for-video 10 重试间隔10秒
如果直播已经开始,那么依然可以下载,但是不会记录下载进度。
直播开始后,将会转移到下载页面
livestreamExperimentalWarning: 实验性功能可能存在未知Bug请谨慎使用
accentSelect: 'Accent'
urlBase: URL base, for reverse proxy support (subdir), defaults to empty
rpcPollingTimeTitle: RPC polling time
rpcPollingTimeDescription: A lower interval results in higher CPU usage (server and client side)
generalDownloadSettings: 'General Download Settings'
deleteCookies: Delete Cookies
noFilesFound: 'No Files Found'
tableView: 'Table View'
deleteSelected: 'Delete selected'

View File

@@ -1,5 +0,0 @@
import { atom } from 'jotai'
import I18nBuilder from '../lib/intl'
import { languageState } from './settings'
export const i18nBuilderState = atom((get) => new I18nBuilder(get(languageState)))

View File

@@ -6,19 +6,21 @@ import { atomWithStorage } from 'jotai/utils'
import { atom } from 'jotai'
export const languages = [
'english',
'chinese',
'russian',
'french',
'italian',
'spanish',
'korean',
'japanese',
'catalan',
'ukrainian',
'swedish',
'chinese',
'english',
'french',
'german',
'italian',
'japanese',
'korean',
'polish',
'german'
'portuguese-br',
'russian',
'spanish',
'swedish',
'ukrainian',
'hungarian'
] as const
export type Language = (typeof languages)[number]
@@ -172,4 +174,4 @@ export const settingsState = atom<SettingsState>((get) => ({
servedFromReverseProxy: get(servedFromReverseProxyState),
appTitle: get(appTitleState)
})
)
)

View File

@@ -0,0 +1,98 @@
import DeleteIcon from '@mui/icons-material/Delete'
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
import OpenInBrowserIcon from '@mui/icons-material/OpenInBrowser'
import SaveAltIcon from '@mui/icons-material/SaveAlt'
import {
Card,
CardActionArea,
CardActions,
CardContent,
CardMedia,
IconButton,
Skeleton,
Tooltip,
Typography
} from '@mui/material'
import { useAtomValue } from 'jotai'
import { serverURL } from '../atoms/settings'
import { ArchiveEntry } from '../types'
import { base64URLEncode, ellipsis } from '../utils'
type Props = {
entry: ArchiveEntry
onDelete: (id: string) => void
onHardDelete: (id: string) => void
}
const ArchiveCard: React.FC<Props> = ({ entry, onDelete, onHardDelete }) => {
const serverAddr = useAtomValue(serverURL)
const viewFile = (path: string) => {
const encoded = base64URLEncode(path)
window.open(`${serverAddr}/filebrowser/v/${encoded}?token=${localStorage.getItem('token')}`)
}
const downloadFile = (path: string) => {
const encoded = base64URLEncode(path)
window.open(`${serverAddr}/filebrowser/d/${encoded}?token=${localStorage.getItem('token')}`)
}
return (
<Card>
<CardActionArea onClick={() => navigator.clipboard.writeText(entry.source)}>
{entry.thumbnail !== '' ?
<CardMedia
component="img"
height={180}
image={entry.thumbnail}
/> :
<Skeleton variant="rectangular" height={180} />
}
<CardContent>
{entry.title !== '' ?
<Typography gutterBottom variant="h6" component="div">
{ellipsis(entry.title, 60)}
</Typography> :
<Skeleton />
}
{/* <code>
{JSON.stringify(JSON.parse(entry.metadata), null, 2)}
</code> */}
<p>{new Date(entry.created_at).toLocaleString()}</p>
</CardContent>
</CardActionArea>
<CardActions>
<Tooltip title="Open in browser">
<IconButton
onClick={() => viewFile(entry.path)}
>
<OpenInBrowserIcon />
</IconButton>
</Tooltip>
<Tooltip title="Download this file">
<IconButton
onClick={() => downloadFile(entry.path)}
>
<SaveAltIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete from archive">
<IconButton
onClick={() => onDelete(entry.id)}
>
<DeleteIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete from disk">
<IconButton
onClick={() => onHardDelete(entry.id)}
>
<DeleteForeverIcon />
</IconButton>
</Tooltip>
</CardActions>
</Card>
)
}
export default ArchiveCard

View File

@@ -11,6 +11,9 @@ import { useSubscription } from '../hooks/observable'
import { useToast } from '../hooks/toast'
import { ffetch } from '../lib/httpClient'
import { useAtomValue } from 'jotai'
import { useI18n } from '../hooks/useI18n'
const { i18n } = useI18n()
const validateCookie = (cookie: string) => pipe(
cookie,
@@ -164,7 +167,7 @@ const CookiesTextField: React.FC = () => {
defaultValue={savedCookies}
onChange={(e) => cookies$.next(e.currentTarget.value)}
/>
<Button onClick={deleteCookies}>Delete cookies</Button>
<Button onClick={deleteCookies}>{i18n.t('deleteCookies')}</Button>
</>
)
}

View File

@@ -1,7 +1,3 @@
import EightK from '@mui/icons-material/EightK'
import FourK from '@mui/icons-material/FourK'
import Hd from '@mui/icons-material/Hd'
import Sd from '@mui/icons-material/Sd'
import {
Button,
Card,
@@ -10,16 +6,23 @@ import {
CardContent,
CardMedia,
Chip,
IconButton,
LinearProgress,
Skeleton,
Stack,
Tooltip,
Typography
} from '@mui/material'
import { useAtomValue } from 'jotai'
import { useCallback } from 'react'
import { serverURL } from '../atoms/settings'
import { RPCResult } from '../types'
import { base64URLEncode, ellipsis, formatSize, formatSpeedMiB, mapProcessStatus } from '../utils'
import { useAtomValue } from 'jotai'
import ResolutionBadge from './ResolutionBadge'
import ClearIcon from '@mui/icons-material/Clear'
import StopCircleIcon from '@mui/icons-material/StopCircle'
import OpenInBrowserIcon from '@mui/icons-material/OpenInBrowser'
import SaveAltIcon from '@mui/icons-material/SaveAlt'
type Props = {
download: RPCResult
@@ -27,15 +30,6 @@ type Props = {
onCopy: () => void
}
const Resolution: React.FC<{ resolution?: string }> = ({ resolution }) => {
if (!resolution) return null
if (resolution.includes('4320')) return <EightK color="primary" />
if (resolution.includes('2160')) return <FourK color="primary" />
if (resolution.includes('1080')) return <Hd color="primary" />
if (resolution.includes('720')) return <Sd color="primary" />
return null
}
const DownloadCard: React.FC<Props> = ({ download, onStop, onCopy }) => {
const serverAddr = useAtomValue(serverURL)
@@ -53,12 +47,12 @@ const DownloadCard: React.FC<Props> = ({ download, onStop, onCopy }) => {
const viewFile = (path: string) => {
const encoded = base64URLEncode(path)
window.open(`${serverAddr}/archive/v/${encoded}?token=${localStorage.getItem('token')}`)
window.open(`${serverAddr}/filebrowser/v/${encoded}?token=${localStorage.getItem('token')}`)
}
const downloadFile = (path: string) => {
const encoded = base64URLEncode(path)
window.open(`${serverAddr}/archive/d/${encoded}?token=${localStorage.getItem('token')}`)
window.open(`${serverAddr}/filebrowser/d/${encoded}?token=${localStorage.getItem('token')}`)
}
return (
@@ -110,37 +104,44 @@ const DownloadCard: React.FC<Props> = ({ download, onStop, onCopy }) => {
<Typography>
{formatSize(download.info.filesize_approx ?? 0)}
</Typography>
<Resolution resolution={download.info.resolution} />
<ResolutionBadge resolution={download.info.resolution} />
</Stack>
</CardContent>
</CardActionArea>
<CardActions>
<Button
variant="contained"
size="small"
color="primary"
onClick={onStop}
>
{isCompleted() ? "Clear" : "Stop"}
</Button>
{isCompleted() ?
<Tooltip title="Clear from the view">
<IconButton
onClick={onStop}
>
<ClearIcon />
</IconButton>
</Tooltip>
:
<Tooltip title="Stop this download">
<IconButton
onClick={onStop}
>
<StopCircleIcon />
</IconButton>
</Tooltip>
}
{isCompleted() &&
<>
<Button
variant="contained"
size="small"
color="primary"
onClick={() => downloadFile(download.output.savedFilePath)}
>
Download
</Button>
<Button
variant="contained"
size="small"
color="primary"
onClick={() => viewFile(download.output.savedFilePath)}
>
View
</Button>
<Tooltip title="Download this file">
<IconButton
onClick={() => downloadFile(download.output.savedFilePath)}
>
<SaveAltIcon />
</IconButton>
</Tooltip>
<Tooltip title="Open in a new tab">
<IconButton
onClick={() => viewFile(download.output.savedFilePath)}
>
<OpenInBrowserIcon />
</IconButton>
</Tooltip>
</>
}
</CardActions>

View File

@@ -348,7 +348,7 @@ const DownloadDialog: FC<Props> = ({ open, onClose, onDownloadStart }) => {
disabled={url === ''}
onClick={() => settings.formatSelection
? startTransition(() => sendUrlFormatSelection())
: sendUrl()
: startTransition(async () => await sendUrl())
}
>
{

View File

@@ -1,11 +1,13 @@
import { Grid } from '@mui/material'
import { Grid2 } from '@mui/material'
import { useAtomValue } from 'jotai'
import { useTransition } from 'react'
import { activeDownloadsState } from '../atoms/downloads'
import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n'
import { useRPC } from '../hooks/useRPC'
import { ProcessStatus, RPCResult } from '../types'
import DownloadCard from './DownloadCard'
import LoadingBackdrop from './LoadingBackdrop'
const DownloadsGridView: React.FC = () => {
const downloads = useAtomValue(activeDownloadsState)
@@ -14,24 +16,31 @@ const DownloadsGridView: React.FC = () => {
const { client } = useRPC()
const { pushMessage } = useToast()
const stop = (r: RPCResult) => r.progress.process_status === ProcessStatus.COMPLETED
? client.clear(r.id)
: client.kill(r.id)
const [isPending, startTransition] = useTransition()
const stop = async (r: RPCResult) => r.progress.process_status === ProcessStatus.COMPLETED
? await client.clear(r.id)
: await client.kill(r.id)
return (
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12, xl: 12 }} pt={2}>
{
downloads.map(download => (
<Grid item xs={4} sm={8} md={6} xl={4} key={download.id}>
<DownloadCard
download={download}
onStop={() => stop(download)}
onCopy={() => pushMessage(i18n.t('clipboardAction'), 'info')}
/>
</Grid>
))
}
</Grid>
<>
<LoadingBackdrop isLoading={isPending} />
<Grid2 container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12, xl: 12 }} pt={2}>
{
downloads.map(download => (
<Grid2 size={{ xs: 4, sm: 8, md: 6, xl: 4 }} key={download.id}>
<DownloadCard
download={download}
onStop={() => startTransition(async () => {
await stop(download)
})}
onCopy={() => pushMessage(i18n.t('clipboardAction'), 'info')}
/>
</Grid2>
))
}
</Grid2>
</>
)
}

View File

@@ -125,12 +125,12 @@ const DownloadsTableView: React.FC = () => {
const viewFile = (path: string) => {
const encoded = base64URLEncode(path)
window.open(`${serverAddr}/archive/v/${encoded}?token=${localStorage.getItem('token')}`)
window.open(`${serverAddr}/filebrowser/v/${encoded}?token=${localStorage.getItem('token')}`)
}
const downloadFile = (path: string) => {
const encoded = base64URLEncode(path)
window.open(`${serverAddr}/archive/d/${encoded}?token=${localStorage.getItem('token')}`)
window.open(`${serverAddr}/filebrowser/d/${encoded}?token=${localStorage.getItem('token')}`)
}
const stop = (r: RPCResult) => r.progress.process_status === ProcessStatus.COMPLETED

View File

@@ -0,0 +1,45 @@
import ArchiveIcon from '@mui/icons-material/Archive'
import { Container, SvgIcon, Typography, styled } from '@mui/material'
import { activeDownloadsState } from '../atoms/downloads'
import { useI18n } from '../hooks/useI18n'
import { useAtomValue } from 'jotai'
const FlexContainer = styled(Container)({
display: 'flex',
minWidth: '100%',
minHeight: '80vh',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column'
})
const Title = styled(Typography)({
display: 'flex',
width: '100%',
alignItems: 'center',
justifyContent: 'center',
paddingBottom: '0.5rem'
})
export default function EmptyArchive() {
const { i18n } = useI18n()
const activeDownloads = useAtomValue(activeDownloadsState)
if (activeDownloads.length !== 0) {
return null
}
return (
<FlexContainer>
<Title fontWeight={'500'} fontSize={72} color={'gray'}>
<SvgIcon sx={{ fontSize: '200px' }}>
<ArchiveIcon />
</SvgIcon>
</Title>
<Title fontWeight={'500'} fontSize={36} color={'gray'}>
{/* {i18n.t('splashText')} */}
Empty Archive
</Title>
</FlexContainer>
)
}

View File

@@ -1,13 +1,22 @@
import { Autocomplete, Box, TextField, Typography } from '@mui/material'
import { useAtomValue, useSetAtom } from 'jotai'
import { useEffect } from 'react'
import { customArgsState, savedTemplatesState } from '../atoms/downloadTemplate'
import { useI18n } from '../hooks/useI18n'
import { useAtom, useAtomValue } from 'jotai'
const ExtraDownloadOptions: React.FC = () => {
const { i18n } = useI18n()
const customTemplates = useAtomValue(savedTemplatesState)
const [, setCustomArgs] = useAtom(customArgsState)
const setCustomArgs = useSetAtom(customArgsState)
useEffect(() => {
setCustomArgs(
customTemplates
.find(f => f.name.toLocaleLowerCase() === 'default')
?.content ?? ''
)
}, [])
return (
<>
@@ -17,7 +26,7 @@ const ExtraDownloadOptions: React.FC = () => {
autoHighlight
defaultValue={
customTemplates
.filter(({ id, name }) => id === "0" || name === "default")
.filter(({ id, name }) => id === "0" || name.toLowerCase() === "default")
.map(({ name, content }) => ({ label: name, content }))
.at(0)
}

View File

@@ -34,7 +34,7 @@ const HomeSpeedDial: React.FC<Props> = ({ onDownloadOpen, onEditorOpen }) => {
>
<SpeedDialAction
icon={listView ? <ViewAgendaIcon /> : <FormatListBulleted />}
tooltipTitle={listView ? 'Card view' : 'Table view'}
tooltipTitle={listView ? 'Card view' : i18n.t('tableView')}
onClick={() => setListView(state => !state)}
/>
<SpeedDialAction

View File

@@ -0,0 +1,15 @@
import EightK from '@mui/icons-material/EightK'
import FourK from '@mui/icons-material/FourK'
import Hd from '@mui/icons-material/Hd'
import Sd from '@mui/icons-material/Sd'
const ResolutionBadge: React.FC<{ resolution?: string }> = ({ resolution }) => {
if (!resolution) return null
if (resolution.includes('4320')) return <EightK color="primary" />
if (resolution.includes('2160')) return <FourK color="primary" />
if (resolution.includes('1080')) return <Hd color="primary" />
if (resolution.includes('720')) return <Sd color="primary" />
return null
}
export default ResolutionBadge

View File

@@ -201,7 +201,7 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
InputProps={{
endAdornment: <Button
variant='contained'
onClick={() => startTransition(() => { addTemplate() })}
onClick={() => startTransition(async () => await addTemplate())}
>
<AddIcon />
</Button>

View File

@@ -31,7 +31,7 @@ export default function NoLivestreams() {
</SvgIcon>
</Title>
<Title fontWeight={'500'} fontSize={36} color={'gray'}>
No livestreams monitored
{i18n.t('livestreamNoMonitoring')}
</Title>
</FlexContainer>
)

View File

@@ -1,8 +1,7 @@
import { useAtomValue } from 'jotai'
import { i18nBuilderState } from '../atoms/i18n'
import Translator from '../lib/i18n'
export const useI18n = () => {
const instance = useAtomValue(i18nBuilderState)
const instance = Translator.instance
return {
i18n: instance,

53
frontend/src/lib/i18n.ts Normal file
View File

@@ -0,0 +1,53 @@
//@ts-ignore
import i18n from '../assets/i18n.yaml'
//@ts-ignore
import fallback from '../assets/i18n/en_US.yaml'
export default class Translator {
static #instance: Translator
private language: string
private current: string[] = []
constructor() {
this.language = localStorage.getItem('language')?.replaceAll('"', '') ?? 'english'
this.setLanguage(this.language)
}
getLanguage(): string {
return this.language
}
async setLanguage(language: string): Promise<void> {
this.language = language
let isoCodeFile: string = i18n.languages[language]
// extension needs to be in source code to help vite bundle all yaml files
if (isoCodeFile.endsWith('.yaml')) {
isoCodeFile = isoCodeFile.replaceAll('.yaml', '')
}
if (isoCodeFile) {
const { default: translations } = await import(`../assets/i18n/${isoCodeFile}.yaml`)
this.current = translations.keys
}
}
t(key: string): string {
if (this.current) {
//@ts-ignore
return this.current[key] ?? fallback.keys[key]
}
return 'caption not defined'
}
public static get instance(): Translator {
if (!Translator.#instance) {
Translator.#instance = new Translator()
}
return Translator.#instance
}
}

View File

@@ -1,28 +0,0 @@
// @ts-nocheck
import i18n from "../assets/i18n.yaml"
export default class I18nBuilder {
private language: string
private textMap = i18n.languages
private current: string[]
constructor(language: string) {
this.setLanguage(language)
}
getLanguage(): string {
return this.language
}
setLanguage(language: string): void {
this.language = language
this.current = this.textMap[this.language]
}
t(key: string): string {
if (this.current) {
return this.current[key] ?? 'caption not defined'
}
return 'caption not defined'
}
}

View File

@@ -41,10 +41,10 @@ export class RPCClient {
})
}
private argsSanitizer(args: string) {
private argsSanitizer(args: string): string[] {
return args
.split(' ')
.map(a => a.trim().replaceAll("'", '').replaceAll('"', ''))
.map(a => a.trim().replaceAll('"', ''))
.filter(Boolean)
}
@@ -128,21 +128,21 @@ export class RPCClient {
}
public kill(id: string) {
this.sendHTTP({
return this.sendHTTP({
method: 'Service.Kill',
params: [id],
})
}
public clear(id: string) {
this.sendHTTP({
return this.sendHTTP({
method: 'Service.Clear',
params: [id],
})
}
public killAll() {
this.sendHTTP({
return this.sendHTTP({
method: 'Service.KillAll',
params: [],
})

View File

@@ -9,6 +9,7 @@ const Login = lazy(() => import('./views/Login'))
const Archive = lazy(() => import('./views/Archive'))
const Settings = lazy(() => import('./views/Settings'))
const LiveStream = lazy(() => import('./views/Livestream'))
const Filebrowser = lazy(() => import('./views/Filebrowser'))
const ErrorBoundary = lazy(() => import('./components/ErrorBoundary'))
@@ -59,6 +60,19 @@ export const router = createHashRouter([
</Suspense >
)
},
{
path: '/filebrowser',
element: (
<Suspense fallback={<CircularProgress />}>
<Filebrowser />
</Suspense >
),
errorElement: (
<Suspense fallback={<CircularProgress />}>
<ErrorBoundary />
</Suspense >
)
},
{
path: '/login',
element: (

View File

@@ -122,4 +122,20 @@ export type LiveStreamProgress = Record<string, {
export type RPCVersion = {
rpcVersion: string
ytdlpVersion: string
}
export type ArchiveEntry = {
id: string
title: string
path: string
thumbnail: string
source: string
metadata: string
created_at: string
}
export type PaginatedResponse<T> = {
first: number
next: number
data: T
}

View File

@@ -1,6 +1,6 @@
import { blue, red } from '@mui/material/colors'
import { pipe } from 'fp-ts/lib/function'
import { Accent } from './atoms/settings'
import { Accent, ThemeNarrowed } from './atoms/settings'
import type { RPCResponse } from "./types"
import { ProcessStatus } from './types'
@@ -83,13 +83,13 @@ export const base64URLEncode = (s: string) => pipe(
encodeURIComponent
)
export const getAccentValue = (accent: Accent) => {
export const getAccentValue = (accent: Accent, mode: ThemeNarrowed) => {
switch (accent) {
case 'default':
return blue[700]
return mode === 'light' ? blue[700] : blue[300]
case 'red':
return red[600]
return mode === 'light' ? red[600] : red[400]
default:
return blue[700]
return mode === 'light' ? blue[700] : blue[300]
}
}

View File

@@ -1,363 +1,112 @@
import {
Backdrop,
Button,
Checkbox,
CircularProgress,
Container,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
MenuItem,
MenuList,
Paper,
SpeedDial,
SpeedDialAction,
SpeedDialIcon,
Typography
} from '@mui/material'
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
import FolderIcon from '@mui/icons-material/Folder'
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'
import VideoFileIcon from '@mui/icons-material/VideoFile'
import DownloadIcon from '@mui/icons-material/Download'
import { matchW } from 'fp-ts/lib/TaskEither'
import { Container, FormControl, Grid2, InputLabel, MenuItem, Pagination, Select } from '@mui/material'
import { pipe } from 'fp-ts/lib/function'
import { useEffect, useMemo, useState, useTransition } from 'react'
import { useNavigate } from 'react-router-dom'
import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs'
import { serverURL } from '../atoms/settings'
import { useObservable } from '../hooks/observable'
import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n'
import { ffetch } from '../lib/httpClient'
import { DirectoryEntry } from '../types'
import { base64URLEncode, formatSize } from '../utils'
import { matchW } from 'fp-ts/lib/TaskEither'
import { useAtomValue } from 'jotai'
import { useEffect, useState, useTransition } from 'react'
import { serverURL } from '../atoms/settings'
import ArchiveCard from '../components/ArchiveCard'
import EmptyArchive from '../components/EmptyArchive'
import { useToast } from '../hooks/toast'
import { ffetch } from '../lib/httpClient'
import { ArchiveEntry, PaginatedResponse } from '../types'
import LoadingBackdrop from '../components/LoadingBackdrop'
export default function Downloaded() {
const [menuPos, setMenuPos] = useState({ x: 0, y: 0 })
const [showMenu, setShowMenu] = useState(false)
const [currentFile, setCurrentFile] = useState<DirectoryEntry>()
const Archive: React.FC = () => {
const [isLoading, setLoading] = useState(true)
const [archiveEntries, setArchiveEntries] = useState<ArchiveEntry[]>()
const serverAddr = useAtomValue(serverURL)
const navigate = useNavigate()
const { i18n } = useI18n()
const { pushMessage } = useToast()
const [openDialog, setOpenDialog] = useState(false)
const files$ = useMemo(() => new Subject<DirectoryEntry[]>(), [])
const selected$ = useMemo(() => new BehaviorSubject<string[]>([]), [])
const [currentCursor, setCurrentCursor] = useState(0)
const [cursor, setCursor] = useState({ first: 0, next: 0 })
const [pageSize, setPageSize] = useState(25)
const [isPending, startTransition] = useTransition()
const fetcher = () => pipe(
ffetch<DirectoryEntry[]>(
`${serverAddr}/archive/downloaded`,
{
method: 'POST',
body: JSON.stringify({
subdir: '',
})
}
),
const serverAddr = useAtomValue(serverURL)
const { pushMessage } = useToast()
const fetchArchived = (startCursor = 0) => pipe(
ffetch<PaginatedResponse<ArchiveEntry[]>>(`${serverAddr}/archive?id=${startCursor}&limit=${pageSize}`),
matchW(
(e) => {
pushMessage(e, 'error')
navigate('/login')
},
(d) => files$.next(d ?? []),
(l) => pushMessage(l, 'error'),
(r) => {
setArchiveEntries(r.data)
setCursor({ ...cursor, first: r.first, next: r.next })
}
)
)()
const fetcherSubfolder = (sub: string) => {
const folders = sub.startsWith('/')
? sub.substring(1).split('/')
: sub.split('/')
const relpath = folders.length >= 2
? folders.slice(-(folders.length - 1)).join('/')
: folders.pop()
const _upperLevel = folders.slice(1, -1)
const upperLevel = _upperLevel.length === 2
? ['.', ..._upperLevel].join('/')
: _upperLevel.join('/')
const task = ffetch<DirectoryEntry[]>(`${serverAddr}/archive/downloaded`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ subdir: relpath })
})
pipe(
task,
matchW(
(l) => pushMessage(l, 'error'),
(r) => files$.next(sub
? [{
isDirectory: true,
isVideo: false,
modTime: '',
name: '..',
path: upperLevel,
size: 0,
}, ...r.filter(f => f.name !== '')]
: r.filter(f => f.name !== '')
)
)
)()
}
const selectable$ = useMemo(() => files$.pipe(
combineLatestWith(selected$),
map(([data, selected]) => data.map(x => ({
...x,
selected: selected.includes(x.name)
}))),
share()
), [])
const selectable = useObservable(selectable$, [])
const addSelected = (name: string) => {
selected$.value.includes(name)
? selected$.next(selected$.value.filter(val => val !== name))
: selected$.next([...selected$.value, name])
}
const deleteFile = (entry: DirectoryEntry) => pipe(
ffetch(`${serverAddr}/archive/delete`, {
method: 'POST',
body: JSON.stringify({
path: entry.path,
})
const softDelete = (id: string) => pipe(
ffetch<ArchiveEntry[]>(`${serverAddr}/archive/soft/${id}`, {
method: 'DELETE'
}),
matchW(
(l) => pushMessage(l, 'error'),
(_) => fetcher()
(_) => startTransition(async () => await fetchArchived())
)
)()
const deleteSelected = () => {
Promise.all(selectable
.filter(entry => entry.selected)
.map(deleteFile)
).then(fetcher)
}
const hardDelete = (id: string) => pipe(
ffetch<ArchiveEntry[]>(`${serverAddr}/archive/hard/${id}`, {
method: 'DELETE'
}),
matchW(
(l) => pushMessage(l, 'error'),
(_) => startTransition(async () => await fetchArchived())
)
)()
const setPage = (page: number) => setCurrentCursor(pageSize * (page - 1))
useEffect(() => {
fetcher()
}, [serverAddr])
const onFileClick = (path: string) => startTransition(() => {
const encoded = base64URLEncode(path)
window.open(`${serverAddr}/archive/v/${encoded}?token=${localStorage.getItem('token')}`)
})
const downloadFile = (path: string) => startTransition(() => {
const encoded = base64URLEncode(path)
window.open(`${serverAddr}/archive/d/${encoded}?token=${localStorage.getItem('token')}`)
})
const onFolderClick = (path: string) => startTransition(() => {
fetcherSubfolder(path)
})
fetchArchived(currentCursor).then(() => setLoading(false))
}, [currentCursor])
return (
<Container
maxWidth="xl"
sx={{ mt: 4, mb: 4, height: '100%' }}
onClick={() => setShowMenu(false)}
>
<IconMenu
posX={menuPos.x}
posY={menuPos.y}
hide={!showMenu}
onDownload={() => {
if (currentFile) {
downloadFile(currentFile?.path)
setCurrentFile(undefined)
}
}}
onDelete={() => {
if (currentFile) {
deleteFile(currentFile)
setCurrentFile(undefined)
}
}}
/>
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={!(files$.observed) || isPending}
>
<CircularProgress color="primary" />
</Backdrop>
<Paper
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
}}
onClick={() => setShowMenu(false)}
>
<Typography py={1} variant="h5" color="primary">
{i18n.t('archiveTitle')}
</Typography>
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
{selectable.length === 0 && 'No files found'}
{selectable.map((file, idx) => (
<ListItem
onContextMenu={(e) => {
e.preventDefault()
setCurrentFile(file)
setMenuPos({ x: e.clientX, y: e.clientY })
setShowMenu(true)
}}
key={idx}
secondaryAction={
<div>
{!file.isDirectory && <Typography
variant="caption"
component="span"
>
{formatSize(file.size)}
</Typography>
}
{!file.isDirectory && <>
<Checkbox
edge="end"
checked={file.selected}
onChange={() => addSelected(file.name)}
/>
</>}
</div>
}
disablePadding
>
<ListItemButton onClick={
() => file.isDirectory
? onFolderClick(file.path)
: onFileClick(file.path)
}>
<ListItemIcon>
{file.isDirectory
? <FolderIcon />
: file.isVideo
? <VideoFileIcon />
: <InsertDriveFileIcon />
}
</ListItemIcon>
<ListItemText
primary={file.name}
secondary={file.name != '..' && new Date(file.modTime).toLocaleString()}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Paper>
<SpeedDial
ariaLabel='archive actions'
sx={{ position: 'absolute', bottom: 64, right: 24 }}
icon={<SpeedDialIcon />}
>
<SpeedDialAction
icon={<DeleteForeverIcon />}
tooltipTitle={`Delete selected`}
tooltipOpen
onClick={() => {
if (selected$.value.length > 0) {
setOpenDialog(true)
}
}}
/>
</SpeedDial>
<Dialog
open={openDialog}
onClose={() => setOpenDialog(false)}
>
<DialogTitle>
Are you sure?
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
You're deleting:
</DialogContentText>
<ul>
{selected$.value.map((entry, idx) => (
<li key={idx}>{entry}</li>
))}
</ul>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>
Cancel
</Button>
<Button
onClick={() => {
deleteSelected()
setOpenDialog(false)
}}
autoFocus
<Container maxWidth="xl" sx={{ mt: 4, mb: 8, minHeight: '80vh' }}>
<LoadingBackdrop isLoading={isPending || isLoading} />
{
archiveEntries && archiveEntries.length !== 0 ?
<Grid2
container
spacing={{ xs: 2, md: 2 }}
columns={{ xs: 4, sm: 8, md: 12, xl: 12 }}
pt={2}
sx={{ minHeight: '77.5vh' }}
>
Ok
</Button>
</DialogActions>
</Dialog>
{
archiveEntries.map((entry) => (
<Grid2 size={{ xs: 4, sm: 8, md: 4, xl: 3 }} key={entry.id}>
<ArchiveCard
entry={entry}
onDelete={() => startTransition(async () => await softDelete(entry.id))}
onHardDelete={() => startTransition(async () => await hardDelete(entry.id))}
/>
</Grid2>
))
}
</Grid2>
: <EmptyArchive />
}
<Pagination
sx={{ mx: 'auto', pt: 2 }}
count={Math.floor(cursor.next / pageSize) + 1}
onChange={(_, v) => setPage(v)}
/>
<FormControl variant="standard" sx={{ m: 1, minWidth: 120 }}>
<InputLabel id="page-size-select-label">Page size</InputLabel>
<Select
labelId="page-size-select-label"
value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))}
label="Page size"
>
<MenuItem value={25}>25</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
</Select>
</FormControl>
</Container>
)
}
const IconMenu: React.FC<{
posX: number
posY: number
hide: boolean
onDownload: () => void
onDelete: () => void
}> = ({ posX, posY, hide, onDelete, onDownload }) => {
return (
<Paper sx={{
width: 320,
maxWidth: '100%',
position: 'absolute',
top: posY,
left: posX,
display: hide ? 'none' : 'block',
zIndex: (theme) => theme.zIndex.drawer + 1,
}}>
<MenuList>
<MenuItem onClick={onDownload}>
<ListItemIcon>
<DownloadIcon fontSize="small" />
</ListItemIcon>
<ListItemText>
Download
</ListItemText>
</MenuItem>
<MenuItem onClick={onDelete}>
<ListItemIcon>
<DeleteForeverIcon fontSize="small" />
</ListItemIcon>
<ListItemText>
Delete
</ListItemText>
</MenuItem>
</MenuList>
</Paper>
)
}
export default Archive

View File

@@ -0,0 +1,360 @@
import {
Backdrop,
Button,
Checkbox,
CircularProgress,
Container,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
MenuItem,
MenuList,
Paper,
SpeedDial,
SpeedDialAction,
SpeedDialIcon,
Typography
} from '@mui/material'
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
import FolderIcon from '@mui/icons-material/Folder'
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'
import VideoFileIcon from '@mui/icons-material/VideoFile'
import DownloadIcon from '@mui/icons-material/Download'
import { matchW } from 'fp-ts/lib/TaskEither'
import { pipe } from 'fp-ts/lib/function'
import { useEffect, useMemo, useState, useTransition } from 'react'
import { useNavigate } from 'react-router-dom'
import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs'
import { serverURL } from '../atoms/settings'
import { useObservable } from '../hooks/observable'
import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n'
import { ffetch } from '../lib/httpClient'
import { DirectoryEntry } from '../types'
import { base64URLEncode, formatSize } from '../utils'
import { useAtomValue } from 'jotai'
export default function Downloaded() {
const [menuPos, setMenuPos] = useState({ x: 0, y: 0 })
const [showMenu, setShowMenu] = useState(false)
const [currentFile, setCurrentFile] = useState<DirectoryEntry>()
const serverAddr = useAtomValue(serverURL)
const navigate = useNavigate()
const { i18n } = useI18n()
const { pushMessage } = useToast()
const [openDialog, setOpenDialog] = useState(false)
const files$ = useMemo(() => new Subject<DirectoryEntry[]>(), [])
const selected$ = useMemo(() => new BehaviorSubject<string[]>([]), [])
const [isPending, startTransition] = useTransition()
const fetcher = () => pipe(
ffetch<DirectoryEntry[]>(
`${serverAddr}/filebrowser/downloaded`,
{
method: 'POST',
body: JSON.stringify({
subdir: '',
})
}
),
matchW(
(e) => {
pushMessage(e, 'error')
navigate('/login')
},
(d) => files$.next(d ?? []),
)
)()
const fetcherSubfolder = (sub: string) => {
const folders = sub.startsWith('/')
? sub.substring(1).split('/')
: sub.split('/')
const relpath = folders.length >= 2
? folders.slice(-(folders.length - 1)).join('/')
: folders.pop()
const _upperLevel = folders.slice(1, -1)
const upperLevel = _upperLevel.length === 2
? ['.', ..._upperLevel].join('/')
: _upperLevel.join('/')
const task = ffetch<DirectoryEntry[]>(`${serverAddr}/filebrowser/downloaded`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ subdir: relpath })
})
pipe(
task,
matchW(
(l) => pushMessage(l, 'error'),
(r) => files$.next(sub
? [{
isDirectory: true,
isVideo: false,
modTime: '',
name: '..',
path: upperLevel,
size: 0,
}, ...r.filter(f => f.name !== '')]
: r.filter(f => f.name !== '')
)
)
)()
}
const selectable$ = useMemo(() => files$.pipe(
combineLatestWith(selected$),
map(([data, selected]) => data.map(x => ({
...x,
selected: selected.includes(x.name)
}))),
share()
), [])
const selectable = useObservable(selectable$, [])
const addSelected = (name: string) => {
selected$.value.includes(name)
? selected$.next(selected$.value.filter(val => val !== name))
: selected$.next([...selected$.value, name])
}
const deleteFile = (entry: DirectoryEntry) => pipe(
ffetch(`${serverAddr}/filebrowser/delete`, {
method: 'POST',
body: JSON.stringify({
path: entry.path,
})
}),
matchW(
(l) => pushMessage(l, 'error'),
(_) => fetcher()
)
)()
const deleteSelected = () => {
Promise.all(selectable
.filter(entry => entry.selected)
.map(deleteFile)
).then(fetcher)
}
useEffect(() => {
fetcher()
}, [serverAddr])
const onFileClick = (path: string) => startTransition(() => {
const encoded = base64URLEncode(path)
window.open(`${serverAddr}/filebrowser/v/${encoded}?token=${localStorage.getItem('token')}`)
})
const downloadFile = (path: string) => startTransition(() => {
const encoded = base64URLEncode(path)
window.open(`${serverAddr}/filebrowser/d/${encoded}?token=${localStorage.getItem('token')}`)
})
const onFolderClick = (path: string) => startTransition(() => {
fetcherSubfolder(path)
})
return (
<Container
maxWidth="xl"
sx={{ mt: 4, mb: 4, minHeight: '100%' }}
onClick={() => setShowMenu(false)}
>
<IconMenu
posX={menuPos.x}
posY={menuPos.y}
hide={!showMenu}
onDownload={() => {
if (currentFile) {
downloadFile(currentFile?.path)
setCurrentFile(undefined)
}
}}
onDelete={() => {
if (currentFile) {
deleteFile(currentFile)
setCurrentFile(undefined)
}
}}
/>
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={!(files$.observed) || isPending}
>
<CircularProgress color="primary" />
</Backdrop>
<Paper
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
}}
onClick={() => setShowMenu(false)}
>
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
{selectable.length === 0 && i18n.t('noFilesFound')}
{selectable.map((file, idx) => (
<ListItem
onContextMenu={(e) => {
e.preventDefault()
setCurrentFile(file)
setMenuPos({ x: e.clientX, y: e.clientY })
setShowMenu(true)
}}
key={idx}
secondaryAction={
<div>
{!file.isDirectory && <Typography
variant="caption"
component="span"
>
{formatSize(file.size)}
</Typography>
}
{!file.isDirectory && <>
<Checkbox
edge="end"
checked={file.selected}
onChange={() => addSelected(file.name)}
/>
</>}
</div>
}
disablePadding
>
<ListItemButton onClick={
() => file.isDirectory
? onFolderClick(file.path)
: onFileClick(file.path)
}>
<ListItemIcon>
{file.isDirectory
? <FolderIcon />
: file.isVideo
? <VideoFileIcon />
: <InsertDriveFileIcon />
}
</ListItemIcon>
<ListItemText
primary={file.name}
secondary={file.name != '..' && new Date(file.modTime).toLocaleString()}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Paper>
<SpeedDial
ariaLabel='archive actions'
sx={{ position: 'absolute', bottom: 64, right: 24 }}
icon={<SpeedDialIcon />}
>
<SpeedDialAction
icon={<DeleteForeverIcon />}
tooltipTitle={i18n.t('deleteSelected')}
tooltipOpen
onClick={() => {
if (selected$.value.length > 0) {
setOpenDialog(true)
}
}}
/>
</SpeedDial>
<Dialog
open={openDialog}
onClose={() => setOpenDialog(false)}
>
<DialogTitle>
Are you sure?
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
You're deleting:
</DialogContentText>
<ul>
{selected$.value.map((entry, idx) => (
<li key={idx}>{entry}</li>
))}
</ul>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>
Cancel
</Button>
<Button
onClick={() => {
deleteSelected()
setOpenDialog(false)
}}
autoFocus
>
Ok
</Button>
</DialogActions>
</Dialog>
</Container>
)
}
const IconMenu: React.FC<{
posX: number
posY: number
hide: boolean
onDownload: () => void
onDelete: () => void
}> = ({ posX, posY, hide, onDelete, onDownload }) => {
return (
<Paper sx={{
width: 320,
maxWidth: '100%',
position: 'absolute',
top: posY,
left: posX,
display: hide ? 'none' : 'block',
zIndex: (theme) => theme.zIndex.drawer + 1,
}}>
<MenuList>
<MenuItem onClick={onDownload}>
<ListItemIcon>
<DownloadIcon fontSize="small" />
</ListItemIcon>
<ListItemText>
Download
</ListItemText>
</MenuItem>
<MenuItem onClick={onDelete}>
<ListItemIcon>
<DeleteForeverIcon fontSize="small" />
</ListItemIcon>
<ListItemText>
Delete
</ListItemText>
</MenuItem>
</MenuList>
</Paper>
)
}

View File

@@ -6,8 +6,10 @@ import {
Paper,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow
} from '@mui/material'
import { useAtomValue } from 'jotai'
import { useState } from 'react'
import { interval } from 'rxjs'
import { rpcPollingTimeState } from '../atoms/rpc'
import LivestreamDialog from '../components/livestream/LivestreamDialog'
import LivestreamSpeedDial from '../components/livestream/LivestreamSpeedDial'
import NoLivestreams from '../components/livestream/NoLivestreams'
@@ -24,7 +26,9 @@ const LiveStreamMonitorView: React.FC = () => {
const [progress, setProgress] = useState<LiveStreamProgress>()
const [openDialog, setOpenDialog] = useState(false)
useSubscription(interval(1000), () => {
const rpcPollingRate = useAtomValue(rpcPollingTimeState)
useSubscription(interval(rpcPollingRate), () => {
client
.progressLivestream()
.then(r => setProgress(r.result))

View File

@@ -93,7 +93,7 @@ export default function Login() {
</Title>
<Title fontWeight={'500'} fontSize={16} color={'gray'}>
To configure authentication check the&nbsp;
<a href='https://github.com/marcopeocchi/yt-dlp-web-ui/wiki/Authentication-methods'>wiki</a>.
<a href='https://github.com/marcopiovanello/yt-dlp-web-ui/wiki/Authentication-methods'>wiki</a>.
</Title>
<TextField
label="Username"

View File

@@ -18,7 +18,7 @@ import {
capitalize
} from '@mui/material'
import { useAtom } from 'jotai'
import { Suspense, useEffect, useMemo, useState } from 'react'
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'
import {
Subject,
debounceTime,
@@ -50,6 +50,7 @@ import CookiesTextField from '../components/CookiesTextField'
import UpdateBinaryButton from '../components/UpdateBinaryButton'
import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n'
import Translator from '../lib/i18n'
import { validateDomain, validateIP } from '../utils'
// NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS
@@ -82,6 +83,9 @@ export default function Settings() {
const serverAddr$ = useMemo(() => new Subject<string>(), [])
const serverPort$ = useMemo(() => new Subject<string>(), [])
const [, updateState] = useState({})
const forceUpdate = useCallback(() => updateState({}), [])
useEffect(() => {
const sub = baseURL$
.pipe(debounceTime(500))
@@ -133,6 +137,11 @@ export default function Settings() {
*/
const handleLanguageChange = (event: SelectChangeEvent<Language>) => {
setLanguage(event.target.value as Language)
Translator.instance.setLanguage(event.target.value)
setTimeout(() => {
forceUpdate()
}, 100)
}
/**
@@ -180,7 +189,6 @@ export default function Settings() {
</Grid>
<Grid item xs={12} md={12}>
<TextField
disabled={reverseProxy}
fullWidth
label={i18n.t('appTitle')}
defaultValue={appTitle}
@@ -276,8 +284,8 @@ export default function Settings() {
label={i18n.t('themeSelect')}
onChange={handleThemeChange}
>
<MenuItem value="light">Light</MenuItem>
<MenuItem value="dark">Dark</MenuItem>
<MenuItem value="light">{i18n.t('lightThemeButton')}</MenuItem>
<MenuItem value="dark">{i18n.t('darkThemeButton')}</MenuItem>
<MenuItem value="system">System</MenuItem>
</Select>
</FormControl>
@@ -300,7 +308,7 @@ export default function Settings() {
</Grid>
</Grid>
<Typography variant="h6" color="primary" sx={{ mt: 2, mb: 0.5 }}>
General download settings
{i18n.t('generalDownloadSettings')}
</Typography>
<FormControlLabel

2
go.mod
View File

@@ -1,4 +1,4 @@
module github.com/marcopeocchi/yt-dlp-web-ui/v3
module github.com/marcopiovanello/yt-dlp-web-ui/v3
go 1.23

View File

@@ -8,10 +8,10 @@ import (
"os"
"runtime"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/cli"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/cli"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/openid"
)
var (

View File

@@ -2,7 +2,7 @@
version = "v3.1.2";
meta = {
description = "A terrible web ui for yt-dlp. Designed to be self-hosted.";
homepage = "https://github.com/marcopeocchi/yt-dlp-web-ui";
homepage = "https://github.com/marcopiovanello/yt-dlp-web-ui";
license = lib.licenses.mpl20;
};
}

View File

@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "Swagger yt-dlp-webui - OpenAPI 3.1",
"description": "yt-dlp-webui api based on the OpenAPI 3.1 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). \n\nSome useful links:\n- [yt-dlp-webui repository](https://github.com/marcopeocchi/yt-dlp-web-ui)",
"description": "yt-dlp-webui api based on the OpenAPI 3.1 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). \n\nSome useful links:\n- [yt-dlp-webui repository](https://github.com/marcopiovanello/yt-dlp-web-ui)",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"email": "apiteam@swagger.io"
@@ -28,7 +28,7 @@
"description": "Everything about your Pets",
"externalDocs": {
"description": "Find out more",
"url": "https://github.com/marcopeocchi/yt-dlp-web-ui"
"url": "https://github.com/marcopiovanello/yt-dlp-web-ui"
}
}
],

18
server/archive/archive.go Normal file
View File

@@ -0,0 +1,18 @@
package archive
import (
"database/sql"
"github.com/go-chi/chi/v5"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/domain"
)
// alias type
// TODO: remove after refactoring
type Service = domain.Service
type Entity = domain.ArchiveEntry
func ApplyRouter(db *sql.DB) func(chi.Router) {
handler, _ := Container(db)
return handler.ApplyRouter()
}

View File

@@ -0,0 +1,16 @@
package archive
import (
"database/sql"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/domain"
)
func Container(db *sql.DB) (domain.RestHandler, domain.Service) {
var (
r = provideRepository(db)
s = provideService(r)
h = provideHandler(s)
)
return h, s
}

View File

@@ -0,0 +1,13 @@
package data
import "time"
type ArchiveEntry struct {
Id string
Title string
Path string
Thumbnail string
Source string
Metadata string
CreatedAt time.Time
}

View File

@@ -0,0 +1,51 @@
package domain
import (
"context"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/data"
)
type ArchiveEntry struct {
Id string `json:"id"`
Title string `json:"title"`
Path string `json:"path"`
Thumbnail string `json:"thumbnail"`
Source string `json:"source"`
Metadata string `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
}
type PaginatedResponse[T any] struct {
First int64 `json:"first"`
Next int64 `json:"next"`
Data T `json:"data"`
}
type Repository interface {
Archive(ctx context.Context, model *data.ArchiveEntry) error
SoftDelete(ctx context.Context, id string) (*data.ArchiveEntry, error)
HardDelete(ctx context.Context, id string) (*data.ArchiveEntry, error)
List(ctx context.Context, startRowId int, limit int) (*[]data.ArchiveEntry, error)
GetCursor(ctx context.Context, id string) (int64, error)
}
type Service interface {
Archive(ctx context.Context, entity *ArchiveEntry) error
SoftDelete(ctx context.Context, id string) (*ArchiveEntry, error)
HardDelete(ctx context.Context, id string) (*ArchiveEntry, error)
List(ctx context.Context, startRowId int, limit int) (*PaginatedResponse[[]ArchiveEntry], error)
GetCursor(ctx context.Context, id string) (int64, error)
}
type RestHandler interface {
List() http.HandlerFunc
Archive() http.HandlerFunc
SoftDelete() http.HandlerFunc
HardDelete() http.HandlerFunc
GetCursor() http.HandlerFunc
ApplyRouter() func(chi.Router)
}

View File

@@ -0,0 +1,42 @@
package archive
import (
"database/sql"
"sync"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/domain"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/repository"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/rest"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/service"
)
var (
repo domain.Repository
svc domain.Service
hand domain.RestHandler
repoOnce sync.Once
svcOnce sync.Once
handOnce sync.Once
)
func provideRepository(db *sql.DB) domain.Repository {
repoOnce.Do(func() {
repo = repository.New(db)
})
return repo
}
func provideService(r domain.Repository) domain.Service {
svcOnce.Do(func() {
svc = service.New(r)
})
return svc
}
func provideHandler(s domain.Service) domain.RestHandler {
handOnce.Do(func() {
hand = rest.New(s)
})
return hand
}

View File

@@ -0,0 +1,156 @@
package repository
import (
"context"
"database/sql"
"os"
"github.com/google/uuid"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/data"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/domain"
)
type Repository struct {
db *sql.DB
}
func New(db *sql.DB) domain.Repository {
return &Repository{
db: db,
}
}
func (r *Repository) Archive(ctx context.Context, entry *data.ArchiveEntry) error {
conn, err := r.db.Conn(ctx)
if err != nil {
return err
}
defer conn.Close()
_, err = conn.ExecContext(
ctx,
"INSERT INTO archive (id, title, path, thumbnail, source, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
uuid.NewString(),
entry.Title,
entry.Path,
entry.Thumbnail,
entry.Source,
entry.Metadata,
entry.CreatedAt,
)
return err
}
func (r *Repository) SoftDelete(ctx context.Context, id string) (*data.ArchiveEntry, error) {
conn, err := r.db.Conn(ctx)
if err != nil {
return nil, err
}
defer conn.Close()
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
var model data.ArchiveEntry
row := tx.QueryRowContext(ctx, "SELECT * FROM archive WHERE id = ?", id)
if err := row.Scan(
&model.Id,
&model.Title,
&model.Path,
&model.Thumbnail,
&model.Source,
&model.Metadata,
&model.CreatedAt,
); err != nil {
return nil, err
}
_, err = tx.ExecContext(ctx, "DELETE FROM archive WHERE id = ?", id)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return &model, nil
}
func (r *Repository) HardDelete(ctx context.Context, id string) (*data.ArchiveEntry, error) {
entry, err := r.SoftDelete(ctx, id)
if err != nil {
return nil, err
}
if err := os.Remove(entry.Path); err != nil {
return nil, err
}
return entry, nil
}
func (r *Repository) List(ctx context.Context, startRowId int, limit int) (*[]data.ArchiveEntry, error) {
conn, err := r.db.Conn(ctx)
if err != nil {
return nil, err
}
defer conn.Close()
var entries []data.ArchiveEntry
// cursor based pagination
rows, err := conn.QueryContext(ctx, "SELECT rowid, * FROM archive WHERE rowid > ? LIMIT ?", startRowId, limit)
if err != nil {
return nil, err
}
for rows.Next() {
var rowId int64
var entry data.ArchiveEntry
if err := rows.Scan(
&rowId,
&entry.Id,
&entry.Title,
&entry.Path,
&entry.Thumbnail,
&entry.Source,
&entry.Metadata,
&entry.CreatedAt,
); err != nil {
return &entries, err
}
entries = append(entries, entry)
}
return &entries, err
}
func (r *Repository) GetCursor(ctx context.Context, id string) (int64, error) {
conn, err := r.db.Conn(ctx)
if err != nil {
return -1, err
}
defer conn.Close()
row := conn.QueryRowContext(ctx, "SELECT rowid FROM archive WHERE id = ?", id)
var rowId int64
if err := row.Scan(&rowId); err != nil {
return -1, err
}
return rowId, nil
}

View File

@@ -0,0 +1,162 @@
package rest
import (
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/domain"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/openid"
middlewares "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/middleware"
)
type Handler struct {
service domain.Service
}
func New(service domain.Service) domain.RestHandler {
return &Handler{
service: service,
}
}
// List implements domain.RestHandler.
func (h *Handler) List() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
var (
startRowIdParam = r.URL.Query().Get("id")
LimitParam = r.URL.Query().Get("limit")
)
startRowId, err := strconv.Atoi(startRowIdParam)
if err != nil {
startRowId = 0
}
limit, err := strconv.Atoi(LimitParam)
if err != nil {
limit = 50
}
res, err := h.service.List(r.Context(), startRowId, limit)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
// Archive implements domain.RestHandler.
func (h *Handler) Archive() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
var req domain.ArchiveEntry
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err := h.service.Archive(r.Context(), &req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode("ok")
}
}
// HardDelete implements domain.RestHandler.
func (h *Handler) HardDelete() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
id := chi.URLParam(r, "id")
res, err := h.service.HardDelete(r.Context(), id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
// SoftDelete implements domain.RestHandler.
func (h *Handler) SoftDelete() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
id := chi.URLParam(r, "id")
res, err := h.service.SoftDelete(r.Context(), id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
// GetCursor implements domain.RestHandler.
func (h *Handler) GetCursor() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
id := chi.URLParam(r, "id")
cursorId, err := h.service.GetCursor(r.Context(), id)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := json.NewEncoder(w).Encode(cursorId); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
// ApplyRouter implements domain.RestHandler.
func (h *Handler) ApplyRouter() func(chi.Router) {
return func(r chi.Router) {
if config.Instance().RequireAuth {
r.Use(middlewares.Authenticated)
}
if config.Instance().UseOpenId {
r.Use(openid.Middleware)
}
r.Get("/", h.List())
r.Get("/cursor/{id}", h.GetCursor())
r.Post("/", h.Archive())
r.Delete("/soft/{id}", h.SoftDelete())
r.Delete("/hard/{id}", h.HardDelete())
}
}

View File

@@ -0,0 +1,121 @@
package service
import (
"context"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/data"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive/domain"
)
type Service struct {
repository domain.Repository
}
func New(repository domain.Repository) domain.Service {
return &Service{
repository: repository,
}
}
// Archive implements domain.Service.
func (s *Service) Archive(ctx context.Context, entity *domain.ArchiveEntry) error {
return s.repository.Archive(ctx, &data.ArchiveEntry{
Id: entity.Id,
Title: entity.Title,
Path: entity.Path,
Thumbnail: entity.Thumbnail,
Source: entity.Source,
Metadata: entity.Metadata,
CreatedAt: entity.CreatedAt,
})
}
// HardDelete implements domain.Service.
func (s *Service) HardDelete(ctx context.Context, id string) (*domain.ArchiveEntry, error) {
res, err := s.repository.HardDelete(ctx, id)
if err != nil {
return nil, err
}
return &domain.ArchiveEntry{
Id: res.Id,
Title: res.Title,
Path: res.Path,
Thumbnail: res.Thumbnail,
Source: res.Source,
Metadata: res.Metadata,
CreatedAt: res.CreatedAt,
}, nil
}
// SoftDelete implements domain.Service.
func (s *Service) SoftDelete(ctx context.Context, id string) (*domain.ArchiveEntry, error) {
res, err := s.repository.SoftDelete(ctx, id)
if err != nil {
return nil, err
}
return &domain.ArchiveEntry{
Id: res.Id,
Title: res.Title,
Path: res.Path,
Thumbnail: res.Thumbnail,
Source: res.Source,
Metadata: res.Metadata,
CreatedAt: res.CreatedAt,
}, nil
}
// List implements domain.Service.
func (s *Service) List(
ctx context.Context,
startRowId int,
limit int,
) (*domain.PaginatedResponse[[]domain.ArchiveEntry], error) {
res, err := s.repository.List(ctx, startRowId, limit)
if err != nil {
return nil, err
}
entities := make([]domain.ArchiveEntry, len(*res))
for i, model := range *res {
entities[i] = domain.ArchiveEntry{
Id: model.Id,
Title: model.Title,
Path: model.Path,
Thumbnail: model.Thumbnail,
Source: model.Source,
Metadata: model.Metadata,
CreatedAt: model.CreatedAt,
}
}
var (
first int64
next int64
)
if len(entities) > 0 {
first, err = s.repository.GetCursor(ctx, entities[0].Id)
if err != nil {
return nil, err
}
next, err = s.repository.GetCursor(ctx, entities[len(entities)-1].Id)
if err != nil {
return nil, err
}
}
return &domain.PaginatedResponse[[]domain.ArchiveEntry]{
First: first,
Next: next,
Data: entities,
}, nil
}
// GetCursor implements domain.Service.
func (s *Service) GetCursor(ctx context.Context, id string) (int64, error) {
return s.repository.GetCursor(ctx, id)
}

View File

@@ -0,0 +1,42 @@
package archiver
import (
"context"
"database/sql"
"log/slog"
evbus "github.com/asaskevich/EventBus"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
)
const QueueName = "process:archive"
var (
eventBus = evbus.New()
archiveService archive.Service
)
type Message = archive.Entity
func Register(db *sql.DB) {
_, s := archive.Container(db)
archiveService = s
}
func init() {
eventBus.Subscribe(QueueName, func(m *Message) {
slog.Info(
"archiving completed download",
slog.String("title", m.Title),
slog.String("source", m.Source),
)
archiveService.Archive(context.Background(), m)
})
}
func Publish(m *Message) {
if config.Instance().AutoArchive {
eventBus.Publish(QueueName, m)
}
}

View File

@@ -29,6 +29,7 @@ type Config struct {
OpenIdClientSecret string `yaml:"openid_client_secret"`
OpenIdRedirectURL string `yaml:"openid_redirect_url"`
FrontendPath string `yaml:"frontend_path"`
AutoArchive bool `yaml:"auto_archive"`
}
var (

View File

@@ -6,7 +6,7 @@ import (
"os"
"path/filepath"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
)
var lockFilePath = filepath.Join(config.Instance().Dir(), ".db.lock")
@@ -34,6 +34,21 @@ func Migrate(ctx context.Context, db *sql.DB) error {
return err
}
if _, err := db.ExecContext(
ctx,
`CREATE TABLE IF NOT EXISTS archive (
id CHAR(36) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
path VARCHAR(255) NOT NULL,
thumbnail TEXT,
source VARCHAR(255),
metadata TEXT,
created_at DATETIME
)`,
); err != nil {
return err
}
if lockFileExists() {
return nil
}

View File

@@ -6,7 +6,7 @@ import (
"os/exec"
"sync"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
)
func ParseURL(url string) (*Metadata, error) {

View File

@@ -23,6 +23,6 @@ type Format struct {
Resolution string `json:"resolution"`
VCodec string `json:"vcodec"`
ACodec string `json:"acodec"`
Size float32 `json:"filesize_approx"`
Size float64 `json:"filesize_approx"`
Language string `json:"language"`
}

View File

@@ -17,8 +17,8 @@ import (
"time"
"github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
)
/*

View File

@@ -7,7 +7,7 @@ import (
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
)
const TOKEN_COOKIE_NAME = "jwt-yt-dlp-webui"

View File

@@ -5,9 +5,9 @@ import "time"
// Used to unmarshall yt-dlp progress
type ProgressTemplate struct {
Percentage string `json:"percentage"`
Speed float32 `json:"speed"`
Speed float64 `json:"speed"`
Size string `json:"size"`
Eta float32 `json:"eta"`
Eta float64 `json:"eta"`
}
type PostprocessTemplate struct {
@@ -25,8 +25,8 @@ type DownloadOutput struct {
type DownloadProgress struct {
Status int `json:"process_status"`
Percentage string `json:"percentage"`
Speed float32 `json:"speed"`
ETA float32 `json:"eta"`
Speed float64 `json:"speed"`
ETA float64 `json:"eta"`
}
// Used to deser the yt-dlp -J output

View File

@@ -10,8 +10,8 @@ import (
"strings"
"time"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
)
const (

View File

@@ -4,8 +4,8 @@ import (
"testing"
"time"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
)
func setupTest() {

View File

@@ -7,8 +7,8 @@ import (
"os"
"path/filepath"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
)
type Monitor struct {

View File

@@ -8,7 +8,7 @@ import (
"sync"
"github.com/google/uuid"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
)
// In-Memory Thread-Safe Key-Value Storage with optional persistence

View File

@@ -6,7 +6,7 @@ import (
"log/slog"
evbus "github.com/asaskevich/EventBus"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"golang.org/x/sync/semaphore"
)

View File

@@ -9,7 +9,7 @@ import (
"strings"
"time"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
)
type metadata struct {

View File

@@ -18,7 +18,8 @@ import (
"strings"
"time"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archiver"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
)
const downloadTemplate = `download:
@@ -87,6 +88,7 @@ func (p *Process) Start() {
buildFilename(&p.Output)
templateReplacer := strings.NewReplacer("\n", "", "\t", "", " ", "")
baseParams := []string{
strings.Split(p.Url, "?list")[0], //no playlist
"--newline",
@@ -193,13 +195,12 @@ func (p *Process) parseLogEntry(entry []byte) {
if err := json.Unmarshal(entry, &postprocess); err == nil {
p.Output.SavedFilePath = postprocess.FilePath
slog.Info("postprocess",
slog.String("id", p.getShortId()),
slog.String("url", p.Url),
slog.String("filepath", postprocess.FilePath),
)
// slog.Info("postprocess",
// slog.String("id", p.getShortId()),
// slog.String("url", p.Url),
// slog.String("filepath", postprocess.FilePath),
// )
}
}
func (p *Process) detectYtDlpErrors(r io.Reader) {
@@ -218,6 +219,24 @@ func (p *Process) detectYtDlpErrors(r io.Reader) {
// Convention: All completed processes has progress -1
// and speed 0 bps.
func (p *Process) Complete() {
// auto archive
// TODO: it's not that deterministic :/
if p.Progress.Percentage == "" && p.Progress.Speed == 0 {
var serializedMetadata bytes.Buffer
json.NewEncoder(&serializedMetadata).Encode(p.Info)
archiver.Publish(&archiver.Message{
Id: p.Id,
Path: p.Output.SavedFilePath,
Title: p.Info.Title,
Thumbnail: p.Info.Thumbnail,
Source: p.Url,
Metadata: serializedMetadata.String(),
CreatedAt: p.Info.CreatedAt,
})
}
p.Progress = DownloadProgress{
Status: StatusCompleted,
Percentage: "-1",

View File

@@ -9,9 +9,9 @@ import (
"github.com/go-chi/chi/v5"
"github.com/gorilla/websocket"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
middlewares "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/openid"
)
var upgrader = websocket.Upgrader{

View File

@@ -4,7 +4,7 @@ import (
"context"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"golang.org/x/oauth2"
)

View File

@@ -3,7 +3,7 @@ package rest
import (
"database/sql"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
)
type ContainerArgs struct {

View File

@@ -2,9 +2,9 @@ package rest
import (
"github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
middlewares "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/openid"
)
func Container(args *ContainerArgs) *Handler {

View File

@@ -5,7 +5,7 @@ import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
)
type Handler struct {

View File

@@ -10,9 +10,9 @@ import (
"time"
"github.com/google/uuid"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/livestream"
)
type Service struct {

View File

@@ -2,11 +2,11 @@ package rpc
import (
"github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/livestream"
middlewares "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/openid"
)
// Dependency injection container.

View File

@@ -4,11 +4,11 @@ import (
"errors"
"log/slog"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/formats"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/sys"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/updater"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/formats"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/livestream"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/sys"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/updater"
)
type Service struct {

View File

@@ -19,16 +19,19 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/dbutil"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/handlers"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/logging"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/rest"
ytdlpRPC "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/rpc"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archiver"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/dbutil"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/handlers"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/livestream"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/logging"
middlewares "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/openid"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/rest"
ytdlpRPC "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/rpc"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/status"
_ "modernc.org/sqlite"
)
@@ -145,6 +148,8 @@ func RunBlocking(rc *RunConfig) {
}
func newServer(c serverConfig) *http.Server {
archiver.Register(c.db)
service := ytdlpRPC.Container(c.mdb, c.mq, c.lm)
rpc.Register(service)
@@ -174,8 +179,8 @@ func newServer(c serverConfig) *http.Server {
// swagger
r.Mount("/openapi", http.FileServerFS(c.swagger))
// Archive routes
r.Route("/archive", func(r chi.Router) {
// Filebrowser routes
r.Route("/filebrowser", func(r chi.Router) {
if config.Instance().RequireAuth {
r.Use(middlewares.Authenticated)
}
@@ -189,6 +194,9 @@ func newServer(c serverConfig) *http.Server {
r.Get("/bulk", handlers.BulkDownload(c.mdb))
})
// Archive routes
r.Route("/archive", archive.ApplyRouter(c.db))
// Authentication routes
r.Route("/auth", func(r chi.Router) {
r.Post("/login", handlers.Login)
@@ -214,6 +222,9 @@ func newServer(c serverConfig) *http.Server {
// Logging
r.Route("/log", logging.ApplyRouter(observableLogger))
// Status
r.Route("/status", status.ApplyRouter(c.mdb))
return &http.Server{Handler: r}
}

View File

@@ -0,0 +1,28 @@
package domain
import (
"context"
"net/http"
)
type Status struct {
Downloading int `json:"downloading"`
Pending int `json:"pending"`
Completed int `json:"completed"`
DownloadSpeed int `json:"download_speed"`
}
type Repository interface {
Pending(ctx context.Context) int
Completed(ctx context.Context) int
Downloading(ctx context.Context) int
DownloadSpeed(ctx context.Context) int64
}
type Service interface {
Status(ctx context.Context) (*Status, error)
}
type RestHandler interface {
Status() http.HandlerFunc
}

View File

@@ -0,0 +1,65 @@
package repository
import (
"context"
"slices"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/status/domain"
)
type Repository struct {
mdb *internal.MemoryDB
}
// DownloadSpeed implements domain.Repository.
func (r *Repository) DownloadSpeed(ctx context.Context) int64 {
processes := r.mdb.All()
var downloadSpeed float64
for _, p := range *processes {
downloadSpeed += p.Progress.Speed
}
return int64(downloadSpeed)
}
// Completed implements domain.Repository.
func (r *Repository) Completed(ctx context.Context) int {
processes := r.mdb.All()
completed := slices.DeleteFunc(*processes, func(p internal.ProcessResponse) bool {
return p.Progress.Status != internal.StatusCompleted
})
return len(completed)
}
// Downloading implements domain.Repository.
func (r *Repository) Downloading(ctx context.Context) int {
processes := r.mdb.All()
downloading := slices.DeleteFunc(*processes, func(p internal.ProcessResponse) bool {
return p.Progress.Status != internal.StatusDownloading
})
return len(downloading)
}
// Pending implements domain.Repository.
func (r *Repository) Pending(ctx context.Context) int {
processes := r.mdb.All()
pending := slices.DeleteFunc(*processes, func(p internal.ProcessResponse) bool {
return p.Progress.Status != internal.StatusPending
})
return len(pending)
}
func New(mdb *internal.MemoryDB) domain.Repository {
return &Repository{
mdb: mdb,
}
}

View File

@@ -0,0 +1,38 @@
package rest
import (
"encoding/json"
"net/http"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/status/domain"
)
type RestHandler struct {
service domain.Service
}
// Status implements domain.RestHandler.
func (h *RestHandler) Status() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
status, err := h.service.Status(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(status); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func New(service domain.Service) domain.RestHandler {
return &RestHandler{
service: service,
}
}

View File

@@ -0,0 +1,69 @@
package service
import (
"context"
"sync"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/rest"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/status/domain"
)
type Service struct {
repository domain.Repository
utilityService *rest.Service
}
// Version implements domain.Status.
func (s *Service) Status(ctx context.Context) (*domain.Status, error) {
// rpcVersion, downloaderVersion, err := s.utilityService.GetVersion(ctx)
// if err != nil {
// return nil, err
// }
var (
wg sync.WaitGroup
pending int
downloading int
completed int
speed int64
// version = fmt.Sprintf("RPC: %s yt-dlp: %s", rpcVersion, downloaderVersion)
)
wg.Add(4)
go func() {
pending = s.repository.Pending(ctx)
wg.Done()
}()
go func() {
downloading = s.repository.Downloading(ctx)
wg.Done()
}()
go func() {
completed = s.repository.Completed(ctx)
wg.Done()
}()
go func() {
speed = s.repository.DownloadSpeed(ctx)
wg.Done()
}()
wg.Wait()
return &domain.Status{
Downloading: downloading,
Pending: pending,
Completed: completed,
DownloadSpeed: int(speed),
}, nil
}
func New(repository domain.Repository, utilityService *rest.Service) domain.Service {
return &Service{
repository: repository,
utilityService: utilityService,
}
}

21
server/status/status.go Normal file
View File

@@ -0,0 +1,21 @@
package status
import (
"github.com/go-chi/chi/v5"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/status/repository"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/status/rest"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/status/service"
)
func ApplyRouter(mdb *internal.MemoryDB) func(chi.Router) {
var (
r = repository.New(mdb)
s = service.New(r, nil) //TODO: nil, wtf?
h = rest.New(s)
)
return func(r chi.Router) {
r.Get("/", h.Status())
}
}

View File

@@ -4,8 +4,8 @@ import (
"os"
"path/filepath"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
"golang.org/x/sys/unix"
)

View File

@@ -3,7 +3,7 @@ package updater
import (
"os/exec"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
)
// Update using the builtin function of yt-dlp