Compare commits
10 Commits
a4b7ace90d
..
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ceed7fbc1 | |||
| ddc78cccf1 | |||
| 83461eda7b | |||
| 81a416cb83 | |||
| 2b60f1a8c2 | |||
| 4f19834b31 | |||
| 50e3ce84c8 | |||
| 4d77b6ba63 | |||
| 8f585dc092 | |||
| 52c1539e79 |
@@ -0,0 +1,46 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'v')
|
||||
runs-on: build-htz-01
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract Version
|
||||
id: get_version
|
||||
run: echo "VERSION=${{ github.head_ref }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate Changelog
|
||||
id: changelog
|
||||
uses: https://github.com/metcalfc/changelog-generator@v4.6.2
|
||||
with:
|
||||
myToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
VERSION=${{ steps.get_version.outputs.VERSION }}
|
||||
docker build -t cr.jdbnet.co.uk/public/encoder:$VERSION \
|
||||
-t cr.jdbnet.co.uk/public/encoder:latest \
|
||||
--build-arg VERSION=$VERSION \
|
||||
.
|
||||
docker push cr.jdbnet.co.uk/public/encoder:$VERSION
|
||||
docker push cr.jdbnet.co.uk/public/encoder:latest
|
||||
|
||||
- name: Create Gitea Release
|
||||
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||
with:
|
||||
tag_name: ${{ steps.get_version.outputs.VERSION }}
|
||||
name: ${{ steps.get_version.outputs.VERSION }}
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
@@ -1,47 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/jdb-net/encoder:${{ github.sha }}
|
||||
ghcr.io/jdb-net/encoder:latest
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: build-and-push
|
||||
runs-on: [ k3s-media-lan-01 ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Apply manifests
|
||||
run: |
|
||||
sudo kubectl replace -f deployment.yml --grace-period=60 --force
|
||||
@@ -1,6 +1,9 @@
|
||||
FROM python:3.13-slim
|
||||
LABEL org.opencontainers.image.vendor="JDB-NET"
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
ARG VERSION=dev
|
||||
ENV VERSION=${VERSION}
|
||||
RUN pip install -r requirements.txt
|
||||
RUN apt-get update && apt-get install -y curl ffmpeg procps
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -18,6 +18,11 @@ load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'your-secret-key-here')
|
||||
ENCODING_ENABLED = os.environ.get('ENABLE_ENCODING', 'true').lower() in ('true', '1', 'yes')
|
||||
|
||||
@app.context_processor
|
||||
def inject_version():
|
||||
return {'version': os.environ.get('VERSION', 'dev')}
|
||||
|
||||
DB_CONFIG = {
|
||||
'host': os.environ.get('DB_HOST', 'localhost'),
|
||||
@@ -77,12 +82,19 @@ class DatabaseManager:
|
||||
watch_folder VARCHAR(500) NOT NULL,
|
||||
ffmpeg_flags TEXT DEFAULT NULL,
|
||||
temp_dir VARCHAR(500) DEFAULT '/temp',
|
||||
target_resolution VARCHAR(20),
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# Add target_resolution column if it doesn't exist (for existing users)
|
||||
try:
|
||||
cursor.execute("ALTER TABLE encoder_config ADD COLUMN target_resolution VARCHAR(20)")
|
||||
except:
|
||||
pass # Column already exists
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS job_reports (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
@@ -96,10 +108,17 @@ class DatabaseManager:
|
||||
status ENUM('success', 'failed', 'processing', 'skipped') NOT NULL,
|
||||
error_message TEXT,
|
||||
processing_time DECIMAL(10,2),
|
||||
target_resolution VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# Add target_resolution column if it doesn't exist (for existing users)
|
||||
try:
|
||||
cursor.execute("ALTER TABLE job_reports ADD COLUMN target_resolution VARCHAR(20)")
|
||||
except:
|
||||
pass # Column already exists
|
||||
|
||||
# New persistent job queue table
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS job_queue (
|
||||
@@ -176,20 +195,20 @@ class DatabaseManager:
|
||||
conn.close()
|
||||
return result
|
||||
|
||||
def add_config(self, encoder_type, watch_folder, ffmpeg_flags=None, temp_dir='/temp'):
|
||||
def add_config(self, encoder_type, watch_folder, ffmpeg_flags=None, temp_dir='/temp', target_resolution=None):
|
||||
"""Add a new watch folder configuration"""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"INSERT INTO encoder_config (encoder_type, watch_folder, ffmpeg_flags, temp_dir) VALUES (%s, %s, %s, %s)",
|
||||
(encoder_type, watch_folder, ffmpeg_flags, temp_dir)
|
||||
"INSERT INTO encoder_config (encoder_type, watch_folder, ffmpeg_flags, temp_dir, target_resolution) VALUES (%s, %s, %s, %s, %s)",
|
||||
(encoder_type, watch_folder, ffmpeg_flags, temp_dir, target_resolution)
|
||||
)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
def add_job_report(self, encoder_type, file_path, original_size, encoded_size,
|
||||
original_format, encoded_format, status, error_message=None, processing_time=None):
|
||||
original_format, encoded_format, status, error_message=None, processing_time=None, target_resolution=None):
|
||||
"""Add a job report to the database"""
|
||||
if status == 'skipped':
|
||||
size_saved = 0
|
||||
@@ -200,10 +219,10 @@ class DatabaseManager:
|
||||
cursor.execute("""
|
||||
INSERT INTO job_reports
|
||||
(encoder_type, file_path, original_size, encoded_size, size_saved,
|
||||
original_format, encoded_format, status, error_message, processing_time)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
original_format, encoded_format, status, error_message, processing_time, target_resolution)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (encoder_type, file_path, original_size, encoded_size, size_saved,
|
||||
original_format, encoded_format, status, error_message, processing_time))
|
||||
original_format, encoded_format, status, error_message, processing_time, target_resolution))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
@@ -324,12 +343,38 @@ class VideoEncoder:
|
||||
logger.error(f"Error checking codec for {file_path}: {e}")
|
||||
return False
|
||||
|
||||
def encode_to_h265(self, input_path, output_path, ffmpeg_flags=None):
|
||||
"""Encode video to H265 MKV format (only 720p and higher), using user ffmpeg_flags if provided"""
|
||||
def is_h265(self, file_path):
|
||||
"""Check if video file is H265 encoded"""
|
||||
try:
|
||||
result = subprocess.run([
|
||||
'ffprobe', '-v', 'quiet', '-select_streams', 'v:0',
|
||||
'-show_entries', 'stream=codec_name', '-of', 'csv=p=0',
|
||||
file_path
|
||||
], capture_output=True, text=True)
|
||||
return result.stdout.strip() == 'hevc'
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking codec for {file_path}: {e}")
|
||||
return False
|
||||
|
||||
def get_resolution(self, file_path):
|
||||
"""Get video resolution (height)"""
|
||||
try:
|
||||
result = subprocess.run([
|
||||
'ffprobe', '-v', 'quiet', '-select_streams', 'v:0',
|
||||
'-show_entries', 'stream=height', '-of', 'csv=p=0',
|
||||
file_path
|
||||
], capture_output=True, text=True)
|
||||
return int(result.stdout.strip())
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting resolution for {file_path}: {e}")
|
||||
return None
|
||||
|
||||
def encode_to_h265(self, input_path, output_path, ffmpeg_flags=None, target_resolution=None):
|
||||
"""Encode video to H265 MKV format (only 720p and higher), with optional target resolution scaling"""
|
||||
try:
|
||||
resolution_check = subprocess.run([
|
||||
'ffprobe', '-v', 'quiet', '-select_streams', 'v:0',
|
||||
'-show_entries', 'stream=height', '-of', 'csv=p=0',
|
||||
'-show_entries', 'stream=height,width', '-of', 'csv=p=0',
|
||||
input_path
|
||||
], capture_output=True, text=True)
|
||||
|
||||
@@ -337,17 +382,31 @@ class VideoEncoder:
|
||||
return False, "Could not determine video resolution"
|
||||
|
||||
try:
|
||||
height = int(resolution_check.stdout.strip())
|
||||
width, height = map(int, resolution_check.stdout.strip().split(','))
|
||||
if height < 720:
|
||||
return False, f"Video resolution {height}p is below 720p minimum, skipping encoding"
|
||||
except ValueError:
|
||||
return False, "Invalid video height information"
|
||||
return False, "Invalid video resolution information"
|
||||
|
||||
cmd = [
|
||||
'ffmpeg', '-i', input_path, '-c:v', 'libx265', '-c:a', 'copy',
|
||||
'-preset', 'medium', '-crf', '28', '-f', 'matroska'
|
||||
]
|
||||
|
||||
# Add scaling filter if target resolution is specified
|
||||
if target_resolution:
|
||||
# target_resolution should be in format like "720p", "1080p", "480p"
|
||||
target_height = int(target_resolution.replace('p', '').lower())
|
||||
# Only scale down, never scale up
|
||||
if target_height > 0 and height > target_height:
|
||||
# Calculate scaled width to maintain aspect ratio
|
||||
scale_width = int((width / height) * target_height)
|
||||
# Ensure width is divisible by 2 (required by video codecs)
|
||||
if scale_width % 2 != 0:
|
||||
scale_width -= 1
|
||||
cmd.insert(2, f'scale={scale_width}:{target_height}')
|
||||
cmd.insert(2, '-vf')
|
||||
|
||||
# Insert user flags if provided
|
||||
if ffmpeg_flags:
|
||||
import shlex
|
||||
@@ -493,6 +552,12 @@ class AudioEncoder:
|
||||
def process_file(encoder_type, file_path, encoder):
|
||||
"""Process a single file"""
|
||||
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:
|
||||
if not os.path.exists(file_path):
|
||||
logger.warning(f"File {file_path} no longer exists, skipping")
|
||||
@@ -504,14 +569,16 @@ def process_file(encoder_type, file_path, encoder):
|
||||
original_size = os.path.getsize(file_path)
|
||||
file_name = os.path.basename(file_path)
|
||||
file_dir = os.path.dirname(file_path)
|
||||
# Get ffmpeg_flags and temp_dir for this file's folder
|
||||
# Get ffmpeg_flags, temp_dir, and target_resolution for this file's folder
|
||||
configs = db_manager.get_config(encoder_type)
|
||||
ffmpeg_flags = None
|
||||
temp_dir = DEFAULT_TEMP_DIR
|
||||
target_resolution = None
|
||||
for config in configs:
|
||||
if file_path.startswith(config['watch_folder']):
|
||||
ffmpeg_flags = config.get('ffmpeg_flags')
|
||||
temp_dir = config.get('temp_dir') or DEFAULT_TEMP_DIR
|
||||
target_resolution = config.get('target_resolution')
|
||||
break
|
||||
if not os.path.exists(temp_dir):
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
@@ -519,12 +586,22 @@ def process_file(encoder_type, file_path, encoder):
|
||||
temp_input = os.path.join(temp_dir, file_name)
|
||||
shutil.copy2(file_path, temp_input)
|
||||
if encoder_type == 'video':
|
||||
if not encoder.is_h264(temp_input):
|
||||
is_h264 = encoder.is_h264(temp_input)
|
||||
is_h265 = encoder.is_h265(temp_input)
|
||||
|
||||
# Skip if neither H.264 nor H.265
|
||||
if not is_h264 and not is_h265:
|
||||
os.remove(temp_input)
|
||||
return
|
||||
|
||||
# Skip H.265 files if no target resolution is set
|
||||
if is_h265 and not target_resolution:
|
||||
os.remove(temp_input)
|
||||
return
|
||||
|
||||
temp_output = os.path.join(temp_dir, f"temp_{file_name}")
|
||||
temp_output = os.path.splitext(temp_output)[0] + '.mkv'
|
||||
success, error = encoder.encode_to_h265(temp_input, temp_output, ffmpeg_flags=ffmpeg_flags)
|
||||
success, error = encoder.encode_to_h265(temp_input, temp_output, ffmpeg_flags=ffmpeg_flags, target_resolution=target_resolution)
|
||||
if success:
|
||||
encoded_size = os.path.getsize(temp_output)
|
||||
valid, validation_error = encoder.validate_encoded_file(temp_input, temp_output)
|
||||
@@ -541,7 +618,8 @@ def process_file(encoder_type, file_path, encoder):
|
||||
db_manager.add_job_report(
|
||||
encoder_type, file_path, original_size, 0,
|
||||
os.path.splitext(file_path)[1], '.mkv', 'failed',
|
||||
error_message=f'Failed to copy encoded file: {copy_exc}'
|
||||
error_message=f'Failed to copy encoded file: {copy_exc}',
|
||||
target_resolution=target_resolution
|
||||
)
|
||||
logger.error(f"Failed to copy encoded file for {file_path}: {copy_exc}")
|
||||
return
|
||||
@@ -549,14 +627,16 @@ def process_file(encoder_type, file_path, encoder):
|
||||
db_manager.add_job_report(
|
||||
encoder_type, file_path, original_size, encoded_size,
|
||||
os.path.splitext(file_path)[1], '.mkv', 'success',
|
||||
processing_time=processing_time
|
||||
processing_time=processing_time,
|
||||
target_resolution=target_resolution
|
||||
)
|
||||
logger.info(f"Successfully encoded {file_path} to H265")
|
||||
else:
|
||||
db_manager.add_job_report(
|
||||
encoder_type, file_path, original_size, 0,
|
||||
os.path.splitext(file_path)[1], '.mkv', 'failed',
|
||||
error_message=validation_error
|
||||
error_message=validation_error,
|
||||
target_resolution=target_resolution
|
||||
)
|
||||
logger.error(f"Validation failed for {file_path}: {validation_error}")
|
||||
# Clean up temp files
|
||||
@@ -571,7 +651,8 @@ def process_file(encoder_type, file_path, encoder):
|
||||
db_manager.add_job_report(
|
||||
encoder_type, file_path, original_size, 0,
|
||||
os.path.splitext(file_path)[1], os.path.splitext(file_path)[1], 'skipped',
|
||||
error_message=error
|
||||
error_message=error,
|
||||
target_resolution=target_resolution
|
||||
)
|
||||
logger.info(f"Skipped {file_path}: {error}")
|
||||
else:
|
||||
@@ -580,7 +661,8 @@ def process_file(encoder_type, file_path, encoder):
|
||||
db_manager.add_job_report(
|
||||
encoder_type, file_path, original_size, 0,
|
||||
os.path.splitext(file_path)[1], '.mkv', 'failed',
|
||||
error_message=error
|
||||
error_message=error,
|
||||
target_resolution=target_resolution
|
||||
)
|
||||
logger.error(f"Failed to encode {file_path}: {error}")
|
||||
# Always remove temp input if it still exists
|
||||
@@ -647,7 +729,8 @@ def process_file(encoder_type, file_path, encoder):
|
||||
db_manager.add_job_report(
|
||||
encoder_type, file_path, os.path.getsize(file_path), 0,
|
||||
os.path.splitext(file_path)[1], '', 'failed',
|
||||
error_message=str(e)
|
||||
error_message=str(e),
|
||||
target_resolution=target_resolution
|
||||
)
|
||||
except:
|
||||
pass
|
||||
@@ -686,10 +769,14 @@ def scan_folders():
|
||||
audio_encoder = AudioEncoder()
|
||||
|
||||
while True:
|
||||
if not ENCODING_ENABLED:
|
||||
time.sleep(60)
|
||||
continue
|
||||
try:
|
||||
video_configs = db_manager.get_config('video')
|
||||
for config in video_configs:
|
||||
folder = config['watch_folder']
|
||||
target_resolution = config.get('target_resolution')
|
||||
if os.path.exists(folder):
|
||||
for root, dirs, files in os.walk(folder):
|
||||
for file in files:
|
||||
@@ -699,6 +786,14 @@ def scan_folders():
|
||||
if (not db_manager.is_in_queue('video', file_path)
|
||||
and not db_manager.file_already_processed(file_path)):
|
||||
db_manager.add_to_queue('video', file_path)
|
||||
elif target_resolution and video_encoder.is_h265(file_path):
|
||||
# Also encode H.265 files if current resolution is higher than target
|
||||
current_height = video_encoder.get_resolution(file_path)
|
||||
target_height = int(target_resolution.replace('p', '').lower())
|
||||
if current_height and current_height > target_height:
|
||||
if (not db_manager.is_in_queue('video', file_path)
|
||||
and not db_manager.file_already_processed(file_path)):
|
||||
db_manager.add_to_queue('video', file_path)
|
||||
|
||||
audio_configs = db_manager.get_config('audio')
|
||||
for config in audio_configs:
|
||||
@@ -803,6 +898,7 @@ def add_config():
|
||||
encoder_type = request.form.get('encoder_type')
|
||||
watch_folder = request.form.get('watch_folder')
|
||||
temp_dir = request.form.get('temp_dir') or '/temp'
|
||||
target_resolution = request.form.get('target_resolution') or None
|
||||
ffmpeg_flags = None
|
||||
if encoder_type == 'video':
|
||||
crf = request.form.get('crf', '24')
|
||||
@@ -812,7 +908,7 @@ def add_config():
|
||||
audio_bitrate = request.form.get('audio_bitrate', '320k')
|
||||
ffmpeg_flags = f'-b:a {audio_bitrate}'
|
||||
if encoder_type in ['video', 'audio'] and watch_folder:
|
||||
db_manager.add_config(encoder_type, watch_folder, ffmpeg_flags, temp_dir)
|
||||
db_manager.add_config(encoder_type, watch_folder, ffmpeg_flags, temp_dir, target_resolution)
|
||||
return redirect(url_for('config'))
|
||||
|
||||
@app.route('/edit_config/<int:config_id>', methods=['POST'])
|
||||
@@ -822,8 +918,8 @@ def edit_config(config_id):
|
||||
conn = db_manager.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"UPDATE encoder_config SET watch_folder=%s, temp_dir=%s, ffmpeg_flags=%s WHERE id=%s",
|
||||
(data.get('watch_folder'), data.get('temp_dir'), data.get('ffmpeg_flags'), config_id)
|
||||
"UPDATE encoder_config SET watch_folder=%s, temp_dir=%s, ffmpeg_flags=%s, target_resolution=%s WHERE id=%s",
|
||||
(data.get('watch_folder'), data.get('temp_dir'), data.get('ffmpeg_flags'), data.get('target_resolution'), config_id)
|
||||
)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
+3
-3
@@ -18,7 +18,7 @@
|
||||
</head>
|
||||
<body class="bg-gray-950 text-white min-h-screen">
|
||||
<nav class="bg-gray-950 border-b border-gray-800 modern-shadow" x-data="{ mobileMenuOpen: false }">
|
||||
<div class="max-w-7xl mx-auto px-4">
|
||||
<div class="max-w-full mx-auto px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="{{ url_for('index') }}" class="flex items-center space-x-2">
|
||||
@@ -81,8 +81,8 @@
|
||||
{% 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">
|
||||
<span>© <script>document.write(new Date().getFullYear());</script> JDB-NET</span>
|
||||
<a href="https://git.jdbnet.co.uk/jamie/encoder" target="_blank" class="hover:text-sky-400 transition-all">VERSION_HERE</a>
|
||||
<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>
|
||||
<a href="https://git.jdbnet.co.uk/jamie/encoder" target="_blank" class="hover:text-sky-400 transition-all">{{ version }}</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
+44
-1
@@ -47,6 +47,19 @@
|
||||
<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>
|
||||
@@ -99,6 +112,9 @@
|
||||
{% 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">
|
||||
@@ -207,6 +223,9 @@
|
||||
{% 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">
|
||||
@@ -252,6 +271,9 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -279,6 +301,19 @@
|
||||
<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">
|
||||
@@ -416,8 +451,11 @@ function openEditModal(config) {
|
||||
document.getElementById('edit-temp-input').value = config.temp_dir || '/temp';
|
||||
// Show/hide flag dropdowns
|
||||
if (config.encoder_type === 'video') {
|
||||
document.getElementById('edit-target-resolution-field').style.display = '';
|
||||
document.getElementById('edit-ffmpeg-flags-video').style.display = '';
|
||||
document.getElementById('edit-ffmpeg-flags-audio').style.display = 'none';
|
||||
// Set target resolution value
|
||||
document.getElementById('edit-target-resolution').value = config.target_resolution || '';
|
||||
// Parse ffmpeg_flags for crf and preset
|
||||
let crf = '24', preset = 'medium';
|
||||
if (config.ffmpeg_flags) {
|
||||
@@ -429,6 +467,7 @@ function openEditModal(config) {
|
||||
document.getElementById('edit-crf').value = crf;
|
||||
document.getElementById('edit-preset').value = preset;
|
||||
} else {
|
||||
document.getElementById('edit-target-resolution-field').style.display = 'none';
|
||||
document.getElementById('edit-ffmpeg-flags-video').style.display = 'none';
|
||||
document.getElementById('edit-ffmpeg-flags-audio').style.display = '';
|
||||
// Parse ffmpeg_flags for audio bitrate
|
||||
@@ -459,15 +498,18 @@ if (editForm) {
|
||||
const id = document.getElementById('edit-config-id').value;
|
||||
const encoderType = document.getElementById('edit-encoder-type').value;
|
||||
let ffmpeg_flags = '';
|
||||
let target_resolution = '';
|
||||
if (encoderType === 'video') {
|
||||
ffmpeg_flags = `-crf ${document.getElementById('edit-crf').value} -preset ${document.getElementById('edit-preset').value}`;
|
||||
target_resolution = document.getElementById('edit-target-resolution').value;
|
||||
} else {
|
||||
ffmpeg_flags = `-b:a ${document.getElementById('edit-audio-bitrate').value}`;
|
||||
}
|
||||
const data = {
|
||||
watch_folder: document.getElementById('edit-folder-input').value,
|
||||
temp_dir: document.getElementById('edit-temp-input').value,
|
||||
ffmpeg_flags: ffmpeg_flags
|
||||
ffmpeg_flags: ffmpeg_flags,
|
||||
target_resolution: target_resolution
|
||||
};
|
||||
fetch(`/edit_config/${id}`, {
|
||||
method: 'POST',
|
||||
@@ -494,6 +536,7 @@ window.allConfigs = [
|
||||
watch_folder: {{ config.watch_folder|tojson }},
|
||||
temp_dir: {{ config.temp_dir|tojson }},
|
||||
ffmpeg_flags: {{ config.ffmpeg_flags|tojson }},
|
||||
target_resolution: {{ config.target_resolution|tojson }},
|
||||
encoder_type: {{ config.encoder_type|tojson }}
|
||||
},
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user