Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9df9dff80 | |||
| dc5f95f7ec | |||
| 9205234b2d | |||
| beb592402e | |||
| dc5092468a | |||
| d03ba848a0 | |||
| c558ce6961 | |||
| c91319de2b | |||
| e4f5ce70b8 | |||
| 8ee7f9fefe | |||
| 4aaff495e1 | |||
| be12f5220b |
@@ -12,7 +12,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"postCreateCommand": "sudo apt update; sudo apt install ffmpeg -y; pip install -r requirements.txt; curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64; chmod +x tailwindcss-linux-x64; mv tailwindcss-linux-x64 tailwindcss",
|
"postCreateCommand": "sudo apt update; sudo apt install ffmpeg -y; pip install -r requirements.txt",
|
||||||
"forwardPorts": [5000],
|
"forwardPorts": [5000],
|
||||||
"remoteUser": "vscode"
|
"remoteUser": "vscode"
|
||||||
}
|
}
|
||||||
@@ -7,10 +7,5 @@ ENV VERSION=${VERSION}
|
|||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
RUN apt-get update && apt-get install -y curl ffmpeg procps
|
RUN apt-get update && apt-get install -y curl ffmpeg procps
|
||||||
RUN rm -rf /var/lib/apt/lists/*
|
RUN rm -rf /var/lib/apt/lists/*
|
||||||
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 \
|
|
||||||
&& chmod +x tailwindcss-linux-x64 \
|
|
||||||
&& mv tailwindcss-linux-x64 tailwindcss \
|
|
||||||
&& ./tailwindcss -i ./static/css/input.css -o ./static/css/output.css --content "./templates/*.html" --minify \
|
|
||||||
&& rm tailwindcss
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app", "--log-level", "info"]
|
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app", "--log-level", "info"]
|
||||||
@@ -1,14 +1,40 @@
|
|||||||
# Encoder
|
# Encoder
|
||||||
|
|
||||||
A Flask-based media encoding application that automatically converts video files from H.264 to H.265 and audio files from FLAC to MP3, with a dark-themed web interface.
|
A Flask-based media encoding application using ffmpeg and a lovely web interface.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Video Encoding**: Converts H.264 videos to H.265 (HEVC) in MKV format
|
- **Video Encoding**: Converts video from H264 to H265 (HEVC) at either original resolution or target resolution
|
||||||
- **Audio Encoding**: Converts FLAC audio files to MP3 320kbps
|
- **Audio Encoding**: Converts audio files to MP3 at your chosen bitrate
|
||||||
- **Watch Folders**: Automatically monitors specified folders for new files
|
- **Watch Folders**: Automatically monitors specified folders for new files
|
||||||
- **Job Queue**: Processes files sequentially with real-time status updates
|
- **Job Queue**: Processes files sequentially with real-time status updates
|
||||||
- **Database Tracking**: Stores job reports and configuration in MySQL
|
- **Database Tracking**: Stores job reports and configuration in MySQL
|
||||||
- **Space Savings**: Tracks file size differences to show storage saved
|
- **Space Savings**: Tracks file size differences to show storage saved
|
||||||
- **Web Interface**: Dark-themed responsive UI with live updates
|
- **Web Interface**: Responsive UI with live updates
|
||||||
- **Job Reports**: Detailed history with pagination and statistics
|
- **Job Reports**: Detailed history with pagination and statistics
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### docker-compose.yaml
|
||||||
|
|
||||||
|
```
|
||||||
|
services:
|
||||||
|
encoder:
|
||||||
|
image: cr.jdbnet.co.uk/public/encoder:latest
|
||||||
|
container_name: encoder
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 5000:5000
|
||||||
|
environment:
|
||||||
|
- DB_HOST=your-mysql-or-mariadb-host
|
||||||
|
- DB_USER=database-user
|
||||||
|
- DB_PASSWORD=database-password
|
||||||
|
- DB_NAME=encoder
|
||||||
|
- SECRET_KEY=set-your-secret-key-here
|
||||||
|
volumes:
|
||||||
|
- encoder-temp:/temp
|
||||||
|
- /path/to/your/local/media:/media
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
encoder-temp:
|
||||||
|
```
|
||||||
@@ -9,6 +9,7 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import logging
|
import logging
|
||||||
|
from decimal import Decimal
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import shutil
|
import shutil
|
||||||
@@ -314,6 +315,180 @@ class DatabaseManager:
|
|||||||
conn.close()
|
conn.close()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def get_reports_dashboard_data(self):
|
||||||
|
"""Aggregate data for the reports dashboard"""
|
||||||
|
conn = self.get_connection()
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_jobs,
|
||||||
|
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_jobs,
|
||||||
|
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_jobs,
|
||||||
|
SUM(CASE WHEN status = 'skipped' THEN 1 ELSE 0 END) as skipped_jobs,
|
||||||
|
SUM(CASE WHEN status = 'success' THEN size_saved ELSE 0 END) as total_saved,
|
||||||
|
SUM(CASE WHEN status = 'success' THEN original_size ELSE 0 END) as total_original,
|
||||||
|
SUM(CASE WHEN status = 'success' THEN encoded_size ELSE 0 END) as total_encoded,
|
||||||
|
AVG(CASE WHEN status = 'success' THEN processing_time END) as avg_processing_time
|
||||||
|
FROM job_reports
|
||||||
|
""")
|
||||||
|
summary_raw = cursor.fetchone() or {}
|
||||||
|
summary = {
|
||||||
|
'total_jobs': int(summary_raw.get('total_jobs') or 0),
|
||||||
|
'success_jobs': int(summary_raw.get('success_jobs') or 0),
|
||||||
|
'failed_jobs': int(summary_raw.get('failed_jobs') or 0),
|
||||||
|
'skipped_jobs': int(summary_raw.get('skipped_jobs') or 0),
|
||||||
|
'total_saved': float(summary_raw.get('total_saved') or 0),
|
||||||
|
'total_original': float(summary_raw.get('total_original') or 0),
|
||||||
|
'total_encoded': float(summary_raw.get('total_encoded') or 0),
|
||||||
|
'avg_processing_time': float(summary_raw.get('avg_processing_time') or 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT DATE(created_at) as day, SUM(size_saved) as total_saved
|
||||||
|
FROM job_reports
|
||||||
|
WHERE status = 'success'
|
||||||
|
GROUP BY day
|
||||||
|
ORDER BY day
|
||||||
|
""")
|
||||||
|
savings_over_time = [
|
||||||
|
{
|
||||||
|
'day': row.get('day').strftime('%Y-%m-%d') if row.get('day') else None,
|
||||||
|
'total_saved': float(row.get('total_saved') or 0)
|
||||||
|
}
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT DATE(created_at) as day,
|
||||||
|
COUNT(*) as total_jobs,
|
||||||
|
SUM(status = 'success') as success_jobs,
|
||||||
|
SUM(status = 'failed') as failed_jobs,
|
||||||
|
SUM(status = 'skipped') as skipped_jobs
|
||||||
|
FROM job_reports
|
||||||
|
GROUP BY day
|
||||||
|
ORDER BY day
|
||||||
|
""")
|
||||||
|
jobs_over_time = [
|
||||||
|
{
|
||||||
|
'day': row.get('day').strftime('%Y-%m-%d') if row.get('day') else None,
|
||||||
|
'total_jobs': int(row.get('total_jobs') or 0),
|
||||||
|
'success_jobs': int(row.get('success_jobs') or 0),
|
||||||
|
'failed_jobs': int(row.get('failed_jobs') or 0),
|
||||||
|
'skipped_jobs': int(row.get('skipped_jobs') or 0)
|
||||||
|
}
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT DATE(created_at) as day,
|
||||||
|
AVG(processing_time) as avg_processing_time
|
||||||
|
FROM job_reports
|
||||||
|
WHERE status = 'success' AND processing_time IS NOT NULL
|
||||||
|
GROUP BY day
|
||||||
|
ORDER BY day
|
||||||
|
""")
|
||||||
|
processing_time_over_time = [
|
||||||
|
{
|
||||||
|
'day': row.get('day').strftime('%Y-%m-%d') if row.get('day') else None,
|
||||||
|
'avg_processing_time': float(row.get('avg_processing_time') or 0)
|
||||||
|
}
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT encoder_type,
|
||||||
|
COUNT(*) as total_jobs,
|
||||||
|
SUM(CASE WHEN status = 'success' THEN size_saved ELSE 0 END) as total_saved
|
||||||
|
FROM job_reports
|
||||||
|
GROUP BY encoder_type
|
||||||
|
""")
|
||||||
|
savings_by_encoder = [
|
||||||
|
{
|
||||||
|
'encoder_type': row.get('encoder_type'),
|
||||||
|
'total_jobs': int(row.get('total_jobs') or 0),
|
||||||
|
'total_saved': float(row.get('total_saved') or 0)
|
||||||
|
}
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT original_format, encoded_format,
|
||||||
|
COUNT(*) as total_jobs,
|
||||||
|
SUM(CASE WHEN status = 'success' THEN size_saved ELSE 0 END) as total_saved
|
||||||
|
FROM job_reports
|
||||||
|
GROUP BY original_format, encoded_format
|
||||||
|
ORDER BY total_jobs DESC
|
||||||
|
LIMIT 10
|
||||||
|
""")
|
||||||
|
format_breakdown = [
|
||||||
|
{
|
||||||
|
'original_format': row.get('original_format'),
|
||||||
|
'encoded_format': row.get('encoded_format'),
|
||||||
|
'total_jobs': int(row.get('total_jobs') or 0),
|
||||||
|
'total_saved': float(row.get('total_saved') or 0)
|
||||||
|
}
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT file_path, size_saved
|
||||||
|
FROM job_reports
|
||||||
|
WHERE status = 'success' AND size_saved IS NOT NULL
|
||||||
|
""")
|
||||||
|
folder_rows = cursor.fetchall()
|
||||||
|
|
||||||
|
folder_sums = {}
|
||||||
|
for row in folder_rows:
|
||||||
|
folder = os.path.dirname(row.get('file_path') or '') or '/'
|
||||||
|
size_saved = row.get('size_saved') or 0
|
||||||
|
if isinstance(size_saved, Decimal):
|
||||||
|
size_saved = float(size_saved)
|
||||||
|
folder_sums[folder] = folder_sums.get(folder, 0) + size_saved
|
||||||
|
top_folders = [
|
||||||
|
{'folder': folder, 'total_saved': total}
|
||||||
|
for folder, total in sorted(folder_sums.items(), key=lambda x: x[1], reverse=True)[:8]
|
||||||
|
]
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT error_message, COUNT(*) as total_errors
|
||||||
|
FROM job_reports
|
||||||
|
WHERE status = 'failed' AND error_message IS NOT NULL AND error_message != ''
|
||||||
|
GROUP BY error_message
|
||||||
|
ORDER BY total_errors DESC
|
||||||
|
LIMIT 8
|
||||||
|
""")
|
||||||
|
top_errors = [
|
||||||
|
{
|
||||||
|
'error_message': row.get('error_message'),
|
||||||
|
'total_errors': int(row.get('total_errors') or 0)
|
||||||
|
}
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT encoder_type, file_path, status, size_saved, created_at, processing_time
|
||||||
|
FROM job_reports
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 8
|
||||||
|
""")
|
||||||
|
recent_jobs = cursor.fetchall()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'summary': summary,
|
||||||
|
'savings_over_time': savings_over_time,
|
||||||
|
'jobs_over_time': jobs_over_time,
|
||||||
|
'processing_time_over_time': processing_time_over_time,
|
||||||
|
'savings_by_encoder': savings_by_encoder,
|
||||||
|
'format_breakdown': format_breakdown,
|
||||||
|
'top_folders': top_folders,
|
||||||
|
'top_errors': top_errors,
|
||||||
|
'recent_jobs': recent_jobs
|
||||||
|
}
|
||||||
|
|
||||||
def file_already_processed(self, file_path):
|
def file_already_processed(self, file_path):
|
||||||
"""Check if a file has already been processed (successfully or failed)"""
|
"""Check if a file has already been processed (successfully or failed)"""
|
||||||
conn = self.get_connection()
|
conn = self.get_connection()
|
||||||
@@ -389,8 +564,7 @@ class VideoEncoder:
|
|||||||
return False, "Invalid video resolution information"
|
return False, "Invalid video resolution information"
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
'ffmpeg', '-i', input_path, '-c:v', 'libx265', '-c:a', 'copy',
|
'ffmpeg', '-i', input_path
|
||||||
'-preset', 'medium', '-crf', '28', '-f', 'matroska'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add scaling filter if target resolution is specified
|
# Add scaling filter if target resolution is specified
|
||||||
@@ -404,8 +578,10 @@ class VideoEncoder:
|
|||||||
# Ensure width is divisible by 2 (required by video codecs)
|
# Ensure width is divisible by 2 (required by video codecs)
|
||||||
if scale_width % 2 != 0:
|
if scale_width % 2 != 0:
|
||||||
scale_width -= 1
|
scale_width -= 1
|
||||||
cmd.insert(2, f'scale={scale_width}:{target_height}')
|
cmd.extend(['-vf', f'scale={scale_width}:{target_height}'])
|
||||||
cmd.insert(2, '-vf')
|
|
||||||
|
cmd.extend(['-c:v', 'libx265', '-c:a', 'copy',
|
||||||
|
'-preset', 'medium', '-crf', '28', '-f', 'matroska'])
|
||||||
|
|
||||||
# Insert user flags if provided
|
# Insert user flags if provided
|
||||||
if ffmpeg_flags:
|
if ffmpeg_flags:
|
||||||
@@ -552,13 +728,11 @@ class AudioEncoder:
|
|||||||
def process_file(encoder_type, file_path, encoder):
|
def process_file(encoder_type, file_path, encoder):
|
||||||
"""Process a single file"""
|
"""Process a single file"""
|
||||||
global current_jobs
|
global current_jobs
|
||||||
if not ENCODING_ENABLED:
|
|
||||||
logger.info(f"Encoding is disabled. Skipping {file_path}")
|
|
||||||
db_manager.remove_from_queue(encoder_type, file_path)
|
|
||||||
with job_lock:
|
|
||||||
current_jobs[encoder_type] = None
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
|
logger.info(f"Starting to process {file_path} for {encoder_type}")
|
||||||
|
if not ENCODING_ENABLED:
|
||||||
|
logger.info(f"Encoding is disabled. Skipping {file_path}")
|
||||||
|
return
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
logger.warning(f"File {file_path} no longer exists, skipping")
|
logger.warning(f"File {file_path} no longer exists, skipping")
|
||||||
return
|
return
|
||||||
@@ -724,42 +898,52 @@ def process_file(encoder_type, file_path, encoder):
|
|||||||
if os.path.exists(temp_input):
|
if os.path.exists(temp_input):
|
||||||
os.remove(temp_input)
|
os.remove(temp_input)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing {file_path}: {e}")
|
logger.error(f"Error processing {file_path}: {e}", exc_info=True)
|
||||||
try:
|
try:
|
||||||
|
file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
|
||||||
db_manager.add_job_report(
|
db_manager.add_job_report(
|
||||||
encoder_type, file_path, os.path.getsize(file_path), 0,
|
encoder_type, file_path, file_size, 0,
|
||||||
os.path.splitext(file_path)[1], '', 'failed',
|
os.path.splitext(file_path)[1], '', 'failed',
|
||||||
error_message=str(e),
|
error_message=str(e),
|
||||||
target_resolution=target_resolution
|
target_resolution=target_resolution if 'target_resolution' in locals() else None
|
||||||
)
|
)
|
||||||
except:
|
except Exception as report_error:
|
||||||
pass
|
logger.error(f"Failed to add job report: {report_error}", exc_info=True)
|
||||||
finally:
|
finally:
|
||||||
# Remove from queue after processing is complete
|
# Remove from queue after processing is complete (only when encoding is enabled)
|
||||||
db_manager.remove_from_queue(encoder_type, file_path)
|
if ENCODING_ENABLED:
|
||||||
|
db_manager.remove_from_queue(encoder_type, file_path)
|
||||||
with job_lock:
|
with job_lock:
|
||||||
current_jobs[encoder_type] = None
|
current_jobs[encoder_type] = None
|
||||||
|
|
||||||
def worker_thread(encoder_type):
|
def worker_thread(encoder_type):
|
||||||
"""Worker thread for processing jobs"""
|
"""Worker thread for processing jobs"""
|
||||||
encoder = VideoEncoder() if encoder_type == 'video' else AudioEncoder()
|
encoder = VideoEncoder() if encoder_type == 'video' else AudioEncoder()
|
||||||
|
logger.info(f"Started {encoder_type} worker thread")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
if not ENCODING_ENABLED:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
with job_lock:
|
with job_lock:
|
||||||
queue = db_manager.get_queue(encoder_type, limit=1)
|
queue = db_manager.get_queue(encoder_type, limit=1)
|
||||||
if queue and not current_jobs[encoder_type]:
|
if queue and not current_jobs[encoder_type]:
|
||||||
file_path = queue[0]
|
file_path = queue[0]
|
||||||
|
logger.debug(f"{encoder_type} queue item: {file_path}")
|
||||||
# Check file exists before processing
|
# Check file exists before processing
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
current_jobs[encoder_type] = {
|
current_jobs[encoder_type] = {
|
||||||
'file_path': file_path,
|
'file_path': file_path,
|
||||||
'start_time': time.time()
|
'start_time': time.time()
|
||||||
}
|
}
|
||||||
|
logger.info(f"Starting job for {file_path}")
|
||||||
# Do NOT remove from queue here anymore
|
# Do NOT remove from queue here anymore
|
||||||
else:
|
else:
|
||||||
# Remove from queue if file does not exist
|
# Remove from queue if file does not exist
|
||||||
|
logger.warning(f"File {file_path} does not exist, removing from queue")
|
||||||
db_manager.remove_from_queue(encoder_type, file_path)
|
db_manager.remove_from_queue(encoder_type, file_path)
|
||||||
if current_jobs[encoder_type]:
|
if current_jobs[encoder_type]:
|
||||||
|
logger.debug(f"Processing {current_jobs[encoder_type]['file_path']}")
|
||||||
process_file(encoder_type, current_jobs[encoder_type]['file_path'], encoder)
|
process_file(encoder_type, current_jobs[encoder_type]['file_path'], encoder)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
@@ -882,6 +1066,20 @@ def reports(encoder_type):
|
|||||||
total=total,
|
total=total,
|
||||||
search_term=search_term)
|
search_term=search_term)
|
||||||
|
|
||||||
|
@app.route('/reports')
|
||||||
|
def reports_dashboard():
|
||||||
|
"""Show overall reports dashboard"""
|
||||||
|
data = db_manager.get_reports_dashboard_data()
|
||||||
|
summary = data.get('summary') or {}
|
||||||
|
total_original = summary.get('total_original') or 0
|
||||||
|
total_saved = summary.get('total_saved') or 0
|
||||||
|
savings_ratio = (total_saved / total_original) if total_original else 0
|
||||||
|
return render_template(
|
||||||
|
'reports_dashboard.html',
|
||||||
|
**data,
|
||||||
|
savings_ratio=savings_ratio
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/config')
|
@app.route('/config')
|
||||||
def config():
|
def config():
|
||||||
"""Configuration page"""
|
"""Configuration page"""
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "Generating CSS..."
|
|
||||||
./tailwindcss -i ./static/css/input.css -o ./static/css/output.css --content "./templates/*.html" --minify
|
|
||||||
|
|
||||||
echo "Starting app..."
|
|
||||||
python app.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@import "tailwindcss"
|
|
||||||
File diff suppressed because it is too large
Load Diff
+150
-90
@@ -1,92 +1,161 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="dark">
|
<html lang="en" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}Encoder{% endblock %}</title>
|
<title>{% block title %}Encoder{% endblock %}</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link href="{{ url_for('static', filename='css/output.css') }}" rel="stylesheet">
|
|
||||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}">
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}">
|
||||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
<link href="{{ url_for('static', filename='css/material-design-3.css') }}" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
html, body { font-family: 'Inter', system-ui, sans-serif; }
|
.hidden { display: none !important; }
|
||||||
.modern-shadow {
|
|
||||||
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 4px 0 rgba(0,0,0,0.12);
|
|
||||||
}
|
|
||||||
.transition-all { transition: all 0.2s cubic-bezier(.4,0,.2,1); }
|
|
||||||
</style>
|
</style>
|
||||||
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-950 text-white min-h-screen">
|
<body>
|
||||||
<nav class="bg-gray-950 border-b border-gray-800 modern-shadow" x-data="{ mobileMenuOpen: false }">
|
<nav>
|
||||||
<div class="max-w-full mx-auto px-8">
|
<div class="nav-container">
|
||||||
<div class="flex justify-between h-16">
|
<a href="{{ url_for('index') }}" class="nav-brand">
|
||||||
<div class="flex items-center">
|
<img src="{{ url_for('static', filename='favicon.png') }}" alt="Encoder" style="height: 2rem; width: auto;">
|
||||||
<a href="{{ url_for('index') }}" class="flex items-center space-x-2">
|
<span>Encoder</span>
|
||||||
<img src="{{ url_for('static', filename='favicon.png') }}" alt="Encoder" class="w-8 h-8 rounded-lg shadow-md">
|
</a>
|
||||||
<span class="text-2xl font-bold tracking-tight bg-gradient-to-r from-fuchsia-400 via-sky-400 to-cyan-400 bg-clip-text text-transparent">Encoder</span>
|
|
||||||
</a>
|
<ul class="nav-links">
|
||||||
</div>
|
<li><a href="{{ url_for('index') }}"><i class="fas fa-gauge-high"></i> <span>Dashboard</span></a></li>
|
||||||
<!-- Desktop Navigation -->
|
<li><a href="{{ url_for('config') }}"><i class="fas fa-sliders"></i> <span>Configuration</span></a></li>
|
||||||
<div class="hidden md:flex items-center space-x-4">
|
<li><a href="{{ url_for('reports_dashboard') }}"><i class="fas fa-chart-line"></i> <span>Reports</span></a></li>
|
||||||
<a href="{{ url_for('index') }}" class="text-sky-300 hover:text-white px-3 py-2 rounded-full text-base font-semibold flex items-center space-x-2 transition-all">
|
<li>
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
<span class="material-symbols-rounded" id="theme-icon">dark_mode</span>
|
||||||
</svg>
|
|
||||||
<span>Dashboard</span>
|
|
||||||
</a>
|
|
||||||
<a href="{{ url_for('config') }}" class="text-fuchsia-300 hover:text-white px-3 py-2 rounded-full text-base font-semibold flex items-center space-x-2 transition-all">
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
||||||
</svg>
|
|
||||||
<span>Configuration</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<!-- Mobile menu button -->
|
|
||||||
<div class="md:hidden flex items-center">
|
|
||||||
<button @click="mobileMenuOpen = !mobileMenuOpen" class="inline-flex items-center justify-center p-2 rounded-full text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white transition-all">
|
|
||||||
<svg class="h-6 w-6" :class="{'hidden': mobileMenuOpen, 'block': !mobileMenuOpen}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
||||||
</svg>
|
|
||||||
<svg class="h-6 w-6" :class="{'block': mobileMenuOpen, 'hidden': !mobileMenuOpen}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
|
||||||
<!-- Mobile menu -->
|
<button class="mobile-menu-button" id="mobile-menu-button" aria-label="Menu">
|
||||||
<div x-show="mobileMenuOpen" @click.away="mobileMenuOpen = false" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="md:hidden bg-gray-800 border-t border-gray-700">
|
<span class="material-symbols-rounded">menu</span>
|
||||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
</button>
|
||||||
<a href="{{ url_for('index') }}" class="text-sky-300 hover:text-white hover:bg-gray-700 block px-3 py-2 rounded-full text-base font-semibold flex items-center space-x-2 transition-all">
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
||||||
</svg>
|
|
||||||
<span>Dashboard</span>
|
|
||||||
</a>
|
|
||||||
<a href="{{ url_for('config') }}" class="text-fuchsia-300 hover:text-white hover:bg-gray-700 block px-3 py-2 rounded-full text-base font-semibold flex items-center space-x-2 transition-all">
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
||||||
</svg>
|
|
||||||
<span>Configuration</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="max-w-8xl mx-auto py-8 sm:px-8 lg:px-8">
|
|
||||||
{% block content %}{% endblock %}
|
<div class="mobile-sidenav-overlay" id="mobile-sidenav-overlay"></div>
|
||||||
|
<div class="mobile-sidenav" id="mobile-sidenav">
|
||||||
|
<div class="mobile-sidenav-header">
|
||||||
|
<a href="{{ url_for('index') }}" class="nav-brand">
|
||||||
|
<img src="{{ url_for('static', filename='favicon.png') }}" alt="Encoder" style="height: 1.5rem; width: auto;">
|
||||||
|
<span>Encoder</span>
|
||||||
|
</a>
|
||||||
|
<button class="mobile-sidenav-close" id="mobile-sidenav-close" aria-label="Close menu">
|
||||||
|
<span class="material-symbols-rounded">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul class="mobile-sidenav-links">
|
||||||
|
<li><a href="{{ url_for('index') }}"><i class="fas fa-gauge-high"></i> <span>Dashboard</span></a></li>
|
||||||
|
<li><a href="{{ url_for('config') }}"><i class="fas fa-sliders"></i> <span>Configuration</span></a></li>
|
||||||
|
<li><a href="{{ url_for('reports_dashboard') }}"><i class="fas fa-chart-line"></i> <span>Reports</span></a></li>
|
||||||
|
<li>
|
||||||
|
<button id="mobile-theme-toggle" aria-label="Toggle theme" style="background: none; border: none;">
|
||||||
|
<span class="material-symbols-rounded" id="mobile-theme-icon">dark_mode</span>
|
||||||
|
<span>Toggle Theme</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main style="flex: 1; padding: 2rem 0;">
|
||||||
|
<div class="container-wide">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<div id="notifications" class="fixed bottom-4 right-4 z-50"></div>
|
<div id="notifications" style="position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 2100;"></div>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
<footer class="w-full mt-12 py-6 bg-gray-950 border-t border-gray-800 text-center text-gray-400 text-sm">
|
|
||||||
<div class="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-2 px-4">
|
<footer style="background-color: var(--md-sys-color-surface-container); border-top: 1px solid var(--md-sys-color-outline-variant); padding: 1rem;">
|
||||||
<span>© <script>document.write(new Date().getFullYear());</script> <a href="https://www.jdbnet.co.uk" target="_blank" class="hover:text-sky-400 transition-all">JDB-NET</a></span>
|
<div class="flex justify-center items-center">
|
||||||
<a href="https://git.jdbnet.co.uk/jamie/encoder" target="_blank" class="hover:text-sky-400 transition-all">{{ version }}</a>
|
<span style="font-size: 0.875rem; color: var(--md-sys-color-on-surface-variant);">
|
||||||
|
© <script>document.write(new Date().getFullYear());</script>
|
||||||
|
<a href="https://www.jdbnet.co.uk" target="_blank" rel="noopener noreferrer" style="color: var(--md-sys-color-primary); text-decoration: none;">
|
||||||
|
JDB-NET
|
||||||
|
</a>
|
||||||
|
<span style="margin: 0 0.5rem;">•</span>
|
||||||
|
<a href="https://git.jdbnet.co.uk/jamie/encoder" target="_blank" rel="noopener noreferrer" style="color: var(--md-sys-color-primary); text-decoration: none;">{{ version }}</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Theme Toggle
|
||||||
|
(function() {
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
const mobileThemeToggle = document.getElementById('mobile-theme-toggle');
|
||||||
|
const themeIcon = document.getElementById('theme-icon');
|
||||||
|
const mobileThemeIcon = document.getElementById('mobile-theme-icon');
|
||||||
|
const html = document.documentElement;
|
||||||
|
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
|
html.setAttribute('data-theme', savedTheme);
|
||||||
|
updateThemeIcon(savedTheme);
|
||||||
|
|
||||||
|
function updateThemeIcon(theme) {
|
||||||
|
const icon = theme === 'dark' ? 'light_mode' : 'dark_mode';
|
||||||
|
if (themeIcon) themeIcon.textContent = icon;
|
||||||
|
if (mobileThemeIcon) mobileThemeIcon.textContent = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
const currentTheme = html.getAttribute('data-theme');
|
||||||
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
html.setAttribute('data-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
updateThemeIcon(newTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeToggle) themeToggle.addEventListener('click', toggleTheme);
|
||||||
|
if (mobileThemeToggle) mobileThemeToggle.addEventListener('click', toggleTheme);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Mobile Menu Toggle
|
||||||
|
(function() {
|
||||||
|
const menuButton = document.getElementById('mobile-menu-button');
|
||||||
|
const closeButton = document.getElementById('mobile-sidenav-close');
|
||||||
|
const sidenav = document.getElementById('mobile-sidenav');
|
||||||
|
const overlay = document.getElementById('mobile-sidenav-overlay');
|
||||||
|
|
||||||
|
function openMenu() {
|
||||||
|
sidenav.classList.add('open');
|
||||||
|
overlay.classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
sidenav.classList.remove('open');
|
||||||
|
overlay.classList.remove('open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuButton) menuButton.addEventListener('click', openMenu);
|
||||||
|
if (closeButton) closeButton.addEventListener('click', closeMenu);
|
||||||
|
if (overlay) overlay.addEventListener('click', closeMenu);
|
||||||
|
|
||||||
|
const menuLinks = sidenav.querySelectorAll('a');
|
||||||
|
menuLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', closeMenu);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Set active nav link
|
||||||
|
(function() {
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
const navLinks = document.querySelectorAll('.nav-links a, .mobile-sidenav-links a');
|
||||||
|
navLinks.forEach(link => {
|
||||||
|
const href = link.getAttribute('href');
|
||||||
|
if (href === '/' && currentPath === '/') {
|
||||||
|
link.classList.add('active');
|
||||||
|
} else if (href !== '/' && currentPath.startsWith(href)) {
|
||||||
|
link.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
// Format seconds to human readable duration (like Python's format_duration)
|
// Format seconds to human readable duration (like Python's format_duration)
|
||||||
function formatDurationJS(seconds) {
|
function formatDurationJS(seconds) {
|
||||||
if (seconds == null || isNaN(seconds)) return 'N/A';
|
if (seconds == null || isNaN(seconds)) return 'N/A';
|
||||||
@@ -180,18 +249,12 @@
|
|||||||
const duration = Math.floor((Date.now() / 1000) - job.start_time);
|
const duration = Math.floor((Date.now() / 1000) - job.start_time);
|
||||||
const durationStr = formatDurationJS(duration);
|
const durationStr = formatDurationJS(duration);
|
||||||
statusElement.innerHTML = `
|
statusElement.innerHTML = `
|
||||||
<div class="flex items-center space-x-2">
|
<span class="md-chip"><span class="status-dot green"></span> Processing: ${fileName}</span>
|
||||||
<div class="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
|
<span style="margin-left: 0.5rem; color: var(--md-sys-color-on-surface-variant); font-size: 0.875rem;">(${durationStr})</span>
|
||||||
<span class="text-sm">Processing: ${fileName}</span>
|
|
||||||
<span class="text-xs text-gray-400">(${durationStr})</span>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
statusElement.innerHTML = `
|
statusElement.innerHTML = `
|
||||||
<div class="flex items-center space-x-2">
|
<span class="md-chip"><span class="status-dot gray"></span> Idle</span>
|
||||||
<div class="w-3 h-3 bg-gray-500 rounded-full"></div>
|
|
||||||
<span class="text-sm">Idle</span>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,29 +274,26 @@
|
|||||||
queueContainer.innerHTML = `
|
queueContainer.innerHTML = `
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
${queue.map(filePath => `
|
${queue.map(filePath => `
|
||||||
<div class="flex items-center space-x-2 text-sm">
|
<div class="md-card-outlined" style="padding: 0.5rem 0.75rem;">
|
||||||
<div class="w-2 h-2 bg-yellow-500 rounded-full"></div>
|
<span class="status-dot" style="background: #f59e0b;"></span>
|
||||||
<span class="text-gray-300">${filePath.split('/').pop()}</span>
|
<span style="margin-left: 0.5rem;">${filePath.split('/').pop()}</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
queueContainer.innerHTML = '<p class="text-gray-400 text-sm">No files in queue</p>';
|
queueContainer.innerHTML = '<p style="color: var(--md-sys-color-on-surface-variant); text-align: center;">No files in queue</p>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNotification(message, type = 'info') {
|
function showNotification(message, type = 'info') {
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.className = `
|
notification.className = `md-snackbar ${type}`;
|
||||||
p-4 rounded-lg shadow-lg max-w-sm mb-4 transition-all duration-300
|
|
||||||
${type === 'success' ? 'bg-green-600' : type === 'error' ? 'bg-red-600' : 'bg-blue-600'}
|
|
||||||
`;
|
|
||||||
notification.innerHTML = `
|
notification.innerHTML = `
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-white text-sm">${message}</span>
|
<span>${message}</span>
|
||||||
<button onclick="this.parentElement.parentElement.remove()" class="text-white hover:text-gray-200">
|
<button onclick="this.parentElement.parentElement.remove()" class="md-button md-button-text" style="padding: 0.25rem 0.5rem;">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
+233
-272
@@ -3,73 +3,60 @@
|
|||||||
{% block title %}Configuration - Encoder{% endblock %}
|
{% block title %}Configuration - Encoder{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="px-4 sm:px-0 max-w-7xl mx-auto">
|
<div class="fade-in">
|
||||||
<div class="mb-8">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div class="flex items-center justify-between">
|
<div>
|
||||||
<div>
|
<h1 class="md-display-medium mb-2">Configuration</h1>
|
||||||
<h1 class="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-fuchsia-400 via-sky-400 to-cyan-400 mb-2 tracking-tight">Configuration</h1>
|
<p style="color: var(--md-sys-color-on-surface-variant);">Manage watch folders for video and audio encoding.</p>
|
||||||
<p class="text-gray-300 text-lg">Manage watch folders for video and audio encoding</p>
|
|
||||||
</div>
|
|
||||||
<a href="{{ url_for('index') }}" class="bg-gradient-to-r from-sky-600 to-fuchsia-600 hover:from-sky-500 hover:to-fuchsia-500 text-white px-6 py-2 rounded-full text-base shadow-lg font-semibold transition-all">
|
|
||||||
← Back to Dashboard
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
<a href="{{ url_for('index') }}" class="md-button md-button-outlined">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<!-- Video Encoder Configuration -->
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
<div class="bg-gray-900 rounded-2xl modern-shadow p-8 transition-all hover:scale-[1.01]">
|
<h2 class="md-title-large" style="margin-top: 0;">
|
||||||
<h2 class="text-2xl font-bold text-white mb-4 flex items-center">
|
<i class="fas fa-video" style="color: var(--md-sys-color-primary);"></i>
|
||||||
<svg class="w-7 h-7 mr-2 text-sky-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
||||||
</svg>
|
|
||||||
Video Encoder Folders
|
Video Encoder Folders
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Add new folder form -->
|
<form method="POST" action="{{ url_for('add_config') }}" class="mt-4" id="video-config-form">
|
||||||
<form method="POST" action="{{ url_for('add_config') }}" class="mb-6" id="video-config-form">
|
|
||||||
<input type="hidden" name="encoder_type" value="video">
|
<input type="hidden" name="encoder_type" value="video">
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<div class="flex space-x-2 items-end">
|
<div>
|
||||||
<div class="flex-1">
|
<label style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Watch Folder</label>
|
||||||
<label for="video-folder-picker" class="text-xs text-gray-400 mb-1 block">Watch Folder</label>
|
<div id="video-folder-picker" class="md-card-outlined" style="padding: 0.75rem; cursor: pointer;">
|
||||||
<div id="video-folder-picker" class="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white cursor-pointer select-none" style="min-height:2.5rem;">
|
<span id="video-folder-path">/</span>
|
||||||
<span id="video-folder-path">/</span>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="watch_folder" id="video-folder-input" value="/">
|
|
||||||
<div id="video-folder-list" class="bg-gray-800 border border-gray-700 rounded-lg mt-1 p-2 text-white text-sm hidden"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<input type="hidden" name="watch_folder" id="video-folder-input" value="/">
|
||||||
<label for="video-temp-picker" class="text-xs text-gray-400 mb-1 block">Temp Directory</label>
|
<div id="video-folder-list" class="md-card-outlined hidden" style="padding: 0.75rem; margin-top: 0.5rem;"></div>
|
||||||
<div id="video-temp-picker" class="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white cursor-pointer select-none" style="min-height:2.5rem;">
|
|
||||||
<span id="video-temp-path">/temp</span>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="temp_dir" id="video-temp-input" value="/temp">
|
|
||||||
<div id="video-temp-list" class="bg-gray-800 border border-gray-700 rounded-lg mt-1 p-2 text-white text-sm hidden"></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label for="video-target-resolution" class="text-xs text-gray-400 mb-1">Target Resolution
|
|
||||||
<span class="text-gray-500 text-xs">(Optional)</span>
|
|
||||||
</label>
|
|
||||||
<select name="target_resolution" id="video-target-resolution" class="bg-gray-700 border border-gray-600 rounded-lg px-2 py-1 text-white">
|
|
||||||
<option value="">Default (Keep Original)</option>
|
|
||||||
<option value="480p">480p</option>
|
|
||||||
<option value="720p">720p</option>
|
|
||||||
<option value="1080p">1080p</option>
|
|
||||||
<option value="1440p">1440p</option>
|
|
||||||
<option value="2160p">4K (2160p)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium">
|
|
||||||
Add Folder
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
|
||||||
<div class="flex flex-col">
|
<div>
|
||||||
<label for="crf" class="text-xs text-gray-400 mb-1">CRF (Quality)
|
<label style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Temp Directory</label>
|
||||||
<span title="Constant Rate Factor. Lower is better quality. 18-28 typical. Recommended: 22-28 for H.265." class="ml-1 text-blue-300 cursor-help">?</span>
|
<div id="video-temp-picker" class="md-card-outlined" style="padding: 0.75rem; cursor: pointer;">
|
||||||
</label>
|
<span id="video-temp-path">/temp</span>
|
||||||
<select name="crf" id="crf" class="bg-gray-700 border border-gray-600 rounded-lg px-2 py-1 text-white">
|
</div>
|
||||||
|
<input type="hidden" name="temp_dir" id="video-temp-input" value="/temp">
|
||||||
|
<div id="video-temp-list" class="md-card-outlined hidden" style="padding: 0.75rem; margin-top: 0.5rem;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="md-text-field">
|
||||||
|
<select name="target_resolution" id="video-target-resolution">
|
||||||
|
<option value="">Default (Keep Original)</option>
|
||||||
|
<option value="480p">480p</option>
|
||||||
|
<option value="720p">720p</option>
|
||||||
|
<option value="1080p">1080p</option>
|
||||||
|
<option value="1440p">1440p</option>
|
||||||
|
<option value="2160p">4K (2160p)</option>
|
||||||
|
</select>
|
||||||
|
<label for="video-target-resolution">Target Resolution</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="md-text-field">
|
||||||
|
<select name="crf" id="crf">
|
||||||
<option value="18">18</option>
|
<option value="18">18</option>
|
||||||
<option value="20">20</option>
|
<option value="20">20</option>
|
||||||
<option value="22">22</option>
|
<option value="22">22</option>
|
||||||
@@ -77,12 +64,10 @@
|
|||||||
<option value="26">26</option>
|
<option value="26">26</option>
|
||||||
<option value="28">28</option>
|
<option value="28">28</option>
|
||||||
</select>
|
</select>
|
||||||
|
<label for="crf">CRF (Quality)</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="md-text-field">
|
||||||
<label for="preset" class="text-xs text-gray-400 mb-1">Preset
|
<select name="preset" id="preset">
|
||||||
<span title="Encoding speed. Slower = better compression. Recommended: medium/slow." class="ml-1 text-blue-300 cursor-help">?</span>
|
|
||||||
</label>
|
|
||||||
<select name="preset" id="preset" class="bg-gray-700 border border-gray-600 rounded-lg px-2 py-1 text-white">
|
|
||||||
<option value="ultrafast">ultrafast</option>
|
<option value="ultrafast">ultrafast</option>
|
||||||
<option value="superfast">superfast</option>
|
<option value="superfast">superfast</option>
|
||||||
<option value="veryfast">veryfast</option>
|
<option value="veryfast">veryfast</option>
|
||||||
@@ -93,67 +78,59 @@
|
|||||||
<option value="slower">slower</option>
|
<option value="slower">slower</option>
|
||||||
<option value="veryslow">veryslow</option>
|
<option value="veryslow">veryslow</option>
|
||||||
</select>
|
</select>
|
||||||
|
<label for="preset">Preset</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="md-button md-button-filled" style="margin-bottom: 2rem;">
|
||||||
|
<i class="fas fa-plus"></i> Add Folder
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Current folders -->
|
<div class="mt-6 space-y-3">
|
||||||
<div class="space-y-3 mt-4">
|
|
||||||
{% if video_configs %}
|
{% if video_configs %}
|
||||||
{% for config in video_configs %}
|
{% for config in video_configs %}
|
||||||
<div class="bg-gray-800 rounded-xl p-4 flex items-center justify-between modern-shadow transition-all hover:scale-[1.01]">
|
<div class="md-card-outlined" style="padding: 1rem;">
|
||||||
<div>
|
<div class="flex items-center justify-between">
|
||||||
<div class="text-white text-lg font-semibold flex items-center">
|
<div style="flex: 1;">
|
||||||
<svg class="w-4 h-4 mr-1 text-sky-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" /></svg>
|
<div style="font-weight: 600;">{{ config.watch_folder }}</div>
|
||||||
{{ config.watch_folder }}
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">
|
||||||
</div>
|
Added: {{ config.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
<div class="text-gray-400 text-xs">Added: {{ config.created_at.strftime('%Y-%m-%d %H:%M') }}</div>
|
|
||||||
{% if config.temp_dir %}
|
|
||||||
<div class="text-blue-200 text-xs mt-1">Temp Dir: <span class="bg-blue-900 px-1 rounded">{{ config.temp_dir }}</span></div>
|
|
||||||
{% endif %}
|
|
||||||
{% if config.target_resolution %}
|
|
||||||
<div class="text-green-200 text-xs mt-1">Resolution: <span class="bg-green-900 px-1 rounded">{{ config.target_resolution }}</span></div>
|
|
||||||
{% endif %}
|
|
||||||
{% if config.ffmpeg_flags %}
|
|
||||||
{% set flags = config.ffmpeg_flags.split() %}
|
|
||||||
<div class="text-blue-300 text-xs mt-1">
|
|
||||||
<span title="FFmpeg flags used for this folder">
|
|
||||||
Flags:
|
|
||||||
{% for flag in flags %}
|
|
||||||
<span class="bg-blue-900 px-1 rounded mr-1">{{ flag }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% if config.temp_dir %}
|
||||||
</div>
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant); margin-top: 0.25rem;">
|
||||||
<div class="flex items-center space-x-2">
|
Temp Dir: <span class="md-chip">{{ config.temp_dir }}</span>
|
||||||
{% if config.watch_folder | path_exists %}
|
</div>
|
||||||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
{% endif %}
|
||||||
<span class="text-green-400 text-xs font-semibold">Active</span>
|
{% if config.target_resolution %}
|
||||||
{% else %}
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant); margin-top: 0.25rem;">
|
||||||
<span class="w-2 h-2 bg-red-500 rounded-full"></span>
|
Resolution: <span class="md-chip">{{ config.target_resolution }}</span>
|
||||||
<span class="text-red-400 text-xs font-semibold">Not Found</span>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button onclick="editConfig({{ config.id }})" class="ml-2 px-3 py-1 rounded-full bg-sky-700 hover:bg-sky-600 text-white text-xs font-semibold transition-all">Edit</button>
|
</div>
|
||||||
<button onclick="removeConfig({{ config.id }})" class="px-3 py-1 rounded-full bg-red-700 hover:bg-red-600 text-white text-xs font-semibold transition-all">Remove</button>
|
<div class="flex items-center gap-2">
|
||||||
|
{% if config.watch_folder | path_exists %}
|
||||||
|
<span class="md-chip"><span class="status-dot green"></span> Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="md-chip"><span class="status-dot red"></span> Not Found</span>
|
||||||
|
{% endif %}
|
||||||
|
<button onclick="editConfig({{ config.id }})" class="md-button md-button-text">Edit</button>
|
||||||
|
<button onclick="removeConfig({{ config.id }})" class="md-button md-button-text" style="color: var(--md-sys-color-error);">Remove</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="bg-gray-800 rounded-xl p-8 text-center modern-shadow">
|
<div class="md-card-outlined" style="padding: 1.5rem; text-align: center;">
|
||||||
<svg class="mx-auto h-8 w-8 text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<p style="color: var(--md-sys-color-on-surface-variant);">No video folders configured.</p>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"/>
|
|
||||||
</svg>
|
|
||||||
<p class="text-gray-400 text-base">No video folders configured</p>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Configuration info -->
|
<div class="md-card-outlined" style="padding: 1rem; margin-top: 1.5rem;">
|
||||||
<div class="mt-6 bg-blue-900 bg-opacity-20 border border-blue-600 rounded-lg p-4">
|
<h3 class="md-title-large" style="font-size: 1rem;">Video Encoder Settings</h3>
|
||||||
<h3 class="text-blue-300 font-medium mb-2">Video Encoder Settings</h3>
|
<ul style="font-size: 0.875rem; color: var(--md-sys-color-on-surface-variant);">
|
||||||
<ul class="text-sm text-blue-200 space-y-1">
|
|
||||||
<li>• Converts H.264 videos to H.265 (HEVC)</li>
|
<li>• Converts H.264 videos to H.265 (HEVC)</li>
|
||||||
<li>• Output format: MKV container</li>
|
<li>• Output format: MKV container</li>
|
||||||
<li>• Supported formats: MP4, AVI, MKV, MOV, WMV, FLV, WebM</li>
|
<li>• Supported formats: MP4, AVI, MKV, MOV, WMV, FLV, WebM</li>
|
||||||
@@ -161,206 +138,184 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Audio Encoder Configuration -->
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
<div class="bg-gray-900 rounded-2xl modern-shadow p-8 transition-all hover:scale-[1.01]">
|
<h2 class="md-title-large" style="margin-top: 0;">
|
||||||
<h2 class="text-2xl font-bold text-white mb-4 flex items-center">
|
<i class="fas fa-music" style="color: var(--md-sys-color-tertiary);"></i>
|
||||||
<svg class="w-7 h-7 mr-2 text-fuchsia-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
|
|
||||||
</svg>
|
|
||||||
Audio Encoder Folders
|
Audio Encoder Folders
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Add new folder form -->
|
<form method="POST" action="{{ url_for('add_config') }}" class="mt-4" id="audio-config-form">
|
||||||
<form method="POST" action="{{ url_for('add_config') }}" class="mb-6" id="audio-config-form">
|
|
||||||
<input type="hidden" name="encoder_type" value="audio">
|
<input type="hidden" name="encoder_type" value="audio">
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<div class="flex space-x-2 items-end">
|
<div>
|
||||||
<div class="flex-1">
|
<label style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Watch Folder</label>
|
||||||
<label for="audio-folder-picker" class="text-xs text-gray-400 mb-1 block">Watch Folder</label>
|
<div id="audio-folder-picker" class="md-card-outlined" style="padding: 0.75rem; cursor: pointer;">
|
||||||
<div id="audio-folder-picker" class="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white cursor-pointer select-none" style="min-height:2.5rem;">
|
<span id="audio-folder-path">/</span>
|
||||||
<span id="audio-folder-path">/</span>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="watch_folder" id="audio-folder-input" value="/">
|
|
||||||
<div id="audio-folder-list" class="bg-gray-800 border border-gray-700 rounded-lg mt-1 p-2 text-white text-sm hidden"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<input type="hidden" name="watch_folder" id="audio-folder-input" value="/">
|
||||||
<label for="audio-temp-picker" class="text-xs text-gray-400 mb-1 block">Temp Directory</label>
|
<div id="audio-folder-list" class="md-card-outlined hidden" style="padding: 0.75rem; margin-top: 0.5rem;"></div>
|
||||||
<div id="audio-temp-picker" class="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white cursor-pointer select-none" style="min-height:2.5rem;">
|
|
||||||
<span id="audio-temp-path">/temp</span>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="temp_dir" id="audio-temp-input" value="/temp">
|
|
||||||
<div id="audio-temp-list" class="bg-gray-800 border border-gray-700 rounded-lg mt-1 p-2 text-white text-sm hidden"></div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg text-sm font-medium">
|
|
||||||
Add Folder
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
|
||||||
<label for="audio_bitrate" class="text-xs text-gray-400 mb-1">Bitrate
|
<div>
|
||||||
<span title="Audio bitrate. Higher = better quality. Recommended: 192k-320k." class="ml-1 text-purple-300 cursor-help">?</span>
|
<label style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Temp Directory</label>
|
||||||
</label>
|
<div id="audio-temp-picker" class="md-card-outlined" style="padding: 0.75rem; cursor: pointer;">
|
||||||
<select name="audio_bitrate" id="audio_bitrate" class="bg-gray-700 border border-gray-600 rounded-lg px-2 py-1 text-white">
|
<span id="audio-temp-path">/temp</span>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="temp_dir" id="audio-temp-input" value="/temp">
|
||||||
|
<div id="audio-temp-list" class="md-card-outlined hidden" style="padding: 0.75rem; margin-top: 0.5rem;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="md-text-field">
|
||||||
|
<select name="audio_bitrate" id="audio_bitrate">
|
||||||
<option value="128k">128k</option>
|
<option value="128k">128k</option>
|
||||||
<option value="192k">192k</option>
|
<option value="192k">192k</option>
|
||||||
<option value="256k">256k</option>
|
<option value="256k">256k</option>
|
||||||
<option value="320k" selected>320k</option>
|
<option value="320k" selected>320k</option>
|
||||||
</select>
|
</select>
|
||||||
|
<label for="audio_bitrate">Bitrate</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="md-button md-button-filled" style="margin-bottom: 2rem;">
|
||||||
|
<i class="fas fa-plus"></i> Add Folder
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Current folders -->
|
<div class="mt-6 space-y-3">
|
||||||
<div class="space-y-3 mt-4">
|
|
||||||
{% if audio_configs %}
|
{% if audio_configs %}
|
||||||
{% for config in audio_configs %}
|
{% for config in audio_configs %}
|
||||||
<div class="bg-gray-800 rounded-xl p-4 flex items-center justify-between modern-shadow transition-all hover:scale-[1.01]">
|
<div class="md-card-outlined" style="padding: 1rem;">
|
||||||
<div>
|
<div class="flex items-center justify-between">
|
||||||
<div class="text-white text-lg font-semibold flex items-center">
|
<div style="flex: 1;">
|
||||||
<svg class="w-4 h-4 mr-1 text-fuchsia-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" /></svg>
|
<div style="font-weight: 600;">{{ config.watch_folder }}</div>
|
||||||
{{ config.watch_folder }}
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">
|
||||||
</div>
|
Added: {{ config.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
<div class="text-gray-400 text-xs">Added: {{ config.created_at.strftime('%Y-%m-%d %H:%M') }}</div>
|
|
||||||
{% if config.temp_dir %}
|
|
||||||
<div class="text-purple-200 text-xs mt-1">Temp Dir: <span class="bg-purple-900 px-1 rounded">{{ config.temp_dir }}</span></div>
|
|
||||||
{% endif %}
|
|
||||||
{% if config.target_resolution %}
|
|
||||||
<div class="text-green-200 text-xs mt-1">Resolution: <span class="bg-green-900 px-1 rounded">{{ config.target_resolution }}</span></div>
|
|
||||||
{% endif %}
|
|
||||||
{% if config.ffmpeg_flags %}
|
|
||||||
{% set flags = config.ffmpeg_flags.split() %}
|
|
||||||
<div class="text-purple-300 text-xs mt-1">
|
|
||||||
<span title="FFmpeg flags used for this folder">
|
|
||||||
Flags:
|
|
||||||
{% for flag in flags %}
|
|
||||||
<span class="bg-purple-900 px-1 rounded mr-1">{{ flag }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% if config.temp_dir %}
|
||||||
</div>
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant); margin-top: 0.25rem;">
|
||||||
<div class="flex items-center space-x-2">
|
Temp Dir: <span class="md-chip">{{ config.temp_dir }}</span>
|
||||||
{% if config.watch_folder | path_exists %}
|
</div>
|
||||||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
{% endif %}
|
||||||
<span class="text-green-400 text-xs font-semibold">Active</span>
|
</div>
|
||||||
{% else %}
|
<div class="flex items-center gap-2">
|
||||||
<span class="w-2 h-2 bg-red-500 rounded-full"></span>
|
{% if config.watch_folder | path_exists %}
|
||||||
<span class="text-red-400 text-xs font-semibold">Not Found</span>
|
<span class="md-chip"><span class="status-dot green"></span> Active</span>
|
||||||
{% endif %}
|
{% else %}
|
||||||
<button onclick="editConfig({{ config.id }})" class="ml-2 px-3 py-1 rounded-full bg-sky-700 hover:bg-sky-600 text-white text-xs font-semibold transition-all">Edit</button>
|
<span class="md-chip"><span class="status-dot red"></span> Not Found</span>
|
||||||
<button onclick="removeConfig({{ config.id }})" class="px-3 py-1 rounded-full bg-red-700 hover:bg-red-600 text-white text-xs font-semibold transition-all">Remove</button>
|
{% endif %}
|
||||||
|
<button onclick="editConfig({{ config.id }})" class="md-button md-button-text">Edit</button>
|
||||||
|
<button onclick="removeConfig({{ config.id }})" class="md-button md-button-text" style="color: var(--md-sys-color-error);">Remove</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="bg-gray-800 rounded-xl p-8 text-center modern-shadow">
|
<div class="md-card-outlined" style="padding: 1.5rem; text-align: center;">
|
||||||
<svg class="mx-auto h-8 w-8 text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<p style="color: var(--md-sys-color-on-surface-variant);">No audio folders configured.</p>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"/>
|
|
||||||
</svg>
|
|
||||||
<p class="text-gray-400 text-base">No audio folders configured</p>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Configuration info -->
|
<div class="md-card-outlined" style="padding: 1rem; margin-top: 1.5rem;">
|
||||||
<div class="mt-6 bg-purple-900 bg-opacity-20 border border-purple-600 rounded-lg p-4">
|
<h3 class="md-title-large" style="font-size: 1rem;">Audio Encoder Settings</h3>
|
||||||
<h3 class="text-purple-300 font-medium mb-2">Audio Encoder Settings</h3>
|
<ul style="font-size: 0.875rem; color: var(--md-sys-color-on-surface-variant);">
|
||||||
<ul class="text-sm text-purple-200 space-y-1">
|
|
||||||
<li>• Converts files to MP3</li>
|
<li>• Converts files to MP3</li>
|
||||||
<li>• Supported formats: FLAC, WAV, M4A, OGG</li>
|
<li>• Supported formats: FLAC, WAV, M4A, OGG</li>
|
||||||
<li>• Codec: libmp3lame</li>
|
<li>• Codec: libmp3lame</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit Config Modal -->
|
<div id="edit-config-modal" class="search-modal-overlay hidden">
|
||||||
<div id="edit-config-modal" class="fixed inset-0 z-50 hidden flex items-center justify-center">
|
<div class="search-modal" style="max-width: 640px;">
|
||||||
<div class="absolute inset-0 bg-black bg-opacity-40 backdrop-blur-sm transition-all"></div>
|
<div class="search-modal-header">
|
||||||
<div class="relative bg-gray-900 rounded-2xl p-8 shadow-2xl max-w-lg w-full mx-auto border border-gray-700 flex flex-col justify-center">
|
<h2 class="md-title-large" style="margin: 0;">Edit Configuration</h2>
|
||||||
<h2 class="text-2xl font-bold mb-4 text-white">Edit Configuration</h2>
|
</div>
|
||||||
<form id="edit-config-form">
|
<div style="padding: 1.5rem;">
|
||||||
<input type="hidden" name="id" id="edit-config-id">
|
<form id="edit-config-form">
|
||||||
<input type="hidden" name="encoder_type" id="edit-encoder-type">
|
<input type="hidden" name="id" id="edit-config-id">
|
||||||
<div class="mb-4">
|
<input type="hidden" name="encoder_type" id="edit-encoder-type">
|
||||||
<label class="block text-xs text-gray-400 mb-1">Watch Folder</label>
|
|
||||||
<div id="edit-folder-picker" class="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white cursor-pointer select-none hover:bg-gray-600 transition-all" style="min-height:2.5rem;">
|
<div style="margin-bottom: 1rem;">
|
||||||
<span id="edit-folder-path">/</span>
|
<label style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Watch Folder</label>
|
||||||
</div>
|
<div id="edit-folder-picker" class="md-card-outlined" style="padding: 0.75rem; cursor: pointer;">
|
||||||
<input type="hidden" name="watch_folder" id="edit-folder-input" value="/">
|
<span id="edit-folder-path">/</span>
|
||||||
<div id="edit-folder-list" class="bg-gray-800 border border-gray-700 rounded-lg mt-1 p-2 text-white text-sm hidden z-50" style="position:absolute;"></div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="block text-xs text-gray-400 mb-1">Temp Directory</label>
|
|
||||||
<div id="edit-temp-picker" class="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white cursor-pointer select-none hover:bg-gray-600 transition-all" style="min-height:2.5rem;">
|
|
||||||
<span id="edit-temp-path">/temp</span>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="temp_dir" id="edit-temp-input" value="/temp">
|
|
||||||
<div id="edit-temp-list" class="bg-gray-800 border border-gray-700 rounded-lg mt-1 p-2 text-white text-sm hidden z-50" style="position:absolute;"></div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4" id="edit-target-resolution-field" style="display:none;">
|
|
||||||
<label for="edit-target-resolution" class="text-xs text-gray-400 mb-1">Target Resolution
|
|
||||||
<span class="text-gray-500 text-xs">(Optional)</span>
|
|
||||||
</label>
|
|
||||||
<select name="target_resolution" id="edit-target-resolution" class="bg-gray-700 border border-gray-600 rounded-lg px-2 py-1 text-white w-full">
|
|
||||||
<option value="">Default (Keep Original)</option>
|
|
||||||
<option value="480p">480p</option>
|
|
||||||
<option value="720p">720p</option>
|
|
||||||
<option value="1080p">1080p</option>
|
|
||||||
<option value="1440p">1440p</option>
|
|
||||||
<option value="2160p">4K (2160p)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4" id="edit-ffmpeg-flags-video" style="display:none;">
|
|
||||||
<label class="block text-xs text-gray-400 mb-1">FFmpeg Flags (Video)</label>
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label for="edit-crf" class="text-xs text-gray-400 mb-1">CRF (Quality)</label>
|
|
||||||
<select name="crf" id="edit-crf" class="bg-gray-700 border border-gray-600 rounded-lg px-2 py-1 text-white">
|
|
||||||
<option value="18">18</option>
|
|
||||||
<option value="20">20</option>
|
|
||||||
<option value="22">22</option>
|
|
||||||
<option value="24">24</option>
|
|
||||||
<option value="26">26</option>
|
|
||||||
<option value="28">28</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label for="edit-preset" class="text-xs text-gray-400 mb-1">Preset</label>
|
|
||||||
<select name="preset" id="edit-preset" class="bg-gray-700 border border-gray-600 rounded-lg px-2 py-1 text-white">
|
|
||||||
<option value="ultrafast">ultrafast</option>
|
|
||||||
<option value="superfast">superfast</option>
|
|
||||||
<option value="veryfast">veryfast</option>
|
|
||||||
<option value="faster">faster</option>
|
|
||||||
<option value="fast">fast</option>
|
|
||||||
<option value="medium">medium</option>
|
|
||||||
<option value="slow">slow</option>
|
|
||||||
<option value="slower">slower</option>
|
|
||||||
<option value="veryslow">veryslow</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
<input type="hidden" name="watch_folder" id="edit-folder-input" value="/">
|
||||||
|
<div id="edit-folder-list" class="md-card-outlined hidden" style="padding: 0.75rem; margin-top: 0.5rem; position: absolute;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="mb-4" id="edit-ffmpeg-flags-audio" style="display:none;">
|
<div style="margin-bottom: 1rem;">
|
||||||
<label class="block text-xs text-gray-400 mb-1">FFmpeg Flags (Audio)</label>
|
<label style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Temp Directory</label>
|
||||||
<div class="flex flex-col">
|
<div id="edit-temp-picker" class="md-card-outlined" style="padding: 0.75rem; cursor: pointer;">
|
||||||
<label for="edit-audio-bitrate" class="text-xs text-gray-400 mb-1">Bitrate</label>
|
<span id="edit-temp-path">/temp</span>
|
||||||
<select name="audio_bitrate" id="edit-audio-bitrate" class="bg-gray-700 border border-gray-600 rounded-lg px-2 py-1 text-white">
|
</div>
|
||||||
<option value="128k">128k</option>
|
<input type="hidden" name="temp_dir" id="edit-temp-input" value="/temp">
|
||||||
<option value="192k">192k</option>
|
<div id="edit-temp-list" class="md-card-outlined hidden" style="padding: 0.75rem; margin-top: 0.5rem; position: absolute;"></div>
|
||||||
<option value="256k">256k</option>
|
</div>
|
||||||
<option value="320k">320k</option>
|
|
||||||
|
<div class="md-text-field" id="edit-target-resolution-field" style="display:none;">
|
||||||
|
<select name="target_resolution" id="edit-target-resolution">
|
||||||
|
<option value="">Default (Keep Original)</option>
|
||||||
|
<option value="480p">480p</option>
|
||||||
|
<option value="720p">720p</option>
|
||||||
|
<option value="1080p">1080p</option>
|
||||||
|
<option value="1440p">1440p</option>
|
||||||
|
<option value="2160p">4K (2160p)</option>
|
||||||
</select>
|
</select>
|
||||||
|
<label for="edit-target-resolution">Target Resolution</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex justify-end space-x-2 mt-6">
|
<div id="edit-ffmpeg-flags-video" style="display:none; margin-top: 1rem;">
|
||||||
<button type="button" onclick="closeEditModal()" class="px-4 py-2 rounded-full bg-gray-700 hover:bg-gray-600 text-white font-semibold transition-all">Cancel</button>
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<button type="submit" class="px-4 py-2 rounded-full bg-sky-600 hover:bg-sky-500 text-white font-semibold transition-all">Save</button>
|
<div class="md-text-field">
|
||||||
</div>
|
<select name="crf" id="edit-crf">
|
||||||
</form>
|
<option value="18">18</option>
|
||||||
|
<option value="20">20</option>
|
||||||
|
<option value="22">22</option>
|
||||||
|
<option value="24">24</option>
|
||||||
|
<option value="26">26</option>
|
||||||
|
<option value="28">28</option>
|
||||||
|
</select>
|
||||||
|
<label for="edit-crf">CRF (Quality)</label>
|
||||||
|
</div>
|
||||||
|
<div class="md-text-field">
|
||||||
|
<select name="preset" id="edit-preset">
|
||||||
|
<option value="ultrafast">ultrafast</option>
|
||||||
|
<option value="superfast">superfast</option>
|
||||||
|
<option value="veryfast">veryfast</option>
|
||||||
|
<option value="faster">faster</option>
|
||||||
|
<option value="fast">fast</option>
|
||||||
|
<option value="medium">medium</option>
|
||||||
|
<option value="slow">slow</option>
|
||||||
|
<option value="slower">slower</option>
|
||||||
|
<option value="veryslow">veryslow</option>
|
||||||
|
</select>
|
||||||
|
<label for="edit-preset">Preset</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="edit-ffmpeg-flags-audio" style="display:none; margin-top: 1rem;">
|
||||||
|
<div class="md-text-field">
|
||||||
|
<select name="audio_bitrate" id="edit-audio-bitrate">
|
||||||
|
<option value="128k">128k</option>
|
||||||
|
<option value="192k">192k</option>
|
||||||
|
<option value="256k">256k</option>
|
||||||
|
<option value="320k">320k</option>
|
||||||
|
</select>
|
||||||
|
<label for="edit-audio-bitrate">Bitrate</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between" style="margin-top: 1.5rem;">
|
||||||
|
<button type="button" onclick="closeEditModal()" class="md-button md-button-text">Cancel</button>
|
||||||
|
<button type="submit" class="md-button md-button-filled">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -378,7 +333,7 @@ function setupFolderPicker(pickerId, pathId, inputId, listId) {
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
list.innerHTML = `<div class='text-red-400'>${data.error}</div>`;
|
list.innerHTML = `<div style="color: var(--md-sys-color-error);">${data.error}</div>`;
|
||||||
list.classList.remove('hidden');
|
list.classList.remove('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -386,7 +341,10 @@ function setupFolderPicker(pickerId, pathId, inputId, listId) {
|
|||||||
if (path !== '/') {
|
if (path !== '/') {
|
||||||
const up = document.createElement('div');
|
const up = document.createElement('div');
|
||||||
up.textContent = '.. (up)';
|
up.textContent = '.. (up)';
|
||||||
up.className = 'cursor-pointer hover:bg-gray-700 px-2 py-1 rounded';
|
up.className = 'md-card-outlined';
|
||||||
|
up.style.cursor = 'pointer';
|
||||||
|
up.style.padding = '0.5rem';
|
||||||
|
up.style.marginBottom = '0.25rem';
|
||||||
up.onclick = () => {
|
up.onclick = () => {
|
||||||
const parent = path.replace(/\/$/, '').split('/').slice(0, -1).join('/') || '/';
|
const parent = path.replace(/\/$/, '').split('/').slice(0, -1).join('/') || '/';
|
||||||
currentPath = parent;
|
currentPath = parent;
|
||||||
@@ -399,7 +357,10 @@ function setupFolderPicker(pickerId, pathId, inputId, listId) {
|
|||||||
data.dirs.forEach(dir => {
|
data.dirs.forEach(dir => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = dir;
|
div.textContent = dir;
|
||||||
div.className = 'cursor-pointer hover:bg-gray-700 px-2 py-1 rounded';
|
div.className = 'md-card-outlined';
|
||||||
|
div.style.cursor = 'pointer';
|
||||||
|
div.style.padding = '0.5rem';
|
||||||
|
div.style.marginBottom = '0.25rem';
|
||||||
div.onclick = () => {
|
div.onclick = () => {
|
||||||
let newPath = path === '/' ? `/${dir}` : `${path}/${dir}`;
|
let newPath = path === '/' ? `/${dir}` : `${path}/${dir}`;
|
||||||
currentPath = newPath;
|
currentPath = newPath;
|
||||||
|
|||||||
+133
-124
@@ -1,171 +1,180 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="px-4 sm:px-0 max-w-7xl mx-auto">
|
<div class="fade-in">
|
||||||
<div class="mb-8">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-fuchsia-400 via-sky-400 to-cyan-400 mb-2 tracking-tight">Encoder Dashboard</h1>
|
<div>
|
||||||
<p class="text-gray-300 text-lg">Monitor your video and audio encoding jobs</p>
|
<h1 class="md-display-medium mb-2">Encoder Dashboard</h1>
|
||||||
|
<p style="color: var(--md-sys-color-on-surface-variant);">Monitor your video and audio encoding jobs in real time.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('reports_dashboard') }}" class="md-button md-button-outlined">
|
||||||
|
<i class="fas fa-chart-line"></i> Explore Reports
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<!-- Video Encoder Column -->
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
<div class="bg-gray-900 rounded-2xl modern-shadow p-8 transition-all hover:scale-[1.01]">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<h2 class="md-title-large" style="margin: 0;">
|
||||||
<h2 class="text-2xl font-bold text-white flex items-center">
|
<i class="fas fa-video" style="color: var(--md-sys-color-primary);"></i>
|
||||||
<svg class="w-7 h-7 mr-2 text-sky-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
||||||
</svg>
|
|
||||||
Video Encoder
|
Video Encoder
|
||||||
</h2>
|
</h2>
|
||||||
<a href="{{ url_for('reports', encoder_type='video') }}" class="text-sky-400 hover:text-white text-base font-semibold transition-all">View Reports</a>
|
<a href="{{ url_for('reports', encoder_type='video') }}" class="md-button md-button-text">Job Reports</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Current Job Status -->
|
<div class="md-card-outlined" style="padding: 1rem; margin-bottom: 1rem;">
|
||||||
<div class="mb-6">
|
<div class="flex items-center justify-between">
|
||||||
<h3 class="text-lg font-semibold text-white mb-3">Current Job</h3>
|
<div>
|
||||||
<div class="bg-gray-800 rounded-xl p-4">
|
<div class="md-title-large" style="font-size: 1rem;">Current Job</div>
|
||||||
<div id="video-status">
|
<div id="video-status" style="margin-top: 0.5rem;">
|
||||||
{% if current_video_job %}
|
{% if current_video_job %}
|
||||||
<div class="flex items-center space-x-2">
|
<span class="md-chip"><span class="status-dot green"></span> Processing: {{ current_video_job.file_path.split('/')[-1] }}</span>
|
||||||
<div class="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
|
<span style="margin-left: 0.5rem; color: var(--md-sys-color-on-surface-variant); font-size: 0.875rem;">
|
||||||
<span class="text-sm">Processing: {{ current_video_job.file_path.split('/')[-1] }}</span>
|
({{ current_video_job_duration|format_duration }})
|
||||||
<span class="text-xs text-gray-400">({{ current_video_job_duration|format_duration }})</span>
|
</span>
|
||||||
</div>
|
{% else %}
|
||||||
{% else %}
|
<span class="md-chip"><span class="status-dot gray"></span> Idle</span>
|
||||||
<div class="flex items-center space-x-2">
|
{% endif %}
|
||||||
<div class="w-3 h-3 bg-gray-500 rounded-full"></div>
|
</div>
|
||||||
<span class="text-sm">Idle</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Queue -->
|
<div class="md-card-outlined" style="padding: 1rem; margin-bottom: 1rem;">
|
||||||
<div class="mb-6">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<h3 class="text-lg font-semibold text-white mb-3">
|
<div class="md-title-large" style="font-size: 1rem;">Queue</div>
|
||||||
Queue (<span id="video-queue-length">{{ video_queue_length }}</span>)
|
<span class="md-chip">Items: <span id="video-queue-length">{{ video_queue_length }}</span></span>
|
||||||
</h3>
|
</div>
|
||||||
<div class="bg-gray-800 rounded-xl p-4 max-h-64 overflow-y-auto">
|
<div id="video-queue-container" style="max-height: 16rem; overflow-y: auto;">
|
||||||
<div id="video-queue-container">
|
{% if video_queue %}
|
||||||
{% if video_queue %}
|
<div class="space-y-2">
|
||||||
<div class="space-y-2">
|
{% for file_path in video_queue %}
|
||||||
{% for file_path in video_queue %}
|
<div class="md-card-outlined" style="padding: 0.5rem 0.75rem;">
|
||||||
<div class="flex items-center space-x-2 text-sm">
|
<span class="status-dot" style="background: #f59e0b;"></span>
|
||||||
<div class="w-2 h-2 bg-yellow-500 rounded-full"></div>
|
<span style="margin-left: 0.5rem;">{{ file_path.split('/')[-1] }}</span>
|
||||||
<span class="text-gray-300">{{ file_path.split('/')[-1] }}</span>
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
{% else %}
|
<div style="color: var(--md-sys-color-on-surface-variant); text-align: center; padding: 1rem;">
|
||||||
<p class="text-gray-400 text-sm">No files in queue</p>
|
No files in queue
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statistics -->
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="bg-gray-800 rounded-xl p-4 flex flex-col items-center">
|
<div class="md-card-outlined" style="padding: 1rem; text-align: center;">
|
||||||
<div id="video-total-jobs" class="text-3xl font-extrabold text-sky-400">{{ video_stats.total_jobs or 0 }}</div>
|
<div id="video-total-saved" class="md-title-large" style="font-weight: 700;">
|
||||||
<div class="text-base text-gray-400">Total Jobs</div>
|
{{ video_stats.total_saved|format_bytes }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Space Saved</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded-xl p-4 flex flex-col items-center">
|
<div class="md-card-outlined" style="padding: 1rem; text-align: center;">
|
||||||
<div id="video-total-saved" class="text-3xl font-extrabold text-green-400">{{ video_stats.total_saved|format_bytes }}</div>
|
<div class="md-title-large" style="font-weight: 700;">
|
||||||
<div class="text-base text-gray-400">Space Saved</div>
|
{{ (video_stats.total_original_size or 0)|format_bytes }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Original Size</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Audio Encoder Column -->
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
<div class="bg-gray-900 rounded-2xl modern-shadow p-8 transition-all hover:scale-[1.01]">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<h2 class="md-title-large" style="margin: 0;">
|
||||||
<h2 class="text-2xl font-bold text-white flex items-center">
|
<i class="fas fa-music" style="color: var(--md-sys-color-tertiary);"></i>
|
||||||
<svg class="w-7 h-7 mr-2 text-fuchsia-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
|
|
||||||
</svg>
|
|
||||||
Audio Encoder
|
Audio Encoder
|
||||||
</h2>
|
</h2>
|
||||||
<a href="{{ url_for('reports', encoder_type='audio') }}" class="text-fuchsia-400 hover:text-white text-base font-semibold transition-all">View Reports</a>
|
<a href="{{ url_for('reports', encoder_type='audio') }}" class="md-button md-button-text">Job Reports</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Current Job Status -->
|
<div class="md-card-outlined" style="padding: 1rem; margin-bottom: 1rem;">
|
||||||
<div class="mb-6">
|
<div class="flex items-center justify-between">
|
||||||
<h3 class="text-lg font-semibold text-white mb-3">Current Job</h3>
|
<div>
|
||||||
<div class="bg-gray-800 rounded-xl p-4">
|
<div class="md-title-large" style="font-size: 1rem;">Current Job</div>
|
||||||
<div id="audio-status">
|
<div id="audio-status" style="margin-top: 0.5rem;">
|
||||||
{% if current_audio_job %}
|
{% if current_audio_job %}
|
||||||
<div class="flex items-center space-x-2">
|
<span class="md-chip"><span class="status-dot green"></span> Processing: {{ current_audio_job.file_path.split('/')[-1] }}</span>
|
||||||
<div class="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
|
<span style="margin-left: 0.5rem; color: var(--md-sys-color-on-surface-variant); font-size: 0.875rem;">
|
||||||
<span class="text-sm">Processing: {{ current_audio_job.file_path.split('/')[-1] }}</span>
|
({{ current_audio_job_duration|format_duration }})
|
||||||
<span class="text-xs text-gray-400">({{ current_audio_job_duration|format_duration }})</span>
|
</span>
|
||||||
</div>
|
{% else %}
|
||||||
{% else %}
|
<span class="md-chip"><span class="status-dot gray"></span> Idle</span>
|
||||||
<div class="flex items-center space-x-2">
|
{% endif %}
|
||||||
<div class="w-3 h-3 bg-gray-500 rounded-full"></div>
|
</div>
|
||||||
<span class="text-sm">Idle</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Queue -->
|
<div class="md-card-outlined" style="padding: 1rem; margin-bottom: 1rem;">
|
||||||
<div class="mb-6">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<h3 class="text-lg font-semibold text-white mb-3">
|
<div class="md-title-large" style="font-size: 1rem;">Queue</div>
|
||||||
Queue (<span id="audio-queue-length">{{ audio_queue_length }}</span>)
|
<span class="md-chip">Items: <span id="audio-queue-length">{{ audio_queue_length }}</span></span>
|
||||||
</h3>
|
</div>
|
||||||
<div class="bg-gray-800 rounded-xl p-4 max-h-64 overflow-y-auto">
|
<div id="audio-queue-container" style="max-height: 16rem; overflow-y: auto;">
|
||||||
<div id="audio-queue-container">
|
{% if audio_queue %}
|
||||||
{% if audio_queue %}
|
<div class="space-y-2">
|
||||||
<div class="space-y-2">
|
{% for file_path in audio_queue %}
|
||||||
{% for file_path in audio_queue %}
|
<div class="md-card-outlined" style="padding: 0.5rem 0.75rem;">
|
||||||
<div class="flex items-center space-x-2 text-sm">
|
<span class="status-dot" style="background: #f59e0b;"></span>
|
||||||
<div class="w-2 h-2 bg-yellow-500 rounded-full"></div>
|
<span style="margin-left: 0.5rem;">{{ file_path.split('/')[-1] }}</span>
|
||||||
<span class="text-gray-300">{{ file_path.split('/')[-1] }}</span>
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
{% else %}
|
<div style="color: var(--md-sys-color-on-surface-variant); text-align: center; padding: 1rem;">
|
||||||
<p class="text-gray-400 text-sm">No files in queue</p>
|
No files in queue
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statistics -->
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="bg-gray-800 rounded-xl p-4 flex flex-col items-center">
|
<div class="md-card-outlined" style="padding: 1rem; text-align: center;">
|
||||||
<div id="audio-total-jobs" class="text-3xl font-extrabold text-fuchsia-400">{{ audio_stats.total_jobs or 0 }}</div>
|
<div id="audio-total-saved" class="md-title-large" style="font-weight: 700;">
|
||||||
<div class="text-base text-gray-400">Total Jobs</div>
|
{{ audio_stats.total_saved|format_bytes }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Space Saved</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded-xl p-4 flex flex-col items-center">
|
<div class="md-card-outlined" style="padding: 1rem; text-align: center;">
|
||||||
<div id="audio-total-saved" class="text-3xl font-extrabold text-green-400">{{ audio_stats.total_saved|format_bytes }}</div>
|
<div class="md-title-large" style="font-weight: 700;">
|
||||||
<div class="text-base text-gray-400">Space Saved</div>
|
{{ (audio_stats.total_original_size or 0)|format_bytes }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Original Size</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Overall Statistics -->
|
<div class="md-card md-card-elevated" style="padding: 1.5rem; margin-top: 2rem;">
|
||||||
<div class="mt-8 bg-gradient-to-r from-fuchsia-900 via-gray-900 to-sky-900 rounded-2xl modern-shadow p-8">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">Overall Statistics</h2>
|
<h2 class="md-title-large" style="margin: 0;">Overall Performance</h2>
|
||||||
|
<a href="{{ url_for('reports_dashboard') }}" class="md-button md-button-text">Full Analytics</a>
|
||||||
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<div class="bg-gray-800 rounded-xl p-4 text-center">
|
<div class="md-card-outlined" style="padding: 1rem; text-align: center;">
|
||||||
<div id="overall-total-jobs" class="text-3xl font-extrabold text-sky-400">{{ (video_stats.total_jobs or 0) + (audio_stats.total_jobs or 0) }}</div>
|
<div id="overall-total-jobs" class="md-title-large" style="font-weight: 700;">
|
||||||
<div class="text-base text-gray-400">Total Jobs Processed</div>
|
{{ (video_stats.total_jobs or 0) + (audio_stats.total_jobs or 0) }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Total Jobs</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded-xl p-4 text-center">
|
<div class="md-card-outlined" style="padding: 1rem; text-align: center;">
|
||||||
<div id="overall-total-saved" class="text-3xl font-extrabold text-green-400">{{ ((video_stats.total_saved or 0) + (audio_stats.total_saved or 0))|format_bytes }}</div>
|
<div id="overall-total-saved" class="md-title-large" style="font-weight: 700;">
|
||||||
<div class="text-base text-gray-400">Total Space Saved</div>
|
{{ ((video_stats.total_saved or 0) + (audio_stats.total_saved or 0))|format_bytes }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Space Saved</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded-xl p-4 text-center">
|
<div class="md-card-outlined" style="padding: 1rem; text-align: center;">
|
||||||
<div id="overall-original-size" class="text-3xl font-extrabold text-yellow-400">{{ ((video_stats.total_original_size or 0) + (audio_stats.total_original_size or 0))|format_bytes }}</div>
|
<div id="overall-original-size" class="md-title-large" style="font-weight: 700;">
|
||||||
<div class="text-base text-gray-400">Original Size</div>
|
{{ ((video_stats.total_original_size or 0) + (audio_stats.total_original_size or 0))|format_bytes }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Original Size</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded-xl p-4 text-center">
|
<div class="md-card-outlined" style="padding: 1rem; text-align: center;">
|
||||||
<div id="overall-encoded-size" class="text-3xl font-extrabold text-fuchsia-400">{{ ((video_stats.total_encoded_size or 0) + (audio_stats.total_encoded_size or 0))|format_bytes }}</div>
|
<div id="overall-encoded-size" class="md-title-large" style="font-weight: 700;">
|
||||||
<div class="text-base text-gray-400">Encoded Size</div>
|
{{ ((video_stats.total_encoded_size or 0) + (audio_stats.total_encoded_size or 0))|format_bytes }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">Encoded Size</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+96
-164
@@ -3,141 +3,112 @@
|
|||||||
{% block title %}{{ encoder_type.title() }} Reports - Encoder{% endblock %}
|
{% block title %}{{ encoder_type.title() }} Reports - Encoder{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="px-4 sm:px-0 max-w-full mx-auto">
|
<div class="fade-in">
|
||||||
<div class="mb-8">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div class="flex items-center justify-between">
|
<div>
|
||||||
<div>
|
<h1 class="md-display-medium mb-2">{{ encoder_type.title() }} Encoder Reports</h1>
|
||||||
<h1 class="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-fuchsia-400 via-sky-400 to-cyan-400 mb-2 tracking-tight">{{ encoder_type.title() }} Encoder Reports</h1>
|
<p style="color: var(--md-sys-color-on-surface-variant);">Job history and statistics for {{ encoder_type }} encoding.</p>
|
||||||
<p class="text-gray-300 text-lg">Job history and statistics for {{ encoder_type }} encoding</p>
|
</div>
|
||||||
</div>
|
<div class="flex gap-2">
|
||||||
<a href="{{ url_for('index') }}" class="bg-gradient-to-r from-sky-600 to-fuchsia-600 hover:from-sky-500 hover:to-fuchsia-500 text-white px-6 py-2 rounded-full text-base shadow-lg font-semibold transition-all">
|
<a href="{{ url_for('reports_dashboard') }}" class="md-button md-button-text">
|
||||||
← Back to Dashboard
|
<i class="fas fa-chart-line"></i> Reports Dashboard
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('index') }}" class="md-button md-button-outlined">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-900 rounded-2xl modern-shadow overflow-hidden px-2 sm:px-6 md:px-12">
|
|
||||||
<div class="px-8 py-6 border-b border-gray-800">
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold text-white">Job Reports</h2>
|
<h2 class="md-title-large" style="margin: 0;">Job Reports</h2>
|
||||||
<p class="text-base text-gray-400">Total: {{ total }} jobs</p>
|
<p style="color: var(--md-sys-color-on-surface-variant);">Total: {{ total }} jobs</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Search Form -->
|
|
||||||
<form method="GET" class="mb-4">
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<input type="text" name="search" value="{{ search_term or '' }}" placeholder="Search files or error messages..."
|
|
||||||
class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent">
|
|
||||||
<button type="submit" class="bg-sky-600 hover:bg-sky-700 text-white px-4 py-2 rounded-lg text-base font-semibold transition-all">
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
{% if search_term %}
|
|
||||||
<a href="{{ url_for('reports', encoder_type=encoder_type) }}" class="bg-gray-700 hover:bg-gray-800 text-white px-4 py-2 rounded-lg text-base font-semibold transition-all">
|
|
||||||
Clear
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form method="GET" class="grid grid-cols-1 md:grid-cols-3 gap-4" style="margin-bottom: 1.5rem;">
|
||||||
|
<div class="md-text-field" style="grid-column: span 2;">
|
||||||
|
<input type="text" name="search" value="{{ search_term or '' }}" placeholder=" " id="report-search">
|
||||||
|
<label for="report-search">Search files or error messages...</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="submit" class="md-button md-button-filled">
|
||||||
|
<i class="fas fa-magnifying-glass"></i> Search
|
||||||
|
</button>
|
||||||
|
{% if search_term %}
|
||||||
|
<a href="{{ url_for('reports', encoder_type=encoder_type) }}" class="md-button md-button-text">Clear</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
{% if reports %}
|
{% if reports %}
|
||||||
<div class="overflow-x-auto">
|
<div style="overflow-x: auto;">
|
||||||
<table class="w-full min-w-full divide-y divide-gray-800 rounded-xl modern-shadow">
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
<thead class="bg-gray-800">
|
<thead>
|
||||||
<tr>
|
<tr style="text-align: left; border-bottom: 1px solid var(--md-sys-color-outline-variant);">
|
||||||
<th class="px-6 py-3 text-left text-xs font-bold text-sky-300 uppercase tracking-wider">File</th>
|
<th style="padding: 0.75rem;">File</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-bold text-sky-300 uppercase tracking-wider">Status</th>
|
<th style="padding: 0.75rem;">Status</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-bold text-sky-300 uppercase tracking-wider">Original Size</th>
|
<th style="padding: 0.75rem;">Original Size</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-bold text-sky-300 uppercase tracking-wider">Encoded Size</th>
|
<th style="padding: 0.75rem;">Encoded Size</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-bold text-sky-300 uppercase tracking-wider">Space Saved</th>
|
<th style="padding: 0.75rem;">Space Saved</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-bold text-sky-300 uppercase tracking-wider">Format</th>
|
<th style="padding: 0.75rem;">Format</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-bold text-sky-300 uppercase tracking-wider">Processing Time</th>
|
<th style="padding: 0.75rem;">Processing Time</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-bold text-sky-300 uppercase tracking-wider">Date</th>
|
<th style="padding: 0.75rem;">Date</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-bold text-sky-300 uppercase tracking-wider">Actions</th>
|
<th style="padding: 0.75rem;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-gray-900 divide-y divide-gray-800">
|
<tbody>
|
||||||
{% for report in reports %}
|
{% for report in reports %}
|
||||||
<tr class="hover:bg-gray-800 transition-all">
|
<tr style="border-bottom: 1px solid var(--md-sys-color-outline-variant);">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td style="padding: 0.75rem;">
|
||||||
<div class="text-base text-white font-semibold">{{ report.file_path.split('/')[-1] }}</div>
|
<div style="font-weight: 600;">{{ report.file_path.split('/')[-1] }}</div>
|
||||||
<div class="text-xs text-gray-400">{{ report.file_path }}</div>
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">{{ report.file_path }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td style="padding: 0.75rem;">
|
||||||
{% if report.status == 'success' %}
|
{% if report.status == 'success' %}
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-green-900 text-green-200">
|
<span class="md-chip"><span class="status-dot green"></span> Success</span>
|
||||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/>
|
|
||||||
</svg>
|
|
||||||
Success
|
|
||||||
</span>
|
|
||||||
{% elif report.status == 'failed' %}
|
{% elif report.status == 'failed' %}
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-red-900 text-red-200">
|
<span class="md-chip"><span class="status-dot red"></span> Failed</span>
|
||||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"/>
|
|
||||||
</svg>
|
|
||||||
Failed
|
|
||||||
</span>
|
|
||||||
{% elif report.status == 'skipped' %}
|
{% elif report.status == 'skipped' %}
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-sky-900 text-sky-200">
|
<span class="md-chip"><span class="status-dot gray"></span> Skipped</span>
|
||||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2" fill="none" />
|
|
||||||
<path fill-rule="evenodd" d="M6 10a4 4 0 118 0 4 4 0 01-8 0z" fill="currentColor" />
|
|
||||||
</svg>
|
|
||||||
Skipped
|
|
||||||
</span>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-yellow-900 text-yellow-200">
|
<span class="md-chip"><span class="status-dot" style="background: #f59e0b;"></span> Processing</span>
|
||||||
<svg class="w-3 h-3 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
Processing
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-base text-gray-300">
|
<td style="padding: 0.75rem;">{{ report.original_size|format_bytes }}</td>
|
||||||
{{ report.original_size|format_bytes }}
|
<td style="padding: 0.75rem;">{{ report.encoded_size|format_bytes }}</td>
|
||||||
</td>
|
<td style="padding: 0.75rem;">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-base text-gray-300">
|
|
||||||
{{ report.encoded_size|format_bytes }}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-base">
|
|
||||||
{% if report.size_saved > 0 %}
|
{% if report.size_saved > 0 %}
|
||||||
<span class="text-green-400 font-semibold">{{ report.size_saved|format_bytes }}</span>
|
<span style="color: #10b981; font-weight: 600;">{{ report.size_saved|format_bytes }}</span>
|
||||||
{% elif report.size_saved < 0 %}
|
{% elif report.size_saved < 0 %}
|
||||||
<span class="text-red-400 font-semibold">{{ report.size_saved|format_bytes }}</span>
|
<span style="color: #ef4444; font-weight: 600;">{{ report.size_saved|format_bytes }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-gray-400 font-semibold">{{ report.size_saved|format_bytes }}</span>
|
<span style="color: var(--md-sys-color-on-surface-variant); font-weight: 600;">{{ report.size_saved|format_bytes }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-base text-gray-300">
|
<td style="padding: 0.75rem;">
|
||||||
<span class="text-gray-400">{{ report.original_format }}</span>
|
<span style="color: var(--md-sys-color-on-surface-variant);">{{ report.original_format }}</span>
|
||||||
<span class="text-gray-500">→</span>
|
<span>→</span>
|
||||||
<span class="text-white">{{ report.encoded_format }}</span>
|
<span>{{ report.encoded_format }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-base text-gray-300">
|
<td style="padding: 0.75rem;">{{ report.processing_time|format_duration }}</td>
|
||||||
{{ report.processing_time|format_duration }}
|
<td style="padding: 0.75rem;">
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-base text-gray-300">
|
|
||||||
<div>{{ report.created_at.strftime('%Y-%m-%d') }}</div>
|
<div>{{ report.created_at.strftime('%Y-%m-%d') }}</div>
|
||||||
<div class="text-xs text-gray-400">{{ report.created_at.strftime('%H:%M:%S') }}</div>
|
<div style="font-size: 0.75rem; color: var(--md-sys-color-on-surface-variant);">{{ report.created_at.strftime('%H:%M:%S') }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-base text-gray-300">
|
<td style="padding: 0.75rem;">
|
||||||
{% if report.status == 'failed' %}
|
{% if report.status == 'failed' %}
|
||||||
<button onclick="requeueJob({{ report.id }})" class="bg-gradient-to-r from-yellow-500 to-yellow-700 hover:from-yellow-400 hover:to-yellow-600 text-white px-3 py-1 rounded-full text-xs font-semibold transition-all">
|
<button onclick="requeueJob({{ report.id }})" class="md-button md-button-text">Requeue</button>
|
||||||
Requeue
|
|
||||||
</button>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-gray-500">-</span>
|
<span style="color: var(--md-sys-color-on-surface-variant);">-</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if report.status == 'failed' and report.error_message %}
|
{% if report.status == 'failed' and report.error_message %}
|
||||||
<tr class="bg-red-900 bg-opacity-20">
|
<tr>
|
||||||
<td colspan="9" class="px-6 py-2">
|
<td colspan="9" style="padding: 0.75rem; color: var(--md-sys-color-error);">
|
||||||
<div class="text-base text-red-300">
|
<strong>Error:</strong> {{ report.error_message }}
|
||||||
<span class="font-semibold">Error:</span> {{ report.error_message }}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -145,71 +116,32 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<!-- Pagination -->
|
|
||||||
{% if total_pages > 1 %}
|
{% if total_pages > 1 %}
|
||||||
<div class="bg-gray-700 px-4 py-3 flex items-center justify-between border-t border-gray-600 sm:px-6">
|
<div class="flex items-center justify-between" style="margin-top: 1.5rem;">
|
||||||
<div class="flex-1 flex justify-between sm:hidden">
|
<div style="color: var(--md-sys-color-on-surface-variant); font-size: 0.875rem;">
|
||||||
{% if page > 1 %}
|
Showing {{ (page-1)*20 + 1 }} to {{ min(page*20, total) }} of {{ total }} results
|
||||||
<a href="{{ url_for('reports', encoder_type=encoder_type, page=page-1) }}" class="relative inline-flex items-center px-4 py-2 border border-gray-600 text-sm font-medium rounded-md text-gray-300 bg-gray-800 hover:bg-gray-700">
|
|
||||||
Previous
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if page < total_pages %}
|
|
||||||
<a href="{{ url_for('reports', encoder_type=encoder_type, page=page+1) }}" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-600 text-sm font-medium rounded-md text-gray-300 bg-gray-800 hover:bg-gray-700">
|
|
||||||
Next
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
<div class="flex items-center gap-2">
|
||||||
<div>
|
{% if page > 1 %}
|
||||||
<p class="text-sm text-gray-300">
|
<a href="{{ url_for('reports', encoder_type=encoder_type, page=page-1) }}" class="md-button md-button-text">Previous</a>
|
||||||
Showing <span class="font-medium">{{ (page-1)*20 + 1 }}</span> to <span class="font-medium">{{ min(page*20, total) }}</span> of <span class="font-medium">{{ total }}</span> results
|
{% endif %}
|
||||||
</p>
|
{% for p in range(max(1, page - 2), min(total_pages, page + 2) + 1) %}
|
||||||
</div>
|
{% if p == page %}
|
||||||
<div>
|
<span class="md-chip">{{ p }}</span>
|
||||||
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
|
{% else %}
|
||||||
{% if page > 1 %}
|
<a href="{{ url_for('reports', encoder_type=encoder_type, page=p) }}" class="md-button md-button-text">{{ p }}</a>
|
||||||
<a href="{{ url_for('reports', encoder_type=encoder_type, page=page-1) }}" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-600 bg-gray-800 text-sm font-medium text-gray-300 hover:bg-gray-700">
|
{% endif %}
|
||||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
{% endfor %}
|
||||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"/>
|
{% if page < total_pages %}
|
||||||
</svg>
|
<a href="{{ url_for('reports', encoder_type=encoder_type, page=page+1) }}" class="md-button md-button-text">Next</a>
|
||||||
</a>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set start_page = max(1, page - 2) %}
|
|
||||||
{% set end_page = min(total_pages, page + 2) %}
|
|
||||||
|
|
||||||
{% for p in range(start_page, end_page + 1) %}
|
|
||||||
{% if p == page %}
|
|
||||||
<span class="relative inline-flex items-center px-4 py-2 border border-gray-600 bg-gray-600 text-sm font-medium text-white">
|
|
||||||
{{ p }}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ url_for('reports', encoder_type=encoder_type, page=p) }}" class="relative inline-flex items-center px-4 py-2 border border-gray-600 bg-gray-800 text-sm font-medium text-gray-300 hover:bg-gray-700">
|
|
||||||
{{ p }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if page < total_pages %}
|
|
||||||
<a href="{{ url_for('reports', encoder_type=encoder_type, page=page+1) }}" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-600 bg-gray-800 text-sm font-medium text-gray-300 hover:bg-gray-700">
|
|
||||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="px-6 py-12 text-center">
|
<div class="md-card-outlined" style="padding: 2rem; text-align: center;">
|
||||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<p style="color: var(--md-sys-color-on-surface-variant);">No encoding jobs have been completed yet.</p>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
||||||
</svg>
|
|
||||||
<h3 class="mt-2 text-sm font-medium text-gray-300">No reports found</h3>
|
|
||||||
<p class="mt-1 text-sm text-gray-400">No encoding jobs have been completed yet.</p>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Reports Dashboard - Encoder{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="fade-in">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="md-display-medium mb-2">Reports Dashboard</h1>
|
||||||
|
<p style="color: var(--md-sys-color-on-surface-variant);">Deep dive into savings, throughput, and encoding reliability.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a href="{{ url_for('reports', encoder_type='video') }}" class="md-button md-button-text">
|
||||||
|
<i class="fas fa-video"></i> Video Jobs
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('reports', encoder_type='audio') }}" class="md-button md-button-text">
|
||||||
|
<i class="fas fa-music"></i> Audio Jobs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1rem;">
|
||||||
|
<div class="md-title-large">{{ summary.total_jobs or 0 }}</div>
|
||||||
|
<div style="color: var(--md-sys-color-on-surface-variant); font-size: 0.875rem;">Total Jobs</div>
|
||||||
|
</div>
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1rem;">
|
||||||
|
<div class="md-title-large">{{ summary.total_saved|format_bytes }}</div>
|
||||||
|
<div style="color: var(--md-sys-color-on-surface-variant); font-size: 0.875rem;">Total Saved</div>
|
||||||
|
</div>
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1rem;">
|
||||||
|
<div class="md-title-large">{{ summary.avg_processing_time|format_duration }}</div>
|
||||||
|
<div style="color: var(--md-sys-color-on-surface-variant); font-size: 0.875rem;">Avg Processing Time</div>
|
||||||
|
</div>
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1rem;">
|
||||||
|
<div class="md-title-large">{{ (savings_ratio * 100) | round(1) }}%</div>
|
||||||
|
<div style="color: var(--md-sys-color-on-surface-variant); font-size: 0.875rem;">Savings Ratio</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8" style="margin-top: 2rem;">
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
|
<h2 class="md-title-large">Storage Saved Over Time</h2>
|
||||||
|
<canvas id="savingsOverTimeChart" height="120"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
|
<h2 class="md-title-large">Jobs Over Time</h2>
|
||||||
|
<canvas id="jobsOverTimeChart" height="120"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
|
<h2 class="md-title-large">Status Breakdown</h2>
|
||||||
|
<canvas id="statusBreakdownChart" height="180"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
|
<h2 class="md-title-large">Average Processing Time</h2>
|
||||||
|
<canvas id="processingTimeChart" height="180"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
|
<h2 class="md-title-large">Savings by Encoder</h2>
|
||||||
|
<canvas id="encoderSavingsChart" height="180"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
|
<h2 class="md-title-large">Top Folders by Savings</h2>
|
||||||
|
<canvas id="topFoldersChart" height="140"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
|
<h2 class="md-title-large">Format Breakdown</h2>
|
||||||
|
<canvas id="formatBreakdownChart" height="140"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="md-card md-card-elevated" style="padding: 1.5rem;">
|
||||||
|
<h2 class="md-title-large">Top Error Messages</h2>
|
||||||
|
{% if top_errors %}
|
||||||
|
<div class="space-y-2">
|
||||||
|
{% for error in top_errors %}
|
||||||
|
<div class="md-card-outlined" style="padding: 0.75rem;">
|
||||||
|
<div style="font-weight: 600;">{{ error.total_errors }}x</div>
|
||||||
|
<div style="color: var(--md-sys-color-on-surface-variant); font-size: 0.875rem;">{{ error.error_message }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="color: var(--md-sys-color-on-surface-variant);">No errors recorded yet.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
const savingsOverTime = {{ savings_over_time|tojson }};
|
||||||
|
const jobsOverTime = {{ jobs_over_time|tojson }};
|
||||||
|
const processingTimeOverTime = {{ processing_time_over_time|tojson }};
|
||||||
|
const savingsByEncoder = {{ savings_by_encoder|tojson }};
|
||||||
|
const formatBreakdown = {{ format_breakdown|tojson }};
|
||||||
|
const topFolders = {{ top_folders|tojson }};
|
||||||
|
const summary = {{ summary|tojson }};
|
||||||
|
|
||||||
|
const savingsLabels = savingsOverTime.map(item => item.day);
|
||||||
|
const savingsValues = savingsOverTime.map(item => item.total_saved || 0);
|
||||||
|
|
||||||
|
const jobsLabels = jobsOverTime.map(item => item.day);
|
||||||
|
const jobsTotal = jobsOverTime.map(item => item.total_jobs || 0);
|
||||||
|
const jobsSuccess = jobsOverTime.map(item => item.success_jobs || 0);
|
||||||
|
const jobsFailed = jobsOverTime.map(item => item.failed_jobs || 0);
|
||||||
|
const jobsSkipped = jobsOverTime.map(item => item.skipped_jobs || 0);
|
||||||
|
|
||||||
|
const processingLabels = processingTimeOverTime.map(item => item.day);
|
||||||
|
const processingValues = processingTimeOverTime.map(item => item.avg_processing_time || 0);
|
||||||
|
|
||||||
|
const encoderLabels = savingsByEncoder.map(item => item.encoder_type || 'unknown');
|
||||||
|
const encoderSavings = savingsByEncoder.map(item => item.total_saved || 0);
|
||||||
|
|
||||||
|
const folderLabels = topFolders.map(item => item.folder);
|
||||||
|
const folderSavings = topFolders.map(item => item.total_saved || 0);
|
||||||
|
|
||||||
|
const formatLabels = formatBreakdown.map(item => `${item.original_format} → ${item.encoded_format}`);
|
||||||
|
const formatCounts = formatBreakdown.map(item => item.total_jobs || 0);
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes == null || bytes === 0) return '0 B';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
let size = Math.abs(bytes);
|
||||||
|
let unitIndex = 0;
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024;
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
const sign = bytes < 0 ? '-' : '';
|
||||||
|
return sign + size.toFixed(1) + ' ' + units[unitIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
new Chart(document.getElementById('savingsOverTimeChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: savingsLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Storage Saved',
|
||||||
|
data: savingsValues,
|
||||||
|
borderColor: '#4CDADB',
|
||||||
|
backgroundColor: 'rgba(76, 218, 219, 0.2)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => `Saved: ${formatBytes(ctx.parsed.y)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => formatBytes(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(document.getElementById('jobsOverTimeChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: jobsLabels,
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Total', data: jobsTotal, backgroundColor: 'rgba(76, 218, 219, 0.4)' },
|
||||||
|
{ label: 'Success', data: jobsSuccess, backgroundColor: 'rgba(16, 185, 129, 0.6)' },
|
||||||
|
{ label: 'Failed', data: jobsFailed, backgroundColor: 'rgba(239, 68, 68, 0.6)' },
|
||||||
|
{ label: 'Skipped', data: jobsSkipped, backgroundColor: 'rgba(107, 114, 128, 0.6)' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
x: { stacked: true },
|
||||||
|
y: { stacked: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(document.getElementById('statusBreakdownChart'), {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: ['Success', 'Failed', 'Skipped'],
|
||||||
|
datasets: [{
|
||||||
|
data: [
|
||||||
|
summary.success_jobs || 0,
|
||||||
|
summary.failed_jobs || 0,
|
||||||
|
summary.skipped_jobs || 0
|
||||||
|
],
|
||||||
|
backgroundColor: ['#10b981', '#ef4444', '#6b7280']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'bottom' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(document.getElementById('processingTimeChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: processingLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Avg Processing Time (s)',
|
||||||
|
data: processingValues,
|
||||||
|
borderColor: '#A5C9E2',
|
||||||
|
backgroundColor: 'rgba(165, 201, 226, 0.2)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(document.getElementById('encoderSavingsChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: encoderLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Total Saved',
|
||||||
|
data: encoderSavings,
|
||||||
|
backgroundColor: 'rgba(76, 218, 219, 0.5)'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => `Saved: ${formatBytes(ctx.parsed.y)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => formatBytes(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(document.getElementById('topFoldersChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: folderLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Total Saved',
|
||||||
|
data: folderSavings,
|
||||||
|
backgroundColor: 'rgba(76, 218, 219, 0.5)'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => `Saved: ${formatBytes(ctx.parsed.x)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => formatBytes(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(document.getElementById('formatBreakdownChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: formatLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jobs',
|
||||||
|
data: formatCounts,
|
||||||
|
backgroundColor: 'rgba(165, 201, 226, 0.6)'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
Reference in New Issue
Block a user