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}')
|
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(
|
images = convert_from_path(
|
||||||
filepath,
|
filepath,
|
||||||
dpi=150, # Good quality for Full HD display
|
dpi=300, # 300 DPI for sharp rendering
|
||||||
fmt='png',
|
fmt='png'
|
||||||
size=(1920, 1080) # Direct Full HD output
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not images:
|
if not images:
|
||||||
@@ -323,18 +322,34 @@ def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
|
|||||||
base_filename = Path(filename).stem
|
base_filename = Path(filename).stem
|
||||||
upload_folder = os.path.dirname(filepath)
|
upload_folder = os.path.dirname(filepath)
|
||||||
|
|
||||||
# Save each page directly as PNG
|
# Save each page with proper aspect ratio preservation
|
||||||
converted_files = []
|
converted_files = []
|
||||||
for idx, image in enumerate(images, start=1):
|
for idx, image in enumerate(images, start=1):
|
||||||
# Create filename for this page
|
# Create filename for this page
|
||||||
page_filename = f"{base_filename}_page{idx:03d}.png"
|
page_filename = f"{base_filename}_page{idx:03d}.png"
|
||||||
page_filepath = os.path.join(upload_folder, page_filename)
|
page_filepath = os.path.join(upload_folder, page_filename)
|
||||||
|
|
||||||
# Save the image directly without additional optimization
|
# Determine orientation and resize maintaining aspect ratio
|
||||||
image.save(page_filepath, 'PNG', optimize=True)
|
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))
|
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}')
|
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);
|
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media-thumbnail {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .media-thumbnail {
|
||||||
|
background: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
.media-icon {
|
.media-icon {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -334,55 +342,57 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
|
<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);">
|
<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>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: center; padding: 40px 20px;">
|
<!-- Compact Upload Section -->
|
||||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 96px; height: 96px; opacity: 0.5; margin-bottom: 20px;">
|
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-radius: 8px; margin-bottom: 20px;">
|
||||||
<h3 style="margin-bottom: 15px;">Upload Media Files</h3>
|
<a href="{{ url_for('content.upload_media_page') }}" class="btn btn-success" style="display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||||
<p style="color: #6c757d; margin-bottom: 25px;">
|
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 16px; height: 16px; filter: brightness(0) invert(1);">
|
||||||
Upload images, videos, and PDFs to your media library.<br>
|
Upload New Media
|
||||||
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
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Media Library Preview -->
|
<!-- Media Library with Thumbnails -->
|
||||||
<hr style="margin: 25px 0;">
|
<h3 style="margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between;">
|
||||||
<h3 style="margin-bottom: 15px;">Media Library ({{ media_files|length }} files)</h3>
|
<span>📚 Available Media ({{ media_files|length }})</span>
|
||||||
<div class="media-library">
|
</h3>
|
||||||
|
<div class="media-library" style="max-height: 500px; overflow-y: auto;">
|
||||||
{% if media_files %}
|
{% if media_files %}
|
||||||
{% for media in media_files[:12] %}
|
{% for media in media_files %}
|
||||||
<div class="media-item" title="{{ media.filename }}">
|
<div class="media-item" title="{{ media.filename }}">
|
||||||
<div class="media-icon">
|
|
||||||
{% if media.content_type == 'image' %}
|
{% if media.content_type == 'image' %}
|
||||||
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="Image" style="width: 48px; height: 48px; opacity: 0.5;">
|
<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;">
|
||||||
{% elif media.content_type == 'video' %}
|
<img src="{{ url_for('static', filename='uploads/' + media.filename) }}"
|
||||||
<img src="{{ url_for('static', filename='icons/monitor.svg') }}" alt="Video" style="width: 48px; height: 48px; opacity: 0.5;">
|
alt="{{ media.filename }}"
|
||||||
{% elif media.content_type == 'pdf' %}
|
style="max-width: 100%; max-height: 100%; object-fit: cover;"
|
||||||
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="PDF" style="width: 48px; height: 48px; opacity: 0.5;">
|
onerror="this.style.display='none'; this.parentElement.innerHTML='<span style=\'font-size: 48px;\'>📷</span>'">
|
||||||
{% else %}
|
|
||||||
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="File" style="width: 48px; height: 48px; opacity: 0.5;">
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media-name">{{ media.filename[:20] }}...</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>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% 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>
|
<p>No media files yet. Upload your first file!</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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>
|
||||||
</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