bcc6d40ed7
Lint & Test / test (push) Successful in 20s
Security
- Default bind 127.0.0.1; first-run bootstrap generates random api_token
and refuses to bind non-loopback without auth unless explicitly opted in
- Path-traversal hardened: BrowserService.validate_path rejects absolute
paths, drive letters, UNC, NUL bytes. /api/browser/{play,metadata,
thumbnail} now require folder_id and a folder-relative path
- Pydantic validators on links: http(s) URLs only, mdi:<slug> icons only
- Scripts/callbacks/links create/update/delete gated by *_management flags
- Strict CSP, X-Frame-Options DENY, Referrer-Policy no-referrer,
X-Content-Type-Options nosniff
- CORS locked to localhost:<port> + 127.0.0.1:<port> by default; configurable
- config.yaml writes atomic (tmp + os.replace) and 0o600 on POSIX
- Subprocesses spawned in their own process group / new session so timeout
kills the whole tree (Windows CREATE_NEW_PROCESS_GROUP, POSIX
start_new_session=True)
- Frontend XSS: monitor name + details escapeHtml'd; power button moved to
delegated data-action handler; remote MDI SVGs parsed and sanitized
(strip script/foreignObject/on*/javascript: hrefs) before innerHTML
- All dynamic URL segments now wrapped in encodeURIComponent
Bugs
- WebSocket reconnect: close previous socket before opening new, clear
ping interval per-socket, clear reconnectTimeout up-front, retry on
online/visibilitychange, try/catch JSON.parse
- Artwork fetch race: AbortController + generation guard
- _broadcast_after_open: initialize status, swallow per-poll errors,
background tasks tracked in a strong-ref set with done-callback cleanup
- Audio analyzer: sticky _unavailable flag prevents infinite start/stop
spin when no loopback device exists; cleared by set_device()
- Volume short-circuit cache invalidated when server reports remote volume
- Browser thumbnail race: per-folder generation counter + isConnected
checks; aborts in-flight fetches on navigation
- Track-skip uses cached title instead of full WinRT status round-trip
Performance
- Linux MPRIS/pactl and /api/display DDC-CI handlers wrapped in
asyncio.to_thread so blocking IO never stalls the event loop
- browse_directory moved off the event loop (SMB shares could freeze it)
- Windows status poll caches one asyncio loop per worker thread via
threading.local instead of new_event_loop/close on every 0.5s tick
- broadcast() serializes JSON once and uses send_text to all clients
- Hourly thumbnail cache cleanup scheduled in lifespan (was never invoked
— cache grew unbounded)
- Progress drag listeners attached only while dragging
Quality
- All asyncio.get_event_loop() in coroutines → get_running_loop()
- ThreadPoolExecutors shut down cleanly during lifespan teardown
- config_manager dedup: 12 near-identical methods collapsed onto generic
_upsert/_delete helpers (~290 lines removed)
- Service worker no longer pass-throughs every fetch
- M3U playlist written via NamedTemporaryFile (no fixed-path symlink
clobber race)
- __version__ now prefers live pyproject.toml in dev checkouts so
pip install -e . users see the source-of-truth version, not the stale
package-metadata version baked in at install time
UI/UX (Studio Reference)
- Green leftover focus rings (rgba(29,185,84,...)) all replaced with
copper accent (rgba(var(--copper-rgb),...))
- Dialogs: square corners, copper top hairline, unified with editorial
chrome
- .browser-item: transparent with copper hover border (was filled card)
- Audio device select uses var(--sans) instead of generic system font
- Mobile container padding tuned for ≤480px screens
- Breadcrumb home is a real <button> with aria-label; aria-current on root
- i18n: filled display.msg.power_*, execution.*, scripts.params.execute,
callbacks.empty in both en + ru
297 lines
20 KiB
JSON
297 lines
20 KiB
JSON
{
|
|
"app.title": "Медиа Сервер",
|
|
"auth.message": "Введите API токен для подключения к медиа серверу.",
|
|
"auth.placeholder": "Введите API токен",
|
|
"auth.connect": "Подключиться",
|
|
"auth.help": "Чтобы получить токен, выполните:",
|
|
"auth.logout": "Выйти",
|
|
"auth.logout.title": "Очистить сохраненный токен",
|
|
"auth.invalid": "Неверный токен. Пожалуйста, попробуйте снова.",
|
|
"auth.cleared": "Токен очищен. Пожалуйста, введите новый токен.",
|
|
"auth.required": "Пожалуйста, введите токен",
|
|
"player.theme": "Переключить тему",
|
|
"accent.custom": "Свой цвет",
|
|
"player.locale": "Изменить язык",
|
|
"player.previous": "Предыдущий",
|
|
"player.play": "Воспроизвести/Пауза",
|
|
"player.next": "Следующий",
|
|
"player.mute": "Без звука",
|
|
"player.status.connected": "Подключено",
|
|
"player.status.disconnected": "Отключено",
|
|
"player.no_media": "Медиа не воспроизводится",
|
|
"player.kicker": "Сейчас играет",
|
|
"player.modes": "Режимы",
|
|
"header.connected": "Подключено",
|
|
"header.volume": "Том I",
|
|
"header.edition": "Studio Reference",
|
|
"header.edition_sub": "Studio Reference Edition",
|
|
"meta.state": "Состояние",
|
|
"meta.source": "Источник",
|
|
"player.title_unavailable": "Название недоступно",
|
|
"player.source": "Источник:",
|
|
"player.unknown_source": "Неизвестно",
|
|
"player.vinyl": "Режим винила",
|
|
"player.visualizer": "Аудио визуализатор",
|
|
"player.background": "Динамический фон",
|
|
"player.fullscreen": "Полноэкранный режим",
|
|
"player.fullscreen.exit": "Выйти из полного экрана",
|
|
"player.fullscreen.exit_short": "Выйти",
|
|
"state.playing": "Воспроизведение",
|
|
"state.paused": "Пауза",
|
|
"state.stopped": "Остановлено",
|
|
"state.idle": "Ожидание",
|
|
"scripts.quick_actions": "Быстрые Действия",
|
|
"scripts.no_scripts": "Скрипты не настроены",
|
|
"scripts.management": "Управление Скриптами",
|
|
"scripts.add": "Добавить",
|
|
"scripts.table.name": "Имя",
|
|
"scripts.table.label": "Метка",
|
|
"scripts.table.command": "Команда",
|
|
"scripts.table.timeout": "Таймаут",
|
|
"scripts.table.actions": "Действия",
|
|
"scripts.empty": "Скрипты не настроены. Нажмите 'Добавить' для создания.",
|
|
"scripts.dialog.add": "Добавить Скрипт",
|
|
"scripts.dialog.edit": "Редактировать Скрипт",
|
|
"scripts.field.name": "Имя Скрипта *",
|
|
"scripts.field.label": "Метка",
|
|
"scripts.field.command": "Команда *",
|
|
"scripts.field.description": "Описание",
|
|
"scripts.field.icon": "Иконка (MDI)",
|
|
"scripts.field.timeout": "Таймаут (секунды)",
|
|
"scripts.placeholder.name": "Только буквы, цифры и подчеркивания",
|
|
"scripts.placeholder.label": "Человеко-читаемое имя",
|
|
"scripts.placeholder.command": "например, shutdown /s /t 0",
|
|
"scripts.placeholder.description": "Что делает этот скрипт?",
|
|
"scripts.placeholder.icon": "например, mdi:power",
|
|
"scripts.button.cancel": "Отмена",
|
|
"scripts.button.save": "Сохранить",
|
|
"scripts.button.edit": "Редактировать",
|
|
"scripts.button.delete": "Удалить",
|
|
"scripts.msg.executed": "{name} выполнен успешно",
|
|
"scripts.msg.execute_failed": "Не удалось выполнить {name}",
|
|
"scripts.msg.execute_error": "Ошибка выполнения {name}",
|
|
"scripts.msg.created": "Скрипт создан успешно",
|
|
"scripts.msg.updated": "Скрипт обновлен успешно",
|
|
"scripts.msg.create_failed": "Не удалось создать скрипт",
|
|
"scripts.msg.update_failed": "Не удалось обновить скрипт",
|
|
"scripts.msg.deleted": "Скрипт удален успешно",
|
|
"scripts.msg.delete_failed": "Не удалось удалить скрипт",
|
|
"scripts.msg.not_found": "Скрипт не найден",
|
|
"scripts.msg.load_failed": "Не удалось загрузить данные скрипта",
|
|
"scripts.msg.list_failed": "Не удалось загрузить скрипты",
|
|
"scripts.confirm.delete": "Вы уверены, что хотите удалить скрипт \"{name}\"?",
|
|
"scripts.execution.title": "Результат выполнения",
|
|
"scripts.execution.output": "Вывод",
|
|
"scripts.execution.error_output": "Вывод ошибок",
|
|
"scripts.execution.close": "Закрыть",
|
|
"scripts.confirm.unsaved": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?",
|
|
"scripts.field.parameters": "Параметры",
|
|
"scripts.params.add": "+ Добавить",
|
|
"scripts.params.remove": "Удалить параметр",
|
|
"scripts.params.required": "Обязательный",
|
|
"scripts.params.name_placeholder": "имя_параметра",
|
|
"scripts.params.description_placeholder": "Описание параметра",
|
|
"scripts.params.default_placeholder": "По умолчанию",
|
|
"scripts.params.options_placeholder": "вариант1, вариант2, ...",
|
|
"scripts.params.execute": "Выполнить",
|
|
"callbacks.management": "Управление Обратными Вызовами",
|
|
"callbacks.description": "Обратные вызовы - это скрипты, автоматически запускаемые при событиях управления медиа (воспроизведение, пауза, остановка и т.д.)",
|
|
"callbacks.add": "Добавить",
|
|
"callbacks.table.event": "Событие",
|
|
"callbacks.table.command": "Команда",
|
|
"callbacks.table.timeout": "Таймаут",
|
|
"callbacks.table.actions": "Действия",
|
|
"callbacks.empty": "Обратные вызовы не настроены. Нажмите 'Добавить' для создания.",
|
|
"callbacks.dialog.add": "Добавить Обратный Вызов",
|
|
"callbacks.dialog.edit": "Редактировать Обратный Вызов",
|
|
"callbacks.field.event": "Событие *",
|
|
"callbacks.field.command": "Команда *",
|
|
"callbacks.field.timeout": "Таймаут (секунды)",
|
|
"callbacks.field.workdir": "Рабочая Директория",
|
|
"callbacks.placeholder.event": "Выберите событие...",
|
|
"callbacks.placeholder.command": "например, shutdown /s /t 0",
|
|
"callbacks.placeholder.workdir": "Опционально",
|
|
"callbacks.button.cancel": "Отмена",
|
|
"callbacks.button.save": "Сохранить",
|
|
"callbacks.button.edit": "Редактировать",
|
|
"callbacks.button.delete": "Удалить",
|
|
"callbacks.event.on_play": "on_play - После успешного воспроизведения",
|
|
"callbacks.event.on_pause": "on_pause - После успешной паузы",
|
|
"callbacks.event.on_stop": "on_stop - После успешной остановки",
|
|
"callbacks.event.on_next": "on_next - После успешного перехода к следующему",
|
|
"callbacks.event.on_previous": "on_previous - После успешного перехода к предыдущему",
|
|
"callbacks.event.on_volume": "on_volume - После изменения громкости",
|
|
"callbacks.event.on_mute": "on_mute - После переключения звука",
|
|
"callbacks.event.on_seek": "on_seek - После успешной перемотки",
|
|
"callbacks.event.on_turn_on": "on_turn_on - Действие только для обратных вызовов",
|
|
"callbacks.event.on_turn_off": "on_turn_off - Действие только для обратных вызовов",
|
|
"callbacks.event.on_toggle": "on_toggle - Действие только для обратных вызовов",
|
|
"callbacks.msg.created": "Обратный вызов создан успешно",
|
|
"callbacks.msg.updated": "Обратный вызов обновлен успешно",
|
|
"callbacks.msg.create_failed": "Не удалось создать обратный вызов",
|
|
"callbacks.msg.update_failed": "Не удалось обновить обратный вызов",
|
|
"callbacks.msg.deleted": "Обратный вызов удален успешно",
|
|
"callbacks.msg.delete_failed": "Не удалось удалить обратный вызов",
|
|
"callbacks.msg.not_found": "Обратный вызов не найден",
|
|
"callbacks.msg.load_failed": "Не удалось загрузить данные обратного вызова",
|
|
"callbacks.msg.list_failed": "Не удалось загрузить обратные вызовы",
|
|
"callbacks.confirm.delete": "Вы уверены, что хотите удалить обратный вызов \"{name}\"?",
|
|
"callbacks.confirm.unsaved": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?",
|
|
"tab.player": "Сейчас играет",
|
|
"tab.browser": "Библиотека",
|
|
"tab.quick_access": "Быстрый Доступ",
|
|
"tab.settings": "Настройки",
|
|
"tab.display": "Дисплей",
|
|
"settings.section.scripts": "Скрипты",
|
|
"settings.section.callbacks": "Колбэки",
|
|
"settings.section.links": "Ссылки",
|
|
"settings.section.audio": "Аудио",
|
|
"settings.audio.description": "Выберите аудиоустройство для захвата звука визуализатора.",
|
|
"settings.audio.device": "Устройство захвата",
|
|
"settings.audio.auto": "Автоопределение",
|
|
"settings.audio.status_active": "Захват аудио",
|
|
"settings.audio.status_available": "Доступно, не захватывает",
|
|
"settings.audio.status_unavailable": "Недоступно",
|
|
"settings.audio.device_changed": "Аудиоустройство изменено",
|
|
"settings.audio.device_change_failed": "Не удалось изменить аудиоустройство",
|
|
"quick_access.no_items": "Быстрые действия и ссылки не настроены",
|
|
"display.loading": "Загрузка мониторов...",
|
|
"display.error": "Не удалось загрузить мониторы",
|
|
"display.no_monitors": "Мониторы не обнаружены",
|
|
"display.power_on": "Включить",
|
|
"display.power_off": "Выключить",
|
|
"display.primary": "Основной",
|
|
"display.brightness": "Яркость",
|
|
"display.contrast": "Контраст",
|
|
"display.tuning": "Настройка изображения",
|
|
"display.input_source": "Вход",
|
|
"display.color_preset": "Цветовая температура",
|
|
"display.picture_mode": "Режим изображения",
|
|
"display.msg.contrast_failed": "Не удалось установить контраст",
|
|
"display.msg.input_changed": "Источник входа переключён",
|
|
"display.msg.input_failed": "Не удалось переключить источник",
|
|
"display.msg.color_changed": "Цветовая температура применена",
|
|
"display.msg.color_failed": "Не удалось применить цветовую температуру",
|
|
"display.msg.mode_changed": "Режим изображения применён",
|
|
"display.msg.mode_failed": "Не удалось применить режим изображения",
|
|
"display.msg.power_on": "Монитор включён",
|
|
"display.msg.power_off": "Монитор выключен",
|
|
"display.msg.power_failed": "Не удалось переключить питание монитора",
|
|
"execution.result": "Результат выполнения",
|
|
"execution.executing": "Выполняется",
|
|
"execution.status": "Статус",
|
|
"execution.exit_code": "Код выхода",
|
|
"execution.duration": "Длительность",
|
|
"execution.success": "Успешно",
|
|
"execution.failed": "Ошибка",
|
|
"execution.running": "Выполняется...",
|
|
"execution.no_output": "(нет вывода)",
|
|
"browser.title": "Медиа Браузер",
|
|
"browser.home": "Главная",
|
|
"browser.manage_folders": "Управление папками",
|
|
"browser.select_folder": "Выберите папку...",
|
|
"browser.select_folder_option": "Выберите папку...",
|
|
"browser.no_folder_selected": "Выберите папку для просмотра медиафайлов",
|
|
"browser.no_items": "В этой папке не найдено медиафайлов",
|
|
"browser.view_grid": "Сетка",
|
|
"browser.view_compact": "Компактный вид",
|
|
"browser.view_list": "Список",
|
|
"browser.search": "Поиск...",
|
|
"browser.items_per_page": "Элементов на странице:",
|
|
"browser.page": "Страница",
|
|
"browser.previous": "Предыдущая",
|
|
"browser.next": "Следующая",
|
|
"browser.download": "Скачать",
|
|
"browser.play_success": "Воспроизведение {filename}",
|
|
"browser.play_error": "Не удалось воспроизвести файл",
|
|
"browser.play_all": "Воспроизвести все",
|
|
"browser.play_all_success": "Воспроизведение {count} файлов",
|
|
"browser.play_all_error": "Не удалось воспроизвести папку",
|
|
"browser.error_loading": "Ошибка загрузки каталога",
|
|
"browser.error_loading_folders": "Не удалось загрузить медиа папки",
|
|
"browser.manage_folders_hint": "Управление папками отключено. Установите media_folders_management: true в config.yaml для включения.",
|
|
"browser.unavailable": "Недоступна",
|
|
"browser.folder_available": "Доступна",
|
|
"browser.folder_unavailable": "Недоступна (путь не найден)",
|
|
"browser.folder_disabled": "отключена",
|
|
"browser.folder_edit": "Редактировать папку",
|
|
"browser.folder_delete": "Удалить папку",
|
|
"browser.folder_created": "Медиа папка успешно создана",
|
|
"browser.folder_updated": "Медиа папка успешно обновлена",
|
|
"browser.folder_deleted": "Медиа папка успешно удалена",
|
|
"browser.folder_save_error": "Не удалось сохранить медиа папку",
|
|
"browser.folder_delete_error": "Не удалось удалить медиа папку",
|
|
"browser.folder_confirm_delete": "Вы уверены, что хотите удалить папку \"{name}\"?",
|
|
"browser.folders_description": "Медиа папки для просмотра. Для сетевых ресурсов показан статус доступности.",
|
|
"browser.folders_empty": "Медиа папки не настроены. Нажмите \"+\" для добавления.",
|
|
"browser.folders_table.id": "ID",
|
|
"browser.folders_table.label": "Метка",
|
|
"browser.folders_table.path": "Путь",
|
|
"browser.folders_table.status": "Статус",
|
|
"browser.folders_table.actions": "Действия",
|
|
"settings.section.media_folders": "Медиа папки",
|
|
"browser.folder_dialog.title_add": "Добавить медиа папку",
|
|
"browser.folder_dialog.title_edit": "Редактировать медиа папку",
|
|
"browser.folder_dialog.folder_id": "ID папки *",
|
|
"browser.folder_dialog.folder_id_help": "Только буквы, цифры и подчеркивание",
|
|
"browser.folder_dialog.label": "Метка *",
|
|
"browser.folder_dialog.label_help": "Отображаемое имя папки",
|
|
"browser.folder_dialog.path": "Путь *",
|
|
"browser.folder_dialog.path_help": "Абсолютный путь к медиа каталогу",
|
|
"browser.folder_dialog.enabled": "Включено",
|
|
"browser.folder_dialog.cancel": "Отмена",
|
|
"browser.folder_dialog.save": "Сохранить",
|
|
"browser.list_header.name": "Название",
|
|
"browser.list_header.bitrate": "Битрейт",
|
|
"browser.list_header.duration": "Длительность",
|
|
"browser.list_header.size": "Размер",
|
|
"browser.showing_items": "Показано {from}\u2013{to} из {total}",
|
|
"browser.download_error": "Не удалось скачать файл",
|
|
"connection.reconnecting": "Соединение потеряно. Переподключение (попытка {attempt})...",
|
|
"connection.lost": "Соединение потеряно. Сервер может быть недоступен.",
|
|
"connection.reconnect": "Переподключиться",
|
|
"dialog.cancel": "Отмена",
|
|
"dialog.confirm": "Подтвердить",
|
|
"links.description": "Быстрые ссылки, отображаемые в виде иконок в шапке. Нажмите на иконку, чтобы открыть URL в новой вкладке.",
|
|
"links.empty": "Ссылки не настроены. Нажмите 'Добавить' для создания.",
|
|
"links.table.name": "Имя",
|
|
"links.table.url": "URL",
|
|
"links.table.label": "Метка",
|
|
"links.table.actions": "Действия",
|
|
"links.dialog.add": "Добавить Ссылку",
|
|
"links.dialog.edit": "Редактировать Ссылку",
|
|
"links.field.name": "Имя Ссылки *",
|
|
"links.field.url": "URL *",
|
|
"links.field.icon": "Иконка (MDI)",
|
|
"links.field.label": "Метка",
|
|
"links.field.description": "Описание",
|
|
"links.placeholder.name": "Только буквы, цифры и подчеркивания",
|
|
"links.placeholder.url": "https://example.com",
|
|
"links.placeholder.icon": "mdi:link",
|
|
"links.placeholder.label": "Текст подсказки",
|
|
"links.placeholder.description": "Куда ведет эта ссылка?",
|
|
"links.button.cancel": "Отмена",
|
|
"links.button.save": "Сохранить",
|
|
"links.button.edit": "Редактировать",
|
|
"links.button.delete": "Удалить",
|
|
"links.msg.created": "Ссылка создана успешно",
|
|
"links.msg.updated": "Ссылка обновлена успешно",
|
|
"links.msg.create_failed": "Не удалось создать ссылку",
|
|
"links.msg.update_failed": "Не удалось обновить ссылку",
|
|
"links.msg.deleted": "Ссылка удалена успешно",
|
|
"links.msg.delete_failed": "Не удалось удалить ссылку",
|
|
"links.msg.not_found": "Ссылка не найдена",
|
|
"links.msg.load_failed": "Не удалось загрузить данные ссылки",
|
|
"links.confirm.delete": "Вы уверены, что хотите удалить ссылку \"{name}\"?",
|
|
"links.confirm.unsaved": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?",
|
|
"about.button_title": "О программе",
|
|
"about.title": "О программе",
|
|
"about.created_by": "Создано",
|
|
"about.email": "Эл. почта",
|
|
"about.repository": "Репозиторий",
|
|
"about.source_code": "Исходный код",
|
|
"dialog.close": "Закрыть",
|
|
"update.available": "Доступно обновление: v{version}",
|
|
"update.view_release": "Перейти к релизу"
|
|
}
|