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:
44
.dockerignore
Normal file
44
.dockerignore
Normal file
@@ -0,0 +1,44 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv
|
||||
|
||||
# Development
|
||||
.git/
|
||||
.gitignore
|
||||
*.md
|
||||
*.sh
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Database (will be created in volume)
|
||||
instance/*.db
|
||||
|
||||
# Uploads (will be in volume)
|
||||
app/static/uploads/*
|
||||
static/uploads/*
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Documentation
|
||||
BLUEPRINT_GUIDE.md
|
||||
ICON_INTEGRATION.md
|
||||
KIVY_PLAYER_COMPATIBILITY.md
|
||||
PLAYER_AUTH.md
|
||||
PROGRESS.md
|
||||
README.md
|
||||
|
||||
# Config templates
|
||||
player_config_template.ini
|
||||
player_auth_module.py
|
||||
252
DOCKER.md
Normal file
252
DOCKER.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Docker Deployment Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Build and Run with Docker Compose
|
||||
|
||||
```bash
|
||||
# Build the Docker image
|
||||
docker-compose build
|
||||
|
||||
# Start the container
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Stop the container
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
The application will be available at `http://localhost:5000`
|
||||
|
||||
Default credentials:
|
||||
- Username: `admin`
|
||||
- Password: `admin123`
|
||||
|
||||
### 2. Build Docker Image Only
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t digiserver-v2:latest .
|
||||
|
||||
# Run the container
|
||||
docker run -d \
|
||||
-p 5000:5000 \
|
||||
-v $(pwd)/instance:/app/instance \
|
||||
-v $(pwd)/app/static/uploads:/app/app/static/uploads \
|
||||
--name digiserver \
|
||||
digiserver-v2:latest
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file based on `.env.example`:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit the `.env` file to set your configuration:
|
||||
- `SECRET_KEY`: Change to a random secret key
|
||||
- `FLASK_ENV`: Set to `production` for production deployments
|
||||
|
||||
### Persistent Data
|
||||
|
||||
The following directories are mounted as volumes:
|
||||
- `./instance`: Database storage
|
||||
- `./app/static/uploads`: Uploaded media files
|
||||
|
||||
These persist even when containers are recreated.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### 1. Using Docker Compose (Recommended)
|
||||
|
||||
```bash
|
||||
# Create .env file with production settings
|
||||
cp .env.example .env
|
||||
nano .env # Edit with your settings
|
||||
|
||||
# Start in production mode
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 2. Behind a Reverse Proxy (Nginx/Traefik)
|
||||
|
||||
Example Nginx configuration:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name yourdomain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# For large file uploads
|
||||
client_max_body_size 100M;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Enable Redis Caching (Optional)
|
||||
|
||||
Uncomment the Redis service in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: digiserver-redis
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
```
|
||||
|
||||
Update `.env`:
|
||||
```
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
```
|
||||
|
||||
## Backup
|
||||
|
||||
### Database Backup
|
||||
|
||||
```bash
|
||||
# Backup database
|
||||
docker exec digiserver tar -czf /tmp/backup.tar.gz /app/instance
|
||||
docker cp digiserver:/tmp/backup.tar.gz ./backup-$(date +%Y%m%d).tar.gz
|
||||
```
|
||||
|
||||
### Full Backup (Database + Uploads)
|
||||
|
||||
```bash
|
||||
# Backup everything
|
||||
tar -czf digiserver-backup-$(date +%Y%m%d).tar.gz instance/ app/static/uploads/
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# All logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Last 100 lines
|
||||
docker-compose logs --tail=100
|
||||
|
||||
# Specific service
|
||||
docker-compose logs -f digiserver
|
||||
```
|
||||
|
||||
### Update Application
|
||||
|
||||
```bash
|
||||
# Pull latest code
|
||||
git pull
|
||||
|
||||
# Rebuild and restart
|
||||
docker-compose down
|
||||
docker-compose build
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Shell Access
|
||||
|
||||
```bash
|
||||
# Access container shell
|
||||
docker-compose exec digiserver bash
|
||||
|
||||
# Or with docker directly
|
||||
docker exec -it digiserver bash
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
Change the port mapping in `docker-compose.yml`:
|
||||
```yaml
|
||||
ports:
|
||||
- "8080:5000" # Change 8080 to your desired port
|
||||
```
|
||||
|
||||
### Permission Issues
|
||||
|
||||
Ensure the volumes have correct permissions:
|
||||
```bash
|
||||
sudo chown -R 1000:1000 instance/ app/static/uploads/
|
||||
```
|
||||
|
||||
### Container Won't Start
|
||||
|
||||
Check logs:
|
||||
```bash
|
||||
docker-compose logs digiserver
|
||||
```
|
||||
|
||||
### Reset Database
|
||||
|
||||
```bash
|
||||
# Stop containers
|
||||
docker-compose down
|
||||
|
||||
# Remove database
|
||||
rm instance/*.db
|
||||
|
||||
# Start fresh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## System Requirements
|
||||
|
||||
- Docker 20.10+
|
||||
- Docker Compose 2.0+
|
||||
- 2GB RAM minimum
|
||||
- 10GB disk space for media files
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
1. **Change default credentials** immediately after first login
|
||||
2. **Set a strong SECRET_KEY** in `.env`
|
||||
3. **Use HTTPS** with a reverse proxy in production
|
||||
4. **Regular backups** of database and uploads
|
||||
5. **Update regularly** to get security patches
|
||||
6. **Restrict network access** using firewall rules
|
||||
7. **Monitor logs** for suspicious activity
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### Adjust Workers
|
||||
|
||||
Edit `Dockerfile` CMD line:
|
||||
```dockerfile
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "8", "--timeout", "120", "app.app:create_app()"]
|
||||
```
|
||||
|
||||
### Resource Limits
|
||||
|
||||
Add to `docker-compose.yml`:
|
||||
```yaml
|
||||
services:
|
||||
digiserver:
|
||||
# ... existing config ...
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 2G
|
||||
reservations:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
```
|
||||
50
Dockerfile
Normal file
50
Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
# Use Python 3.13 slim image
|
||||
FROM python:3.13-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
poppler-utils \
|
||||
libreoffice \
|
||||
ffmpeg \
|
||||
libmagic1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first for better caching
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Copy and set permissions for entrypoint script
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
# Create directories for uploads and database
|
||||
RUN mkdir -p app/static/uploads instance
|
||||
|
||||
# Set environment variables
|
||||
ENV FLASK_APP=app.app:create_app
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV FLASK_ENV=production
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5000
|
||||
|
||||
# Create a non-root user
|
||||
RUN useradd -m -u 1000 appuser && \
|
||||
chown -R appuser:appuser /app /docker-entrypoint.sh
|
||||
|
||||
USER appuser
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/').read()" || exit 1
|
||||
|
||||
# Run the application via entrypoint
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
269
README.md
Normal file
269
README.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# DigiServer v2
|
||||
|
||||
Digital Signage Management System - A modern Flask-based application for managing content playlists across multiple display screens.
|
||||
|
||||
## Features
|
||||
|
||||
- 📺 **Multi-Player Management** - Control multiple display screens from one interface
|
||||
- 🎬 **Playlist System** - Create and manage content playlists with drag-and-drop reordering
|
||||
- 📁 **Media Library** - Upload and organize images, videos, PDFs, and presentations
|
||||
- 📄 **PDF to Image Conversion** - Automatic conversion of PDF pages to Full HD images (300 DPI)
|
||||
- 📊 **PowerPoint Support** - Convert PPTX slides to images automatically
|
||||
- 🖼️ **Live Preview** - Real-time content preview for each player
|
||||
- ⚡ **Real-time Updates** - Players automatically sync with playlist changes
|
||||
- 🌓 **Dark Mode** - Full dark mode support across all interfaces
|
||||
- 🗑️ **Media Management** - Clean up unused media files with leftover media manager
|
||||
- 🔒 **User Authentication** - Secure admin access with role-based permissions
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Docker (Recommended)
|
||||
|
||||
```bash
|
||||
# Quick start with Docker
|
||||
./docker-start.sh
|
||||
```
|
||||
|
||||
Access at: `http://localhost:5000`
|
||||
|
||||
Default credentials: `admin` / `admin123`
|
||||
|
||||
See [DOCKER.md](DOCKER.md) for detailed Docker documentation.
|
||||
|
||||
### Option 2: Manual Installation
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- Python 3.13+
|
||||
- LibreOffice (for PPTX conversion)
|
||||
- Poppler Utils (for PDF conversion)
|
||||
- FFmpeg (for video processing)
|
||||
|
||||
#### Installation
|
||||
|
||||
```bash
|
||||
# Install system dependencies (Debian/Ubuntu)
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y poppler-utils libreoffice ffmpeg libmagic1
|
||||
|
||||
# Create virtual environment
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
|
||||
# Install Python dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Initialize database
|
||||
python -c "
|
||||
from app.app import create_app
|
||||
from app.extensions import db, bcrypt
|
||||
from app.models import User
|
||||
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
hashed = bcrypt.generate_password_hash('admin123').decode('utf-8')
|
||||
admin = User(username='admin', password=hashed, role='admin')
|
||||
db.session.add(admin)
|
||||
db.session.commit()
|
||||
print('Admin user created')
|
||||
"
|
||||
|
||||
# Run development server
|
||||
./run_dev.sh
|
||||
```
|
||||
|
||||
Access at: `http://localhost:5000`
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
```bash
|
||||
# Build and run with Docker Compose
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Stop
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Production Deployment
|
||||
|
||||
For production, use:
|
||||
- Gunicorn or uWSGI as WSGI server
|
||||
- Nginx as reverse proxy
|
||||
- Redis for caching (optional)
|
||||
- PostgreSQL for larger deployments (optional)
|
||||
|
||||
See [DOCKER.md](DOCKER.md) for detailed deployment instructions.
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Create a Playlist
|
||||
|
||||
1. Navigate to **Playlist Management**
|
||||
2. Fill in playlist details (name, orientation, description)
|
||||
3. Click **Create Playlist**
|
||||
|
||||
### 2. Upload Media
|
||||
|
||||
1. Go to **Upload Media** page
|
||||
2. Select files (images, videos, PDFs, PPTX)
|
||||
3. Choose media type and duration
|
||||
4. Select target playlist (optional)
|
||||
5. Click **Upload**
|
||||
|
||||
**Supported Formats:**
|
||||
- Images: JPG, PNG, GIF, BMP, WEBP
|
||||
- Videos: MP4, AVI, MOV, MKV, WEBM
|
||||
- Documents: PDF, PPT, PPTX
|
||||
|
||||
### 3. Manage Playlists
|
||||
|
||||
1. Open playlist management
|
||||
2. Drag and drop to reorder content
|
||||
3. Edit duration for each item
|
||||
4. Remove unwanted items
|
||||
5. Changes sync automatically to players
|
||||
|
||||
### 4. Assign to Players
|
||||
|
||||
1. Go to **Player Assignments**
|
||||
2. Select playlist from dropdown for each player
|
||||
3. View live preview to verify content
|
||||
|
||||
### 5. Clean Up Media
|
||||
|
||||
1. Navigate to **Admin** → **Manage Leftover Media**
|
||||
2. Review unused files
|
||||
3. Delete individual files or bulk delete by type
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file:
|
||||
|
||||
```env
|
||||
FLASK_ENV=production
|
||||
SECRET_KEY=your-random-secret-key
|
||||
DATABASE_URL=sqlite:///instance/digiserver.db
|
||||
```
|
||||
|
||||
### Upload Settings
|
||||
|
||||
Edit `app/config.py` to adjust:
|
||||
- Upload folder location
|
||||
- Maximum file size
|
||||
- Allowed file extensions
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
digiserver-v2/
|
||||
├── app/
|
||||
│ ├── blueprints/ # Route handlers
|
||||
│ │ ├── admin.py # Admin panel routes
|
||||
│ │ ├── content.py # Content management
|
||||
│ │ ├── playlist.py # Playlist operations
|
||||
│ │ └── players.py # Player management
|
||||
│ ├── models/ # Database models
|
||||
│ ├── templates/ # HTML templates
|
||||
│ ├── static/ # CSS, JS, uploads
|
||||
│ └── utils/ # Helper functions
|
||||
├── instance/ # Database storage
|
||||
├── Dockerfile # Docker configuration
|
||||
├── docker-compose.yml # Docker Compose config
|
||||
└── requirements.txt # Python dependencies
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Player API
|
||||
- `GET /api/playlist/<player_id>` - Get player playlist
|
||||
- `POST /api/players/<player_id>/heartbeat` - Send heartbeat
|
||||
|
||||
### Admin API
|
||||
- `POST /playlist/<player_id>/update-duration/<content_id>` - Update content duration
|
||||
- `POST /playlist/<player_id>/reorder` - Reorder playlist items
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### PDF Conversion Fails
|
||||
Ensure poppler-utils is installed:
|
||||
```bash
|
||||
sudo apt-get install poppler-utils
|
||||
```
|
||||
|
||||
### PPTX Conversion Fails
|
||||
Install LibreOffice:
|
||||
```bash
|
||||
sudo apt-get install libreoffice
|
||||
```
|
||||
|
||||
### Upload Fails
|
||||
Check folder permissions:
|
||||
```bash
|
||||
chmod -R 755 app/static/uploads
|
||||
```
|
||||
|
||||
### Database Issues
|
||||
Reset database:
|
||||
```bash
|
||||
rm instance/*.db
|
||||
# Then reinitialize (see Installation)
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
|
||||
### Code Formatting
|
||||
```bash
|
||||
black app/
|
||||
flake8 app/
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
```bash
|
||||
flask db migrate -m "Description"
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
This project is proprietary software. All rights reserved.
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
- Check [DOCKER.md](DOCKER.md) for deployment help
|
||||
- Review troubleshooting section
|
||||
- Check application logs
|
||||
|
||||
## Version History
|
||||
|
||||
- **v2.0** - Complete rewrite with playlist-centric architecture
|
||||
- PDF to image conversion (300 DPI)
|
||||
- PPTX slide conversion
|
||||
- Leftover media management
|
||||
- Enhanced dark mode
|
||||
- Duration editing for all content types
|
||||
|
||||
---
|
||||
|
||||
Built with ❤️ using Flask, SQLAlchemy, and modern web technologies
|
||||
@@ -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>
|
||||
|
||||
|
||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
digiserver:
|
||||
build: .
|
||||
container_name: digiserver-v2
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- ./instance:/app/instance
|
||||
- ./app/static/uploads:/app/app/static/uploads
|
||||
environment:
|
||||
- FLASK_ENV=production
|
||||
- SECRET_KEY=${SECRET_KEY:-your-secret-key-change-this}
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5000/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# Optional: Redis for caching (uncomment if needed)
|
||||
# redis:
|
||||
# image: redis:7-alpine
|
||||
# container_name: digiserver-redis
|
||||
# restart: unless-stopped
|
||||
# volumes:
|
||||
# - redis-data:/data
|
||||
|
||||
# volumes:
|
||||
# redis-data:
|
||||
44
docker-entrypoint.sh
Executable file
44
docker-entrypoint.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Starting DigiServer v2..."
|
||||
|
||||
# Create necessary directories
|
||||
mkdir -p /app/instance
|
||||
mkdir -p /app/app/static/uploads
|
||||
|
||||
# Initialize database if it doesn't exist
|
||||
if [ ! -f /app/instance/digiserver.db ]; then
|
||||
echo "Initializing database..."
|
||||
python -c "
|
||||
from app.app import create_app
|
||||
from app.extensions import db, bcrypt
|
||||
from app.models import User
|
||||
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
# Create admin user
|
||||
admin = User.query.filter_by(username='admin').first()
|
||||
if not admin:
|
||||
hashed = bcrypt.generate_password_hash('admin123').decode('utf-8')
|
||||
admin = User(username='admin', password=hashed, role='admin')
|
||||
db.session.add(admin)
|
||||
db.session.commit()
|
||||
print('✅ Admin user created (admin/admin123)')
|
||||
else:
|
||||
print('✅ Admin user already exists')
|
||||
"
|
||||
echo "Database initialized!"
|
||||
fi
|
||||
|
||||
# Start the application
|
||||
echo "Starting Gunicorn..."
|
||||
exec gunicorn \
|
||||
--bind 0.0.0.0:5000 \
|
||||
--workers 4 \
|
||||
--timeout 120 \
|
||||
--access-logfile - \
|
||||
--error-logfile - \
|
||||
"app.app:create_app()"
|
||||
69
docker-start.sh
Executable file
69
docker-start.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🚀 DigiServer v2 - Docker Quick Start"
|
||||
echo "====================================="
|
||||
echo ""
|
||||
|
||||
# Check if Docker is installed
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ Docker is not installed. Please install Docker first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker Compose is installed
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
echo "❌ Docker Compose is not installed. Please install Docker Compose first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create .env file if it doesn't exist
|
||||
if [ ! -f .env ]; then
|
||||
echo "📝 Creating .env file..."
|
||||
cp .env.example .env
|
||||
|
||||
# Generate random secret key
|
||||
SECRET_KEY=$(openssl rand -base64 32)
|
||||
sed -i "s/change-this-to-a-random-secret-key/$SECRET_KEY/" .env
|
||||
echo "✅ Created .env with generated SECRET_KEY"
|
||||
fi
|
||||
|
||||
# Create required directories
|
||||
echo "📁 Creating required directories..."
|
||||
mkdir -p instance app/static/uploads
|
||||
echo "✅ Directories created"
|
||||
|
||||
echo ""
|
||||
echo "🔨 Building Docker image..."
|
||||
docker-compose build
|
||||
|
||||
echo ""
|
||||
echo "🚀 Starting DigiServer v2..."
|
||||
docker-compose up -d
|
||||
|
||||
echo ""
|
||||
echo "⏳ Waiting for application to start..."
|
||||
sleep 5
|
||||
|
||||
# Check if container is running
|
||||
if docker-compose ps | grep -q "Up"; then
|
||||
echo ""
|
||||
echo "✅ DigiServer v2 is running!"
|
||||
echo ""
|
||||
echo "📍 Access the application at: http://localhost:5000"
|
||||
echo ""
|
||||
echo "👤 Default credentials:"
|
||||
echo " Username: admin"
|
||||
echo " Password: admin123"
|
||||
echo ""
|
||||
echo "📋 Useful commands:"
|
||||
echo " View logs: docker-compose logs -f"
|
||||
echo " Stop: docker-compose down"
|
||||
echo " Restart: docker-compose restart"
|
||||
echo " Shell access: docker-compose exec digiserver bash"
|
||||
echo ""
|
||||
echo "⚠️ IMPORTANT: Change the admin password after first login!"
|
||||
else
|
||||
echo ""
|
||||
echo "❌ Failed to start DigiServer v2"
|
||||
echo " Check logs with: docker-compose logs"
|
||||
fi
|
||||
Reference in New Issue
Block a user