Add Docker support and improve PDF conversion
Features: - Dockerfile for containerized deployment - Docker Compose configuration with health checks - Automated database initialization via entrypoint script - Quick start script for easy Docker deployment - Comprehensive Docker deployment documentation (DOCKER.md) - Complete README with installation and usage instructions - .dockerignore for optimized image builds Improvements: - PDF conversion now preserves orientation (portrait/landscape) - PDF rendering at 300 DPI for sharp quality - Maintains aspect ratio during conversion - Compact media library view with image thumbnails - Better media preview with scrollable gallery Docker Features: - Multi-stage build for smaller images - Non-root user for security - Health checks for container monitoring - Volume mounts for persistent data - Production-ready Gunicorn configuration - Support for Redis caching (optional)
This commit is contained in:
@@ -308,12 +308,11 @@ def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
|
||||
|
||||
log_action('info', f'Converting PDF to images: {filename}')
|
||||
|
||||
# Convert PDF pages to images at Full HD resolution
|
||||
# Convert PDF pages to images at high DPI for quality
|
||||
images = convert_from_path(
|
||||
filepath,
|
||||
dpi=150, # Good quality for Full HD display
|
||||
fmt='png',
|
||||
size=(1920, 1080) # Direct Full HD output
|
||||
dpi=300, # 300 DPI for sharp rendering
|
||||
fmt='png'
|
||||
)
|
||||
|
||||
if not images:
|
||||
@@ -323,18 +322,34 @@ def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
|
||||
base_filename = Path(filename).stem
|
||||
upload_folder = os.path.dirname(filepath)
|
||||
|
||||
# Save each page directly as PNG
|
||||
# Save each page with proper aspect ratio preservation
|
||||
converted_files = []
|
||||
for idx, image in enumerate(images, start=1):
|
||||
# Create filename for this page
|
||||
page_filename = f"{base_filename}_page{idx:03d}.png"
|
||||
page_filepath = os.path.join(upload_folder, page_filename)
|
||||
|
||||
# Save the image directly without additional optimization
|
||||
image.save(page_filepath, 'PNG', optimize=True)
|
||||
# Determine orientation and resize maintaining aspect ratio
|
||||
width, height = image.size
|
||||
is_portrait = height > width
|
||||
|
||||
# Define Full HD dimensions based on orientation
|
||||
if is_portrait:
|
||||
# Portrait: max height 1920, max width 1080 (rotated Full HD)
|
||||
max_size = (1080, 1920)
|
||||
else:
|
||||
# Landscape: max width 1920, max height 1080 (standard Full HD)
|
||||
max_size = (1920, 1080)
|
||||
|
||||
# Resize maintaining aspect ratio (thumbnail maintains ratio)
|
||||
from PIL import Image as PILImage
|
||||
image.thumbnail(max_size, PILImage.Resampling.LANCZOS)
|
||||
|
||||
# Save the optimized image
|
||||
image.save(page_filepath, 'PNG', optimize=True, quality=95)
|
||||
|
||||
converted_files.append((page_filepath, page_filename))
|
||||
log_action('info', f'Converted PDF page {idx}/{len(images)}: {page_filename}')
|
||||
log_action('info', f'Converted PDF page {idx}/{len(images)} ({width}x{height} -> {image.size[0]}x{image.size[1]}): {page_filename}')
|
||||
|
||||
log_action('info', f'PDF converted successfully: {len(images)} pages from {filename}')
|
||||
|
||||
|
||||
@@ -205,6 +205,14 @@
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.media-thumbnail {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
body.dark-mode .media-thumbnail {
|
||||
background: #1a202c;
|
||||
}
|
||||
|
||||
.media-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 10px;
|
||||
@@ -334,55 +342,57 @@
|
||||
<div class="card-header">
|
||||
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 24px; height: 24px; filter: brightness(0) invert(1);">
|
||||
Upload Media
|
||||
Media Library
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; padding: 40px 20px;">
|
||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 96px; height: 96px; opacity: 0.5; margin-bottom: 20px;">
|
||||
<h3 style="margin-bottom: 15px;">Upload Media Files</h3>
|
||||
<p style="color: #6c757d; margin-bottom: 25px;">
|
||||
Upload images, videos, and PDFs to your media library.<br>
|
||||
Assign them to playlists during or after upload.
|
||||
</p>
|
||||
<a href="{{ url_for('content.upload_media_page') }}" class="btn btn-success" style="padding: 15px 40px; font-size: 16px; display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||||
Go to Upload Page
|
||||
<!-- Compact Upload Section -->
|
||||
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-radius: 8px; margin-bottom: 20px;">
|
||||
<a href="{{ url_for('content.upload_media_page') }}" class="btn btn-success" style="display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 16px; height: 16px; filter: brightness(0) invert(1);">
|
||||
Upload New Media
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Media Library Preview -->
|
||||
<hr style="margin: 25px 0;">
|
||||
<h3 style="margin-bottom: 15px;">Media Library ({{ media_files|length }} files)</h3>
|
||||
<div class="media-library">
|
||||
<!-- Media Library with Thumbnails -->
|
||||
<h3 style="margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between;">
|
||||
<span>📚 Available Media ({{ media_files|length }})</span>
|
||||
</h3>
|
||||
<div class="media-library" style="max-height: 500px; overflow-y: auto;">
|
||||
{% if media_files %}
|
||||
{% for media in media_files[:12] %}
|
||||
{% for media in media_files %}
|
||||
<div class="media-item" title="{{ media.filename }}">
|
||||
<div class="media-icon">
|
||||
{% if media.content_type == 'image' %}
|
||||
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="Image" style="width: 48px; height: 48px; opacity: 0.5;">
|
||||
{% elif media.content_type == 'video' %}
|
||||
<img src="{{ url_for('static', filename='icons/monitor.svg') }}" alt="Video" style="width: 48px; height: 48px; opacity: 0.5;">
|
||||
{% elif media.content_type == 'pdf' %}
|
||||
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="PDF" style="width: 48px; height: 48px; opacity: 0.5;">
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="File" style="width: 48px; height: 48px; opacity: 0.5;">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="media-name">{{ media.filename[:20] }}...</div>
|
||||
{% if media.content_type == 'image' %}
|
||||
<div class="media-thumbnail" style="width: 100%; height: 100px; overflow: hidden; border-radius: 6px; margin-bottom: 8px; background: #f0f0f0; display: flex; align-items: center; justify-content: center;">
|
||||
<img src="{{ url_for('static', filename='uploads/' + media.filename) }}"
|
||||
alt="{{ media.filename }}"
|
||||
style="max-width: 100%; max-height: 100%; object-fit: cover;"
|
||||
onerror="this.style.display='none'; this.parentElement.innerHTML='<span style=\'font-size: 48px;\'>📷</span>'">
|
||||
</div>
|
||||
{% elif media.content_type == 'video' %}
|
||||
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
|
||||
🎥
|
||||
</div>
|
||||
{% elif media.content_type == 'pdf' %}
|
||||
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
|
||||
📄
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
|
||||
📁
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="media-name" style="font-size: 11px; line-height: 1.3;">{{ media.filename[:25] }}{% if media.filename|length > 25 %}...{% endif %}</div>
|
||||
<div style="font-size: 10px; color: #999; margin-top: 4px;">{{ "%.1f"|format(media.file_size_mb) }} MB</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div style="text-align: center; padding: 20px; color: #999;">
|
||||
<div style="text-align: center; padding: 40px; color: #999; grid-column: 1 / -1;">
|
||||
<div style="font-size: 48px; margin-bottom: 10px;">📭</div>
|
||||
<p>No media files yet. Upload your first file!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if media_files|length > 12 %}
|
||||
<p style="text-align: center; margin-top: 15px; color: #999;">
|
||||
+ {{ media_files|length - 12 }} more files
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user