mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-20 05:36:03 -05:00
Compare commits
144 Commits
v2.0
...
feat-docke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66e67c751e | ||
|
|
9b3d9fe1e7 | ||
|
|
ea340b6a2e | ||
|
|
ba2c31b1e6 | ||
|
|
5eb6fafb8c | ||
|
|
c035bcddf5 | ||
|
|
01912ea1f9 | ||
|
|
d9f299af4d | ||
|
|
e80590a3aa | ||
|
|
2a0cb977f2 | ||
|
|
a5e6e7b9ca | ||
|
|
b39072c8ae | ||
|
|
f4ab60e894 | ||
|
|
8656b40c5b | ||
|
|
553a25a86f | ||
|
|
1c1af489f1 | ||
|
|
c4c5ecf06a | ||
|
|
725cc71960 | ||
|
|
0c1c5bbb09 | ||
|
|
56182f0a6d | ||
|
|
208f0b338b | ||
|
|
cbef629baf | ||
|
|
8e8454d8c2 | ||
|
|
8d982ace92 | ||
|
|
6cee02085c | ||
|
|
e33aa17911 | ||
|
|
a8db23f204 | ||
|
|
d6428e3334 | ||
|
|
fd342fd1d6 | ||
|
|
7a1b32f1ba | ||
|
|
817e16ac60 | ||
|
|
8cbeb72dd2 | ||
|
|
e9f862a0ff | ||
|
|
02eac68b51 | ||
|
|
e790795bfd | ||
|
|
de99d84c18 | ||
|
|
8aa89c0958 | ||
|
|
df98b65704 | ||
|
|
a607996bfa | ||
|
|
79f2e2bb11 | ||
|
|
d54732040a | ||
|
|
e8520bc7cd | ||
|
|
b6e46e7b62 | ||
|
|
36eab954bd | ||
|
|
610716533b | ||
|
|
4f1c4a2b4c | ||
|
|
83f3eec940 | ||
|
|
a5acce4ab1 | ||
|
|
a4e9309350 | ||
|
|
6beaf0bbe2 | ||
|
|
70168299ba | ||
|
|
b28c2d8271 | ||
|
|
d34fc328bf | ||
|
|
ab4d9d67df | ||
|
|
f7a2f049bd | ||
|
|
05414f66c7 | ||
|
|
8fecccce1c | ||
|
|
2a7123ca0b | ||
|
|
20f305e69e | ||
|
|
d1fda05fdc | ||
|
|
a02e0a8a66 | ||
|
|
21f76dbb6e | ||
|
|
50e9f3103f | ||
|
|
0b9a203123 | ||
|
|
5cbd815496 | ||
|
|
3a8cacc847 | ||
|
|
5402ee7bc5 | ||
|
|
a6a2b50c8d | ||
|
|
23e48a8bb7 | ||
|
|
313cd9cbc6 | ||
|
|
0392dbe1ed | ||
|
|
a7562c244e | ||
|
|
d2ee12087c | ||
|
|
6db01932e1 | ||
|
|
53d8215346 | ||
|
|
1b960b28f8 | ||
|
|
02d9188aa1 | ||
|
|
8d9a4618f0 | ||
|
|
cf93a77802 | ||
|
|
5a1e4f25ed | ||
|
|
9fc7597e73 | ||
|
|
9b3e0250d4 | ||
|
|
1384471745 | ||
|
|
29b362c8ce | ||
|
|
b8ee2e9fb8 | ||
|
|
99be0f07dd | ||
|
|
27d1660192 | ||
|
|
98adb22205 | ||
|
|
673ddeb5bd | ||
|
|
aa8a2d92dc | ||
|
|
6bbd4c2809 | ||
|
|
c4148bd504 | ||
|
|
ea8b2af26f | ||
|
|
5aa899cef0 | ||
|
|
4992cc425c | ||
|
|
ef4067cbdd | ||
|
|
8cc3513a8a | ||
|
|
90e593946d | ||
|
|
f7136e2a11 | ||
|
|
0151e834a1 | ||
|
|
5fe4d3a9fc | ||
|
|
94c646fdb8 | ||
|
|
d665058b80 | ||
|
|
986c7d1074 | ||
|
|
1adee8c156 | ||
|
|
ffd7a52863 | ||
|
|
c5047d8df8 | ||
|
|
dcbfaca91c | ||
|
|
918df010f5 | ||
|
|
e9739bab45 | ||
|
|
e7ce9ef5c0 | ||
|
|
4829adf110 | ||
|
|
fdff0811a1 | ||
|
|
92c0ff579a | ||
|
|
847cff2b5c | ||
|
|
e8d3ff25be | ||
|
|
15d217453b | ||
|
|
029665145e | ||
|
|
487e098b96 | ||
|
|
fe7427a1f2 | ||
|
|
4bf41fe80e | ||
|
|
1fd04ca947 | ||
|
|
a1962d4b32 | ||
|
|
6e9c9ed81f | ||
|
|
51186e3253 | ||
|
|
150967b342 | ||
|
|
bb6244d862 | ||
|
|
a002422b77 | ||
|
|
24167b9624 | ||
|
|
b9db1a5e2e | ||
|
|
296aeac567 | ||
|
|
10c386f886 | ||
|
|
367faaddd1 | ||
|
|
3d59b87f09 | ||
|
|
5dee41de39 | ||
|
|
08bba5fc05 | ||
|
|
102414b514 | ||
|
|
c866fdd6ba | ||
|
|
5b601698a4 | ||
|
|
f040f73f51 | ||
|
|
b7a70d92fa | ||
|
|
2f43cef8da | ||
|
|
ad633e6fdf | ||
|
|
cd8d0ea49a |
113
.docker-backup/Dockerfile
Normal file
113
.docker-backup/Dockerfile
Normal file
@@ -0,0 +1,113 @@
|
||||
FROM python:3.13.5-slim-bookworm AS build-image
|
||||
|
||||
# Install system dependencies needed for downloading and extracting
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends wget xz-utils unzip && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt-get purge --auto-remove && \
|
||||
apt-get clean
|
||||
|
||||
RUN wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
||||
|
||||
RUN mkdir -p ffmpeg-tmp && \
|
||||
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C ffmpeg-tmp && \
|
||||
cp -v ffmpeg-tmp/ffmpeg ffmpeg-tmp/ffprobe ffmpeg-tmp/qt-faststart /usr/local/bin && \
|
||||
rm -rf ffmpeg-tmp ffmpeg-release-amd64-static.tar.xz
|
||||
|
||||
# Install Bento4 in the specified location
|
||||
RUN mkdir -p /home/mediacms.io/bento4 && \
|
||||
wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
|
||||
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d /home/mediacms.io/bento4 && \
|
||||
mv /home/mediacms.io/bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* /home/mediacms.io/bento4/ && \
|
||||
rm -rf /home/mediacms.io/bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
|
||||
rm -rf /home/mediacms.io/bento4/docs && \
|
||||
rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
|
||||
|
||||
############ BASE RUNTIME IMAGE ############
|
||||
FROM python:3.13.5-slim-bookworm AS base
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV CELERY_APP='cms'
|
||||
ENV VIRTUAL_ENV=/home/mediacms.io
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
# Install system dependencies first
|
||||
RUN apt-get update -y && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
supervisor \
|
||||
nginx \
|
||||
imagemagick \
|
||||
procps \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
zlib1g-dev \
|
||||
zlib1g \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
libpq-dev \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set up virtualenv first
|
||||
RUN mkdir -p /home/mediacms.io/mediacms/{logs} && \
|
||||
cd /home/mediacms.io && \
|
||||
python3 -m venv $VIRTUAL_ENV
|
||||
|
||||
# Copy requirements files
|
||||
COPY requirements.txt requirements-dev.txt ./
|
||||
|
||||
# Install Python dependencies using pip (within virtualenv)
|
||||
ARG DEVELOPMENT_MODE=False
|
||||
RUN pip install --no-cache-dir uv && \
|
||||
uv pip install --no-binary lxml --no-binary xmlsec -r requirements.txt && \
|
||||
if [ "$DEVELOPMENT_MODE" = "True" ]; then \
|
||||
echo "Installing development dependencies..." && \
|
||||
uv pip install -r requirements-dev.txt; \
|
||||
fi && \
|
||||
apt-get purge -y --auto-remove \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libpq-dev
|
||||
|
||||
# Copy ffmpeg and Bento4 from build image
|
||||
COPY --from=build-image /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg
|
||||
COPY --from=build-image /usr/local/bin/ffprobe /usr/local/bin/ffprobe
|
||||
COPY --from=build-image /usr/local/bin/qt-faststart /usr/local/bin/qt-faststart
|
||||
COPY --from=build-image /home/mediacms.io/bento4 /home/mediacms.io/bento4
|
||||
|
||||
# Copy application files
|
||||
COPY . /home/mediacms.io/mediacms
|
||||
WORKDIR /home/mediacms.io/mediacms
|
||||
|
||||
# required for sprite thumbnail generation for large video files
|
||||
COPY deploy/docker/policy.xml /etc/ImageMagick-6/policy.xml
|
||||
|
||||
# Set process control environment variables
|
||||
ENV ENABLE_UWSGI='yes' \
|
||||
ENABLE_NGINX='yes' \
|
||||
ENABLE_CELERY_BEAT='yes' \
|
||||
ENABLE_CELERY_SHORT='yes' \
|
||||
ENABLE_CELERY_LONG='yes' \
|
||||
ENABLE_MIGRATIONS='yes'
|
||||
|
||||
EXPOSE 9000 80
|
||||
|
||||
RUN chmod +x ./deploy/docker/entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["./deploy/docker/entrypoint.sh"]
|
||||
CMD ["./deploy/docker/start.sh"]
|
||||
|
||||
############ FULL IMAGE ############
|
||||
FROM base AS full
|
||||
COPY requirements-full.txt ./
|
||||
RUN mkdir -p /root/.cache/ && \
|
||||
chmod go+rwx /root/ && \
|
||||
chmod go+rwx /root/.cache/
|
||||
RUN uv pip install -r requirements-full.txt
|
||||
119
.docker-backup/docker-compose-cert.yaml
Normal file
119
.docker-backup/docker-compose-cert.yaml
Normal file
@@ -0,0 +1,119 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
nginx-proxy:
|
||||
image: nginxproxy/nginx-proxy
|
||||
container_name: nginx-proxy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- conf:/etc/nginx/conf.d
|
||||
- vhost:/etc/nginx/vhost.d
|
||||
- html:/usr/share/nginx/html
|
||||
- dhparam:/etc/nginx/dhparam
|
||||
- certs:/etc/nginx/certs:ro
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
- ./deploy/docker/reverse_proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
|
||||
|
||||
acme-companion:
|
||||
image: nginxproxy/acme-companion
|
||||
container_name: nginx-proxy-acme
|
||||
volumes_from:
|
||||
- nginx-proxy
|
||||
volumes:
|
||||
- certs:/etc/nginx/certs:rw
|
||||
- acme:/etc/acme.sh
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
migrations:
|
||||
image: mediacms/mediacms:latest
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_EMAIL: 'Y'
|
||||
ADMIN_PASSWORD: 'X'
|
||||
command: "./deploy/docker/prestart.sh"
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
web:
|
||||
image: mediacms/mediacms:latest
|
||||
deploy:
|
||||
replicas: 1
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
VIRTUAL_HOST: 'X.mediacms.io'
|
||||
LETSENCRYPT_HOST: 'X.mediacms.io'
|
||||
LETSENCRYPT_EMAIL: 'X'
|
||||
depends_on:
|
||||
- migrations
|
||||
celery_beat:
|
||||
image: mediacms/mediacms:latest
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
depends_on:
|
||||
- redis
|
||||
celery_worker:
|
||||
image: mediacms/mediacms:full
|
||||
deploy:
|
||||
replicas: 1
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres:17.2-alpine
|
||||
volumes:
|
||||
- ../postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: mediacms
|
||||
POSTGRES_PASSWORD: mediacms
|
||||
POSTGRES_DB: mediacms
|
||||
TZ: Europe/London
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
redis:
|
||||
image: "redis:alpine"
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli","ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
volumes:
|
||||
conf:
|
||||
vhost:
|
||||
html:
|
||||
dhparam:
|
||||
certs:
|
||||
acme:
|
||||
89
.docker-backup/docker-compose-dev.yaml
Normal file
89
.docker-backup/docker-compose-dev.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
migrations:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
target: base
|
||||
args:
|
||||
- DEVELOPMENT_MODE=True
|
||||
image: mediacms/mediacms-dev:latest
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
command: "./deploy/docker/prestart.sh"
|
||||
environment:
|
||||
DEVELOPMENT_MODE: True
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
ADMIN_PASSWORD: 'admin'
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
frontend:
|
||||
image: node:20
|
||||
volumes:
|
||||
- ${PWD}/frontend:/home/mediacms.io/mediacms/frontend/
|
||||
working_dir: /home/mediacms.io/mediacms/frontend/
|
||||
command: bash -c "npm install && npm run start"
|
||||
env_file:
|
||||
- ${PWD}/frontend/.env
|
||||
ports:
|
||||
- "8088:8088"
|
||||
depends_on:
|
||||
- web
|
||||
web:
|
||||
image: mediacms/mediacms-dev:latest
|
||||
command: "python manage.py runserver 0.0.0.0:80"
|
||||
environment:
|
||||
DEVELOPMENT_MODE: True
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres:17.2-alpine
|
||||
volumes:
|
||||
- ../postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: mediacms
|
||||
POSTGRES_PASSWORD: mediacms
|
||||
POSTGRES_DB: mediacms
|
||||
TZ: Europe/London
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
redis:
|
||||
image: "redis:alpine"
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
celery_worker:
|
||||
image: mediacms/mediacms-dev:latest
|
||||
deploy:
|
||||
replicas: 1
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
depends_on:
|
||||
- web
|
||||
86
.docker-backup/docker-compose.yaml
Normal file
86
.docker-backup/docker-compose.yaml
Normal file
@@ -0,0 +1,86 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
migrations:
|
||||
image: mediacms/mediacms:latest
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
# ADMIN_PASSWORD: 'uncomment_and_set_password_here'
|
||||
command: "./deploy/docker/prestart.sh"
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
web:
|
||||
image: mediacms/mediacms:latest
|
||||
deploy:
|
||||
replicas: 1
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
depends_on:
|
||||
- migrations
|
||||
celery_beat:
|
||||
image: mediacms/mediacms:latest
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
depends_on:
|
||||
- redis
|
||||
celery_worker:
|
||||
image: mediacms/mediacms:latest
|
||||
deploy:
|
||||
replicas: 1
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres:17.2-alpine
|
||||
volumes:
|
||||
- ../postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: mediacms
|
||||
POSTGRES_PASSWORD: mediacms
|
||||
POSTGRES_DB: mediacms
|
||||
TZ: Europe/London
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
redis:
|
||||
image: "redis:alpine"
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli","ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
@@ -1,2 +1,37 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
npm-debug.log
|
||||
npm-debug.log
|
||||
|
||||
# Local development files - exclude uploaded content but keep placeholder images
|
||||
media_files/*
|
||||
!media_files/userlogos/
|
||||
media_files/userlogos/*
|
||||
!media_files/userlogos/*.jpg
|
||||
logs
|
||||
static_collected
|
||||
|
||||
# Version control
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
|
||||
# Development/testing
|
||||
.pytest_cache
|
||||
.qodo
|
||||
.claude
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
.docker-backup
|
||||
|
||||
# Documentation (if you don't need it in the image)
|
||||
docs
|
||||
|
||||
# Other
|
||||
*.pyc
|
||||
__pycache__
|
||||
.env
|
||||
.vscode
|
||||
.idea
|
||||
20
.github/workflows/ci.yml
vendored
Normal file
20
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "CI"
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**/README.md'
|
||||
jobs:
|
||||
pre-commit:
|
||||
uses: ./.github/workflows/pre-commit.yml
|
||||
test:
|
||||
uses: ./.github/workflows/python.yml
|
||||
needs: [pre-commit]
|
||||
release:
|
||||
uses: ./.github/workflows/docker-build-push.yml
|
||||
secrets: inherit # pass all secrets
|
||||
needs: [test]
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
134
.github/workflows/docker-build-push.yml
vendored
Normal file
134
.github/workflows/docker-build-push.yml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
name: Docker build and push
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
tags:
|
||||
- v*.*.*
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build & release to DockerHub
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker meta for web image
|
||||
id: meta-web
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
mediacms/mediacms
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
labels: |
|
||||
org.opencontainers.image.title=MediaCMS
|
||||
org.opencontainers.image.description=MediaCMS is a modern, fully featured open source video and media CMS, written in Python/Django and React, featuring a REST API.
|
||||
org.opencontainers.image.vendor=MediaCMS
|
||||
org.opencontainers.image.url=https://mediacms.io/
|
||||
org.opencontainers.image.source=https://github.com/mediacms-io/mediacms
|
||||
org.opencontainers.image.licenses=AGPL-3.0
|
||||
|
||||
- name: Build and push web image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
target: web
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta-web.outputs.tags }}
|
||||
labels: ${{ steps.meta-web.outputs.labels }}
|
||||
|
||||
- name: Docker meta for worker image
|
||||
id: meta-worker
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
mediacms/mediacms-worker
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
labels: |
|
||||
org.opencontainers.image.title=MediaCMS Worker
|
||||
org.opencontainers.image.description=MediaCMS Celery worker for background task processing.
|
||||
org.opencontainers.image.vendor=MediaCMS
|
||||
org.opencontainers.image.url=https://mediacms.io/
|
||||
org.opencontainers.image.source=https://github.com/mediacms-io/mediacms
|
||||
org.opencontainers.image.licenses=AGPL-3.0
|
||||
|
||||
- name: Build and push worker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
target: worker
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta-worker.outputs.tags }}
|
||||
labels: ${{ steps.meta-worker.outputs.labels }}
|
||||
|
||||
- name: Docker meta for worker-full image
|
||||
id: meta-worker-full
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
mediacms/mediacms-worker
|
||||
tags: |
|
||||
type=raw,value=latest-full,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
type=semver,pattern={{version}}-full
|
||||
type=semver,pattern={{major}}.{{minor}}-full
|
||||
type=semver,pattern={{major}}-full
|
||||
labels: |
|
||||
org.opencontainers.image.title=MediaCMS Worker Full
|
||||
org.opencontainers.image.description=MediaCMS Celery worker with additional codecs for advanced transcoding features.
|
||||
org.opencontainers.image.vendor=MediaCMS
|
||||
org.opencontainers.image.url=https://mediacms.io/
|
||||
org.opencontainers.image.source=https://github.com/mediacms-io/mediacms
|
||||
org.opencontainers.image.licenses=AGPL-3.0
|
||||
|
||||
- name: Build and push worker-full image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
target: worker-full
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta-worker-full.outputs.tags }}
|
||||
labels: ${{ steps.meta-worker-full.outputs.labels }}
|
||||
|
||||
- name: Docker meta for nginx image
|
||||
id: meta-nginx
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
mediacms/mediacms-nginx
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
labels: |
|
||||
org.opencontainers.image.title=MediaCMS Nginx
|
||||
org.opencontainers.image.description=Nginx web server for MediaCMS, serving static and media files.
|
||||
org.opencontainers.image.vendor=MediaCMS
|
||||
org.opencontainers.image.url=https://mediacms.io/
|
||||
org.opencontainers.image.source=https://github.com/mediacms-io/mediacms
|
||||
org.opencontainers.image.licenses=AGPL-3.0
|
||||
|
||||
- name: Build and push nginx image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.nginx
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta-nginx.outputs.tags }}
|
||||
labels: ${{ steps.meta-nginx.outputs.labels }}
|
||||
15
.github/workflows/lint_test.yml
vendored
15
.github/workflows/lint_test.yml
vendored
@@ -1,15 +0,0 @@
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: pre-commit/action@v3.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
6
.github/workflows/pre-commit.yml
vendored
6
.github/workflows/pre-commit.yml
vendored
@@ -1,13 +1,11 @@
|
||||
name: pre-commit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
name: Pre-Commit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
15
.github/workflows/python.yml
vendored
15
.github/workflows/python.yml
vendored
@@ -1,14 +1,11 @@
|
||||
name: Python Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
name: Build & test via docker-compose
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -16,10 +13,10 @@ jobs:
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Build the Stack
|
||||
run: docker-compose -f docker-compose-dev.yaml build
|
||||
run: docker compose -f docker-compose-dev.yaml build
|
||||
|
||||
- name: Start containers
|
||||
run: docker-compose -f docker-compose-dev.yaml up -d
|
||||
run: docker compose -f docker-compose-dev.yaml up -d
|
||||
|
||||
- name: List containers
|
||||
run: docker ps
|
||||
@@ -29,10 +26,10 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Run Django Tests
|
||||
run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
|
||||
run: docker compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
|
||||
|
||||
# Run with coverage, saves report on htmlcov dir
|
||||
# run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest --cov --cov-report=html --cov-config=.coveragerc
|
||||
|
||||
- name: Tear down the Stack
|
||||
run: docker-compose -f docker-compose-dev.yaml down
|
||||
run: docker compose -f docker-compose-dev.yaml down
|
||||
|
||||
26
.gitignore
vendored
26
.gitignore
vendored
@@ -1,10 +1,16 @@
|
||||
cli-tool/.env
|
||||
frontend/package-lock.json
|
||||
custom/local_settings.py
|
||||
custom/static/images/*
|
||||
!custom/static/images/.gitkeep
|
||||
custom/static/css/*
|
||||
!custom/static/css/.gitkeep
|
||||
media_files/encoded/
|
||||
media_files/original/
|
||||
media_files/hls/
|
||||
media_files/chunks/
|
||||
media_files/uploads/
|
||||
media_files/tinymce_media/
|
||||
postgres_data/
|
||||
celerybeat-schedule
|
||||
logs/
|
||||
@@ -16,4 +22,22 @@ static/mptt/
|
||||
static/rest_framework/
|
||||
static/drf-yasg
|
||||
cms/local_settings.py
|
||||
deploy/docker/local_settings.py
|
||||
config/local_settings.py
|
||||
yt.readme.md
|
||||
/frontend-tools/video-editor/node_modules
|
||||
/frontend-tools/video-editor/client/node_modules
|
||||
/static_collected
|
||||
/frontend-tools/video-editor-v1
|
||||
frontend-tools/.DS_Store
|
||||
static/video_editor/videos/sample-video-30s.mp4
|
||||
static/video_editor/videos/sample-video-37s.mp4
|
||||
/frontend-tools/video-editor-v2
|
||||
.DS_Store
|
||||
static/video_editor/videos/sample-video-10m.mp4
|
||||
static/video_editor/videos/sample-video-10s.mp4
|
||||
frontend-tools/video-js/public/videos/sample-video-white.mp4
|
||||
frontend-tools/video-editor/client/public/videos/sample-video.mp3
|
||||
frontend-tools/chapters-editor/client/public/videos/sample-video.mp3
|
||||
static/chapters_editor/videos/sample-video.mp3
|
||||
static/video_editor/videos/sample-video.mp3
|
||||
backups/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 6.0.0
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/pycqa/isort
|
||||
|
||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
/templates/cms/*
|
||||
/templates/*.html
|
||||
*.scss
|
||||
21
.prettierrc
Normal file
21
.prettierrc
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.css", "*.scss"],
|
||||
"options": {
|
||||
"singleQuote": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1 @@
|
||||
Yiannis Stergiou - ys.stergiou@gmail.com
|
||||
Markos Gogoulos - mgogoulos@gmail.com
|
||||
Swift Ugandan - swiftugandan@gmail.com
|
||||
|
||||
Please see https://github.com/mediacms-io/mediacms/graphs/contributors for complete list of contributors to this repository!
|
||||
441
DOCKER_RESTRUCTURE_SUMMARY.md
Normal file
441
DOCKER_RESTRUCTURE_SUMMARY.md
Normal file
@@ -0,0 +1,441 @@
|
||||
# MediaCMS Docker Restructure Summary - Version 7.3
|
||||
|
||||
## Overview
|
||||
|
||||
MediaCMS 7.3 introduces a complete Docker architecture restructure, moving from a monolithic supervisord-based setup to modern microservices with proper separation of concerns.
|
||||
|
||||
**⚠️ BREAKING CHANGES** - See [`UPGRADE_TO_7.3.md`](./UPGRADE_TO_7.3.md) for migration guide.
|
||||
|
||||
## Architecture Comparison
|
||||
|
||||
### Before (7.x) - Monolithic
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Single Container │
|
||||
│ ┌──────────┐ │
|
||||
│ │Supervisor│ │
|
||||
│ └────┬─────┘ │
|
||||
│ ├─── nginx (port 80) │
|
||||
│ ├─── uwsgi (Django) │
|
||||
│ ├─── celery beat │
|
||||
│ ├─── celery workers │
|
||||
│ └─── migrations │
|
||||
│ │
|
||||
│ Volumes: ./ mounted to container │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### After (7.3) - Microservices
|
||||
```
|
||||
┌────────┐ ┌─────┐ ┌───────────┐ ┌──────────┐
|
||||
│ nginx │→ │ web │ │celery_beat│ │ celery │
|
||||
│ │ │uwsgi│ │ │ │ workers │
|
||||
└────────┘ └─────┘ └───────────┘ └──────────┘
|
||||
│
|
||||
┌───────┴────────┐
|
||||
│ db │ redis │
|
||||
└───────┴────────┘
|
||||
|
||||
Volumes: Named volumes + custom/ bind mount
|
||||
```
|
||||
|
||||
## What Changed
|
||||
|
||||
### 1. Container Services
|
||||
|
||||
| Component | Before (7.x) | After (7.3) |
|
||||
|-----------|-------------|-------------|
|
||||
| **nginx** | Inside main container | Separate container |
|
||||
| **Django/uWSGI** | Inside main container | Dedicated `web` container |
|
||||
| **Celery Beat** | Inside main container | Dedicated container |
|
||||
| **Celery Workers** | Inside main container | Separate containers (short/long) |
|
||||
| **Migrations** | Via environment flag | Init container (runs once) |
|
||||
|
||||
### 2. Volume Strategy
|
||||
|
||||
| Data | Before (7.x) | After (7.3) |
|
||||
|------|-------------|-------------|
|
||||
| **Application code** | Bind mount `./` | **Built into image** |
|
||||
| **Media files** | `./media_files` | **Named volume** `media_files` |
|
||||
| **Static files** | `./static` | **Built into image** (collectstatic at build) |
|
||||
| **Logs** | `./logs` | **Named volume** `logs` |
|
||||
| **PostgreSQL** | `../postgres_data` | **Named volume** `postgres_data` |
|
||||
| **Custom config** | `cms/local_settings.py` | **Bind mount** `./custom/` |
|
||||
|
||||
### 3. Removed Components
|
||||
|
||||
- ❌ supervisord and all supervisord configs
|
||||
- ❌ docker-entrypoint.sh (permission fixing script)
|
||||
- ❌ `ENABLE_*` environment variables
|
||||
- ❌ Runtime collectstatic
|
||||
- ❌ nginx from base image
|
||||
|
||||
### 4. New Components
|
||||
|
||||
- ✅ `custom/` directory for user customizations
|
||||
- ✅ Multi-stage Dockerfile (base, web, worker, worker-full)
|
||||
- ✅ Separate nginx image (`Dockerfile.nginx`)
|
||||
- ✅ Build-time collectstatic
|
||||
- ✅ USER www-data (non-root containers)
|
||||
- ✅ Health checks for all services
|
||||
- ✅ Makefile with common tasks
|
||||
|
||||
## Key Improvements
|
||||
|
||||
### Security
|
||||
- ✅ Containers run as `www-data` (UID 33), not root
|
||||
- ✅ Read-only mounts where possible
|
||||
- ✅ Smaller attack surface per container
|
||||
- ✅ No privilege escalation needed
|
||||
|
||||
### Performance
|
||||
- ✅ Named volumes have better I/O than bind mounts
|
||||
- ✅ Static files built into image (no runtime collection)
|
||||
- ✅ Faster container startups
|
||||
- ✅ No chown on millions of files at startup
|
||||
|
||||
### Scalability
|
||||
- ✅ Scale web and workers independently
|
||||
- ✅ Ready for load balancing
|
||||
- ✅ Can use Docker Swarm or Kubernetes
|
||||
- ✅ Horizontal scaling: `docker compose scale celery_short=3`
|
||||
|
||||
### Maintainability
|
||||
- ✅ One process per container (proper separation)
|
||||
- ✅ Clear service dependencies
|
||||
- ✅ Standard Docker patterns
|
||||
- ✅ Easier debugging (service-specific logs)
|
||||
- ✅ Immutable images
|
||||
|
||||
### Developer Experience
|
||||
- ✅ Separate dev compose with hot reload
|
||||
- ✅ `custom/` directory for all customizations
|
||||
- ✅ Clear documentation and examples
|
||||
- ✅ Makefile targets for common tasks
|
||||
|
||||
## New Customization System
|
||||
|
||||
### The `custom/` Directory
|
||||
|
||||
All user customizations now go in a dedicated directory:
|
||||
|
||||
```
|
||||
custom/
|
||||
├── README.md # Full documentation
|
||||
├── local_settings.py.example # Template file
|
||||
├── local_settings.py # Your Django settings (gitignored)
|
||||
└── static/
|
||||
├── images/ # Custom logos (gitignored)
|
||||
│ └── logo_dark.png
|
||||
└── css/ # Custom CSS (gitignored)
|
||||
└── custom.css
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Clear separation from core code
|
||||
- Works out-of-box (empty directory is fine)
|
||||
- Gitignored customizations
|
||||
- Well documented with examples
|
||||
|
||||
See [`custom/README.md`](./custom/README.md) for usage guide.
|
||||
|
||||
## Docker Images
|
||||
|
||||
### Images to Build
|
||||
|
||||
```bash
|
||||
# Web image (Django + uWSGI)
|
||||
docker build --target web -t mediacms/mediacms:7.3 .
|
||||
|
||||
# Worker image (Celery)
|
||||
docker build --target worker -t mediacms/mediacms-worker:7.3 .
|
||||
|
||||
# Worker-full image (Celery with extra codecs)
|
||||
docker build --target worker-full -t mediacms/mediacms-worker:7.3-full .
|
||||
|
||||
# Nginx image
|
||||
docker build -f Dockerfile.nginx -t mediacms/mediacms-nginx:7.3 .
|
||||
```
|
||||
|
||||
### Image Sizes
|
||||
|
||||
| Image | Approximate Size |
|
||||
|-------|-----------------|
|
||||
| mediacms:7.3 | ~800MB |
|
||||
| mediacms-worker:7.3 | ~800MB |
|
||||
| mediacms-worker:7.3-full | ~1.2GB |
|
||||
| mediacms-nginx:7.3 | ~50MB |
|
||||
|
||||
## Deployment Scenarios
|
||||
|
||||
### 1. Development
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-dev.yaml up
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- File mounts for live editing
|
||||
- Django runserver with DEBUG=True
|
||||
- Frontend hot reload
|
||||
- Immediate code changes
|
||||
|
||||
### 2. Production (HTTP)
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Immutable images
|
||||
- Named volumes for data
|
||||
- Production-ready
|
||||
- Port 80
|
||||
|
||||
### 3. Production (HTTPS with Let's Encrypt)
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yaml -f docker-compose-cert.yaml up -d
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Automatic SSL certificates
|
||||
- Auto-renewal
|
||||
- nginx-proxy + acme-companion
|
||||
- Production-ready
|
||||
|
||||
## Minimal Deployment (No Code Required!)
|
||||
|
||||
**Version 7.3 requires ONLY:**
|
||||
|
||||
1. ✅ `docker-compose.yaml` file
|
||||
2. ✅ Docker images (from Docker Hub)
|
||||
3. ⚠️ `custom/` directory (optional, only if customizing)
|
||||
|
||||
**No git repo needed!** Download docker-compose.yaml from release/docs and start.
|
||||
|
||||
## Migration Requirements
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
⚠️ **Not backward compatible** - Manual migration required
|
||||
|
||||
**What needs migration:**
|
||||
1. ✅ PostgreSQL database (dump and restore)
|
||||
2. ✅ Media files (copy to named volume)
|
||||
3. ✅ Custom settings → `custom/local_settings.py` (if you had them)
|
||||
4. ✅ Custom logos/CSS → `custom/static/` (if you had them)
|
||||
5. ⚠️ Backup scripts (new volume paths)
|
||||
6. ⚠️ Monitoring (new container names)
|
||||
|
||||
### Migration Steps
|
||||
|
||||
See [`UPGRADE_TO_7.3.md`](./UPGRADE_TO_7.3.md) for complete guide.
|
||||
|
||||
**Quick overview:**
|
||||
```bash
|
||||
# 1. Backup
|
||||
docker compose exec db pg_dump -U mediacms mediacms > backup.sql
|
||||
tar -czf media_backup.tar.gz media_files/
|
||||
cp docker-compose.yaml docker-compose.yaml.old
|
||||
|
||||
# 2. Download new docker-compose.yaml
|
||||
wget https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/docker-compose.yaml
|
||||
|
||||
# 3. Create custom/ if needed
|
||||
mkdir -p custom/static/{images,css}
|
||||
# Copy your old settings/logos if you had them
|
||||
|
||||
# 4. Pull images and start
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
|
||||
# 5. Restore data
|
||||
cat backup.sql | docker compose exec -T db psql -U mediacms mediacms
|
||||
# (See full guide for media migration)
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### Created/Reorganized
|
||||
|
||||
```
|
||||
├── Dockerfile # Multi-stage (base, web, worker)
|
||||
├── Dockerfile.nginx # Nginx image
|
||||
├── docker-compose.yaml # Production
|
||||
├── docker-compose-cert.yaml # Production + HTTPS
|
||||
├── docker-compose-dev.yaml # Development
|
||||
├── Makefile # Common tasks
|
||||
├── custom/ # User customizations
|
||||
│ ├── README.md
|
||||
│ ├── local_settings.py.example
|
||||
│ └── static/
|
||||
├── config/
|
||||
│ ├── imagemagick/policy.xml
|
||||
│ ├── nginx/
|
||||
│ │ ├── nginx.conf
|
||||
│ │ └── site.conf
|
||||
│ ├── nginx-proxy/
|
||||
│ │ └── client_max_body_size.conf
|
||||
│ └── uwsgi/
|
||||
│ └── uwsgi.ini
|
||||
└── scripts/
|
||||
└── run-migrations.sh
|
||||
```
|
||||
|
||||
## Makefile Targets
|
||||
|
||||
New Makefile with common operations:
|
||||
|
||||
```bash
|
||||
make backup-db # PostgreSQL dump with timestamp
|
||||
make admin-shell # Quick Django shell access
|
||||
make build-frontend # Rebuild frontend assets
|
||||
make test # Run test suite
|
||||
```
|
||||
|
||||
## Rollback Strategy
|
||||
|
||||
If migration fails:
|
||||
|
||||
```bash
|
||||
# 1. Stop new version
|
||||
docker compose down
|
||||
|
||||
# 2. Checkout old version
|
||||
git checkout main
|
||||
|
||||
# 3. Restore old compose
|
||||
git checkout main docker-compose.yaml
|
||||
|
||||
# 4. Restore data from backups
|
||||
# (See UPGRADE_TO_7.3.md for details)
|
||||
|
||||
# 5. Start old version
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
Before production deployment:
|
||||
|
||||
- [ ] Migrations run successfully
|
||||
- [ ] Static files load correctly
|
||||
- [ ] Media files upload/download work
|
||||
- [ ] Video transcoding works (check celery_long logs)
|
||||
- [ ] Admin panel accessible
|
||||
- [ ] Custom settings loaded (if using custom/)
|
||||
- [ ] Database persists across restarts
|
||||
- [ ] Media persists across restarts
|
||||
- [ ] Logs accessible via `docker compose logs`
|
||||
- [ ] Health checks pass: `docker compose ps`
|
||||
|
||||
## Common Post-Upgrade Tasks
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
# Before: tail -f logs/uwsgi.log
|
||||
# After:
|
||||
docker compose logs -f web
|
||||
docker compose logs -f celery_long
|
||||
```
|
||||
|
||||
### Access Shell
|
||||
```bash
|
||||
# Before: docker exec -it <container> bash
|
||||
# After:
|
||||
make admin-shell
|
||||
# Or: docker compose exec web bash
|
||||
```
|
||||
|
||||
### Restart Service
|
||||
```bash
|
||||
# Before: docker restart <container>
|
||||
# After:
|
||||
docker compose restart web
|
||||
```
|
||||
|
||||
### Scale Workers
|
||||
```bash
|
||||
# New capability:
|
||||
docker compose up -d --scale celery_short=3 --scale celery_long=2
|
||||
```
|
||||
|
||||
### Database Backup
|
||||
```bash
|
||||
# Before: Custom script
|
||||
# After:
|
||||
make backup-db
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Startup Time
|
||||
- **Before**: Slower (chown on all files)
|
||||
- **After**: Faster (no permission fixing)
|
||||
|
||||
### I/O Performance
|
||||
- **Before**: Bind mount overhead
|
||||
- **After**: Named volumes (better performance)
|
||||
|
||||
### Memory Usage
|
||||
- **Before**: Single large container
|
||||
- **After**: Multiple smaller containers (better resource allocation)
|
||||
|
||||
## New Volume Management
|
||||
|
||||
### List Volumes
|
||||
```bash
|
||||
docker volume ls | grep mediacms
|
||||
```
|
||||
|
||||
### Inspect Volume
|
||||
```bash
|
||||
docker volume inspect mediacms_media_files
|
||||
```
|
||||
|
||||
### Backup Volume
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v mediacms_media_files:/data:ro \
|
||||
-v $(pwd):/backup \
|
||||
alpine tar czf /backup/media_backup.tar.gz -C /data .
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Upgrade Guide**: [`UPGRADE_TO_7.3.md`](./UPGRADE_TO_7.3.md)
|
||||
- **Customization**: [`custom/README.md`](./custom/README.md)
|
||||
- **Admin Docs**: `docs/admins_docs.md`
|
||||
|
||||
## Timeline Estimates
|
||||
|
||||
| Instance Size | Expected Migration Time |
|
||||
|---------------|------------------------|
|
||||
| Small (<100 videos) | 30-60 minutes |
|
||||
| Medium (100-1000 videos) | 1-3 hours |
|
||||
| Large (>1000 videos) | 3-8 hours |
|
||||
|
||||
**Plan accordingly and schedule during low-traffic periods!**
|
||||
|
||||
## Getting Help
|
||||
|
||||
1. Read [`UPGRADE_TO_7.3.md`](./UPGRADE_TO_7.3.md) thoroughly
|
||||
2. Check [`custom/README.md`](./custom/README.md) for customization
|
||||
3. Search GitHub Issues
|
||||
4. Test in staging first
|
||||
5. Keep backups for at least 1 week post-upgrade
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Read [`UPGRADE_TO_7.3.md`](./UPGRADE_TO_7.3.md)
|
||||
2. ✅ Test in development: `docker compose -f docker-compose-dev.yaml up`
|
||||
3. ✅ Backup production data
|
||||
4. ✅ Test migration in staging
|
||||
5. ✅ Plan maintenance window
|
||||
6. ✅ Execute migration
|
||||
7. ✅ Monitor for 24-48 hours
|
||||
|
||||
---
|
||||
|
||||
**Ready to upgrade?** Start with: [`UPGRADE_TO_7.3.md`](./UPGRADE_TO_7.3.md)
|
||||
167
Dockerfile
167
Dockerfile
@@ -1,69 +1,126 @@
|
||||
FROM python:3.8-buster AS compile-image
|
||||
FROM python:3.13.5-slim-bookworm AS build-image
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
# Set up virtualenv
|
||||
ENV VIRTUAL_ENV=/home/mediacms.io
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
ENV PIP_NO_CACHE_DIR=1
|
||||
|
||||
RUN mkdir -p /home/mediacms.io/mediacms/{logs} && cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
|
||||
|
||||
# Install dependencies:
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . /home/mediacms.io/mediacms
|
||||
WORKDIR /home/mediacms.io/mediacms
|
||||
|
||||
RUN wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
|
||||
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d ../bento4 && \
|
||||
mv ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* ../bento4/ && \
|
||||
rm -rf ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
|
||||
rm -rf ../bento4/docs && \
|
||||
rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
|
||||
|
||||
############ RUNTIME IMAGE ############
|
||||
FROM python:3.8-slim-buster as runtime-image
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# See: https://github.com/celery/celery/issues/6285#issuecomment-715316219
|
||||
ENV CELERY_APP='cms'
|
||||
|
||||
# Use these to toggle which processes supervisord should run
|
||||
ENV ENABLE_UWSGI='yes'
|
||||
ENV ENABLE_NGINX='yes'
|
||||
ENV ENABLE_CELERY_BEAT='yes'
|
||||
ENV ENABLE_CELERY_SHORT='yes'
|
||||
ENV ENABLE_CELERY_LONG='yes'
|
||||
ENV ENABLE_MIGRATIONS='yes'
|
||||
|
||||
# Set up virtualenv
|
||||
ENV VIRTUAL_ENV=/home/mediacms.io
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
COPY --chown=www-data:www-data --from=compile-image /home/mediacms.io /home/mediacms.io
|
||||
|
||||
RUN apt-get update -y && apt-get -y upgrade && apt-get install --no-install-recommends \
|
||||
supervisor nginx imagemagick procps wget xz-utils -y && \
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends wget xz-utils unzip && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt-get purge --auto-remove && \
|
||||
apt-get clean
|
||||
|
||||
RUN wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
|
||||
mkdir -p ffmpeg-tmp && \
|
||||
RUN wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
||||
|
||||
RUN mkdir -p ffmpeg-tmp && \
|
||||
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C ffmpeg-tmp && \
|
||||
cp -v ffmpeg-tmp/ffmpeg ffmpeg-tmp/ffprobe ffmpeg-tmp/qt-faststart /usr/local/bin && \
|
||||
rm -rf ffmpeg-tmp ffmpeg-release-amd64-static.tar.xz
|
||||
|
||||
RUN mkdir -p /home/mediacms.io/bento4 && \
|
||||
wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
|
||||
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d /home/mediacms.io/bento4 && \
|
||||
mv /home/mediacms.io/bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* /home/mediacms.io/bento4/ && \
|
||||
rm -rf /home/mediacms.io/bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
|
||||
rm -rf /home/mediacms.io/bento4/docs && \
|
||||
rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
|
||||
|
||||
############ BASE RUNTIME IMAGE ############
|
||||
FROM python:3.13.5-slim-bookworm AS base
|
||||
|
||||
LABEL org.opencontainers.image.version="7.3"
|
||||
LABEL org.opencontainers.image.title="MediaCMS"
|
||||
LABEL org.opencontainers.image.description="Modern, scalable and open source video platform"
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
CELERY_APP='cms' \
|
||||
VIRTUAL_ENV=/home/mediacms.io \
|
||||
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
RUN apt-get update -y && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
imagemagick \
|
||||
procps \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
zlib1g-dev \
|
||||
zlib1g \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
libpq-dev \
|
||||
gosu \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /home/mediacms.io/mediacms/{logs,media_files,static} && \
|
||||
cd /home/mediacms.io && \
|
||||
python3 -m venv $VIRTUAL_ENV
|
||||
|
||||
COPY requirements.txt requirements-dev.txt ./
|
||||
|
||||
ARG DEVELOPMENT_MODE=False
|
||||
RUN pip install --no-cache-dir uv && \
|
||||
uv pip install --no-binary lxml --no-binary xmlsec -r requirements.txt && \
|
||||
if [ "$DEVELOPMENT_MODE" = "True" ]; then \
|
||||
echo "Installing development dependencies..." && \
|
||||
uv pip install -r requirements-dev.txt; \
|
||||
fi && \
|
||||
apt-get purge -y --auto-remove \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libpq-dev
|
||||
|
||||
COPY --from=build-image /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg
|
||||
COPY --from=build-image /usr/local/bin/ffprobe /usr/local/bin/ffprobe
|
||||
COPY --from=build-image /usr/local/bin/qt-faststart /usr/local/bin/qt-faststart
|
||||
COPY --from=build-image /home/mediacms.io/bento4 /home/mediacms.io/bento4
|
||||
|
||||
COPY --chown=www-data:www-data . /home/mediacms.io/mediacms
|
||||
WORKDIR /home/mediacms.io/mediacms
|
||||
|
||||
EXPOSE 9000 80
|
||||
# Copy imagemagick policy for sprite thumbnail generation
|
||||
COPY config/imagemagick/policy.xml /etc/ImageMagick-6/policy.xml
|
||||
|
||||
RUN chmod +x ./deploy/docker/entrypoint.sh
|
||||
# Create www-data user directories and set permissions
|
||||
RUN mkdir -p /var/run/mediacms && \
|
||||
chown -R www-data:www-data /home/mediacms.io/mediacms/logs \
|
||||
/home/mediacms.io/mediacms/media_files \
|
||||
/home/mediacms.io/mediacms/static \
|
||||
/var/run/mediacms
|
||||
|
||||
ENTRYPOINT ["./deploy/docker/entrypoint.sh"]
|
||||
# Collect static files during build
|
||||
RUN python manage.py collectstatic --noinput && \
|
||||
chown -R www-data:www-data /home/mediacms.io/mediacms/static
|
||||
|
||||
CMD ["./deploy/docker/start.sh"]
|
||||
# Run container as www-data user
|
||||
USER www-data
|
||||
|
||||
############ WEB IMAGE (Django/uWSGI) ############
|
||||
FROM base AS web
|
||||
|
||||
# Install uWSGI
|
||||
RUN uv pip install uwsgi
|
||||
|
||||
# Copy uWSGI configuration
|
||||
COPY config/uwsgi/uwsgi.ini /home/mediacms.io/mediacms/uwsgi.ini
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
CMD ["/home/mediacms.io/bin/uwsgi", "--ini", "/home/mediacms.io/mediacms/uwsgi.ini"]
|
||||
|
||||
############ WORKER IMAGE (Celery) ############
|
||||
FROM base AS worker
|
||||
|
||||
# CMD will be overridden in docker-compose for different worker types
|
||||
|
||||
############ FULL WORKER IMAGE (Celery with extra codecs) ############
|
||||
FROM worker AS worker-full
|
||||
|
||||
COPY requirements-full.txt ./
|
||||
RUN mkdir -p /root/.cache/ && \
|
||||
chmod go+rwx /root/ && \
|
||||
chmod go+rwx /root/.cache/ && \
|
||||
uv pip install -r requirements-full.txt
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
FROM mediacms/mediacms:latest
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
# Set up virtualenv
|
||||
ENV VIRTUAL_ENV=/home/mediacms.io
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
ENV PIP_NO_CACHE_DIR=1
|
||||
|
||||
RUN cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
|
||||
|
||||
COPY requirements.txt .
|
||||
COPY requirements-dev.txt .
|
||||
RUN pip install -r requirements-dev.txt
|
||||
|
||||
WORKDIR /home/mediacms.io/mediacms
|
||||
18
Dockerfile.nginx
Normal file
18
Dockerfile.nginx
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
LABEL org.opencontainers.image.version="7.3"
|
||||
LABEL org.opencontainers.image.title="MediaCMS Nginx"
|
||||
LABEL org.opencontainers.image.description="Nginx server for MediaCMS"
|
||||
|
||||
# Copy nginx configurations
|
||||
COPY config/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY config/nginx/site.conf /etc/nginx/conf.d/default.conf
|
||||
COPY config/nginx/uwsgi_params /etc/nginx/uwsgi_params
|
||||
|
||||
# Create directories for static and media files (will be volumes)
|
||||
RUN mkdir -p /var/www/media /var/www/static && \
|
||||
chown -R nginx:nginx /var/www
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
23
HISTORY.md
Normal file
23
HISTORY.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# History
|
||||
|
||||
## 3.0.0
|
||||
|
||||
### Features
|
||||
- Updates Python/Django requirements and Dockerfile to use latest 3.11 Python - https://github.com/mediacms-io/mediacms/pull/826/files. This update requires some manual steps, for existing (not new) installations. Check the update section under the [Admin docs](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#2-server-installation), either for single server or for Docker Compose installations
|
||||
- Upgrade postgres on Docker Compose - https://github.com/mediacms-io/mediacms/pull/749
|
||||
|
||||
### Fixes
|
||||
- video player options for HLS - https://github.com/mediacms-io/mediacms/pull/832
|
||||
- AVI videos not correctly recognised as videos - https://github.com/mediacms-io/mediacms/pull/833
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Fixes
|
||||
- Increase uwsgi buffer-size parameter. This prevents an error by uwsgi with large headers - [#5b60](https://github.com/mediacms-io/mediacms/commit/5b601698a41ad97f08c1830e14b1c18f73ab8315)
|
||||
- Fix issues with comments. These were not reported on the tracker but it is certain that they would not show comments on media files (non videos but also videos). Unfortunately this reverts work done with Timestamps on comments + Mentions on comments, more on PR [#802](https://github.com/mediacms-io/mediacms/pull/802)
|
||||
|
||||
### Features
|
||||
- Allow tags to contains other characters too, not only English alphabet ones [#801](https://github.com/mediacms-io/mediacms/pull/801)
|
||||
- Add simple cookie consent code [#799](https://github.com/mediacms-io/mediacms/pull/799)
|
||||
- Allow password reset & email verify pages on global login required [#790](https://github.com/mediacms-io/mediacms/pull/790)
|
||||
- Add api_url field to search api [#692](https://github.com/mediacms-io/mediacms/pull/692)
|
||||
32
Makefile
Normal file
32
Makefile
Normal file
@@ -0,0 +1,32 @@
|
||||
.PHONY: admin-shell build-frontend backup-db
|
||||
|
||||
admin-shell:
|
||||
@container_id=$$(docker compose ps -q web); \
|
||||
if [ -z "$$container_id" ]; then \
|
||||
echo "Web container not found"; \
|
||||
exit 1; \
|
||||
else \
|
||||
docker exec -it $$container_id /bin/bash; \
|
||||
fi
|
||||
|
||||
build-frontend:
|
||||
docker compose -f docker-compose-dev.yaml exec frontend npm run dist
|
||||
cp -r frontend/dist/static/* static/
|
||||
docker compose -f docker-compose-dev.yaml restart web
|
||||
|
||||
test:
|
||||
docker compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
|
||||
|
||||
backup-db:
|
||||
@echo "Creating PostgreSQL database dump..."
|
||||
@mkdir -p backups
|
||||
@timestamp=$$(date +%Y%m%d_%H%M%S); \
|
||||
dump_file="backups/mediacms_dump_$${timestamp}.sql"; \
|
||||
docker compose exec -T db pg_dump -U mediacms -d mediacms > "$${dump_file}"; \
|
||||
if [ $$? -eq 0 ]; then \
|
||||
echo "Database dump created successfully: $${dump_file}"; \
|
||||
else \
|
||||
echo "Database dump failed"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
292
QUICKSTART.md
Normal file
292
QUICKSTART.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# MediaCMS 7.3 - Quick Start
|
||||
|
||||
## Minimal Deployment (No Code Required!)
|
||||
|
||||
MediaCMS 7.3 can be deployed with **just 2 files**:
|
||||
|
||||
1. `docker-compose.yaml`
|
||||
2. `custom/` directory (optional)
|
||||
|
||||
**No git repo, no code checkout needed!** Everything runs from Docker images.
|
||||
|
||||
---
|
||||
|
||||
## Fresh Installation
|
||||
|
||||
### 1. Create deployment directory
|
||||
|
||||
```bash
|
||||
mkdir mediacms && cd mediacms
|
||||
```
|
||||
|
||||
### 2. Download docker-compose.yaml
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/docker-compose.yaml
|
||||
```
|
||||
|
||||
Or with curl:
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/docker-compose.yaml
|
||||
```
|
||||
|
||||
### 3. Start MediaCMS
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 4. Access your site
|
||||
|
||||
- **Frontend**: http://localhost
|
||||
- **Admin**: http://localhost/admin
|
||||
- Username: `admin`
|
||||
- Password: Check logs for auto-generated password:
|
||||
```bash
|
||||
docker compose logs migrations | grep "password:"
|
||||
```
|
||||
|
||||
**That's it!** 🎉
|
||||
|
||||
---
|
||||
|
||||
## Optional: Customization
|
||||
|
||||
### Add Custom Settings
|
||||
|
||||
```bash
|
||||
# 1. Create custom directory
|
||||
mkdir -p custom/static/{images,css}
|
||||
|
||||
# 2. Download example template
|
||||
wget -O custom/local_settings.py.example \
|
||||
https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/custom/local_settings.py.example
|
||||
|
||||
# 3. Copy and edit
|
||||
cp custom/local_settings.py.example custom/local_settings.py
|
||||
nano custom/local_settings.py
|
||||
```
|
||||
|
||||
Example customizations:
|
||||
```python
|
||||
# custom/local_settings.py
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = ['media.example.com']
|
||||
PORTAL_NAME = "My Media Portal"
|
||||
```
|
||||
|
||||
### Add Custom Logo
|
||||
|
||||
```bash
|
||||
# 1. Copy your logo
|
||||
cp ~/my-logo.png custom/static/images/logo_dark.png
|
||||
|
||||
# 2. Reference in settings
|
||||
cat >> custom/local_settings.py <<EOF
|
||||
PORTAL_LOGO_DARK_PNG = "/custom/static/images/logo_dark.png"
|
||||
EOF
|
||||
|
||||
# 3. Restart (no rebuild needed!)
|
||||
docker compose restart web
|
||||
```
|
||||
|
||||
### Add Custom CSS
|
||||
|
||||
```bash
|
||||
# 1. Create CSS file
|
||||
cat > custom/static/css/custom.css <<EOF
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
EOF
|
||||
|
||||
# 2. Reference in settings
|
||||
cat >> custom/local_settings.py <<EOF
|
||||
EXTRA_CSS_PATHS = ["/custom/static/css/custom.css"]
|
||||
EOF
|
||||
|
||||
# 3. Restart (no rebuild needed!)
|
||||
docker compose restart web
|
||||
```
|
||||
|
||||
**Note**: Both settings AND static files only need restart - nginx serves custom/ files directly!
|
||||
|
||||
---
|
||||
|
||||
## HTTPS with Let's Encrypt
|
||||
|
||||
### 1. Download cert overlay
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/docker-compose-cert.yaml
|
||||
```
|
||||
|
||||
### 2. Edit domains
|
||||
|
||||
```bash
|
||||
nano docker-compose-cert.yaml
|
||||
```
|
||||
|
||||
Change these lines:
|
||||
```yaml
|
||||
VIRTUAL_HOST: 'media.example.com' # Your domain
|
||||
LETSENCRYPT_HOST: 'media.example.com' # Your domain
|
||||
LETSENCRYPT_EMAIL: 'admin@example.com' # Your email
|
||||
```
|
||||
|
||||
### 3. Start with SSL
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yaml -f docker-compose-cert.yaml up -d
|
||||
```
|
||||
|
||||
**SSL certificates are issued automatically!**
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
Your deployment directory:
|
||||
|
||||
```
|
||||
mediacms/
|
||||
├── docker-compose.yaml # Required
|
||||
├── docker-compose-cert.yaml # Optional (for HTTPS)
|
||||
└── custom/ # Optional (for customizations)
|
||||
├── local_settings.py # Django settings
|
||||
└── static/
|
||||
├── images/ # Custom logos
|
||||
└── css/ # Custom CSS
|
||||
```
|
||||
|
||||
**Named volumes** (managed by Docker):
|
||||
- `mediacms_postgres_data` - Database
|
||||
- `mediacms_media_files` - Uploaded media
|
||||
- `mediacms_static_files` - Static assets
|
||||
- `mediacms_logs` - Application logs
|
||||
|
||||
---
|
||||
|
||||
## Common Commands
|
||||
|
||||
### View logs
|
||||
```bash
|
||||
docker compose logs -f web
|
||||
docker compose logs -f celery_long
|
||||
```
|
||||
|
||||
### Access Django shell
|
||||
```bash
|
||||
docker compose exec web python manage.py shell
|
||||
```
|
||||
|
||||
### Create admin user
|
||||
```bash
|
||||
docker compose exec web python manage.py createsuperuser
|
||||
```
|
||||
|
||||
### Restart service
|
||||
```bash
|
||||
docker compose restart web
|
||||
```
|
||||
|
||||
### Stop everything
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Update to newer version
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backup
|
||||
|
||||
### Database backup
|
||||
```bash
|
||||
docker compose exec db pg_dump -U mediacms mediacms > backup_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
### Media files backup
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v mediacms_media_files:/data:ro \
|
||||
-v $(pwd):/backup \
|
||||
alpine tar czf /backup/media_backup_$(date +%Y%m%d).tar.gz -C /data .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Upgrading from 7.x?
|
||||
|
||||
If you're upgrading from an older MediaCMS version, see:
|
||||
- **[UPGRADE_TO_7.3.md](./UPGRADE_TO_7.3.md)** - Complete migration guide
|
||||
- **[DOCKER_RESTRUCTURE_SUMMARY.md](./DOCKER_RESTRUCTURE_SUMMARY.md)** - What changed
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Customization**: Download [`custom/README.md`](https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/custom/README.md)
|
||||
- **Upgrade Guide**: [UPGRADE_TO_7.3.md](./UPGRADE_TO_7.3.md)
|
||||
- **Architecture**: [DOCKER_RESTRUCTURE_SUMMARY.md](./DOCKER_RESTRUCTURE_SUMMARY.md)
|
||||
- **Project Docs**: https://docs.mediacms.io
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Can't access the site?
|
||||
|
||||
Check services are running:
|
||||
```bash
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
All services should be "Up" or "Exited (0)" for migrations.
|
||||
|
||||
### Forgot admin password?
|
||||
|
||||
Check logs:
|
||||
```bash
|
||||
docker compose logs migrations | grep "password:"
|
||||
```
|
||||
|
||||
Or create new admin:
|
||||
```bash
|
||||
docker compose exec web python manage.py createsuperuser
|
||||
```
|
||||
|
||||
### Videos not encoding?
|
||||
|
||||
Check celery workers:
|
||||
```bash
|
||||
docker compose logs celery_long
|
||||
docker compose logs celery_short
|
||||
```
|
||||
|
||||
### Port 80 already in use?
|
||||
|
||||
Edit docker-compose.yaml to use different port:
|
||||
```yaml
|
||||
nginx:
|
||||
ports:
|
||||
- "8080:80" # Use port 8080 instead
|
||||
```
|
||||
|
||||
Then access at http://localhost:8080
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues**: https://github.com/mediacms-io/mediacms/issues
|
||||
- **Discussions**: https://github.com/mediacms-io/mediacms/discussions
|
||||
- **Docs**: https://docs.mediacms.io
|
||||
|
||||
---
|
||||
|
||||
**🎉 Enjoy MediaCMS!**
|
||||
66
README.md
66
README.md
@@ -6,7 +6,7 @@
|
||||
|
||||
|
||||
|
||||
MediaCMS is a modern, fully featured open source video and media CMS. It is developed to meet the needs of modern web platforms for viewing and sharing media. It can be used to build a small to medium video and media portal within minutes.
|
||||
MediaCMS is a modern, fully featured open source video and media CMS. It is developed to meet the needs of modern web platforms for viewing and sharing media. It can be used to build a small to medium video and media portal within minutes.
|
||||
|
||||
It is built mostly using the modern stack Django + React and includes a REST API.
|
||||
|
||||
@@ -23,11 +23,15 @@ A demo is available at https://demo.mediacms.io
|
||||
|
||||
## Features
|
||||
- **Complete control over your data**: host it yourself!
|
||||
- **Support for multiple publishing workflows**: public, private, unlisted and custom
|
||||
- **Modern technologies**: Django/Python/Celery, React.
|
||||
- **Support for multiple publishing workflows**: public, private, unlisted and custom
|
||||
- **Role-Based Access Control (RBAC)**: create RBAC categories and connect users to groups with view/edit access on their media
|
||||
- **Automatic transcription**: through integration with Whisper running locally
|
||||
- **Multiple media types support**: video, audio, image, pdf
|
||||
- **Multiple media classification options**: categories, tags and custom
|
||||
- **Multiple media sharing options**: social media share, videos embed code generation
|
||||
- **Video Trimmer**: trim video, replace, save as new or create segments
|
||||
- **SAML support**: with ability to add mappings to system roles and groups
|
||||
- **Easy media searching**: enriched with live search functionality
|
||||
- **Playlists for audio and video content**: create playlists, add and reorder content
|
||||
- **Responsive design**: including light and dark themes
|
||||
@@ -35,83 +39,83 @@ A demo is available at https://demo.mediacms.io
|
||||
- **Configurable actions**: allow download, add comments, add likes, dislikes, report media
|
||||
- **Configuration options**: change logos, fonts, styling, add more pages
|
||||
- **Enhanced video player**: customized video.js player with multiple resolution and playback speed options
|
||||
- **Multiple transcoding profiles**: sane defaults for multiple dimensions (240p, 360p, 480p, 720p, 1080p) and multiple profiles (h264, h265, vp9)
|
||||
- **Multiple transcoding profiles**: sane defaults for multiple dimensions (144p, 240p, 360p, 480p, 720p, 1080p) and multiple profiles (h264, h265, vp9)
|
||||
- **Adaptive video streaming**: possible through HLS protocol
|
||||
- **Subtitles/CC**: support for multilingual subtitle files
|
||||
- **Scalable transcoding**: transcoding through priorities. Experimental support for remote workers
|
||||
- **Chunked file uploads**: for pausable/resumable upload of content
|
||||
- **REST API**: Documented through Swagger
|
||||
|
||||
- **Translation**: Most of the CMS is translated to a number of languages
|
||||
|
||||
## Example cases
|
||||
|
||||
- **Schools, education.** Administrators and editors keep what content will be published, students are not distracted with advertisements and irrelevant content, plus they have the ability to select either to stream or download content.
|
||||
|
||||
- **Universities, schools, education.** Administrators and editors keep what content will be published, students are not distracted with advertisements and irrelevant content, plus they have the ability to select either to stream or download content.
|
||||
- **Organization sensitive content.** In cases where content is sensitive and cannot be uploaded to external sites.
|
||||
|
||||
- **Build a great community.** MediaCMS can be customized (URLs, logos, fonts, aesthetics) so that you create a highly customized video portal for your community!
|
||||
|
||||
- **Personal portal.** Organize, categorize and host your content the way you prefer.
|
||||
|
||||
|
||||
## Philosophy
|
||||
|
||||
We believe there's a need for quality open source web applications that can be used to build community portals and support collaboration.
|
||||
|
||||
We have three goals for MediaCMS: a) deliver all functionality one would expect from a modern system, b) allow for easy installation and maintenance, c) allow easy customization and addition of features.
|
||||
We believe there's a need for quality open source web applications that can be used to build community portals and support collaboration.
|
||||
We have three goals for MediaCMS: a) deliver all functionality one would expect from a modern system, b) allow for easy installation and maintenance, c) allow easy customization and addition of features.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
MediaCMS is released under [GNU Affero General Public License v3.0 license](LICENSE.txt).
|
||||
Copyright Markos Gogoulos and Yiannis Stergiou
|
||||
MediaCMS is released under [GNU Affero General Public License v3.0 license](LICENSE.txt).
|
||||
Copyright Markos Gogoulos.
|
||||
|
||||
|
||||
## Support and paid services
|
||||
|
||||
We provide custom installations, development of extra functionality, migration from existing systems, integrations with legacy systems, training and support. Contact us at info@mediacms.io for more information.
|
||||
|
||||
### Commercial Hostings
|
||||
**Elestio**
|
||||
|
||||
You can deploy MediaCMS on Elestio using one-click deployment. Elestio supports MediaCMS by providing revenue share so go ahead and click below to deploy and use MediaCMS.
|
||||
|
||||
## Hardware dependencies
|
||||
[](https://elest.io/open-source/mediacms)
|
||||
|
||||
For a small to medium installation, with a few hours of video uploaded daily, and a few hundreds of active daily users viewing content, 4GB Ram / 2-4 CPUs as minimum is ok. For a larger installation with many hours of video uploaded daily, consider adding more CPUs and more Ram.
|
||||
## Hardware considerations
|
||||
|
||||
For a small to medium installation, with a few hours of video uploaded daily, and a few hundreds of active daily users viewing content, 4GB Ram / 2-4 CPUs as minimum is ok. For a larger installation with many hours of video uploaded daily, consider adding more CPUs and more Ram.
|
||||
|
||||
In terms of disk space, think of what the needs will be. A general rule is to multiply by three the size of the expected uploaded videos (since the system keeps original versions, encoded versions plus HLS), so if you receive 1G of videos daily and maintain all of them, you should consider a 1T disk across a year (1G * 3 * 365).
|
||||
|
||||
|
||||
## Releases
|
||||
|
||||
Visit [Releases Page](https://github.com/mediacms-io/mediacms/releases) for detailed Changelog
|
||||
|
||||
In order to support automatic transcriptions through Whisper, consider more CPUs.
|
||||
|
||||
## Installation / Maintanance
|
||||
|
||||
There are two ways to run MediaCMS, through Docker Compose and through installing it on a server via an automation script that installs and configures all needed services. Find the related pages:
|
||||
|
||||
* [Single Server](docs/admins_docs.md#2-server-installation) page
|
||||
* [Docker Compose](docs/admins_docs.md#3-docker-installation) page
|
||||
|
||||
## Configuration
|
||||
|
||||
Visit [Configuration](docs/admins_docs.md#5-configuration) page.
|
||||
- [Single Server](docs/admins_docs.md#2-server-installation) page
|
||||
- [Docker Compose](docs/admins_docs.md#3-docker-installation) page
|
||||
|
||||
A complete guide can be found on the blog post [How to self-host and share your videos in 2021](https://medium.com/@MediaCMS.io/how-to-self-host-and-share-your-videos-in-2021-14067e3b291b).
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Users documentation](docs/user_docs.md) page
|
||||
* [Administrators documentation](docs/admins_docs.md) page
|
||||
* [Developers documentation](docs/developers_docs.md) page
|
||||
* [Configuration](docs/admins_docs.md#5-configuration) page
|
||||
* [Transcoding](docs/transcoding.md) page
|
||||
* [Developer Experience](docs/dev_exp.md) page
|
||||
* [Media Permissions](docs/media_permissions.md) page
|
||||
|
||||
|
||||
## Technology
|
||||
|
||||
This software uses the following list of awesome technologies: Python, Django, Django Rest Framework, Celery, PostgreSQL, Redis, Nginx, uWSGI, React, Fine Uploader, video.js, FFMPEG, Bento4
|
||||
|
||||
|
||||
## Who is using it
|
||||
|
||||
- **Multiple Universities** for hosting educational videos
|
||||
- **Cinemata** non-profit media, technology and culture organization - https://cinemata.org
|
||||
- **Critical Commons** public media archive and fair use advocacy network - https://criticalcommons.org
|
||||
- **Heritales** International Heritage Film Festival - https://stage.heritales.org
|
||||
- **American Association of Gynecologic Laparoscopists** - https://surgeryu.org/
|
||||
|
||||
|
||||
## How to contribute
|
||||
@@ -121,10 +125,12 @@ If you like the project, here's a few things you can do
|
||||
- Suggest us to others that are interested to hire us
|
||||
- Write a blog post/article about MediaCMS
|
||||
- Share on social media about the project
|
||||
- Open issues, participate on discussions, report bugs, suggest ideas
|
||||
- Open issues, participate on [discussions](https://github.com/mediacms-io/mediacms/discussions), report bugs, suggest ideas
|
||||
- [Show and tell](https://github.com/mediacms-io/mediacms/discussions/categories/show-and-tell) how you are using the project
|
||||
- Star the project
|
||||
- Add functionality, work on a PR, fix an issue!
|
||||
- Add functionality, work on a PR, fix an issue!
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
info@mediacms.io
|
||||
|
||||
477
UPGRADE_TO_7.3.md
Normal file
477
UPGRADE_TO_7.3.md
Normal file
@@ -0,0 +1,477 @@
|
||||
# Upgrade Guide: MediaCMS 7.x to 7.3
|
||||
|
||||
**IMPORTANT: This is a major architectural change. Read this entire guide before upgrading.**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Fresh Install (Not Upgrading)?
|
||||
|
||||
If you're starting fresh with 7.3, you don't need this guide!
|
||||
|
||||
**All you need:**
|
||||
```bash
|
||||
# 1. Download docker-compose.yaml
|
||||
wget https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/docker-compose.yaml
|
||||
|
||||
# 2. Start (creates everything automatically)
|
||||
docker compose up -d
|
||||
|
||||
# 3. Done! Visit http://localhost
|
||||
```
|
||||
|
||||
**Optional: Add customizations**
|
||||
```bash
|
||||
# Create custom/ directory
|
||||
mkdir -p custom/static/{images,css}
|
||||
|
||||
# Download example settings
|
||||
wget -O custom/local_settings.py.example \
|
||||
https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/custom/local_settings.py.example
|
||||
|
||||
# Edit and use
|
||||
cp custom/local_settings.py.example custom/local_settings.py
|
||||
nano custom/local_settings.py
|
||||
|
||||
# Restart
|
||||
docker compose restart web
|
||||
```
|
||||
|
||||
See [`custom/README.md`](https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/custom/README.md) for customization options.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Upgrading from 7.x? Continue reading...
|
||||
|
||||
## What Changed in 7.3
|
||||
|
||||
### Architecture Changes
|
||||
- **Before**: Monolithic container (supervisor + nginx + uwsgi + celery in one)
|
||||
- **After**: Microservices (separate nginx, web, celery_beat, celery_short, celery_long containers)
|
||||
|
||||
### Volume Strategy Changes
|
||||
- **Before**: Entire project directory mounted (`./:/home/mediacms.io/mediacms/`)
|
||||
- **After**: Named volumes for data, bind mount only for `custom/` directory
|
||||
|
||||
### Specific Changes
|
||||
|
||||
| Component | Before (7.x) | After (7.3) |
|
||||
|-----------|-------------|-------------|
|
||||
| media_files | Bind mount `./media_files` | Named volume `media_files` |
|
||||
| static files | Bind mount `./static` | Named volume `static_files` (built into image) |
|
||||
| logs | Bind mount `./logs` | Named volume `logs` |
|
||||
| postgres_data | `../postgres_data` | Named volume `postgres_data` |
|
||||
| Custom config | `cms/local_settings.py` in mounted dir | `custom/local_settings.py` bind mount |
|
||||
| Static collection | Runtime (via entrypoint) | Build time (in Dockerfile) |
|
||||
| User | Root with gosu switch | www-data from start |
|
||||
|
||||
## What You Need for 7.3
|
||||
|
||||
**Minimal deployment - NO CODE REQUIRED:**
|
||||
|
||||
1. ✅ `docker-compose.yaml` (download from release or docs)
|
||||
2. ✅ Docker images (pulled from Docker Hub)
|
||||
3. ⚠️ `custom/` directory (only if you have customizations)
|
||||
|
||||
**That's it!** No git repo, no code checkout needed.
|
||||
|
||||
## Pre-Upgrade Checklist
|
||||
|
||||
### 1. Backup Everything
|
||||
|
||||
```bash
|
||||
# Stop services
|
||||
docker compose down
|
||||
|
||||
# Backup media files
|
||||
tar -czf backup_media_$(date +%Y%m%d).tar.gz media_files/
|
||||
|
||||
# Backup database
|
||||
docker compose up -d db
|
||||
docker compose exec db pg_dump -U mediacms mediacms > backup_db_$(date +%Y%m%d).sql
|
||||
docker compose down
|
||||
|
||||
# Backup logs (optional)
|
||||
tar -czf backup_logs_$(date +%Y%m%d).tar.gz logs/
|
||||
|
||||
# Backup local settings if you had them
|
||||
cp cms/local_settings.py backup_local_settings.py 2>/dev/null || echo "No local_settings.py found"
|
||||
|
||||
# Backup current docker-compose.yaml
|
||||
cp docker-compose.yaml docker-compose.yaml.old
|
||||
```
|
||||
|
||||
### 2. Document Current Setup
|
||||
|
||||
```bash
|
||||
# Save current docker-compose version
|
||||
git branch backup-pre-7.3-upgrade
|
||||
|
||||
# Document current state
|
||||
docker compose ps > pre_upgrade_state.txt
|
||||
docker compose config > pre_upgrade_config.yaml
|
||||
df -h > pre_upgrade_disk_usage.txt
|
||||
```
|
||||
|
||||
### 3. Check Disk Space
|
||||
|
||||
You'll need enough space for:
|
||||
- Existing data (media_files, postgres_data)
|
||||
- New Docker volumes (will copy data here)
|
||||
- Database dump
|
||||
|
||||
```bash
|
||||
du -sh media_files/ postgres_data/ logs/
|
||||
df -h .
|
||||
```
|
||||
|
||||
## Upgrade Methods
|
||||
|
||||
### Method 1: Clean Migration (Recommended)
|
||||
|
||||
This method migrates your data to the new volume structure.
|
||||
|
||||
#### Step 1: Get New docker-compose.yaml
|
||||
|
||||
**Option A: Download from release**
|
||||
```bash
|
||||
# Download docker-compose.yaml for 7.3
|
||||
wget https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/docker-compose.yaml
|
||||
|
||||
# Or using curl
|
||||
curl -O https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/docker-compose.yaml
|
||||
|
||||
# Optional: Download HTTPS version
|
||||
wget https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/docker-compose-cert.yaml
|
||||
```
|
||||
|
||||
**Option B: Copy from docs/release notes**
|
||||
- Copy the docker-compose.yaml content from release notes
|
||||
- Save as `docker-compose.yaml` in your deployment directory
|
||||
|
||||
#### Step 2: Prepare Custom Configuration (if needed)
|
||||
|
||||
```bash
|
||||
# Create custom directory structure (only if you need customizations)
|
||||
mkdir -p custom/static/{images,css}
|
||||
touch custom/static/{images,css}/.gitkeep
|
||||
|
||||
# If you had local_settings.py, create it in custom/
|
||||
if [ -f backup_local_settings.py ]; then
|
||||
# Copy your old settings
|
||||
cp backup_local_settings.py custom/local_settings.py
|
||||
echo "✓ Migrated local_settings.py"
|
||||
else
|
||||
# Download example template (optional)
|
||||
wget -O custom/local_settings.py.example \
|
||||
https://raw.githubusercontent.com/mediacms-io/mediacms/v7.3/custom/local_settings.py.example
|
||||
echo "Downloaded example template to custom/local_settings.py.example"
|
||||
fi
|
||||
|
||||
# Copy any custom logos/css you had
|
||||
# (adjust paths as needed for your old setup)
|
||||
# cp my-old-logo.png custom/static/images/logo_dark.png
|
||||
# cp my-custom.css custom/static/css/custom.css
|
||||
```
|
||||
|
||||
#### Step 3: Start New Stack (Without Data)
|
||||
|
||||
```bash
|
||||
# Pull new images
|
||||
docker compose pull
|
||||
|
||||
# Start database first
|
||||
docker compose up -d db redis
|
||||
|
||||
# Wait for DB to be ready
|
||||
sleep 10
|
||||
```
|
||||
|
||||
#### Step 4: Restore Database
|
||||
|
||||
```bash
|
||||
# Copy backup into container
|
||||
docker compose cp backup_db_*.sql db:/tmp/backup.sql
|
||||
|
||||
# Restore database
|
||||
docker compose exec db psql -U mediacms mediacms < /tmp/backup.sql
|
||||
|
||||
# Or from host:
|
||||
cat backup_db_*.sql | docker compose exec -T db psql -U mediacms mediacms
|
||||
```
|
||||
|
||||
#### Step 5: Restore Media Files
|
||||
|
||||
```bash
|
||||
# Start all services (will create volumes)
|
||||
docker compose up -d
|
||||
|
||||
# Find the volume name
|
||||
docker volume ls | grep media_files
|
||||
|
||||
# Copy media files to volume
|
||||
# Method A: Using a temporary container
|
||||
docker run --rm \
|
||||
-v $(pwd)/media_files:/source:ro \
|
||||
-v mediacms_media_files:/dest \
|
||||
alpine sh -c "cp -av /source/* /dest/"
|
||||
|
||||
# Method B: Using existing container
|
||||
docker compose exec web sh -c "exit" # Ensure web is running
|
||||
# Then copy from host
|
||||
tar -C media_files -cf - . | docker compose exec -T web tar -C /home/mediacms.io/mediacms/media_files -xf -
|
||||
```
|
||||
|
||||
#### Step 6: Verify and Test
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
docker compose logs -f web
|
||||
|
||||
# Verify media files are accessible
|
||||
docker compose exec web ls -la /home/mediacms.io/mediacms/media_files/
|
||||
|
||||
# Check database connection
|
||||
docker compose exec web python manage.py dbshell
|
||||
|
||||
# Access the site
|
||||
curl http://localhost
|
||||
|
||||
# Check admin panel
|
||||
# Visit http://localhost/admin
|
||||
```
|
||||
|
||||
### Method 2: In-Place Migration with Symlinks (Advanced)
|
||||
|
||||
**Warning**: This is more complex but avoids data copying.
|
||||
|
||||
#### Step 1: Keep Old Data Locations
|
||||
|
||||
```bash
|
||||
# Modify docker-compose.yaml to mount old locations temporarily
|
||||
# Add to appropriate services:
|
||||
volumes:
|
||||
- ./media_files:/home/mediacms.io/mediacms/media_files
|
||||
- ./logs:/home/mediacms.io/mediacms/logs
|
||||
# Instead of named volumes
|
||||
```
|
||||
|
||||
#### Step 2: Gradually Migrate
|
||||
|
||||
After confirming everything works:
|
||||
1. Copy data to named volumes
|
||||
2. Remove bind mounts
|
||||
3. Switch to named volumes
|
||||
|
||||
### Method 3: Fresh Install (If Possible)
|
||||
|
||||
If your MediaCMS instance is new or test:
|
||||
|
||||
```bash
|
||||
# Backup what you need
|
||||
# ...
|
||||
|
||||
# Clean slate
|
||||
docker compose down -v
|
||||
rm -rf media_files/ logs/ static/
|
||||
|
||||
# Fresh start
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Post-Upgrade Steps
|
||||
|
||||
### 1. Verify Everything Works
|
||||
|
||||
```bash
|
||||
# Check all services are running
|
||||
docker compose ps
|
||||
|
||||
# Should see: migrations (exited 0), web, nginx, celery_beat, celery_short, celery_long, db, redis
|
||||
|
||||
# Check logs for errors
|
||||
docker compose logs web
|
||||
docker compose logs nginx
|
||||
|
||||
# Test upload functionality
|
||||
# Test video encoding (check celery_long logs)
|
||||
# Test frontend
|
||||
```
|
||||
|
||||
### 2. Verify Media Files
|
||||
|
||||
```bash
|
||||
# Check media files are accessible
|
||||
docker compose exec web ls -lh /home/mediacms.io/mediacms/media_files/
|
||||
|
||||
# Check file counts match
|
||||
# Old: ls media_files/ | wc -l
|
||||
# New: docker compose exec web sh -c "ls /home/mediacms.io/mediacms/media_files/ | wc -l"
|
||||
```
|
||||
|
||||
### 3. Verify Database
|
||||
|
||||
```bash
|
||||
# Check users
|
||||
docker compose exec db psql -U mediacms mediacms -c "SELECT count(*) FROM users_user;"
|
||||
|
||||
# Check videos
|
||||
docker compose exec db psql -U mediacms mediacms -c "SELECT count(*) FROM files_media;"
|
||||
```
|
||||
|
||||
### 4. Update Backups
|
||||
|
||||
```bash
|
||||
# Update your backup scripts for new volume locations
|
||||
# Use: make backup-db (if Makefile target exists)
|
||||
# Or: docker compose exec db pg_dump ...
|
||||
```
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
If something goes wrong:
|
||||
|
||||
### Quick Rollback
|
||||
|
||||
```bash
|
||||
# Stop new version
|
||||
docker compose down
|
||||
|
||||
# Restore old docker-compose file
|
||||
mv docker-compose.yaml.old docker-compose.yaml
|
||||
|
||||
# Pull old images (if you had old image tags documented)
|
||||
docker compose pull
|
||||
|
||||
# Start old version
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Full Rollback with Data Restore
|
||||
|
||||
```bash
|
||||
# Stop everything
|
||||
docker compose down -v
|
||||
|
||||
# Restore old docker-compose
|
||||
mv docker-compose.yaml.old docker-compose.yaml
|
||||
|
||||
# Restore backups
|
||||
tar -xzf backup_media_*.tar.gz -C ./media_files
|
||||
cat backup_db_*.sql | docker compose exec -T db psql -U mediacms mediacms
|
||||
|
||||
# Start old version
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Issue: "Volume not found"
|
||||
|
||||
**Solution**: Volumes are created with project name prefix. Check:
|
||||
```bash
|
||||
docker volume ls
|
||||
# Look for: mediacms_media_files, mediacms_static_files, etc.
|
||||
```
|
||||
|
||||
### Issue: "Permission denied" on media files
|
||||
|
||||
**Solution**: Files must be owned by www-data (UID 33)
|
||||
```bash
|
||||
docker compose exec web chown -R www-data:www-data /home/mediacms.io/mediacms/media_files
|
||||
```
|
||||
|
||||
### Issue: Static files not loading
|
||||
|
||||
**Solution**: Rebuild image (collectstatic runs at build time)
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose build --no-cache web
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Issue: Database connection refused
|
||||
|
||||
**Solution**: Check database is healthy
|
||||
```bash
|
||||
docker compose logs db
|
||||
docker compose exec db pg_isready -U mediacms
|
||||
```
|
||||
|
||||
### Issue: Custom settings not loading
|
||||
|
||||
**Solution**: Check custom/local_settings.py exists and syntax
|
||||
```bash
|
||||
docker compose exec web cat /home/mediacms.io/mediacms/custom/local_settings.py
|
||||
docker compose exec web python -m py_compile /home/mediacms.io/mediacms/custom/local_settings.py
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### New Volume Performance
|
||||
|
||||
Named volumes are typically faster than bind mounts:
|
||||
- **Before**: Filesystem overhead on host
|
||||
- **After**: Direct container filesystem (better I/O)
|
||||
|
||||
### Monitoring Volume Usage
|
||||
|
||||
```bash
|
||||
# Check volume sizes
|
||||
docker system df -v
|
||||
|
||||
# Check specific volume
|
||||
docker volume inspect mediacms_media_files
|
||||
```
|
||||
|
||||
## New Backup Strategy
|
||||
|
||||
With named volumes, backups change:
|
||||
|
||||
```bash
|
||||
# Database backup
|
||||
docker compose exec db pg_dump -U mediacms mediacms > backup.sql
|
||||
|
||||
# Media files backup
|
||||
docker run --rm \
|
||||
-v mediacms_media_files:/data:ro \
|
||||
-v $(pwd):/backup \
|
||||
alpine tar czf /backup/media_backup_$(date +%Y%m%d).tar.gz -C /data .
|
||||
```
|
||||
|
||||
Or use the Makefile:
|
||||
```bash
|
||||
make backup-db
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. **Check logs**: `docker compose logs <service>`
|
||||
2. **Check GitHub Issues**: Search for similar problems
|
||||
3. **Rollback**: Use the rollback procedure above
|
||||
4. **Report**: Open an issue with:
|
||||
- Your docker-compose.yaml
|
||||
- Output of `docker compose ps`
|
||||
- Relevant logs
|
||||
- Steps to reproduce
|
||||
|
||||
## Summary of Benefits
|
||||
|
||||
After upgrading to 7.3:
|
||||
|
||||
✅ **Better separation of concerns** - each service has one job
|
||||
✅ **Easier scaling** - scale web/workers independently
|
||||
✅ **Better security** - containers run as www-data, not root
|
||||
✅ **Faster deployments** - static files built into image
|
||||
✅ **Cleaner customization** - dedicated custom/ directory
|
||||
✅ **Easier SSL setup** - docker-compose-cert.yaml overlay
|
||||
✅ **Better volume management** - named volumes instead of bind mounts
|
||||
|
||||
## Timeline Recommendation
|
||||
|
||||
- **Small instance** (<100 videos): 30-60 minutes
|
||||
- **Medium instance** (100-1000 videos): 1-3 hours
|
||||
- **Large instance** (>1000 videos): Plan for several hours
|
||||
|
||||
Schedule during low-traffic period!
|
||||
0
admin_customizations/__init__.py
Normal file
0
admin_customizations/__init__.py
Normal file
0
admin_customizations/admin.py
Normal file
0
admin_customizations/admin.py
Normal file
86
admin_customizations/apps.py
Normal file
86
admin_customizations/apps.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
class AdminCustomizationsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'admin_customizations'
|
||||
|
||||
def ready(self):
|
||||
original_get_app_list = admin.AdminSite.get_app_list
|
||||
|
||||
def get_app_list(self, request, app_label=None):
|
||||
"""Custom get_app_list"""
|
||||
app_list = original_get_app_list(self, request, app_label)
|
||||
# To see the list:
|
||||
# print([a.get('app_label') for a in app_list])
|
||||
|
||||
email_model = None
|
||||
rbac_group_model = None
|
||||
identity_providers_user_log_model = None
|
||||
identity_providers_login_option = None
|
||||
auth_app = None
|
||||
rbac_app = None
|
||||
socialaccount_app = None
|
||||
|
||||
for app in app_list:
|
||||
if app['app_label'] == 'users':
|
||||
auth_app = app
|
||||
|
||||
elif app['app_label'] == 'account':
|
||||
for model in app['models']:
|
||||
if model['object_name'] == 'EmailAddress':
|
||||
email_model = model
|
||||
elif app['app_label'] == 'rbac':
|
||||
if not getattr(settings, 'USE_RBAC', False):
|
||||
continue
|
||||
rbac_app = app
|
||||
for model in app['models']:
|
||||
if model['object_name'] == 'RBACGroup':
|
||||
rbac_group_model = model
|
||||
elif app['app_label'] == 'identity_providers':
|
||||
if not getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
|
||||
continue
|
||||
|
||||
models_to_check = list(app['models'])
|
||||
|
||||
for model in models_to_check:
|
||||
if model['object_name'] == 'IdentityProviderUserLog':
|
||||
identity_providers_user_log_model = model
|
||||
if model['object_name'] == 'LoginOption':
|
||||
identity_providers_login_option = model
|
||||
elif app['app_label'] == 'socialaccount':
|
||||
socialaccount_app = app
|
||||
|
||||
if email_model and auth_app:
|
||||
auth_app['models'].append(email_model)
|
||||
if rbac_group_model and rbac_app and auth_app:
|
||||
auth_app['models'].append(rbac_group_model)
|
||||
if identity_providers_login_option and socialaccount_app:
|
||||
socialaccount_app['models'].append(identity_providers_login_option)
|
||||
if identity_providers_user_log_model and socialaccount_app:
|
||||
socialaccount_app['models'].append(identity_providers_user_log_model)
|
||||
|
||||
# 2. don't include the following apps
|
||||
apps_to_hide = ['authtoken', 'auth', 'account', 'saml_auth', 'rbac']
|
||||
if not getattr(settings, 'USE_RBAC', False):
|
||||
apps_to_hide.append('rbac')
|
||||
if not getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
|
||||
apps_to_hide.append('socialaccount')
|
||||
|
||||
app_list = [app for app in app_list if app['app_label'] not in apps_to_hide]
|
||||
|
||||
# 3. change the ordering
|
||||
app_order = {
|
||||
'files': 1,
|
||||
'users': 2,
|
||||
'socialaccount': 3,
|
||||
'rbac': 5,
|
||||
}
|
||||
|
||||
app_list.sort(key=lambda x: app_order.get(x['app_label'], 999))
|
||||
|
||||
return app_list
|
||||
|
||||
admin.AdminSite.get_app_list = get_app_list
|
||||
0
admin_customizations/migrations/__init__.py
Normal file
0
admin_customizations/migrations/__init__.py
Normal file
0
admin_customizations/models.py
Normal file
0
admin_customizations/models.py
Normal file
0
admin_customizations/tests.py
Normal file
0
admin_customizations/tests.py
Normal file
0
admin_customizations/views.py
Normal file
0
admin_customizations/views.py
Normal file
@@ -59,7 +59,7 @@ def login():
|
||||
file.writelines(f'USERNAME={json.loads(response.text)["username"]}\n')
|
||||
print(f"Welcome to MediaCMS [bold blue]{username}[/bold blue]. Your auth creds have been suceesfully stored in the .env file", ":v:")
|
||||
else:
|
||||
print(f'Error: {"non_field_errors":["User not found."]}')
|
||||
print(f'Error: {"non_field_errors": ["User not found."]}')
|
||||
|
||||
|
||||
@apis.command()
|
||||
@@ -73,7 +73,7 @@ def upload_media():
|
||||
if os.path.isdir(path):
|
||||
for filename in os.listdir(path):
|
||||
files = {}
|
||||
abs = os.path.abspath("{path}/{filename}")
|
||||
abs = os.path.abspath(f"{path}/{filename}")
|
||||
files['media_file'] = open(f'{abs}', 'rb')
|
||||
response = requests.post(url=f'{BASE_URL}/media', headers=headers, files=files)
|
||||
if response.status_code == 201:
|
||||
|
||||
10
cms/auth_backends.py
Normal file
10
cms/auth_backends.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
|
||||
class ApprovalBackend(ModelBackend):
|
||||
def user_can_authenticate(self, user):
|
||||
can_authenticate = super().user_can_authenticate(user)
|
||||
if can_authenticate and settings.USERS_NEEDS_TO_BE_APPROVED and not user.is_superuser:
|
||||
return getattr(user, 'is_approved', False)
|
||||
return can_authenticate
|
||||
57
cms/dev_settings.py
Normal file
57
cms/dev_settings.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# Development settings, used in docker-compose-dev.yaml
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"admin_customizations",
|
||||
"django.contrib.auth",
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"jazzmin",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.sites",
|
||||
"rest_framework",
|
||||
"rest_framework.authtoken",
|
||||
"imagekit",
|
||||
"files.apps.FilesConfig",
|
||||
"users.apps.UsersConfig",
|
||||
"actions.apps.ActionsConfig",
|
||||
"rbac.apps.RbacConfig",
|
||||
"identity_providers.apps.IdentityProvidersConfig",
|
||||
"debug_toolbar",
|
||||
"mptt",
|
||||
"crispy_forms",
|
||||
"crispy_bootstrap5",
|
||||
"uploader.apps.UploaderConfig",
|
||||
"djcelery_email",
|
||||
"drf_yasg",
|
||||
"allauth.socialaccount.providers.saml",
|
||||
"saml_auth.apps.SamlAuthConfig",
|
||||
"corsheaders",
|
||||
"tinymce",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
"django.middleware.locale.LocaleMiddleware",
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
"allauth.account.middleware.AccountMiddleware",
|
||||
]
|
||||
|
||||
DEBUG = True
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static_collected')
|
||||
23
cms/middleware.py
Normal file
23
cms/middleware.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.conf import settings
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class ApprovalMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if settings.USERS_NEEDS_TO_BE_APPROVED and request.user.is_authenticated and not request.user.is_superuser and not getattr(request.user, 'is_approved', False):
|
||||
allowed_paths = [
|
||||
reverse('approval_required'),
|
||||
reverse('account_logout'),
|
||||
]
|
||||
if request.path not in allowed_paths:
|
||||
if request.path.startswith('/api/'):
|
||||
return JsonResponse({'detail': 'User account not approved.'}, status=403)
|
||||
return redirect('approval_required')
|
||||
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
@@ -1,14 +1,29 @@
|
||||
from django.conf import settings
|
||||
from rest_framework import permissions
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from files.methods import is_mediacms_editor, is_mediacms_manager
|
||||
from files.methods import (
|
||||
is_mediacms_editor,
|
||||
is_mediacms_manager,
|
||||
user_allowed_to_upload,
|
||||
)
|
||||
|
||||
|
||||
class IsAuthorizedToAdd(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
return user_allowed_to_upload(request)
|
||||
if not user_allowed_to_upload(request):
|
||||
raise PermissionDenied("You don't have permission to upload media, or have reached max number of media uploads.")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class IsAuthorizedToAddComment(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
return user_allowed_to_comment(request)
|
||||
|
||||
|
||||
class IsUserOrManager(permissions.BasePermission):
|
||||
@@ -48,21 +63,22 @@ class IsUserOrEditor(permissions.BasePermission):
|
||||
return obj.user == request.user
|
||||
|
||||
|
||||
def user_allowed_to_upload(request):
|
||||
def user_allowed_to_comment(request):
|
||||
"""Any custom logic for whether a user is allowed
|
||||
to upload content lives here
|
||||
to comment lives here
|
||||
"""
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
if request.user.is_superuser:
|
||||
return True
|
||||
|
||||
if settings.CAN_ADD_MEDIA == "all":
|
||||
# Default is "all"
|
||||
if not hasattr(settings, "CAN_COMMENT") or settings.CAN_COMMENT == "all":
|
||||
return True
|
||||
elif settings.CAN_ADD_MEDIA == "email_verified":
|
||||
elif settings.CAN_COMMENT == "email_verified":
|
||||
if request.user.email_is_verified:
|
||||
return True
|
||||
elif settings.CAN_ADD_MEDIA == "advancedUser":
|
||||
elif settings.CAN_COMMENT == "advancedUser":
|
||||
if request.user.advancedUser:
|
||||
return True
|
||||
return False
|
||||
|
||||
341
cms/settings.py
341
cms/settings.py
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
|
||||
from celery.schedules import crontab
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
DEBUG = False
|
||||
|
||||
@@ -8,13 +9,16 @@ DEBUG = False
|
||||
# is also shown on several places as emails
|
||||
PORTAL_NAME = "MediaCMS"
|
||||
PORTAL_DESCRIPTION = ""
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = "Europe/London"
|
||||
|
||||
# who can add media
|
||||
# valid options include 'all', 'email_verified', 'advancedUser'
|
||||
CAN_ADD_MEDIA = "all"
|
||||
|
||||
# who can comment
|
||||
# valid options include 'all', 'email_verified', 'advancedUser'
|
||||
CAN_COMMENT = "all"
|
||||
|
||||
# valid choices here are 'public', 'private', 'unlisted
|
||||
PORTAL_WORKFLOW = "public"
|
||||
|
||||
@@ -93,22 +97,49 @@ ALLOW_MENTION_IN_COMMENTS = False # allowing to mention other users with @ in t
|
||||
# valid options: content, author
|
||||
RELATED_MEDIA_STRATEGY = "content"
|
||||
|
||||
# Whether or not to generate a sitemap.xml listing the pages on the site (default: False)
|
||||
GENERATE_SITEMAP = False
|
||||
|
||||
# Whether to include media count numbers on categories and tags listing pages
|
||||
INCLUDE_LISTING_NUMBERS = True
|
||||
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
SITE_ID = 1
|
||||
|
||||
# these are the portal logos (dark and light)
|
||||
# set new paths for svg or png if you want to override
|
||||
# svg has priority over png, so if you want to use
|
||||
# custom pngs and not svgs, remove the lines with svgs
|
||||
# Logo paths (served from /static/)
|
||||
# Default logos are built into the image
|
||||
# To customize: place files in custom/static/images/ and reference as /custom/static/images/file.png
|
||||
# or set as empty strings to disable
|
||||
# example:
|
||||
# PORTAL_LOGO_DARK_PNG = "/custom/static/images/my-logo.png"
|
||||
# PORTAL_LOGO_DARK_SVG = ""
|
||||
PORTAL_LOGO_DARK_SVG = "/static/images/logo_dark.svg"
|
||||
PORTAL_LOGO_DARK_PNG = "/static/images/logo_dark.png"
|
||||
PORTAL_LOGO_LIGHT_SVG = "/static/images/logo_light.svg"
|
||||
PORTAL_LOGO_LIGHT_PNG = "/static/images/logo_dark.png"
|
||||
|
||||
# Extra CSS files to include in templates
|
||||
# To add custom CSS: place files in custom/static/css/ and add paths here
|
||||
# Use /custom/static/ prefix for files in custom/ directory
|
||||
# Example: EXTRA_CSS_PATHS = ["/custom/static/css/custom.css"]
|
||||
EXTRA_CSS_PATHS = []
|
||||
# protection agains anonymous users
|
||||
# per ip address limit, for actions as like/dislike/report
|
||||
TIME_TO_ACTION_ANONYMOUS = 10 * 60
|
||||
|
||||
# django-allauth settings
|
||||
ACCOUNT_SESSION_REMEMBER = True
|
||||
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
|
||||
ACCOUNT_LOGIN_METHODS = {"username", "email"}
|
||||
ACCOUNT_EMAIL_REQUIRED = True # new users need to specify email
|
||||
ACCOUNT_EMAIL_VERIFICATION = "optional" # 'mandatory' 'none'
|
||||
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||
ACCOUNT_USERNAME_MIN_LENGTH = "4"
|
||||
ACCOUNT_USERNAME_MIN_LENGTH = 4
|
||||
ACCOUNT_ADAPTER = "users.adapter.MyAccountAdapter"
|
||||
ACCOUNT_SIGNUP_FORM_CLASS = "users.forms.SignupForm"
|
||||
ACCOUNT_USERNAME_VALIDATORS = "users.validators.custom_username_validators"
|
||||
@@ -116,13 +147,19 @@ ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
|
||||
ACCOUNT_USERNAME_REQUIRED = True
|
||||
ACCOUNT_LOGIN_ON_PASSWORD_RESET = True
|
||||
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 1
|
||||
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 20
|
||||
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 5
|
||||
# registration won't be open, might also consider to remove links for register
|
||||
USERS_CAN_SELF_REGISTER = True
|
||||
|
||||
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = ["xxx.com", "emaildomainwhatever.com"]
|
||||
|
||||
# by default users do not need to be approved. If this is set to True, then new users
|
||||
# will have to be approved before they can login successfully
|
||||
USERS_NEEDS_TO_BE_APPROVED = False
|
||||
|
||||
# Comma separated list of domains: ["organization.com", "private.organization.com", "org2.com"]
|
||||
# Empty list disables.
|
||||
ALLOWED_DOMAINS_FOR_USER_REGISTRATION = []
|
||||
|
||||
# django rest settings
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
@@ -146,6 +183,10 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
STATIC_URL = "/static/" # where js/css files are stored on the filesystem
|
||||
MEDIA_URL = "/media/" # URL where static files are served from the server
|
||||
STATIC_ROOT = BASE_DIR + "/static/"
|
||||
# Additional locations for static files
|
||||
# Note: custom/static is NOT included here because it's served directly by nginx
|
||||
# at /custom/static/ and doesn't need collectstatic
|
||||
STATICFILES_DIRS = []
|
||||
# where uploaded + encoded media are stored
|
||||
MEDIA_ROOT = BASE_DIR + "/media_files/"
|
||||
|
||||
@@ -177,7 +218,7 @@ CHUNKIZE_VIDEO_DURATION = 60 * 5
|
||||
VIDEO_CHUNKS_DURATION = 60 * 4
|
||||
|
||||
# always get these two, even if upscaling
|
||||
MINIMUM_RESOLUTIONS_TO_ENCODE = [240, 360]
|
||||
MINIMUM_RESOLUTIONS_TO_ENCODE = [144, 240]
|
||||
|
||||
# default settings for notifications
|
||||
# not all of them are implemented
|
||||
@@ -217,13 +258,13 @@ POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY = ""
|
||||
# only in case where unlisted workflow is used and no commentary
|
||||
# exists
|
||||
|
||||
CANNOT_ADD_MEDIA_MESSAGE = ""
|
||||
CANNOT_ADD_MEDIA_MESSAGE = "User cannot add media, or maximum number of media uploads has been reached."
|
||||
|
||||
# mp4hls command, part of Bendo4
|
||||
MP4HLS_COMMAND = "/home/mediacms.io/mediacms/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/bin/mp4hls"
|
||||
# mp4hls command, part of Bento4
|
||||
MP4HLS_COMMAND = "/home/mediacms.io/bento4/bin/mp4hls"
|
||||
|
||||
# highly experimental, related with remote workers
|
||||
ADMIN_TOKEN = "c2b8e1838b6128asd333ddc5e24"
|
||||
ADMIN_TOKEN = ""
|
||||
# this is used by remote workers to push
|
||||
# encodings once they are done
|
||||
# USE_BASIC_HTTP = True
|
||||
@@ -238,35 +279,6 @@ ADMIN_TOKEN = "c2b8e1838b6128asd333ddc5e24"
|
||||
# uncomment the two lines related to htpasswd
|
||||
|
||||
|
||||
CKEDITOR_CONFIGS = {
|
||||
"default": {
|
||||
"toolbar": "Custom",
|
||||
"width": "100%",
|
||||
"toolbar_Custom": [
|
||||
["Styles"],
|
||||
["Format"],
|
||||
["Bold", "Italic", "Underline"],
|
||||
["HorizontalRule"],
|
||||
[
|
||||
"NumberedList",
|
||||
"BulletedList",
|
||||
"-",
|
||||
"Outdent",
|
||||
"Indent",
|
||||
"-",
|
||||
"JustifyLeft",
|
||||
"JustifyCenter",
|
||||
"JustifyRight",
|
||||
"JustifyBlock",
|
||||
],
|
||||
["Link", "Unlink"],
|
||||
["Image"],
|
||||
["RemoveFormat", "Source"],
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AUTH_USER_MODEL = "users.User"
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
|
||||
@@ -276,7 +288,7 @@ AUTHENTICATION_BACKENDS = (
|
||||
)
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"admin_customizations",
|
||||
"django.contrib.auth",
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
@@ -285,6 +297,8 @@ INSTALLED_APPS = [
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"jazzmin",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.sites",
|
||||
"rest_framework",
|
||||
"rest_framework.authtoken",
|
||||
@@ -292,24 +306,31 @@ INSTALLED_APPS = [
|
||||
"files.apps.FilesConfig",
|
||||
"users.apps.UsersConfig",
|
||||
"actions.apps.ActionsConfig",
|
||||
"rbac.apps.RbacConfig",
|
||||
"identity_providers.apps.IdentityProvidersConfig",
|
||||
"debug_toolbar",
|
||||
"mptt",
|
||||
"crispy_forms",
|
||||
"crispy_bootstrap5",
|
||||
"uploader.apps.UploaderConfig",
|
||||
"djcelery_email",
|
||||
"ckeditor",
|
||||
"drf_yasg",
|
||||
"allauth.socialaccount.providers.saml",
|
||||
"saml_auth.apps.SamlAuthConfig",
|
||||
"tinymce",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.locale.LocaleMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
"allauth.account.middleware.AccountMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "cms.urls"
|
||||
@@ -337,11 +358,15 @@ WSGI_APPLICATION = "cms.wsgi.application"
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
"OPTIONS": {
|
||||
"user_attributes": ("username", "email", "first_name", "last_name"),
|
||||
"max_similarity": 0.7,
|
||||
},
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
"OPTIONS": {
|
||||
"min_length": 5,
|
||||
"min_length": 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -353,50 +378,30 @@ FILE_UPLOAD_HANDLERS = [
|
||||
"django.core.files.uploadhandler.TemporaryFileUploadHandler",
|
||||
]
|
||||
|
||||
LOGS_DIR = os.path.join(BASE_DIR, "logs")
|
||||
|
||||
error_filename = os.path.join(LOGS_DIR, "debug.log")
|
||||
if not os.path.exists(LOGS_DIR):
|
||||
try:
|
||||
os.mkdir(LOGS_DIR)
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
if not os.path.isfile(error_filename):
|
||||
open(error_filename, 'a').close()
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "%(levelname)s %(asctime)s %(module)s "
|
||||
"%(process)d %(thread)d %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"file": {
|
||||
"level": "ERROR",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": error_filename,
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django": {
|
||||
"handlers": ["file"],
|
||||
"level": "ERROR",
|
||||
"propagate": True,
|
||||
},
|
||||
"console": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
}
|
||||
},
|
||||
"root": {"level": "INFO", "handlers": ["console"]},
|
||||
}
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": "mediacms",
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": "5432",
|
||||
"USER": "mediacms",
|
||||
"PASSWORD": "mediacms",
|
||||
}
|
||||
}
|
||||
DATABASES = {"default": {"ENGINE": "django.db.backends.postgresql", "NAME": "mediacms", "HOST": "db", "PORT": "5432", "USER": "mediacms", "PASSWORD": "mediacms", "OPTIONS": {'pool': True}}}
|
||||
|
||||
|
||||
REDIS_LOCATION = "redis://127.0.0.1:6379/1"
|
||||
REDIS_LOCATION = "redis://redis:6379/1"
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
@@ -453,33 +458,193 @@ CELERY_TASK_ALWAYS_EAGER = False
|
||||
if os.environ.get("TESTING"):
|
||||
CELERY_TASK_ALWAYS_EAGER = True
|
||||
|
||||
# if True, only show original, don't perform any action on videos
|
||||
DO_NOT_TRANSCODE_VIDEO = False
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
LANGUAGES = [
|
||||
('ar', _('Arabic')),
|
||||
('bn', _('Bengali')),
|
||||
('da', _('Danish')),
|
||||
('nl', _('Dutch')),
|
||||
('en', _('English')),
|
||||
('fr', _('French')),
|
||||
('de', _('German')),
|
||||
('hi', _('Hindi')),
|
||||
('id', _('Indonesian')),
|
||||
('it', _('Italian')),
|
||||
('ja', _('Japanese')),
|
||||
('ko', _('Korean')),
|
||||
('pt', _('Portuguese')),
|
||||
('ru', _('Russian')),
|
||||
('zh-hans', _('Simplified Chinese')),
|
||||
('sl', _('Slovenian')),
|
||||
('zh-hant', _('Traditional Chinese')),
|
||||
('es', _('Spanish')),
|
||||
('tr', _('Turkish')),
|
||||
('el', _('Greek')),
|
||||
('ur', _('Urdu')),
|
||||
('he', _('Hebrew')),
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = 'en' # default language
|
||||
|
||||
TINYMCE_DEFAULT_CONFIG = {
|
||||
"theme": "silver",
|
||||
"height": 500,
|
||||
"resize": "both",
|
||||
"menubar": "file edit view insert format tools table help",
|
||||
"menu": {
|
||||
"format": {
|
||||
"title": "Format",
|
||||
"items": "blocks | bold italic underline strikethrough superscript subscript code | " "fontfamily fontsize align lineheight | " "forecolor backcolor removeformat",
|
||||
},
|
||||
},
|
||||
"plugins": "advlist,autolink,autosave,lists,link,image,charmap,print,preview,anchor,"
|
||||
"searchreplace,visualblocks,code,fullscreen,insertdatetime,media,table,paste,directionality,"
|
||||
"code,help,wordcount,emoticons,file,image,media",
|
||||
"toolbar": "undo redo | code preview | blocks | "
|
||||
"bold italic | alignleft aligncenter "
|
||||
"alignright alignjustify ltr rtl | bullist numlist outdent indent | "
|
||||
"removeformat | restoredraft help | image media",
|
||||
"branding": False, # remove branding
|
||||
"promotion": False, # remove promotion
|
||||
"body_class": "page-main-inner custom-page-wrapper", # class of the body element in tinymce
|
||||
"block_formats": "Paragraph=p; Heading 1=h1; Heading 2=h2; Heading 3=h3;",
|
||||
"formats": { # customize h2 to always have emphasis-large class
|
||||
"h2": {"block": "h2", "classes": "emphasis-large"},
|
||||
},
|
||||
"font_size_formats": "16px 18px 24px 32px",
|
||||
"images_upload_url": "/tinymce/upload/",
|
||||
"images_upload_handler": "tinymce.views.upload_image",
|
||||
"automatic_uploads": True,
|
||||
"file_picker_types": "image",
|
||||
"paste_data_images": True,
|
||||
"paste_as_text": False,
|
||||
"paste_enable_default_filters": True,
|
||||
"paste_word_valid_elements": "b,strong,i,em,h1,h2,h3,h4,h5,h6,p,br,a,ul,ol,li",
|
||||
"paste_retain_style_properties": "all",
|
||||
"paste_remove_styles": False,
|
||||
"paste_merge_formats": True,
|
||||
"sandbox_iframes": False,
|
||||
}
|
||||
|
||||
SPRITE_NUM_SECS = 10
|
||||
# number of seconds for sprite image.
|
||||
# If you plan to change this, you must also follow the instructions on admins_docs.md
|
||||
# to change the equivalent value in ./frontend/src/static/js/components/media-viewer/VideoViewer/index.js and then re-build frontend
|
||||
|
||||
# how many images will be shown on the slideshow
|
||||
SLIDESHOW_ITEMS = 30
|
||||
# this calculation is redundant most probably, setting as an option
|
||||
CALCULATE_MD5SUM = False
|
||||
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
|
||||
# allow option to override the default admin url
|
||||
# keep the trailing slash
|
||||
DJANGO_ADMIN_URL = "admin/"
|
||||
|
||||
# this are used around a number of places and will need to be well documented!!!
|
||||
|
||||
USE_SAML = False
|
||||
USE_RBAC = False
|
||||
USE_IDENTITY_PROVIDERS = False
|
||||
JAZZMIN_UI_TWEAKS = {"theme": "flatly"}
|
||||
|
||||
USE_ROUNDED_CORNERS = True
|
||||
|
||||
ALLOW_VIDEO_TRIMMER = True
|
||||
|
||||
ALLOW_CUSTOM_MEDIA_URLS = False
|
||||
|
||||
# Whether to allow anonymous users to list all users
|
||||
ALLOW_ANONYMOUS_USER_LISTING = True
|
||||
|
||||
# Who can see the members page
|
||||
# valid choices are all, editors, admins
|
||||
CAN_SEE_MEMBERS_PAGE = "all"
|
||||
|
||||
# User search field setting
|
||||
# valid choices are name_username, name_username_email
|
||||
# this searches for users in the share media modal under my media
|
||||
USER_SEARCH_FIELD = "name_username"
|
||||
|
||||
# Maximum number of media a user can upload
|
||||
NUMBER_OF_MEDIA_USER_CAN_UPLOAD = 100
|
||||
|
||||
# ffmpeg options
|
||||
FFMPEG_DEFAULT_PRESET = "medium" # see https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||
|
||||
# If 'all' is in the list, no check is performed
|
||||
ALLOWED_MEDIA_UPLOAD_TYPES = ["video", "audio", "image", "pdf"]
|
||||
|
||||
# transcription options
|
||||
# the mediacms-full docker image needs to be used in order to be able to use transcription
|
||||
# if you are using the mediacms-full image, change USE_WHISPER_TRANSCRIBE to True
|
||||
USE_WHISPER_TRANSCRIBE = False
|
||||
|
||||
# by default all users can request a video to be transcribed. If you want to
|
||||
# allow only editors, set this to False
|
||||
USER_CAN_TRANSCRIBE_VIDEO = True
|
||||
|
||||
# Whisper transcribe options - https://github.com/openai/whisper
|
||||
WHISPER_MODEL = "base"
|
||||
|
||||
# show a custom text in the sidebar footer, otherwise the default will be shown if this is empty
|
||||
SIDEBAR_FOOTER_TEXT = ""
|
||||
|
||||
try:
|
||||
# keep a local_settings.py file for local overrides
|
||||
from .local_settings import * # noqa
|
||||
# Load custom settings from custom/local_settings.py
|
||||
import sys
|
||||
sys.path.insert(0, BASE_DIR)
|
||||
from custom.local_settings import * # noqa
|
||||
|
||||
# ALLOWED_HOSTS needs a url/ip
|
||||
ALLOWED_HOSTS.append(FRONTEND_HOST.replace("http://", "").replace("https://", ""))
|
||||
except ImportError:
|
||||
# local_settings not in use
|
||||
# custom/local_settings.py not in use or empty
|
||||
pass
|
||||
|
||||
# Don't add new settings below that could be overridden in local_settings.py!!!
|
||||
|
||||
if "http" not in FRONTEND_HOST:
|
||||
# FRONTEND_HOST needs a http:// preffix
|
||||
FRONTEND_HOST = f"http://{FRONTEND_HOST}"
|
||||
FRONTEND_HOST = f"http://{FRONTEND_HOST}" # noqa
|
||||
|
||||
if LOCAL_INSTALL:
|
||||
SSL_FRONTEND_HOST = FRONTEND_HOST.replace("http", "https")
|
||||
else:
|
||||
SSL_FRONTEND_HOST = FRONTEND_HOST
|
||||
|
||||
|
||||
# CSRF_COOKIE_SECURE = True
|
||||
# SESSION_COOKIE_SECURE = True
|
||||
|
||||
PYSUBS_COMMAND = "pysubs2"
|
||||
|
||||
# the following is related to local development using docker
|
||||
# and docker-compose-dev.yaml
|
||||
try:
|
||||
DEVELOPMENT_MODE = os.environ.get("DEVELOPMENT_MODE")
|
||||
if DEVELOPMENT_MODE:
|
||||
# keep a dev_settings.py file for local overrides
|
||||
from .dev_settings import * # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
if GLOBAL_LOGIN_REQUIRED:
|
||||
# this should go after the AuthenticationMiddleware middleware
|
||||
MIDDLEWARE.insert(5, "login_required.middleware.LoginRequiredMiddleware")
|
||||
LOGIN_REQUIRED_IGNORE_PATHS = [
|
||||
r'/accounts/login/$',
|
||||
r'/accounts/logout/$',
|
||||
r'/accounts/signup/$',
|
||||
r'/api/v[0-9]+/',
|
||||
]
|
||||
auth_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
|
||||
MIDDLEWARE.insert(auth_index + 1, "django.contrib.auth.middleware.LoginRequiredMiddleware")
|
||||
|
||||
|
||||
if USERS_NEEDS_TO_BE_APPROVED:
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'cms.auth_backends.ApprovalBackend',
|
||||
'allauth.account.auth_backends.AuthenticationBackend',
|
||||
)
|
||||
auth_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
|
||||
MIDDLEWARE.insert(auth_index + 1, "cms.middleware.ApprovalMiddleware")
|
||||
|
||||
13
cms/urls.py
13
cms/urls.py
@@ -1,7 +1,8 @@
|
||||
import debug_toolbar
|
||||
from django.conf.urls import include, re_path
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.urls import path, re_path
|
||||
from django.views.generic.base import TemplateView
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
@@ -13,6 +14,7 @@ schema_view = get_schema_view(
|
||||
permission_classes=(AllowAny,),
|
||||
)
|
||||
|
||||
# refactor seriously
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
@@ -24,8 +26,13 @@ urlpatterns = [
|
||||
re_path(r"^", include("users.urls")),
|
||||
re_path(r"^accounts/", include("allauth.urls")),
|
||||
re_path(r"^api-auth/", include("rest_framework.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
path(settings.DJANGO_ADMIN_URL, admin.site.urls),
|
||||
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||
re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||
path('docs/api/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
||||
path("tinymce/", include("tinymce.urls")),
|
||||
]
|
||||
|
||||
admin.site.site_header = "MediaCMS Admin"
|
||||
admin.site.site_title = "MediaCMS"
|
||||
admin.site.index_title = "Admin"
|
||||
|
||||
1
cms/version.py
Normal file
1
cms/version.py
Normal file
@@ -0,0 +1 @@
|
||||
VERSION = "7.2.1"
|
||||
99
config/imagemagick/policy.xml
Normal file
99
config/imagemagick/policy.xml
Normal file
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE policymap [
|
||||
<!ELEMENT policymap (policy)*>
|
||||
<!ATTLIST policymap xmlns CDATA #FIXED ''>
|
||||
<!ELEMENT policy EMPTY>
|
||||
<!ATTLIST policy xmlns CDATA #FIXED '' domain NMTOKEN #REQUIRED
|
||||
name NMTOKEN #IMPLIED pattern CDATA #IMPLIED rights NMTOKEN #IMPLIED
|
||||
stealth NMTOKEN #IMPLIED value CDATA #IMPLIED>
|
||||
]>
|
||||
<!--
|
||||
Configure ImageMagick policies.
|
||||
|
||||
Domains include system, delegate, coder, filter, path, or resource.
|
||||
|
||||
Rights include none, read, write, execute and all. Use | to combine them,
|
||||
for example: "read | write" to permit read from, or write to, a path.
|
||||
|
||||
Use a glob expression as a pattern.
|
||||
|
||||
Suppose we do not want users to process MPEG video images:
|
||||
|
||||
<policy domain="delegate" rights="none" pattern="mpeg:decode" />
|
||||
|
||||
Here we do not want users reading images from HTTP:
|
||||
|
||||
<policy domain="coder" rights="none" pattern="HTTP" />
|
||||
|
||||
The /repository file system is restricted to read only. We use a glob
|
||||
expression to match all paths that start with /repository:
|
||||
|
||||
<policy domain="path" rights="read" pattern="/repository/*" />
|
||||
|
||||
Lets prevent users from executing any image filters:
|
||||
|
||||
<policy domain="filter" rights="none" pattern="*" />
|
||||
|
||||
Any large image is cached to disk rather than memory:
|
||||
|
||||
<policy domain="resource" name="area" value="1GP"/>
|
||||
|
||||
Use the default system font unless overwridden by the application:
|
||||
|
||||
<policy domain="system" name="font" value="/usr/share/fonts/favorite.ttf"/>
|
||||
|
||||
Define arguments for the memory, map, area, width, height and disk resources
|
||||
with SI prefixes (.e.g 100MB). In addition, resource policies are maximums
|
||||
for each instance of ImageMagick (e.g. policy memory limit 1GB, -limit 2GB
|
||||
exceeds policy maximum so memory limit is 1GB).
|
||||
|
||||
Rules are processed in order. Here we want to restrict ImageMagick to only
|
||||
read or write a small subset of proven web-safe image types:
|
||||
|
||||
<policy domain="delegate" rights="none" pattern="*" />
|
||||
<policy domain="filter" rights="none" pattern="*" />
|
||||
<policy domain="coder" rights="none" pattern="*" />
|
||||
<policy domain="coder" rights="read|write" pattern="{GIF,JPEG,PNG,WEBP}" />
|
||||
-->
|
||||
<policymap>
|
||||
<!-- <policy domain="resource" name="temporary-path" value="/tmp"/> -->
|
||||
<policy domain="resource" name="memory" value="1GiB"/>
|
||||
<policy domain="resource" name="map" value="30GiB"/>
|
||||
<policy domain="resource" name="width" value="16MP"/>
|
||||
<policy domain="resource" name="height" value="16MP"/>
|
||||
<!-- <policy domain="resource" name="list-length" value="128"/> -->
|
||||
<policy domain="resource" name="area" value="40GP"/>
|
||||
<policy domain="resource" name="disk" value="100GiB"/>
|
||||
<!-- <policy domain="resource" name="file" value="768"/> -->
|
||||
<!-- <policy domain="resource" name="thread" value="4"/> -->
|
||||
<!-- <policy domain="resource" name="throttle" value="0"/> -->
|
||||
<!-- <policy domain="resource" name="time" value="3600"/> -->
|
||||
<!-- <policy domain="coder" rights="none" pattern="MVG" /> -->
|
||||
<!-- <policy domain="module" rights="none" pattern="{PS,PDF,XPS}" /> -->
|
||||
<!-- <policy domain="path" rights="none" pattern="@*" /> -->
|
||||
<!-- <policy domain="cache" name="memory-map" value="anonymous"/> -->
|
||||
<!-- <policy domain="cache" name="synchronize" value="True"/> -->
|
||||
<!-- <policy domain="cache" name="shared-secret" value="passphrase" stealth="true"/>
|
||||
<!-- <policy domain="system" name="max-memory-request" value="256MiB"/> -->
|
||||
<!-- <policy domain="system" name="shred" value="2"/> -->
|
||||
<!-- <policy domain="system" name="precision" value="6"/> -->
|
||||
<!-- <policy domain="system" name="font" value="/path/to/font.ttf"/> -->
|
||||
<!-- <policy domain="system" name="pixel-cache-memory" value="anonymous"/> -->
|
||||
<!-- <policy domain="system" name="shred" value="2"/> -->
|
||||
<!-- <policy domain="system" name="precision" value="6"/> -->
|
||||
<!-- not needed due to the need to use explicitly by mvg: -->
|
||||
<!-- <policy domain="delegate" rights="none" pattern="MVG" /> -->
|
||||
<!-- use curl -->
|
||||
<policy domain="delegate" rights="none" pattern="URL" />
|
||||
<policy domain="delegate" rights="none" pattern="HTTPS" />
|
||||
<policy domain="delegate" rights="none" pattern="HTTP" />
|
||||
<!-- in order to avoid to get image with password text -->
|
||||
<policy domain="path" rights="none" pattern="@*"/>
|
||||
<!-- disable ghostscript format types -->
|
||||
<policy domain="coder" rights="none" pattern="PS" />
|
||||
<policy domain="coder" rights="none" pattern="PS2" />
|
||||
<policy domain="coder" rights="none" pattern="PS3" />
|
||||
<policy domain="coder" rights="none" pattern="EPS" />
|
||||
<policy domain="coder" rights="none" pattern="PDF" />
|
||||
<policy domain="coder" rights="none" pattern="XPS" />
|
||||
</policymap>
|
||||
@@ -1,4 +1,4 @@
|
||||
user www-data;
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
@@ -23,8 +23,8 @@ http {
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
access_log /var/log/mediacms/nginx-main.access.log;
|
||||
error_log /var/log/mediacms/nginx-main.error.log;
|
||||
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
@@ -2,20 +2,24 @@ server {
|
||||
listen 80 ;
|
||||
|
||||
gzip on;
|
||||
access_log /var/log/nginx/mediacms.io.access.log;
|
||||
access_log /var/log/mediacms/nginx.access.log;
|
||||
|
||||
error_log /var/log/nginx/mediacms.io.error.log warn;
|
||||
error_log /var/log/mediacms/nginx.error.log warn;
|
||||
|
||||
location /static {
|
||||
alias /home/mediacms.io/mediacms/static ;
|
||||
alias /var/www/static ;
|
||||
}
|
||||
|
||||
location /custom/static {
|
||||
alias /var/www/custom ;
|
||||
}
|
||||
|
||||
location /media/original {
|
||||
alias /home/mediacms.io/mediacms/media_files/original;
|
||||
alias /var/www/media/original;
|
||||
}
|
||||
|
||||
location /media {
|
||||
alias /home/mediacms.io/mediacms/media_files ;
|
||||
alias /var/www/media ;
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
@@ -28,7 +32,7 @@ server {
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||
|
||||
include /etc/nginx/sites-enabled/uwsgi_params;
|
||||
uwsgi_pass 127.0.0.1:9000;
|
||||
include /etc/nginx/uwsgi_params;
|
||||
uwsgi_pass web:9000;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ threads = 2
|
||||
|
||||
master = true
|
||||
|
||||
socket = 127.0.0.1:9000
|
||||
socket = 0.0.0.0:9000
|
||||
|
||||
workers = 2
|
||||
|
||||
@@ -21,3 +21,4 @@ vacuum = true
|
||||
hook-master-start = unix_signal:15 gracefully_kill_them_all
|
||||
need-app = true
|
||||
die-on-term = true
|
||||
buffer-size=32768
|
||||
@@ -1,5 +0,0 @@
|
||||
from pytest_factoryboy import register
|
||||
|
||||
from tests.users.factories import UserFactory
|
||||
|
||||
register(UserFactory)
|
||||
0
custom/.gitkeep
Normal file
0
custom/.gitkeep
Normal file
238
custom/README.md
Normal file
238
custom/README.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# Custom Configuration
|
||||
|
||||
This directory allows you to customize MediaCMS without modifying the codebase or rebuilding images.
|
||||
|
||||
## How It Works - Production Ready!
|
||||
|
||||
**The Flow:**
|
||||
|
||||
```
|
||||
1. CI/CD builds base image: docker build (no custom files)
|
||||
↓
|
||||
Pushes to Docker Hub
|
||||
|
||||
2. Production pulls image: docker compose pull
|
||||
↓
|
||||
Mounts custom/ directory
|
||||
|
||||
3. You add files: custom/static/css/custom.css
|
||||
custom/static/images/logo.png
|
||||
↓
|
||||
Nginx serves directly!
|
||||
|
||||
4. You reference in settings: EXTRA_CSS_PATHS = ["/custom/static/css/custom.css"]
|
||||
PORTAL_LOGO_DARK_PNG = "/custom/static/images/logo.png"
|
||||
↓
|
||||
Restart containers
|
||||
|
||||
5. Done! No rebuild needed!
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- ✅ Files go in `custom/static/` on your host
|
||||
- ✅ Nginx serves them directly from `/custom/static/` URL
|
||||
- ✅ **NO rebuild needed** - just restart containers!
|
||||
- ✅ Works with pre-built images from Docker Hub
|
||||
- ✅ Perfect for production deployments
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: No Customization (Default)
|
||||
Just run docker compose - everything works out of the box:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Option 2: With Customization
|
||||
Add your custom files, then restart:
|
||||
```bash
|
||||
# 1. Copy example settings
|
||||
cp custom/local_settings.py.example custom/local_settings.py
|
||||
|
||||
# 2. Edit settings
|
||||
nano custom/local_settings.py
|
||||
|
||||
# 3. Restart containers (no rebuild!)
|
||||
docker compose restart web celery_beat celery_short celery_long
|
||||
```
|
||||
|
||||
## Customization Options
|
||||
|
||||
### 1. Django Settings (`local_settings.py`)
|
||||
|
||||
**Create the file:**
|
||||
```bash
|
||||
cp custom/local_settings.py.example custom/local_settings.py
|
||||
```
|
||||
|
||||
**Edit with your settings:**
|
||||
```python
|
||||
# custom/local_settings.py
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = ['example.com']
|
||||
PORTAL_NAME = "My Media Site"
|
||||
```
|
||||
|
||||
**Apply changes (restart only - no rebuild):**
|
||||
```bash
|
||||
docker compose restart web celery_beat celery_short celery_long
|
||||
```
|
||||
|
||||
### 2. Custom Logo
|
||||
|
||||
**Add your logo:**
|
||||
```bash
|
||||
cp ~/my-logo.png custom/static/images/logo_dark.png
|
||||
```
|
||||
|
||||
**Reference it in settings:**
|
||||
```bash
|
||||
cat >> custom/local_settings.py <<EOF
|
||||
PORTAL_LOGO_DARK_PNG = "/custom/static/images/logo_dark.png"
|
||||
EOF
|
||||
```
|
||||
|
||||
**Restart (no rebuild needed!):**
|
||||
```bash
|
||||
docker compose restart web
|
||||
```
|
||||
|
||||
### 3. Custom CSS
|
||||
|
||||
**Create CSS file:**
|
||||
```bash
|
||||
cat > custom/static/css/custom.css <<EOF
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
.header {
|
||||
background-color: #333;
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
**Reference it in settings:**
|
||||
```bash
|
||||
cat >> custom/local_settings.py <<EOF
|
||||
EXTRA_CSS_PATHS = ["/custom/static/css/custom.css"]
|
||||
EOF
|
||||
```
|
||||
|
||||
**Restart (no rebuild needed!):**
|
||||
```bash
|
||||
docker compose restart web
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
custom/
|
||||
├── README.md # This file
|
||||
├── local_settings.py.example # Template (copy to local_settings.py)
|
||||
├── local_settings.py # Your settings (gitignored)
|
||||
└── static/
|
||||
├── images/ # Custom logos (gitignored)
|
||||
│ └── logo_dark.png
|
||||
└── css/ # Custom CSS (gitignored)
|
||||
└── custom.css
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
✅ **No rebuild needed** - nginx serves custom/ files directly
|
||||
✅ **Works with pre-built images** - perfect for production
|
||||
✅ **Files are gitignored** - your customizations won't be committed
|
||||
✅ **Settings need restart only** - just restart containers
|
||||
✅ **Static files also just restart** - served directly by nginx
|
||||
|
||||
## Complete Example
|
||||
|
||||
```bash
|
||||
# 1. Create settings file
|
||||
cp custom/local_settings.py.example custom/local_settings.py
|
||||
|
||||
# 2. Add custom logo
|
||||
cp ~/logo.png custom/static/images/logo_dark.png
|
||||
|
||||
# 3. Add custom CSS
|
||||
echo "body { background: #f5f5f5; }" > custom/static/css/custom.css
|
||||
|
||||
# 4. Configure settings to use them
|
||||
cat >> custom/local_settings.py <<EOF
|
||||
|
||||
# Custom branding
|
||||
PORTAL_NAME = "My Media Portal"
|
||||
PORTAL_LOGO_DARK_PNG = "/custom/static/images/logo_dark.png"
|
||||
EXTRA_CSS_PATHS = ["/custom/static/css/custom.css"]
|
||||
|
||||
# Security
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = ['media.example.com']
|
||||
EOF
|
||||
|
||||
# 5. Apply changes (just restart!)
|
||||
docker compose restart web
|
||||
|
||||
# Done! No rebuild needed.
|
||||
```
|
||||
|
||||
## URL Paths Explained
|
||||
|
||||
| Your file | nginx serves at | You reference as |
|
||||
|-----------|----------------|------------------|
|
||||
| `custom/static/css/custom.css` | `http://localhost/custom/static/css/custom.css` | `"/custom/static/css/custom.css"` |
|
||||
| `custom/static/images/logo.png` | `http://localhost/custom/static/images/logo.png` | `"/custom/static/images/logo.png"` |
|
||||
|
||||
**Why `/custom/static/`?**
|
||||
- Distinguishes from core `/static/` (built into image)
|
||||
- Allows nginx to serve from different mount point
|
||||
- No rebuild needed when files change
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Changes not appearing?**
|
||||
- Restart containers: `docker compose restart web nginx`
|
||||
- Check nginx has custom/ mounted: `docker compose exec nginx ls /var/www/custom`
|
||||
- Check file exists: `docker compose exec nginx ls /var/www/custom/css/`
|
||||
- Test URL: `curl http://localhost/custom/static/css/custom.css`
|
||||
|
||||
**Import errors?**
|
||||
- Make sure `local_settings.py` has valid Python syntax
|
||||
- Check logs: `docker compose logs web`
|
||||
|
||||
**Logo not showing?**
|
||||
- Verify file is in `custom/static/images/`
|
||||
- Check path in `local_settings.py` uses `/custom/static/` prefix
|
||||
- Restart web container: `docker compose restart web`
|
||||
|
||||
## Advanced: Multiple CSS Files
|
||||
|
||||
```python
|
||||
# custom/local_settings.py
|
||||
EXTRA_CSS_PATHS = [
|
||||
"/custom/static/css/colors.css",
|
||||
"/custom/static/css/fonts.css",
|
||||
"/custom/static/css/layout.css",
|
||||
]
|
||||
```
|
||||
|
||||
## Advanced: Environment-Specific Settings
|
||||
|
||||
```python
|
||||
# custom/local_settings.py
|
||||
import os
|
||||
|
||||
if os.getenv('ENVIRONMENT') == 'production':
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = ['media.example.com']
|
||||
else:
|
||||
DEBUG = True
|
||||
ALLOWED_HOSTS = ['*']
|
||||
```
|
||||
|
||||
Then set in docker-compose.yaml:
|
||||
```yaml
|
||||
web:
|
||||
environment:
|
||||
ENVIRONMENT: production
|
||||
```
|
||||
57
custom/local_settings.py.example
Normal file
57
custom/local_settings.py.example
Normal file
@@ -0,0 +1,57 @@
|
||||
# MediaCMS Local Settings Example
|
||||
# Copy this file to local_settings.py and customize as needed:
|
||||
# cp custom/local_settings.py.example custom/local_settings.py
|
||||
|
||||
# ===== Basic Settings =====
|
||||
|
||||
# DEBUG = False
|
||||
# ALLOWED_HOSTS = ['example.com', 'www.example.com']
|
||||
# PORTAL_NAME = "My Media Portal"
|
||||
|
||||
# ===== Database Settings =====
|
||||
|
||||
# DATABASES = {
|
||||
# 'default': {
|
||||
# 'ENGINE': 'django.db.backends.postgresql',
|
||||
# 'NAME': 'mediacms',
|
||||
# 'USER': 'mediacms',
|
||||
# 'PASSWORD': 'mediacms',
|
||||
# 'HOST': 'db',
|
||||
# 'PORT': '5432',
|
||||
# }
|
||||
# }
|
||||
|
||||
# ===== Custom Branding =====
|
||||
|
||||
# Custom logos (place files in custom/static/images/)
|
||||
# Nginx serves these directly from /custom/static/ (no rebuild needed!)
|
||||
# PORTAL_LOGO_DARK_SVG = "/custom/static/images/logo_dark.svg"
|
||||
# PORTAL_LOGO_DARK_PNG = "/custom/static/images/logo_dark.png"
|
||||
# PORTAL_LOGO_LIGHT_SVG = "/custom/static/images/logo_light.svg"
|
||||
# PORTAL_LOGO_LIGHT_PNG = "/custom/static/images/logo_light.png"
|
||||
|
||||
# Custom CSS (place files in custom/static/css/)
|
||||
# Nginx serves these directly from /custom/static/ (no rebuild needed!)
|
||||
# EXTRA_CSS_PATHS = ["/custom/static/css/custom.css"]
|
||||
|
||||
# ===== Email Settings =====
|
||||
|
||||
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
# EMAIL_HOST = 'smtp.gmail.com'
|
||||
# EMAIL_PORT = 587
|
||||
# EMAIL_USE_TLS = True
|
||||
# EMAIL_HOST_USER = 'your-email@example.com'
|
||||
# EMAIL_HOST_PASSWORD = 'your-password'
|
||||
# DEFAULT_FROM_EMAIL = 'noreply@example.com'
|
||||
|
||||
# ===== Security Settings =====
|
||||
|
||||
# SECRET_KEY = 'your-secret-key-here'
|
||||
# SECURE_SSL_REDIRECT = True
|
||||
# SESSION_COOKIE_SECURE = True
|
||||
# CSRF_COOKIE_SECURE = True
|
||||
|
||||
# ===== Other Settings =====
|
||||
|
||||
# Any other Django setting can be overridden here
|
||||
# See cms/settings.py for available settings
|
||||
0
custom/static/.gitkeep
Normal file
0
custom/static/.gitkeep
Normal file
0
custom/static/css/.gitkeep
Normal file
0
custom/static/css/.gitkeep
Normal file
0
custom/static/images/.gitkeep
Normal file
0
custom/static/images/.gitkeep
Normal file
75
deic_setup_notes.md
Normal file
75
deic_setup_notes.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# MediaCMS: Document Changes for DEIC
|
||||
|
||||
## Configuration Changes
|
||||
The following changes are required in `config/local_settings.py`:
|
||||
|
||||
```python
|
||||
|
||||
# default workflow
|
||||
PORTAL_WORKFLOW = 'private'
|
||||
|
||||
# Authentication Settings
|
||||
# these two are necessary so that users cannot register through system accounts. They can only register through identity providers
|
||||
REGISTER_ALLOWED = False
|
||||
USERS_CAN_SELF_REGISTER = False
|
||||
|
||||
USE_RBAC = True
|
||||
USE_SAML = True
|
||||
USE_IDENTITY_PROVIDERS = True
|
||||
|
||||
# Proxy and SSL Settings
|
||||
USE_X_FORWARDED_HOST = True
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
SECURE_SSL_REDIRECT = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
# SAML Configuration
|
||||
SOCIALACCOUNT_ADAPTER = 'saml_auth.adapter.SAMLAccountAdapter'
|
||||
ACCOUNT_USERNAME_VALIDATORS = "users.validators.less_restrictive_username_validators"
|
||||
SOCIALACCOUNT_PROVIDERS = {
|
||||
"saml": {
|
||||
"provider_class": "saml_auth.custom.provider.CustomSAMLProvider",
|
||||
}
|
||||
}
|
||||
SOCIALACCOUNT_AUTO_SIGNUP = True
|
||||
SOCIALACCOUNT_EMAIL_REQUIRED = False
|
||||
|
||||
# if set to strict, user is created with the email from the saml provider without
|
||||
# checking if the email is already on the system
|
||||
# however if this is ommited, and user tries to login with an email that already exists on
|
||||
# the system, then they get to the ugly form where it suggests they add a username/email/name
|
||||
|
||||
ACCOUNT_PREVENT_ENUMERATION = 'strict'
|
||||
|
||||
```
|
||||
|
||||
## SAML Configuration Steps
|
||||
|
||||
### Step 1: Add SAML Identity Provider
|
||||
1. Navigate to Admin panel
|
||||
2. Select "Identity Provider"
|
||||
3. Configure as follows:
|
||||
- **Provider**: saml # ensure this is set with lower case!
|
||||
- **Provider ID**: `wayf.wayf.dk`
|
||||
- **IDP Config Name**: `Deic` (or preferred name)
|
||||
- **Client ID**: `wayf_dk` (important: defines the URL, e.g., `https://deic.mediacms.io/accounts/saml/wayf_dk`)
|
||||
- **Site**: Set the default one
|
||||
|
||||
### Step 2: Add SAML Configuration
|
||||
Can be set through the SAML Configurations tab:
|
||||
|
||||
1. **IDP ID**: Must be a URL, e.g., `https://wayf.wayf.dk`
|
||||
2. **IDP Certificate**: x509cert from your SAML provider
|
||||
3. **SSO URL**: `https://wayf.wayf.dk/saml2/idp/SSOService2.php`
|
||||
4. **SLO URL**: `https://wayf.wayf.dk/saml2/idp/SingleLogoutService.php`
|
||||
5. **SP Metadata URL**: The metadata URL set for the SP, e.g., `https://deic.mediacms.io/saml/metadata`. This should point to the URL of the SP and is autogenerated
|
||||
|
||||
### Step 3: Set the other Options
|
||||
1. **Email Settings**:
|
||||
- `verified_email`: When enabled, emails from SAML responses will be marked as verified
|
||||
- `Remove from groups`: When enabled, user is removed from a group after login, if they have been removed from the group on the IDP
|
||||
2. **Global Role Mapping**: Maps the role returned by SAML (as set in the SAML Configuration tab) with the role in MediaCMS
|
||||
3. **Group Role Mapping**: Maps the role returned by SAML (as set in the SAML Configuration tab) with the role in groups that user will be added
|
||||
4. **Group mapping**: This creates groups associated with this IDP. Group ids as they come from SAML, associated with MediaCMS groups
|
||||
5. **Category Mapping**: This maps a group id (from SAML response) with a category in MediaCMS
|
||||
@@ -1,3 +0,0 @@
|
||||
# MediaCMS on Docker
|
||||
|
||||
See: [Details](../../docs/Docker_deployment.md)
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# forward request and error logs to docker log collector
|
||||
ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log && \
|
||||
ln -sf /dev/stdout /var/log/nginx/mediacms.io.access.log && ln -sf /dev/stderr /var/log/nginx/mediacms.io.error.log
|
||||
|
||||
cp /home/mediacms.io/mediacms/deploy/docker/local_settings.py /home/mediacms.io/mediacms/cms/local_settings.py
|
||||
|
||||
mkdir -p /home/mediacms.io/mediacms/{logs,media_files/hls}
|
||||
touch /home/mediacms.io/mediacms/logs/debug.log
|
||||
|
||||
mkdir -p /var/run/mediacms
|
||||
chown www-data:www-data /var/run/mediacms
|
||||
|
||||
TARGET_GID=$(stat -c "%g" /home/mediacms.io/mediacms/)
|
||||
|
||||
EXISTS=$(cat /etc/group | grep $TARGET_GID | wc -l)
|
||||
|
||||
# Create new group using target GID and add www-data user
|
||||
if [ $EXISTS == "0" ]; then
|
||||
groupadd -g $TARGET_GID tempgroup
|
||||
usermod -a -G tempgroup www-data
|
||||
else
|
||||
# GID exists, find group name and add
|
||||
GROUP=$(getent group $TARGET_GID | cut -d: -f1)
|
||||
usermod -a -G $GROUP www-data
|
||||
fi
|
||||
|
||||
# We should do this only for folders that have a different owner, since it is an expensive operation
|
||||
find /home/mediacms.io/ ! \( -user www-data -group $TARGET_GID \) -exec chown www-data:$TARGET_GID {} +
|
||||
|
||||
chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh
|
||||
|
||||
exec "$@"
|
||||
@@ -1,34 +0,0 @@
|
||||
FRONTEND_HOST = 'http://localhost'
|
||||
PORTAL_NAME = 'MediaCMS'
|
||||
SECRET_KEY = 'ma!s3^b-cw!f#7s6s0m3*jx77a@riw(7701**(r=ww%w!2+yk2'
|
||||
POSTGRES_HOST = 'db'
|
||||
REDIS_LOCATION = "redis://redis:6379/1"
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": "mediacms",
|
||||
"HOST": POSTGRES_HOST,
|
||||
"PORT": "5432",
|
||||
"USER": "mediacms",
|
||||
"PASSWORD": "mediacms",
|
||||
}
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_LOCATION,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# CELERY STUFF
|
||||
BROKER_URL = REDIS_LOCATION
|
||||
CELERY_RESULT_BACKEND = BROKER_URL
|
||||
|
||||
MP4HLS_COMMAND = "/home/mediacms.io/bento4/bin/mp4hls"
|
||||
|
||||
DEBUG = False
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
RANDOM_ADMIN_PASS=`python -c "import secrets;chars = 'abcdefghijklmnopqrstuvwxyz0123456789';print(''.join(secrets.choice(chars) for i in range(10)))"`
|
||||
ADMIN_PASSWORD=${ADMIN_PASSWORD:-$RANDOM_ADMIN_PASS}
|
||||
|
||||
if [ X"$ENABLE_MIGRATIONS" = X"yes" ]; then
|
||||
echo "Running migrations service"
|
||||
python manage.py migrate
|
||||
EXISTING_INSTALLATION=`echo "from users.models import User; print(User.objects.exists())" |python manage.py shell`
|
||||
if [ "$EXISTING_INSTALLATION" = "True" ]; then
|
||||
echo "Loaddata has already run"
|
||||
else
|
||||
echo "Running loaddata and creating admin user"
|
||||
python manage.py loaddata fixtures/encoding_profiles.json
|
||||
python manage.py loaddata fixtures/categories.json
|
||||
|
||||
# post_save, needs redis to succeed (ie. migrate depends on redis)
|
||||
DJANGO_SUPERUSER_PASSWORD=$ADMIN_PASSWORD python manage.py createsuperuser \
|
||||
--no-input \
|
||||
--username=$ADMIN_USER \
|
||||
--email=$ADMIN_EMAIL \
|
||||
--database=default || true
|
||||
echo "Created admin user with password: $ADMIN_PASSWORD"
|
||||
|
||||
fi
|
||||
echo "RUNNING COLLECTSTATIC"
|
||||
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
# echo "Updating hostname ..."
|
||||
# TODO: Get the FRONTEND_HOST from cms/local_settings.py
|
||||
# echo "from django.contrib.sites.models import Site; Site.objects.update(name='$FRONTEND_HOST', domain='$FRONTEND_HOST')" | python manage.py shell
|
||||
fi
|
||||
|
||||
# Setting up internal nginx server
|
||||
# HTTPS setup is delegated to a reverse proxy running infront of the application
|
||||
|
||||
cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-available/default
|
||||
cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-enabled/default
|
||||
cp deploy/docker/uwsgi_params /etc/nginx/sites-enabled/uwsgi_params
|
||||
cp deploy/docker/nginx.conf /etc/nginx/
|
||||
|
||||
#### Supervisord Configurations #####
|
||||
|
||||
cp deploy/docker/supervisord/supervisord-debian.conf /etc/supervisor/conf.d/supervisord-debian.conf
|
||||
|
||||
if [ X"$ENABLE_UWSGI" = X"yes" ] ; then
|
||||
echo "Enabling uwsgi app server"
|
||||
cp deploy/docker/supervisord/supervisord-uwsgi.conf /etc/supervisor/conf.d/supervisord-uwsgi.conf
|
||||
fi
|
||||
|
||||
if [ X"$ENABLE_NGINX" = X"yes" ] ; then
|
||||
echo "Enabling nginx as uwsgi app proxy and media server"
|
||||
cp deploy/docker/supervisord/supervisord-nginx.conf /etc/supervisor/conf.d/supervisord-nginx.conf
|
||||
fi
|
||||
|
||||
if [ X"$ENABLE_CELERY_BEAT" = X"yes" ] ; then
|
||||
echo "Enabling celery-beat scheduling server"
|
||||
cp deploy/docker/supervisord/supervisord-celery_beat.conf /etc/supervisor/conf.d/supervisord-celery_beat.conf
|
||||
fi
|
||||
|
||||
if [ X"$ENABLE_CELERY_SHORT" = X"yes" ] ; then
|
||||
echo "Enabling celery-short task worker"
|
||||
cp deploy/docker/supervisord/supervisord-celery_short.conf /etc/supervisor/conf.d/supervisord-celery_short.conf
|
||||
fi
|
||||
|
||||
if [ X"$ENABLE_CELERY_LONG" = X"yes" ] ; then
|
||||
echo "Enabling celery-long task worker"
|
||||
cp deploy/docker/supervisord/supervisord-celery_long.conf /etc/supervisor/conf.d/supervisord-celery_long.conf
|
||||
fi
|
||||
@@ -1,17 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICwzCCAaugAwIBAgIJAOyvdwguJQd+MA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
|
||||
BAMTCWxvY2FsaG9zdDAeFw0yMTAxMjQxMjUwMzFaFw0zMTAxMjIxMjUwMzFaMBQx
|
||||
EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||
ggEBAONswEwBzkgoO+lkewiKUnwvYqC54qleCUg9hidqjoyzd5XWKh1mIF7aaSCG
|
||||
rJGSxCce8CbqAqGkpvsgXzwwbY72l7FwmAXFHO5ObQfpmFhjt2fsKRM9MTCo/UyU
|
||||
liuhgP+Q+BNzUontTUC40NVHs8R7IHG4z8unB7qB/7zGK2tfilLB8JDqPTkc22vN
|
||||
C4P1YxiGyY5bm37wQrroC9zPJ8bqanrF9Y90QJHubibnPWqnZvK2HkDWjp5LYkn8
|
||||
IuzBycs1cLd8eMjU9aT72kweykvnGDDc3YbXFzT2zBTGSFEBROsVdPrNF9PaeE3j
|
||||
pu4UZ8Ge3Fp3VYd+04DnWtbQq0MCAwEAAaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxo
|
||||
b3N0MA0GCSqGSIb3DQEBBQUAA4IBAQAdm2aGn4evosbdWgBHgzr6oYWBIiPpf1SA
|
||||
GXizuf5OaMActFP0rZ0mogndLH5d51J2qqSfOtaWSA5qwlPvDSTn1nvJeHoVLfZf
|
||||
kQHaB7/DaOPGsZCQBELPhYHwl7+Ej3HYE+siiaRfjC2NVgf8P/pAsTlKbe2e+34l
|
||||
GwWSFol24w5xAmUezCF41JiZbqHoZhSh7s/PuJnK2RvhpjkrIot8GvxnbvOcKDIv
|
||||
JzEKo3qPq8pc5RBkpP7Kp2+EgAYn1xAn0CekxZracW/MY+tg2mCeFucZW2V1iwVs
|
||||
LpAw6GJnjYz5mbrQskPbrJ9t78JGUKQ0kL/VUTfryUHMHYCiJlvd
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,27 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA42zATAHOSCg76WR7CIpSfC9ioLniqV4JSD2GJ2qOjLN3ldYq
|
||||
HWYgXtppIIaskZLEJx7wJuoCoaSm+yBfPDBtjvaXsXCYBcUc7k5tB+mYWGO3Z+wp
|
||||
Ez0xMKj9TJSWK6GA/5D4E3NSie1NQLjQ1UezxHsgcbjPy6cHuoH/vMYra1+KUsHw
|
||||
kOo9ORzba80Lg/VjGIbJjlubfvBCuugL3M8nxupqesX1j3RAke5uJuc9aqdm8rYe
|
||||
QNaOnktiSfwi7MHJyzVwt3x4yNT1pPvaTB7KS+cYMNzdhtcXNPbMFMZIUQFE6xV0
|
||||
+s0X09p4TeOm7hRnwZ7cWndVh37TgOda1tCrQwIDAQABAoIBAQCmKKyOW7tlCNBN
|
||||
AzbI1JbTWKOMnoM2DxhlCV5cqgOgVPcIKEL428bGxniMZRjr+vkJRBddtxdZFj1R
|
||||
uSMbjJ5fF1dZMtQ/UvaCPhZ283p1CdXUPbz863ZnAPCf5Oea1RK0piw5ucYSM6h/
|
||||
owgg65Qx92uK6uYW+uAwqg440+ihNvnaZoVTx5CjZbL9KISkrlNJnuYiB5vzOD0i
|
||||
UVklO5Qz8VCuOcOVGZCA2SxHm4HAbg/aiQnpaUa9de4TsZ4ygF66pZh77T0wNOos
|
||||
sS1riKtHQpX+osJyoTI/rIKFAhycsZ+AA7Qpu6GW4xQlNS6K8vRiIbktwkC+IT0O
|
||||
RSn8Dg7BAoGBAPe5R8SpgXx9jKdA1eFa/Vjx5bmB96r2MviIOIWF8rs2K33xe+rj
|
||||
v+BZ2ZjdpVjcm2nRMf9r/eDq2ScNFWmKoZsUmdyT84Qq9yLcTSUdno+zCy+L0LNH
|
||||
DqJq5jIxJaV7amHeR/w10BVuiDmzhSsTmhfnXTUGRO/h2PjRyC3yEYdxAoGBAOsF
|
||||
2+gTsdOGlq6AVzW5MLZkreq8WCU2wWpZRiCPh6HJa8htuynYxO5AWUiNUbYKddj2
|
||||
0za9DFiXgH+Oo8wrkTYLEdN0T5/o+ScL5t3VG3m9R6pnuudLC2vmGQP0hNuZUpnF
|
||||
7FzdJ85h6taR2bM1zFzOfl81K0BhTHGxTU2r70vzAoGAVXuLJ3LyqtnMKn72DzDN
|
||||
0d6PTkdqBoW0qwyerHy/eRjFQ02MXE7BDJMUwmphv1tJCefVX/WNAwsnahFavTPI
|
||||
dnJSccpgMtB8vXvV5yPkbmPzTTHrD6JKi4Nl8hYBjqwa1rDUmFSdfHfK7FZlcqrt
|
||||
9qexAzYpnbmKnLoPYMNyhxECgYEAm5OCUeuPoL2MS7GLiXWwyFx3QFczZlcLzBGS
|
||||
uYUpvLBwF/qDlhz3p9uS/tMFzyK3hktF4Ate+9o2ZroOtd31PzgusbJh7zIylGVt
|
||||
i1VB3eGtaiFGeUuVIPTthE++Dvw80KxTXdnMOvNYmHduDBLF2H2c6/tvSSvfhbdf
|
||||
u9XgD38CgYAiLcVySxMKNpsXatuC31wjT+rnaH22SD/7pXe2q6MRW/s+bGOspu0v
|
||||
NeJSLoM98v8F99q0W0lgqesYJVI20Frru0DfXIp60ryaDolzve3Iwk8SOJUlcnUG
|
||||
cCtmPUkjyr18QAlrcCB4PozJGjpPWyabaY8gGwo8wAEpJWHrIJlHew==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,17 +0,0 @@
|
||||
#! /usr/bin/env sh
|
||||
set -e
|
||||
|
||||
# If there's a prestart.sh script in the /app directory, run it before starting
|
||||
PRE_START_PATH=deploy/docker/prestart.sh
|
||||
echo "Checking for script in $PRE_START_PATH"
|
||||
if [ -f $PRE_START_PATH ] ; then
|
||||
echo "Running script $PRE_START_PATH"
|
||||
. $PRE_START_PATH
|
||||
else
|
||||
echo "There is no script $PRE_START_PATH"
|
||||
fi
|
||||
|
||||
# Start Supervisor, with Nginx and uWSGI
|
||||
echo "Starting server using supervisord..."
|
||||
|
||||
exec /usr/bin/supervisord
|
||||
@@ -1,12 +0,0 @@
|
||||
[program:celery_beat]
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
startsecs=0
|
||||
numprocs=1
|
||||
user=www-data
|
||||
directory=/home/mediacms.io/mediacms
|
||||
priority=300
|
||||
startinorder=true
|
||||
command=/home/mediacms.io/bin/celery beat --pidfile=/var/run/mediacms/beat%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_beat.log
|
||||
@@ -1,13 +0,0 @@
|
||||
[program:celery_long]
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
startsecs=10
|
||||
numprocs=1
|
||||
user=www-data
|
||||
directory=/home/mediacms.io/mediacms
|
||||
priority=500
|
||||
startinorder=true
|
||||
startsecs=0
|
||||
command=/home/mediacms.io/bin/celery multi start long1 --pidfile=/var/run/mediacms/%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_long.log -Ofair --prefetch-multiplier=1 -Q long_tasks
|
||||
@@ -1,12 +0,0 @@
|
||||
[program:celery_short]
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
startsecs=0
|
||||
numprocs=1
|
||||
user=www-data
|
||||
directory=/home/mediacms.io/mediacms
|
||||
priority=400
|
||||
startinorder=true
|
||||
command=/home/mediacms.io/bin/celery multi start short1 short2 --pidfile=/var/run/mediacms/%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_short.log --soft-time-limit=300 -c10 -Q short_tasks
|
||||
@@ -1,2 +0,0 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
@@ -1,11 +0,0 @@
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx -g 'daemon off;'
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
priority=200
|
||||
startinorder=true
|
||||
startsecs=0
|
||||
# Graceful stop, see http://nginx.org/en/docs/control.html
|
||||
stopsignal=QUIT
|
||||
@@ -1,9 +0,0 @@
|
||||
[program:uwsgi]
|
||||
command=/home/mediacms.io/bin/uwsgi --ini /home/mediacms.io/mediacms/deploy/docker/uwsgi.ini
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
priority=100
|
||||
startinorder=true
|
||||
startsecs=0
|
||||
@@ -8,15 +8,13 @@ User=www-data
|
||||
Group=www-data
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
Environment=APP_DIR="/home/mediacms.io/mediacms"
|
||||
WorkingDirectory=/home/mediacms.io/mediacms
|
||||
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
|
||||
Environment=CELERY_APP="cms"
|
||||
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/beat%n.pid"
|
||||
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/beat%N.log"
|
||||
Environment=CELERYD_LOG_LEVEL="INFO"
|
||||
Environment=APP_DIR="/home/mediacms.io/mediacms"
|
||||
|
||||
ExecStart=/bin/sh -c '${CELERY_BIN} beat -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} --workdir=${APP_DIR}'
|
||||
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms beat --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
|
||||
ExecStop=/bin/kill -s TERM $MAINPID
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -8,23 +8,21 @@ User=www-data
|
||||
Group=www-data
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
Environment=APP_DIR="/home/mediacms.io/mediacms"
|
||||
WorkingDirectory=/home/mediacms.io/mediacms
|
||||
Environment=CELERYD_NODES="long1"
|
||||
Environment=CELERY_QUEUE="long_tasks"
|
||||
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
|
||||
Environment=CELERY_APP="cms"
|
||||
Environment=CELERYD_MULTI="multi"
|
||||
Environment=CELERYD_OPTS="-Ofair --prefetch-multiplier=1"
|
||||
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/%n.pid"
|
||||
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/%N.log"
|
||||
Environment=CELERYD_LOG_LEVEL="INFO"
|
||||
Environment=APP_DIR="/home/mediacms.io/mediacms"
|
||||
|
||||
ExecStart=/bin/sh -c '${CELERY_BIN} multi start ${CELERYD_NODES} -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} --workdir=${APP_DIR} -Q ${CELERY_QUEUE}'
|
||||
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms multi start ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
|
||||
|
||||
ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
|
||||
ExecStop=/bin/sh -c '${CELERY_BIN} -A cms multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
|
||||
|
||||
ExecReload=/bin/sh -c '${CELERY_BIN} multi restart ${CELERYD_NODES} -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} --workdir=${APP_DIR} -Q ${CELERY_QUEUE}'
|
||||
ExecReload=/bin/sh -c '${CELERY_BIN} -A cms multi restart ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -8,14 +8,13 @@ User=www-data
|
||||
Group=www-data
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
Environment=APP_DIR="/home/mediacms.io/mediacms"
|
||||
WorkingDirectory=/home/mediacms.io/mediacms
|
||||
Environment=CELERYD_NODES="short1 short2"
|
||||
Environment=CELERY_QUEUE="short_tasks"
|
||||
# Absolute or relative path to the 'celery' command:
|
||||
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
|
||||
# App instance to use
|
||||
# comment out this line if you don't use an app
|
||||
Environment=CELERY_APP="cms"
|
||||
# or fully qualified:
|
||||
#CELERY_APP="proj.tasks:app"
|
||||
# How to call manage.py
|
||||
@@ -28,13 +27,12 @@ Environment=CELERYD_OPTS="--soft-time-limit=300 -c10"
|
||||
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/%n.pid"
|
||||
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/%N.log"
|
||||
Environment=CELERYD_LOG_LEVEL="INFO"
|
||||
Environment=APP_DIR="/home/mediacms.io/mediacms"
|
||||
|
||||
ExecStart=/bin/sh -c '${CELERY_BIN} multi start ${CELERYD_NODES} -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} --workdir=${APP_DIR} -Q ${CELERY_QUEUE}'
|
||||
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms multi start ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
|
||||
|
||||
ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
|
||||
ExecStop=/bin/sh -c '${CELERY_BIN} -A cms multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
|
||||
|
||||
ExecReload=/bin/sh -c '${CELERY_BIN} multi restart ${CELERYD_NODES} -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} --workdir=${APP_DIR} -Q ${CELERY_QUEUE}'
|
||||
ExecReload=/bin/sh -c '${CELERY_BIN} -A cms multi restart ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -49,7 +49,7 @@ server {
|
||||
ssl_dhparam /etc/nginx/dhparams/dhparams.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_ecdh_curve secp521r1:secp384r1;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
|
||||
34
deploy/local_install/selinux-mediacms.te
Normal file
34
deploy/local_install/selinux-mediacms.te
Normal file
@@ -0,0 +1,34 @@
|
||||
module selinux-mediacms 1.0;
|
||||
|
||||
require {
|
||||
type init_t;
|
||||
type var_t;
|
||||
type redis_port_t;
|
||||
type postgresql_port_t;
|
||||
type httpd_t;
|
||||
type httpd_sys_content_t;
|
||||
type httpd_sys_rw_content_t;
|
||||
class file { append create execute execute_no_trans getattr ioctl lock open read rename setattr unlink write };
|
||||
class dir { add_name remove_name rmdir };
|
||||
class tcp_socket name_connect;
|
||||
class lnk_file read;
|
||||
}
|
||||
|
||||
#============= httpd_t ==============
|
||||
|
||||
allow httpd_t var_t:file { getattr open read };
|
||||
|
||||
#============= init_t ==============
|
||||
allow init_t postgresql_port_t:tcp_socket name_connect;
|
||||
|
||||
allow init_t redis_port_t:tcp_socket name_connect;
|
||||
|
||||
allow init_t httpd_sys_content_t:dir rmdir;
|
||||
|
||||
allow init_t httpd_sys_content_t:file { append create execute execute_no_trans ioctl lock open read rename setattr unlink write };
|
||||
|
||||
allow init_t httpd_sys_content_t:lnk_file read;
|
||||
|
||||
allow init_t httpd_sys_rw_content_t:dir { add_name remove_name rmdir };
|
||||
|
||||
allow init_t httpd_sys_rw_content_t:file { create ioctl lock open read setattr unlink write };
|
||||
@@ -24,4 +24,4 @@ vacuum = true
|
||||
logto = /home/mediacms.io/mediacms/logs/errorlog.txt
|
||||
|
||||
disable-logging = true
|
||||
|
||||
buffer-size=32768
|
||||
|
||||
40
deploy/scripts/build_and_deploy.sh
Normal file
40
deploy/scripts/build_and_deploy.sh
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# This script builds the video editor package and deploys the frontend assets to the static directory.
|
||||
# How to run: sh deploy/scripts/build_and_deploy.sh
|
||||
|
||||
# Exit on any error
|
||||
set -e
|
||||
|
||||
echo "Starting build process..."
|
||||
|
||||
# Build video editor package
|
||||
echo "Building video editor package..."
|
||||
cd frontend-tools/video-editor
|
||||
yarn build:django
|
||||
cd ../../
|
||||
|
||||
# Build chapter editor package
|
||||
echo "Building chapters editor package..."
|
||||
cd frontend-tools/chapters-editor
|
||||
yarn build:django
|
||||
cd ../../
|
||||
|
||||
# Build video js package
|
||||
echo "Building video js package..."
|
||||
cd frontend-tools/video-js
|
||||
yarn build:django
|
||||
cd ../../
|
||||
|
||||
# Run npm build in the frontend container
|
||||
echo "Building frontend assets..."
|
||||
docker compose -f docker-compose/docker-compose-dev-updated.yaml exec frontend npm run dist
|
||||
|
||||
# Copy static assets to the static directory
|
||||
echo "Copying static assets..."
|
||||
cp -r frontend/dist/static/* static/
|
||||
|
||||
# Restart the web service
|
||||
echo "Restarting web service..."
|
||||
docker compose -f docker-compose/docker-compose-dev-updated.yaml restart web
|
||||
|
||||
echo "Build and deployment completed successfully!"
|
||||
62
docker-compose-cert.yaml
Normal file
62
docker-compose-cert.yaml
Normal file
@@ -0,0 +1,62 @@
|
||||
version: "3.8"
|
||||
|
||||
# HTTPS/SSL certificate overlay for docker-compose.yaml
|
||||
# Uses nginx-proxy with Let's Encrypt via acme-companion
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yaml -f docker-compose-cert.yaml up -d
|
||||
#
|
||||
# Before running:
|
||||
# 1. Change VIRTUAL_HOST to your domain
|
||||
# 2. Change LETSENCRYPT_HOST to your domain
|
||||
# 3. Change LETSENCRYPT_EMAIL to your email
|
||||
|
||||
services:
|
||||
# Reverse proxy with automatic SSL
|
||||
nginx-proxy:
|
||||
image: nginxproxy/nginx-proxy
|
||||
container_name: nginx-proxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- conf:/etc/nginx/conf.d
|
||||
- vhost:/etc/nginx/vhost.d
|
||||
- html:/usr/share/nginx/html
|
||||
- dhparam:/etc/nginx/dhparam
|
||||
- certs:/etc/nginx/certs:ro
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
- ./config/nginx-proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
|
||||
|
||||
# Let's Encrypt certificate manager
|
||||
acme-companion:
|
||||
image: nginxproxy/acme-companion
|
||||
container_name: nginx-proxy-acme
|
||||
restart: unless-stopped
|
||||
volumes_from:
|
||||
- nginx-proxy
|
||||
volumes:
|
||||
- certs:/etc/nginx/certs:rw
|
||||
- acme:/etc/acme.sh
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
# Override nginx to work with nginx-proxy
|
||||
nginx:
|
||||
expose:
|
||||
- "80"
|
||||
ports: [] # Remove ports, nginx-proxy handles external access
|
||||
environment:
|
||||
# CHANGE THESE VALUES:
|
||||
VIRTUAL_HOST: 'mediacms.example.com'
|
||||
LETSENCRYPT_HOST: 'mediacms.example.com'
|
||||
LETSENCRYPT_EMAIL: 'admin@example.com'
|
||||
|
||||
volumes:
|
||||
# nginx-proxy volumes
|
||||
conf:
|
||||
vhost:
|
||||
html:
|
||||
dhparam:
|
||||
certs:
|
||||
acme:
|
||||
@@ -1,55 +1,136 @@
|
||||
version: "3"
|
||||
version: "3.8"
|
||||
|
||||
# Development setup with hot-reload and file mounts
|
||||
# This is the ONLY compose file that mounts the source code
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: node:14
|
||||
volumes:
|
||||
- ${PWD}/frontend:/home/mediacms.io/mediacms/frontend/
|
||||
working_dir: /home/mediacms.io/mediacms/frontend/
|
||||
command: bash -c "npm install && npm run start"
|
||||
env_file:
|
||||
- ${PWD}/frontend/.env
|
||||
ports:
|
||||
- "8088:8088"
|
||||
depends_on:
|
||||
- web
|
||||
web:
|
||||
migrations:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-dev
|
||||
image: mediacms/mediacms-dev:latest
|
||||
dockerfile: ./Dockerfile
|
||||
target: base
|
||||
args:
|
||||
- DEVELOPMENT_MODE=True
|
||||
image: mediacms/mediacms-dev:7.3
|
||||
command: ["/bin/bash", "/home/mediacms.io/mediacms/scripts/run-migrations.sh"]
|
||||
environment:
|
||||
DEVELOPMENT_MODE: 'True'
|
||||
DEBUG: 'True'
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_PASSWORD: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
ADMIN_PASSWORD: 'admin'
|
||||
restart: "no"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
db:
|
||||
image: postgres:13
|
||||
volumes:
|
||||
- ../postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
|
||||
web:
|
||||
image: mediacms/mediacms-dev:7.3
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:8000"
|
||||
command: ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||
environment:
|
||||
DEVELOPMENT_MODE: 'True'
|
||||
DEBUG: 'True'
|
||||
depends_on:
|
||||
migrations:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
|
||||
frontend:
|
||||
image: node:20-alpine
|
||||
working_dir: /home/mediacms.io/mediacms/frontend/
|
||||
command: sh -c "npm install && npm run start"
|
||||
ports:
|
||||
- "8088:8088"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
env_file:
|
||||
- ./frontend/.env
|
||||
volumes:
|
||||
- ./frontend:/home/mediacms.io/mediacms/frontend/
|
||||
depends_on:
|
||||
- web
|
||||
|
||||
celery_beat:
|
||||
image: mediacms/mediacms-dev:7.3
|
||||
restart: unless-stopped
|
||||
command: ["/home/mediacms.io/bin/celery", "-A", "cms", "beat", "--loglevel=INFO"]
|
||||
environment:
|
||||
DEVELOPMENT_MODE: 'True'
|
||||
DEBUG: 'True'
|
||||
depends_on:
|
||||
migrations:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
|
||||
celery_short:
|
||||
image: mediacms/mediacms-dev:7.3
|
||||
restart: unless-stopped
|
||||
command: ["/home/mediacms.io/bin/celery", "-A", "cms", "worker", "-Q", "short_tasks", "-c", "10", "--soft-time-limit=300", "--loglevel=INFO", "-n", "short@%h"]
|
||||
environment:
|
||||
DEVELOPMENT_MODE: 'True'
|
||||
DEBUG: 'True'
|
||||
depends_on:
|
||||
migrations:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
|
||||
celery_long:
|
||||
image: mediacms/mediacms-dev:7.3
|
||||
restart: unless-stopped
|
||||
command: ["/home/mediacms.io/bin/celery", "-A", "cms", "worker", "-Q", "long_tasks", "-c", "1", "-Ofair", "--prefetch-multiplier=1", "--loglevel=INFO", "-n", "long@%h"]
|
||||
environment:
|
||||
DEVELOPMENT_MODE: 'True'
|
||||
DEBUG: 'True'
|
||||
depends_on:
|
||||
migrations:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
|
||||
db:
|
||||
image: postgres:17.2-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: mediacms
|
||||
POSTGRES_PASSWORD: mediacms
|
||||
POSTGRES_DB: mediacms
|
||||
TZ: Europe/London
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U mediacms"]
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: "redis:alpine"
|
||||
restart: always
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
5
docker-compose.full.yaml
Normal file
5
docker-compose.full.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
celery_worker:
|
||||
image: mediacms/mediacms:full
|
||||
@@ -1,85 +1,126 @@
|
||||
version: "3"
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
migrations:
|
||||
image: mediacms/mediacms:latest
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
image: mediacms/mediacms:7.3
|
||||
command: ["/bin/bash", "/home/mediacms.io/mediacms/scripts/run-migrations.sh"]
|
||||
environment:
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
#ADMIN_PASSWORD: 'uncomment_and_set_password_here'
|
||||
command: "./deploy/docker/prestart.sh"
|
||||
restart: on-failure
|
||||
ADMIN_PASSWORD: # ADMIN_PASSWORD: 'uncomment_and_set_password_here'
|
||||
restart: "no"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./custom:/home/mediacms.io/mediacms/custom:ro
|
||||
- static_files:/home/mediacms.io/mediacms/static
|
||||
- media_files:/home/mediacms.io/mediacms/media_files
|
||||
- logs:/home/mediacms.io/mediacms/logs
|
||||
|
||||
web:
|
||||
image: mediacms/mediacms:latest
|
||||
deploy:
|
||||
replicas: 1
|
||||
image: mediacms/mediacms:7.3
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "9000"
|
||||
depends_on:
|
||||
migrations:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./custom:/home/mediacms.io/mediacms/custom:ro
|
||||
- static_files:/home/mediacms.io/mediacms/static
|
||||
- media_files:/home/mediacms.io/mediacms/media_files
|
||||
- logs:/home/mediacms.io/mediacms/logs
|
||||
|
||||
nginx:
|
||||
image: mediacms/mediacms-nginx:7.3
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
depends_on:
|
||||
- migrations
|
||||
- web
|
||||
volumes:
|
||||
- ./custom/static:/var/www/custom:ro
|
||||
- static_files:/var/www/static:ro
|
||||
- media_files:/var/www/media:ro
|
||||
- logs:/var/log/mediacms
|
||||
|
||||
celery_beat:
|
||||
image: mediacms/mediacms:latest
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
image: mediacms/mediacms-worker:7.3
|
||||
restart: unless-stopped
|
||||
command: ["/home/mediacms.io/bin/celery", "-A", "cms", "beat", "--loglevel=INFO", "--schedule=/home/mediacms.io/mediacms/logs/celerybeat-schedule"]
|
||||
depends_on:
|
||||
- redis
|
||||
celery_worker:
|
||||
image: mediacms/mediacms:latest
|
||||
deploy:
|
||||
replicas: 1
|
||||
migrations:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
- ./custom:/home/mediacms.io/mediacms/custom:ro
|
||||
- media_files:/home/mediacms.io/mediacms/media_files
|
||||
- logs:/home/mediacms.io/mediacms/logs
|
||||
|
||||
celery_short:
|
||||
image: mediacms/mediacms-worker:7.3
|
||||
restart: unless-stopped
|
||||
command: ["/home/mediacms.io/bin/celery", "-A", "cms", "worker", "-Q", "short_tasks", "-c", "10", "--soft-time-limit=300", "--loglevel=INFO", "-n", "short@%h"]
|
||||
depends_on:
|
||||
- migrations
|
||||
migrations:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./custom:/home/mediacms.io/mediacms/custom:ro
|
||||
- media_files:/home/mediacms.io/mediacms/media_files
|
||||
- logs:/home/mediacms.io/mediacms/logs
|
||||
|
||||
celery_long:
|
||||
image: mediacms/mediacms-worker:7.3
|
||||
# To use extra codecs, change image to: mediacms/mediacms-worker:7.3-full
|
||||
restart: unless-stopped
|
||||
command: ["/home/mediacms.io/bin/celery", "-A", "cms", "worker", "-Q", "long_tasks", "-c", "1", "-Ofair", "--prefetch-multiplier=1", "--loglevel=INFO", "-n", "long@%h"]
|
||||
depends_on:
|
||||
migrations:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./custom:/home/mediacms.io/mediacms/custom:ro
|
||||
- media_files:/home/mediacms.io/mediacms/media_files
|
||||
- logs:/home/mediacms.io/mediacms/logs
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
volumes:
|
||||
- ../postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
image: postgres:17.2-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: mediacms
|
||||
POSTGRES_PASSWORD: mediacms
|
||||
POSTGRES_DB: mediacms
|
||||
TZ: Europe/London
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U mediacms"]
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: "redis:alpine"
|
||||
restart: always
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli","ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
static_files:
|
||||
media_files:
|
||||
logs:
|
||||
|
||||
124
docker-compose/docker-compose-dev-updated.yaml
Normal file
124
docker-compose/docker-compose-dev-updated.yaml
Normal file
@@ -0,0 +1,124 @@
|
||||
name: mediacms-dev
|
||||
services:
|
||||
migrations:
|
||||
platform: linux/amd64
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- DEVELOPMENT_MODE=True
|
||||
image: mediacms/mediacms:latest
|
||||
volumes:
|
||||
- ../:/home/mediacms.io/mediacms/
|
||||
command: "/home/mediacms.io/mediacms/deploy/docker/prestart.sh"
|
||||
environment:
|
||||
DEVELOPMENT_MODE: True
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_SHORT: 'no'
|
||||
ENABLE_CELERY_LONG: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ADMIN_USER: 'admin'
|
||||
ADMIN_EMAIL: 'admin@localhost'
|
||||
ADMIN_PASSWORD: 'admin'
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
frontend:
|
||||
image: node:20
|
||||
user: "root"
|
||||
volumes:
|
||||
- ${PWD}/frontend:/home/mediacms.io/mediacms/frontend/
|
||||
- frontend_node_modules:/home/mediacms.io/mediacms/frontend/node_modules
|
||||
- scripts_node_modules:/home/mediacms.io/mediacms/frontend/packages/scripts/node_modules
|
||||
- npm_cache:/home/node/.npm
|
||||
working_dir: /home/mediacms.io/mediacms/frontend/
|
||||
command: >
|
||||
bash -c "
|
||||
echo 'Checking dependencies...' &&
|
||||
if [ ! -f node_modules/.install-complete ]; then
|
||||
echo 'First-time setup or dependencies changed, installing...' &&
|
||||
npm install --legacy-peer-deps --cache /home/node/.npm &&
|
||||
cd packages/scripts &&
|
||||
npm install --legacy-peer-deps --cache /home/node/.npm &&
|
||||
npm run build &&
|
||||
cd ../.. &&
|
||||
touch node_modules/.install-complete &&
|
||||
echo 'Dependencies installed successfully'
|
||||
else
|
||||
echo 'Dependencies already installed, skipping installation...' &&
|
||||
if [ ! -d packages/scripts/dist ]; then
|
||||
echo 'Building scripts package...' &&
|
||||
cd packages/scripts &&
|
||||
npm run build &&
|
||||
cd ../..
|
||||
fi
|
||||
fi &&
|
||||
echo 'Starting development server...' &&
|
||||
npm run start
|
||||
"
|
||||
env_file:
|
||||
- ${PWD}/frontend/.env
|
||||
ports:
|
||||
- "8088:8088"
|
||||
depends_on:
|
||||
- web
|
||||
restart: unless-stopped
|
||||
web:
|
||||
platform: linux/amd64
|
||||
image: mediacms/mediacms:latest
|
||||
command: "python manage.py runserver 0.0.0.0:80"
|
||||
environment:
|
||||
DEVELOPMENT_MODE: True
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ../:/home/mediacms.io/mediacms/
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres:17.2-alpine
|
||||
volumes:
|
||||
- ../postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: mediacms
|
||||
POSTGRES_PASSWORD: mediacms
|
||||
POSTGRES_DB: mediacms
|
||||
TZ: Europe/London
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
redis:
|
||||
image: "redis:alpine"
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
celery_worker:
|
||||
platform: linux/amd64
|
||||
image: mediacms/mediacms:latest
|
||||
deploy:
|
||||
replicas: 1
|
||||
volumes:
|
||||
- ../:/home/mediacms.io/mediacms/
|
||||
environment:
|
||||
ENABLE_UWSGI: 'no'
|
||||
ENABLE_NGINX: 'no'
|
||||
ENABLE_CELERY_BEAT: 'no'
|
||||
ENABLE_MIGRATIONS: 'no'
|
||||
DEVELOPMENT_MODE: True
|
||||
depends_on:
|
||||
- web
|
||||
|
||||
volumes:
|
||||
frontend_node_modules:
|
||||
scripts_node_modules:
|
||||
npm_cache:
|
||||
@@ -68,7 +68,7 @@ services:
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres:13
|
||||
image: postgres:17.2-alpine
|
||||
volumes:
|
||||
- ../postgres_data/:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
@@ -76,8 +76,9 @@ services:
|
||||
POSTGRES_USER: mediacms
|
||||
POSTGRES_PASSWORD: mediacms
|
||||
POSTGRES_DB: mediacms
|
||||
TZ: Europe/London
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U mediacms"]
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres:13
|
||||
image: postgres:17.2-alpine
|
||||
volumes:
|
||||
- ../postgres_data/:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
@@ -78,8 +78,9 @@ services:
|
||||
POSTGRES_USER: mediacms
|
||||
POSTGRES_PASSWORD: mediacms
|
||||
POSTGRES_DB: mediacms
|
||||
TZ: Europe/London
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U mediacms"]
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -90,7 +90,7 @@ services:
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres:13
|
||||
image: postgres:17.2-alpine
|
||||
volumes:
|
||||
- ../postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
@@ -98,8 +98,9 @@ services:
|
||||
POSTGRES_USER: mediacms
|
||||
POSTGRES_PASSWORD: mediacms
|
||||
POSTGRES_DB: mediacms
|
||||
TZ: Europe/London
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U mediacms"]
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres:13
|
||||
image: postgres:17.2-alpine
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
@@ -74,8 +74,9 @@ services:
|
||||
POSTGRES_USER: mediacms
|
||||
POSTGRES_PASSWORD: mediacms
|
||||
POSTGRES_DB: mediacms
|
||||
TZ: Europe/London
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U mediacms"]
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
367
docs/DOCKER_V7.3_MIGRATION.md
Normal file
367
docs/DOCKER_V7.3_MIGRATION.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# MediaCMS 7.3 Docker Architecture Migration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
MediaCMS 7.3 introduces a modernized Docker architecture that removes supervisord and implements Docker best practices with one process per container.
|
||||
|
||||
## What Changed
|
||||
|
||||
### Old Architecture (pre-7.3)
|
||||
- Single multi-purpose image with supervisord
|
||||
- Environment variables (`ENABLE_UWSGI`, `ENABLE_NGINX`, etc.) to control services
|
||||
- All services bundled in `deploy/docker/` folder
|
||||
- File mounts required for all deployments
|
||||
|
||||
### New Architecture (7.3+)
|
||||
- **Dedicated images** for each service:
|
||||
- `mediacms/mediacms:7.3` - Django/uWSGI application
|
||||
- `mediacms/mediacms-worker:7.3` - Celery workers
|
||||
- `mediacms/mediacms-worker:7.3-full` - Celery workers with extra codecs
|
||||
- `mediacms/mediacms-nginx:7.3` - Nginx web server
|
||||
- **No supervisord** - Native Docker process management
|
||||
- **Separated services**:
|
||||
- `migrations` - Runs database migrations on every startup
|
||||
- `nginx` - Serves static/media files and proxies to Django
|
||||
- `web` - Django application (uWSGI)
|
||||
- `celery_short` - Short-running tasks (thumbnails, etc.)
|
||||
- `celery_long` - Long-running tasks (video encoding)
|
||||
- `celery_beat` - Task scheduler
|
||||
- **No ENABLE_* environment variables**
|
||||
- **Config centralized** in `config/` directory
|
||||
- **File mounts only for development** (`docker-compose-dev.yaml`)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
config/
|
||||
├── nginx/
|
||||
│ ├── nginx.conf # Main nginx config
|
||||
│ ├── site.conf # Virtual host config
|
||||
│ └── uwsgi_params # uWSGI parameters
|
||||
├── nginx-proxy/
|
||||
│ └── client_max_body_size.conf # For production HTTPS proxy
|
||||
├── uwsgi/
|
||||
│ └── uwsgi.ini # uWSGI configuration
|
||||
└── imagemagick/
|
||||
└── policy.xml # ImageMagick policy
|
||||
|
||||
scripts/
|
||||
├── entrypoint-web.sh # Web container entrypoint
|
||||
├── entrypoint-worker.sh # Worker container entrypoint
|
||||
└── run-migrations.sh # Migration script
|
||||
|
||||
Dockerfile.new # Main Dockerfile (base, web, worker, worker-full)
|
||||
Dockerfile.nginx # Nginx Dockerfile
|
||||
docker-compose.yaml # Production deployment
|
||||
docker-compose-cert.yaml # Production with HTTPS
|
||||
docker-compose-dev.yaml # Development with file mounts
|
||||
```
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### For Existing Production Systems
|
||||
|
||||
#### Step 1: Backup your data
|
||||
```bash
|
||||
# Backup database
|
||||
docker exec mediacms_db_1 pg_dump -U mediacms mediacms > backup.sql
|
||||
|
||||
# Backup media files
|
||||
cp -r media_files media_files.backup
|
||||
```
|
||||
|
||||
#### Step 2: Update configuration location
|
||||
```bash
|
||||
# The client_max_body_size.conf has moved
|
||||
# No action needed if you haven't customized it
|
||||
```
|
||||
|
||||
#### Step 3: Pull latest images
|
||||
```bash
|
||||
docker pull mediacms/mediacms:7.3
|
||||
docker pull mediacms/mediacms-worker:7.3
|
||||
docker pull mediacms/mediacms-nginx:7.3
|
||||
```
|
||||
|
||||
#### Step 4: Update docker-compose file
|
||||
If using **docker-compose.yaml**:
|
||||
- No changes needed, just use the new version
|
||||
|
||||
If using **docker-compose-cert.yaml** (HTTPS):
|
||||
- Update `VIRTUAL_HOST`, `LETSENCRYPT_HOST`, and `LETSENCRYPT_EMAIL` in the nginx service
|
||||
- Update the path to client_max_body_size.conf:
|
||||
```yaml
|
||||
- ./config/nginx-proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
|
||||
```
|
||||
|
||||
#### Step 5: Restart services
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### For Development Systems
|
||||
|
||||
Development now requires the `-dev` compose file:
|
||||
|
||||
```bash
|
||||
# Old way (no longer works)
|
||||
docker compose up
|
||||
|
||||
# New way (development)
|
||||
docker compose -f docker-compose-dev.yaml up
|
||||
```
|
||||
|
||||
## Deployment Options
|
||||
|
||||
### Standard Deployment (HTTP)
|
||||
|
||||
**File**: `docker-compose.yaml`
|
||||
|
||||
**Command**:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Self-contained images (no file mounts)
|
||||
- Nginx serves on port 80
|
||||
- Separate containers for each service
|
||||
- Named volumes for persistence
|
||||
|
||||
**Architecture**:
|
||||
```
|
||||
Client → nginx:80 → web:9000 (uWSGI)
|
||||
↓
|
||||
static_files (volume)
|
||||
media_files (volume)
|
||||
```
|
||||
|
||||
### Production Deployment (HTTPS with Let's Encrypt)
|
||||
|
||||
**File**: `docker-compose-cert.yaml`
|
||||
|
||||
**Prerequisites**:
|
||||
1. Domain name pointing to your server
|
||||
2. Ports 80 and 443 open
|
||||
|
||||
**Setup**:
|
||||
```bash
|
||||
# 1. Edit docker-compose-cert.yaml
|
||||
# Update these values in the nginx service:
|
||||
# VIRTUAL_HOST: 'your-domain.com'
|
||||
# LETSENCRYPT_HOST: 'your-domain.com'
|
||||
# LETSENCRYPT_EMAIL: 'your-email@example.com'
|
||||
|
||||
# 2. Start services
|
||||
docker compose -f docker-compose-cert.yaml up -d
|
||||
|
||||
# 3. Check logs
|
||||
docker compose -f docker-compose-cert.yaml logs -f nginx-proxy acme-companion
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Automatic HTTPS via Let's Encrypt
|
||||
- Certificate auto-renewal
|
||||
- Reverse proxy handles SSL termination
|
||||
|
||||
**Architecture**:
|
||||
```
|
||||
Client → nginx-proxy:443 (HTTPS) → nginx:80 → web:9000 (uWSGI)
|
||||
```
|
||||
|
||||
### Development Deployment
|
||||
|
||||
**File**: `docker-compose-dev.yaml`
|
||||
|
||||
**Command**:
|
||||
```bash
|
||||
docker compose -f docker-compose-dev.yaml up
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Source code mounted for live editing
|
||||
- Django debug mode enabled
|
||||
- Django's `runserver` instead of uWSGI
|
||||
- Frontend hot-reload on port 8088
|
||||
- No nginx (direct Django access on port 80)
|
||||
|
||||
**Ports**:
|
||||
- `80` - Django API
|
||||
- `8088` - Frontend dev server
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
All configuration is done via environment variables or `cms/local_settings.py`.
|
||||
|
||||
**Key Variables**:
|
||||
- `FRONTEND_HOST` - Your domain (e.g., `https://mediacms.example.com`)
|
||||
- `PORTAL_NAME` - Your portal name
|
||||
- `SECRET_KEY` - Django secret key
|
||||
- `POSTGRES_*` - Database credentials
|
||||
- `REDIS_LOCATION` - Redis connection string
|
||||
- `DEBUG` - Enable debug mode (development only)
|
||||
|
||||
**Setting variables**:
|
||||
|
||||
Option 1: In docker-compose file:
|
||||
```yaml
|
||||
environment:
|
||||
FRONTEND_HOST: 'https://mediacms.example.com'
|
||||
PORTAL_NAME: 'My MediaCMS'
|
||||
```
|
||||
|
||||
Option 2: Using .env file (recommended):
|
||||
```bash
|
||||
# Create .env file
|
||||
cat > .env << EOF
|
||||
FRONTEND_HOST=https://mediacms.example.com
|
||||
PORTAL_NAME=My MediaCMS
|
||||
SECRET_KEY=your-secret-key-here
|
||||
EOF
|
||||
```
|
||||
|
||||
### Customizing Settings
|
||||
|
||||
For advanced customization, you can build a custom image:
|
||||
|
||||
```dockerfile
|
||||
# Dockerfile.custom
|
||||
FROM mediacms/mediacms:7.3
|
||||
COPY my_local_settings.py /home/mediacms.io/mediacms/cms/local_settings.py
|
||||
```
|
||||
|
||||
## Celery Workers
|
||||
|
||||
### Standard Workers
|
||||
|
||||
By default, `celery_long` uses the standard image:
|
||||
```yaml
|
||||
celery_long:
|
||||
image: mediacms/mediacms-worker:7.3
|
||||
```
|
||||
|
||||
### Full Workers (Extra Codecs)
|
||||
|
||||
To enable extra codecs for better transcoding (including Whisper for subtitles):
|
||||
|
||||
**Edit docker-compose file**:
|
||||
```yaml
|
||||
celery_long:
|
||||
image: mediacms/mediacms-worker:7.3-full # Changed from :7.3
|
||||
```
|
||||
|
||||
**Then restart**:
|
||||
```bash
|
||||
docker compose up -d celery_long
|
||||
```
|
||||
|
||||
### Scaling Workers
|
||||
|
||||
You can scale workers independently:
|
||||
|
||||
```bash
|
||||
# Scale short task workers
|
||||
docker compose up -d --scale celery_short=3
|
||||
|
||||
# Scale long task workers
|
||||
docker compose up -d --scale celery_long=2
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Migrations not running
|
||||
```bash
|
||||
# Check migrations container logs
|
||||
docker compose logs migrations
|
||||
|
||||
# Manually run migrations
|
||||
docker compose run --rm migrations
|
||||
```
|
||||
|
||||
### Static files not loading
|
||||
```bash
|
||||
# Ensure migrations completed (it runs collectstatic)
|
||||
docker compose logs migrations
|
||||
|
||||
# Check nginx can access volumes
|
||||
docker compose exec nginx ls -la /var/www/static
|
||||
```
|
||||
|
||||
### Permission issues
|
||||
```bash
|
||||
# Check volume ownership
|
||||
docker compose exec web ls -la /home/mediacms.io/mediacms/media_files
|
||||
|
||||
# If needed, rebuild images
|
||||
docker compose build --no-cache
|
||||
```
|
||||
|
||||
### Celery workers not processing tasks
|
||||
```bash
|
||||
# Check worker logs
|
||||
docker compose logs celery_short celery_long
|
||||
|
||||
# Check Redis connection
|
||||
docker compose exec redis redis-cli ping
|
||||
|
||||
# Restart workers
|
||||
docker compose restart celery_short celery_long celery_beat
|
||||
```
|
||||
|
||||
## Removed Components
|
||||
|
||||
The following are **no longer used** in 7.3:
|
||||
|
||||
- ❌ `deploy/docker/supervisord/` - Supervisord configs
|
||||
- ❌ `deploy/docker/start.sh` - Start script
|
||||
- ❌ `deploy/docker/entrypoint.sh` - Old entrypoint
|
||||
- ❌ Environment variables: `ENABLE_UWSGI`, `ENABLE_NGINX`, `ENABLE_CELERY_BEAT`, `ENABLE_CELERY_SHORT`, `ENABLE_CELERY_LONG`, `ENABLE_MIGRATIONS`
|
||||
|
||||
**These are still available but moved**:
|
||||
- ✅ `config/nginx/` - Nginx configs (moved from `deploy/docker/`)
|
||||
- ✅ `config/uwsgi/` - uWSGI config (moved from `deploy/docker/`)
|
||||
- ✅ `config/nginx-proxy/` - Reverse proxy config (moved from `deploy/docker/reverse_proxy/`)
|
||||
|
||||
## Persistent Volumes
|
||||
|
||||
MediaCMS 7.3 uses Docker named volumes for data persistence:
|
||||
|
||||
- **`media_files`** - All uploaded media (videos, images, thumbnails, HLS streams)
|
||||
- Mounted on: migrations, web, nginx, celery_beat, celery_short, celery_long
|
||||
- Persists across container restarts, updates, and image removals
|
||||
|
||||
- **`logs`** - Application and nginx logs
|
||||
- Mounted on: migrations, web, nginx, celery_beat, celery_short, celery_long
|
||||
- Nginx logs: `/var/log/mediacms/nginx.access.log`, `/var/log/mediacms/nginx.error.log`
|
||||
- Django/Celery logs: `/home/mediacms.io/mediacms/logs/`
|
||||
- Persists across container restarts, updates, and image removals
|
||||
|
||||
- **`static_files`** - Django static files (CSS, JS, images)
|
||||
- Mounted on: migrations, web, nginx
|
||||
- Regenerated during migrations via `collectstatic`
|
||||
|
||||
- **`postgres_data`** - PostgreSQL database
|
||||
- Mounted on: db
|
||||
- Persists across container restarts, updates, and image removals
|
||||
|
||||
**Important**: Use `docker compose down -v` to remove volumes (⚠️ causes data loss!)
|
||||
|
||||
## Benefits of New Architecture
|
||||
|
||||
1. **Better resource management** - Scale services independently
|
||||
2. **Easier debugging** - Clear separation of concerns
|
||||
3. **Faster restarts** - Restart only affected services
|
||||
4. **Production-ready** - No file mounts, immutable images
|
||||
5. **Standard Docker practices** - One process per container
|
||||
6. **Clearer logs** - Each service has isolated logs, persistent storage
|
||||
7. **Better health checks** - Per-service monitoring
|
||||
8. **Data persistence** - media_files and logs survive all container operations
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- GitHub Issues: https://github.com/mediacms-io/mediacms/issues
|
||||
- Documentation: https://docs.mediacms.io
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Table of contents
|
||||
- [1. Welcome](#1-welcome)
|
||||
- [2. Server Installaton](#2-server-installation)
|
||||
- [2. Single Server Installaton](#2-single-server-installation)
|
||||
- [3. Docker Installation](#3-docker-installation)
|
||||
- [4. Docker Deployement options](#4-docker-deployment-options)
|
||||
- [4. Docker Deployment options](#4-docker-deployment-options)
|
||||
- [5. Configuration](#5-configuration)
|
||||
- [6. Manage pages](#6-manage-pages)
|
||||
- [7. Django admin dashboard](#7-django-admin-dashboard)
|
||||
@@ -17,19 +17,31 @@
|
||||
- [14. Add Google Analytics](#14-add-google-analytics)
|
||||
- [15. Debugging email issues](#15-debugging-email-issues)
|
||||
- [16. Frequently Asked Questions](#16-frequently-asked-questions)
|
||||
- [17. Cookie consent code](#17-cookie-consent-code)
|
||||
- [18. Disable encoding and show only original file](#18-disable-encoding-and-show-only-original-file)
|
||||
- [19. Rounded corners on videos](#19-rounded-corners)
|
||||
- [20. Translations](#20-translations)
|
||||
- [21. How to change the video frames on videos](#21-how-to-change-the-video-frames-on-videos)
|
||||
- [22. Role-Based Access Control](#22-role-based-access-control)
|
||||
- [23. SAML setup](#23-saml-setup)
|
||||
- [24. Identity Providers setup](#24-identity-providers-setup)
|
||||
- [25. Custom urls](#25-custom-urls)
|
||||
- [26. Allowed files](#26-allowed-files)
|
||||
- [27. User upload limits](#27-user-upload-limits)
|
||||
- [28. Whisper Transcribe for Automatic Subtitles](#28-whisper-transcribe-for-automatic-subtitles)
|
||||
|
||||
|
||||
## 1. Welcome
|
||||
This page is created for MediaCMS administrators that are responsible for setting up the software, maintaining it and making modifications.
|
||||
This page is created for MediaCMS administrators that are responsible for setting up the software, maintaining it and making modifications.
|
||||
|
||||
## 2. Server Installation
|
||||
## 2. Single Server Installation
|
||||
|
||||
The core dependencies are Python3, Django3, Celery, PostgreSQL, Redis, ffmpeg. Any system that can have these dependencies installed, can run MediaCMS. But we strongly suggest installing on Linux Ubuntu 18 or 20 versions.
|
||||
The core dependencies are python3, Django, celery, PostgreSQL, redis, ffmpeg. Any system that can have these dependencies installed, can run MediaCMS. But the install.sh is only tested in Linux Ubuntu 24 and 22 versions.
|
||||
|
||||
Installation on an Ubuntu 22/24 system with git utility installed should be completed in a few minutes with the following steps.
|
||||
Make sure you run it as user root, on a clear system, since the automatic script will install and configure the following services: Celery/PostgreSQL/Redis/Nginx and will override any existing settings.
|
||||
|
||||
Installation on a Ubuntu 18 or 20 system with git utility installed should be completed in a few minutes with the following steps.
|
||||
Make sure you run it as user root, on a clear system, since the automatic script will install and configure the following services: Celery/PostgreSQL/Redis/Nginx and will override any existing settings.
|
||||
|
||||
Automated script - tested on Ubuntu 18, Ubuntu 20, and Debian Buster
|
||||
|
||||
```bash
|
||||
mkdir /home/mediacms.io && cd /home/mediacms.io/
|
||||
@@ -37,7 +49,7 @@ git clone https://github.com/mediacms-io/mediacms
|
||||
cd /home/mediacms.io/mediacms/ && bash ./install.sh
|
||||
```
|
||||
|
||||
The script will ask if you have a URL where you want to deploy MediaCMS, otherwise it will use localhost. If you provide a URL, it will use Let's Encrypt service to install a valid ssl certificate.
|
||||
The script will ask if you have a URL where you want to deploy MediaCMS, otherwise it will use localhost. If you provide a URL, it will use Let's Encrypt service to install a valid ssl certificate.
|
||||
|
||||
|
||||
### Update
|
||||
@@ -48,10 +60,25 @@ If you've used the above way to install MediaCMS, update with the following:
|
||||
cd /home/mediacms.io/mediacms # enter mediacms directory
|
||||
source /home/mediacms.io/bin/activate # use virtualenv
|
||||
git pull # update code
|
||||
pip install -r requirements.txt -U # run pip install to update
|
||||
python manage.py migrate # run Django migrations
|
||||
sudo systemctl restart mediacms celery_long celery_short # restart services
|
||||
```
|
||||
|
||||
### Update from version 2 to version 3
|
||||
Version 3 is using Django 4 and Celery 5, and needs a recent Python 3.x version. If you are updating from an older version, make sure Python is updated first. Version 2 could run on Python 3.6, but version 3 needs Python3.8 and higher.
|
||||
The syntax for starting Celery has also changed, so you have to copy the celery related systemctl files and restart
|
||||
|
||||
```
|
||||
# cp deploy/local_install/celery_long.service /etc/systemd/system/celery_long.service
|
||||
# cp deploy/local_install/celery_short.service /etc/systemd/system/celery_short.service
|
||||
# cp deploy/local_install/celery_beat.service /etc/systemd/system/celery_beat.service
|
||||
# systemctl daemon-reload
|
||||
# systemctl start celery_long celery_short celery_beat
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Configuration
|
||||
Checkout the configuration section here.
|
||||
|
||||
@@ -65,13 +92,11 @@ Database can be backed up with pg_dump and media_files on /home/mediacms.io/medi
|
||||
## Installation
|
||||
Install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
For Ubuntu 18/20 systems this is:
|
||||
For Ubuntu systems this is:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
Then run as root
|
||||
@@ -87,7 +112,7 @@ If you want to explore more options (including setup of https with letsencrypt c
|
||||
Run
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
docker compose up
|
||||
```
|
||||
|
||||
This will download all MediaCMS related Docker images and start all containers. Once it finishes, MediaCMS will be installed and available on http://localhost or http://ip
|
||||
@@ -100,6 +125,12 @@ migrations_1 | Created admin user with password: gwg1clfkwf
|
||||
|
||||
or if you have set the ADMIN_PASSWORD variable on docker-compose file you have used (example `docker-compose.yaml`), that variable will be set as the admin user's password
|
||||
|
||||
`Note`: if you want to use the automatic transcriptions, you have to do one of the following:
|
||||
* either use the docker-compose.full.yaml, so in this case run `docker-compose -f docker-compose.yaml -f docker-compose.full.yaml up`
|
||||
* or edit the docker-compose.yaml file and set the image for the celery_worker service as mediacms/mediacms:full instead of mediacms/mediacms:latest
|
||||
|
||||
Plus set variable `USE_WHISPER_TRANSCRIBE = True` in the settings.py file
|
||||
|
||||
### Update
|
||||
|
||||
Get latest MediaCMS image and stop/start containers
|
||||
@@ -107,10 +138,22 @@ Get latest MediaCMS image and stop/start containers
|
||||
```bash
|
||||
cd /path/to/mediacms/installation
|
||||
docker pull mediacms/mediacms
|
||||
docker-compose down
|
||||
docker-compose up
|
||||
docker compose down
|
||||
docker compose up
|
||||
```
|
||||
|
||||
### Update from version 2 to version 3
|
||||
Version 3 is using Python 3.11 and PostgreSQL 15. If you are updating from an older version, that was using PostgreSQL 13, the automatic update will not work, as you will receive the following message when the PostgreSQL container starts:
|
||||
|
||||
```
|
||||
db_1 | 2023-06-27 11:07:42.959 UTC [1] FATAL: database files are incompatible with server
|
||||
db_1 | 2023-06-27 11:07:42.959 UTC [1] DETAIL: The data directory was initialized by PostgreSQL version 13, which is not compatible with this version 15.2.
|
||||
```
|
||||
|
||||
At this point there are two options: either edit the Docker Compose file and make use of the existing postgres:13 image, or otherwise you have to perform the migration from postgresql 13 to version 15. More notes on https://github.com/mediacms-io/mediacms/pull/749
|
||||
|
||||
|
||||
|
||||
## Configuration
|
||||
Checkout the configuration docs here.
|
||||
|
||||
@@ -121,55 +164,123 @@ Database is stored on ../postgres_data/ and media_files on media_files/
|
||||
|
||||
## 4. Docker Deployment options
|
||||
|
||||
The mediacms image is built to use supervisord as the main process, which manages one or more services required to run mediacms. We can toggle which services are run in a given container by setting the environment variables below to `yes` or `no`:
|
||||
**⚠️ IMPORTANT**: MediaCMS 7.3 introduces a new Docker architecture. If you're upgrading from an earlier version, please see the [Migration Guide](DOCKER_V7.3_MIGRATION.md).
|
||||
|
||||
* ENABLE_UWSGI
|
||||
* ENABLE_NGINX
|
||||
* ENABLE_CELERY_BEAT
|
||||
* ENABLE_CELERY_SHORT
|
||||
* ENABLE_CELERY_LONG
|
||||
* ENABLE_MIGRATIONS
|
||||
### Architecture Overview
|
||||
|
||||
By default, all these services are enabled, but in order to create a scaleable deployment, some of them can be disabled, splitting the service up into smaller services.
|
||||
MediaCMS 7.3+ uses a modern microservices architecture with dedicated containers:
|
||||
|
||||
Also see the `Dockerfile` for other environment variables which you may wish to override. Application settings, eg. `FRONTEND_HOST` can also be overridden by updating the `deploy/docker/local_settings.py` file.
|
||||
- **nginx** - Web server for static/media files and reverse proxy
|
||||
- **web** - Django application (uWSGI)
|
||||
- **celery_short** - Short-running background tasks
|
||||
- **celery_long** - Long-running tasks (video encoding)
|
||||
- **celery_beat** - Task scheduler
|
||||
- **migrations** - Database migrations (runs on startup)
|
||||
- **db** - PostgreSQL database
|
||||
- **redis** - Cache and message broker
|
||||
|
||||
See example deployments in the sections below. These example deployments have been tested on `docker-compose version 1.27.4` running on `Docker version 19.03.13`
|
||||
### Key Changes from Previous Versions
|
||||
|
||||
To run, update the configs above if necessary, build the image by running `docker-compose build`, then run `docker-compose run`
|
||||
- ✅ **No supervisord** - Native Docker process management
|
||||
- ✅ **Dedicated images** per service
|
||||
- ✅ **No ENABLE_* environment variables** - Services are separated into individual containers
|
||||
- ✅ **Production images** don't mount source code (immutable)
|
||||
- ✅ **config/** directory for centralized configuration
|
||||
- ✅ **Separate celery workers** for short and long tasks
|
||||
|
||||
### Simple Deployment, accessed as http://localhost
|
||||
### Configuration
|
||||
|
||||
The main container runs migrations, mediacms_web, celery_beat, celery_workers (celery_short and celery_long services), exposed on port 80 supported by redis and postgres database.
|
||||
Application settings can be overridden using environment variables in your docker-compose file or by building a custom image with a modified `cms/local_settings.py` file.
|
||||
|
||||
The FRONTEND_HOST in `deploy/docker/local_settings.py` is configured as http://localhost, on the docker host machine.
|
||||
Key environment variables:
|
||||
- `FRONTEND_HOST` - Your domain (e.g., `https://mediacms.example.com`)
|
||||
- `PORTAL_NAME` - Portal name
|
||||
- `SECRET_KEY` - Django secret key
|
||||
- `DEBUG` - Enable debug mode (development only)
|
||||
- Database and Redis connection settings
|
||||
|
||||
### Server with ssl certificate through letsencrypt service, accessed as https://my_domain.com
|
||||
Before trying this out make sure the ip points to my_domain.com.
|
||||
See the [Migration Guide](DOCKER_V7.3_MIGRATION.md) for detailed configuration options
|
||||
|
||||
With this method [this deployment](../docker-compose-letsencrypt.yaml) is used.
|
||||
### Simple Deployment (HTTP)
|
||||
|
||||
Edit this file and set `VIRTUAL_HOST` as my_domain.com, `LETSENCRYPT_HOST` as my_domain.com, and your email on `LETSENCRYPT_EMAIL`
|
||||
Use `docker-compose.yaml` for a standard HTTP deployment on port 80:
|
||||
|
||||
Edit `deploy/docker/local_settings.py` and set https://my_domain.com as `FRONTEND_HOST`
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Now run docker-compose -f docker-compose-letsencrypt.yaml up, when installation finishes you will be able to access https://my_domain.com using a valid Letsencrypt certificate!
|
||||
This starts all services (nginx, web, celery workers, database, redis) with the nginx container exposed on port 80. Access at http://localhost or http://your-server-ip.
|
||||
|
||||
### Advanced Deployment, accessed as http://localhost:8000
|
||||
**Features:**
|
||||
- Production-ready with immutable images
|
||||
- Named volumes for data persistence
|
||||
- Separate containers for each service
|
||||
|
||||
Here we can run 1 mediacms_web instance, with the FRONTEND_HOST in `deploy/docker/local_settings.py` configured as http://localhost:8000. This is bootstrapped by a single migrations instance and supported by a single celery_beat instance and 1 or more celery_worker instances. Redis and postgres containers are also used for persistence. Clients can access the service on http://localhost:8000, on the docker host machine. This is similar to [this deployment](../docker-compose.yaml), with a `port` defined in FRONTEND_HOST.
|
||||
### Production Deployment with HTTPS (Let's Encrypt)
|
||||
|
||||
### Advanced Deployment, with reverse proxy, accessed as http://mediacms.io
|
||||
Use `docker-compose-cert.yaml` for automatic HTTPS with Let's Encrypt:
|
||||
|
||||
Here we can use `jwilder/nginx-proxy` to reverse proxy to 1 or more instances of mediacms_web supported by other services as mentioned in the previous deployment. The FRONTEND_HOST in `deploy/docker/local_settings.py` is configured as http://mediacms.io, nginx-proxy has port 80 exposed. Clients can access the service on http://mediacms.io (Assuming DNS or the hosts file is setup correctly to point to the IP of the nginx-proxy instance). This is similar to [this deployment](../docker-compose-http-proxy.yaml).
|
||||
**Prerequisites:**
|
||||
- Domain name pointing to your server
|
||||
- Ports 80 and 443 open
|
||||
|
||||
### Advanced Deployment, with reverse proxy, accessed as https://localhost
|
||||
**Setup:**
|
||||
1. Edit `docker-compose-cert.yaml` and update:
|
||||
- `VIRTUAL_HOST` - Your domain
|
||||
- `LETSENCRYPT_HOST` - Your domain
|
||||
- `LETSENCRYPT_EMAIL` - Your email
|
||||
|
||||
The reverse proxy (`jwilder/nginx-proxy`) can be configured to provide SSL termination using self-signed certificates, letsencrypt or CA signed certificates (see: https://hub.docker.com/r/jwilder/nginx-proxy or [LetsEncrypt Example](https://www.singularaspect.com/use-nginx-proxy-and-letsencrypt-companion-to-host-multiple-websites/) ). In this case the FRONTEND_HOST should be set to https://mediacms.io. This is similar to [this deployment](../docker-compose-http-proxy.yaml).
|
||||
2. Run:
|
||||
```bash
|
||||
docker compose -f docker-compose-cert.yaml up -d
|
||||
```
|
||||
|
||||
This uses `nginxproxy/nginx-proxy` with `acme-companion` for automatic HTTPS certificate management. Access at https://your-domain.com.
|
||||
|
||||
### Development Deployment
|
||||
|
||||
Use `docker-compose-dev.yaml` for development with live code reloading:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-dev.yaml up
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Source code mounted for live editing
|
||||
- Django debug mode enabled
|
||||
- Frontend dev server on port 8088
|
||||
- Direct Django access (no nginx) on port 80
|
||||
|
||||
### Scaling Workers
|
||||
|
||||
Scale celery workers independently based on load:
|
||||
|
||||
```bash
|
||||
# Scale short task workers to 3 instances
|
||||
docker compose up -d --scale celery_short=3
|
||||
|
||||
# Scale long task workers to 2 instances
|
||||
docker compose up -d --scale celery_long=2
|
||||
```
|
||||
|
||||
### Using Extra Codecs (Full Image)
|
||||
|
||||
For advanced transcoding features (including Whisper for automatic subtitles), use the full worker image:
|
||||
|
||||
Edit your docker-compose file:
|
||||
```yaml
|
||||
celery_long:
|
||||
image: mediacms/mediacms-worker:7.3-full # Changed from :7.3
|
||||
```
|
||||
|
||||
Then restart:
|
||||
```bash
|
||||
docker compose up -d celery_long
|
||||
```
|
||||
|
||||
### A Scaleable Deployment Architecture (Docker, Swarm, Kubernetes)
|
||||
|
||||
The architecture below generalises all the deployment scenarios above, and provides a conceptual design for other deployments based on kubernetes and docker swarm. It allows for horizontal scaleability through the use of multiple mediacms_web instances and celery_workers. For large deployments, managed postgres, redis and storage may be adopted.
|
||||
The architecture below provides a conceptual design for deployments based on kubernetes and docker swarm. It allows for horizontal scaleability through the use of multiple web instances and celery workers. For large deployments, managed postgres, redis and storage may be adopted.
|
||||
|
||||

|
||||
|
||||
@@ -177,29 +288,46 @@ The architecture below generalises all the deployment scenarios above, and provi
|
||||
## 5. Configuration
|
||||
Several options are available on `cms/settings.py`, most of the things that are allowed or should be disallowed are described there.
|
||||
|
||||
It is advisable to override any of them by adding it to `local_settings.py` .
|
||||
It is advisable to override any of them by adding it to `local_settings.py`.
|
||||
|
||||
In case of a the single server installation, add to `cms/local_settings.py` .
|
||||
|
||||
In case of a docker compose installation, add to `deploy/docker/local_settings.py` . This will automatically overwrite `cms/local_settings.py` .
|
||||
|
||||
Any change needs restart of MediaCMS in order to take effect.
|
||||
|
||||
Single server installation: edit `cms/local_settings.py`, make a change and restart MediaCMS
|
||||
**Single server installation:** edit `cms/local_settings.py`, make changes and restart MediaCMS:
|
||||
|
||||
```bash
|
||||
#systemctl restart mediacms
|
||||
systemctl restart mediacms celery_beat celery_short celery_long
|
||||
```
|
||||
|
||||
Docker Compose installation: edit `deploy/docker/local_settings.py`, make a change and restart MediaCMS containers
|
||||
**Docker installation:** Configuration can be done in two ways:
|
||||
|
||||
1. **Environment variables** (recommended for simple changes):
|
||||
Add to your docker-compose file:
|
||||
```yaml
|
||||
environment:
|
||||
FRONTEND_HOST: 'https://mediacms.example.com'
|
||||
PORTAL_NAME: 'My MediaCMS'
|
||||
```
|
||||
|
||||
2. **Custom image with local_settings.py** (for complex changes):
|
||||
- Create a custom Dockerfile:
|
||||
```dockerfile
|
||||
FROM mediacms/mediacms:7.3
|
||||
COPY my_custom_settings.py /home/mediacms.io/mediacms/cms/local_settings.py
|
||||
```
|
||||
- Build and use your custom image
|
||||
|
||||
After changes, restart the affected containers:
|
||||
|
||||
```bash
|
||||
#docker-compose restart web celery_worker celery_beat
|
||||
docker compose restart web celery_short celery_long celery_beat
|
||||
```
|
||||
|
||||
### 5.1 Change portal logo
|
||||
|
||||
Set a new svg file for the white theme (`static/images/logo_dark.svg`) or the dark theme (`static/images/logo_light.svg`)
|
||||
Find the default svg files for the white theme on `static/images/logo_dark.svg` and for the dark theme on `static/images/logo_light.svg`
|
||||
You can specify new svg paths to override by editing the `PORTAL_LOGO_DARK_SVG` and `PORTAL_LOGO_LIGHT_SVG` variables in `settings.py`.
|
||||
|
||||
You can also use custom pngs, by setting the variables `PORTAL_LOGO_DARK_PNG` and `PORTAL_LOGO_LIGHT_PNG` in `settings.py`. The svg files have priority over png files, so if both are set, svg files will be used.
|
||||
|
||||
In any case, make sure the files are placed on the static/images folder.
|
||||
|
||||
### 5.2 Set global portal title
|
||||
|
||||
@@ -213,7 +341,7 @@ PORTAL_NAME = 'my awesome portal'
|
||||
|
||||
By default `CAN_ADD_MEDIA = "all"` means that all registered users can add media. Other valid options are:
|
||||
|
||||
- **email_verified**, a user not only has to register an account but also verify the email (by clicking the link sent upon registration). Apparently email configuration need to work, otherise users won't receive emails.
|
||||
- **email_verified**, a user not only has to register an account but also verify the email (by clicking the link sent upon registration). Apparently email configuration need to work, otherise users won't receive emails.
|
||||
|
||||
- **advancedUser**, only users that are marked as advanced users can add media. Admins or MediaCMS managers can make users advanced users by editing their profile and selecting advancedUser.
|
||||
|
||||
@@ -282,7 +410,7 @@ Make changes (True/False) to any of the following:
|
||||
|
||||
### 5.9 Show or hide the download option on a media
|
||||
|
||||
Edit `templates/config/installation/features.html` and set
|
||||
Edit `templates/config/installation/features.html` and set
|
||||
|
||||
```
|
||||
download: false
|
||||
@@ -291,7 +419,7 @@ download: false
|
||||
### 5.10 Automatically hide media upon being reported
|
||||
|
||||
set a low number for variable `REPORTED_TIMES_THRESHOLD`
|
||||
eg
|
||||
eg
|
||||
|
||||
```
|
||||
REPORTED_TIMES_THRESHOLD = 2
|
||||
@@ -324,13 +452,22 @@ ADMIN_EMAIL_LIST = ['info@mediacms.io']
|
||||
|
||||
### 5.13 Disallow user registrations from specific domains
|
||||
|
||||
set domains that are not valid for registration via this variable:
|
||||
Set domains that are not valid for registration via this variable:
|
||||
|
||||
```
|
||||
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = [
|
||||
'xxx.com', 'emaildomainwhatever.com']
|
||||
```
|
||||
|
||||
Alternatively, allow only permitted domains to register. This can be useful if you're using mediacms as a private service within an organization, and want to give free registration for those in the org, but deny registration from all other domains. Setting this option bans all domains NOT in the list from registering. Default is a blank list, which is ignored. To disable, set to a blank list.
|
||||
```
|
||||
ALLOWED_DOMAINS_FOR_USER_REGISTRATION = [
|
||||
"private.com",
|
||||
"vod.private.com",
|
||||
"my.favorite.domain",
|
||||
"test.private.com"]
|
||||
```
|
||||
|
||||
### 5.14 Require a review by MediaCMS editors/managers/admins
|
||||
|
||||
set value
|
||||
@@ -339,7 +476,7 @@ set value
|
||||
MEDIA_IS_REVIEWED = False
|
||||
```
|
||||
|
||||
any uploaded media now needs to be reviewed before it can appear to the listings.
|
||||
any uploaded media now needs to be reviewed before it can appear to the listings.
|
||||
MediaCMS editors/managers/admins can visit the media page and edit it, where they can see the option to mark media as reviewed. By default this is set to True, so all media don't require to be reviewed
|
||||
|
||||
### 5.15 Specify maximum number of media for a playlist
|
||||
@@ -354,7 +491,7 @@ MAX_MEDIA_PER_PLAYLIST = 14
|
||||
|
||||
### 5.16 Specify maximum size of a media that can be uploaded
|
||||
|
||||
change `UPLOAD_MAX_SIZE`.
|
||||
change `UPLOAD_MAX_SIZE`.
|
||||
|
||||
default is 4GB
|
||||
|
||||
@@ -417,7 +554,7 @@ Global notifications that are implemented are controlled by the following option
|
||||
|
||||
```
|
||||
USERS_NOTIFICATIONS = {
|
||||
'MEDIA_ADDED': True,
|
||||
'MEDIA_ADDED': True,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -442,6 +579,86 @@ ADMINS_NOTIFICATIONS = {
|
||||
- Make the portal workflow public, but at the same time set `GLOBAL_LOGIN_REQUIRED = True` so that only logged in users can see content.
|
||||
- You can either set `REGISTER_ALLOWED = False` if you want to add members yourself or checkout options on "django-allauth settings" that affects registration in `cms/settings.py`. Eg set the portal invite only, or set email confirmation as mandatory, so that you control who registers.
|
||||
|
||||
### 5.24 Enable the sitemap
|
||||
|
||||
Whether or not to enable generation of a sitemap file at http://your_installation/sitemap.xml (default: False)
|
||||
|
||||
```
|
||||
GENERATE_SITEMAP = False
|
||||
```
|
||||
|
||||
|
||||
### 5.25 Control who can add comments
|
||||
|
||||
By default `CAN_COMMENT = "all"` means that all registered users can add comment. Other valid options are:
|
||||
|
||||
- **email_verified**, a user not only has to register an account but also verify the email (by clicking the link sent upon registration). Apparently email configuration need to work, otherise users won't receive emails.
|
||||
|
||||
- **advancedUser**, only users that are marked as advanced users can add comment. Admins or MediaCMS managers can make users advanced users by editing their profile and selecting advancedUser.
|
||||
|
||||
### 5.26 Control whether anonymous users can list all users
|
||||
|
||||
By default, anonymous users can view the list of all users on the platform. To restrict this to authenticated users only, set:
|
||||
|
||||
```
|
||||
ALLOW_ANONYMOUS_USER_LISTING = False
|
||||
```
|
||||
|
||||
When set to False, only logged-in users will be able to access the user listing API endpoint.
|
||||
|
||||
|
||||
### 5.27 Control who can see the members page
|
||||
|
||||
By default `CAN_SEE_MEMBERS_PAGE = "all"` means that all registered users can see the members page. Other valid options are:
|
||||
|
||||
- **editors**, only MediaCMS editors can view the page
|
||||
- **admins**, only MediaCMS admins can view the page
|
||||
|
||||
|
||||
### 5.28 Configure user search fields
|
||||
|
||||
By default, when searching for users (e.g., in bulk actions modals or the users API), the search is performed on the user's name and username. You can configure this behavior using the `USER_SEARCH_FIELD` setting:
|
||||
|
||||
```
|
||||
USER_SEARCH_FIELD = "name_username" # Default - searches in name and username
|
||||
```
|
||||
|
||||
To also include email addresses in the search and display them in the user interface:
|
||||
|
||||
```
|
||||
USER_SEARCH_FIELD = "name_username_email" # Searches in name, username, and email
|
||||
```
|
||||
|
||||
When set to `"name_username_email"`:
|
||||
- The user search will also match email addresses
|
||||
- The email field will be returned in the API response
|
||||
- Frontend components will display users as "Name - Email" instead of "Name - Username"
|
||||
|
||||
This setting is useful when you want to make it easier to find users by their email addresses, particularly in administrative interfaces like bulk action modals.
|
||||
|
||||
|
||||
### 5.29 Require user approval on registration
|
||||
|
||||
By default, users do not require approval, so they can login immediately after registration (if registration is open). However, if the parameter `USERS_NEEDS_TO_BE_APPROVED` is set to `True`, they will first have to have their accounts approved by an administrator before they can successfully sign in.
|
||||
Administrators can approve users through the following ways: 1. through Django administration, 2. through the users management page, 3. through editing the profile page directly. In all cases, set 'Is approved' to True.
|
||||
|
||||
### 5.30 Show or hide media count numbers on categories and tags pages
|
||||
|
||||
By default, the number of media items is displayed next to each category and tag on the `/categories` and `/tags` pages. To hide these numbers:
|
||||
|
||||
```
|
||||
INCLUDE_LISTING_NUMBERS = False
|
||||
```
|
||||
|
||||
To show the numbers (default behavior):
|
||||
|
||||
```
|
||||
INCLUDE_LISTING_NUMBERS = True
|
||||
```
|
||||
|
||||
This setting affects only the visual display on the categories and tags listing pages and does not impact the functionality of filtering by categories or tags.
|
||||
|
||||
|
||||
## 6. Manage pages
|
||||
to be written
|
||||
|
||||
@@ -466,13 +683,13 @@ For example, the `Active` state of any profile can be toggled to enable or disab
|
||||
|
||||
## 13. How To Add A Static Page To The Sidebar
|
||||
|
||||
### 1. Create your html page in templates/cms/
|
||||
### 1. Create your html page in templates/cms/
|
||||
e.g. duplicate and rename about.html
|
||||
```
|
||||
sudo cp templates/cms/about.html templates/cms/volunteer.html
|
||||
```
|
||||
|
||||
### 2. Create your css file in static/css/
|
||||
### 2. Create your css file in static/css/
|
||||
```
|
||||
touch static/css/volunteer.css
|
||||
```
|
||||
@@ -536,24 +753,24 @@ urlpatterns = [
|
||||
|
||||
### 8. Add your page to the left sidebar
|
||||
To add a link to your page as a menu item in the left sidebar,
|
||||
add the following code after the last line in _commons.js
|
||||
add the following code after the last line in _commons.js
|
||||
```
|
||||
/* Checks that a given selector has loaded. */
|
||||
const checkElement = async selector => {
|
||||
while ( document.querySelector(selector) === null) {
|
||||
await new Promise( resolve => requestAnimationFrame(resolve) )
|
||||
}
|
||||
return document.querySelector(selector);
|
||||
return document.querySelector(selector);
|
||||
};
|
||||
|
||||
/* Checks that sidebar nav menu has loaded, then adds menu item. */
|
||||
checkElement('.nav-menu')
|
||||
.then((element) => {
|
||||
(function(){
|
||||
var a = document.createElement('a');
|
||||
(function(){
|
||||
var a = document.createElement('a');
|
||||
a.href = "/volunteer";
|
||||
a.title = "Volunteer";
|
||||
|
||||
|
||||
var s = document.createElement('span');
|
||||
s.className = "menu-item-icon";
|
||||
|
||||
@@ -563,7 +780,7 @@ checkElement('.nav-menu')
|
||||
|
||||
s.appendChild(icon);
|
||||
a.appendChild(s);
|
||||
|
||||
|
||||
var linkText = document.createTextNode("Volunteer");
|
||||
var t = document.createElement('span');
|
||||
|
||||
@@ -575,14 +792,14 @@ checkElement('.nav-menu')
|
||||
listItem.appendChild(a);
|
||||
|
||||
//if signed out use 3rd nav-menu
|
||||
var elem = document.querySelector(".nav-menu:nth-child(3) nav ul");
|
||||
var elem = document.querySelector(".nav-menu:nth-child(3) nav ul");
|
||||
var loc = elem.innerText;
|
||||
if (loc.includes("About")){
|
||||
elem.insertBefore(listItem, elem.children[2]);
|
||||
} else { //if signed in use 4th nav-menu
|
||||
elem = document.querySelector(".nav-menu:nth-child(4) nav ul");
|
||||
elem.insertBefore(listItem, elem.children[2]);
|
||||
}
|
||||
}
|
||||
})();
|
||||
});
|
||||
```
|
||||
@@ -608,7 +825,7 @@ Instructions contributed by @alberto98fx
|
||||
|
||||
2. Add the Gtag/Analytics script
|
||||
|
||||
3. Inside ``` $DIR/mediacms/templates/root.html``` you'll see a file like this one:
|
||||
3. Inside ``` $DIR/mediacms/templates/root.html``` you'll see a file like this one:
|
||||
|
||||
```
|
||||
<head>
|
||||
@@ -619,7 +836,7 @@ Instructions contributed by @alberto98fx
|
||||
{% include "common/head-meta.html" %}
|
||||
|
||||
{% block headermeta %}
|
||||
|
||||
|
||||
<meta property="og:title" content="{{PORTAL_NAME}}">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
@@ -632,17 +849,17 @@ Instructions contributed by @alberto98fx
|
||||
{% block topimports %}{%endblock topimports %}
|
||||
|
||||
{% include "config/index.html" %}
|
||||
|
||||
|
||||
{% endblock head %}
|
||||
|
||||
</head>
|
||||
```
|
||||
|
||||
4. Add ``` {% include "tracking.html" %} ``` at the end inside the section ```<head>```
|
||||
|
||||
5. If you are using Docker and didn't mount the entire dir you need to bind a new volume:
|
||||
|
||||
5. If you are using Docker and didn't mount the entire dir you need to bind a new volume:
|
||||
```
|
||||
|
||||
|
||||
web:
|
||||
image: mediacms/mediacms:latest
|
||||
restart: unless-stopped
|
||||
@@ -653,7 +870,7 @@ Instructions contributed by @alberto98fx
|
||||
volumes:
|
||||
- ./templates/root.html:/home/mediacms.io/mediacms/templates/root.html
|
||||
- ./templates/tracking.html://home/mediacms.io/mediacms/templates/tracking.html
|
||||
|
||||
|
||||
```
|
||||
|
||||
## 15. Debugging email issues
|
||||
@@ -684,7 +901,7 @@ email = EmailMessage(
|
||||
email.send(fail_silently=False)
|
||||
```
|
||||
|
||||
You have the chance to either receive the email (in this case it will be sent to recipient@email.com) otherwise you will see the error.
|
||||
You have the chance to either receive the email (in this case it will be sent to recipient@email.com) otherwise you will see the error.
|
||||
For example, while specifying wrong password for my Gmail account I get
|
||||
|
||||
```
|
||||
@@ -694,7 +911,7 @@ SMTPAuthenticationError: (535, b'5.7.8 Username and Password not accepted. Learn
|
||||
## 16. Frequently Asked Questions
|
||||
Video is playing but preview thumbnails are not showing for large video files
|
||||
|
||||
Chances are that the sprites file was not created correctly.
|
||||
Chances are that the sprites file was not created correctly.
|
||||
The output of files.tasks.produce_sprite_from_video() function in this case is something like this
|
||||
|
||||
```
|
||||
@@ -708,16 +925,16 @@ Solution: edit file `/etc/ImageMagick-6/policy.xml` and set bigger values for th
|
||||
<policy domain="resource" name="width" value="16000KP"/>
|
||||
```
|
||||
|
||||
Newly added video files now will be able to produce the sprites file needed for thumbnail previews. To re-run that task on existing videos, enter the Django shell
|
||||
Newly added video files now will be able to produce the sprites file needed for thumbnail previews. To re-run that task on existing videos, enter the Django shell
|
||||
|
||||
|
||||
```
|
||||
root@8433f923ccf5:/home/mediacms.io/mediacms# source /home/mediacms.io/bin/activate
|
||||
root@8433f923ccf5:/home/mediacms.io/mediacms# source /home/mediacms.io/bin/activate
|
||||
root@8433f923ccf5:/home/mediacms.io/mediacms# python manage.py shell
|
||||
Python 3.8.14 (default, Sep 13 2022, 02:23:58)
|
||||
Python 3.8.14 (default, Sep 13 2022, 02:23:58)
|
||||
```
|
||||
|
||||
and run
|
||||
and run
|
||||
|
||||
```
|
||||
In [1]: from files.models import Media
|
||||
@@ -727,4 +944,217 @@ In [3]: for media in Media.objects.filter(media_type='video', sprites=''):
|
||||
...: produce_sprite_from_video(media.friendly_token)
|
||||
```
|
||||
|
||||
this will re-create the sprites for videos that the task failed.
|
||||
this will re-create the sprites for videos that the task failed.
|
||||
|
||||
|
||||
## 17. Cookie consent code
|
||||
On file `templates/components/header.html` you can find a simple cookie consent code. It is commented, so you have to remove the `{% comment %}` and `{% endcomment %}` lines in order to enable it. Or you can replace that part with your own code that handles cookie consent banners.
|
||||
|
||||

|
||||
|
||||
## 18. Disable encoding and show only original file
|
||||
When videos are uploaded, they are getting encoded to multiple resolutions, a procedure called transcoding. Sometimes this is not needed and you only need to show the original file, eg when MediaCMS is running on a low capabilities server. To achieve this, edit settings.py and set
|
||||
|
||||
```
|
||||
DO_NOT_TRANSCODE_VIDEO = True
|
||||
```
|
||||
|
||||
This will disable the transcoding process and only the original file will be shown. Note that this will also disable the sprites file creation, so you will not have the preview thumbnails on the video player.
|
||||
|
||||
## 19. Rounded corners on videos
|
||||
|
||||
By default the video player and media items are now having rounded corners, on larger screens (not in mobile). If you don't like this change, set `USE_ROUNDED_CORNERS = False` in `local_settings.py`.
|
||||
|
||||
|
||||
|
||||
## 20. Translations
|
||||
|
||||
### 20.1 Set a default language
|
||||
|
||||
By default MediaCMS is available in a number of languages. To set the default language, edit `settings.py` and set LANGUAGE_CODE to the code of one of the languages.
|
||||
|
||||
### 20.2 Remove existing languages
|
||||
To limit the number of languages that are shown as available, remove them from the LANGUAGES list in `settings.py` or comment them. Only what is there is shown.
|
||||
|
||||
### 20.3 Improve existing translation
|
||||
To make improvements in existing translated content, in a language that is already translated, check the language by the code name in `files/frontend-translations/` and edit the corresponding file.
|
||||
|
||||
### 20.4 Add more content to existing translation
|
||||
Not all text is translated, so at any time you may find strings missing that need to be added to the translation. The idea here is that
|
||||
|
||||
a) you made the text as translatable, in the code
|
||||
b) you add the translated string
|
||||
|
||||
For a), you have to see if the string to be translated lives in the frontend directory (React app) or on the Django templates. There are examples for both.
|
||||
1. the Django templates, which is found in templates/ dir. Have a look on `templates/cms/about.html` to see an example of how it is done
|
||||
2. the frontend code (React), have a look how `translateString` is used in `frontend`
|
||||
|
||||
|
||||
After the string is marked as translatable, add the string to `files/frontend-translations/en.py` first, and then run
|
||||
|
||||
```
|
||||
python manage.py process_translations
|
||||
```
|
||||
|
||||
In order to populate the string in all the languages. NO PR will be accepted if this procedure is not followed. You don't have to translate the string to all supported languages, but the command has to run and populate the existing dictionaries with the new strings for all languages. This ensures that there is no missing string to be translated in any language.
|
||||
|
||||
After this command is run, translate the string to the language you want. If the string to be translated lives in Django templates, you don't have to re-build the frontend. If the change lives in the frontend, you will have to re-build in order to see the changes. The Makefile command `make build-frontend` can help with this.
|
||||
|
||||
|
||||
### 20.5 Add a new language and translate
|
||||
To add a new language: add the language in settings.py, then add the file in `files/frontend-translations/`. Make sure you copy the initial strings by copying `files/frontend-translations/en.py` to it.
|
||||
|
||||
## 21. How to change the video frames on videos
|
||||
|
||||
By default while watching a video you can hover and see the small images named sprites that are extracted every 10 seconds of a video. You can change this number to something smaller by performing the following:
|
||||
|
||||
* edit ./frontend/src/static/js/components/media-viewer/VideoViewer/index.js and change `seconds: 10 ` to the value you prefer, eg 2.
|
||||
* edit settings.py and set the same number for value SPRITE_NUM_SECS
|
||||
* now you have to re-build the frontend: the easiest way is to run `make build-frontend`, which requires Docker
|
||||
|
||||
After that, newly uploaded videos will have sprites generated with the new number of seconds.
|
||||
|
||||
|
||||
|
||||
## 22. Role-Based Access Control
|
||||
|
||||
By default there are 3 statuses for any Media that lives on the system, public, unlisted, private. When RBAC support is added, a user that is part of a group has access to media that are published to one or more categories that the group is associated with. The workflow is this:
|
||||
|
||||
|
||||
1. A Group is created
|
||||
2. A Category is associated with the Group
|
||||
3. A User is added to the Group
|
||||
|
||||
Now user can view the Media even if it is in private state. User also sees all media in Category page
|
||||
|
||||
When user is added to group, they can be set as Member, Contributor, Manager.
|
||||
|
||||
- Member: user can view media that are published on one or more categories that this group is associated with
|
||||
- Contributor: besides viewing, user can also edit the Media in a category associated with this Group. They can also publish Media to this category
|
||||
- Manager: same as Contributor for now
|
||||
|
||||
Use cases facilitated with RBAC:
|
||||
- viewing a Media in private state: if RBAC is enabled, if user is Member on a Group that is associated with a Category, and the media is published to this Category, then user can view the media
|
||||
- editing a Media: if RBAC is enabled, and user is Contributor to one or more Categories, they can publish media to these Categories as long as they are associated with one Group
|
||||
- viewing all media of a category: if RBAC is enabled, and user visits a Category, they are able to see the listing of all media that are published in this category, independent of their state, provided that the category is associated with a group that the user is member of
|
||||
- viewing all categories associated with groups the user is member of: if RBAC is enabled, and user visits the listing of categories, they can view all categories that are associated with a group the user is member
|
||||
|
||||
How to enable RBAC support:
|
||||
|
||||
```
|
||||
USE_RBAC = True
|
||||
```
|
||||
|
||||
on `local_settings.py` and restart the instance.
|
||||
|
||||
|
||||
## 23. SAML setup
|
||||
SAML authentication is supported along with the option to utilize the SAML response and do useful things as setting up the user role in MediaCMS or participation in groups.
|
||||
|
||||
To enable SAML support, edit local_settings.py and set the following options:
|
||||
|
||||
```
|
||||
USE_RBAC = True
|
||||
USE_SAML = True
|
||||
USE_IDENTITY_PROVIDERS = True
|
||||
|
||||
USE_X_FORWARDED_HOST = True
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
SECURE_SSL_REDIRECT = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
SOCIALACCOUNT_ADAPTER = 'saml_auth.adapter.SAMLAccountAdapter'
|
||||
SOCIALACCOUNT_PROVIDERS = {
|
||||
"saml": {
|
||||
"provider_class": "saml_auth.custom.provider.CustomSAMLProvider",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
To set a SAML provider:
|
||||
|
||||
- Step 1: Add SAML Identity Provider
|
||||
1. Navigate to Admin panel
|
||||
2. Select "Identity Provider"
|
||||
3. Configure as follows:
|
||||
- **Provider**: saml
|
||||
- **Provider ID**: an ID for the provider
|
||||
- **IDP Config Name**: a name for the provider
|
||||
- **Client ID**: the identifier that is part of the login, and that is shared with the IDP.
|
||||
- **Site**: Set the default one
|
||||
|
||||
- Step 2: Add SAML Configuration
|
||||
Select the SAML Configurations tab, create a new one and set:
|
||||
|
||||
1. **IDP ID**: Must be a URL
|
||||
2. **IDP Certificate**: x509cert from your SAML provider
|
||||
3. **SSO URL**:
|
||||
4. **SLO URL**:
|
||||
5. **SP Metadata URL**: The metadata URL that the IDP will utilize. This can be https://{portal}/saml/metadata and is autogenerated by MediaCMS
|
||||
|
||||
- Step 3: Set other Options
|
||||
1. **Email Settings**:
|
||||
- `verified_email`: When enabled, emails from SAML responses will be marked as verified
|
||||
- `Remove from groups`: When enabled, user is removed from a group after login, if they have been removed from the group on the IDP
|
||||
2. **Global Role Mapping**: Maps the role returned by SAML (as set in the SAML Configuration tab) with the role in MediaCMS
|
||||
3. **Group Role Mapping**: Maps the role returned by SAML (as set in the SAML Configuration tab) with the role in groups that user will be added
|
||||
4. **Group mapping**: This creates groups associated with this IDP. Group ids as they come from SAML, associated with MediaCMS groups
|
||||
5. **Category Mapping**: This maps a group id (from SAML response) with a category in MediaCMS
|
||||
|
||||
A full SAML deployment with [EntraID guide and troubleshooting steps is available here.](./saml_entraid_setup.md). This guide can be used as reference for other IDPs too.
|
||||
|
||||
## 24. Identity Providers setup
|
||||
|
||||
A separate Django app identity_providers has been added in order to facilitate a number of configurations related to different identity providers. If this is enabled, it gives the following options:
|
||||
|
||||
- allows to add an Identity Provider through Django admin, and set a number of mappings, as Group Mapping, Global Role mapping and more. While SAML is the only provider that can be added out of the box, any identity provider supported by django allauth can be added with minimal effort. If the response of the identity provider contains attributes as role, or groups, then these can be mapped to MediaCMS specific roles (advanced user, editor, manager, admin) and groups (rbac groups)
|
||||
- saves SAML response logs after user is authenticated (can be utilized for other providers too)
|
||||
- allows to specify a list of login options through the admin (eg system login, identity provider login)
|
||||
|
||||
|
||||
to enable the identity providers, set the following setting on `local_settings.py`:
|
||||
|
||||
|
||||
```
|
||||
USE_IDENTITY_PROVIDERS = True
|
||||
```
|
||||
|
||||
Visiting the admin, you will see the Identity Providers tab and you can add one.
|
||||
|
||||
## 25. Custom urls
|
||||
To enable custom urls, set `ALLOW_CUSTOM_MEDIA_URLS = True` on settings.py or local_settings.py
|
||||
This will enable editing the URL of the media, while editing a media. If the URL is already taken you get a message you cannot update this.
|
||||
|
||||
## 26. Allowed files
|
||||
MediaCMS performs identification attempts on new file uploads and only allows certain file types specified in the `ALLOWED_MEDIA_UPLOAD_TYPES` setting. By default, only ["video", "audio", "image", "pdf"] files are allowed.
|
||||
|
||||
When a file is not identified as one of these allowed types, the file gets removed from the system and there's an entry indicating that this is not a supported media type.
|
||||
|
||||
If you want to change the allowed file types, edit the `ALLOWED_MEDIA_UPLOAD_TYPES` list in your `settings.py` or `local_settings.py` file. If 'all' is specified in this list, no check is performed and all files are allowed.
|
||||
|
||||
## 27. User upload limits
|
||||
MediaCMS allows you to set a maximum number of media files that each user can upload. This is controlled by the `NUMBER_OF_MEDIA_USER_CAN_UPLOAD` setting in `settings.py` or `local_settings.py`. By default, this is set to 100 media items per user.
|
||||
|
||||
When a user reaches this limit, they will no longer be able to upload new media until they delete some of their existing content. This limit applies regardless of the user's role or permissions in the system.
|
||||
|
||||
To change the maximum number of uploads allowed per user, modify the `NUMBER_OF_MEDIA_USER_CAN_UPLOAD` value in your settings file:
|
||||
|
||||
```
|
||||
NUMBER_OF_MEDIA_USER_CAN_UPLOAD = 5
|
||||
```
|
||||
|
||||
## 28. Whisper Transcribe for Automatic Subtitles
|
||||
MediaCMS can integrate with OpenAI's Whisper to automatically generate subtitles for your media files. This feature is useful for making your content more accessible.
|
||||
|
||||
### How it works
|
||||
When the whisper transcribe task is triggered for a media file, MediaCMS runs the `whisper` command-line tool to process the audio and generate a subtitle file in VTT format. The generated subtitles are then associated with the media and are available under the "automatic" language option.
|
||||
|
||||
### Configuration
|
||||
|
||||
Transcription functionality is available only for the Docker installation. To enable this feature, you must either use the `docker-compose.full.yaml` file, as it contains an image with the necessary requirements, or you can also set that celery_worker service is usine mediacms:full image instead of mediacms:latest. Then you also have to set the setting: `USE_WHISPER_TRANSCRIBE = True` in your local_settings.py file.
|
||||
|
||||
By default, all users have the ability to send a request for a video to be transcribed, as well as transcribed and translated to English. If you wish to change this behavior, you can edit the `settings.py` file and set `USER_CAN_TRANSCRIBE_VIDEO=False`.
|
||||
|
||||
The transcription uses the base model of Whisper speech-to-text by default. However, you can change the model by editing the `WHISPER_MODEL` setting in `settings.py`.
|
||||
|
||||
89
docs/dev_exp.md
Normal file
89
docs/dev_exp.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Developer Experience
|
||||
There is ongoing effort to provide a better developer experience and document it.
|
||||
|
||||
## How to develop locally with Docker
|
||||
First install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
Then run `docker compose -f docker-compose-dev.yaml up`
|
||||
|
||||
```
|
||||
user@user:~/mediacms$ docker compose -f docker-compose-dev.yaml up
|
||||
```
|
||||
|
||||
In a few minutes the app will be available at http://localhost . Login via admin/admin
|
||||
|
||||
### What does docker-compose-dev.yaml do?
|
||||
It build the two images used for backend and frontend.
|
||||
|
||||
* Backend: `mediacms/mediacms-dev:latest`
|
||||
* Frontend: `frontend`
|
||||
|
||||
and will start all services required for MediaCMS, as Celery/Redis for asynchronous tasks, PostgreSQL database, Django and React
|
||||
|
||||
For Django, the changes from the image produced by docker-compose.yaml are these:
|
||||
|
||||
* Django runs in debug mode, with `python manage.py runserver`
|
||||
* uwsgi and nginx are not run
|
||||
* Django runs in Debug mode, with Debug Toolbar
|
||||
* Static files (js/css) are loaded from static/ folder
|
||||
* corsheaders is installed and configured to allow all origins
|
||||
|
||||
For React, it will run `npm start` in the frontend folder, which will start the development server.
|
||||
Check it on http://localhost:8088/
|
||||
|
||||
### How to develop in Django
|
||||
Django starts at http://localhost and is reloading automatically. Making any change to the python code should refresh Django.
|
||||
|
||||
If Django breaks due to an error (eg SyntaxError, while editing the code), you might have to restart it
|
||||
|
||||
```
|
||||
docker compose -f docker-compose-dev.yaml restart web
|
||||
```
|
||||
|
||||
|
||||
|
||||
### How to develop in React
|
||||
React is started on http://localhost:8088/ , code is located in frontend/ , so making changes there should have instant effect on the page. Keep in mind that React is loading data from Django, and that it has to be built so that Django can serve it.
|
||||
|
||||
### Making changes to the frontend
|
||||
|
||||
The way React is added is more complicated than the usual SPA project and this is because React is used as a library loaded by Django Templates, so it is not a standalone project and is not handling routes etc.
|
||||
|
||||
The two directories to consider are:
|
||||
* frontend/src , for the React files
|
||||
* templates/, for the Django templates.
|
||||
|
||||
Django is using a highly intuitive hierarchical templating system (https://docs.djangoproject.com/en/4.2/ref/templates/), where the base template is templates/root.html and all other templates are extending it.
|
||||
|
||||
React is called through the Django templates, eg templates/cms/media.html is loading js/media.js
|
||||
|
||||
In order to make changes to React code, edit code on frontend/src and check it's effect on http://localhost:8088/ . Once ready, build it and copy it to the Django static folder, so that it is served by Django.
|
||||
|
||||
### Development workflow with the frontend
|
||||
1. Edit frontend/src/ files
|
||||
2. Check changes on http://localhost:8088/
|
||||
3. Build frontend with `docker compose -f docker-compose-dev.yaml exec frontend npm run dist`
|
||||
4. Copy static files to Django static folder with`cp -r frontend/dist/static/* static/`
|
||||
5. Restart Django - `docker compose -f docker-compose-dev.yaml restart web` so that it uses the new static files
|
||||
6. Commit the changes
|
||||
|
||||
### Helper commands
|
||||
There is ongoing effort to provide helper commands, check the Makefile for what it supports. Eg
|
||||
|
||||
Bash into the web container:
|
||||
|
||||
```
|
||||
user@user:~/mediacms$ make admin-shell
|
||||
root@ca8c1096726b:/home/mediacms.io/mediacms# ./manage.py shell
|
||||
```
|
||||
|
||||
Build the frontend:
|
||||
|
||||
```
|
||||
user@user:~/mediacms$ make build-frontend
|
||||
docker compose -f docker-compose-dev.yaml exec frontend npm run dist
|
||||
|
||||
> mediacms-frontend@0.9.1 dist /home/mediacms.io/mediacms/frontend
|
||||
> mediacms-scripts rimraf ./dist && mediacms-scripts build --config=./config/mediacms.config.js --env=dist
|
||||
...
|
||||
```
|
||||
@@ -17,7 +17,7 @@ to be written
|
||||
|
||||
## 3. API documentation
|
||||
API is documented using Swagger - checkout ot http://your_installation/swagger - example https://demo.mediacms.io/swagger/
|
||||
This page allows you to login to perform authenticated actions - it will also use your session if logged in.
|
||||
This page allows you to login to perform authenticated actions - it will also use your session if logged in.
|
||||
|
||||
|
||||
An example of working with Python requests library:
|
||||
@@ -50,8 +50,8 @@ Checkout the [Code of conduct page](../CODE_OF_CONDUCT.md) if you want to contri
|
||||
To perform the Docker installation, follow instructions to install Docker + Docker compose (docs/Docker_Compose.md) and then build/start docker-compose-dev.yaml . This will run the frontend application on port 8088 on top of all other containers (including the Django web application on port 80)
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-dev.yaml build
|
||||
docker-compose -f docker-compose-dev.yaml up
|
||||
docker compose -f docker-compose-dev.yaml build
|
||||
docker compose -f docker-compose-dev.yaml up
|
||||
```
|
||||
|
||||
An `admin` user is created during the installation process. Its attributes are defined in `docker-compose-dev.yaml`:
|
||||
@@ -65,16 +65,16 @@ ADMIN_EMAIL: 'admin@localhost'
|
||||
Eg change `frontend/src/static/js/pages/HomePage.tsx` , dev application refreshes in a number of seconds (hot reloading) and I see the changes, once I'm happy I can run
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-dev.yaml exec -T frontend npm run dist
|
||||
docker compose -f docker-compose-dev.yaml exec -T frontend npm run dist
|
||||
```
|
||||
|
||||
And then in order for the changes to be visible on the application while served through nginx,
|
||||
And then in order for the changes to be visible on the application while served through nginx,
|
||||
|
||||
```
|
||||
cp -r frontend/dist/static/* static/
|
||||
```
|
||||
|
||||
POST calls: cannot be performed through the dev server, you have to make through the normal application (port 80) and then see changes on the dev application on port 8088.
|
||||
POST calls: cannot be performed through the dev server, you have to make through the normal application (port 80) and then see changes on the dev application on port 8088.
|
||||
Make sure the urls are set on `frontend/.env` if different than localhost
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ http://localhost:8088/manage-media.html manage_media
|
||||
After I make changes to the django application (eg make a change on `files/forms.py`) in order to see the changes I have to restart the web container
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-dev.yaml restart web
|
||||
docker compose -f docker-compose-dev.yaml restart web
|
||||
```
|
||||
|
||||
## How video is transcoded
|
||||
@@ -113,7 +113,7 @@ there is also an experimental small service (not commited to the repo currently)
|
||||
|
||||
When the Encode object is marked as success and chunk=False, and thus is available for download/stream, there is a task that gets started and saves an HLS version of the file (1 mp4-->x number of small .ts chunks). This would be FILES_C
|
||||
|
||||
This mechanism allows for workers that have access on the same filesystem (either localhost, or through a shared network filesystem, eg NFS/EFS) to work on the same time and produce results.
|
||||
This mechanism allows for workers that have access on the same filesystem (either localhost, or through a shared network filesystem, eg NFS/EFS) to work on the same time and produce results.
|
||||
|
||||
## 6. Working with the automated tests
|
||||
|
||||
@@ -122,19 +122,19 @@ This instructions assume that you're using the docker installation
|
||||
1. start docker-compose
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
docker compose up
|
||||
```
|
||||
|
||||
2. Install the requirements on `requirements-dev.txt ` on web container (we'll use the web container for this)
|
||||
|
||||
```
|
||||
docker-compose exec -T web pip install -r requirements-dev.txt
|
||||
docker compose exec -T web pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
3. Now you can run the existing tests
|
||||
|
||||
```
|
||||
docker-compose exec --env TESTING=True -T web pytest
|
||||
docker compose exec --env TESTING=True -T web pytest
|
||||
```
|
||||
|
||||
The `TESTING=True` is passed for Django to be aware this is a testing environment (so that it runs Celery tasks as functions for example and not as background tasks, since Celery is not started in the case of pytest)
|
||||
@@ -143,13 +143,13 @@ The `TESTING=True` is passed for Django to be aware this is a testing environmen
|
||||
4. You may try a single test, by specifying the path, for example
|
||||
|
||||
```
|
||||
docker-compose exec --env TESTING=True -T web pytest tests/test_fixtures.py
|
||||
docker compose exec --env TESTING=True -T web pytest tests/test_fixtures.py
|
||||
```
|
||||
|
||||
5. You can also see the coverage
|
||||
|
||||
```
|
||||
docker-compose exec --env TESTING=True -T web pytest --cov=. --cov-report=html
|
||||
docker compose exec --env TESTING=True -T web pytest --cov=. --cov-report=html
|
||||
```
|
||||
|
||||
and of course...you are very welcome to help us increase it ;)
|
||||
|
||||
BIN
docs/images/cookie_consent.png
Normal file
BIN
docs/images/cookie_consent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 580 KiB |
166
docs/media_permissions.md
Normal file
166
docs/media_permissions.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Media Permissions in MediaCMS
|
||||
|
||||
This document explains the permission system in MediaCMS, which controls who can view, edit, and manage media files.
|
||||
|
||||
## Overview
|
||||
|
||||
MediaCMS provides a flexible permission system that allows fine-grained control over media access. The system supports:
|
||||
|
||||
1. **Basic permissions** - Public, private, and unlisted media
|
||||
2. **User-specific permissions** - Direct permissions granted to specific users
|
||||
3. **Role-Based Access Control (RBAC)** - Category-based permissions through group membership
|
||||
|
||||
## Media States
|
||||
|
||||
Every media file has a state that determines its basic visibility:
|
||||
|
||||
- **Public** - Visible to everyone
|
||||
- **Private** - Only visible to the owner and users with explicit permissions
|
||||
- **Unlisted** - Not listed in public listings but accessible via direct link
|
||||
|
||||
|
||||
## User Roles
|
||||
|
||||
MediaCMS has several user roles that affect permissions:
|
||||
|
||||
- **Regular User** - Can upload and manage their own media
|
||||
- **Advanced User** - Additional capabilities (configurable)
|
||||
- **MediaCMS Editor** - Can edit and review content across the platform
|
||||
- **MediaCMS Manager** - Full management capabilities
|
||||
- **Admin** - Complete system access
|
||||
|
||||
## Direct Media Permissions
|
||||
|
||||
The `MediaPermission` model allows granting specific permissions to individual users:
|
||||
|
||||
### Permission Levels
|
||||
|
||||
- **Viewer** - Can view the media even if it's private
|
||||
- **Editor** - Can view and edit the media's metadata
|
||||
- **Owner** - Full control, including deletion
|
||||
|
||||
## Role-Based Access Control (RBAC)
|
||||
|
||||
When RBAC is enabled (`USE_RBAC` setting), permissions can be managed through categories and groups:
|
||||
|
||||
1. Categories can be marked as RBAC-controlled
|
||||
2. Users are assigned to RBAC groups with specific roles
|
||||
3. RBAC groups are associated with categories
|
||||
4. Users inherit permissions to media in those categories based on their role
|
||||
|
||||
### RBAC Roles
|
||||
|
||||
- **Member** - Can view media in the category
|
||||
- **Contributor** - Can view and edit media in the category
|
||||
- **Manager** - Full control over media in the category
|
||||
|
||||
## Permission Checking Methods
|
||||
|
||||
The User model provides several methods to check permissions:
|
||||
|
||||
```python
|
||||
# From users/models.py
|
||||
def has_member_access_to_media(self, media):
|
||||
# Check if user can view the media
|
||||
# ...
|
||||
|
||||
def has_contributor_access_to_media(self, media):
|
||||
# Check if user can edit the media
|
||||
# ...
|
||||
|
||||
def has_owner_access_to_media(self, media):
|
||||
# Check if user has full control over the media
|
||||
# ...
|
||||
```
|
||||
|
||||
## How Permissions Are Applied
|
||||
|
||||
When a user attempts to access media, the system checks permissions in this order:
|
||||
|
||||
1. Is the media public? If yes, allow access.
|
||||
2. Is the user the owner of the media? If yes, allow full access.
|
||||
3. Does the user have direct permissions through MediaPermission? If yes, grant the corresponding access level.
|
||||
4. If RBAC is enabled, does the user have access through category membership? If yes, grant the corresponding access level.
|
||||
5. If none of the above, deny access.
|
||||
|
||||
## Media Sharing
|
||||
|
||||
Users can share media with others by:
|
||||
|
||||
1. Making it public or unlisted
|
||||
2. Granting direct permissions to specific users
|
||||
3. Adding it to a category that's accessible to an RBAC group
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Media Listing
|
||||
|
||||
When listing media, the system filters based on permissions:
|
||||
|
||||
```python
|
||||
# Simplified example from files/views/media.py
|
||||
def _get_media_queryset(self, request, user=None):
|
||||
# 1. Public media
|
||||
listable_media = Media.objects.filter(listable=True)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return listable_media
|
||||
|
||||
# 2. User permissions for authenticated users
|
||||
user_media = Media.objects.filter(permissions__user=request.user)
|
||||
|
||||
# 3. RBAC for authenticated users
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_categories = request.user.get_rbac_categories_as_member()
|
||||
rbac_media = Media.objects.filter(category__in=rbac_categories)
|
||||
|
||||
# Combine all accessible media
|
||||
return listable_media.union(user_media, rbac_media)
|
||||
```
|
||||
|
||||
### Permission Checking
|
||||
|
||||
The system uses helper methods to check permissions:
|
||||
|
||||
```python
|
||||
# From users/models.py
|
||||
def has_member_access_to_media(self, media):
|
||||
# First check if user is the owner
|
||||
if media.user == self:
|
||||
return True
|
||||
|
||||
# Then check RBAC permissions
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_groups = RBACGroup.objects.filter(
|
||||
memberships__user=self,
|
||||
memberships__role__in=["member", "contributor", "manager"],
|
||||
categories__in=media.category.all()
|
||||
).distinct()
|
||||
if rbac_groups.exists():
|
||||
return True
|
||||
|
||||
# Then check MediaShare permissions for any access
|
||||
media_permission_exists = MediaPermission.objects.filter(
|
||||
user=self,
|
||||
media=media,
|
||||
).exists()
|
||||
|
||||
return media_permission_exists
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Default to Private** - Consider setting new uploads to private by default
|
||||
2. **Use Categories** - Organize media into categories for easier permission management
|
||||
3. **RBAC for Teams** - Use RBAC for team collaboration scenarios
|
||||
4. **Direct Permissions for Exceptions** - Use direct permissions for one-off sharing
|
||||
|
||||
## Configuration
|
||||
|
||||
The permission system can be configured through several settings:
|
||||
|
||||
- `USE_RBAC` - Enable/disable Role-Based Access Control
|
||||
|
||||
## Conclusion
|
||||
|
||||
MediaCMS provides a flexible and powerful permission system that can accommodate various use cases, from simple personal media libraries to complex team collaboration scenarios with fine-grained access control.
|
||||
315
docs/saml_entraid_setup.md
Normal file
315
docs/saml_entraid_setup.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# Integrating Microsoft Entra ID (formerly Azure AD) with MediaCMS via SAML Authentication
|
||||
|
||||
This guide provides step-by-step instructions on how to configure Microsoft Entra ID as a SAML Identity Provider (IdP) for MediaCMS, an open-source content management system. The goal is to enable single sign-on (SSO) authentication for users in a secure and scalable way.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Prerequisites](#prerequisites)
|
||||
3. [Step 1: Configure MediaCMS for SAML](#step-1-configure-mediacms-for-saml)
|
||||
4. [Step 2: Register MediaCMS as an Enterprise App in Entra ID](#step-2-register-mediacms-as-an-enterprise-app-in-entra-id)
|
||||
5. [Step 3: Configure SAML Settings in Entra ID](#step-3-configure-saml-settings-in-entra-id)
|
||||
6. [Step 4: Configure SAML Settings in MediaCMS](#step-4-configure-saml-settings-in-mediacms)
|
||||
7. [Step 5: Allow Users or Groups to Log Into the Application](#step-5-allow-users-or-groups-to-log-into-the-application)
|
||||
8. [Step 6: Test and Validate Login Flow](#step-6-test-and-validate-login-flow)
|
||||
9. [Troubleshooting](#troubleshooting)
|
||||
10. [Resources](#resources)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
MediaCMS supports SAML 2.0 authentication by acting as a Service Provider (SP). By integrating with Microsoft Entra ID, organizations can allow users to authenticate using their existing enterprise credentials.
|
||||
|
||||
In our particular deployment of MediaCMS, the application is hosted internally with no direct inbound access from the public Internet. As an internal company application, it was essential to integrate it with our existing authentication systems and provide a seamless single sign-on experience. This is where the SAML protocol shines.
|
||||
|
||||
One of the major advantages of SAML authentication is that all communication between the Identity Provider (IdP) — in this case, Microsoft Entra ID — and the Service Provider (SP) — MediaCMS — is brokered entirely by the end user's browser. The browser initiates the authentication flow, communicates securely with Microsoft’s login portal, receives the identity assertion, and then passes it back to the internal MediaCMS server.
|
||||
|
||||
This architecture enables the MediaCMS server to remain isolated from the Internet while still participating in a modern and seamless federated login experience.
|
||||
|
||||
Even though the deployment method outlined in this tutorial is for EntraID on an isolated MediaCMS server, the same steps and general information could be applied to another authentication SAML provider/identity provider on a non-isolated system.
|
||||
|
||||
> **Note**: This guide assumes you are running MediaCMS with Django backend and that the `django-allauth` library is enabled and configured.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before beginning, ensure the following:
|
||||
|
||||
* You have administrator access to both MediaCMS and Microsoft Entra ID (Azure portal).
|
||||
* MediaCMS is installed and accessible via HTTPS, with a valid SSL certificate.
|
||||
* Your MediaCMS installation has SAML support enabled (via `django-allauth`).
|
||||
* You have a dedicated domain or subdomain for MediaCMS (e.g., `https://<MyMediaCMS.MyDomain.com>`).
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Configure MediaCMS for SAML
|
||||
|
||||
The first step in enabling SAML authentication is to modify the `local_settings.py` (for Docker: `./config/local_settings.py`) file of your MediaCMS deployment. Add the following configuration block to enable SAML support, role-based access control (RBAC), and enforce secure communication settings:
|
||||
|
||||
```python
|
||||
USE_RBAC = True
|
||||
USE_SAML = True
|
||||
USE_IDENTITY_PROVIDERS = True
|
||||
|
||||
USE_X_FORWARDED_HOST = True
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
SECURE_SSL_REDIRECT = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
SOCIALACCOUNT_ADAPTER = 'saml_auth.adapter.SAMLAccountAdapter'
|
||||
SOCIALACCOUNT_PROVIDERS = {
|
||||
"saml": {
|
||||
"provider_class": "saml_auth.custom.provider.CustomSAMLProvider",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These settings enable SAML authentication, configure MediaCMS to respect role-based access, and apply important headers and cookie policies for secure browser handling — all of which are necessary for the SAML flow to function properly.
|
||||
|
||||
> ⚠️ **Important**: After updating the `local_settings.py` file, you must restart your MediaCMS service (e.g., by rebooting the Docker container) in order for the changes to take effect. This step must be completed before proceeding to the next configuration stage.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Register MediaCMS as an Enterprise App in Entra ID
|
||||
|
||||
To begin the integration process on the Microsoft Entra ID (formerly Azure AD) side, follow the steps below to register MediaCMS as a new Enterprise Application.
|
||||
|
||||
### 1. Navigate to Enterprise Applications
|
||||
|
||||
* Log in to your [Azure Portal](https://portal.azure.com).
|
||||
* Navigate to **Enterprise Applications**.
|
||||
|
||||
> *Note: This guide assumes you already have an existing Azure tenant and Entra ID configured with users and groups.*
|
||||
|
||||
### 2. Create a New Application
|
||||
|
||||
* Click the **+ New Application** button.
|
||||
* On the next screen, choose **Create your own application**.
|
||||
* Enter a name for the application (e.g., `MediaCMS`).
|
||||
* Under "What are you looking to do with your application?", select **Integrate any other application you don't find in the gallery (Non-gallery)**.
|
||||
* Click **Create**.
|
||||
|
||||
After a few moments, Azure will create the new application and redirect you to its configuration page.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Configure SAML Settings in Entra ID
|
||||
|
||||
### 1. Configure SAML-Based Single Sign-On
|
||||
|
||||
* From the application overview page, in the left-hand menu under **Manage**, click **Single sign-on**.
|
||||
* You will be prompted to choose a sign-on method. Select **SAML**.
|
||||
|
||||
### 2. Choose a Client ID Name
|
||||
|
||||
Before filling out the SAML configuration, you must decide on a client ID name. This name will uniquely identify your SAML integration and appear in your login URL.
|
||||
|
||||
* Choose a name that is descriptive and easy to remember (e.g., `mediacms_entraid`).
|
||||
* You will use this name in both MediaCMS and Entra ID configuration settings.
|
||||
|
||||
### 3. Fill Out Basic SAML Configuration
|
||||
|
||||
Now input the following values under the **Basic SAML Configuration** section:
|
||||
|
||||
| Field | Value |
|
||||
| -------------------------- | --------------------------------------------------------------------- |
|
||||
| **Identifier (Entity ID)** | `https://<MyMediaCMS.MyDomain.com>/saml/metadata/` |
|
||||
| **Reply URL (ACS URL)** | `https://<MyMediaCMS.MyDomain.com>/accounts/saml/<MyClientID>/acs/` |
|
||||
| **Sign-on URL** | `https://<MyMediaCMS.MyDomain.com>/accounts/saml/<MyClientID>/login/` |
|
||||
| **Relay State (Optional)** | `https://<MyMediaCMS.MyDomain.com>/` |
|
||||
| **Logout URL (Optional)** | `https://<MyMediaCMS.MyDomain.com>/accounts/saml/<MyClientID>/sls/` |
|
||||
|
||||
> 🔐 Replace `<MyClientID>` with your own chosen client ID if different.
|
||||
|
||||
Once these fields are filled in, save your configuration.
|
||||
|
||||
Keep the Azure Enterprise single sign-on configuration window up, as we are now going to configure some of the details from this Azure page into our MediaCMS system.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Configure SAML Settings in MediaCMS
|
||||
|
||||
In MediaCMS, start by logging into the back-end administrative web page. You will now have new options under the left-hand menu bar.
|
||||
|
||||
### 1. Add Login Option
|
||||
|
||||
* Navigate to **Identity Providers → Login Options**.
|
||||
|
||||
* Click **Add Login Option**.
|
||||
|
||||
* Give the login option a title. This title can be anything you like but it will appear to the end-user when they select a method of logging in, so ensure the name is clear. (e.g., `EntraID-SSO`).
|
||||
|
||||
* Set the **Login URL** to the same Sign-on URL:
|
||||
|
||||
```
|
||||
https://<MyMediaCMS.MyDomain.com>/accounts/saml/<MyClientID>/login/
|
||||
```
|
||||
|
||||
* Leave the ordering at `0` if you have no other authentication methods.
|
||||
|
||||
* Ensure the **Active** box is checked to make this an active login method.
|
||||
|
||||
* Click **Save** to continue.
|
||||
|
||||
### 2. Add ID Provider
|
||||
|
||||
* Navigate to **Identity Providers → ID Providers**.
|
||||
* Click **Add ID Provider**.
|
||||
|
||||
Back in your Azure Enterprise application configuration window (at the bottom of the Single Sign-On configuration menu), find your application-specific details. They will look like the following example:
|
||||
|
||||
```
|
||||
Example unique AppID: 123456ab-1234-12ab-ab12-abc123abc123
|
||||
The unique AppID is automatically generated when you create the application.
|
||||
|
||||
-- Example URLs --
|
||||
Login URL: https://login.microsoftonline.com/123456ab-1234-12ab-ab12-abc123abc123/saml2
|
||||
Microsoft Entra Identifier: https://sts.windows.net/123456ab-1234-12ab-ab12-abc123abc123/
|
||||
Logout URL: https://login.microsoftonline.com/123456ab-1234-12ab-ab12-abc123abc123/saml2
|
||||
```
|
||||
|
||||
Back in MediaCMS's new ID Provider window, under the **General** tab:
|
||||
|
||||
* **Protocol**: `saml` (all lowercase)
|
||||
* **Provider ID**: The Microsoft Entra Identifier (as shown above), the whole URL.
|
||||
* **IDP Configuration Name**: Any unique name (e.g., `EntraID`)
|
||||
* **Client ID**: The exact same client ID you used earlier when configuring EntraID (e.g., `mediacms_entraid`).
|
||||
* **Sites**: Add all the sites you want this login to appear on (e.g., all of them)
|
||||
|
||||
Click **Save and Continue**, then go to the **SAML Configuration** tab.
|
||||
|
||||
On the **SAML Configuration** tab:
|
||||
|
||||
* **SSO URL**: Use the same Logon URL from EntraID example listed above.
|
||||
|
||||
* **SLO URL**: Use the Logout URL from EntraID example listed above.
|
||||
|
||||
* **SP Metadata URL**:
|
||||
|
||||
```
|
||||
https://<MyMediaCMS.MyDomain.com>/saml/metadata/
|
||||
```
|
||||
|
||||
* **IdP ID**: Use the same Microsoft Entra Identifier URL as listed above.
|
||||
|
||||
#### LDP Certificate
|
||||
|
||||
Back in Azure's Enterprise Application page (SAML certificates section), download the **Base64 Certificate**, open it in a text editor, and copy the contents into the **LDP Certificate** setting inside of MediaCMS.
|
||||
|
||||
### 3. Configure Identity Mappings
|
||||
|
||||
Map the identity attributes that Entra ID will provide to MediaCMS. Even though only UID is specified as mandatory, Entra ID will not work unless all of these details are filled in(YES, you must type NA in the fields; you cannot leave anything blank. You will get 500 errors if this is not done). You can use the exact settings below:
|
||||
|
||||
| Field | Value |
|
||||
| -------------- | -------------------------------------------------------------------- |
|
||||
| **Uid** | `http://schemas.microsoft.com/identity/claims/objectidentifier` |
|
||||
| **Name** | `http://schemas.microsoft.com/identity/claims/displayname` |
|
||||
| **Email** | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress` |
|
||||
| **Groups** | `NA` |
|
||||
| **First name** | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname` |
|
||||
| **Last name** | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname` |
|
||||
| **User logo** | `NA` |
|
||||
| **Role** | `NA` |
|
||||
|
||||
> ℹ️ Groups and Role can be changed or remapped inside the Azure Enterprise Application under **Attributes and Claims**.
|
||||
|
||||
Check the **Verified Email** box (since EntraID will verify the user for you). While setting up, you can enable **Save SAML Response Log** for troubleshooting purposes.
|
||||
|
||||
Finally, click **Save** to finish adding the new ID provider.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Allow Users or Groups to Log Into the Application
|
||||
|
||||
Back inside Azure AD, within your MediaCMS Enterprise Application, you must assign users or groups that are allowed to use the MediaCMS authentication sign-on.
|
||||
|
||||
### 1. Navigate to Users and Groups
|
||||
|
||||
* Open the Azure Portal and go to your **MediaCMS Enterprise Application**.
|
||||
* In the left-hand **Manage** menu, click **Users and Groups**.
|
||||
|
||||
### 2. Assign Users or Groups
|
||||
|
||||
* Add individual users or groups of users who are allowed to use the EntraID authentication method with MediaCMS.
|
||||
* In this example, the application was provided to all registered users inside of EntraID by using the special group **All Users**, which grants any registered user in the tenant access to MediaCMS.
|
||||
|
||||
> ⚠️ **Important**: Nested groups will not work. All users must be directly assigned to the group you are giving permission to. If a group contains another group, the users of the nested group will not inherit the permissions to use this application from the parent group.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Test and Validate Login Flow
|
||||
|
||||
At this point, you should go to your MediaCMS webpage and attempt to log in using the authentication method that you have just set up.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you're experiencing logon issues, it is helpful to first review the SAML authentication data directly.
|
||||
|
||||
1. Go to MediaCMS's login page. It should redirect you to Microsoft's login page.
|
||||
2. Before completing the Microsoft authentication, open Firefox or Chrome Developer Tools (press **F12**) and navigate to the **Network** tab.
|
||||
3. Enable **Persistent Logging**.
|
||||
4. Complete the Microsoft authentication steps on your page (including two-factor authentication if enabled).
|
||||
|
||||
On the final step of the authentication (usually after entering a code and confirming "Stay signed in?"), you will see several POST requests going back to your MediaCMS server URL. Find the POST request that is going to your MediaCMS server's Assertion Consumer Service (ACS) URL, which will look like this:
|
||||
|
||||
```
|
||||
https://<MyMediaCMS.MyDomain.com>/accounts/saml/<MyClientID>/acs/
|
||||
```
|
||||
|
||||
Inside the request section of the Network tab, you will see a **Form Data** field labeled **SAMLResponse**, which contains a Base64-encoded XML string of your authenticated assertion from EntraID.
|
||||
|
||||
* Click into the data field of the SAML response so you can highlight and copy all of the Base64-encoded text.
|
||||
* You can then take this Base64-encoded text to a tool like [CyberChef](https://gchq.github.io/CyberChef/) and use the **From Base64** decoder and **XML Beautify** to reveal the XML-formatted SAML response.
|
||||
|
||||
This decoded XML contains all the assertion and token details passed back to MediaCMS. You can use this information to troubleshoot any issues or misconfigurations that arise.
|
||||
|
||||
You can also confirm your MediaCMS server has the SAML authentication settings correct by opening a private browsing window and navigating to the following URL, which will output the current XML data that your MediaCMS server is configured with:
|
||||
|
||||
```
|
||||
https://<MyMediaCMS.MyDomain.com>/saml/metadata/
|
||||
```
|
||||
|
||||
You can use the returned XML data from this URL to confirm that MediaCMS is configured appropriately as expected and is providing the correct information to the identity provider.
|
||||
|
||||
### Infinite Redirect Loop
|
||||
|
||||
Another issue you might encounter is an **infinite redirect loop**. This can happen when global login is enforced and local user login is disabled.
|
||||
|
||||
**Symptoms:** The system continuously redirects between the homepage and the login URL.
|
||||
|
||||
**Root Cause:** With global login required and local login disabled, Django attempts to redirect users to the default local login page. Since that login method is unavailable, users are bounced back to the homepage, triggering the same redirect logic again — resulting in a loop.
|
||||
|
||||
**Solution:** Specify the correct SAML authentication URL in your local settings. For example:
|
||||
|
||||
* "Login Option" URL configured for EntraID in MediaCMS:
|
||||
|
||||
```
|
||||
https://<MyDomainName>/accounts/saml/mediacms_entraid/login/
|
||||
```
|
||||
|
||||
* Add the following line to `./config/local_settings.py`:
|
||||
|
||||
```python
|
||||
LOGIN_URL = "/accounts/saml/mediacms_entraid/login/"
|
||||
```
|
||||
|
||||
This change ensures Django uses the proper SAML login route, breaking the redirect loop and allowing authentication via EntraID as intended.
|
||||
|
||||
> **Note:** The `LOGIN_URL` setting works because we are using the Django AllAuth module to perform the SAML authentication. If you review the AllAuth Django configuration settings, you will find that this is a setting, among other settings, that you can set inside of your local settings file that Django will pick up when using the AllAuth module. You can review the module documentation at the following URL for more details and additional settings that can be set through AllAuth via `local_settings.py`: [https://django-allauth.readthedocs.io/en/latest/account/configuration.html](https://django-allauth.readthedocs.io/en/latest/account/configuration.html)
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
* [MediaCMS SAML Docs](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#24-identity-providers-setup)
|
||||
* [Enable SAML single sign-on for an enterprise application](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal-setup-sso)
|
||||
* [Django AllAuth](https://django-allauth.readthedocs.io/en/latest/index.html)
|
||||
|
||||
---
|
||||
|
||||
*This documentation is a work-in-progress and will be updated as further steps are dictated or completed.*
|
||||
50
docs/transcoding.md
Normal file
50
docs/transcoding.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Transcoding in MediaCMS
|
||||
|
||||
MediaCMS uses FFmpeg for transcoding media files. Most of the transcoding settings and configurations are defined in `files/helpers.py`.
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Several transcoding parameters can be customized in `cms/settings.py`:
|
||||
|
||||
### FFmpeg Preset
|
||||
|
||||
The default FFmpeg preset is set to "medium". This setting controls the encoding speed and compression efficiency trade-off.
|
||||
|
||||
```python
|
||||
# ffmpeg options
|
||||
FFMPEG_DEFAULT_PRESET = "medium" # see https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||
```
|
||||
|
||||
Available presets include:
|
||||
- ultrafast
|
||||
- superfast
|
||||
- veryfast
|
||||
- faster
|
||||
- fast
|
||||
- medium (default)
|
||||
- slow
|
||||
- slower
|
||||
- veryslow
|
||||
|
||||
Faster presets result in larger file sizes for the same quality, while slower presets provide better compression but take longer to encode.
|
||||
|
||||
### Other Transcoding Settings
|
||||
|
||||
Additional transcoding settings in `settings.py` include:
|
||||
|
||||
- `FFMPEG_COMMAND`: Path to the FFmpeg executable
|
||||
- `FFPROBE_COMMAND`: Path to the FFprobe executable
|
||||
- `DO_NOT_TRANSCODE_VIDEO`: If set to True, only the original video is shown without transcoding
|
||||
- `CHUNKIZE_VIDEO_DURATION`: For videos longer than this duration (in seconds), they get split into chunks and encoded independently
|
||||
- `VIDEO_CHUNKS_DURATION`: Duration of each chunk (must be smaller than CHUNKIZE_VIDEO_DURATION)
|
||||
- `MINIMUM_RESOLUTIONS_TO_ENCODE`: Always encode these resolutions, even if upscaling is required
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
For more advanced transcoding settings, you may need to modify the following in `files/helpers.py`:
|
||||
|
||||
- Video bitrates for different codecs and resolutions
|
||||
- Audio encoders and bitrates
|
||||
- CRF (Constant Rate Factor) values
|
||||
- Keyframe settings
|
||||
- Encoding parameters for different codecs (H.264, H.265, VP9)
|
||||
@@ -11,6 +11,7 @@
|
||||
- [Share media](#share-media)
|
||||
- [Embed media](#embed-media)
|
||||
- [Customize my profile options](#customize-my-profile-options)
|
||||
- [Trim videos](#trim-videos)
|
||||
|
||||
## Uploading media
|
||||
|
||||
@@ -198,7 +199,7 @@ You can now watch the captions/subtitles play back in the video player - and tog
|
||||
<img src="./images/CC-display.png"/>
|
||||
</p>
|
||||
|
||||
## Using Timestamps for sharing
|
||||
## Using Timestamps for sharing
|
||||
|
||||
### Using Timestamp in the URL
|
||||
|
||||
@@ -240,7 +241,7 @@ Comments send with mentions will contain a link to the user page, and can be set
|
||||
When enabled, comments including a timestamp will also be displayed in the current video Timebar as a little colorful dot. The comment can be previewed by hovering the dot (left image) and it will be displayed on top of the video when reaching the correct time (right image).
|
||||
|
||||
Only comments with correct timestamps formats (HH:MM:SS or MM:SS) will be picked up and appear in the Timebar.
|
||||
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/TimebarComments_Hover.png" height="180" alt="Comment preview on hover"/>
|
||||
<img src="./images/TimebarComments_Hit.png" height="180" alt="Comment shown when the timestamp is reached "/>
|
||||
@@ -257,3 +258,7 @@ How to use the embed media option
|
||||
|
||||
## Customize my profile options
|
||||
Customize profile and channel
|
||||
|
||||
## Trim videos
|
||||
Once a video is uploaded, you can trim it to create a new video or to replace the original one. You can also create segments of the video, which will be available as separate videos. Edit the video and click on the "Trime Video" option. If the original video has finished processing (encodings are created for all resolutions), then this is an action that runs instantly. If the original video hasn't processed, which is the case when you upload a video and edit it right away, then the trim action will trigger processing of the video and will take some time to finish. In all cases, you get to see the original video (or the trimmed versions) immediately, so you are sure of what you have uploaded or trimmed, with a message that the video is being processed.
|
||||
|
||||
|
||||
203
files/admin.py
203
files/admin.py
@@ -1,4 +1,11 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from tinymce.widgets import TinyMCE
|
||||
|
||||
from rbac.models import RBACGroup
|
||||
|
||||
from .models import (
|
||||
Category,
|
||||
@@ -7,8 +14,12 @@ from .models import (
|
||||
Encoding,
|
||||
Language,
|
||||
Media,
|
||||
Page,
|
||||
Subtitle,
|
||||
Tag,
|
||||
TinyMCEMedia,
|
||||
TranscriptionRequest,
|
||||
VideoTrimRequest,
|
||||
)
|
||||
|
||||
|
||||
@@ -40,15 +51,135 @@ class MediaAdmin(admin.ModelAdmin):
|
||||
def get_comments_count(self, obj):
|
||||
return obj.comments.count()
|
||||
|
||||
@admin.action(description="Generate missing encoding(s)", permissions=["change"])
|
||||
def generate_missing_encodings(modeladmin, request, queryset):
|
||||
for m in queryset:
|
||||
m.encode(force=False)
|
||||
|
||||
actions = [generate_missing_encodings]
|
||||
get_comments_count.short_description = "Comments count"
|
||||
|
||||
|
||||
class CategoryAdminForm(forms.ModelForm):
|
||||
rbac_groups = forms.ModelMultipleChoiceField(queryset=RBACGroup.objects.all(), required=False, widget=admin.widgets.FilteredSelectMultiple('Groups', False))
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = '__all__'
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
is_rbac_category = cleaned_data.get('is_rbac_category')
|
||||
identity_provider = cleaned_data.get('identity_provider')
|
||||
# Check if this category has any RBAC groups
|
||||
if self.instance.pk:
|
||||
has_rbac_groups = cleaned_data.get('rbac_groups')
|
||||
else:
|
||||
has_rbac_groups = False
|
||||
|
||||
if not is_rbac_category:
|
||||
if has_rbac_groups:
|
||||
cleaned_data['is_rbac_category'] = True
|
||||
# self.add_error('is_rbac_category', ValidationError('This category has RBAC groups assigned. "Is RBAC Category" must be enabled.'))
|
||||
|
||||
for rbac_group in cleaned_data.get('rbac_groups'):
|
||||
if rbac_group.identity_provider != identity_provider:
|
||||
self.add_error('rbac_groups', ValidationError('Chosen Groups are associated with a different Identity Provider than the one selected here.'))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.instance.pk:
|
||||
self.fields['rbac_groups'].initial = self.instance.rbac_groups.all()
|
||||
|
||||
def save(self, commit=True):
|
||||
category = super().save(commit=True)
|
||||
|
||||
if commit:
|
||||
self.save_m2m()
|
||||
|
||||
if self.instance.rbac_groups.exists() or self.cleaned_data.get('rbac_groups'):
|
||||
if not self.cleaned_data['is_rbac_category']:
|
||||
category.is_rbac_category = True
|
||||
category.save(update_fields=['is_rbac_category'])
|
||||
return category
|
||||
|
||||
@transaction.atomic
|
||||
def save_m2m(self):
|
||||
if self.instance.pk:
|
||||
rbac_groups = self.cleaned_data['rbac_groups']
|
||||
self._update_rbac_groups(rbac_groups)
|
||||
|
||||
def _update_rbac_groups(self, rbac_groups):
|
||||
new_rbac_group_ids = RBACGroup.objects.filter(pk__in=rbac_groups).values_list('pk', flat=True)
|
||||
|
||||
existing_rbac_groups = RBACGroup.objects.filter(categories=self.instance)
|
||||
existing_rbac_groups_ids = existing_rbac_groups.values_list('pk', flat=True)
|
||||
|
||||
rbac_groups_to_add = RBACGroup.objects.filter(pk__in=new_rbac_group_ids).exclude(pk__in=existing_rbac_groups_ids)
|
||||
rbac_groups_to_remove = existing_rbac_groups.exclude(pk__in=new_rbac_group_ids)
|
||||
|
||||
for rbac_group in rbac_groups_to_add:
|
||||
rbac_group.categories.add(self.instance)
|
||||
|
||||
for rbac_group in rbac_groups_to_remove:
|
||||
rbac_group.categories.remove(self.instance)
|
||||
|
||||
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
search_fields = ["title"]
|
||||
list_display = ["title", "user", "add_date", "is_global", "media_count"]
|
||||
list_filter = ["is_global"]
|
||||
form = CategoryAdminForm
|
||||
|
||||
search_fields = ["title", "uid"]
|
||||
list_display = ["title", "user", "add_date", "media_count"]
|
||||
list_filter = []
|
||||
ordering = ("-add_date",)
|
||||
readonly_fields = ("user", "media_count")
|
||||
change_form_template = 'admin/files/category/change_form.html'
|
||||
|
||||
def get_list_filter(self, request):
|
||||
list_filter = list(self.list_filter)
|
||||
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
list_filter.insert(0, "is_rbac_category")
|
||||
if getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
|
||||
list_filter.insert(-1, "identity_provider")
|
||||
|
||||
return list_filter
|
||||
|
||||
def get_list_display(self, request):
|
||||
list_display = list(self.list_display)
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
list_display.insert(-1, "is_rbac_category")
|
||||
if getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
|
||||
list_display.insert(-1, "identity_provider")
|
||||
|
||||
return list_display
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
basic_fieldset = [
|
||||
(
|
||||
'Category Information',
|
||||
{
|
||||
'fields': ['uid', 'title', 'description', 'user', 'media_count', 'thumbnail', 'listings_thumbnail'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_fieldset = [
|
||||
('RBAC Settings', {'fields': ['is_rbac_category'], 'classes': ['tab'], 'description': 'Role-Based Access Control settings'}),
|
||||
('Group Access', {'fields': ['rbac_groups'], 'description': 'Select the Groups that have access to category'}),
|
||||
]
|
||||
if getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
|
||||
rbac_fieldset = [
|
||||
('RBAC Settings', {'fields': ['is_rbac_category', 'identity_provider'], 'classes': ['tab'], 'description': 'Role-Based Access Control settings'}),
|
||||
('Group Access', {'fields': ['rbac_groups'], 'description': 'Select the Groups that have access to category'}),
|
||||
]
|
||||
return basic_fieldset + rbac_fieldset
|
||||
else:
|
||||
return basic_fieldset
|
||||
|
||||
|
||||
class TagAdmin(admin.ModelAdmin):
|
||||
@@ -70,11 +201,68 @@ class LanguageAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
class SubtitleAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
list_display = ["id", "language", "media"]
|
||||
list_filter = ["language"]
|
||||
search_fields = ["media__title"]
|
||||
readonly_fields = ("media", "user")
|
||||
|
||||
|
||||
class VideoTrimRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ["media", "status", "add_date", "video_action", "media_trim_style", "timestamps"]
|
||||
list_filter = ["status", "video_action", "media_trim_style", "add_date"]
|
||||
search_fields = ["media__title"]
|
||||
readonly_fields = ("add_date",)
|
||||
ordering = ("-add_date",)
|
||||
|
||||
|
||||
class EncodingAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
list_display = ["get_title", "chunk", "profile", "progress", "status", "has_file"]
|
||||
list_filter = ["chunk", "profile", "status"]
|
||||
|
||||
def get_title(self, obj):
|
||||
return str(obj)
|
||||
|
||||
get_title.short_description = "Encoding"
|
||||
|
||||
def has_file(self, obj):
|
||||
return obj.media_encoding_url is not None
|
||||
|
||||
has_file.short_description = "Has file"
|
||||
|
||||
|
||||
class TranscriptionRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ["media", "add_date", "status", "translate_to_english"]
|
||||
list_filter = ["status", "translate_to_english", "add_date"]
|
||||
search_fields = ["media__title"]
|
||||
readonly_fields = ("add_date", "logs")
|
||||
ordering = ("-add_date",)
|
||||
|
||||
|
||||
class PageAdminForm(forms.ModelForm):
|
||||
description = forms.CharField(widget=TinyMCE())
|
||||
|
||||
def clean_description(self):
|
||||
content = self.cleaned_data['description']
|
||||
# Add sandbox attribute to all iframes
|
||||
content = content.replace('<iframe ', '<iframe sandbox="allow-scripts allow-same-origin allow-presentation" ')
|
||||
return content
|
||||
|
||||
class Meta:
|
||||
model = Page
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class PageAdmin(admin.ModelAdmin):
|
||||
form = PageAdminForm
|
||||
|
||||
|
||||
@admin.register(TinyMCEMedia)
|
||||
class TinyMCEMediaAdmin(admin.ModelAdmin):
|
||||
list_display = ['original_filename', 'file_type', 'uploaded_at', 'user']
|
||||
list_filter = ['file_type', 'uploaded_at']
|
||||
search_fields = ['original_filename']
|
||||
readonly_fields = ['uploaded_at']
|
||||
date_hierarchy = 'uploaded_at'
|
||||
|
||||
|
||||
admin.site.register(EncodeProfile, EncodeProfileAdmin)
|
||||
@@ -82,6 +270,11 @@ admin.site.register(Comment, CommentAdmin)
|
||||
admin.site.register(Media, MediaAdmin)
|
||||
admin.site.register(Encoding, EncodingAdmin)
|
||||
admin.site.register(Category, CategoryAdmin)
|
||||
admin.site.register(Page, PageAdmin)
|
||||
admin.site.register(Tag, TagAdmin)
|
||||
admin.site.register(Subtitle, SubtitleAdmin)
|
||||
admin.site.register(Language, LanguageAdmin)
|
||||
admin.site.register(VideoTrimRequest, VideoTrimRequestAdmin)
|
||||
admin.site.register(TranscriptionRequest, TranscriptionRequestAdmin)
|
||||
|
||||
Media._meta.app_config.verbose_name = "Media"
|
||||
|
||||
@@ -15,7 +15,7 @@ class VideoEncodingError(Exception):
|
||||
|
||||
|
||||
RE_TIMECODE = re.compile(r"time=(\d+:\d+:\d+.\d+)")
|
||||
console_encoding = locale.getdefaultlocale()[1] or "UTF-8"
|
||||
console_encoding = locale.getlocale()[1] or "UTF-8"
|
||||
|
||||
|
||||
class FFmpegBackend(object):
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from django.conf import settings
|
||||
|
||||
from cms.version import VERSION
|
||||
|
||||
from .frontend_translations import get_translation, get_translation_strings
|
||||
from .methods import is_mediacms_editor, is_mediacms_manager
|
||||
|
||||
|
||||
@@ -9,6 +12,12 @@ def stuff(request):
|
||||
ret["FRONTEND_HOST"] = request.build_absolute_uri('/').rstrip('/')
|
||||
ret["DEFAULT_THEME"] = settings.DEFAULT_THEME
|
||||
ret["PORTAL_NAME"] = settings.PORTAL_NAME
|
||||
|
||||
ret["PORTAL_LOGO_DARK_SVG"] = getattr(settings, 'PORTAL_LOGO_DARK_SVG', "")
|
||||
ret["PORTAL_LOGO_DARK_PNG"] = getattr(settings, 'PORTAL_LOGO_DARK_PNG', "")
|
||||
ret["PORTAL_LOGO_LIGHT_SVG"] = getattr(settings, 'PORTAL_LOGO_LIGHT_SVG', "")
|
||||
ret["PORTAL_LOGO_LIGHT_PNG"] = getattr(settings, 'PORTAL_LOGO_LIGHT_PNG', "")
|
||||
ret["EXTRA_CSS_PATHS"] = getattr(settings, 'EXTRA_CSS_PATHS', [])
|
||||
ret["PORTAL_DESCRIPTION"] = settings.PORTAL_DESCRIPTION
|
||||
ret["LOAD_FROM_CDN"] = settings.LOAD_FROM_CDN
|
||||
ret["CAN_LOGIN"] = settings.LOGIN_ALLOWED
|
||||
@@ -23,12 +32,35 @@ def stuff(request):
|
||||
ret["UPLOAD_MAX_SIZE"] = settings.UPLOAD_MAX_SIZE
|
||||
ret["UPLOAD_MAX_FILES_NUMBER"] = settings.UPLOAD_MAX_FILES_NUMBER
|
||||
ret["PRE_UPLOAD_MEDIA_MESSAGE"] = settings.PRE_UPLOAD_MEDIA_MESSAGE
|
||||
ret["SIDEBAR_FOOTER_TEXT"] = settings.SIDEBAR_FOOTER_TEXT
|
||||
ret["POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY"] = settings.POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY
|
||||
ret["IS_MEDIACMS_ADMIN"] = request.user.is_superuser
|
||||
ret["IS_MEDIACMS_EDITOR"] = is_mediacms_editor(request.user)
|
||||
ret["IS_MEDIACMS_MANAGER"] = is_mediacms_manager(request.user)
|
||||
ret["USERS_NEEDS_TO_BE_APPROVED"] = settings.USERS_NEEDS_TO_BE_APPROVED
|
||||
|
||||
can_see_members_page = False
|
||||
if request.user.is_authenticated:
|
||||
if settings.CAN_SEE_MEMBERS_PAGE == "all":
|
||||
can_see_members_page = True
|
||||
elif settings.CAN_SEE_MEMBERS_PAGE == "editors" and is_mediacms_editor(request.user):
|
||||
can_see_members_page = True
|
||||
elif settings.CAN_SEE_MEMBERS_PAGE == "admins" and request.user.is_superuser:
|
||||
can_see_members_page = True
|
||||
ret["CAN_SEE_MEMBERS_PAGE"] = can_see_members_page
|
||||
ret["ALLOW_RATINGS"] = settings.ALLOW_RATINGS
|
||||
ret["ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY"] = settings.ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY
|
||||
ret["VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE"] = settings.VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE
|
||||
ret["RSS_URL"] = "/rss"
|
||||
ret["TRANSLATION"] = get_translation(request.LANGUAGE_CODE)
|
||||
ret["REPLACEMENTS"] = get_translation_strings(request.LANGUAGE_CODE)
|
||||
ret["USE_SAML"] = settings.USE_SAML
|
||||
ret["USE_RBAC"] = settings.USE_RBAC
|
||||
ret["USE_ROUNDED_CORNERS"] = settings.USE_ROUNDED_CORNERS
|
||||
ret["INCLUDE_LISTING_NUMBERS"] = settings.INCLUDE_LISTING_NUMBERS
|
||||
ret["VERSION"] = VERSION
|
||||
|
||||
if request.user.is_superuser:
|
||||
ret["DJANGO_ADMIN_URL"] = settings.DJANGO_ADMIN_URL
|
||||
|
||||
return ret
|
||||
|
||||
@@ -83,7 +83,7 @@ class IndexRSSFeed(Feed):
|
||||
return item.edit_date
|
||||
|
||||
def item_link(self, item):
|
||||
return reverse("get_media") + "?m={0}".format(item.friendly_token)
|
||||
return f"{reverse('get_media')}?m={item.friendly_token}"
|
||||
|
||||
def item_extra_kwargs(self, item):
|
||||
item = {
|
||||
@@ -151,7 +151,7 @@ class SearchRSSFeed(Feed):
|
||||
return item.edit_date
|
||||
|
||||
def item_link(self, item):
|
||||
return reverse("get_media") + "?m={0}".format(item.friendly_token)
|
||||
return f"{reverse('get_media')}?m={item.friendly_token}"
|
||||
|
||||
def item_extra_kwargs(self, item):
|
||||
item = {
|
||||
|
||||
276
files/forms.py
276
files/forms.py
@@ -1,48 +1,104 @@
|
||||
from crispy_forms.bootstrap import FormActions
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import HTML, Field, Layout, Submit
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
from .methods import get_next_state, is_mediacms_editor
|
||||
from .models import Media, Subtitle
|
||||
from .models import MEDIA_STATES, Category, Media, Subtitle
|
||||
|
||||
|
||||
class CustomField(Field):
|
||||
template = 'cms/crispy_custom_field.html'
|
||||
|
||||
|
||||
class MultipleSelect(forms.CheckboxSelectMultiple):
|
||||
input_type = "checkbox"
|
||||
|
||||
|
||||
class MediaForm(forms.ModelForm):
|
||||
new_tags = forms.CharField(label="Tags", help_text="a comma separated list of new tags.", required=False)
|
||||
class MediaMetadataForm(forms.ModelForm):
|
||||
new_tags = forms.CharField(label="Tags", help_text="a comma separated list of tags.", required=False)
|
||||
|
||||
class Meta:
|
||||
model = Media
|
||||
fields = (
|
||||
"friendly_token",
|
||||
"title",
|
||||
"category",
|
||||
"new_tags",
|
||||
"add_date",
|
||||
"uploaded_poster",
|
||||
"description",
|
||||
"state",
|
||||
"enable_comments",
|
||||
"featured",
|
||||
"thumbnail_time",
|
||||
"reported_times",
|
||||
"is_reviewed",
|
||||
"allow_download",
|
||||
)
|
||||
|
||||
widgets = {
|
||||
"tags": MultipleSelect(),
|
||||
"new_tags": MultipleSelect(),
|
||||
"description": forms.Textarea(attrs={'rows': 4}),
|
||||
"add_date": forms.DateTimeInput(attrs={'type': 'datetime-local', 'step': '1'}, format='%Y-%m-%dT%H:%M:%S'),
|
||||
"thumbnail_time": forms.NumberInput(attrs={'min': 0, 'step': 0.1}),
|
||||
}
|
||||
labels = {
|
||||
"friendly_token": "Slug",
|
||||
"uploaded_poster": "Poster Image",
|
||||
"thumbnail_time": "Thumbnail Time (seconds)",
|
||||
}
|
||||
help_texts = {
|
||||
"title": "",
|
||||
"friendly_token": "Media URL slug",
|
||||
"thumbnail_time": "Select the time in seconds for the video thumbnail",
|
||||
"uploaded_poster": "Maximum file size: 5MB",
|
||||
}
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.user = user
|
||||
super(MediaForm, self).__init__(*args, **kwargs)
|
||||
super(MediaMetadataForm, self).__init__(*args, **kwargs)
|
||||
if not getattr(settings, 'ALLOW_CUSTOM_MEDIA_URLS', False):
|
||||
self.fields.pop("friendly_token")
|
||||
if self.instance.media_type != "video":
|
||||
self.fields.pop("thumbnail_time")
|
||||
if not is_mediacms_editor(user):
|
||||
self.fields.pop("featured")
|
||||
self.fields.pop("reported_times")
|
||||
self.fields.pop("is_reviewed")
|
||||
if self.instance.media_type == "image":
|
||||
self.fields.pop("uploaded_poster")
|
||||
|
||||
self.fields["new_tags"].initial = ", ".join([tag.title for tag in self.instance.tags.all()])
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = True
|
||||
self.helper.form_class = 'post-form'
|
||||
self.helper.form_method = 'post'
|
||||
self.helper.form_enctype = "multipart/form-data"
|
||||
self.helper.form_show_errors = False
|
||||
|
||||
layout_fields = [
|
||||
CustomField('title'),
|
||||
CustomField('new_tags'),
|
||||
CustomField('add_date'),
|
||||
CustomField('description'),
|
||||
CustomField('enable_comments'),
|
||||
]
|
||||
if self.instance.media_type != "image":
|
||||
layout_fields.append(CustomField('uploaded_poster'))
|
||||
|
||||
self.helper.layout = Layout(*layout_fields)
|
||||
|
||||
if self.instance.media_type == "video":
|
||||
self.helper.layout.append(CustomField('thumbnail_time'))
|
||||
if getattr(settings, 'ALLOW_CUSTOM_MEDIA_URLS', False):
|
||||
self.helper.layout.insert(0, CustomField('friendly_token'))
|
||||
|
||||
self.helper.layout.append(FormActions(Submit('submit', 'Update Media', css_class='primaryAction')))
|
||||
|
||||
def clean_friendly_token(self):
|
||||
token = self.cleaned_data.get("friendly_token", "").strip()
|
||||
|
||||
if token:
|
||||
if not all(c.isalnum() or c in "-_" for c in token):
|
||||
raise forms.ValidationError("Slug can only contain alphanumeric characters, underscores, or hyphens.")
|
||||
|
||||
if Media.objects.filter(friendly_token=token).exclude(pk=self.instance.pk).exists():
|
||||
raise forms.ValidationError("This slug is already in use. Please choose a different one.")
|
||||
return token
|
||||
|
||||
def clean_uploaded_poster(self):
|
||||
image = self.cleaned_data.get("uploaded_poster", False)
|
||||
if image:
|
||||
@@ -50,31 +106,219 @@ class MediaForm(forms.ModelForm):
|
||||
raise forms.ValidationError("Image file too large ( > 5mb )")
|
||||
return image
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
data = self.cleaned_data # noqa
|
||||
|
||||
media = super(MediaMetadataForm, self).save(*args, **kwargs)
|
||||
return media
|
||||
|
||||
|
||||
class MediaPublishForm(forms.ModelForm):
|
||||
confirm_state = forms.BooleanField(required=False, initial=False, label="Acknowledge sharing status", help_text="")
|
||||
|
||||
class Meta:
|
||||
model = Media
|
||||
fields = ("category", "state", "featured", "reported_times", "is_reviewed", "allow_download")
|
||||
|
||||
widgets = {
|
||||
"category": MultipleSelect(),
|
||||
}
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.user = user
|
||||
super(MediaPublishForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if not is_mediacms_editor(user):
|
||||
for field in ["featured", "reported_times", "is_reviewed"]:
|
||||
self.fields[field].disabled = True
|
||||
self.fields[field].widget.attrs['class'] = 'read-only-field'
|
||||
self.fields[field].widget.attrs['title'] = "This field can only be modified by MediaCMS admins or editors"
|
||||
|
||||
if settings.PORTAL_WORKFLOW not in ["public"]:
|
||||
valid_states = ["unlisted", "private"]
|
||||
if self.instance.state and self.instance.state not in valid_states:
|
||||
valid_states.append(self.instance.state)
|
||||
self.fields["state"].choices = [(state, dict(MEDIA_STATES).get(state, state)) for state in valid_states]
|
||||
|
||||
if getattr(settings, 'USE_RBAC', False) and 'category' in self.fields:
|
||||
if is_mediacms_editor(user):
|
||||
pass
|
||||
else:
|
||||
self.fields['category'].initial = self.instance.category.all()
|
||||
|
||||
non_rbac_categories = Category.objects.filter(is_rbac_category=False)
|
||||
rbac_categories = user.get_rbac_categories_as_contributor()
|
||||
combined_category_ids = list(non_rbac_categories.values_list('id', flat=True)) + list(rbac_categories.values_list('id', flat=True))
|
||||
|
||||
if self.instance.pk:
|
||||
instance_category_ids = list(self.instance.category.all().values_list('id', flat=True))
|
||||
combined_category_ids = list(set(combined_category_ids + instance_category_ids))
|
||||
|
||||
self.fields['category'].queryset = Category.objects.filter(id__in=combined_category_ids).order_by('title')
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = True
|
||||
self.helper.form_class = 'post-form'
|
||||
self.helper.form_method = 'post'
|
||||
self.helper.form_enctype = "multipart/form-data"
|
||||
self.helper.form_show_errors = False
|
||||
self.helper.layout = Layout(
|
||||
CustomField('category'),
|
||||
CustomField('state'),
|
||||
CustomField('featured'),
|
||||
CustomField('reported_times'),
|
||||
CustomField('is_reviewed'),
|
||||
CustomField('allow_download'),
|
||||
)
|
||||
|
||||
self.helper.layout.append(FormActions(Submit('submit', 'Publish Media', css_class='primaryAction')))
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
state = cleaned_data.get("state")
|
||||
categories = cleaned_data.get("category")
|
||||
|
||||
if state in ['private', 'unlisted']:
|
||||
custom_permissions = self.instance.permissions.exists()
|
||||
rbac_categories = categories.filter(is_rbac_category=True).values_list('title', flat=True)
|
||||
if rbac_categories or custom_permissions:
|
||||
self.fields['confirm_state'].widget = forms.CheckboxInput()
|
||||
state_index = None
|
||||
for i, layout_item in enumerate(self.helper.layout):
|
||||
if isinstance(layout_item, CustomField) and layout_item.fields[0] == 'state':
|
||||
state_index = i
|
||||
break
|
||||
|
||||
if state_index:
|
||||
layout_items = list(self.helper.layout)
|
||||
layout_items.insert(state_index + 1, CustomField('confirm_state'))
|
||||
self.helper.layout = Layout(*layout_items)
|
||||
|
||||
if not cleaned_data.get('confirm_state'):
|
||||
if rbac_categories:
|
||||
error_message = f"I understand that although media state is {state}, the media is also shared with users that have access to categories: {', '.join(rbac_categories)}"
|
||||
self.add_error('confirm_state', error_message)
|
||||
if custom_permissions:
|
||||
error_message = f"I understand that although media state is {state}, the media is also shared by me with other users, that I can see in the 'Shared by me' page"
|
||||
self.add_error('confirm_state', error_message)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
data = self.cleaned_data
|
||||
state = data.get("state")
|
||||
if state != self.initial["state"]:
|
||||
self.instance.state = get_next_state(self.user, self.initial["state"], self.instance.state)
|
||||
|
||||
media = super(MediaForm, self).save(*args, **kwargs)
|
||||
media = super(MediaPublishForm, self).save(*args, **kwargs)
|
||||
|
||||
return media
|
||||
|
||||
|
||||
class WhisperSubtitlesForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Media
|
||||
fields = (
|
||||
"allow_whisper_transcribe",
|
||||
"allow_whisper_transcribe_and_translate",
|
||||
)
|
||||
labels = {
|
||||
"allow_whisper_transcribe": "Transcription",
|
||||
"allow_whisper_transcribe_and_translate": "English Translation",
|
||||
}
|
||||
help_texts = {
|
||||
"allow_whisper_transcribe": "",
|
||||
"allow_whisper_transcribe_and_translate": "",
|
||||
}
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.user = user
|
||||
super(WhisperSubtitlesForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if self.instance.allow_whisper_transcribe:
|
||||
self.fields['allow_whisper_transcribe'].widget.attrs['readonly'] = True
|
||||
self.fields['allow_whisper_transcribe'].widget.attrs['disabled'] = True
|
||||
if self.instance.allow_whisper_transcribe_and_translate:
|
||||
self.fields['allow_whisper_transcribe_and_translate'].widget.attrs['readonly'] = True
|
||||
self.fields['allow_whisper_transcribe_and_translate'].widget.attrs['disabled'] = True
|
||||
|
||||
both_readonly = self.instance.allow_whisper_transcribe and self.instance.allow_whisper_transcribe_and_translate
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = True
|
||||
self.helper.form_class = 'post-form'
|
||||
self.helper.form_method = 'post'
|
||||
self.helper.form_enctype = "multipart/form-data"
|
||||
self.helper.form_show_errors = False
|
||||
self.helper.layout = Layout(
|
||||
CustomField('allow_whisper_transcribe'),
|
||||
CustomField('allow_whisper_transcribe_and_translate'),
|
||||
)
|
||||
|
||||
if not both_readonly:
|
||||
self.helper.layout.append(FormActions(Submit('submit_whisper', 'Submit', css_class='primaryAction')))
|
||||
else:
|
||||
# Optional: Add a disabled button with explanatory text
|
||||
self.helper.layout.append(
|
||||
FormActions(Submit('submit_whisper', 'Submit', css_class='primaryAction', disabled=True), HTML('<small class="text-muted">Cannot submit - both options are already enabled</small>'))
|
||||
)
|
||||
|
||||
def clean_allow_whisper_transcribe(self):
|
||||
# Ensure the field value doesn't change if it was originally True
|
||||
if self.instance and self.instance.allow_whisper_transcribe:
|
||||
return self.instance.allow_whisper_transcribe
|
||||
return self.cleaned_data['allow_whisper_transcribe']
|
||||
|
||||
def clean_allow_whisper_transcribe_and_translate(self):
|
||||
# Ensure the field value doesn't change if it was originally True
|
||||
if self.instance and self.instance.allow_whisper_transcribe_and_translate:
|
||||
return self.instance.allow_whisper_transcribe_and_translate
|
||||
return self.cleaned_data['allow_whisper_transcribe_and_translate']
|
||||
|
||||
|
||||
class SubtitleForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Subtitle
|
||||
fields = ["language", "subtitle_file"]
|
||||
|
||||
labels = {
|
||||
"subtitle_file": "Upload Caption File",
|
||||
}
|
||||
help_texts = {
|
||||
"subtitle_file": "SubRip (.srt) and WebVTT (.vtt) are supported file formats.",
|
||||
}
|
||||
|
||||
def __init__(self, media_item, *args, **kwargs):
|
||||
super(SubtitleForm, self).__init__(*args, **kwargs)
|
||||
self.instance.media = media_item
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = True
|
||||
self.helper.form_class = 'post-form'
|
||||
self.helper.form_method = 'post'
|
||||
self.helper.form_enctype = "multipart/form-data"
|
||||
self.helper.form_show_errors = False
|
||||
self.helper.layout = Layout(
|
||||
CustomField('subtitle_file'),
|
||||
CustomField('language'),
|
||||
)
|
||||
|
||||
self.helper.layout.append(FormActions(Submit('submit', 'Submit', css_class='primaryAction')))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.instance.user = self.instance.media.user
|
||||
media = super(SubtitleForm, self).save(*args, **kwargs)
|
||||
return media
|
||||
|
||||
|
||||
class EditSubtitleForm(forms.Form):
|
||||
subtitle = forms.CharField(widget=forms.Textarea, required=True)
|
||||
|
||||
def __init__(self, subtitle, *args, **kwargs):
|
||||
super(EditSubtitleForm, self).__init__(*args, **kwargs)
|
||||
self.fields["subtitle"].initial = subtitle.subtitle_file.read().decode("utf-8")
|
||||
|
||||
|
||||
class ContactForm(forms.Form):
|
||||
from_email = forms.EmailField(required=True)
|
||||
name = forms.CharField(required=False)
|
||||
|
||||
60
files/frontend_translations/__init__.py
Normal file
60
files/frontend_translations/__init__.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import importlib
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
files = os.listdir(current_dir)
|
||||
translation_strings = {}
|
||||
replacement_strings = {}
|
||||
|
||||
|
||||
def check_language_code(language_code):
|
||||
# helper function
|
||||
if language_code not in [pair[0] for pair in settings.LANGUAGES]:
|
||||
return False
|
||||
if language_code in ['en', 'en-us', 'en-gb']:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
for translation_file in files:
|
||||
# the language code is zh-hans but the file is zh_hans.py
|
||||
|
||||
language_code_file = translation_file.split('.')[0]
|
||||
language_code = language_code_file.replace('_', '-')
|
||||
if not check_language_code(language_code):
|
||||
continue
|
||||
|
||||
module_name = f"files.frontend_translations.{language_code_file}"
|
||||
tr_module = importlib.import_module(module_name)
|
||||
translation_strings[language_code] = tr_module.translation_strings
|
||||
replacement_strings[language_code] = tr_module.replacement_strings
|
||||
|
||||
|
||||
def get_translation(language_code):
|
||||
# get list of translations per language
|
||||
if not check_language_code(language_code):
|
||||
return {}
|
||||
|
||||
translation = translation_strings[language_code]
|
||||
|
||||
return translation
|
||||
|
||||
|
||||
def get_translation_strings(language_code):
|
||||
# get list of replacement strings per language
|
||||
if not check_language_code(language_code):
|
||||
return {}
|
||||
|
||||
translation = replacement_strings[language_code]
|
||||
|
||||
return translation
|
||||
|
||||
|
||||
def translate_string(language_code, string):
|
||||
# translate a string to the given language
|
||||
if not check_language_code(language_code):
|
||||
return string
|
||||
|
||||
return translation_strings[language_code].get(string, string)
|
||||
288
files/frontend_translations/ar.py
Normal file
288
files/frontend_translations/ar.py
Normal file
@@ -0,0 +1,288 @@
|
||||
translation_strings = {
|
||||
"+ Create Playlist": "+ إنشاء قائمة تشغيل",
|
||||
"00 - 20 min": "00 - 20 دقيقة",
|
||||
"1 result for": "نتيجة واحدة لـ",
|
||||
"20 - 40 min": "20 - 40 دقيقة",
|
||||
"40 - 60 min": "40 - 60 دقيقة",
|
||||
"60 - 120 min+": "60 - 120 دقيقة+",
|
||||
"ABOUT": "حول",
|
||||
"AUTOPLAY": "تشغيل تلقائي",
|
||||
"About": "حول",
|
||||
"Add / Remove Co-Editors": "إضافة / إزالة المحررين المشاركين",
|
||||
"Add / Remove Co-Owners": "إضافة / إزالة المالكين المشاركين",
|
||||
"Add / Remove Co-Viewers": "إضافة / إزالة المشاهدين المشاركين",
|
||||
"Add / Remove Tags": "إضافة / إزالة العلامات",
|
||||
"Add / Remove from Categories": "إضافة / إزالة من الفئات",
|
||||
"Add a ": "أضف ",
|
||||
"Add to": "إضافة إلى",
|
||||
"Add to / Remove from Category": "إضافة / إزالة من الفئة",
|
||||
"Add to / Remove from Playlist": "إضافة / إزالة من قائمة التشغيل",
|
||||
"All": "الكل",
|
||||
"All categories already added": "تمت إضافة جميع الفئات بالفعل",
|
||||
"All tags already added": "تمت إضافة جميع العلامات بالفعل",
|
||||
"Alphabetically - A-Z": "أبجدياً - أ-ي",
|
||||
"Alphabetically - Z-A": "أبجدياً - ي-أ",
|
||||
"Audio": "صوت",
|
||||
"Browse your files": "تصفح ملفاتك",
|
||||
"Bulk Actions": "إجراءات جماعية",
|
||||
"COMMENT": "تعليق",
|
||||
"Cancel": "إلغاء",
|
||||
"Categories": "الفئات",
|
||||
"Category": "الفئة",
|
||||
"Change Language": "تغيير اللغة",
|
||||
"Change Owner": "تغيير المالك",
|
||||
"Change password": "تغيير كلمة المرور",
|
||||
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "انقر على 'بدء التسجيل' واختر الشاشة أو علامة التبويب المراد تسجيلها. بمجرد الانتهاء من التسجيل، انقر على 'إيقاف التسجيل'، وسيتم تحميل التسجيل.",
|
||||
"Co-Editors": "المحررون المشاركون",
|
||||
"Co-Owners": "المالكون المشاركون",
|
||||
"Co-Viewers": "المشاهدون المشاركون",
|
||||
"Comment": "تعليق",
|
||||
"Comments": "تعليقات",
|
||||
"Comments are disabled": "التعليقات معطلة",
|
||||
"Confirm": "تأكيد",
|
||||
"Confirm Action": "تأكيد الإجراء",
|
||||
"Contact": "اتصل",
|
||||
"Copy Media": "نسخ الوسائط",
|
||||
"Create": "إنشاء",
|
||||
"DELETE": "حذف",
|
||||
"DELETE MEDIA": "حذف الوسائط",
|
||||
"DOWNLOAD": "تحميل",
|
||||
"DURATION": "المدة",
|
||||
"Delete Media": "حذف الوسائط",
|
||||
"Delete media": "حذف الوسائط",
|
||||
"Disable Comments": "تعطيل التعليقات",
|
||||
"Disable Download": "تعطيل التنزيل",
|
||||
"Drag and drop files": "سحب وإفلات الملفات",
|
||||
"EDIT MEDIA": "تعديل الوسائط",
|
||||
"EDIT PROFILE": "تعديل الملف الشخصي",
|
||||
"EDIT SUBTITLE": "تعديل الترجمة",
|
||||
"Edit media": "تعديل الوسائط",
|
||||
"Edit profile": "تعديل الملف الشخصي",
|
||||
"Edit subtitle": "تعديل الترجمة",
|
||||
"Enable Comments": "تفعيل التعليقات",
|
||||
"Enable Download": "تفعيل التنزيل",
|
||||
"Enter playlist name...": "أدخل اسم قائمة التشغيل...",
|
||||
"Failed to add categories": "فشل إضافة الفئات",
|
||||
"Failed to add media to playlists": "فشل إضافة الوسائط إلى قوائم التشغيل",
|
||||
"Failed to add tags": "فشل إضافة العلامات",
|
||||
"Failed to add users": "فشل إضافة المستخدمين",
|
||||
"Failed to change owner": "فشل تغيير المالك",
|
||||
"Failed to change owner. Please try again.": "فشل تغيير المالك. يرجى المحاولة مرة أخرى.",
|
||||
"Failed to copy media.": "فشل نسخ الوسائط.",
|
||||
"Failed to create playlist": "فشل إنشاء قائمة التشغيل",
|
||||
"Failed to delete media. Please try again.": "فشل حذف الوسائط. يرجى المحاولة مرة أخرى.",
|
||||
"Failed to disable comments.": "فشل تعطيل التعليقات.",
|
||||
"Failed to disable download.": "فشل تعطيل التنزيل.",
|
||||
"Failed to enable comments.": "فشل تفعيل التعليقات.",
|
||||
"Failed to enable download.": "فشل تفعيل التنزيل.",
|
||||
"Failed to fetch all categories": "فشل جلب جميع الفئات",
|
||||
"Failed to fetch all tags": "فشل جلب جميع العلامات",
|
||||
"Failed to fetch existing categories": "فشل جلب الفئات الموجودة",
|
||||
"Failed to fetch existing tags": "فشل جلب العلامات الموجودة",
|
||||
"Failed to fetch existing users": "فشل جلب المستخدمين الموجودين",
|
||||
"Failed to fetch playlist membership": "فشل جلب عضوية قائمة التشغيل",
|
||||
"Failed to fetch playlists": "فشل جلب قوائم التشغيل",
|
||||
"Failed to load categories": "فشل تحميل الفئات",
|
||||
"Failed to load existing permissions": "فشل تحميل الأذونات الموجودة",
|
||||
"Failed to load playlists": "فشل تحميل قوائم التشغيل",
|
||||
"Failed to load tags": "فشل تحميل العلامات",
|
||||
"Failed to remove categories": "فشل إزالة الفئات",
|
||||
"Failed to remove media from playlists": "فشل إزالة الوسائط من قوائم التشغيل",
|
||||
"Failed to remove tags": "فشل إزالة العلامات",
|
||||
"Failed to remove users": "فشل إزالة المستخدمين",
|
||||
"Failed to search users": "فشل البحث عن المستخدمين",
|
||||
"Failed to set publish state": "فشل تعيين حالة النشر",
|
||||
"Failed to set publish state. Please try again.": "فشل تعيين حالة النشر. يرجى المحاولة مرة أخرى.",
|
||||
"Failed to update categories. Please try again.": "فشل تحديث الفئات. يرجى المحاولة مرة أخرى.",
|
||||
"Failed to update permissions. Please try again.": "فشل تحديث الأذونات. يرجى المحاولة مرة أخرى.",
|
||||
"Failed to update playlists. Please try again.": "فشل تحديث قوائم التشغيل. يرجى المحاولة مرة أخرى.",
|
||||
"Failed to update tags. Please try again.": "فشل تحديث العلامات. يرجى المحاولة مرة أخرى.",
|
||||
"Featured": "مميز",
|
||||
"Filter existing users...": "تصفية المستخدمين الموجودين...",
|
||||
"Filter playlists...": "تصفية قوائم التشغيل...",
|
||||
"Filters": "الفلاتر",
|
||||
"Go": "اذهب",
|
||||
"History": "التاريخ",
|
||||
"Home": "الرئيسية",
|
||||
"Image": "صورة",
|
||||
"Language": "اللغة",
|
||||
"Latest": "الأحدث",
|
||||
"Like count": "عدد الإعجابات",
|
||||
"Liked media": "الوسائط المفضلة",
|
||||
"Likes - Least": "الإعجابات - الأقل",
|
||||
"Likes - Most": "الإعجابات - الأكثر",
|
||||
"Loading categories...": "جارٍ تحميل الفئات...",
|
||||
"Loading existing users...": "جارٍ تحميل المستخدمين الموجودين...",
|
||||
"Loading playlists...": "جارٍ تحميل قوائم التشغيل...",
|
||||
"Loading tags...": "جارٍ تحميل العلامات...",
|
||||
"MEDIA TYPE": "نوع الوسائط",
|
||||
"Manage": "إدارة",
|
||||
"Manage Playlists": "إدارة قوائم التشغيل",
|
||||
"Manage comments": "إدارة التعليقات",
|
||||
"Manage media": "إدارة الوسائط",
|
||||
"Manage users": "إدارة المستخدمين",
|
||||
"Media": "وسائط",
|
||||
"Media I own": "الوسائط التي أمتلكها",
|
||||
"Media was edited": "تم تعديل الوسائط",
|
||||
"Members": "الأعضاء",
|
||||
"My media": "وسائطي",
|
||||
"My playlists": "قوائم التشغيل الخاصة بي",
|
||||
"No": "لا",
|
||||
"No categories": "لا توجد فئات",
|
||||
"No comment yet": "لا يوجد تعليق بعد",
|
||||
"No comments yet": "لا توجد تعليقات بعد",
|
||||
"No existing": "لا يوجد موجود",
|
||||
"No playlists available": "لا توجد قوائم تشغيل متاحة",
|
||||
"No playlists selected": "لم يتم تحديد قوائم تشغيل",
|
||||
"No results for": "لا توجد نتائج لـ",
|
||||
"No tags": "لا توجد علامات",
|
||||
"No users to add": "لا يوجد مستخدمون لإضافتهم",
|
||||
"PLAYLISTS": "قوائم التشغيل",
|
||||
"PUBLISH STATE": "حالة النشر",
|
||||
"Pdf": "PDF",
|
||||
"Playlists": "قوائم التشغيل",
|
||||
"Plays - Least": "المشاهدات - الأقل",
|
||||
"Plays - Most": "المشاهدات - الأكثر",
|
||||
"Please select a publish state": "يرجى تحديد حالة النشر",
|
||||
"Please select a user": "يرجى تحديد مستخدم",
|
||||
"Powered by": "مدعوم من",
|
||||
"Private": "خاص",
|
||||
"Proceed": "متابعة",
|
||||
"Processing...": "جارٍ المعالجة...",
|
||||
"Public": "عام",
|
||||
"Publish": "نشر",
|
||||
"Publish State": "حالة النشر",
|
||||
"Published": "منشور",
|
||||
"Published on": "نشر في",
|
||||
"Recent uploads": "التحميلات الأخيرة",
|
||||
"Recommended": "موصى به",
|
||||
"Record Screen": "تسجيل الشاشة",
|
||||
"Register": "تسجيل",
|
||||
"Remove category": "إزالة الفئة",
|
||||
"Remove from list": "إزالة من القائمة",
|
||||
"Remove tag": "إزالة العلامة",
|
||||
"Remove user": "إزالة المستخدم",
|
||||
"SAVE": "حفظ",
|
||||
"SEARCH": "بحث",
|
||||
"SHARE": "مشاركة",
|
||||
"SHOW MORE": "عرض المزيد",
|
||||
"SORT BY": "ترتيب حسب",
|
||||
"SUBMIT": "إرسال",
|
||||
"Search": "بحث",
|
||||
"Search for user...": "البحث عن مستخدم...",
|
||||
"Search users to add...": "البحث عن مستخدمين لإضافتهم...",
|
||||
"Select": "اختر",
|
||||
"Select Owner": "اختر المالك",
|
||||
"Select all": "تحديد الكل",
|
||||
"Select all media": "تحديد جميع الوسائط",
|
||||
"Select publish state:": "اختر حالة النشر:",
|
||||
"Selected": "محدد",
|
||||
"Shared by me": "مشاركة مني",
|
||||
"Shared with me": "مشاركة معي",
|
||||
"Sign in": "تسجيل الدخول",
|
||||
"Sign out": "تسجيل الخروج",
|
||||
"Sort By": "ترتيب حسب",
|
||||
"Start Recording": "بدء التسجيل",
|
||||
"Start uploading media and sharing your work. Media that you upload will show up here.": "ابدأ في تحميل الوسائط ومشاركة عملك. ستظهر الوسائط التي تحملها هنا.",
|
||||
"Stop Recording": "إيقاف التسجيل",
|
||||
"Submit": "إرسال",
|
||||
"Subtitle was added": "تمت إضافة الترجمة",
|
||||
"Subtitles": "ترجمات",
|
||||
"Successfully Copied": "تم النسخ بنجاح",
|
||||
"Successfully Disabled Download": "تم تعطيل التنزيل بنجاح",
|
||||
"Successfully Disabled comments": "تم تعطيل التعليقات بنجاح",
|
||||
"Successfully Enabled Download": "تم تفعيل التنزيل بنجاح",
|
||||
"Successfully Enabled comments": "تم تفعيل التعليقات بنجاح",
|
||||
"Successfully changed owner": "تم تغيير المالك بنجاح",
|
||||
"Successfully deleted": "تم الحذف بنجاح",
|
||||
"Successfully updated": "تم التحديث بنجاح",
|
||||
"Successfully updated categories": "تم تحديث الفئات بنجاح",
|
||||
"Successfully updated playlist membership": "تم تحديث عضوية قائمة التشغيل بنجاح",
|
||||
"Successfully updated publish state": "تم تحديث حالة النشر بنجاح",
|
||||
"Successfully updated tags": "تم تحديث العلامات بنجاح",
|
||||
"TAGS": "العلامات",
|
||||
"Tag": "علامة",
|
||||
"Tags": "العلامات",
|
||||
"Terms": "الشروط",
|
||||
"The intersection of categories in the selected media is shown": "يتم عرض تقاطع الفئات في الوسائط المحددة",
|
||||
"The intersection of playlists in the selected media is shown": "يتم عرض تقاطع قوائم التشغيل في الوسائط المحددة",
|
||||
"The intersection of tags in the selected media is shown": "يتم عرض تقاطع العلامات في الوسائط المحددة",
|
||||
"The intersection of users in the selected media is shown": "يتم عرض تقاطع المستخدمين في الوسائط المحددة",
|
||||
"The media was deleted successfully.": "تم حذف الوسائط بنجاح.",
|
||||
"This month": "هذا الشهر",
|
||||
"This week": "هذا الأسبوع",
|
||||
"This works in Chrome, Safari and Edge browsers.": "هذا يعمل في متصفحات Chrome و Safari و Edge.",
|
||||
"This year": "هذا العام",
|
||||
"To add": "للإضافة",
|
||||
"Today": "اليوم",
|
||||
"Trim": "قص",
|
||||
"UPLOAD": "رفع",
|
||||
"UPLOAD DATE": "تاريخ التحميل",
|
||||
"UPLOAD MEDIA": "تحميل الوسائط",
|
||||
"Undo removal": "التراجع عن الإزالة",
|
||||
"Unlisted": "غير مدرج",
|
||||
"Up Next": "التالي",
|
||||
"Up next": "التالي",
|
||||
"Upload": "رفع",
|
||||
"Upload date (newest)": "تاريخ التحميل (الأحدث)",
|
||||
"Upload date (oldest)": "تاريخ التحميل (الأقدم)",
|
||||
"Upload date - Newest": "تاريخ التحميل - الأحدث",
|
||||
"Upload date - Oldest": "تاريخ التحميل - الأقدم",
|
||||
"Upload media": "رفع الوسائط",
|
||||
"Uploads": "التحميلات",
|
||||
"Users": "المستخدمون",
|
||||
"VIEW ALL": "عرض الكل",
|
||||
"Video": "فيديو",
|
||||
"View all": "عرض الكل",
|
||||
"View count": "عدد المشاهدات",
|
||||
"View media": "عرض الوسائط",
|
||||
"Welcome": "مرحباً",
|
||||
"You are going to copy": "سوف تقوم بالنسخ",
|
||||
"You are going to delete": "سوف تقوم بالحذف",
|
||||
"You are going to disable comments to": "سوف تقوم بتعطيل التعليقات لـ",
|
||||
"You are going to disable download for": "سوف تقوم بتعطيل التنزيل لـ",
|
||||
"You are going to enable comments to": "سوف تقوم بتفعيل التعليقات لـ",
|
||||
"You are going to enable download for": "سوف تقوم بتفعيل التنزيل لـ",
|
||||
"comment": "تعليق",
|
||||
"is a modern, fully featured open source video and media CMS. It is developed to meet the needs of modern web platforms for viewing and sharing media": "هو نظام إدارة محتوى فيديو ووسائط مفتوح المصدر وحديث ومتكامل. تم تطويره لتلبية احتياجات المنصات الويب الحديثة لمشاهدة ومشاركة الوسائط",
|
||||
"media in category": "وسائط في الفئة",
|
||||
"media in tag": "وسائط في العلامة",
|
||||
"media, are you sure?": "وسائط، هل أنت متأكد؟",
|
||||
"media.": "وسائط.",
|
||||
"or": "أو",
|
||||
"results for": "نتائج لـ",
|
||||
"selected": "محدد",
|
||||
"view": "عرض",
|
||||
"views": "مشاهدات",
|
||||
"yet": "بعد",
|
||||
}
|
||||
|
||||
replacement_strings = {
|
||||
"Apr": "أبريل",
|
||||
"Aug": "أغسطس",
|
||||
"Dec": "ديسمبر",
|
||||
"Feb": "فبراير",
|
||||
"Jan": "يناير",
|
||||
"Jul": "يوليو",
|
||||
"Jun": "يونيو",
|
||||
"Mar": "مارس",
|
||||
"May": "مايو",
|
||||
"Nov": "نوفمبر",
|
||||
"Oct": "أكتوبر",
|
||||
"Sep": "سبتمبر",
|
||||
"day ago": "منذ يوم",
|
||||
"days ago": "منذ أيام",
|
||||
"hour ago": "منذ ساعة",
|
||||
"hours ago": "منذ ساعات",
|
||||
"just now": "الآن",
|
||||
"minute ago": "منذ دقيقة",
|
||||
"minutes ago": "منذ دقائق",
|
||||
"month ago": "منذ شهر",
|
||||
"months ago": "منذ شهور",
|
||||
"second ago": "منذ ثانية",
|
||||
"seconds ago": "منذ ثوان",
|
||||
"week ago": "منذ أسبوع",
|
||||
"weeks ago": "منذ أسابيع",
|
||||
"year ago": "منذ سنة",
|
||||
"years ago": "منذ سنوات",
|
||||
}
|
||||
288
files/frontend_translations/bn.py
Normal file
288
files/frontend_translations/bn.py
Normal file
@@ -0,0 +1,288 @@
|
||||
translation_strings = {
|
||||
"+ Create Playlist": "",
|
||||
"00 - 20 min": "00 - 20 মিনিট",
|
||||
"1 result for": "1টি ফলাফল",
|
||||
"20 - 40 min": "20 - 40 মিনিট",
|
||||
"40 - 60 min": "40 - 60 মিনিট",
|
||||
"60 - 120 min+": "60 - 120 মিনিট+",
|
||||
"ABOUT": "সম্পর্কে",
|
||||
"AUTOPLAY": "স্বয়ংক্রিয় প্লে",
|
||||
"About": "সম্পর্কে",
|
||||
"Add / Remove Co-Editors": "",
|
||||
"Add / Remove Co-Owners": "",
|
||||
"Add / Remove Co-Viewers": "",
|
||||
"Add / Remove Tags": "",
|
||||
"Add / Remove from Categories": "",
|
||||
"Add a ": "যোগ করুন",
|
||||
"Add to": "",
|
||||
"Add to / Remove from Category": "",
|
||||
"Add to / Remove from Playlist": "",
|
||||
"All": "সব",
|
||||
"All categories already added": "",
|
||||
"All tags already added": "",
|
||||
"Alphabetically - A-Z": "বর্ণানুক্রমিক - A-Z",
|
||||
"Alphabetically - Z-A": "বর্ণানুক্রমিক - Z-A",
|
||||
"Audio": "অডিও",
|
||||
"Browse your files": "আপনার ফাইল ব্রাউজ করুন",
|
||||
"Bulk Actions": "",
|
||||
"COMMENT": "মন্তব্য",
|
||||
"Cancel": "",
|
||||
"Categories": "বিভাগসমূহ",
|
||||
"Category": "বিভাগ",
|
||||
"Change Language": "ভাষা পরিবর্তন করুন",
|
||||
"Change Owner": "",
|
||||
"Change password": "পাসওয়ার্ড পরিবর্তন করুন",
|
||||
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "'রেকর্ডিং শুরু করুন'-এ ক্লিক করুন এবং রেকর্ড করার জন্য স্ক্রিন বা ট্যাব নির্বাচন করুন। রেকর্ডিং শেষ হলে, 'রেকর্ডিং বন্ধ করুন'-এ ক্লিক করুন এবং রেকর্ডিং আপলোড হয়ে যাবে।",
|
||||
"Co-Editors": "",
|
||||
"Co-Owners": "",
|
||||
"Co-Viewers": "",
|
||||
"Comment": "মন্তব্য",
|
||||
"Comments": "মন্তব্যসমূহ",
|
||||
"Comments are disabled": "মন্তব্য নিষ্ক্রিয় করা হয়েছে",
|
||||
"Confirm": "",
|
||||
"Confirm Action": "",
|
||||
"Contact": "যোগাযোগ",
|
||||
"Copy Media": "",
|
||||
"Create": "",
|
||||
"DELETE": "মুছে ফেলুন",
|
||||
"DELETE MEDIA": "মিডিয়া মুছুন",
|
||||
"DOWNLOAD": "ডাউনলোড",
|
||||
"DURATION": "সময়কাল",
|
||||
"Delete Media": "",
|
||||
"Delete media": "মিডিয়া মুছুন",
|
||||
"Disable Comments": "",
|
||||
"Disable Download": "",
|
||||
"Drag and drop files": "ফাইল টেনে আনুন",
|
||||
"EDIT MEDIA": "মিডিয়া সম্পাদনা করুন",
|
||||
"EDIT PROFILE": "প্রোফাইল সম্পাদনা করুন",
|
||||
"EDIT SUBTITLE": "সাবটাইটেল সম্পাদনা করুন",
|
||||
"Edit media": "মিডিয়া সম্পাদনা করুন",
|
||||
"Edit profile": "প্রোফাইল সম্পাদনা করুন",
|
||||
"Edit subtitle": "সাবটাইটেল সম্পাদনা করুন",
|
||||
"Enable Comments": "",
|
||||
"Enable Download": "",
|
||||
"Enter playlist name...": "",
|
||||
"Failed to add categories": "",
|
||||
"Failed to add media to playlists": "",
|
||||
"Failed to add tags": "",
|
||||
"Failed to add users": "",
|
||||
"Failed to change owner": "",
|
||||
"Failed to change owner. Please try again.": "",
|
||||
"Failed to copy media.": "মিডিয়া কপি করতে ব্যর্থ হয়েছে।",
|
||||
"Failed to create playlist": "",
|
||||
"Failed to delete media. Please try again.": "মিডিয়া মুছতে ব্যর্থ হয়েছে। দয়া করে আবার চেষ্টা করুন।",
|
||||
"Failed to disable comments.": "মন্তব্য নিষ্ক্রিয় করতে ব্যর্থ হয়েছে।",
|
||||
"Failed to disable download.": "ডাউনলোড নিষ্ক্রিয় করতে ব্যর্থ হয়েছে।",
|
||||
"Failed to enable comments.": "মন্তব্য সক্রিয় করতে ব্যর্থ হয়েছে।",
|
||||
"Failed to enable download.": "ডাউনলোড সক্রিয় করতে ব্যর্থ হয়েছে।",
|
||||
"Failed to fetch all categories": "",
|
||||
"Failed to fetch all tags": "",
|
||||
"Failed to fetch existing categories": "",
|
||||
"Failed to fetch existing tags": "",
|
||||
"Failed to fetch existing users": "",
|
||||
"Failed to fetch playlist membership": "",
|
||||
"Failed to fetch playlists": "",
|
||||
"Failed to load categories": "",
|
||||
"Failed to load existing permissions": "",
|
||||
"Failed to load playlists": "",
|
||||
"Failed to load tags": "",
|
||||
"Failed to remove categories": "",
|
||||
"Failed to remove media from playlists": "",
|
||||
"Failed to remove tags": "",
|
||||
"Failed to remove users": "",
|
||||
"Failed to search users": "",
|
||||
"Failed to set publish state": "",
|
||||
"Failed to set publish state. Please try again.": "",
|
||||
"Failed to update categories. Please try again.": "",
|
||||
"Failed to update permissions. Please try again.": "",
|
||||
"Failed to update playlists. Please try again.": "",
|
||||
"Failed to update tags. Please try again.": "",
|
||||
"Featured": "বৈশিষ্ট্যযুক্ত",
|
||||
"Filter existing users...": "",
|
||||
"Filter playlists...": "",
|
||||
"Filters": "ফিল্টার",
|
||||
"Go": "যাও",
|
||||
"History": "ইতিহাস",
|
||||
"Home": "বাড়ি",
|
||||
"Image": "ছবি",
|
||||
"Language": "ভাষা",
|
||||
"Latest": "সর্বশেষ",
|
||||
"Like count": "পছন্দের সংখ্যা",
|
||||
"Liked media": "পছন্দের মিডিয়া",
|
||||
"Likes - Least": "পছন্দ - সবচেয়ে কম",
|
||||
"Likes - Most": "পছন্দ - সবচেয়ে বেশি",
|
||||
"Loading categories...": "",
|
||||
"Loading existing users...": "",
|
||||
"Loading playlists...": "",
|
||||
"Loading tags...": "",
|
||||
"MEDIA TYPE": "মিডিয়ার ধরন",
|
||||
"Manage": "",
|
||||
"Manage Playlists": "",
|
||||
"Manage comments": "মন্তব্য পরিচালনা করুন",
|
||||
"Manage media": "মিডিয়া পরিচালনা করুন",
|
||||
"Manage users": "ব্যবহারকারীদের পরিচালনা করুন",
|
||||
"Media": "মিডিয়া",
|
||||
"Media I own": "",
|
||||
"Media was edited": "মিডিয়া সম্পাদিত হয়েছে",
|
||||
"Members": "সদস্যরা",
|
||||
"My media": "আমার মিডিয়া",
|
||||
"My playlists": "আমার প্লেলিস্ট",
|
||||
"No": "না",
|
||||
"No categories": "",
|
||||
"No comment yet": "এখনও কোন মন্তব্য নেই",
|
||||
"No comments yet": "এখনও কোন মন্তব্য নেই",
|
||||
"No existing": "",
|
||||
"No playlists available": "",
|
||||
"No playlists selected": "",
|
||||
"No results for": "এর জন্য কোন ফলাফল নেই",
|
||||
"No tags": "",
|
||||
"No users to add": "",
|
||||
"PLAYLISTS": "প্লেলিস্ট",
|
||||
"PUBLISH STATE": "প্রকাশের অবস্থা",
|
||||
"Pdf": "PDF",
|
||||
"Playlists": "প্লেলিস্ট",
|
||||
"Plays - Least": "প্লে - সবচেয়ে কম",
|
||||
"Plays - Most": "প্লে - সবচেয়ে বেশি",
|
||||
"Please select a publish state": "",
|
||||
"Please select a user": "",
|
||||
"Powered by": "দ্বারা চালিত",
|
||||
"Private": "ব্যক্তিগত",
|
||||
"Proceed": "",
|
||||
"Processing...": "",
|
||||
"Public": "",
|
||||
"Publish": "প্রকাশ করুন",
|
||||
"Publish State": "",
|
||||
"Published": "প্রকাশিত",
|
||||
"Published on": "প্রকাশিত",
|
||||
"Recent uploads": "সাম্প্রতিক আপলোড",
|
||||
"Recommended": "প্রস্তাবিত",
|
||||
"Record Screen": "স্ক্রিন রেকর্ড করুন",
|
||||
"Register": "নিবন্ধন করুন",
|
||||
"Remove category": "",
|
||||
"Remove from list": "",
|
||||
"Remove tag": "",
|
||||
"Remove user": "",
|
||||
"SAVE": "সংরক্ষণ করুন",
|
||||
"SEARCH": "অনুসন্ধান",
|
||||
"SHARE": "শেয়ার করুন",
|
||||
"SHOW MORE": "আরও দেখুন",
|
||||
"SORT BY": "সাজান",
|
||||
"SUBMIT": "জমা দিন",
|
||||
"Search": "অনুসন্ধান",
|
||||
"Search for user...": "",
|
||||
"Search users to add...": "",
|
||||
"Select": "নির্বাচন করুন",
|
||||
"Select Owner": "",
|
||||
"Select all": "",
|
||||
"Select all media": "",
|
||||
"Select publish state:": "",
|
||||
"Selected": "",
|
||||
"Shared by me": "আমার দ্বারা শেয়ার করা",
|
||||
"Shared with me": "আমার সাথে শেয়ার করা",
|
||||
"Sign in": "সাইন ইন করুন",
|
||||
"Sign out": "সাইন আউট করুন",
|
||||
"Sort By": "সাজান",
|
||||
"Start Recording": "রেকর্ডিং শুরু করুন",
|
||||
"Start uploading media and sharing your work. Media that you upload will show up here.": "মিডিয়া আপলোড করা এবং আপনার কাজ শেয়ার করা শুরু করুন। আপনি যে মিডিয়া আপলোড করবেন তা এখানে প্রদর্শিত হবে।",
|
||||
"Stop Recording": "রেকর্ডিং বন্ধ করুন",
|
||||
"Submit": "",
|
||||
"Subtitle was added": "সাবটাইটেল যোগ করা হয়েছে",
|
||||
"Subtitles": "সাবটাইটেল",
|
||||
"Successfully Copied": "সফলভাবে কপি হয়েছে",
|
||||
"Successfully Disabled Download": "ডাউনলোড সফলভাবে নিষ্ক্রিয় হয়েছে",
|
||||
"Successfully Disabled comments": "মন্তব্য সফলভাবে নিষ্ক্রিয় হয়েছে",
|
||||
"Successfully Enabled Download": "ডাউনলোড সফলভাবে সক্রিয় হয়েছে",
|
||||
"Successfully Enabled comments": "মন্তব্য সফলভাবে সক্রিয় হয়েছে",
|
||||
"Successfully changed owner": "",
|
||||
"Successfully deleted": "সফলভাবে মুছে ফেলা হয়েছে",
|
||||
"Successfully updated": "",
|
||||
"Successfully updated categories": "",
|
||||
"Successfully updated playlist membership": "",
|
||||
"Successfully updated publish state": "",
|
||||
"Successfully updated tags": "",
|
||||
"TAGS": "ট্যাগ",
|
||||
"Tag": "ট্যাগ",
|
||||
"Tags": "ট্যাগ",
|
||||
"Terms": "শর্তাবলী",
|
||||
"The intersection of categories in the selected media is shown": "",
|
||||
"The intersection of playlists in the selected media is shown": "",
|
||||
"The intersection of tags in the selected media is shown": "",
|
||||
"The intersection of users in the selected media is shown": "",
|
||||
"The media was deleted successfully.": "মিডিয়া সফলভাবে মুছে ফেলা হয়েছে।",
|
||||
"This month": "এই মাসে",
|
||||
"This week": "এই সপ্তাহে",
|
||||
"This works in Chrome, Safari and Edge browsers.": "এটি ক্রোম, সাফারি এবং এজ ব্রাউজারে কাজ করে।",
|
||||
"This year": "এই বছর",
|
||||
"To add": "",
|
||||
"Today": "আজ",
|
||||
"Trim": "ছাঁটাই",
|
||||
"UPLOAD": "আপলোড করুন",
|
||||
"UPLOAD DATE": "আপলোডের তারিখ",
|
||||
"UPLOAD MEDIA": "মিডিয়া আপলোড করুন",
|
||||
"Undo removal": "",
|
||||
"Unlisted": "তালিকাভুক্ত নয়",
|
||||
"Up Next": "পরবর্তী",
|
||||
"Up next": "পরবর্তী",
|
||||
"Upload": "আপলোড করুন",
|
||||
"Upload date (newest)": "আপলোডের তারিখ (নতুন)",
|
||||
"Upload date (oldest)": "আপলোডের তারিখ (পুরাতন)",
|
||||
"Upload date - Newest": "আপলোডের তারিখ - নতুন",
|
||||
"Upload date - Oldest": "আপলোডের তারিখ - পুরাতন",
|
||||
"Upload media": "মিডিয়া আপলোড করুন",
|
||||
"Uploads": "আপলোডসমূহ",
|
||||
"Users": "",
|
||||
"VIEW ALL": "সব দেখুন",
|
||||
"Video": "ভিডিও",
|
||||
"View all": "সব দেখুন",
|
||||
"View count": "দেখার সংখ্যা",
|
||||
"View media": "মিডিয়া দেখুন",
|
||||
"Welcome": "স্বাগতম",
|
||||
"You are going to copy": "আপনি কপি করতে চলেছেন",
|
||||
"You are going to delete": "আপনি মুছে ফেলতে চলেছেন",
|
||||
"You are going to disable comments to": "আপনি মন্তব্য নিষ্ক্রিয় করতে চলেছেন",
|
||||
"You are going to disable download for": "আপনি ডাউনলোড নিষ্ক্রিয় করতে চলেছেন",
|
||||
"You are going to enable comments to": "আপনি মন্তব্য সক্রিয় করতে চলেছেন",
|
||||
"You are going to enable download for": "আপনি ডাউনলোড সক্রিয় করতে চলেছেন",
|
||||
"comment": "মন্তব্য",
|
||||
"is a modern, fully featured open source video and media CMS. It is developed to meet the needs of modern web platforms for viewing and sharing media": "একটি আধুনিক, সম্পূর্ণ বৈশিষ্ট্যযুক্ত ওপেন সোর্স ভিডিও এবং মিডিয়া CMS। এটি আধুনিক ওয়েব প্ল্যাটফর্মের জন্য মিডিয়া দেখার এবং শেয়ার করার প্রয়োজন মেটাতে তৈরি করা হয়েছে",
|
||||
"media in category": "বিভাগে মিডিয়া",
|
||||
"media in tag": "ট্যাগে মিডিয়া",
|
||||
"media, are you sure?": "মিডিয়া, আপনি কি নিশ্চিত?",
|
||||
"media.": "মিডিয়া।",
|
||||
"or": "অথবা",
|
||||
"results for": "এর জন্য ফলাফল",
|
||||
"selected": "",
|
||||
"view": "দেখুন",
|
||||
"views": "দেখা হয়েছে",
|
||||
"yet": "এখনও",
|
||||
}
|
||||
|
||||
replacement_strings = {
|
||||
"Apr": "এপ্রিল",
|
||||
"Aug": "আগস্ট",
|
||||
"Dec": "ডিসেম্বর",
|
||||
"Feb": "ফেব্রু",
|
||||
"Jan": "জানু",
|
||||
"Jul": "জুলাই",
|
||||
"Jun": "জুন",
|
||||
"Mar": "মার্চ",
|
||||
"May": "মে",
|
||||
"Nov": "নভেম্বর",
|
||||
"Oct": "অক্টোবর",
|
||||
"Sep": "সেপ্টেম্বর",
|
||||
"day ago": "দিন আগে",
|
||||
"days ago": "দিন আগে",
|
||||
"hour ago": "ঘণ্টা আগে",
|
||||
"hours ago": "ঘণ্টা আগে",
|
||||
"just now": "এখনই",
|
||||
"minute ago": "মিনিট আগে",
|
||||
"minutes ago": "মিনিট আগে",
|
||||
"month ago": "মাস আগে",
|
||||
"months ago": "মাস আগে",
|
||||
"second ago": "সেকেন্ড আগে",
|
||||
"seconds ago": "সেকেন্ড আগে",
|
||||
"week ago": "সপ্তাহ আগে",
|
||||
"weeks ago": "সপ্তাহ আগে",
|
||||
"year ago": "বছর আগে",
|
||||
"years ago": "বছর আগে",
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user