diff --git a/.gitignore b/.gitignore index bd78101..6eebd7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ -# Runtime config (generated from config.example.json on first run) -config.json # Database *.db +# Local config (use data/config.json or data/) +config.json + # Python __pycache__/ *.pyc @@ -22,6 +23,7 @@ static/css/main.css.map # User data directories (configured in config.json) +data/ transcoded_files/ audio-tracks/ diff --git a/Dockerfile b/Dockerfile index f82f327..421bb89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ FROM python:3.12-slim RUN apt-get update \ - && apt-get install -y --no-install-recommends ffmpeg \ + && apt-get install -y --no-install-recommends ffmpeg vainfo libva-drm2 pciutils \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/README.md b/README.md index cee7734..09f264d 100644 --- a/README.md +++ b/README.md @@ -75,20 +75,37 @@ `config.json` is automatically created from `config.example.json` on first run. -**For Docker**, place your config at `data/config.json` and mount your media directories in `docker-compose.yml`: +**For Docker**, place your config at `data/config.json` and mount your media directories in `docker-compose.yml`. Mount them at the same path as on the host so `config.json` requires no changes: ```yaml volumes: - ./data:/data - - /your/nas/films:/media/films:ro - - /your/nas/series:/media/series:ro + - /your/media-storage:/your/media-storage:ro ``` -Then reference those paths in `data/config.json`: +You can add as many mounts as needed: + +```yaml +volumes: + - ./data:/data + - /mnt/disk1/films:/mnt/disk1/films:ro + - /mnt/disk2/series:/mnt/disk2/series:ro + - /home/user/media:/home/user/media:ro +``` + +```bash +docker compose down && docker compose up -d +``` + +Then reference the same paths in `data/config.json`: ```json { - "directories": ["/media/films", "/media/series"], + "directories": [ + "/mnt/disk1/films", + "/mnt/disk2/series", + "/home/user/media" + ], "cache_dir": "/data/cache", "transcoded_directory": "/data/transcoded_files", "audio_tracks_directory": "/data/audio-tracks" diff --git a/app.py b/app.py index 4ff770a..3d8b1c6 100644 --- a/app.py +++ b/app.py @@ -1,3 +1,6 @@ +from gevent import monkey +monkey.patch_all() + from flask import Flask, render_template, request, jsonify, send_from_directory, session, redirect, url_for from functools import wraps import os @@ -15,7 +18,7 @@ app = Flask(__name__) # Load configuration -CONFIG_FILE = os.environ.get('MCTL_CONFIG', 'config.json') +CONFIG_FILE = os.environ.get('MCTL_CONFIG', 'data/config.json') config = load_config(CONFIG_FILE) # Secret key: generate once and persist in config @@ -215,10 +218,7 @@ @app.route('/media-list', methods=['GET']) @login_required def media_list(): - files = get_media_from_db() - if not files: - scan_medialib(config, GStorage, socketio) - files = [] + files = get_media_from_db() or [] if GStorage["scaning_state"] == "inprogress": response = {"status": "scaning", "data": files} diff --git a/config.json b/config.json deleted file mode 100644 index a2cbfed..0000000 --- a/config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "directories": [ - "/home/gmikcon/media-storage/test-data", - "/home/gmikcon/media-storage/NetTera8/films", - "/home/gmikcon/media-storage/NetTera8/serials", - "/home/gmikcon/media-storage/firstraid5/films", - "/home/gmikcon/media-storage/firstraid5/serials", - "/home/gmikcon/media-storage/secraid5_storage/films", - "/home/gmikcon/media-storage/secraid5_storage/serials" - ], - "allowed_formats": [ - ".mp4", - ".m4p", - ".m4v", - ".mkv", - ".webm", - ".flv", - ".vob", - ".ogv", - ".amv", - ".mng", - ".avi", - ".mov" - ], - "cache_dir": "cache", - "transcoded_directory": "/home/gmikcon/media-storage/transcoded", - "audio_tracks_directory": "/home/gmikcon/media-storage/audio-tracks", - "secret_key": "aa50a9f8d26a9c4e0756ea7d595ea947b728a025cfd418dc2c3d0b9e4f56a0e6" -} \ No newline at end of file diff --git a/db.py b/db.py index d23ae5f..79555a8 100644 --- a/db.py +++ b/db.py @@ -1,10 +1,13 @@ import sqlite3 import json import os -from datetime import datetime +from datetime import datetime, timezone from contextlib import contextmanager -DB_PATH = os.environ.get('MCTL_DB_PATH', 'medialib.db') +DB_PATH = os.environ.get('MCTL_DB_PATH', 'data/medialib.db') + + +os.makedirs(os.path.dirname(DB_PATH) or '.', exist_ok=True) @contextmanager @@ -101,7 +104,7 @@ def upsert_file(path, name, size_bytes, media_info): - now = datetime.utcnow().isoformat() + now = datetime.now(timezone.utc).isoformat() media_info_json = json.dumps(media_info) if media_info is not None else None with get_connection() as conn: conn.execute(""" @@ -130,7 +133,7 @@ def create_operation(file_id, op_type, params, snapshot_before, backup_path): - now = datetime.utcnow().isoformat() + now = datetime.now(timezone.utc).isoformat() with get_connection() as conn: cursor = conn.execute(""" INSERT INTO operations (file_id, type, started_at, status, params, snapshot_before, backup_path) @@ -148,7 +151,7 @@ def update_operation(operation_id, status): - now = datetime.utcnow().isoformat() + now = datetime.now(timezone.utc).isoformat() with get_connection() as conn: conn.execute( "UPDATE operations SET status = ?, finished_at = ? WHERE id = ?", @@ -182,7 +185,7 @@ def create_audio_track(source_file_id, track_index, title, language, codec, bitrate, channels, path): - now = datetime.utcnow().isoformat() + now = datetime.now(timezone.utc).isoformat() with get_connection() as conn: cursor = conn.execute(""" INSERT INTO audio_tracks (source_file_id, track_index, title, language, codec, bitrate, channels, path, created_at) @@ -263,7 +266,7 @@ def create_user(username, password_hash, is_superadmin=False): - now = datetime.utcnow().isoformat() + now = datetime.now(timezone.utc).isoformat() with get_connection() as conn: conn.execute( "INSERT INTO users (username, password_hash, is_superadmin, created_at) VALUES (?, ?, ?, ?)", @@ -354,7 +357,7 @@ 'percent_saved': percent_saved, } - now = datetime.utcnow().isoformat() + now = datetime.now(timezone.utc).isoformat() with get_connection() as conn: conn.execute(""" INSERT INTO app_stats (key, value, updated_at) VALUES ('transcoding_stats', ?, ?) @@ -366,7 +369,7 @@ def update_file_media_info(path, size_bytes, media_info): - now = datetime.utcnow().isoformat() + now = datetime.now(timezone.utc).isoformat() with get_connection() as conn: conn.execute(""" UPDATE files SET size_bytes = ?, media_info = ?, last_seen = ? WHERE path = ? @@ -375,7 +378,7 @@ def create_notification(user_id, notif_type, title, message): - now = datetime.utcnow().isoformat() + now = datetime.now(timezone.utc).isoformat() with get_connection() as conn: cursor = conn.execute( "INSERT INTO notifications (user_id, type, title, message, created_at) VALUES (?, ?, ?, ?, ?)", diff --git a/docker-compose.yml b/docker-compose.yml index 815f192..6cda56c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,20 @@ MCTL_DB_PATH: /data/medialib.db MCTL_CONFIG: /data/config.json volumes: - # Persistent app data (db, config, cache, thumbnails, output dirs) - ./data:/data - # Mount your media directories (read-only recommended): - # - /path/to/your/media:/media:ro + # Mount your media directories at the same path as on the host + # so config.json paths require no changes: + # - /your/media-storage:/your/media-storage + # AMD / Intel GPU (VAAPI, QSV) + devices: + - /dev/dri:/dev/dri + # NVIDIA GPU (NVENC) — requires nvidia-container-toolkit on the host: + # https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] restart: unless-stopped diff --git a/mediascan.py b/mediascan.py index 3a02283..0e82914 100644 --- a/mediascan.py +++ b/mediascan.py @@ -7,6 +7,7 @@ def load_config(config_file): + os.makedirs(os.path.dirname(config_file) or '.', exist_ok=True) if os.path.exists(config_file): with open(config_file, 'r') as f: return json.load(f) @@ -159,6 +160,8 @@ def get_single_media_by_path(path): from db import get_file_by_path + if not os.path.exists(path): + return None media_info = get_media_info_with_ffprobe(path) size, size_unit, size_bytes = human_readable_size(os.path.getsize(path)) db_file = get_file_by_path(path) diff --git a/requirements.txt b/requirements.txt index a85de7a..6ca5662 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ bidict==0.23.1 +gevent>=24.2.1 +gevent-websocket>=0.10.1 blinker==1.9.0 click==8.1.8 ffmpeg-python==0.2.0 diff --git a/scss/main.scss b/scss/main.scss index 6b16ecc..b0e5f4b 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -777,7 +777,7 @@ opacity: 0; transform: translateX(16px); transition: opacity 0.25s, transform 0.25s; - max-width: 360px; + max-width: 440px; font-size: 0.83rem; &.show { @@ -787,7 +787,7 @@ .notif-toast-header { display: flex; - align-items: center; + align-items: flex-start; gap: 7px; } @@ -802,6 +802,9 @@ font-weight: 400; letter-spacing: 0.02em; flex-grow: 1; + min-width: 0; + word-break: break-all; + overflow-wrap: anywhere; } .notif-toast-msg { diff --git a/static/css/main.css.map b/static/css/main.css.map deleted file mode 100644 index 35a1837..0000000 --- a/static/css/main.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["main.scss","components/media-list.scss","components/errors-center.scss","components/file-history.scss","components/thumbnails.scss"],"names":[],"mappings":"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","file":"main.css","sourcesContent":["// Tokyo Night palette\n:root {\n --tn-bg: #1a1b26;\n --tn-bg-dark: #16161e;\n --tn-bg-card: #1f2335;\n --tn-border: #292e42;\n --tn-fg: #c0caf5;\n --tn-fg-dim: #a9b1d6;\n --tn-muted: #565f89;\n --tn-blue: #7aa2f7;\n --tn-cyan: #7dcfff;\n --tn-purple: #bb9af7;\n --tn-green: #9ece6a;\n --tn-yellow: #e0af68;\n --tn-red: #f7768e;\n}\n\n// Bootstrap muted override\n.text-muted { color: var(--tn-fg-dim) !important; }\n\n// Force line breaks for long unbreakable strings (filenames, paths)\nh1, h2, h3, h4, h5, h6,\n.filename a,\n.filepath,\n.stream-title,\ntd,\n.file a,\n.modal-body strong {\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n\n// Base\n* { box-sizing: border-box; }\n\nbody {\n font-family: 'JetBrains Mono', monospace;\n background-color: var(--tn-bg);\n color: var(--tn-fg);\n font-size: 1rem;\n}\n\na {\n text-decoration: none;\n color: var(--tn-blue);\n &:hover { color: var(--tn-cyan); }\n}\n\n// Headings\nh1, h2, h3, h4, h5, h6 {\n color: var(--tn-blue);\n letter-spacing: 0.04em;\n font-weight: 400;\n}\n\n// Cards\n.card {\n background-color: var(--tn-bg-card);\n border: 1px solid var(--tn-border);\n border-radius: 8px;\n transition: border-color 0.2s, box-shadow 0.2s;\n\n .card-header {\n background-color: var(--tn-bg-dark);\n border-bottom: 1px solid var(--tn-border);\n color: var(--tn-blue);\n letter-spacing: 0.04em;\n }\n\n .card-body { color: var(--tn-fg); }\n}\n\n// Tables\n.table {\n color: var(--tn-fg);\n border-color: var(--tn-border);\n\n thead th {\n color: var(--tn-blue);\n font-weight: 400;\n border-color: var(--tn-border);\n letter-spacing: 0.04em;\n background-color: var(--tn-bg-dark);\n }\n\n td {\n color: var(--tn-fg);\n border-color: var(--tn-border);\n padding: 6px 10px;\n }\n\n &.table-striped > tbody > tr:nth-of-type(odd) > * {\n background-color: rgba(41, 46, 66, 0.4);\n color: var(--tn-fg);\n }\n\n &.table-hover > tbody > tr:hover > * {\n background-color: rgba(122, 162, 247, 0.08);\n color: var(--tn-fg);\n }\n}\n\n// DataTables overrides\n.dataTables_wrapper {\n color: var(--tn-fg);\n\n .dataTables_filter input,\n .dataTables_length select {\n background-color: var(--tn-bg-dark);\n border: 1px solid var(--tn-border);\n color: var(--tn-fg);\n border-radius: 4px;\n padding: 2px 8px;\n font-family: 'JetBrains Mono', monospace;\n outline: none;\n &:focus { border-color: var(--tn-blue); }\n }\n\n .dataTables_info,\n .dataTables_filter label,\n .dataTables_length label { color: var(--tn-muted); }\n\n .dataTables_paginate .paginate_button {\n color: var(--tn-muted) !important;\n border-radius: 4px !important;\n &:hover { background: var(--tn-bg-card) !important; color: var(--tn-blue) !important; border-color: var(--tn-border) !important; }\n &.current { background: var(--tn-bg-card) !important; color: var(--tn-blue) !important; border-color: var(--tn-border) !important; }\n &.disabled { color: var(--tn-border) !important; }\n }\n}\n\n// Buttons\n.btn {\n font-family: 'JetBrains Mono', monospace;\n border-radius: 4px;\n letter-spacing: 0.02em;\n\n &.border-spinner { margin-right: 2px; position: relative; top: 1px; }\n &.inprogress { display: inline-flex; gap: 6px; align-items: center; }\n}\n\n.btn-primary {\n background-color: var(--tn-blue);\n border-color: var(--tn-blue);\n color: var(--tn-bg-dark);\n &:hover { background-color: var(--tn-cyan); border-color: var(--tn-cyan); color: var(--tn-bg-dark); }\n}\n\n.btn-warning {\n background-color: var(--tn-yellow);\n border-color: var(--tn-yellow);\n color: var(--tn-bg-dark);\n &:hover { background-color: #f0c070; border-color: #f0c070; color: var(--tn-bg-dark); }\n}\n\n.btn-success {\n background-color: var(--tn-green);\n border-color: var(--tn-green);\n color: var(--tn-bg-dark);\n &:hover { background-color: #acd87a; border-color: #acd87a; color: var(--tn-bg-dark); }\n}\n\n.btn-danger {\n background-color: var(--tn-red);\n border-color: var(--tn-red);\n color: var(--tn-bg-dark);\n &:hover { background-color: #f98a9e; border-color: #f98a9e; color: var(--tn-bg-dark); }\n}\n\n.btn-outline-secondary {\n border-color: var(--tn-border);\n color: var(--tn-fg-dim);\n background-color: transparent;\n &:hover { background-color: var(--tn-bg-card); border-color: var(--tn-blue); color: var(--tn-blue); box-shadow: 0 0 12px rgba(122,162,247,0.15); }\n}\n\n.btn-outline-primary {\n border-color: var(--tn-blue);\n color: var(--tn-blue);\n background-color: transparent;\n &:hover { background-color: rgba(122,162,247,0.1); color: var(--tn-blue); }\n}\n\n.btn-close {\n filter: invert(1) brightness(0.6);\n &:hover { filter: invert(1) brightness(1); }\n}\n\n// Forms\n.form-select, .form-control {\n background-color: var(--tn-bg-dark);\n border: 1px solid var(--tn-border);\n color: var(--tn-fg);\n font-family: 'JetBrains Mono', monospace;\n border-radius: 4px;\n\n &:focus {\n background-color: var(--tn-bg-dark);\n border-color: var(--tn-blue);\n color: var(--tn-fg);\n box-shadow: 0 0 0 2px rgba(122,162,247,0.15);\n }\n\n option { background-color: var(--tn-bg-card); }\n}\n\n.form-check-input {\n background-color: var(--tn-bg-dark);\n border-color: var(--tn-border);\n &:checked { background-color: var(--tn-blue); border-color: var(--tn-blue); }\n &:focus { box-shadow: 0 0 0 2px rgba(122,162,247,0.15); border-color: var(--tn-blue); }\n}\n\n.form-check-label { color: var(--tn-fg-dim); }\n\n.input-group-text {\n background-color: var(--tn-bg-dark);\n border-color: var(--tn-border);\n color: var(--tn-muted);\n font-family: 'JetBrains Mono', monospace;\n}\n\n// Alerts\n.alert {\n background-color: var(--tn-bg-card);\n border-color: var(--tn-border);\n color: var(--tn-fg);\n\n &.alert-dark {\n background-color: var(--tn-bg-dark);\n border-color: var(--tn-border);\n color: var(--tn-fg-dim);\n }\n}\n\n// Badges\n.badge {\n font-family: 'JetBrains Mono', monospace;\n font-weight: 400;\n\n &.bg-success { background-color: var(--tn-green) !important; color: var(--tn-bg-dark); }\n &.bg-danger { background-color: var(--tn-red) !important; color: var(--tn-bg-dark); }\n &.bg-warning { background-color: var(--tn-yellow) !important; color: var(--tn-bg-dark); }\n &.bg-secondary { background-color: var(--tn-muted) !important; color: var(--tn-fg); }\n &.bg-light { background-color: var(--tn-bg-dark) !important; color: var(--tn-fg) !important; border-color: var(--tn-border) !important; }\n}\n\n// Modal\n.modal-content {\n background-color: var(--tn-bg-card);\n border: 1px solid var(--tn-border);\n border-radius: 8px;\n color: var(--tn-fg);\n}\n\n.modal-header {\n background-color: var(--tn-bg-dark);\n border-bottom: 1px solid var(--tn-border);\n color: var(--tn-blue);\n}\n\n.modal-footer {\n background-color: var(--tn-bg-dark);\n border-top: 1px solid var(--tn-border);\n color: var(--tn-muted);\n}\n\n// List group\n.list-group-item {\n background-color: var(--tn-bg-card);\n border-color: var(--tn-border);\n color: var(--tn-fg);\n\n &.task {\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n\n .file {\n display: flex;\n flex-direction: row;\n gap: 8px;\n align-items: center;\n min-width: 0;\n\n a { min-width: 0; }\n }\n }\n}\n\n// Progress bar\n.progress {\n background-color: var(--tn-bg-dark);\n border: 1px solid var(--tn-border);\n\n .progress-bar {\n background-color: var(--tn-blue);\n }\n}\n\n// Spinners\n.loader {\n width: 48px;\n height: 48px;\n border: 5px solid var(--tn-border);\n border-bottom-color: var(--tn-blue);\n border-radius: 50%;\n display: inline-block;\n box-sizing: border-box;\n animation: rotation 1s linear infinite;\n}\n\n@keyframes rotation {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n}\n\n.loading-spinner-container {\n width: 100%;\n margin: 50px auto;\n text-align: center;\n}\n\n.sm-spinner { width: 18px; height: 18px; border-width: 3px; }\n\n// Nav brand\n.nav-brand {\n display: flex;\n align-items: center;\n line-height: 1;\n}\n\n.nav-icon-btn {\n background-color: var(--tn-bg-card);\n border: 1px solid var(--tn-border);\n color: var(--tn-muted);\n border-radius: 50% !important;\n width: 52px;\n height: 52px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: border-color 0.2s, box-shadow 0.2s, color 0.2s;\n &:hover {\n border-color: var(--tn-blue);\n color: var(--tn-blue);\n box-shadow: 0 0 12px rgba(122,162,247,0.25);\n }\n}\n\n// Header nav button\n.open-transcoding-tasks {\n background-color: var(--tn-bg-card);\n border: 1px solid var(--tn-border);\n color: var(--tn-muted);\n border-radius: 50% !important;\n width: 52px;\n height: 52px;\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n transition: border-color 0.2s, box-shadow 0.2s, color 0.2s;\n\n &:hover {\n border-color: var(--tn-blue);\n color: var(--tn-blue);\n box-shadow: 0 0 12px rgba(122,162,247,0.25);\n }\n\n .total-tasks:not(:empty) {\n position: absolute;\n top: -6px;\n right: -6px;\n background-color: var(--tn-yellow);\n color: var(--tn-bg-dark);\n font-size: 0.75rem;\n font-weight: 700;\n min-width: 20px;\n height: 20px;\n border-radius: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 5px;\n line-height: 1;\n }\n}\n\n// Transcoding form\n.transcodate-form-container {\n margin-top: 20px;\n display: flex;\n flex-wrap: wrap;\n flex-direction: row;\n gap: 10px;\n\n > .form-control {\n display: flex;\n flex-wrap: nowrap;\n flex-direction: row;\n justify-content: flex-start;\n gap: 10px;\n align-items: center;\n background: transparent;\n border: none;\n padding: 0;\n\n > strong {\n white-space: nowrap;\n display: inline-block;\n width: 110px;\n color: var(--tn-muted);\n font-weight: 400;\n font-size: 0.875rem;\n letter-spacing: 0.02em;\n }\n }\n\n .arrow-to { color: var(--tn-muted); }\n\n .btns { width: 100%; }\n}\n\n.crf-range { margin-left: 5px; color: var(--tn-muted); }\n\n#progress {\n margin-top: 20px;\n margin-bottom: 0;\n font-size: 0.85rem;\n color: var(--tn-fg-dim);\n}\n\n// Modal empty state\n.empty-tasks-message { color: var(--tn-fg-dim); }\n\n// Media list file details\n.filename a {\n color: var(--tn-fg);\n font-weight: 400;\n &:hover { color: var(--tn-blue); }\n}\n\n.filepath {\n font-size: 0.78rem;\n color: var(--tn-muted);\n margin-bottom: 6px;\n}\n\n.filedetails {\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n\n.streams-section {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n}\n\n.stream-label {\n font-size: 0.7rem;\n color: var(--tn-muted);\n letter-spacing: 0.06em;\n text-transform: uppercase;\n min-width: 36px;\n padding-top: 3px;\n flex-shrink: 0;\n}\n\n.stream-rows {\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n\n.stream-row {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 4px;\n}\n\n.stream-bitrate {\n font-size: 0.75rem;\n color: var(--tn-muted);\n}\n\n.stream-title {\n font-size: 0.75rem;\n color: var(--tn-fg-dim);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 380px;\n}\n\n// Language badges\n.lang-badge {\n font-size: 0.68rem;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n min-width: 30px;\n text-align: center;\n}\n.lang-ukr { background-color: var(--tn-cyan) !important; color: var(--tn-bg-dark); }\n.lang-eng { background-color: var(--tn-blue) !important; color: var(--tn-bg-dark); }\n.lang-rus { background-color: var(--tn-red) !important; color: var(--tn-bg-dark); }\n.lang-jpn { background-color: var(--tn-purple) !important; color: var(--tn-bg-dark); }\n.lang-deu,\n.lang-fra,\n.lang-spa,\n.lang-ita,\n.lang-pol { background-color: var(--tn-green) !important; color: var(--tn-bg-dark); }\n.lang-und { background-color: var(--tn-muted) !important; color: var(--tn-fg); }\n\n// Footer stats\n.footer {\n border-top: 1px solid var(--tn-border);\n padding: 16px 0 20px;\n}\n\n.footer-stats {\n display: flex;\n align-items: center;\n gap: 20px;\n flex-wrap: wrap;\n}\n\n.footer-stat {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.footer-stat-icon {\n color: var(--tn-muted);\n font-size: 0.95rem;\n}\n\n.footer-stat-value {\n color: var(--tn-blue);\n font-size: 0.9rem;\n letter-spacing: 0.02em;\n}\n\n.footer-stat-label {\n color: var(--tn-muted);\n font-size: 0.78rem;\n letter-spacing: 0.03em;\n}\n\n.footer-stat-divider {\n width: 1px;\n height: 16px;\n background-color: var(--tn-border);\n}\n\n// Nav logo\n.nav-logo {\n height: 32px;\n width: auto;\n display: block;\n transition: opacity 0.2s, filter 0.2s;\n filter: brightness(0.85);\n\n .nav-brand:hover & {\n opacity: 1;\n filter: brightness(1) drop-shadow(0 0 8px rgba(122, 162, 247, 0.5));\n }\n}\n\n// Auth logo\n.auth-logo {\n max-height: 100px;\n width: auto;\n display: block;\n margin: 0 auto;\n}\n\n// Nav user info\n.nav-user-divider {\n width: 1px;\n height: 24px;\n background-color: var(--tn-border);\n margin: 0 4px;\n flex-shrink: 0;\n}\n\n.nav-username {\n color: var(--tn-muted);\n font-size: 0.78rem;\n letter-spacing: 0.04em;\n white-space: nowrap;\n line-height: 1;\n}\n\n// Auth pages\n.auth-page {\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 2rem 1rem;\n}\n\n.auth-card {\n background-color: var(--tn-bg-card);\n border: 1px solid var(--tn-border);\n border-radius: 10px;\n padding: 2.5rem 2rem;\n width: 100%;\n max-width: 380px;\n}\n\n.auth-logo-space {\n height: 100px;\n margin-bottom: 1.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.auth-title {\n color: var(--tn-blue);\n font-size: 1.15rem;\n letter-spacing: 0.06em;\n font-weight: 400;\n margin-bottom: 0.25rem;\n text-align: center;\n}\n\n.auth-subtitle {\n color: var(--tn-muted);\n font-size: 0.78rem;\n text-align: center;\n margin-bottom: 1.5rem;\n letter-spacing: 0.03em;\n}\n\n.auth-field {\n margin-bottom: 1.1rem;\n\n label {\n display: block;\n color: var(--tn-muted);\n font-size: 0.78rem;\n letter-spacing: 0.04em;\n margin-bottom: 0.3rem;\n }\n}\n\n.auth-input-wrap {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.auth-input {\n width: 100%;\n background-color: var(--tn-bg-dark);\n border: 1px solid var(--tn-border);\n border-radius: 6px;\n color: var(--tn-fg);\n font-family: 'JetBrains Mono', monospace;\n font-size: 0.9rem;\n padding: 0.55rem 2.4rem 0.55rem 0.75rem;\n outline: none;\n transition: border-color 0.2s;\n\n &:focus {\n border-color: var(--tn-blue);\n }\n\n // standalone input (no wrap)\n &:not(.auth-input-wrap .auth-input) {\n padding-right: 0.75rem;\n }\n}\n\n.auth-eye-btn {\n position: absolute;\n right: 0.55rem;\n background: none;\n border: none;\n color: var(--tn-muted);\n cursor: pointer;\n padding: 0;\n line-height: 1;\n font-size: 1rem;\n\n &:hover { color: var(--tn-fg-dim); }\n}\n\n.auth-btn {\n width: 100%;\n margin-top: 0.5rem;\n padding: 0.6rem;\n background-color: var(--tn-blue);\n color: var(--tn-bg-dark);\n border: none;\n border-radius: 6px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 0.9rem;\n letter-spacing: 0.04em;\n cursor: pointer;\n transition: background-color 0.2s;\n\n &:hover { background-color: var(--tn-cyan); }\n &:disabled { opacity: 0.5; cursor: default; }\n}\n\n.auth-error {\n background-color: rgba(247, 118, 142, 0.12);\n border: 1px solid var(--tn-red);\n border-radius: 6px;\n color: var(--tn-red);\n font-size: 0.82rem;\n padding: 0.5rem 0.75rem;\n margin-bottom: 1rem;\n}\n\n.auth-success {\n background-color: rgba(158, 206, 106, 0.12);\n border: 1px solid var(--tn-green);\n border-radius: 6px;\n color: var(--tn-green);\n font-size: 0.82rem;\n padding: 0.5rem 0.75rem;\n margin-bottom: 1rem;\n}\n\n// Scanning message\n.scaning-process-container {\n font-size: 0.85rem;\n color: var(--tn-muted);\n background-color: var(--tn-bg-dark);\n border-color: var(--tn-border);\n word-break: break-all;\n overflow-wrap: anywhere;\n}\n\n// ── Notification bell badge ────────────────────────────────────────────────────\n.open-notifications {\n position: relative;\n\n .notif-badge:not(:empty) {\n position: absolute;\n top: -6px;\n right: -6px;\n background-color: var(--tn-red);\n color: var(--tn-bg-dark);\n font-size: 0.7rem;\n font-weight: 700;\n min-width: 18px;\n height: 18px;\n border-radius: 9px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 4px;\n line-height: 1;\n }\n}\n\n// ── Notification toast ─────────────────────────────────────────────────────────\n.notif-toast {\n background-color: var(--tn-bg-card);\n border: 1px solid var(--tn-border);\n border-left: 3px solid var(--notif-color, var(--tn-blue));\n border-radius: 6px;\n padding: 10px 12px;\n margin-bottom: 8px;\n opacity: 0;\n transform: translateX(16px);\n transition: opacity 0.25s, transform 0.25s;\n max-width: 360px;\n font-size: 0.83rem;\n\n &.show {\n opacity: 1;\n transform: translateX(0);\n }\n\n .notif-toast-header {\n display: flex;\n align-items: center;\n gap: 7px;\n }\n\n .notif-toast-icon {\n color: var(--notif-color, var(--tn-blue));\n flex-shrink: 0;\n font-size: 0.9rem;\n }\n\n .notif-toast-title {\n color: var(--tn-fg);\n font-weight: 400;\n letter-spacing: 0.02em;\n flex-grow: 1;\n }\n\n .notif-toast-msg {\n margin-top: 6px;\n color: var(--tn-muted);\n font-size: 0.78rem;\n max-height: 80px;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 3;\n -webkit-box-orient: vertical;\n word-break: break-all;\n overflow-wrap: anywhere;\n }\n}\n\n// ── Transcoding tasks offcanvas ────────────────────────────────────────────────\n.transcoding-offcanvas {\n background-color: var(--tn-bg-card);\n border-left: 1px solid var(--tn-border);\n width: 480px !important;\n font-family: 'JetBrains Mono', monospace;\n\n .offcanvas-header {\n background-color: var(--tn-bg-dark);\n border-bottom: 1px solid var(--tn-border);\n padding: 16px 20px;\n\n .offcanvas-title {\n color: var(--tn-blue);\n font-size: 0.95rem;\n letter-spacing: 0.04em;\n font-weight: 400;\n }\n }\n\n .transcoding-toolbar {\n border-bottom: 1px solid var(--tn-border);\n background-color: var(--tn-bg-dark);\n flex-shrink: 0;\n min-height: 40px;\n }\n\n .transcoding-toolbar-count {\n color: var(--tn-muted);\n font-size: 0.8rem;\n letter-spacing: 0.03em;\n\n .total-tasks { color: var(--tn-yellow); }\n }\n\n .transcoding-tasks-container {\n .list-group-item.task {\n background-color: transparent;\n border-color: rgba(41, 46, 66, 0.6);\n padding: 14px 20px;\n }\n }\n}\n\n// ── Notification offcanvas ─────────────────────────────────────────────────────\n.notifications-offcanvas {\n background-color: var(--tn-bg-card);\n border-left: 1px solid var(--tn-border);\n width: 480px !important;\n font-family: 'JetBrains Mono', monospace;\n\n .offcanvas-header {\n background-color: var(--tn-bg-dark);\n border-bottom: 1px solid var(--tn-border);\n padding: 16px 20px;\n\n .offcanvas-title {\n color: var(--tn-blue);\n font-size: 0.95rem;\n letter-spacing: 0.04em;\n font-weight: 400;\n }\n }\n\n .notif-toolbar {\n border-bottom: 1px solid var(--tn-border);\n background-color: var(--tn-bg-dark);\n flex-shrink: 0;\n padding: 8px 16px;\n\n .btn {\n font-size: 0.8rem;\n padding: 4px 12px;\n }\n }\n}\n\n.notif-list {\n padding: 8px 0;\n}\n\n.notif-item {\n padding: 14px 20px;\n border-bottom: 1px solid rgba(41, 46, 66, 0.5);\n transition: background-color 0.15s, opacity 0.25s;\n border-left: 3px solid transparent;\n\n &.is-unread {\n border-left-color: var(--notif-color, var(--tn-blue));\n background-color: rgba(122, 162, 247, 0.04);\n }\n\n &.removing { opacity: 0; }\n\n &:hover { background-color: rgba(122, 162, 247, 0.05); }\n\n .notif-item-header {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n }\n\n .notif-item-icon {\n color: var(--notif-color, var(--tn-blue));\n font-size: 1rem;\n flex-shrink: 0;\n margin-top: 2px;\n }\n\n .notif-item-meta {\n flex-grow: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 3px;\n }\n\n .notif-item-title {\n color: var(--tn-fg);\n font-size: 0.95rem;\n letter-spacing: 0.02em;\n word-break: break-word;\n }\n\n .notif-item-time {\n color: var(--tn-muted);\n font-size: 0.8rem;\n }\n\n .notif-delete-btn {\n background: none;\n border: none;\n color: var(--tn-muted);\n padding: 0 2px;\n line-height: 1;\n cursor: pointer;\n flex-shrink: 0;\n font-size: 1.1rem;\n opacity: 0;\n transition: color 0.15s, opacity 0.15s;\n\n &:hover { color: var(--tn-red); }\n }\n\n &:hover .notif-delete-btn { opacity: 1; }\n\n .notif-item-msg {\n margin-top: 8px;\n color: var(--tn-muted);\n font-size: 0.86rem;\n word-break: break-all;\n overflow-wrap: anywhere;\n line-height: 1.55;\n }\n\n .notif-expand-btn {\n background: none;\n border: none;\n color: var(--tn-blue);\n font-family: 'JetBrains Mono', monospace;\n font-size: 0.76rem;\n cursor: pointer;\n padding: 3px 0;\n margin-top: 3px;\n\n &:hover { color: var(--tn-cyan); }\n }\n}\n",".component.media-list {\n\t.do-rescan-media-lib {\n\t\t.spinner-border {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\t&.inprogress {\n\t\t\t.spinner-border {\n\t\t\t\tdisplay: block;\n\t\t\t}\n\t\t}\n\t}\n}",".component.errors-center {\n\tposition: fixed;\n\tbottom: 0;\n\twidth: 100%;\n\tmax-width: 500px;\n\tpadding: 0 20px;\n\tbox-sizing: border-box;\n\tleft: calc(50% - 250px);\n\tz-index: 100;\n}","#file-history {\n .history-item {\n padding: 16px 20px;\n\n .params-table {\n margin-top: 10px;\n\n td {\n padding: 4px 8px;\n font-size: .9em;\n }\n\n td:first-child {\n width: 130px;\n color: #666;\n }\n }\n\n .snapshot-before {\n margin-top: 12px;\n\n small {\n font-size: .85em;\n }\n\n .badge {\n font-size: .85em;\n padding: 5px 10px;\n }\n }\n\n & > .d-flex {\n .fw-semibold {\n font-size: 1.05em;\n }\n\n small {\n font-size: .9em;\n }\n }\n\n & > div:last-child small {\n font-size: .9em;\n }\n }\n}\n",".thumbnails-row {\n display: flex;\n gap: 8px;\n\n .thumb-placeholder {\n flex: 1;\n height: 90px;\n background-color: var(--tn-bg-card);\n border: 1px solid var(--tn-border);\n border-radius: 6px;\n animation: thumb-pulse 1.5s ease-in-out infinite;\n }\n\n .thumb-img {\n flex: 1;\n height: 90px;\n object-fit: cover;\n border-radius: 6px;\n border: 1px solid var(--tn-border);\n cursor: pointer;\n transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s;\n\n &:hover {\n border-color: var(--tn-blue);\n box-shadow: 0 0 14px rgba(122, 162, 247, 0.25);\n transform: translateY(-2px);\n }\n }\n}\n\n@keyframes thumb-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 0.7; }\n}\n\n#thumb-lightbox .modal-content {\n background-color: var(--tn-bg-dark);\n border-color: var(--tn-border);\n}\n"]} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..b974873 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1 @@ +:root{--tn-bg:#1a1b26;--tn-bg-dark:#16161e;--tn-bg-card:#1f2335;--tn-border:#292e42;--tn-fg:#c0caf5;--tn-fg-dim:#a9b1d6;--tn-muted:#565f89;--tn-blue:#7aa2f7;--tn-cyan:#7dcfff;--tn-purple:#bb9af7;--tn-green:#9ece6a;--tn-yellow:#e0af68;--tn-red:#f7768e}.text-muted{color:var(--tn-fg-dim)!important}.file a,.filename a,.filepath,.modal-body strong,.stream-title,h1,h2,h3,h4,h5,h6,td{word-break:break-word;overflow-wrap:anywhere}*{box-sizing:border-box}body{font-family:"JetBrains Mono",monospace;background-color:var(--tn-bg);color:var(--tn-fg);font-size:1rem}a{text-decoration:none;color:var(--tn-blue)}a:hover{color:var(--tn-cyan)}h1,h2,h3,h4,h5,h6{color:var(--tn-blue);letter-spacing:.04em;font-weight:400}.card{background-color:var(--tn-bg-card);border:1px solid var(--tn-border);border-radius:8px;transition:border-color .2s,box-shadow .2s}.card .card-header{background-color:var(--tn-bg-dark);border-bottom:1px solid var(--tn-border);color:var(--tn-blue);letter-spacing:.04em}.card .card-body{color:var(--tn-fg)}.table{color:var(--tn-fg);border-color:var(--tn-border)}.table thead th{color:var(--tn-blue);font-weight:400;border-color:var(--tn-border);letter-spacing:.04em;background-color:var(--tn-bg-dark)}.table td{color:var(--tn-fg);border-color:var(--tn-border);padding:6px 10px}.table.table-striped>tbody>tr:nth-of-type(odd)>*{background-color:rgba(41,46,66,.4);color:var(--tn-fg)}.table.table-hover>tbody>tr:hover>*{background-color:rgba(122,162,247,.08);color:var(--tn-fg)}.dataTables_wrapper{color:var(--tn-fg)}.dataTables_wrapper .dataTables_filter input,.dataTables_wrapper .dataTables_length select{background-color:var(--tn-bg-dark);border:1px solid var(--tn-border);color:var(--tn-fg);border-radius:4px;padding:2px 8px;font-family:"JetBrains Mono",monospace;outline:0}.dataTables_wrapper .dataTables_filter input:focus,.dataTables_wrapper .dataTables_length select:focus{border-color:var(--tn-blue)}.dataTables_wrapper .dataTables_filter label,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_length label{color:var(--tn-muted)}.dataTables_wrapper .dataTables_paginate .paginate_button{color:var(--tn-muted)!important;border-radius:4px!important}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{background:var(--tn-bg-card)!important;color:var(--tn-blue)!important;border-color:var(--tn-border)!important}.dataTables_wrapper .dataTables_paginate .paginate_button.current{background:var(--tn-bg-card)!important;color:var(--tn-blue)!important;border-color:var(--tn-border)!important}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled{color:var(--tn-border)!important}.btn{font-family:"JetBrains Mono",monospace;border-radius:4px;letter-spacing:.02em}.btn.border-spinner{margin-right:2px;position:relative;top:1px}.btn.inprogress{display:inline-flex;gap:6px;align-items:center}.btn-primary{background-color:var(--tn-blue);border-color:var(--tn-blue);color:var(--tn-bg-dark)}.btn-primary:hover{background-color:var(--tn-cyan);border-color:var(--tn-cyan);color:var(--tn-bg-dark)}.btn-warning{background-color:var(--tn-yellow);border-color:var(--tn-yellow);color:var(--tn-bg-dark)}.btn-warning:hover{background-color:#f0c070;border-color:#f0c070;color:var(--tn-bg-dark)}.btn-success{background-color:var(--tn-green);border-color:var(--tn-green);color:var(--tn-bg-dark)}.btn-success:hover{background-color:#acd87a;border-color:#acd87a;color:var(--tn-bg-dark)}.btn-danger{background-color:var(--tn-red);border-color:var(--tn-red);color:var(--tn-bg-dark)}.btn-danger:hover{background-color:#f98a9e;border-color:#f98a9e;color:var(--tn-bg-dark)}.btn-outline-secondary{border-color:var(--tn-border);color:var(--tn-fg-dim);background-color:transparent}.btn-outline-secondary:hover{background-color:var(--tn-bg-card);border-color:var(--tn-blue);color:var(--tn-blue);box-shadow:0 0 12px rgba(122,162,247,.15)}.btn-outline-primary{border-color:var(--tn-blue);color:var(--tn-blue);background-color:transparent}.btn-outline-primary:hover{background-color:rgba(122,162,247,.1);color:var(--tn-blue)}.btn-close{filter:invert(1) brightness(.6)}.btn-close:hover{filter:invert(1) brightness(1)}.form-control,.form-select{background-color:var(--tn-bg-dark);border:1px solid var(--tn-border);color:var(--tn-fg);font-family:"JetBrains Mono",monospace;border-radius:4px}.form-control:focus,.form-select:focus{background-color:var(--tn-bg-dark);border-color:var(--tn-blue);color:var(--tn-fg);box-shadow:0 0 0 2px rgba(122,162,247,.15)}.form-control option,.form-select option{background-color:var(--tn-bg-card)}.form-check-input{background-color:var(--tn-bg-dark);border-color:var(--tn-border)}.form-check-input:checked{background-color:var(--tn-blue);border-color:var(--tn-blue)}.form-check-input:focus{box-shadow:0 0 0 2px rgba(122,162,247,.15);border-color:var(--tn-blue)}.form-check-label{color:var(--tn-fg-dim)}.input-group-text{background-color:var(--tn-bg-dark);border-color:var(--tn-border);color:var(--tn-muted);font-family:"JetBrains Mono",monospace}.alert{background-color:var(--tn-bg-card);border-color:var(--tn-border);color:var(--tn-fg)}.alert.alert-dark{background-color:var(--tn-bg-dark);border-color:var(--tn-border);color:var(--tn-fg-dim)}.badge{font-family:"JetBrains Mono",monospace;font-weight:400}.badge.bg-success{background-color:var(--tn-green)!important;color:var(--tn-bg-dark)}.badge.bg-danger{background-color:var(--tn-red)!important;color:var(--tn-bg-dark)}.badge.bg-warning{background-color:var(--tn-yellow)!important;color:var(--tn-bg-dark)}.badge.bg-secondary{background-color:var(--tn-muted)!important;color:var(--tn-fg)}.badge.bg-light{background-color:var(--tn-bg-dark)!important;color:var(--tn-fg)!important;border-color:var(--tn-border)!important}.modal-content{background-color:var(--tn-bg-card);border:1px solid var(--tn-border);border-radius:8px;color:var(--tn-fg)}.modal-header{background-color:var(--tn-bg-dark);border-bottom:1px solid var(--tn-border);color:var(--tn-blue)}.modal-footer{background-color:var(--tn-bg-dark);border-top:1px solid var(--tn-border);color:var(--tn-muted)}.list-group-item{background-color:var(--tn-bg-card);border-color:var(--tn-border);color:var(--tn-fg)}.list-group-item.task{display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:12px}.list-group-item.task .file{display:flex;flex-direction:row;gap:8px;align-items:center;min-width:0}.list-group-item.task .file a{min-width:0}.progress{background-color:var(--tn-bg-dark);border:1px solid var(--tn-border)}.progress .progress-bar{background-color:var(--tn-blue)}.loader{width:48px;height:48px;border:5px solid var(--tn-border);border-bottom-color:var(--tn-blue);border-radius:50%;display:inline-block;box-sizing:border-box;animation:rotation 1s linear infinite}@keyframes rotation{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.loading-spinner-container{width:100%;margin:50px auto;text-align:center}.sm-spinner{width:18px;height:18px;border-width:3px}.nav-brand{display:flex;align-items:center;line-height:1}.nav-icon-btn{background-color:var(--tn-bg-card);border:1px solid var(--tn-border);color:var(--tn-muted);border-radius:50%!important;width:52px;height:52px;display:flex;align-items:center;justify-content:center;transition:border-color .2s,box-shadow .2s,color .2s}.nav-icon-btn:hover{border-color:var(--tn-blue);color:var(--tn-blue);box-shadow:0 0 12px rgba(122,162,247,.25)}.open-transcoding-tasks{background-color:var(--tn-bg-card);border:1px solid var(--tn-border);color:var(--tn-muted);border-radius:50%!important;width:52px;height:52px;display:flex;align-items:center;justify-content:center;position:relative;transition:border-color .2s,box-shadow .2s,color .2s}.open-transcoding-tasks:hover{border-color:var(--tn-blue);color:var(--tn-blue);box-shadow:0 0 12px rgba(122,162,247,.25)}.open-transcoding-tasks .total-tasks:not(:empty){position:absolute;top:-6px;right:-6px;background-color:var(--tn-yellow);color:var(--tn-bg-dark);font-size:.75rem;font-weight:700;min-width:20px;height:20px;border-radius:10px;display:flex;align-items:center;justify-content:center;padding:0 5px;line-height:1}.transcodate-form-container{margin-top:20px;display:flex;flex-wrap:wrap;flex-direction:row;gap:10px}.transcodate-form-container>.form-control{display:flex;flex-wrap:nowrap;flex-direction:row;justify-content:flex-start;gap:10px;align-items:center;background:0 0;border:none;padding:0}.transcodate-form-container>.form-control>strong{white-space:nowrap;display:inline-block;width:110px;color:var(--tn-muted);font-weight:400;font-size:.875rem;letter-spacing:.02em}.transcodate-form-container .arrow-to{color:var(--tn-muted)}.transcodate-form-container .btns{width:100%}.crf-range{margin-left:5px;color:var(--tn-muted)}#progress{margin-top:20px;margin-bottom:0;font-size:.85rem;color:var(--tn-fg-dim)}.empty-tasks-message{color:var(--tn-fg-dim)}.filename a{color:var(--tn-fg);font-weight:400}.filename a:hover{color:var(--tn-blue)}.filepath{font-size:.78rem;color:var(--tn-muted);margin-bottom:6px}.filedetails{display:flex;flex-direction:column;gap:3px}.streams-section{display:flex;align-items:flex-start;gap:8px}.stream-label{font-size:.7rem;color:var(--tn-muted);letter-spacing:.06em;text-transform:uppercase;min-width:36px;padding-top:3px;flex-shrink:0}.stream-rows{display:flex;flex-direction:column;gap:3px}.stream-row{display:flex;align-items:center;flex-wrap:wrap;gap:4px}.stream-bitrate{font-size:.75rem;color:var(--tn-muted)}.stream-title{font-size:.75rem;color:var(--tn-fg-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:380px}.lang-badge{font-size:.68rem;letter-spacing:.04em;text-transform:uppercase;min-width:30px;text-align:center}.lang-ukr{background-color:var(--tn-cyan)!important;color:var(--tn-bg-dark)}.lang-eng{background-color:var(--tn-blue)!important;color:var(--tn-bg-dark)}.lang-rus{background-color:var(--tn-red)!important;color:var(--tn-bg-dark)}.lang-jpn{background-color:var(--tn-purple)!important;color:var(--tn-bg-dark)}.lang-deu,.lang-fra,.lang-ita,.lang-pol,.lang-spa{background-color:var(--tn-green)!important;color:var(--tn-bg-dark)}.lang-und{background-color:var(--tn-muted)!important;color:var(--tn-fg)}.footer{border-top:1px solid var(--tn-border);padding:16px 0 20px}.footer-stats{display:flex;align-items:center;gap:20px;flex-wrap:wrap}.footer-stat{display:flex;align-items:center;gap:6px}.footer-stat-icon{color:var(--tn-muted);font-size:.95rem}.footer-stat-value{color:var(--tn-blue);font-size:.9rem;letter-spacing:.02em}.footer-stat-label{color:var(--tn-muted);font-size:.78rem;letter-spacing:.03em}.footer-stat-divider{width:1px;height:16px;background-color:var(--tn-border)}.nav-logo{height:32px;width:auto;display:block;transition:opacity .2s,filter .2s;filter:brightness(.85)}.nav-brand:hover .nav-logo{opacity:1;filter:brightness(1) drop-shadow(0 0 8px rgba(122, 162, 247, .5))}.auth-logo{max-height:100px;width:auto;display:block;margin:0 auto}.nav-user-divider{width:1px;height:24px;background-color:var(--tn-border);margin:0 4px;flex-shrink:0}.nav-username{color:var(--tn-muted);font-size:.78rem;letter-spacing:.04em;white-space:nowrap;line-height:1}.auth-page{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem 1rem}.auth-card{background-color:var(--tn-bg-card);border:1px solid var(--tn-border);border-radius:10px;padding:2.5rem 2rem;width:100%;max-width:380px}.auth-logo-space{height:100px;margin-bottom:1.5rem;display:flex;align-items:center;justify-content:center}.auth-title{color:var(--tn-blue);font-size:1.15rem;letter-spacing:.06em;font-weight:400;margin-bottom:.25rem;text-align:center}.auth-subtitle{color:var(--tn-muted);font-size:.78rem;text-align:center;margin-bottom:1.5rem;letter-spacing:.03em}.auth-field{margin-bottom:1.1rem}.auth-field label{display:block;color:var(--tn-muted);font-size:.78rem;letter-spacing:.04em;margin-bottom:.3rem}.auth-input-wrap{position:relative;display:flex;align-items:center}.auth-input{width:100%;background-color:var(--tn-bg-dark);border:1px solid var(--tn-border);border-radius:6px;color:var(--tn-fg);font-family:"JetBrains Mono",monospace;font-size:.9rem;padding:.55rem 2.4rem .55rem .75rem;outline:0;transition:border-color .2s}.auth-input:focus{border-color:var(--tn-blue)}.auth-input:not(.auth-input-wrap.auth-input){padding-right:.75rem}.auth-eye-btn{position:absolute;right:.55rem;background:0 0;border:none;color:var(--tn-muted);cursor:pointer;padding:0;line-height:1;font-size:1rem}.auth-eye-btn:hover{color:var(--tn-fg-dim)}.auth-btn{width:100%;margin-top:.5rem;padding:.6rem;background-color:var(--tn-blue);color:var(--tn-bg-dark);border:none;border-radius:6px;font-family:"JetBrains Mono",monospace;font-size:.9rem;letter-spacing:.04em;cursor:pointer;transition:background-color .2s}.auth-btn:hover{background-color:var(--tn-cyan)}.auth-btn:disabled{opacity:.5;cursor:default}.auth-error{background-color:rgba(247,118,142,.12);border:1px solid var(--tn-red);border-radius:6px;color:var(--tn-red);font-size:.82rem;padding:.5rem .75rem;margin-bottom:1rem}.auth-success{background-color:rgba(158,206,106,.12);border:1px solid var(--tn-green);border-radius:6px;color:var(--tn-green);font-size:.82rem;padding:.5rem .75rem;margin-bottom:1rem}.scaning-process-container{font-size:.85rem;color:var(--tn-muted);background-color:var(--tn-bg-dark);border-color:var(--tn-border);word-break:break-all;overflow-wrap:anywhere}.open-notifications{position:relative}.open-notifications .notif-badge:not(:empty){position:absolute;top:-6px;right:-6px;background-color:var(--tn-red);color:var(--tn-bg-dark);font-size:.7rem;font-weight:700;min-width:18px;height:18px;border-radius:9px;display:flex;align-items:center;justify-content:center;padding:0 4px;line-height:1}.notif-toast{background-color:var(--tn-bg-card);border:1px solid var(--tn-border);border-left:3px solid var(--notif-color,var(--tn-blue));border-radius:6px;padding:10px 12px;margin-bottom:8px;opacity:0;transform:translateX(16px);transition:opacity .25s,transform .25s;max-width:440px;font-size:.83rem}.notif-toast.show{opacity:1;transform:translateX(0)}.notif-toast .notif-toast-header{display:flex;align-items:flex-start;gap:7px}.notif-toast .notif-toast-icon{color:var(--notif-color,var(--tn-blue));flex-shrink:0;font-size:.9rem}.notif-toast .notif-toast-title{color:var(--tn-fg);font-weight:400;letter-spacing:.02em;flex-grow:1;min-width:0;word-break:break-all;overflow-wrap:anywhere}.notif-toast .notif-toast-msg{margin-top:6px;color:var(--tn-muted);font-size:.78rem;max-height:80px;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;word-break:break-all;overflow-wrap:anywhere}.transcoding-offcanvas{background-color:var(--tn-bg-card);border-left:1px solid var(--tn-border);width:480px!important;font-family:"JetBrains Mono",monospace}.transcoding-offcanvas .offcanvas-header{background-color:var(--tn-bg-dark);border-bottom:1px solid var(--tn-border);padding:16px 20px}.transcoding-offcanvas .offcanvas-header .offcanvas-title{color:var(--tn-blue);font-size:.95rem;letter-spacing:.04em;font-weight:400}.transcoding-offcanvas .transcoding-toolbar{border-bottom:1px solid var(--tn-border);background-color:var(--tn-bg-dark);flex-shrink:0;min-height:40px}.transcoding-offcanvas .transcoding-toolbar-count{color:var(--tn-muted);font-size:.8rem;letter-spacing:.03em}.transcoding-offcanvas .transcoding-toolbar-count .total-tasks{color:var(--tn-yellow)}.transcoding-offcanvas .transcoding-tasks-container .list-group-item.task{background-color:transparent;border-color:rgba(41,46,66,.6);padding:14px 20px}.notifications-offcanvas{background-color:var(--tn-bg-card);border-left:1px solid var(--tn-border);width:480px!important;font-family:"JetBrains Mono",monospace}.notifications-offcanvas .offcanvas-header{background-color:var(--tn-bg-dark);border-bottom:1px solid var(--tn-border);padding:16px 20px}.notifications-offcanvas .offcanvas-header .offcanvas-title{color:var(--tn-blue);font-size:.95rem;letter-spacing:.04em;font-weight:400}.notifications-offcanvas .notif-toolbar{border-bottom:1px solid var(--tn-border);background-color:var(--tn-bg-dark);flex-shrink:0;padding:8px 16px}.notifications-offcanvas .notif-toolbar .btn{font-size:.8rem;padding:4px 12px}.notif-list{padding:8px 0}.notif-item{padding:14px 20px;border-bottom:1px solid rgba(41,46,66,.5);transition:background-color .15s,opacity .25s;border-left:3px solid transparent}.notif-item.is-unread{border-left-color:var(--notif-color,var(--tn-blue));background-color:rgba(122,162,247,.04)}.notif-item.removing{opacity:0}.notif-item:hover{background-color:rgba(122,162,247,.05)}.notif-item .notif-item-header{display:flex;align-items:flex-start;gap:10px}.notif-item .notif-item-icon{color:var(--notif-color,var(--tn-blue));font-size:1rem;flex-shrink:0;margin-top:2px}.notif-item .notif-item-meta{flex-grow:1;min-width:0;display:flex;flex-direction:column;gap:3px}.notif-item .notif-item-title{color:var(--tn-fg);font-size:.95rem;letter-spacing:.02em;word-break:break-word}.notif-item .notif-item-time{color:var(--tn-muted);font-size:.8rem}.notif-item .notif-delete-btn{background:0 0;border:none;color:var(--tn-muted);padding:0 2px;line-height:1;cursor:pointer;flex-shrink:0;font-size:1.1rem;opacity:0;transition:color .15s,opacity .15s}.notif-item .notif-delete-btn:hover{color:var(--tn-red)}.notif-item:hover .notif-delete-btn{opacity:1}.notif-item .notif-item-msg{margin-top:8px;color:var(--tn-muted);font-size:.86rem;word-break:break-all;overflow-wrap:anywhere;line-height:1.55}.notif-item .notif-expand-btn{background:0 0;border:none;color:var(--tn-blue);font-family:"JetBrains Mono",monospace;font-size:.76rem;cursor:pointer;padding:3px 0;margin-top:3px}.notif-item .notif-expand-btn:hover{color:var(--tn-cyan)}.component.media-list .do-rescan-media-lib .spinner-border{display:none}.component.media-list .do-rescan-media-lib.inprogress .spinner-border{display:block}.component.errors-center{position:fixed;bottom:0;width:100%;max-width:500px;padding:0 20px;box-sizing:border-box;left:calc(50% - 250px);z-index:100}#file-history .history-item{padding:16px 20px}#file-history .history-item .params-table{margin-top:10px}#file-history .history-item .params-table td{padding:4px 8px;font-size:.9em}#file-history .history-item .params-table td:first-child{width:130px;color:#666}#file-history .history-item .snapshot-before{margin-top:12px}#file-history .history-item .snapshot-before small{font-size:.85em}#file-history .history-item .snapshot-before .badge{font-size:.85em;padding:5px 10px}#file-history .history-item>.d-flex .fw-semibold{font-size:1.05em}#file-history .history-item>.d-flex small{font-size:.9em}#file-history .history-item>div:last-child small{font-size:.9em}.thumbnails-row{display:flex;gap:8px}.thumbnails-row .thumb-placeholder{flex:1;height:90px;background-color:var(--tn-bg-card);border:1px solid var(--tn-border);border-radius:6px;animation:thumb-pulse 1.5s ease-in-out infinite}.thumbnails-row .thumb-img{flex:1;height:90px;object-fit:cover;border-radius:6px;border:1px solid var(--tn-border);cursor:pointer;transition:border-color .2s,box-shadow .2s,transform .15s}.thumbnails-row .thumb-img:hover{border-color:var(--tn-blue);box-shadow:0 0 14px rgba(122,162,247,.25);transform:translateY(-2px)}@keyframes thumb-pulse{0%,100%{opacity:.4}50%{opacity:.7}}#thumb-lightbox .modal-content{background-color:var(--tn-bg-dark);border-color:var(--tn-border)} \ No newline at end of file diff --git a/static/js/components/global-transcoding-tasks.js b/static/js/components/global-transcoding-tasks.js index 2e55faf..f8bc9b6 100644 --- a/static/js/components/global-transcoding-tasks.js +++ b/static/js/components/global-transcoding-tasks.js @@ -231,9 +231,6 @@ socket.on("error", data => { cleanupTask(data); - const cmd = data.task.command.join(" "); - const log = data.ffmpeg_log ? `
${data.ffmpeg_log}
` : ""; - pushErrMsg(`${data.message}
${cmd}${log}`); }); socket.on("medialib-scaning-process", () => startScanTitleAnimation()); diff --git a/static/js/components/media-list.js b/static/js/components/media-list.js index 5d90215..7bba882 100644 --- a/static/js/components/media-list.js +++ b/static/js/components/media-list.js @@ -186,8 +186,8 @@ socket.on("medialib-scaning-complete", resp => { if($('#media-table_wrapper').length) { $('#media-table').DataTable().destroy(); - $("#media-table tbody").empty(); } + $("#media-table tbody").empty(); renderMediaList(resp.data); diff --git a/static/js/components/notifications.js b/static/js/components/notifications.js index c64f9a3..baa89e3 100644 --- a/static/js/components/notifications.js +++ b/static/js/components/notifications.js @@ -56,7 +56,7 @@ const icon = _notifIcon(notif.type); const color = _notifColor(notif.type); const msgHtml = notif.message - ? `
${notif.message.replace(/` + ? `
${notif.message.replace(/')}
` : ''; const el = document.createElement('div'); @@ -124,31 +124,17 @@ if (notif.message) { const escaped = notif.message.replace(/'); const preview = longMsg - ? notif.message.slice(0, MSG_THRESHOLD).replace(/') + '…' : escaped; - msgHtml = ` -
- ${preview} -
- ${longMsg ? `` : ''} - `; + const expandBtn = longMsg ? `` : ''; + msgHtml = `
${preview}
${expandBtn}`; } const el = document.createElement('div'); el.className = `notif-item ${isRead ? 'is-read' : 'is-unread'}`; el.dataset.id = notif.id; el.style.setProperty('--notif-color', color); - el.innerHTML = ` -
- -
- ${notif.title.replace(/ - ${time} -
- -
- ${msgHtml} - `; + el.innerHTML = `
${notif.title.replace(/${time}
${msgHtml}`; if (longMsg) { el.querySelector('.notif-expand-btn').addEventListener('click', function() { diff --git a/transcodate.py b/transcodate.py index d0e23d5..0221a2c 100644 --- a/transcodate.py +++ b/transcodate.py @@ -14,8 +14,8 @@ if _find_vaapi_device() is not None: available.append("VAAPI") - # Intel QSV — требует intel-media-sdk/oneVPL и сборки ffmpeg с QSV - if _has_ffmpeg_hwaccel("qsv") and _has_ffmpeg_encoder("h264_qsv"): + # Intel QSV — требует Intel GPU + intel-media-sdk/oneVPL + ffmpeg с QSV + if _has_intel_gpu() and _has_ffmpeg_hwaccel("qsv") and _has_ffmpeg_encoder("h264_qsv"): available.append("QSV") # NVENC — наличие NVIDIA устройства или nvidia-smi @@ -41,12 +41,16 @@ """Возвращает путь к первому рабочему VAAPI render-устройству или None.""" import glob as _glob for device in sorted(_glob.glob("/dev/dri/renderD*")): - result = subprocess.run( - ["vainfo", "--display", "drm", "--device", device], - capture_output=True, timeout=5 - ) - if result.returncode == 0: - return device + try: + result = subprocess.run( + ["vainfo", "--display", "drm", "--device", device], + capture_output=True, timeout=5 + ) + if result.returncode == 0: + return device + except Exception: + # vainfo не установлен или устройство недоступно + break # Fallback: vainfo без явного указания устройства if _cmd_succeeds(["vainfo"]): return "/dev/dri/renderD128" @@ -71,6 +75,19 @@ return False +def _has_intel_gpu(): + try: + result = subprocess.run(["lspci"], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + return any( + ("vga" in line or "display" in line or "3d controller" in line) and "intel" in line + for line in result.stdout.lower().splitlines() + ) + except Exception: + pass + return False + + def _has_amd_gpu(): try: result = subprocess.run(["lspci"], capture_output=True, text=True, timeout=5)