diff --git a/.gitattributes b/.gitattributes index 7bee716a5d..7a5df63373 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,26 +1,6 @@ # Auto-detect text files and perform LF normalization * text=auto -# Fork-specific files - always keep ours during merge conflicts -# These files contain customizations that should not be overwritten by upstream merges -README.md merge=ours -package_info merge=ours -bazarr/app/check_update.py merge=ours -custom_libs/subliminal_patch/providers/opensubtitles_scraper.py merge=ours -Dockerfile merge=ours -docker-compose.yml merge=ours -docker/entrypoint.sh merge=ours -.dockerignore merge=ours -.gitattributes merge=ours -docs/FORK_MAINTENANCE.md merge=ours - -# GitHub workflows - keep our fork's versions -.github/workflows/* merge=ours - -# Git submodule - keep our reference -.gitmodules merge=ours -opensubtitles-scraper merge=ours - # Binary files *.png binary *.jpg binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b663249502..b774a8b9e7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -21,7 +21,7 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Software (please complete the following information):** - - Bazarr: [e.g. v 0.6.1] + - Bazarr+: [e.g. v2.0.0] - Radarr version [e.g. v 0.2.0.0001] - Sonarr version [e.g. v 2.0.0.0001] - OS: [e.g. Windows 10] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 77b0f431ff..cb76eff4d6 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,4 +5,4 @@ contact_links: about: The Bazarr wiki should help guide you through installation and setup as well as help resolve common problems and answer frequently asked questions. - name: 🚀 Feature suggestions url: https://github.com/LavX/bazarr/issues - about: Share your suggestions or ideas to make Bazarr better! + about: Share your suggestions or ideas to make Bazarr+ better! diff --git a/.github/scripts/build_test.sh b/.github/scripts/build_test.sh index 56ccd6c1a7..d0714b5db0 100755 --- a/.github/scripts/build_test.sh +++ b/.github/scripts/build_test.sh @@ -7,7 +7,7 @@ sleep 30 if kill -s 0 $PID then - echo "Bazarr is still running. We'll test if UI is working..." + echo "Bazarr+ is still running. We'll test if UI is working..." else exit 1 fi diff --git a/.github/workflows/build-docker-manual.yml b/.github/workflows/build-docker-manual.yml index c247fe8f04..55a6764b15 100644 --- a/.github/workflows/build-docker-manual.yml +++ b/.github/workflows/build-docker-manual.yml @@ -15,10 +15,6 @@ on: options: - master - development - - feature/audio-display-with-filter - - python_3_14 - - ai_translate - - no_telemetry default: 'master' build_type: @@ -33,7 +29,7 @@ on: default: 'dev' version_tag: - description: 'Version tag (e.g., v1.5.3) - used for release builds' + description: 'Version tag (e.g., v2.0.0) - used for release builds' required: false default: '' type: string @@ -77,7 +73,7 @@ jobs: ref: ${{ inputs.branch }} - name: Setup NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc" cache: 'npm' @@ -161,7 +157,7 @@ jobs: VERSION=$(git describe --tags --always 2>/dev/null || echo "0.0.0") fi VERSION="${VERSION#v}" # Remove 'v' prefix - FORK_VERSION="${VERSION}+$(date -u +%y%m%d)" + FORK_VERSION="${VERSION}" TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${FORK_VERSION}" TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:v${VERSION}" diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 80c9cbca9f..4522e7f11a 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -3,7 +3,6 @@ name: Build Docker Image on: push: branches: - - main - master paths-ignore: - '*.md' @@ -12,14 +11,7 @@ on: workflow_dispatch: inputs: version_tag: - description: 'Version tag for the image (e.g., v1.5.3)' - required: false - default: '' - type: string - workflow_call: - inputs: - version_tag: - description: 'Version tag for the image' + description: 'Version tag for the image (e.g., v2.0.0)' required: false default: '' type: string @@ -39,7 +31,7 @@ jobs: uses: actions/checkout@v5 - name: Setup NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc" cache: 'npm' @@ -116,19 +108,17 @@ jobs: # Remove 'v' prefix for semver SEMVER="${VERSION#v}" - # Create Bazarr+ version + # Bazarr+ version is the semver tag directly SHORT_SHA=$(git rev-parse --short HEAD) - BUILD_DATE=$(date -u +%y%m%d) - FORK_VERSION="${SEMVER}+${BUILD_DATE}" - + FORK_VERSION="${SEMVER}" + echo "version=$VERSION" >> $GITHUB_OUTPUT echo "semver=$SEMVER" >> $GITHUB_OUTPUT echo "fork_version=$FORK_VERSION" >> $GITHUB_OUTPUT echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT - + echo "## Version Info" >> $GITHUB_STEP_SUMMARY - echo "- **Base Version:** $VERSION" >> $GITHUB_STEP_SUMMARY - echo "- **Fork Version:** $FORK_VERSION" >> $GITHUB_STEP_SUMMARY + echo "- **Version:** $FORK_VERSION" >> $GITHUB_STEP_SUMMARY echo "- **Short SHA:** $SHORT_SHA" >> $GITHUB_STEP_SUMMARY - name: Extract Docker Metadata @@ -139,7 +129,7 @@ jobs: tags: | # Latest tag on main/master branch type=raw,value=latest,enable={{is_default_branch}} - # Version tag (e.g., v1.5.7+250324) + # Version tag (e.g., v2.0.0) type=raw,value=${{ steps.version.outputs.fork_version }} # Short SHA tag type=raw,value=sha-${{ steps.version.outputs.short_sha }} @@ -221,6 +211,5 @@ jobs: This release is based on upstream Bazarr with the following custom modifications: - OpenSubtitles.org web scraper provider (no VIP API needed) - - Manual sync with upstream on major releases See the auto-generated release notes below for detailed changes. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58aa6fa4ce..6699519db9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [master, development] paths: - frontend/** - bazarr/** @@ -14,7 +14,7 @@ on: - dev-requirements.txt - .github/workflows/ci.yml pull_request: - branches: [main] + branches: [master, development] env: ROOT_DIRECTORY: . @@ -77,7 +77,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14' ] + python-version: [ '3.12', '3.13', '3.14' ] name: Python ${{ matrix.python-version }} backend steps: diff --git a/.gitignore b/.gitignore index 5ad12489e5..16e1ffd382 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,6 @@ VERSION !*.dll .claude/ +.superpowers/ docs/superpowers/ .coverage diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca49bf167c..c9d3609e26 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ ### Branch model -- `master` contains stable releases, tagged with `v{upstream}+{YYMMDD}` versions +- `master` contains stable releases, tagged with semver versions (e.g., `v2.0.0`, `v2.1.0`) - `development` is the integration branch where upstream merges and new features land - Feature branches are created from `development` and merged back via PR @@ -20,18 +20,11 @@ - `master` is not merged back to `development` - All feature branches are branched from `development` -- Upstream sync merges go into `development` first, never directly to `master` +- Cherry-picked upstream fixes go into `development` first, never directly to `master` -## Upstream sync +## Upstream relationship -Bazarr+ syncs with [upstream Bazarr](https://github.com/morpheus65535/bazarr) manually after major releases. Upstream merges are always done with `--no-commit --no-ff` and reviewed before committing, to avoid reintroducing removed telemetry, overwriting branding, or conflicting with fork-specific features. - -Files that are always kept as the Bazarr+ version during upstream merges: -- `package_info` -- `Dockerfile`, `docker-compose.yml` -- `README.md` -- Logo and branding assets -- Any telemetry/analytics code (removed in Bazarr+) +Bazarr+ is a hard fork of [upstream Bazarr](https://github.com/morpheus65535/bazarr). There is no automatic synchronization. Bug fixes from upstream may be cherry-picked selectively when relevant, but upstream releases are not merged wholesale. ## Contribution workflow @@ -65,7 +58,7 @@ Fix all errors before submitting. Warnings should be addressed when practical. PRs should include tests when the change is testable. We use: - **Backend:** pytest for Python tests -- **Frontend:** Jest for React component and page tests +- **Frontend:** Vitest for React component and page tests ```bash # Run backend tests @@ -76,7 +69,7 @@ cd frontend npm test # Run a specific test file -npm test -- --testPathPattern=Translator +npm test -- Translator ``` When to include tests: diff --git a/README.md b/README.md index 31404eb434..88b0da9a71 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@

- Bazarr+ Logo + Bazarr+ v2.0.0 - Codename: Eclipse +

+ +

+ Bazarr+ Logo

#

Bazarr+ @@ -15,14 +19,75 @@

- No tracking · Provider priority · OpenSubtitles.org web scraper · AI translation via OpenRouter (300+ LLMs) · API key encryption · batch translation · mass subtitle sync · 11 bulk operations · advanced table filters · security hardening · Python 3.14 · navy + amber dark theme + No tracking · Provider priority · OpenSubtitles.org web scraper · AI translation via OpenRouter (300+ LLMs) · API key encryption · batch translation · mass subtitle sync · 11 bulk operations · subtitle viewer · advanced table filters · security hardening · Python 3.14 · navy + amber dark theme

--- -## Why Bazarr+? +## Switching from upstream Bazarr? + +- Migration can be as simple as replacing the container image with `ghcr.io/lavx/bazarr:latest` and starting the container +- Back up your `/config` directory first +- Bazarr+ uses independent versioning starting at v2.0.0, unrelated to upstream version numbers +- Config changes made by Bazarr+ are not backwards-compatible with upstream Bazarr, so switching back requires restoring your backup +- Recommended: test with a copy of your config before committing to the switch + +--- + +## At a Glance -Bazarr is great at finding subtitles. Bazarr+ takes it further with features upstream doesn't have: +| Feature | Upstream Bazarr | Bazarr+ | +|---------|-----------------|---------| +| **Provider Priority** | [Rejected](https://bazarr.featureupvote.com/suggestions/112323/provider-prioritization) (62 votes) | Dual mode: priority order with early stop, or classic simultaneous | +| **OpenSubtitles.org (Scraper)** | Not available | Self-hosted FastAPI microservice via CloudScraper | +| **AI Subtitle Translator (OpenRouter)** | Not available | 300+ LLMs + any custom model ID | +| **API Key Encryption** | Not available | AES-256-GCM encryption for keys in transit | +| **Translate from Missing Menu** | Not available | Action menu on missing subs with source language picker | +| **Batch Translation** | Not available | Translate entire series/libraries from Wanted pages | +| **Mass Subtitle Sync** | [Rejected](https://bazarr.featureupvote.com/suggestions/172013/mass-sync-all-subtitles) (249 votes) | Bulk sync from Tasks page or Mass Edit, skips already-synced | +| **Bulk Operations** | One-at-a-time only | 11 batch actions: sync, translate, OCR fixes, common fixes, remove HI, remove tags, fix uppercase, reverse RTL, scan disk, search missing, upgrade (up to 10k items) | +| **Dedicated Translator Settings** | Not available | 4-zone page with pricing, cost estimates, status panel | +| **No Tracking** | GA4 + legacy UA phone home to Google | All telemetry removed, nothing phones home | +| **Security Hardening** | MD5, no CSRF/SSRF/rate limiting | PBKDF2 (600k iter), CSRF, SSRF, brute-force, 4 more | +| **Subtitle Viewer** | Not available | Read-only subtitle preview with SRT/VTT/ASS parsing, cue table, and format detection | +| **Audio Language Display** | Not shown in tables | Badges in all table views | +| **Advanced Table Filters** | No filters | Include/exclude audio, missing subtitle, title search | +| **Floating Save + Ctrl+S** | Not available | Sticky save button with 3-option unsaved changes modal | +| **Navy + Amber Theme** | Purple | `#121125` navy to `#fff8e1` cream, amber accents | +| OpenSubtitles.com (API) | Available | Available | +| Docker images | linuxserver.io / hotio | ghcr.io/lavx (self-built, multi-arch) | +| Python runtime | 3.8-3.13 | 3.14 | + +--- + +## Quick Start + +### Option 1: Docker Compose (Recommended) + +```bash +# Clone with the scraper submodule +git clone --recursive https://github.com/LavX/bazarr.git +cd bazarr + +# Configure your media paths in docker-compose.yml, then: +docker compose up -d + +# Access Bazarr at http://localhost:6767 +``` + +### Option 2: Pull Pre-built Images + +```bash +# Pull all images +docker pull ghcr.io/lavx/bazarr:latest +docker pull ghcr.io/lavx/opensubtitles-scraper:latest +docker pull ghcr.io/lavx/ai-subtitle-translator:latest +``` + +--- + +
+Feature Details ### OpenSubtitles.org Web Scraper OpenSubtitles.org shut down their XML-RPC API for all third-party apps, VIP included. Bazarr+ ships a self-hosted FastAPI microservice that scrapes OpenSubtitles.org directly via CloudScraper with optional FlareSolverr fallback. It provides search, subtitle listing, and download endpoints (`/api/v1/search`, `/api/v1/subtitles`, `/api/v1/download/subtitle`) and integrates into Bazarr's provider system through a mixin class. No API key or VIP subscription needed. @@ -33,7 +98,7 @@ Upstream Bazarr queries all subtitle providers simultaneously and picks the high Bazarr+ solves it with a **Provider Priority toggle** in Settings > Providers. When enabled, providers are queried sequentially in the order you've arranged them. If a provider returns subtitles meeting the minimum score, Bazarr+ stops searching and uses those results. Your preferred providers (curated community sites, specialized language sources) always get first shot. When disabled, the original behavior is preserved: all providers queried simultaneously, best score wins. ### AI Subtitle Translation via OpenRouter -Upstream has Google Translate, Gemini, and Lingarr. Bazarr+ adds **OpenRouter** as a fourth translator engine, giving access to 30+ preconfigured LLMs (Claude, Gemini, GPT, LLaMA, Grok, and more) plus any custom model ID from openrouter.ai. It runs as a separate microservice with an async job queue supporting 1-5 concurrent jobs and 1-8 parallel batches. Features include: +Upstream has Google Translate, Gemini, and Lingarr. Bazarr+ adds **OpenRouter** as a fourth translator engine, giving access to 300+ LLMs (Claude, Gemini, GPT, LLaMA, Grok, and more) plus any custom model ID from openrouter.ai. It runs as a separate microservice with an async job queue supporting 1-5 concurrent jobs and 1-8 parallel batches. Features include: - **Translate from the subtitle action menu**: click (...) on a missing subtitle row, pick an existing source subtitle to translate from - **Batch translation** for entire series/movie libraries from the Wanted pages - **Dedicated settings page** with 4 zones: engine picker, connection config, model tuning (temperature, reasoning mode, parallel batches), and a live status panel showing queue stats, job progress, token usage, cost, and speed @@ -41,6 +106,9 @@ Upstream has Google Translate, Gemini, and Lingarr. Bazarr+ adds **OpenRouter** - **AES-256-GCM encryption** for API keys in transit between Bazarr and the translator service, with a Test Connection button that validates encryption and API key status before saving - **Auto disk scan** triggers Sonarr/Radarr to rescan after translation completes +### Subtitle Viewer +Read-only subtitle preview accessible from the subtitle action menu. Supports SRT, VTT, and ASS/SSA formats with automatic format detection. Shows a cue table with timestamps and text, file size, and format badge. Useful for quickly checking subtitle content and timing without downloading. + ### Advanced UI - **Table filters** on Wanted and Library pages: include/exclude audio language (multi-select), missing subtitle language filter, title search, with active filter chips and a collapsible filter panel - **Floating save button** with Ctrl+S/Cmd+S keyboard shortcut, visible only when settings have unsaved changes @@ -68,7 +136,7 @@ Select multiple movies or series from the library pages and apply operations in - **Remove Style Tags** -- remove ``, ``, `` and other formatting tags - **Fix Uppercase** -- convert ALL CAPS subtitles to proper case - **Reverse RTL** -- fix right-to-left punctuation for Arabic, Hebrew, and similar languages -- **Translate** -- batch translate subtitles using any configured translator engine (Google, Gemini, Lingarr, or OpenRouter with 30+ LLMs) +- **Translate** -- batch translate subtitles using any configured translator engine (Google, Gemini, Lingarr, or OpenRouter with 300+ LLMs) **Media operations** (search and scan actions for selected items): - **Scan Disk** -- rescan selected items for on-disk subtitle files @@ -95,56 +163,10 @@ Upstream Bazarr ships two analytics systems that phone home to Google: a GA4 pro ### Python 3.14 Dockerfile uses `python:3.14-slim-bookworm`. Upstream supports Python 3.8-3.13 and relies on third-party Docker images (LinuxServer.io, hotio). Bazarr+ builds and publishes its own multi-arch image to GHCR. ---- - -## 🚀 Quick Start - -### Option 1: Docker Compose (Recommended) - -```bash -# Clone with the scraper submodule -git clone --recursive https://github.com/LavX/bazarr.git -cd bazarr - -# Configure your media paths in docker-compose.yml, then: -docker compose up -d - -# Access Bazarr at http://localhost:6767 -``` - -### Option 2: Pull Pre-built Images - -```bash -# Pull all images -docker pull ghcr.io/lavx/bazarr:latest -docker pull ghcr.io/lavx/opensubtitles-scraper:latest -docker pull ghcr.io/lavx/ai-subtitle-translator:latest -``` - ---- - -## At a Glance +
-| Feature | Upstream Bazarr | Bazarr+ | -|---------|-----------------|---------| -| **Provider Priority** | ❌ [Rejected](https://bazarr.featureupvote.com/suggestions/112323/provider-prioritization) (62 votes) | ✅ Dual mode: priority order with early stop, or classic simultaneous | -| **OpenSubtitles.org (Scraper)** | ❌ Not available | ✅ Self-hosted FastAPI microservice via CloudScraper | -| **AI Subtitle Translator (OpenRouter)** | ❌ Not available | ✅ 30+ preconfigured LLMs + any custom model ID | -| **API Key Encryption** | ❌ Not available | ✅ AES-256-GCM encryption for keys in transit | -| **Translate from Missing Menu** | ❌ Not available | ✅ Action menu on missing subs with source language picker | -| **Batch Translation** | ❌ Not available | ✅ Translate entire series/libraries from Wanted pages | -| **Mass Subtitle Sync** | ❌ [Rejected](https://bazarr.featureupvote.com/suggestions/172013/mass-sync-all-subtitles) (249 votes) | ✅ Bulk sync from Tasks page or Mass Edit, skips already-synced | -| **Bulk Operations** | ❌ One-at-a-time only | ✅ 11 batch actions: sync, translate, OCR fixes, common fixes, remove HI, remove tags, fix uppercase, reverse RTL, scan disk, search missing, upgrade (up to 10k items) | -| **Dedicated Translator Settings** | ❌ Not available | ✅ 4-zone page with pricing, cost estimates, status panel | -| **No Tracking** | GA4 + legacy UA phone home to Google | ✅ All telemetry removed, nothing phones home | -| **Security Hardening** | MD5, no CSRF/SSRF/rate limiting | ✅ PBKDF2 (600k iter), CSRF, SSRF, brute-force, 4 more | -| **Audio Language Display** | ❌ Not shown in tables | ✅ Badges in all table views | -| **Advanced Table Filters** | ❌ No filters | ✅ Include/exclude audio, missing subtitle, title search | -| **Floating Save + Ctrl+S** | ❌ Not available | ✅ Sticky save button with 3-option unsaved changes modal | -| **Navy + Amber Theme** | Purple | ✅ `#121125` navy to `#fff8e1` cream, amber accents | -| OpenSubtitles.com (API) | ✅ Available | ✅ Available | -| Docker images | linuxserver.io / hotio | ghcr.io/lavx (self-built, multi-arch) | -| Python runtime | 3.8-3.13 | 3.14 | +
+Screenshots | Series overview | Series detail with translate menu | |:---:|:---:| @@ -158,9 +180,10 @@ docker pull ghcr.io/lavx/ai-subtitle-translator:latest |:---:| | ![Wanted Filters](/screenshot/wanted-filters.png?raw=true "Wanted page with include/exclude audio and subtitle filters") | ---- +
-## 📦 Installation +
+Installation and Configuration ### Docker Compose Setup @@ -227,8 +250,6 @@ services: - PUID=1000 - PGID=1000 - TZ=Europe/Budapest - # Enable the web scraper mode (auto-enables "Use Web Scraper" in settings) - - OPENSUBTITLES_USE_WEB_SCRAPER=true # Point to the scraper service (port 8000) - OPENSUBTITLES_SCRAPER_URL=http://opensubtitles-scraper:8000 volumes: @@ -249,32 +270,40 @@ docker compose up -d ### Environment Variables -| Variable | Description | Default | -|----------|-------------|---------| -| `PUID` | User ID for file permissions | `1000` | -| `PGID` | Group ID for file permissions | `1000` | -| `TZ` | Timezone | `UTC` | -| `OPENSUBTITLES_USE_WEB_SCRAPER` | Enable web scraper mode | `true` | -| `OPENSUBTITLES_SCRAPER_URL` | URL of the scraper service | `http://localhost:8000` | +| Variable | Default | Description | +|----------|---------|-------------| +| `PUID` | `1000` | User ID for file permissions | +| `PGID` | `1000` | Group ID for file permissions | +| `TZ` | `UTC` | Timezone (e.g., `Europe/Budapest`) | +| `OPENSUBTITLES_SCRAPER_URL` | `http://opensubtitles-scraper:8000` | OpenSubtitles.org scraper service URL (port 8000, not 8765) | + +### Volumes + +| Path | Description | +|------|-------------| +| `/config` | Bazarr configuration and database | +| `/movies` | Movies library (match your Radarr path) | +| `/tv` | TV shows library (match your Sonarr path) | ### Enabling the Provider -1. Go to **Settings** → **Providers** -2. Enable **"OpenSubtitles.org"** (not OpenSubtitles.com - that's the API version) -3. If `OPENSUBTITLES_USE_WEB_SCRAPER=true` is set, "Use Web Scraper" will auto-enable +1. Go to **Settings** > **Providers** +2. Enable **"OpenSubtitles.org"** (not OpenSubtitles.com, that's the API version) +3. Set the scraper service URL (or use the `OPENSUBTITLES_SCRAPER_URL` env var) 4. Save and test with a manual search ### Enabling AI Translation -1. Go to **Settings** → **AI Translator** +1. Go to **Settings** > **AI Translator** 2. Select **"AI Subtitle Translator"** as the translator engine 3. Enter your **OpenRouter API Key** (get one at [openrouter.ai/keys](https://openrouter.ai/keys)) 4. Choose your preferred **AI Model** (Google: Gemini 2.5 Flash Lite Preview 09-2025 recommended) 5. Save and test with a manual translation ---- +
-## 🏗️ Architecture +
+Architecture ``` ┌─────────────────────────────────────────────────────────────────────────────────┐ @@ -301,33 +330,12 @@ docker compose up -d └─────────────────────────────────────────────────────────────────────────────────┘ ``` -> **Note:** The scraper service uses [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) (port 8191) to handle browser challenges. See the Docker Compose example above for the full setup. +> **Note:** The scraper service can optionally use [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) (port 8191) to handle browser challenges. See the Docker Compose example above for the full setup. ---- - -## 🛠️ Configuration Options - -### Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `PUID` | `1000` | User ID for file permissions | -| `PGID` | `1000` | Group ID for file permissions | -| `TZ` | `UTC` | Timezone (e.g., `Europe/Budapest`) | -| `OPENSUBTITLES_USE_WEB_SCRAPER` | `true` | Enable the OpenSubtitles.org web scraper provider | -| `OPENSUBTITLES_SCRAPER_URL` | `http://opensubtitles-scraper:8000` | Scraper service URL (port 8000, not 8765) | - -### Volumes - -| Path | Description | -|------|-------------| -| `/config` | Bazarr configuration and database | -| `/movies` | Movies library (match your Radarr path) | -| `/tv` | TV shows library (match your Sonarr path) | - ---- +
-## 🔧 Troubleshooting +
+Troubleshooting ### Scraper Connection Issues @@ -350,60 +358,13 @@ curl -X POST http://localhost:8000/search \ |-------|----------| | "Connection refused" | Ensure scraper is running and healthy | | "No subtitles found" | Check IMDB ID is correct, try different language | -| Provider not showing | Enable it in Settings → Providers | +| Provider not showing | Enable it in Settings > Providers | | Wrong file permissions | Check PUID/PGID match your user | ---- - -## 📚 Documentation - -- [Fork Maintenance Guide](docs/FORK_MAINTENANCE.md) - How sync works -- [OpenSubtitles Scraper](https://github.com/LavX/opensubtitles-scraper) - Scraper docs -- [AI Subtitle Translator](https://github.com/LavX/ai-subtitle-translator) - AI translator docs -- [Bazarr Wiki](https://wiki.bazarr.media) - General Bazarr documentation - ---- - -## 🤝 Contributing - -Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide. - -1. Fork this repository -2. Create a feature branch from `development` -3. Submit a PR targeting `development` - -For major changes, please open an issue first to discuss. - ---- - -## 🌐 About the Maintainer - -This fork is maintained by **LavX**. Explore more of my projects and services: - -### 🚀 Services -- **[LavX Managed Systems](https://lavx.hu)** – Enterprise AI solutions, RAG systems, and LLMOps. -- **[LavX News](https://news.lavx.hu)** – Latest insights on AI, Open Source, and emerging tech. -- **[LMS Tools](https://tools.lavx.hu)** – 140+ free, privacy-focused online tools for developers and researchers. - -### 🛠️ Open Source Projects -- **[AI Subtitle Translator](https://github.com/LavX/ai-subtitle-translator)** – LLM-powered subtitle translator using OpenRouter API. -- **[OpenSubtitles Scraper](https://github.com/LavX/opensubtitles-scraper)** – Web scraper for OpenSubtitles.org (no VIP required). -- **[JFrog to Nexus OSS](https://github.com/LavX/jfrogtonexusoss)** – Automated migration tool for repository managers. -- **[WeatherFlow](https://github.com/LavX/weatherflow)** – Multi-platform weather data forwarding (WU to Windy/Idokep). -- **[Like4Like Suite](https://github.com/LavX/Like4Like-Suite)** – Social media automation and engagement toolkit. - ---- - -## 📄 License - -- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) -- Original Bazarr: [upstream repository](https://github.com/morpheus65535/bazarr) -- Fork modifications Copyright 2025-2026 LavX - ---- +
-

📜 Supported Subtitle Providers

+Supported Subtitle Providers Includes all upstream providers plus fork additions: @@ -461,3 +422,47 @@ Includes all upstream providers plus fork additions: - Zimuku
+ +
+About the Maintainer + +This fork is maintained by **LavX**. Explore more projects and services: + +### Services +- **[LavX Managed Systems](https://lavx.hu)** -- Enterprise AI solutions, RAG systems, and LLMOps. +- **[LavX News](https://news.lavx.hu)** -- Latest insights on AI, Open Source, and emerging tech. +- **[LMS Tools](https://tools.lavx.hu)** -- 140+ free, privacy-focused online tools for developers and researchers. + +### Open Source Projects +- **[AI Subtitle Translator](https://github.com/LavX/ai-subtitle-translator)** -- LLM-powered subtitle translator using OpenRouter API. +- **[OpenSubtitles Scraper](https://github.com/LavX/opensubtitles-scraper)** -- Web scraper for OpenSubtitles.org (no VIP required). +- **[JFrog to Nexus OSS](https://github.com/LavX/jfrogtonexusoss)** -- Automated migration tool for repository managers. +- **[WeatherFlow](https://github.com/LavX/weatherflow)** -- Multi-platform weather data forwarding (WU to Windy/Idokep). +- **[Like4Like Suite](https://github.com/LavX/Like4Like-Suite)** -- Social media automation and engagement toolkit. + +
+ +--- + +## Documentation + +- [Fork Maintenance Guide](docs/FORK_MAINTENANCE.md) - How sync works +- [OpenSubtitles Scraper](https://github.com/LavX/opensubtitles-scraper) - Scraper docs +- [AI Subtitle Translator](https://github.com/LavX/ai-subtitle-translator) - AI translator docs +- [Bazarr Wiki](https://wiki.bazarr.media) - General Bazarr documentation + +## Contributing + +Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide. + +1. Fork this repository +2. Create a feature branch from `development` +3. Submit a PR targeting `development` + +For major changes, please open an issue first to discuss. + +## License + +- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) +- Based on [Bazarr](https://github.com/morpheus65535/bazarr) by morpheus65535 +- Fork modifications Copyright 2025-2026 LavX diff --git a/bazarr/api/subtitles/content.py b/bazarr/api/subtitles/content.py index 28eac57919..e3afdc8800 100644 --- a/bazarr/api/subtitles/content.py +++ b/bazarr/api/subtitles/content.py @@ -7,7 +7,7 @@ from flask import make_response, jsonify, request from flask_restx import Resource, Namespace -from app.database import TableEpisodes, TableMovies, database, select +from app.database import TableEpisodes, TableMovies, TableShows, database, select from utilities.path_mappings import path_mappings from ..utils import authenticate @@ -41,18 +41,33 @@ def resolve_subtitle_path(media_type, media_id, language_code): language_code can be like "en", "hu", "en:hi", "en:forced". Matches against the language field in the subtitles array. - Returns (path, language) on success, or (message, status_code) on failure. + Returns (path, language, metadata) on success, or (message, status_code) on failure. """ + metadata = {} if media_type == 'episode': row = database.execute( - select(TableEpisodes.subtitles, TableEpisodes.path) + select(TableEpisodes.subtitles, TableEpisodes.path, TableEpisodes.sonarrSeriesId, TableEpisodes.title) .where(TableEpisodes.sonarrEpisodeId == media_id) ).first() + if row: + series_row = database.execute( + select(TableShows.title).where(TableShows.sonarrSeriesId == row.sonarrSeriesId) + ).first() + metadata = { + 'mediaTitle': series_row.title if series_row else None, + 'mediaId': row.sonarrSeriesId, + 'episodeTitle': row.title, + } elif media_type == 'movie': row = database.execute( - select(TableMovies.subtitles, TableMovies.path) + select(TableMovies.subtitles, TableMovies.path, TableMovies.title, TableMovies.radarrId) .where(TableMovies.radarrId == media_id) ).first() + if row: + metadata = { + 'mediaTitle': row.title, + 'mediaId': row.radarrId, + } else: return 'Invalid media type', 400 @@ -98,7 +113,7 @@ def resolve_subtitle_path(media_type, media_id, language_code): if not os.path.isfile(subtitle_path): return 'Subtitle file not found on disk', 404 - return subtitle_path, language + return subtitle_path, language, metadata def read_subtitle_file(path): @@ -162,7 +177,7 @@ def _get_subtitle_content(media_type, media_id, language_code): if isinstance(result[1], int): return result[0], result[1] - subtitle_path, language = result + subtitle_path, language, metadata = result etag = generate_etag(subtitle_path) @@ -179,14 +194,17 @@ def _get_subtitle_content(media_type, media_id, language_code): stat = os.stat(subtitle_path) fmt = detect_subtitle_format(subtitle_path) - response = make_response(jsonify({ + response_data = { 'content': content, 'encoding': encoding, 'format': fmt, 'language': language, 'size': stat.st_size, 'lastModified': stat.st_mtime, - })) + } + response_data.update(metadata) + + response = make_response(jsonify(response_data)) response.headers['ETag'] = f'"{etag}"' response.headers['X-Content-Type-Options'] = 'nosniff' diff --git a/bazarr/app/check_update.py b/bazarr/app/check_update.py index 2818aaa958..b7297e1f3d 100644 --- a/bazarr/app/check_update.py +++ b/bazarr/app/check_update.py @@ -49,7 +49,12 @@ def _fetch_repo_releases(repo, label=None): except requests.exceptions.RequestException: logging.exception(f"Error trying to get releases from Github ({repo}).") else: - for release in r.json(): + try: + releases_data = r.json() + except ValueError: + logging.error(f"Error parsing JSON from Github releases response ({repo}). Skipping.") + return releases + for release in releases_data: download_link = None for asset in release.get('assets', []): download_link = asset['browser_download_url'] @@ -88,6 +93,13 @@ def check_releases(job_id=None, startup=False): def check_if_new_update(): + # Skip auto-update when running from source (no BAZARR_VERSION set) + bazarr_version = os.environ.get("BAZARR_VERSION", "") + if not bazarr_version: + logging.debug('BAZARR running from source, skipping auto-update') + check_releases(startup=True) + return + if settings.general.branch == 'master': use_prerelease = False elif settings.general.branch == 'development': @@ -151,6 +163,9 @@ def check_if_new_update(): def download_release(url): + if not url: + logging.debug('BAZARR release has no download URL, skipping update') + return r = None update_dir = os.path.join(args.config_dir, 'update') try: @@ -159,7 +174,7 @@ def download_release(url): logging.debug(f'BAZARR unable to create update directory {update_dir}') else: logging.debug(f'BAZARR downloading release from Github: {url}') - r = requests.get(url, allow_redirects=True) + r = requests.get(url, allow_redirects=True, timeout=300) if r: try: with open(os.path.join(update_dir, 'bazarr.zip'), 'wb') as f: diff --git a/bazarr/app/config.py b/bazarr/app/config.py index 69231fb679..2eec84bab0 100644 --- a/bazarr/app/config.py +++ b/bazarr/app/config.py @@ -89,7 +89,7 @@ def check_parser_binary(value): Validator('general.port', must_exist=True, default=6767, is_type_of=int, gte=1, lte=65535), Validator('general.hostname', must_exist=True, default=platform.node(), is_type_of=str), Validator('general.base_url', must_exist=True, default='', is_type_of=str), - Validator('general.instance_name', must_exist=True, default='Bazarr', is_type_of=str, + Validator('general.instance_name', must_exist=True, default='Bazarr+', is_type_of=str, apply_default_on_none=True), Validator('general.path_mappings', must_exist=True, default=[], is_type_of=list), Validator('general.debug', must_exist=True, default=False, is_type_of=bool), @@ -311,9 +311,9 @@ def check_parser_binary(value): Validator('opensubtitles.ssl', must_exist=True, default=False, is_type_of=bool), Validator('opensubtitles.timeout', must_exist=True, default=15, is_type_of=int, gte=1), Validator('opensubtitles.skip_wrong_fps', must_exist=True, default=False, is_type_of=bool), - # Web scraper mode - enabled by default, can be disabled via OPENSUBTITLES_USE_WEB_SCRAPER=false + # Web scraper mode - always enabled (OpenSubtitles.org login no longer available) Validator('opensubtitles.use_web_scraper', must_exist=True, - default=os.environ.get('OPENSUBTITLES_USE_WEB_SCRAPER', 'true').lower() in ('true', '1', 'yes'), + default=True, is_type_of=bool), # Scraper URL - can be set via OPENSUBTITLES_SCRAPER_URL environment variable Validator('opensubtitles.scraper_service_url', must_exist=True, diff --git a/bazarr/subtitles/mass_operations.py b/bazarr/subtitles/mass_operations.py index e017ae508a..3ec79beafe 100644 --- a/bazarr/subtitles/mass_operations.py +++ b/bazarr/subtitles/mass_operations.py @@ -456,9 +456,11 @@ def mass_batch_operation(items=None, action='sync', options=None, job_id=None): # downstream functions like sync_subtitles run inline instead of re-queuing # themselves as individual jobs. if not job_id: - label = action.replace('_', ' ').replace('-', ' ').title() - scope = 'Library' if items is None else f'{len(items)} items' - jobs_queue.add_job_from_function(f"Mass {label} ({scope})", is_progress=True) + jobs_queue.add_job_from_function( + f"Mass {action.replace('_', ' ').replace('-', ' ').title()} " + f"({'Library' if items is None else f'{len(items)} items'})", + is_progress=True, + ) return # Media actions (scan-disk, search-missing) work on media items directly diff --git a/docs/AI_TRANSLATOR_INVESTIGATION.md b/docs/AI_TRANSLATOR_INVESTIGATION.md deleted file mode 100644 index ab2ede82aa..0000000000 --- a/docs/AI_TRANSLATOR_INVESTIGATION.md +++ /dev/null @@ -1,248 +0,0 @@ -# AI Subtitle Translator Integration - Investigation Report - -## Executive Summary - -This document details the findings from investigating two issues with the AI Subtitle Translator integration in Bazarr: -1. Configuration not being applied to the microservice -2. Understanding how AI translation gets triggered - ---- - -## Issue 1: Configuration Not Being Applied to Microservice - -### Symptoms -- User enters API key and Max Concurrent Jobs in Settings -- Service Status panel shows "API Key: × Not Set" and "Max Concurrent: 2" (default) - -### Root Cause Analysis - -The architecture involves **two separate systems**: - -``` -┌─────────────────────────────────────┐ ┌──────────────────────────────────┐ -│ BAZARR │ │ AI SUBTITLE TRANSLATOR │ -│ │ │ (Microservice) │ -│ ┌─────────────────────────────┐ │ │ │ -│ │ Frontend Settings Page │ │ │ ┌────────────────────────────┐ │ -│ │ - API Key input │ │ │ │ /api/v1/status │ │ -│ │ - Max Concurrent selector │────┼─────│ │ Returns microservice's │ │ -│ │ - Model selector │ │ │ │ internal config (NOT │ │ -│ └─────────────────────────────┘ │ │ │ Bazarr's config) │ │ -│ │ │ │ └────────────────────────────┘ │ -│ ▼ │ │ │ -│ ┌─────────────────────────────┐ │ │ ┌────────────────────────────┐ │ -│ │ Bazarr Config (config.yaml)│ │ │ │ /api/v1/jobs/translate │ │ -│ │ - openrouter_api_key │ │ │ │ Receives config PER- │ │ -│ │ - openrouter_max_concurrent│ │ │ │ REQUEST in payload │ │ -│ │ - openrouter_model │ │ │ └────────────────────────────┘ │ -│ └─────────────────────────────┘ │ │ │ -└─────────────────────────────────────┘ └──────────────────────────────────┘ -``` - -#### Data Flow When Saving Settings: -1. User enters values in frontend Settings page -2. Frontend POSTs to `/api/system/settings` -3. Bazarr saves to its config file (`config.yaml`) -4. **Config is NOT sent to microservice** - settings are saved locally only - -#### Data Flow When Translating: -1. Translation request is submitted -2. [`OpenRouterTranslatorService._submit_and_poll()`](bazarr/subtitles/tools/translate/services/openrouter_translator.py:110) creates payload WITH config: -```python -payload = { - ... - "config": { - "apiKey": settings.translator.openrouter_api_key, - "model": settings.translator.openrouter_model, - "temperature": settings.translator.openrouter_temperature, - } -} -``` -3. **Problem**: `maxConcurrent` is NOT included in the payload! - -#### Data Flow When Viewing Status Panel: -1. [`TranslatorStatusPanel`](frontend/src/components/TranslatorStatus.tsx:146) calls `useTranslatorStatus()` -2. Fetches from Bazarr API `/translator/status` -3. [`TranslatorStatus.get()`](bazarr/api/translator/translator.py:29) proxies to microservice `/api/v1/status` -4. **Returns microservice's internal config** (defaults), not Bazarr's saved config - -### The Design Issue - -The current design sends config **per-request** (at translation time), but: -1. **`max_concurrent` is missing** from the translation payload -2. **Status panel shows wrong data** - displays microservice defaults, not Bazarr's settings - -### Solution - -#### Fix 1: Add `maxConcurrent` to translation payload - -In [`bazarr/subtitles/tools/translate/services/openrouter_translator.py`](bazarr/subtitles/tools/translate/services/openrouter_translator.py:137), add `maxConcurrent`: - -```python -# Line 136-142 -payload = { - ... - "config": { - "apiKey": settings.translator.openrouter_api_key, - "model": settings.translator.openrouter_model, - "temperature": settings.translator.openrouter_temperature, - "maxConcurrent": settings.translator.openrouter_max_concurrent, # ADD THIS - } -} -``` - -#### Fix 2: Update Status Panel to show Bazarr's config - -Option A: **Modify Status Panel to show Bazarr settings** (Recommended) -- Update frontend to fetch Bazarr's settings from `/api/system/settings` -- Display Bazarr's config alongside microservice status - -Option B: **Add endpoint to sync config to microservice** -- Add a POST method to [`TranslatorConfig`](bazarr/api/translator/translator.py:128) -- Call it when settings are saved -- Microservice would need a `/api/v1/config` POST endpoint - ---- - -## Issue 2: How AI Translation Gets Triggered - -### Current Behavior: Manual Only - -AI translation is **NOT automatic**. It must be manually triggered by the user. - -### Translation Trigger Flow - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ MANUAL TRANSLATION FLOW │ -└────────────────────────────────────────────────────────────────────────────┘ - -User Action in Bazarr UI: -┌─────────────────────────────────┐ -│ Episode/Movie Detail Page │ -│ Click on subtitle file │ -│ Select "Translate" action │ -│ Choose target language │ -└─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────┐ -│ Frontend API Call │ -│ PATCH /api/subtitles/subtitles │ -│ action: "translate" │ -│ language: "target_lang" │ -│ path: "/path/to/subtitle.srt" │ -└─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────┐ -│ bazarr/api/subtitles/ │ -│ subtitles.py:170-181 │ -│ if action == 'translate': │ -│ translate_subtitles_file() │ -└─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────┐ -│ bazarr/subtitles/tools/ │ -│ translate/main.py:12 │ -│ translate_subtitles_file() │ -└─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────┐ -│ TranslatorFactory │ -│ .create_translator() │ -│ Based on translator_type: │ -│ - google_translate │ -│ - gemini │ -│ - lingarr │ -│ - openrouter (AI Translator) │ -└─────────────────────────────────┘ - │ - ▼ (if openrouter) -┌─────────────────────────────────┐ -│ OpenRouterTranslatorService │ -│ 1. Load subtitle file │ -│ 2. Submit job to microservice │ -│ 3. Poll for completion │ -│ 4. Save translated subtitle │ -│ 5. Log to history │ -└─────────────────────────────────┘ -``` - -### How to Trigger Translation - -#### Method 1: Via Bazarr UI (Recommended) -1. Go to Series/Movies in Bazarr -2. Click on an episode/movie -3. Look at the subtitles section -4. Click on an existing subtitle file (e.g., English) -5. Select "Translate" from the actions menu -6. Choose the target language -7. Click confirm - -#### Method 2: Via API -```bash -curl -X PATCH "http://localhost:6767/api/subtitles/subtitles" \ - -H "X-API-KEY: your-api-key" \ - -F "action=translate" \ - -F "language=hu" \ - -F "path=/path/to/subtitle.en.srt" \ - -F "type=episode" \ - -F "id=12345" -``` - -### Automatic Translation (NOT Currently Implemented) - -There is currently **no automatic translation** in Bazarr. To implement this, you would need to: - -1. **Post-download hook**: Add translation to the subtitle download pipeline -2. **Scheduled task**: Create a scheduler job to translate missing languages -3. **Language profile enhancement**: Add "auto-translate" option to language profiles - ---- - -## Relevant Files Reference - -### Configuration -- [`bazarr/app/config.py:186-197`](bazarr/app/config.py:186) - Translator settings validators - -### Backend API -- [`bazarr/api/translator/translator.py`](bazarr/api/translator/translator.py) - Translator API endpoints (status, jobs, config) -- [`bazarr/api/subtitles/subtitles.py:170-181`](bazarr/api/subtitles/subtitles.py:170) - Translate action handler -- [`bazarr/api/system/settings.py`](bazarr/api/system/settings.py) - Settings save endpoint - -### Translation Services -- [`bazarr/subtitles/tools/translate/main.py`](bazarr/subtitles/tools/translate/main.py) - Main translation entry point -- [`bazarr/subtitles/tools/translate/services/translator_factory.py`](bazarr/subtitles/tools/translate/services/translator_factory.py) - Translator factory -- [`bazarr/subtitles/tools/translate/services/openrouter_translator.py`](bazarr/subtitles/tools/translate/services/openrouter_translator.py) - AI Subtitle Translator service - -### Frontend -- [`frontend/src/pages/Settings/Subtitles/index.tsx:534-655`](frontend/src/pages/Settings/Subtitles/index.tsx:534) - Translator settings UI -- [`frontend/src/components/TranslatorStatus.tsx`](frontend/src/components/TranslatorStatus.tsx) - Status panel component -- [`frontend/src/apis/hooks/translator.ts`](frontend/src/apis/hooks/translator.ts) - API hooks for translator - ---- - -## Recommended Fixes Summary - -### Immediate Fixes - -1. **Add `maxConcurrent` to translation payload** (1 line change) - - File: `bazarr/subtitles/tools/translate/services/openrouter_translator.py` - - Add: `"maxConcurrent": settings.translator.openrouter_max_concurrent` - -2. **Update Status Panel display** - - Show Bazarr's configured values, with clarification label - - Alternative: Sync config to microservice on save - -### Future Enhancements - -1. **Add automatic translation option** - - Post-download hook for newly downloaded subtitles - - Option in language profiles - -2. **Real-time config sync** - - Push config changes to microservice immediately - - Keep microservice in sync with Bazarr settings \ No newline at end of file diff --git a/docs/FORK_MAINTENANCE.md b/docs/FORK_MAINTENANCE.md index 30f0d79ab9..3f22a05033 100644 --- a/docs/FORK_MAINTENANCE.md +++ b/docs/FORK_MAINTENANCE.md @@ -1,271 +1,88 @@ # Bazarr+ Maintenance Guide -This document describes the workflow for maintaining Bazarr+, a fork of [Bazarr](https://github.com/morpheus65535/bazarr) (upstream). +Bazarr+ is a hard fork of [Bazarr](https://github.com/morpheus65535/bazarr). It shares the original codebase as a starting point but is developed independently. There is no automatic upstream synchronization. -## Overview +## Relationship with Upstream -Bazarr+ contains custom modifications (OpenSubtitles.org web scraper, AI subtitle translator, security hardening, UI enhancements) that are manually kept in sync with upstream releases. The workflow handles: +Bazarr+ may selectively cherry-pick bug fixes from upstream when relevant, but does not merge upstream releases wholesale. The codebases have diverged significantly in security model, UI, features, and architecture. -1. **Upstream Synchronization** - Manual merge after upstream major releases -2. **Conflict Resolution** - Code review every merge to preserve fork customizations -3. **Docker Builds** - Automated multi-architecture Docker image builds -4. **Publishing** - Images published to GitHub Container Registry +**When cherry-picking from upstream:** +1. Evaluate whether the fix applies to Bazarr+ (upstream may fix things we've already addressed differently) +2. Cherry-pick individual commits: `git cherry-pick ` +3. Review for conflicts with fork-specific code (security hardening, telemetry removal, UI changes) +4. Test thoroughly before merging -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ GitHub Actions Workflows │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────────────────────┐ │ -│ │ sync-upstream.yml │ │ build-docker.yml │ │ -│ │ │ │ │ │ -│ │ • Daily 4 AM UTC │────────>│ • Build frontend │ │ -│ │ • Fetch upstream │ trigger │ • Build Docker image │ │ -│ │ • Auto merge │ │ • Push to ghcr.io │ │ -│ │ • Conflict PR │ │ • multi-arch (amd64/arm64) │ │ -│ └─────────────────────┘ └─────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` +## Versioning -## Workflows +Bazarr+ uses independent semantic versioning, starting at v2.0.0. Versions are not tied to upstream Bazarr release numbers. -### 1. Upstream Sync (Manual) +``` +v{major}.{minor}.{patch} -**When:** After upstream major releases (e.g. v1.5.7, v1.5.8) +Example: v2.0.0, v2.1.0, v2.0.1 +``` -**Process:** -1. Fetch upstream: `git fetch upstream` -2. Merge into development with review: `git merge upstream/master --no-commit --no-ff` -3. Review all changes: `git diff --cached` -4. Restore fork-specific files: `git checkout HEAD -- package_info` (and other protected files) -5. Commit, test, then merge to master and tag +- Patch (v2.0.1): bug fixes +- Minor (v2.1.0): new features, backwards-compatible +- Major (v3.0.0): breaking changes -### 2. Docker Build (`build-docker.yml`) +## Docker Build (`build-docker.yml`) **Triggers:** -- Push to `main` branch -- Called by sync workflow after successful merge +- Push to `master` branch - Manual dispatch - Tag creation (for releases) **Output:** - `ghcr.io/lavx/bazarr:latest` - Latest build -- `ghcr.io/lavx/bazarr:vX.Y.Z+YYMMDD` - Versioned build +- `ghcr.io/lavx/bazarr:X.Y.Z` - Versioned build - `ghcr.io/lavx/bazarr:sha-XXXXXXX` - Git SHA reference -## Using the Docker Image - -### Quick Start - -```bash -docker run -d \ - --name bazarr \ - -p 6767:6767 \ - -v /path/to/config:/config \ - -v /path/to/movies:/movies \ - -v /path/to/tv:/tv \ - -e PUID=1000 \ - -e PGID=1000 \ - -e TZ=Europe/Budapest \ - ghcr.io/lavx/bazarr:latest -``` - -### Docker Compose - -Create a `docker-compose.yml`: - -```yaml -services: - bazarr: - image: ghcr.io/lavx/bazarr:latest - container_name: bazarr - restart: unless-stopped - ports: - - "6767:6767" - environment: - - PUID=1000 - - PGID=1000 - - TZ=Europe/Budapest - volumes: - - ./config:/config - - /path/to/movies:/movies - - /path/to/tv:/tv -``` - -Then run: -```bash -docker compose up -d -``` - -### Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `PUID` | `1000` | User ID for file permissions | -| `PGID` | `1000` | Group ID for file permissions | -| `TZ` | `UTC` | Timezone (e.g., `Europe/Budapest`) | - -## Handling Merge Conflicts - -When upstream changes conflict with fork modifications: +## Branch Model -1. **Notification:** A PR is automatically created with the `sync-conflict` label -2. **Review:** Check the PR for conflict markers -3. **Fix locally:** - ```bash - git fetch origin sync/upstream-XXXXXXXX - git checkout sync/upstream-XXXXXXXX - # Resolve conflicts in your editor - git add . - git commit -m "Resolve merge conflicts" - git push origin sync/upstream-XXXXXXXX - ``` -4. **Merge:** Merge the PR via GitHub UI -5. **Build:** Docker build will trigger automatically +- `master` contains stable releases +- `development` is the integration branch where new features land +- Feature branches are created from `development` and merged back via PR ## Fork-Specific Files -These files are unique to this fork and should be preserved during merges: +Key files that define Bazarr+ and differentiate it from upstream: | File | Purpose | |------|---------| | `custom_libs/subliminal_patch/providers/opensubtitles_scraper.py` | OpenSubtitles.org scraper mixin | | `custom_libs/subliminal_patch/providers/opensubtitles.py` | Modified provider with scraper support | -| `opensubtitles-scraper/` | Git submodule - web scraper service | +| `opensubtitles-scraper/` | Git submodule: web scraper service | +| `ai-subtitle-translator/` | Git submodule: AI translator service | | `package_info` | Fork identification (shown in System Status) | -| `bazarr/app/check_update.py` | Modified to use fork's releases | -| `.github/workflows/sync-upstream.yml` | Upstream sync workflow | +| `bazarr/app/check_update.py` | Uses fork's releases, not upstream | | `.github/workflows/build-docker.yml` | Docker build workflow | -| `Dockerfile` | Production Docker image | +| `Dockerfile` | Production Docker image (Python 3.14) | | `docker-compose.yml` | User deployment template | -| `.gitattributes` | Merge conflict protection rules | - -## OpenSubtitles Scraper Service - -This fork includes the [OpenSubtitles Scraper](https://github.com/LavX/opensubtitles-scraper) as a git submodule. The scraper is a standalone service that provides web scraping capabilities for OpenSubtitles.org. - -### Architecture - -``` -┌────────────────────┐ HTTP API ┌─────────────────────────┐ -│ Bazarr │ ───────────────> │ OpenSubtitles Scraper │ -│ (Bazarr+) │ │ (Port 8765) │ -│ │ <─────────────── │ │ -│ Uses provider: │ JSON Response │ Scrapes: │ -│ opensubtitles.org │ │ - opensubtitles.org │ -└────────────────────┘ └─────────────────────────┘ -``` +| `bazarr/utilities/analytics.py` | Deleted (contained GA4 + UA telemetry) | -### Docker Compose Deployment - -The `docker-compose.yml` includes both services: - -```yaml -services: - opensubtitles-scraper: - image: ghcr.io/lavx/opensubtitles-scraper:latest - ports: - - "8765:8765" - - bazarr: - image: ghcr.io/lavx/bazarr:latest - depends_on: - - opensubtitles-scraper - environment: - - OPENSUBTITLES_SCRAPER_URL=http://opensubtitles-scraper:8765 -``` +## Auto-Update Behavior -### Updating the Scraper Submodule +### Docker: Auto-Update is Disabled -To update the scraper to the latest version: +The Docker image runs with `--no-update` flag. Update by pulling new images: ```bash -cd opensubtitles-scraper -git pull origin main -cd .. -git add opensubtitles-scraper -git commit -m "Update opensubtitles-scraper submodule" -git push -``` - -## Versioning - -Bazarr+ uses a versioning scheme that combines the upstream version with a date-based suffix: - -``` -v{upstream_version}+{YYMMDD} - -Example: v1.5.7+250324 +docker compose pull +docker compose up -d ``` -- The upstream version indicates which Bazarr release the build is based on -- The date suffix (YYMMDD) indicates when this Bazarr+ release was built -- Hotfixes on the same day append a dot counter: `v1.5.7+250324.1` - -## Auto-Update Behavior - -### Important: Auto-Update is Disabled in Docker +### Release Repository -The Docker image runs with `--no-update` flag to prevent Bazarr's built-in update mechanism from overwriting your fork modifications. **This is intentional.** - -### How Updates Work - -| Scenario | Behavior | -|----------|----------| -| **Docker Container** | Auto-update disabled; use new Docker image for updates | -| **Manual Installation** | Auto-update can be enabled, but will pull from this fork | -| **Release Info in UI** | Shows releases from this fork (LavX/bazarr) | - -### Release Repository Configuration - -The fork is configured to check releases from `LavX/bazarr` instead of upstream. This is controlled by: +The fork checks releases from `LavX/bazarr`: ```python # In bazarr/app/check_update.py RELEASES_REPO = os.environ.get('BAZARR_RELEASES_REPO', 'LavX/bazarr') ``` -To change the release source (e.g., for debugging), set the environment variable: - -```yaml -# docker-compose.yml -environment: - - BAZARR_RELEASES_REPO=morpheus65535/Bazarr # Use upstream releases instead -``` - -### Updating Docker Containers - -To update to a new version: - -```bash -# Pull the latest image -docker compose pull - -# Recreate the container -docker compose up -d - -# Or in one command -docker compose up -d --pull always -``` - -### Why Docker Doesn't Auto-Update - -1. **Preservation of modifications**: Auto-update would download vanilla Bazarr, losing the OpenSubtitles scraper -2. **Immutable containers**: Docker best practices recommend replacing containers rather than modifying them -3. **Reproducibility**: Pinned versions ensure consistent behavior -4. **Rollback capability**: Easy to rollback by pulling a specific tag - ## Troubleshooting -### Sync Workflow Fails - -1. Check the workflow logs in GitHub Actions -2. Verify upstream repository is accessible -3. Check if there are unresolved conflicts from previous sync - ### Docker Build Fails 1. Check if frontend build succeeded @@ -282,17 +99,8 @@ echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin docker pull ghcr.io/lavx/bazarr:latest ``` -## Contributing - -See [CONTRIBUTING.md](../CONTRIBUTING.md) for the full guide. Key points for fork-specific files: - -1. Test changes locally first -2. Ensure changes don't conflict with upstream structure -3. Document any new environment variables or features -4. Update this documentation if workflow changes - ## Related Links -- [Upstream Bazarr Repository](https://github.com/morpheus65535/bazarr) +- [Upstream Bazarr Repository](https://github.com/morpheus65535/bazarr) (original, not synced) - [GitHub Container Registry](https://ghcr.io/lavx/bazarr) -- [Bazarr Wiki](https://wiki.bazarr.media) \ No newline at end of file +- [Contributing Guide](../CONTRIBUTING.md) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ba661d98c4..1f20a331b2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "GPL-3", "dependencies": { + "@fontsource-variable/geist": "^5.2.8", "@mantine/core": "^8.3.9", "@mantine/dropzone": "^8.3.9", "@mantine/form": "^8.3.9", @@ -27,7 +28,6 @@ "socket.io-client": "^4.7.5" }, "devDependencies": { - "@fontsource/roboto": "^5.0.12", "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.1.0", @@ -1854,9 +1854,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "dev": true, "license": "MIT", "optional": true, @@ -2425,11 +2425,14 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, - "node_modules/@fontsource/roboto": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.12.tgz", - "integrity": "sha512-x0o17jvgoSSbS9OZnUX2+xJmVRvVCfeaYJjkS7w62iN7CuJWtMf5vJj8LqgC7ibqIkitOHVW+XssRjgrcHn62g==", - "dev": true + "node_modules/@fontsource-variable/geist": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.2.8.tgz", + "integrity": "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "7.1.0", @@ -6381,9 +6384,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { diff --git a/frontend/package.json b/frontend/package.json index 68b0d3a75a..c61cf51d6e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ }, "private": true, "dependencies": { + "@fontsource-variable/geist": "^5.2.8", "@mantine/core": "^8.3.9", "@mantine/dropzone": "^8.3.9", "@mantine/form": "^8.3.9", @@ -31,7 +32,6 @@ "socket.io-client": "^4.7.5" }, "devDependencies": { - "@fontsource/roboto": "^5.0.12", "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.1.0", diff --git a/frontend/public/images/android-chrome-96x96.png b/frontend/public/images/android-chrome-96x96.png index 7065ec89ba..53ca9176d2 100644 Binary files a/frontend/public/images/android-chrome-96x96.png and b/frontend/public/images/android-chrome-96x96.png differ diff --git a/frontend/public/images/apple-touch-icon-180x180.png b/frontend/public/images/apple-touch-icon-180x180.png index e8b67b6096..5054fb650e 100644 Binary files a/frontend/public/images/apple-touch-icon-180x180.png and b/frontend/public/images/apple-touch-icon-180x180.png differ diff --git a/frontend/public/images/favicon-16x16.png b/frontend/public/images/favicon-16x16.png index 80492b5e4a..f15db17621 100644 Binary files a/frontend/public/images/favicon-16x16.png and b/frontend/public/images/favicon-16x16.png differ diff --git a/frontend/public/images/favicon-32x32.png b/frontend/public/images/favicon-32x32.png index 8428460be6..d4f4f07a67 100644 Binary files a/frontend/public/images/favicon-32x32.png and b/frontend/public/images/favicon-32x32.png differ diff --git a/frontend/public/images/favicon.ico b/frontend/public/images/favicon.ico index 274a04472a..149200567b 100644 Binary files a/frontend/public/images/favicon.ico and b/frontend/public/images/favicon.ico differ diff --git a/frontend/public/images/logo.png b/frontend/public/images/logo.png index afa8155493..d0d9c2d764 100644 Binary files a/frontend/public/images/logo.png and b/frontend/public/images/logo.png differ diff --git a/frontend/public/images/logo.svg b/frontend/public/images/logo.svg index e1fe02ff54..49a19b755d 100644 --- a/frontend/public/images/logo.svg +++ b/frontend/public/images/logo.svg @@ -1,36 +1,55 @@ - + - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/images/logo128.png b/frontend/public/images/logo128.png index 760c46fc19..4f8fe42bdc 100644 Binary files a/frontend/public/images/logo128.png and b/frontend/public/images/logo128.png differ diff --git a/frontend/public/images/logo64.png b/frontend/public/images/logo64.png index 7444a70447..03e7685768 100644 Binary files a/frontend/public/images/logo64.png and b/frontend/public/images/logo64.png differ diff --git a/frontend/public/images/logo_minimal_b.png b/frontend/public/images/logo_minimal_b.png index dcaa8913ad..585df9c81c 100644 Binary files a/frontend/public/images/logo_minimal_b.png and b/frontend/public/images/logo_minimal_b.png differ diff --git a/frontend/public/images/logo_minimal_b.svg b/frontend/public/images/logo_minimal_b.svg index a37d848cbd..2651b2727d 100644 --- a/frontend/public/images/logo_minimal_b.svg +++ b/frontend/public/images/logo_minimal_b.svg @@ -1,11 +1,6 @@ - - - - - - - - - + + + + \ No newline at end of file diff --git a/frontend/public/images/logo_minimal_b128.png b/frontend/public/images/logo_minimal_b128.png index 65842f7fa6..8718706cba 100644 Binary files a/frontend/public/images/logo_minimal_b128.png and b/frontend/public/images/logo_minimal_b128.png differ diff --git a/frontend/public/images/logo_minimal_b64.png b/frontend/public/images/logo_minimal_b64.png index 9575572996..16c976043a 100644 Binary files a/frontend/public/images/logo_minimal_b64.png and b/frontend/public/images/logo_minimal_b64.png differ diff --git a/frontend/public/images/logo_minimal_w.png b/frontend/public/images/logo_minimal_w.png index 515bf14851..db081b03cc 100644 Binary files a/frontend/public/images/logo_minimal_w.png and b/frontend/public/images/logo_minimal_w.png differ diff --git a/frontend/public/images/logo_minimal_w.svg b/frontend/public/images/logo_minimal_w.svg index ec2fecbbd4..99436cb939 100644 --- a/frontend/public/images/logo_minimal_w.svg +++ b/frontend/public/images/logo_minimal_w.svg @@ -1,11 +1,6 @@ - - - - - - - - - + + + + \ No newline at end of file diff --git a/frontend/public/images/logo_minimal_w128.png b/frontend/public/images/logo_minimal_w128.png index 2d19d633e4..951f259f23 100644 Binary files a/frontend/public/images/logo_minimal_w128.png and b/frontend/public/images/logo_minimal_w128.png differ diff --git a/frontend/public/images/logo_minimal_w64.png b/frontend/public/images/logo_minimal_w64.png index f15d2eecc5..675b926be6 100644 Binary files a/frontend/public/images/logo_minimal_w64.png and b/frontend/public/images/logo_minimal_w64.png differ diff --git a/frontend/public/images/logo_no_orb.png b/frontend/public/images/logo_no_orb.png index a7a194d4b9..c89c016e68 100644 Binary files a/frontend/public/images/logo_no_orb.png and b/frontend/public/images/logo_no_orb.png differ diff --git a/frontend/public/images/logo_no_orb.svg b/frontend/public/images/logo_no_orb.svg index db1cac613a..01ae43816d 100644 --- a/frontend/public/images/logo_no_orb.svg +++ b/frontend/public/images/logo_no_orb.svg @@ -1,29 +1,13 @@ - + - - - + + + + - - - - - - - - - - - - - - - - - - - - + + + \ No newline at end of file diff --git a/frontend/public/images/logo_no_orb128.png b/frontend/public/images/logo_no_orb128.png index b0ce577015..e0cfc893dd 100644 Binary files a/frontend/public/images/logo_no_orb128.png and b/frontend/public/images/logo_no_orb128.png differ diff --git a/frontend/public/images/logo_no_orb64.png b/frontend/public/images/logo_no_orb64.png index 9227e439f4..46804394d3 100644 Binary files a/frontend/public/images/logo_no_orb64.png and b/frontend/public/images/logo_no_orb64.png differ diff --git a/frontend/public/images/maskable-icon-512x512.png b/frontend/public/images/maskable-icon-512x512.png index 2992ae09b3..a6f20b650d 100644 Binary files a/frontend/public/images/maskable-icon-512x512.png and b/frontend/public/images/maskable-icon-512x512.png differ diff --git a/frontend/public/images/mstile-150x150.png b/frontend/public/images/mstile-150x150.png index adb0369626..1cbdf406eb 100644 Binary files a/frontend/public/images/mstile-150x150.png and b/frontend/public/images/mstile-150x150.png differ diff --git a/frontend/public/images/pwa-192x192.png b/frontend/public/images/pwa-192x192.png index 89b2d90933..dc45d26c54 100644 Binary files a/frontend/public/images/pwa-192x192.png and b/frontend/public/images/pwa-192x192.png differ diff --git a/frontend/public/images/pwa-512x512.png b/frontend/public/images/pwa-512x512.png index 2992ae09b3..b05f07065d 100644 Binary files a/frontend/public/images/pwa-512x512.png and b/frontend/public/images/pwa-512x512.png differ diff --git a/frontend/public/images/pwa-64x64.png b/frontend/public/images/pwa-64x64.png index 0c48df95c2..fa0f87cbd0 100644 Binary files a/frontend/public/images/pwa-64x64.png and b/frontend/public/images/pwa-64x64.png differ diff --git a/frontend/src/App/Header.module.scss b/frontend/src/App/Header.module.scss index 8f22398097..47e5096592 100644 --- a/frontend/src/App/Header.module.scss +++ b/frontend/src/App/Header.module.scss @@ -1,9 +1,52 @@ .header { - @include mantine.light { - color: var(--mantine-color-gray-0); + box-shadow: none; +} + +.headerInner { + background: var(--bz-surface-base); + border: 1px solid var(--bz-border-card); + border-radius: var(--bz-radius-xl); + padding: 10px 16px; + margin-right: 8px; + display: flex; + align-items: center; + + color: var(--bz-text-primary); + + // Ghost-style action buttons (theme toggle, notifications, system gear) + :global(.mantine-ActionIcon-root) { + width: 32px; + height: 32px; + min-width: 32px; + min-height: 32px; + background: var(--bz-hover-bg); + border: 1px solid var(--bz-border-interactive); + border-radius: var(--bz-radius-md); + color: var(--bz-text-tertiary); + font-size: 12px; + font-weight: 500; + &:hover { + background: var(--bz-hover-bg-emphasis); + } } - @include mantine.dark { - color: var(--mantine-color-dark-0); + // Search input + :global(.mantine-Select-input) { + background: var(--bz-hover-bg); + border: 1px solid var(--bz-border-interactive); + border-radius: var(--bz-radius-md); + transition: background var(--bz-duration-normal) var(--bz-ease-standard); + + &::placeholder { + color: var(--bz-text-tertiary); + } + + &:hover { + background: var(--bz-hover-bg-emphasis); + } + + &:focus { + background: var(--bz-hover-bg-emphasis); + } } } diff --git a/frontend/src/App/Header.tsx b/frontend/src/App/Header.tsx index c5aa21e160..408ddbc78e 100644 --- a/frontend/src/App/Header.tsx +++ b/frontend/src/App/Header.tsx @@ -3,11 +3,11 @@ import { Anchor, AppShell, Avatar, - Badge, Burger, Divider, Group, Menu, + Text, useComputedColorScheme, useMantineColorScheme, } from "@mantine/core"; @@ -53,79 +53,104 @@ const AppHeader: FunctionComponent = () => { const { data: jobs } = useSystemJobs(); return ( - - - - show(!showed)} - size="sm" - hiddenFrom="sm" - > - - - - - Bazarr+ - - - + +
+ + + show(!showed)} + size="sm" + hiddenFrom="sm" + > + + + + + Bazarr + + + + + + + + +
+ +
+ + toggleColorScheme()} + icon={dark ? faSun : faMoon} + size="sm" + > + job.status === "running").length, + )} + onClick={openJobsManager} + > + + + + + + } + onClick={() => restart()} + > + Restart + + } + onClick={() => shutdown()} + > + Shutdown + + + + + +
- - - toggleColorScheme()} - icon={dark ? faSun : faMoon} - size="sm" - > - job.status === "running").length, - )} - onClick={openJobsManager} - > - - - - - - } - onClick={() => restart()} - > - Restart - - } - onClick={() => shutdown()} - > - Shutdown - - - - - - - +
r.path !== undefined && !r.hidden && !r.path.includes(":") && r.name, + ); + + const groups: { label: string; items: CustomRouteObject[] }[] = []; + + for (const section of sectionGroups) { + const items = section.paths + .map((p) => navItems.find((r) => r.path === p)) + .filter((r): r is CustomRouteObject => r !== undefined); + + if (items.length > 0) { + groups.push({ label: section.label, items }); + } + } + + // Catch any remaining items not in a defined group + const groupedPaths = new Set(sectionGroups.flatMap((s) => s.paths)); + const ungrouped = navItems.filter((r) => !groupedPaths.has(r.path ?? "")); + if (ungrouped.length > 0) { + groups.push({ label: "Other", items: ungrouped }); + } + + return groups; +} + const AppNavbar: FunctionComponent = () => { const [selection, select] = useState(null); @@ -97,24 +125,36 @@ const AppNavbar: FunctionComponent = () => { select(null); }, [pathname]); + // The top-level route (path "/") contains the nav items as children. + // useRouteItems returns the full routes array, and the nameless "/" route + // renders its children directly. We need to find the app route's children. + const navRoutes = useMemo(() => { + const appRoute = routes.find((r) => r.path === "/"); + return appRoute?.children ?? routes; + }, [routes]); + + const groups = useMemo(() => groupRoutes(navRoutes), [navRoutes]); + return ( - - - + +
+ - {routes.map((route, idx) => ( - + {groups.map((group) => ( +
+
{group.label}
+ {group.items.map((route, idx) => ( + + ))} +
))}
- -
+ +
); }; @@ -146,7 +186,7 @@ const RouteItem: FunctionComponent<{ parent={link} key={BuildKey(link, "nav", idx)} route={child} - > + /> ))} ); @@ -155,7 +195,6 @@ const RouteItem: FunctionComponent<{ return ( + /> @@ -188,12 +227,7 @@ const RouteItem: FunctionComponent<{ } } else { return ( - + ); } }; @@ -203,7 +237,6 @@ interface NavbarItemProps { link: string; icon?: IconDefinition; badge?: number | string; - primary?: boolean; onClick?: (event: React.MouseEvent) => void; } @@ -213,7 +246,6 @@ const NavbarItem: FunctionComponent = ({ name, badge, onClick, - primary = false, }) => { const { show } = useNavbar(); @@ -247,24 +279,11 @@ const NavbarItem: FunctionComponent = ({ ) } > - - {icon && ( - - )} + + {icon && } {name} {!shouldHideBadge && ( - + {badge} )} diff --git a/frontend/src/App/NotificationDrawer.tsx b/frontend/src/App/NotificationDrawer.tsx index 232ce06308..b4917afb40 100644 --- a/frontend/src/App/NotificationDrawer.tsx +++ b/frontend/src/App/NotificationDrawer.tsx @@ -139,7 +139,7 @@ const NotificationDrawer: FunctionComponent = ({ )} {!jobsLoading && jobsError && ( - + Failed to load jobs. @@ -227,7 +227,7 @@ const NotificationDrawer: FunctionComponent = ({ )} - + {grouped[status as string].length} job {grouped[status as string].length > 1 ? "s" : ""} @@ -254,7 +254,7 @@ const NotificationDrawer: FunctionComponent = ({ = ({ position="right" > = ({ = ({ {job?.progress_message && ( - + {job.progress_message} )} @@ -493,14 +494,14 @@ const NotificationDrawer: FunctionComponent = ({ )); })() ) : ( - + No jobs to display )} ) : ( - - + + Jobs { > - Your password is currently stored using a weak MD5 hash. - Would you like to upgrade to PBKDF2-SHA256 for better security? + Your password is currently stored using a weak MD5 hash. Would + you like to upgrade to PBKDF2-SHA256 for better security? - + Note: After upgrading, reverting to upstream Bazarr will require resetting your password via the config file. diff --git a/frontend/src/apis/hooks/system.ts b/frontend/src/apis/hooks/system.ts index e28560099d..49fb92aad2 100644 --- a/frontend/src/apis/hooks/system.ts +++ b/frontend/src/apis/hooks/system.ts @@ -275,7 +275,12 @@ export function useSystem() { api.system.login(param.username, param.password), onSuccess: (data) => { - if (data && typeof data === "object" && "upgrade_token" in data && data.upgrade_token) { + if ( + data && + typeof data === "object" && + "upgrade_token" in data && + data.upgrade_token + ) { // Store opaque token (not password) for upgrade prompt sessionStorage.setItem("password_upgrade_token", data.upgrade_token); } diff --git a/frontend/src/apis/hooks/translator.ts b/frontend/src/apis/hooks/translator.ts index ef620e5395..e8415c2abf 100644 --- a/frontend/src/apis/hooks/translator.ts +++ b/frontend/src/apis/hooks/translator.ts @@ -4,7 +4,13 @@ import client from "@/apis/raw/client"; export interface TranslatorJob { jobId: string; - status: "queued" | "processing" | "completed" | "partial" | "failed" | "cancelled"; + status: + | "queued" + | "processing" + | "completed" + | "partial" + | "failed" + | "cancelled"; progress: number; message?: string; createdAt: string; diff --git a/frontend/src/apis/raw/subtitles.ts b/frontend/src/apis/raw/subtitles.ts index ae34a75c9d..2185d24e46 100644 --- a/frontend/src/apis/raw/subtitles.ts +++ b/frontend/src/apis/raw/subtitles.ts @@ -8,6 +8,9 @@ export interface SubtitleContentResponse { language: string; size: number; lastModified: number; + mediaTitle?: string; + mediaId?: number; + episodeTitle?: string; } export type BatchAction = @@ -31,6 +34,16 @@ export interface BatchItem { } export interface BatchOptions { + maxOffsetSeconds?: number; + noFixFramerate?: boolean; + gss?: boolean; + forceResync?: boolean; + fromLang?: string; + toLang?: string; +} + +/* eslint-disable camelcase -- backend API contract */ +interface BatchPayload { max_offset_seconds?: number; no_fix_framerate?: boolean; gss?: boolean; @@ -39,6 +52,18 @@ export interface BatchOptions { to_lang?: string; } +function toBatchPayload(options: BatchOptions): BatchPayload { + return { + max_offset_seconds: options.maxOffsetSeconds, + no_fix_framerate: options.noFixFramerate, + gss: options.gss, + force_resync: options.forceResync, + from_lang: options.fromLang, + to_lang: options.toLang, + }; +} +/* eslint-enable camelcase */ + export interface BatchResponse { queued: number; skipped: number; @@ -91,7 +116,7 @@ class SubtitlesApi extends BaseApi { const response = await this.postRaw("/batch", { items, action, - options, + options: options ? toBatchPayload(options) : undefined, }); return response.data; } diff --git a/frontend/src/apis/raw/system.ts b/frontend/src/apis/raw/system.ts index eb7392960f..24ba7f49d3 100644 --- a/frontend/src/apis/raw/system.ts +++ b/frontend/src/apis/raw/system.ts @@ -18,7 +18,11 @@ class SystemApi extends BaseApi { } async upgradePasswordHash(upgradeToken: string) { - await this.post("/account", { password: upgradeToken }, { action: "upgrade_hash" }); + await this.post( + "/account", + { password: upgradeToken }, + { action: "upgrade_hash" }, + ); } async logout() { diff --git a/frontend/src/assets/_bazarr.scss b/frontend/src/assets/_bazarr.scss index d6b8d72c54..d31f51aca8 100644 --- a/frontend/src/assets/_bazarr.scss +++ b/frontend/src/assets/_bazarr.scss @@ -11,43 +11,7 @@ $color-brand-7: #995c00; $color-brand-8: #7a4900; $color-brand-9: #5c3700; -// Based on Mantine Cyan -$color-highlight-0: #e3fafc; -$color-highlight-1: #c5f6fa; -$color-highlight-2: #99e9f2; -$color-highlight-3: #66d9e8; -$color-highlight-4: #3bc9db; -$color-highlight-5: #22b8cf; -$color-highlight-6: #15aabf; -$color-highlight-7: #1098ad; -$color-highlight-8: #0c8599; -$color-highlight-9: #0b7285; - -// Based on Mantine Yellow -$color-warning-0: #fff9db; -$color-warning-1: #fff3bf; -$color-warning-2: #ffec99; -$color-warning-3: #ffe066; -$color-warning-4: #ffd43b; -$color-warning-5: #fcc419; -$color-warning-6: #fab005; -$color-warning-7: #f59f00; -$color-warning-8: #f08c00; -$color-warning-9: #e67700; - -// Based on Mantine Gray -$color-disabled-0: #f8f9fa; -$color-disabled-1: #f1f3f5; -$color-disabled-2: #e9ecef; -$color-disabled-3: #dee2e6; -$color-disabled-4: #ced4da; -$color-disabled-5: #adb5bd; -$color-disabled-6: #868e96; -$color-disabled-7: #495057; -$color-disabled-8: #343a40; -$color-disabled-9: #212529; - -$header-height: 64px; +$header-height: 76px; :global { .table-long-break { @@ -82,20 +46,337 @@ $header-height: 64px; :root { @include dark { - --mantine-color-body: var(--mantine-color-dark-8); + --mantine-color-body: var(--mantine-color-dark-9); } } +// Grain texture overlay (z-index above content, pointer-events:none) +body::before { + content: ""; + position: fixed; + inset: 0; + z-index: 10000; + pointer-events: none; + opacity: 0.07; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + background-repeat: repeat; + background-size: 256px 256px; + + @include light { + opacity: 0.035; + mix-blend-mode: multiply; + } +} + +// Ambient atmospheric glow (z-index above content backgrounds, below grain) +body::after { + content: ""; + position: fixed; + inset: 0; + z-index: 9999; + pointer-events: none; + background: radial-gradient( + ellipse at 15% 10%, + rgba(230, 138, 0, 0.12) 0%, + transparent 50% + ), + radial-gradient( + ellipse at 85% 90%, + rgba(26, 138, 138, 0.08) 0%, + transparent 50% + ), + radial-gradient( + ellipse at 50% 50%, + rgba(140, 80, 200, 0.05) 0%, + transparent 60% + ); + + @include light { + display: none; + } +} + +#root { + position: relative; + z-index: 1; +} + +/* Shared tokens (scheme-independent) */ +:root { + /* Border radius scale */ + --bz-radius-xs: 6px; + --bz-radius-sm: 8px; + --bz-radius-md: 10px; + --bz-radius-lg: 14px; + --bz-radius-xl: 20px; + + /* Animation tokens */ + --bz-ease-standard: cubic-bezier(0.2, 0, 0, 1); + --bz-ease-enter: cubic-bezier(0, 0, 0.2, 1); + --bz-ease-exit: cubic-bezier(0.4, 0, 1, 1); + --bz-duration-fast: 120ms; + --bz-duration-normal: 150ms; + --bz-duration-slow: 250ms; + --bz-duration-card: 300ms; + --bz-duration-page: 200ms; + + /* Stagger tokens */ + --bz-stagger-delay: 40ms; +} + :root[data-mantine-color-scheme="dark"] { + --bz-surface-ground: #0c0b1a; --bz-surface-base: #121125; --bz-surface-raised: #1a1a2e; --bz-surface-card: #22223a; --bz-surface-card-hover: #2a2a45; - --bz-border-subtle: #33334d; + --bz-surface-overlay: #2a2a45; --bz-stat-processing: #ffca28; --bz-stat-queued: #4dabf7; --bz-stat-completed: #69db7c; --bz-stat-failed: #ff6b6b; --bz-shadow-float: 0 4px 12px rgba(0, 0, 0, 0.5); - --bz-transition-lift: transform 150ms ease, background-color 150ms ease; + + /* Text hierarchy */ + --bz-text-primary: #e8e8f0; + --bz-text-secondary: #b8b8cc; + --bz-text-tertiary: #8e8fa3; + --bz-text-disabled: #6c6d85; + + /* Border opacity scale */ + --bz-border-divider: rgba(255, 255, 255, 0.04); + --bz-border-card: rgba(255, 255, 255, 0.05); + --bz-border-interactive: rgba(255, 255, 255, 0.06); + --bz-border-hover: rgba(255, 255, 255, 0.07); + + /* Hover tokens */ + --bz-hover-bg: rgba(255, 255, 255, 0.04); + --bz-hover-bg-emphasis: rgba(255, 255, 255, 0.07); +} + +:root[data-mantine-color-scheme="light"] { + --bz-text-primary: #12121f; + --bz-text-secondary: #343a40; + --bz-text-tertiary: #656d76; + --bz-text-disabled: #909aa5; + + --bz-border-divider: rgba(0, 0, 0, 0.06); + --bz-border-card: rgba(0, 0, 0, 0.08); + --bz-border-interactive: rgba(0, 0, 0, 0.1); + --bz-border-hover: rgba(0, 0, 0, 0.12); + + --bz-surface-ground: #f8f9fa; + --bz-surface-base: #ffffff; + --bz-surface-raised: #f1f3f5; + --bz-surface-card: #ffffff; + --bz-surface-card-hover: #f8f9fa; + --bz-surface-overlay: #ffffff; + --bz-stat-processing: #e67700; + --bz-stat-queued: #1971c2; + --bz-stat-completed: #2b8a3e; + --bz-stat-failed: #e03131; + --bz-shadow-float: 0 4px 12px rgba(0, 0, 0, 0.1); + + --bz-hover-bg: rgba(0, 0, 0, 0.04); + --bz-hover-bg-emphasis: rgba(0, 0, 0, 0.07); +} + +// Floaty main content area +:global { + .mantine-AppShell-main { + background: var(--bz-surface-base); + margin: 0 8px 8px 0; + border-radius: var(--bz-radius-xl); + border: 1px solid var(--bz-border-card); + } + + .mantine-AppShell-navbar { + background: transparent; + border-right: none; + } + + .mantine-AppShell-header { + background: transparent !important; + border: none !important; + padding: 8px !important; + } + + .mantine-AppShell-root { + background: var(--bz-surface-ground); + } +} + +// Global Paper/Card styling +:global { + .mantine-Paper-root { + border-radius: var(--bz-radius-lg); + + &[data-with-border] { + border-color: var(--bz-border-card); + } + } + + .mantine-Alert-root { + border-radius: var(--bz-radius-lg); + } + + .mantine-Card-root { + background: var(--bz-surface-card); + border-radius: var(--bz-radius-lg); + border: 1px solid var(--bz-border-card); + box-shadow: none; + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + + &:hover { + border-color: var(--bz-border-hover); + } + } + + // Tabs styling + .mantine-Tabs-tab { + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + color var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + } + + // Switch styling + .mantine-Switch-track { + transition: + background-color var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + } + + // Checkbox styling + .mantine-Checkbox-input { + border-radius: var(--bz-radius-xs); + transition: + background-color var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + } + + // Anchor/link styling + .mantine-Anchor-root { + transition: color var(--bz-duration-normal) var(--bz-ease-standard); + } + + // Tooltip styling + .mantine-Tooltip-tooltip { + background: var(--bz-surface-overlay); + border: 1px solid var(--bz-border-card); + border-radius: var(--bz-radius-sm); + color: var(--bz-text-primary); + box-shadow: var(--bz-shadow-float); + } + + // Popover styling + .mantine-Popover-dropdown { + background: var(--bz-surface-overlay); + border: 1px solid var(--bz-border-card); + border-radius: var(--bz-radius-lg); + box-shadow: var(--bz-shadow-float); + } +} + +// Global dropdown/menu/combobox styling +:global { + .mantine-Menu-dropdown, + .mantine-Combobox-dropdown, + .mantine-Select-dropdown, + .mantine-MultiSelect-dropdown, + .mantine-Autocomplete-dropdown { + background: var(--bz-surface-overlay); + border: 1px solid var(--bz-border-card); + border-radius: var(--bz-radius-lg); + box-shadow: var(--bz-shadow-float); + } + + .mantine-Menu-item { + border-radius: var(--bz-radius-sm); + color: var(--bz-text-primary); + transition: background var(--bz-duration-normal) var(--bz-ease-standard); + + &:hover, + &[data-hovered] { + background: var(--bz-hover-bg); + } + } + + .mantine-Combobox-option { + border-radius: var(--bz-radius-sm); + transition: background var(--bz-duration-normal) var(--bz-ease-standard); + + &:hover, + &[data-combobox-selected], + &[data-hovered] { + background: var(--bz-hover-bg); + } + } + + .mantine-Menu-label { + color: var(--bz-text-disabled); + } + + .mantine-Menu-divider { + border-color: var(--bz-border-divider); + } +} + +// Card reveal entry animation +@keyframes bz-card-reveal { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +// Apply card reveal to Mantine Cards and bordered Papers +:global { + .mantine-Card-root { + animation: bz-card-reveal var(--bz-duration-card) var(--bz-ease-enter) both; + } + + // Stagger cards inside Stack/SimpleGrid containers + .mantine-Stack-root, + .mantine-SimpleGrid-root { + > .mantine-Card-root, + > .mantine-Paper-root[data-with-border] { + animation: bz-card-reveal var(--bz-duration-card) var(--bz-ease-enter) + both; + + @for $i from 1 through 6 { + &:nth-child(#{$i}) { + animation-delay: calc(#{$i - 1} * var(--bz-stagger-delay)); + } + } + + // Items beyond 6th appear without extra stagger + &:nth-child(n + 7) { + animation-delay: calc(5 * var(--bz-stagger-delay)); + } + } + } +} + +// Reduced motion support +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-delay: 0ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + body::after { + display: none !important; + } } diff --git a/frontend/src/assets/_variables.module.scss b/frontend/src/assets/_variables.module.scss index c707980bfe..c3683f5ab6 100644 --- a/frontend/src/assets/_variables.module.scss +++ b/frontend/src/assets/_variables.module.scss @@ -1,5 +1,7 @@ $navbar-width: 200; +$surface-ground: #0c0b1a; + :export { colorBrand0: bazarr.$color-brand-0; colorBrand1: bazarr.$color-brand-1; @@ -15,4 +17,6 @@ $navbar-width: 200; headerHeight: bazarr.$header-height; navBarWidth: $navbar-width; + + surfaceGround: $surface-ground; } diff --git a/frontend/src/assets/action_icon.module.scss b/frontend/src/assets/action_icon.module.scss index 3ff702f35b..79d71034d1 100644 --- a/frontend/src/assets/action_icon.module.scss +++ b/frontend/src/assets/action_icon.module.scss @@ -1,16 +1,18 @@ @layer mantine { .root { --ai-bg: transparent; + border-radius: var(--bz-radius-sm); + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + color var(--bz-duration-normal) var(--bz-ease-standard); - @include mantine.light { - color: var(--mantine-color-dark-2); - --ai-hover: var(--mantine-color-gray-1); - --ai-hover-color: var(--mantine-color-gray-1); - } + color: var(--bz-text-secondary); + --ai-hover: var(--bz-hover-bg); + --ai-hover-color: var(--bz-text-primary); - @include mantine.dark { - color: var(--mantine-color-dark-0); - --ai-hover: var(--mantine-color-gray-8); + &[data-variant="gradient"], + &[data-variant="filled"] { + color: #fff; } } } diff --git a/frontend/src/assets/badge.module.scss b/frontend/src/assets/badge.module.scss index b509aaa6c0..07c37e5a3e 100644 --- a/frontend/src/assets/badge.module.scss +++ b/frontend/src/assets/badge.module.scss @@ -1,73 +1,188 @@ -@use "sass:color"; - @layer mantine { .root { - background-color: color.adjust(bazarr.$color-brand-6, $alpha: -0.8); + border-radius: var(--bz-radius-sm); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + padding: 2px 8px; + transition: + background var(--bz-duration-fast) var(--bz-ease-standard), + border-color var(--bz-duration-fast) var(--bz-ease-standard); - &[data-variant="warning"] { - color: #ff6b6b; - background-color: transparent; - border: 1px dashed #ff6b6b; + // Default variant (ghost): neutral for audio tracks and general use + background-color: rgba(255, 255, 255, 0.05); + color: #a9aabb; + border: 1px solid rgba(255, 255, 255, 0.07); + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.12); } + // Highlight variant (green): available subtitles &[data-variant="highlight"] { - color: color.adjust(bazarr.$color-highlight-2, $lightness: 100%); - background-color: color.adjust(bazarr.$color-highlight-5, $alpha: -0.8); + background: rgba(105, 219, 124, 0.12); + color: #69db7c; + border: 1px solid rgba(105, 219, 124, 0.18); + + &:hover { + background: rgba(105, 219, 124, 0.17); + border-color: rgba(105, 219, 124, 0.23); + } + } + + // Warning variant (amber): upgradeable subtitles + &[data-variant="warning"] { + background: rgba(230, 138, 0, 0.12); + color: #ffd54f; + border: 1px solid rgba(230, 138, 0, 0.18); + + &:hover { + background: rgba(230, 138, 0, 0.17); + border-color: rgba(230, 138, 0, 0.23); + } } + // Disabled variant (ghost dim): embedded/disabled subtitles &[data-variant="disabled"] { - color: color.adjust(bazarr.$color-disabled-0, $lightness: 100%); - background-color: color.adjust(bazarr.$color-disabled-7, $alpha: -0.8); + background: rgba(255, 255, 255, 0.03); + color: #6c6d85; + border: 1px solid rgba(255, 255, 255, 0.04); + + &:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.08); + } } + // HI subs variant (warm): hearing impaired subtitles + &[data-variant="hi"] { + background: rgba(179, 107, 0, 0.1); + color: #ffd54f; + border: 1px solid rgba(179, 107, 0, 0.18); + + &:hover { + background: rgba(179, 107, 0, 0.15); + border-color: rgba(179, 107, 0, 0.23); + } + } + + // Missing variant: missing subtitles + &[data-variant="missing"] { + background: rgba(255, 107, 107, 0.06); + color: #ff8787; + border: 1px dashed rgba(255, 107, 107, 0.4); + + &:hover { + background: rgba(255, 107, 107, 0.11); + border-color: rgba(255, 107, 107, 0.5); + } + } + + // Preserve passthrough for Mantine built-in variants used elsewhere &[data-variant="light"], &[data-variant="outline"] { color: unset; background: unset; background-color: unset; + border: unset; } &[data-variant="gradient"] { color: #fff; + border: unset; } &[data-variant="transparent"] { color: unset; + background: unset; + border: unset; } + // Light mode overrides @include mantine.light { - color: bazarr.$color-brand-6; - background-color: color.adjust(bazarr.$color-brand-3, $alpha: -0.8); + background-color: rgba(0, 0, 0, 0.05); + color: #495057; + border: 1px solid rgba(0, 0, 0, 0.08); + + &:hover { + background-color: rgba(0, 0, 0, 0.08); + border-color: rgba(0, 0, 0, 0.12); + } &[data-variant="warning"] { - color: #e03131; - background-color: transparent; - border: 1px dashed #e03131; + background: rgba(230, 138, 0, 0.1); + color: #995c00; + border: 1px solid rgba(230, 138, 0, 0.2); + + &:hover { + background: rgba(230, 138, 0, 0.15); + border-color: rgba(230, 138, 0, 0.28); + } } &[data-variant="disabled"] { - color: color.adjust(bazarr.$color-disabled-6, $lightness: -100%); - background-color: color.adjust(bazarr.$color-disabled-4, $alpha: -0.8); + background: rgba(0, 0, 0, 0.03); + color: #adb5bd; + border: 1px solid rgba(0, 0, 0, 0.06); + + &:hover { + background: rgba(0, 0, 0, 0.05); + border-color: rgba(0, 0, 0, 0.1); + } } &[data-variant="highlight"] { - color: color.adjust(bazarr.$color-highlight-6, $lightness: -100%); - background-color: color.adjust(bazarr.$color-highlight-5, $alpha: -0.8); + background: rgba(105, 219, 124, 0.1); + color: #2b8a3e; + border: 1px solid rgba(105, 219, 124, 0.2); + + &:hover { + background: rgba(105, 219, 124, 0.15); + border-color: rgba(105, 219, 124, 0.28); + } + } + + &[data-variant="missing"] { + background: rgba(255, 107, 107, 0.06); + color: #e03131; + border: 1px dashed rgba(255, 107, 107, 0.4); + + &:hover { + background: rgba(255, 107, 107, 0.1); + border-color: rgba(255, 107, 107, 0.5); + } + } + + &[data-variant="hi"] { + background: rgba(179, 107, 0, 0.1); + color: #7a4900; + border: 1px solid rgba(179, 107, 0, 0.2); + + &:hover { + background: rgba(179, 107, 0, 0.15); + border-color: rgba(179, 107, 0, 0.28); + } } &[data-variant="light"], &[data-variant="outline"] { color: unset; background: unset; - background-color: unset; + border: unset; } &[data-variant="gradient"] { color: #fff; + background-color: transparent; + border: none; } &[data-variant="transparent"] { color: unset; + background: unset; + border: unset; } } } diff --git a/frontend/src/assets/button.module.scss b/frontend/src/assets/button.module.scss index 2d78728c98..14b9a3a879 100644 --- a/frontend/src/assets/button.module.scss +++ b/frontend/src/assets/button.module.scss @@ -1,11 +1,16 @@ @layer mantine { .root { - @include mantine.dark { - color: var(--mantine-color-dark-0); + border-radius: var(--bz-radius-sm); + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + transform var(--bz-duration-normal) var(--bz-ease-standard), + box-shadow var(--bz-duration-normal) var(--bz-ease-standard); - &[data-variant="gradient"] { - color: #fff; - } + color: var(--bz-text-primary); + + &[data-variant="filled"], + &[data-variant="gradient"] { + color: #fff; } &[data-variant="danger"] { @@ -15,8 +20,10 @@ } .root:disabled { - @include mantine.dark { - color: var(--mantine-color-dark-9); + color: var(--bz-text-disabled); + + @include mantine.light { + color: var(--bz-text-tertiary); } } } diff --git a/frontend/src/assets/input.module.scss b/frontend/src/assets/input.module.scss new file mode 100644 index 0000000000..7d29daebcf --- /dev/null +++ b/frontend/src/assets/input.module.scss @@ -0,0 +1,24 @@ +@layer mantine { + .input { + border-radius: var(--bz-radius-md); + background: var(--bz-hover-bg); + border: 1px solid var(--bz-border-interactive); + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + + &::placeholder { + color: var(--bz-text-tertiary); + } + + &:hover { + background: var(--bz-hover-bg-emphasis); + } + + &:focus, + &:focus-within { + background: var(--bz-hover-bg-emphasis); + border-color: var(--bz-border-hover); + } + } +} diff --git a/frontend/src/assets/pagination.module.scss b/frontend/src/assets/pagination.module.scss index 2b66d75103..f5c9e924bb 100644 --- a/frontend/src/assets/pagination.module.scss +++ b/frontend/src/assets/pagination.module.scss @@ -1,3 +1,15 @@ -.control { - --pagination-active-bg: var(--mantine-color-brand-filled); +@layer mantine { + .control { + --pagination-active-bg: var(--mantine-color-brand-filled); + border-radius: var(--bz-radius-xs); + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + + color: var(--bz-text-secondary); + + &:hover:not([data-active]) { + background: var(--bz-hover-bg); + } + } } diff --git a/frontend/src/assets/progress.module.scss b/frontend/src/assets/progress.module.scss new file mode 100644 index 0000000000..3b99a26dd0 --- /dev/null +++ b/frontend/src/assets/progress.module.scss @@ -0,0 +1,9 @@ +$noise-svg: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.15'/%3E%3C/svg%3E"); + +@layer mantine { + .section { + background-image: $noise-svg !important; + background-size: 128px 128px !important; + background-repeat: repeat !important; + } +} diff --git a/frontend/src/components/StateIcon.tsx b/frontend/src/components/StateIcon.tsx index 23bb2d507d..4e164729fd 100644 --- a/frontend/src/components/StateIcon.tsx +++ b/frontend/src/components/StateIcon.tsx @@ -73,7 +73,7 @@ const StateIcon: FunctionComponent = ({ - + Scoring Criteria diff --git a/frontend/src/components/SubtitleToolsMenu.tsx b/frontend/src/components/SubtitleToolsMenu.tsx index cdce86031e..f99f563790 100644 --- a/frontend/src/components/SubtitleToolsMenu.tsx +++ b/frontend/src/components/SubtitleToolsMenu.tsx @@ -218,7 +218,10 @@ const SubtitleToolsMenu: FunctionComponent = ({ )} {isMissing && !hasSources && ( - }> + } + > No source subtitles to translate from )} diff --git a/frontend/src/components/TranslatorStatus.module.css b/frontend/src/components/TranslatorStatus.module.css index 5aa26e285b..70d62bcbd3 100644 --- a/frontend/src/components/TranslatorStatus.module.css +++ b/frontend/src/components/TranslatorStatus.module.css @@ -1,14 +1,34 @@ @keyframes heartbeat { - 0%, 100% { opacity: 1; transform: scale(1); } - 14% { opacity: 1; transform: scale(1.15); } - 28% { opacity: 1; transform: scale(1); } - 42% { opacity: 1; transform: scale(1.1); } - 56% { opacity: 1; transform: scale(1); } + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + 14% { + opacity: 1; + transform: scale(1.15); + } + 28% { + opacity: 1; + transform: scale(1); + } + 42% { + opacity: 1; + transform: scale(1.1); + } + 56% { + opacity: 1; + transform: scale(1); + } } @keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } .promptConnected { diff --git a/frontend/src/components/TranslatorStatus.tsx b/frontend/src/components/TranslatorStatus.tsx index e936c80eb8..b6916dc66f 100644 --- a/frontend/src/components/TranslatorStatus.tsx +++ b/frontend/src/components/TranslatorStatus.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent, useCallback,useState } from "react"; +import { FunctionComponent, useCallback, useState } from "react"; import { Alert, Badge, @@ -77,9 +77,7 @@ function formatCostValue(cost: number): string { return `$${cost.toFixed(2)}`; } -const JobRow: FunctionComponent = ({ - job, -}) => { +const JobRow: FunctionComponent = ({ job }) => { const modelUsed = job.result?.model_used || job.model; // Prefer top-level tokensUsed (live from API), fall back to result const tokensUsed = job.tokensUsed || job.result?.tokens_used; @@ -103,7 +101,9 @@ const JobRow: FunctionComponent = ({ durationStr = formatDuration(durationSec); } else if (job.startedAt && job.completedAt) { durationSec = - (new Date(job.completedAt).getTime() - new Date(job.startedAt).getTime()) / 1000; + (new Date(job.completedAt).getTime() - + new Date(job.startedAt).getTime()) / + 1000; durationStr = formatDuration(durationSec); } else if (job.status === "processing" && job.startedAt) { const elapsed = (Date.now() - new Date(job.startedAt).getTime()) / 1000; @@ -111,19 +111,22 @@ const JobRow: FunctionComponent = ({ } // TPS - const tpsStr = tokensUsed && durationSec > 0 - ? `${(tokensUsed / durationSec).toFixed(0)} t/s` - : "-"; + const tpsStr = + tokensUsed && durationSec > 0 + ? `${(tokensUsed / durationSec).toFixed(0)} t/s` + : "-"; // Progress — show lines if available - const progressLabel = job.completedLines && job.totalLines - ? `${job.completedLines}/${job.totalLines} lines` - : `${job.progress}%`; + const progressLabel = + job.completedLines && job.totalLines + ? `${job.completedLines}/${job.totalLines} lines` + : `${job.progress}%`; // Batch info for tooltip - const batchInfo = job.completedBatches && job.totalBatches - ? `Batch ${job.completedBatches}/${job.totalBatches}` - : undefined; + const batchInfo = + job.completedBatches && job.totalBatches + ? `Batch ${job.completedBatches}/${job.totalBatches}` + : undefined; return ( @@ -137,7 +140,9 @@ const JobRow: FunctionComponent = ({ {job.error ? ( - + + + ) : ( @@ -145,7 +150,10 @@ const JobRow: FunctionComponent = ({ {job.status === "processing" ? ( - + ) : ( @@ -153,7 +161,13 @@ const JobRow: FunctionComponent = ({ )} - + {modelUsed || "-"} @@ -165,12 +179,17 @@ const JobRow: FunctionComponent = ({ {job.totalCost != null && job.totalCost > 0 ? ( - + {formatCostValue(job.totalCost)} - ) : "-"} + ) : ( + "-" + )} @@ -179,7 +198,11 @@ const JobRow: FunctionComponent = ({ - + {tpsStr} @@ -207,7 +230,13 @@ const StatCard: FunctionComponent = ({ {value} - + {label} @@ -246,7 +275,9 @@ export const TranslatorStatusPanel: FunctionComponent< ); @@ -263,7 +294,9 @@ export const TranslatorStatusPanel: FunctionComponent< color="yellow" title="AI Subtitle Translator Unavailable" mt="md" - icon={