Compare commits

..

98 Commits

Author SHA1 Message Date
Markos Gogoulos
66e67c751e Docker story refactoring 2025-11-17 11:09:38 +02:00
Markos Gogoulos
9b3d9fe1e7 trim (#1431) 2025-11-13 12:42:48 +02:00
Markos Gogoulos
ea340b6a2e V7 f4 (#1430) 2025-11-13 12:30:25 +02:00
Markos Gogoulos
ba2c31b1e6 fix: static files (#1429) 2025-11-12 14:08:02 +02:00
Yiannis Christodoulou
5eb6fafb8c fix: Show default chapter names in textarea instead of placeholder text (#1428)
* Refactor chapter filtering and auto-save logic

Simplified chapter filtering to only exclude empty titles, allowing default chapter names. Updated auto-save logic to skip saving when there are no chapters or mediaId. Removed unused helper function and improved debug logging.

* Show default chapter title in editor and set initial title

The chapter title is now always displayed in the textarea, including default names like 'Chapter 1'. Also, the initial segment is created with 'Chapter 1' as its title instead of an empty string for better clarity.

* build assets
2025-11-12 14:04:07 +02:00
Markos Gogoulos
c035bcddf5 small 7.2.x fixes 2025-11-11 19:51:42 +02:00
Markos Gogoulos
01912ea1f9 fix: adjust poster url for audio 2025-11-11 13:21:10 +02:00
Markos Gogoulos
d9f299af4d V7 small fixes (#1426) 2025-11-11 13:15:36 +02:00
Markos Gogoulos
e80590a3aa Bulk actions support (#1418) 2025-11-11 11:32:54 +02:00
Yiannis Christodoulou
2a0cb977f2 Video.js fixes and improvements after major upgrade (#1413) 2025-10-27 11:53:35 +02:00
Yiannis Christodoulou
a5e6e7b9ca feat: Major Upgrade to Video.js v8 — Chapters Functionality, Fixes and Improvements 2025-10-20 15:30:00 +03:00
Markos Gogoulos
b39072c8ae feat: disapprove user 2025-10-09 17:29:29 +03:00
Markos Gogoulos
f4ab60e894 fix: extend gitignore 2025-09-26 11:33:44 +03:00
Markos Gogoulos
8656b40c5b fix: url 2025-09-25 08:51:26 +03:00
Markos Gogoulos
553a25a86f feat: pass extra css (#1392) 2025-09-24 19:01:06 +03:00
Markos Gogoulos
1c1af489f1 feat: allow portal logo override (#1391) 2025-09-24 16:26:45 +03:00
Markos Gogoulos
c4c5ecf06a update version 2025-09-21 16:18:31 +03:00
Markos Gogoulos
725cc71960 fix migration 2025-09-21 16:17:18 +03:00
Markos Gogoulos
0c1c5bbb09 add new migration 2025-09-21 16:15:46 +03:00
Markos Gogoulos
56182f0a6d feat: allow customizable about page 2025-09-21 15:47:04 +03:00
Markos Gogoulos
208f0b338b feat: update versions for python packages, add Pages functionality (#1386)
This PR updates Django core version and also brings html pages support (that admins can create)
2025-09-21 15:38:43 +03:00
Markos Gogoulos
cbef629baf feat: approve users, edit users through manage users page (#1383) 2025-09-20 15:16:52 +03:00
Bret.S (AKA: CyberGladius)
8e8454d8c2 Docs Update with SAML Deployement Guide and Troubleshooting (#1377)
* Docs update with SAML deployment guide.

* Docs update with SAML deployment guide. URL Fix

---------

Co-authored-by: root <git@tdcme.com>
2025-09-16 14:51:05 +03:00
Markos Gogoulos
8d982ace92 Feat whisper opts (#1368) 2025-09-04 13:39:41 +03:00
Meet Dholakia
6cee02085c Updated the title splicing length to 100 in playlist, category and media models (#1364) 2025-09-02 19:43:46 +03:00
Markos Gogoulos
e33aa17911 fix version 2025-09-02 11:32:02 +03:00
Markos Gogoulos
a8db23f204 docs: add whisper section 2025-09-02 11:00:17 +03:00
Markos Gogoulos
d6428e3334 version bump 2025-09-01 20:34:15 +03:00
Markos Gogoulos
fd342fd1d6 version bump 2025-09-01 20:32:11 +03:00
Markos Gogoulos
7a1b32f1ba fix: delete media (#1366) 2025-09-01 20:06:49 +03:00
Markos Gogoulos
817e16ac60 feat: whisper STT and record screen (#1363) 2025-09-01 15:11:38 +03:00
Markos Gogoulos
8cbeb72dd2 feat: add fily type and max user media uplod limits 2025-08-19 11:35:49 +03:00
Markos Gogoulos
e9f862a0ff feat: 3 small fixes (#1347)
* fix: datetime input

* show message on upload user only

* show all media of user for editors/managers/admins
2025-08-17 19:18:47 +03:00
Markos Gogoulos
02eac68b51 fix: swagger (#1345)
* fix: swagger

* fix: swagger
2025-08-12 12:24:26 +03:00
Markos Gogoulos
e790795bfd feat: bulk actions API 2025-08-07 13:21:12 +03:00
Markos Gogoulos
de99d84c18 feat: revert ffmpeg install 2025-07-07 13:33:45 +03:00
Markos Gogoulos
8aa89c0958 feat: use apt for installing ffmpeg 2025-07-07 13:00:33 +03:00
Markos Gogoulos
df98b65704 feat: pass version on static files (#1318) 2025-07-07 11:54:02 +03:00
Markos Gogoulos
a607996bfa feat: adds minimum resolution of 144p 2025-07-07 11:34:02 +03:00
Markos Gogoulos
79f2e2bb11 feat: replace format with fstrings 2025-07-07 11:26:08 +03:00
Markos Gogoulos
d54732040a feat: add DB connection pooling 2025-07-07 11:18:40 +03:00
Andy
e8520bc7cd fix: date picker in edit media (#1297)
By default, Django uses type=text for the date input, which respects
the locale settings. However, when changing the input type to date,
it should only accept YYYY-MM-DD format so the input field can be
properly handled.
2025-07-06 11:44:44 +03:00
Markos Gogoulos
b6e46e7b62 feat: replace login middleware (#1314) 2025-07-06 11:25:50 +03:00
Adam Stradovnik
36eab954bd feat: Adds support for Slovenian frontend translations (#1306) 2025-07-06 11:05:07 +03:00
Markos Gogoulos
610716533b fix formatting 2025-07-01 15:46:34 +03:00
Yiannis Christodoulou
4f1c4a2b4c fix: Disable Segment Tools and Reset Preview State During Playback (#1305)
* fix: Disable Segment Tools and Reset Preview State During Playback

* chore: remove some unnecessary comments

* chore: build assets

* fix: do not display the handles (left/right) on preview mode

* fix: Disable all tools on preview mode (undo, redo, reset, etc.)

* Update README.md

* feat: Prettier configuration for video editor

* Update README.md

* Update .prettierrc

* style: Format entire codebase (video-editor) with Prettier

* fix: During segments playback mode, disable button interactions but keep hover working

* feat: Add yarn format

* prettier format

* Update package.json

* feat: Install prettier and improve formatting

* build assets

* Update version.py 6.2.0
2025-07-01 15:33:39 +03:00
Markos Gogoulos
83f3eec940 feat: enable editing of media slug, show categories on manage media 2025-06-24 11:13:33 +03:00
Markos Gogoulos
a5acce4ab1 fix: single server install issues 2025-06-15 11:19:29 +03:00
Ofek
a4e9309350 (FEAT) Add Hebrew Translation (#1295) 2025-06-14 11:08:17 +03:00
Casper Tollund
6beaf0bbe2 Add support for Danish translation (#1293)
Co-authored-by: Casper Tollund <casto@mac.ait.clients.local>
2025-06-12 17:35:37 +03:00
Andy
70168299ba Adds support for Traditional Chinese translation (#1282)
* Adds support for Traditional Chinese translation

* Reformats the style to match black formatter
2025-06-11 15:10:49 +03:00
Markos Gogoulos
b28c2d8271 feat: Video Trimmer and more 2025-06-11 14:48:30 +03:00
angy91m
d34fc328bf feat: IT traslation added (#1267)
* Create it.py

* feat: IT traslation added
2025-05-15 13:30:07 +03:00
Markos Gogoulos
ab4d9d67df fix: issue with import (#1245) 2025-04-07 18:36:38 +03:00
Markos Gogoulos
f7a2f049bd Update README.md (#1244) 2025-04-05 12:52:29 +03:00
Markos Gogoulos
05414f66c7 feat: RBAC + SAML support 2025-04-05 12:44:21 +03:00
Markos Gogoulos
8fecccce1c feat: move docker compose files 2025-03-18 19:21:39 +02:00
Markos Gogoulos
2a7123ca0b feat: move docker compose files in dir (#1231) 2025-03-18 19:05:04 +02:00
Yiannis Christodoulou
20f305e69e feat: Frontend Dependencies Upgrade +Fix Timestamps in videos 2025-03-18 19:01:50 +02:00
Markos Gogoulos
d1fda05fdc fix: flake8 2025-03-09 20:50:07 +02:00
Markos Gogoulos
a02e0a8a66 fix: flake8 2025-03-09 20:48:09 +02:00
Markos Gogoulos
21f76dbb6e feat: playlist optimizations (#1216) 2025-03-09 20:44:04 +02:00
Markos Gogoulos
50e9f3103f feat: better support for subtitles (#1215) 2025-03-09 20:29:26 +02:00
Markos Gogoulos
0b9a203123 revert head changes 2025-02-13 20:31:19 +02:00
Sven-Thorsten Dietrich
5cbd815496 fix: Fix Docker WARN: FromAsCasing (#1196)
Fixes: L27 'as' and 'FROM' keywords' casing do not match

Signed-off-by: Sven-Thorsten Dietrich <thebigcorporation@gmail.com>
2025-02-13 13:57:14 +02:00
Markos Gogoulos
3a8cacc847 feat: Bulk fixes (#1195)
remove ckeditor - not in use
add more strict default password validators
set Django admin as configurable URL
add nginx HSTS and CSP headers
enable moving from private to unlisted in the PORTAL_WORKFLOW private
on default comments listing, show only comments for public media
in case of a private media, dont expose any unneeded metadata
2025-02-13 13:41:53 +02:00
Markos Gogoulos
5402ee7bc5 fix: crispy forms (#1194) 2025-02-12 14:27:27 +02:00
Markos Gogoulos
a6a2b50c8d remove redundant message 2025-02-10 22:25:24 +02:00
Markos Gogoulos
23e48a8bb7 remove redundant message 2025-02-10 22:24:42 +02:00
Markos Gogoulos
313cd9cbc6 fix issue with static files in dev 2025-02-10 22:04:14 +02:00
Markos Gogoulos
0392dbe1ed feat: lib updates and more (#1187)
This PR performs the following changes:

- update python in Dockerfile
- updates all python libraries (including Django)
- makes md5sum calculation redundant by default
- updates Postgresql used in Docker. For new installations this is ok. For existing ones this is backwards incompatible, and restore through pg_dump+pg_restore has to be performed in order to utilize Postgres 17 (since its not compatible with previous versions and will complain)
- fixes issues with HLS, and adds test to ensure they won't happen again
- allows . and @ in usernames
2025-02-10 11:34:00 +02:00
Markos Gogoulos
a7562c244e fix: black (#1164) 2025-01-22 20:16:21 +02:00
Sonu Kumar
d2ee12087c feat: Re-Arranged Subtitles by first letter of language (#863) 2025-01-22 19:25:47 +02:00
aleensd
6db01932e1 feat: fullscreen images slideshow (#1120) 2025-01-22 19:02:01 +02:00
Markos Gogoulos
53d8215346 feat: comment URL (#1163) 2025-01-22 14:55:43 +02:00
David
1b960b28f8 feat: Update insert login_required middleware to allow auth middleware (#1082) 2025-01-22 14:54:25 +02:00
Markos Gogoulos
02d9188aa1 feat: increase task limit (#1161) 2025-01-22 11:22:07 +02:00
Meng Sen
8d9a4618f0 feat: support for reading environment variables on Docker 2024-11-22 18:35:47 +02:00
aleensd
cf93a77802 feat: integrate react-pdf-viewer for PDF display (#1112) 2024-11-21 12:15:25 +02:00
Markos Gogoulos
5a1e4f25ed fix: issue with create_hls 2024-11-20 15:04:27 +02:00
Markos Gogoulos
9fc7597e73 fix: GH action 2024-11-20 13:47:29 +02:00
Markos Gogoulos
9b3e0250d4 fix: GH action 2024-11-20 13:36:58 +02:00
Markos Gogoulos
1384471745 fix: flake8 2024-11-20 13:29:37 +02:00
Markos Gogoulos
29b362c8ce fix: flake8 2024-11-20 13:27:57 +02:00
Markos Gogoulos
b8ee2e9fb8 fix: pre-commit 2024-11-20 13:17:25 +02:00
Markos Gogoulos
99be0f07dd feat: edit slideshow_url results 2024-11-18 15:48:02 +02:00
Markos Gogoulos
27d1660192 feat: provide slideshow media for images (#1108) 2024-11-14 12:22:23 +02:00
Nathan Hyde
98adb22205 Fixes mediacms-io/mediacms#1046 - submition => submission (#1094) 2024-10-30 19:49:26 +02:00
yatesdr
673ddeb5bd feat: Set option for allowing only certain domains to register (#1086) 2024-10-19 14:51:20 +03:00
Markos Gogoulos
aa8a2d92dc Feat/check input (#1089)
* docs: instructions to set frames per seconds on sprites

* feat: add more validation

* remove reduntant line
2024-10-19 14:17:19 +03:00
Kaiwalya Koparkar
6bbd4c2809 feat: Added Elestio as one-click deploy option (#1055) 2024-10-08 10:44:44 +03:00
Markos Gogoulos
c4148bd504 feat: semantic release 2024-10-07 09:10:21 +03:00
Markos Gogoulos
ea8b2af26f fix: remove duplicate setting 2024-10-04 16:40:53 +03:00
Markos Gogoulos
5aa899cef0 Feat translations improvements v1 (#1076) 2024-10-04 13:39:28 +03:00
Markos Gogoulos
4992cc425c feat: translations support 2024-10-04 13:17:40 +03:00
Tudorel Oprisan
ef4067cbdd fix: replaced pipe with empty string on helper function 2024-10-02 19:16:18 +03:00
Markos Gogoulos
8cc3513a8a docs 2024-10-02 15:57:19 +03:00
Kyle Maas
90e593946d feat: allow commenting by regular users when posting media requires advanced permissions (#1023) 2024-10-02 15:52:30 +03:00
1015 changed files with 114562 additions and 250750 deletions

113
.docker-backup/Dockerfile Normal file
View 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

View 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:

View 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

View 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

View File

@@ -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

View File

@@ -15,15 +15,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
- 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:
# List of Docker images to use as base name for tags
images: |
mediacms/mediacms
# Generate Docker tags based on the following events/attributes
# Set latest tag for default branch
tags: |
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
type=semver,pattern={{version}}
@@ -37,16 +40,95 @@ jobs:
org.opencontainers.image.source=https://github.com/mediacms-io/mediacms
org.opencontainers.image.licenses=AGPL-3.0
- name: Login to Docker Hub
uses: docker/login-action@v2.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
- 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.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
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 }}

View File

@@ -13,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
@@ -26,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
View File

@@ -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/

View File

@@ -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
View File

@@ -0,0 +1,3 @@
/templates/cms/*
/templates/*.html
*.scss

21
.prettierrc Normal file
View 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
}
}
]
}

View 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)

View File

@@ -1,70 +1,126 @@
FROM python:3.11.4-bookworm 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.11.4-bookworm 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

View File

@@ -1,73 +0,0 @@
FROM python:3.11.4-bookworm AS compile-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 .
COPY requirements-dev.txt .
RUN pip install -r requirements.txt
RUN pip install -r requirements-dev.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.11.4-bookworm 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 && \
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 && \
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
WORKDIR /home/mediacms.io/mediacms
EXPOSE 9000 80
RUN chmod +x ./deploy/docker/entrypoint.sh
ENTRYPOINT ["./deploy/docker/entrypoint.sh"]
CMD ["./deploy/docker/start.sh"]

18
Dockerfile.nginx Normal file
View 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;"]

32
Makefile Normal file
View 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
View 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!**

View File

@@ -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,29 +39,25 @@ 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.
@@ -71,7 +71,12 @@ Copyright Markos Gogoulos.
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.
[![Deploy on Elestio](https://elest.io/images/logos/deploy-to-elestio-btn.png)](https://elest.io/open-source/mediacms)
## Hardware considerations
@@ -79,35 +84,26 @@ For a small to medium installation, with a few hours of video uploaded daily, an
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
- [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).
## Configuration
Visit [Configuration](docs/admins_docs.md#5-configuration) page.
## Information for developers
Check out the new section on the [Developer Experience](docs/dev_exp.md) page
## 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
@@ -116,10 +112,10 @@ This software uses the following list of awesome technologies: Python, Django, D
## 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
- **American Association of Gynecologic Laparoscopists** - https://surgeryu.aagl.org/
- **American Association of Gynecologic Laparoscopists** - https://surgeryu.org/
## How to contribute

477
UPGRADE_TO_7.3.md Normal file
View 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!

View File

View File

View 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

View File

View File

View File

10
cms/auth_backends.py Normal file
View 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

View File

@@ -4,45 +4,54 @@ import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'allauth',
'allauth.account',
'allauth.socialaccount',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'rest_framework',
'rest_framework.authtoken',
'imagekit',
'files.apps.FilesConfig',
'users.apps.UsersConfig',
'actions.apps.ActionsConfig',
'debug_toolbar',
'mptt',
'crispy_forms',
'uploader.apps.UploaderConfig',
'djcelery_email',
'ckeditor',
'drf_yasg',
'corsheaders',
"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 = None
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)
STATIC_ROOT = os.path.join(BASE_DIR, 'static_collected')

23
cms/middleware.py Normal file
View 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

View File

@@ -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

View File

@@ -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"
@@ -96,22 +100,46 @@ 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"
@@ -119,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": (
@@ -149,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/"
@@ -180,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
@@ -220,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
@@ -241,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 = "/"
@@ -279,7 +288,7 @@ AUTHENTICATION_BACKENDS = (
)
INSTALLED_APPS = [
"django.contrib.admin",
"admin_customizations",
"django.contrib.auth",
"allauth",
"allauth.account",
@@ -288,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",
@@ -295,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"
@@ -340,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,
},
},
{
@@ -356,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",
@@ -456,17 +458,157 @@ 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
@@ -477,22 +619,11 @@ if LOCAL_INSTALL:
else:
SSL_FRONTEND_HOST = FRONTEND_HOST
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'/accounts/password/.*/$',
r'/accounts/confirm-email/.*/$',
r'/api/v[0-9]+/',
]
# if True, only show original, don't perform any action on videos
DO_NOT_TRANSCODE_VIDEO = False
# CSRF_COOKIE_SECURE = True
# SESSION_COOKIE_SECURE = True
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
PYSUBS_COMMAND = "pysubs2"
# the following is related to local development using docker
# and docker-compose-dev.yaml
@@ -503,3 +634,17 @@ try:
from .dev_settings import * # noqa
except ImportError:
pass
if GLOBAL_LOGIN_REQUIRED:
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")

View File

@@ -1,4 +1,5 @@
import debug_toolbar
from django.conf import settings
from django.conf.urls import include
from django.contrib import admin
from django.urls import path, re_path
@@ -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
View File

@@ -0,0 +1 @@
VERSION = "7.2.1"

View 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>

View File

@@ -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";

View File

@@ -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;
}
}

View File

@@ -12,7 +12,7 @@ threads = 2
master = true
socket = 127.0.0.1:9000
socket = 0.0.0.0:9000
workers = 2

View File

@@ -1,5 +0,0 @@
from pytest_factoryboy import register
from tests.users.factories import UserFactory
register(UserFactory)

0
custom/.gitkeep Normal file
View File

238
custom/README.md Normal file
View 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
```

View 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
View File

View File

View File

75
deic_setup_notes.md Normal file
View 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

View File

@@ -1,3 +0,0 @@
# MediaCMS on Docker
See: [Details](../../docs/Docker_deployment.md)

View File

@@ -1,36 +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
# Also ignoring .git folder to fix this issue https://github.com/mediacms-io/mediacms/issues/934
find /home/mediacms.io/mediacms ! \( -path "*.git*" \) -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 "$@"

View File

@@ -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

View File

@@ -1,71 +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
rm /var/run/mediacms/* -f # remove any stale id, so that on forced restarts of celery workers there are no stale processes that prevent new ones
fi

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,2 +0,0 @@
[supervisord]
nodaemon=true

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View 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
View 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:

View File

@@ -1,84 +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:
migrations:
build:
context: .
dockerfile: ./Dockerfile-dev
image: mediacms/mediacms-dev:latest
volumes:
- ./:/home/mediacms.io/mediacms/
command: "python manage.py migrate"
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"
restart: on-failure
DEVELOPMENT_MODE: 'True'
DEBUG: 'True'
ADMIN_USER: 'admin'
ADMIN_EMAIL: 'admin@localhost'
ADMIN_PASSWORD: 'admin'
restart: "no"
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
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:
build:
context: .
dockerfile: ./Dockerfile-dev
image: mediacms/mediacms-dev:latest
command: "python manage.py runserver 0.0.0.0:80"
environment:
DEVELOPMENT_MODE: "True"
ADMIN_USER: 'admin'
ADMIN_PASSWORD: 'admin'
ADMIN_EMAIL: 'admin@localhost'
ports:
- "80:80"
volumes:
- ./:/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
db:
image: postgres:15.2-alpine
migrations:
condition: service_completed_successfully
redis:
condition: service_healthy
db:
condition: service_healthy
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always
- ./:/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 -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
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
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
volumes:
postgres_data:

5
docker-compose.full.yaml Normal file
View File

@@ -0,0 +1,5 @@
version: "3"
services:
celery_worker:
image: mediacms/mediacms:full

View File

@@ -1,86 +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:15.2-alpine
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 -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
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:

View 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:

View File

@@ -68,7 +68,7 @@ services:
depends_on:
- migrations
db:
image: postgres:15.2-alpine
image: postgres:17.2-alpine
volumes:
- ../postgres_data/:/var/lib/postgresql/data/
restart: always

View File

@@ -70,7 +70,7 @@ services:
depends_on:
- migrations
db:
image: postgres:15.2-alpine
image: postgres:17.2-alpine
volumes:
- ../postgres_data/:/var/lib/postgresql/data/
restart: always

View File

@@ -90,7 +90,7 @@ services:
depends_on:
- migrations
db:
image: postgres:15.2-alpine
image: postgres:17.2-alpine
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always

View File

@@ -66,7 +66,7 @@ services:
depends_on:
- migrations
db:
image: postgres:15.2-alpine
image: postgres:17.2-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
restart: always

View 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

View File

@@ -2,7 +2,7 @@
## 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 Deployment options](#4-docker-deployment-options)
- [5. Configuration](#5-configuration)
@@ -20,18 +20,28 @@
- [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.
## 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 (tested on versions 20, 22).
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 system with git utility installed should be completed in a few minutes with the following steps.
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.
Automated script - tested on Ubuntu 20, Ubuntu 22 and Debian Buster
```bash
mkdir /home/mediacms.io && cd /home/mediacms.io/
@@ -82,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 20/22 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
@@ -104,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
@@ -117,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
@@ -124,8 +138,8 @@ 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
@@ -150,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.
![MediaCMS](images/architecture.png)
@@ -206,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
@@ -353,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
@@ -479,6 +587,78 @@ Whether or not to enable generation of a sitemap file at http://your_installatio
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
@@ -783,11 +963,198 @@ This will disable the transcoding process and only the original file will be sho
## 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, remove the `border-radius` added on the following files:
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
```
frontend/src/static/css/_extra.css
frontend/src/static/js/components/list-item/Item.scss
frontend/src/static/js/components/media-page/MediaPage.scss
python manage.py process_translations
```
you now have to re-run the frontend build in order to see the changes (check docs/dev_exp.md)
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`.

View File

@@ -4,10 +4,10 @@ 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`
Then run `docker compose -f docker-compose-dev.yaml up`
```
user@user:~/mediacms$ 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
@@ -34,6 +34,14 @@ 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.
@@ -54,7 +62,28 @@ In order to make changes to React code, edit code on frontend/src and check it's
### 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`
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
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
...
```

View File

@@ -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 ;)

166
docs/media_permissions.md Normal file
View 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
View 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 Microsofts 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
View 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)

View File

@@ -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.

View File

@@ -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,
)
@@ -49,12 +60,126 @@ class MediaAdmin(admin.ModelAdmin):
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):
@@ -76,7 +201,18 @@ 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):
@@ -94,11 +230,51 @@ class EncodingAdmin(admin.ModelAdmin):
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)
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"

View File

@@ -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):

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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)

View 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)

View 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": "منذ سنوات",
}

View 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": "বছর আগে",
}

View File

@@ -0,0 +1,288 @@
translation_strings = {
"+ Create Playlist": "+ Opret Playliste",
"00 - 20 min": "00 - 20 min",
"1 result for": "1 resultat for",
"20 - 40 min": "20 - 40 min",
"40 - 60 min": "40 - 60 min",
"60 - 120 min+": "60 - 120 min+",
"ABOUT": "OM",
"AUTOPLAY": "Automatisk afspilning",
"About": "Om",
"Add / Remove Co-Editors": "Tilføj / Fjern Medredaktører",
"Add / Remove Co-Owners": "Tilføj / Fjern Medejere",
"Add / Remove Co-Viewers": "Tilføj / Fjern Medseere",
"Add / Remove Tags": "Tilføj / Fjern Tags",
"Add / Remove from Categories": "Tilføj / Fjern fra Kategorier",
"Add a ": "Tilføj en ",
"Add to": "Tilføj til",
"Add to / Remove from Category": "Tilføj til / Fjern fra Kategori",
"Add to / Remove from Playlist": "Tilføj til / Fjern fra Playliste",
"All": "Alle",
"All categories already added": "Alle kategorier allerede tilføjet",
"All tags already added": "Alle tags allerede tilføjet",
"Alphabetically - A-Z": "Alfabetisk - A-Å",
"Alphabetically - Z-A": "Alfabetisk - Å-A",
"Audio": "Lyd",
"Browse your files": "Gennemse dine filer",
"Bulk Actions": "Massehandlinger",
"COMMENT": "KOMMENTAR",
"Cancel": "Annuller",
"Categories": "Kategorier",
"Category": "Kategori",
"Change Language": "Skift sprog",
"Change Owner": "Skift Ejer",
"Change password": "Skift adgangskode",
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "Klik på 'Start optagelse' og vælg den skærm eller fane, du vil optage. Når optagelsen er færdig, skal du klikke på 'Stop optagelse', og optagelsen vil blive uploadet.",
"Co-Editors": "Medredaktører",
"Co-Owners": "Medejere",
"Co-Viewers": "Medseere",
"Comment": "Kommentar",
"Comments": "Kommentarer",
"Comments are disabled": "Kommentarer er slået fra",
"Confirm": "Bekræft",
"Confirm Action": "Bekræft Handling",
"Contact": "Kontakt",
"Copy Media": "Kopier Medie",
"Create": "Opret",
"DELETE": "SLET",
"DELETE MEDIA": "SLET MEDIE",
"DOWNLOAD": "HENT",
"DURATION": "VARIGHED",
"Delete Media": "Slet Medie",
"Delete media": "Slet medie",
"Disable Comments": "Deaktiver Kommentarer",
"Disable Download": "Deaktiver Download",
"Drag and drop files": "Træk og slip filer",
"EDIT MEDIA": "REDIGER MEDIE",
"EDIT PROFILE": "REDIGER PROFIL",
"EDIT SUBTITLE": "REDIGER UNDERTEKSTER",
"Edit media": "Rediger medie",
"Edit profile": "Rediger profil",
"Edit subtitle": "Rediger undertekster",
"Enable Comments": "Aktiver Kommentarer",
"Enable Download": "Aktiver Download",
"Enter playlist name...": "Indtast playlistenavn...",
"Failed to add categories": "Tilføjelse af kategorier mislykkedes",
"Failed to add media to playlists": "Tilføjelse af medie til playlister mislykkedes",
"Failed to add tags": "Tilføjelse af tags mislykkedes",
"Failed to add users": "Tilføjelse af brugere mislykkedes",
"Failed to change owner": "Ændring af ejer mislykkedes",
"Failed to change owner. Please try again.": "Ændring af ejer mislykkedes. Prøv venligst igen.",
"Failed to copy media.": "Kopiering af medie mislykkedes.",
"Failed to create playlist": "Oprettelse af playliste mislykkedes",
"Failed to delete media. Please try again.": "Sletning af medie mislykkedes. Prøv venligst igen.",
"Failed to disable comments.": "Deaktivering af kommentarer mislykkedes.",
"Failed to disable download.": "Deaktivering af download mislykkedes.",
"Failed to enable comments.": "Aktivering af kommentarer mislykkedes.",
"Failed to enable download.": "Aktivering af download mislykkedes.",
"Failed to fetch all categories": "Hentning af alle kategorier mislykkedes",
"Failed to fetch all tags": "Hentning af alle tags mislykkedes",
"Failed to fetch existing categories": "Hentning af eksisterende kategorier mislykkedes",
"Failed to fetch existing tags": "Hentning af eksisterende tags mislykkedes",
"Failed to fetch existing users": "Hentning af eksisterende brugere mislykkedes",
"Failed to fetch playlist membership": "Hentning af playlistemedlemskab mislykkedes",
"Failed to fetch playlists": "Hentning af playlister mislykkedes",
"Failed to load categories": "Indlæsning af kategorier mislykkedes",
"Failed to load existing permissions": "Indlæsning af eksisterende tilladelser mislykkedes",
"Failed to load playlists": "Indlæsning af playlister mislykkedes",
"Failed to load tags": "Indlæsning af tags mislykkedes",
"Failed to remove categories": "Fjernelse af kategorier mislykkedes",
"Failed to remove media from playlists": "Fjernelse af medie fra playlister mislykkedes",
"Failed to remove tags": "Fjernelse af tags mislykkedes",
"Failed to remove users": "Fjernelse af brugere mislykkedes",
"Failed to search users": "Søgning af brugere mislykkedes",
"Failed to set publish state": "Indstilling af publiceringsstatus mislykkedes",
"Failed to set publish state. Please try again.": "Indstilling af publiceringsstatus mislykkedes. Prøv venligst igen.",
"Failed to update categories. Please try again.": "Opdatering af kategorier mislykkedes. Prøv venligst igen.",
"Failed to update permissions. Please try again.": "Opdatering af tilladelser mislykkedes. Prøv venligst igen.",
"Failed to update playlists. Please try again.": "Opdatering af playlister mislykkedes. Prøv venligst igen.",
"Failed to update tags. Please try again.": "Opdatering af tags mislykkedes. Prøv venligst igen.",
"Featured": "Fremhævede",
"Filter existing users...": "Filtrer eksisterende brugere...",
"Filter playlists...": "Filtrer playlister...",
"Filters": "Filtre",
"Go": "Vælg",
"History": "Historik",
"Home": "Hjem",
"Image": "Billede",
"Language": "Sprog",
"Latest": "Nyeste",
"Like count": "Antal likes",
"Liked media": "Medier du har liket",
"Likes - Least": "Likes - Færrest",
"Likes - Most": "Likes - Flest",
"Loading categories...": "Indlæser kategorier...",
"Loading existing users...": "Indlæser eksisterende brugere...",
"Loading playlists...": "Indlæser playlister...",
"Loading tags...": "Indlæser tags...",
"MEDIA TYPE": "MEDIETYPE",
"Manage": "Administrer",
"Manage Playlists": "Administrer Playlister",
"Manage comments": "Administrer kommentarer",
"Manage media": "Administrer medier",
"Manage users": "Administrer brugere",
"Media": "Medier",
"Media I own": "Medier jeg ejer",
"Media was edited": "Mediet er blevet redigeret",
"Members": "Medlemmer",
"My media": "Mine medier",
"My playlists": "Mine playlister",
"No": "Nej",
"No categories": "Ingen kategorier",
"No comment yet": "Ingen kommentar endnu",
"No comments yet": "Ingen komentarer endnu",
"No existing": "Ingen eksisterende",
"No playlists available": "Ingen playlister tilgængelige",
"No playlists selected": "Ingen playlister valgt",
"No results for": "Ingen resultater for",
"No tags": "Ingen tags",
"No users to add": "Ingen brugere at tilføje",
"PLAYLISTS": "PLAYLISTER",
"PUBLISH STATE": "PUBLICERINGSSTATUS",
"Pdf": "PDF",
"Playlists": "Playlister",
"Plays - Least": "Afspilninger - Færrest",
"Plays - Most": "Afspilninger - Flest",
"Please select a publish state": "Vælg venligst en publiceringsstatus",
"Please select a user": "Vælg venligst en bruger",
"Powered by": "Drevet af",
"Private": "Privat",
"Proceed": "Fortsæt",
"Processing...": "Behandler...",
"Public": "Offentlig",
"Publish": "Udgiv",
"Publish State": "Publiceringsstatus",
"Published": "Publiceret",
"Published on": "Udgivet på",
"Recent uploads": "Nylige uploads",
"Recommended": "Anbefalet",
"Record Screen": "Optag skærm",
"Register": "Registrer",
"Remove category": "Fjern kategori",
"Remove from list": "Fjern fra liste",
"Remove tag": "Fjern tag",
"Remove user": "Fjern bruger",
"SAVE": "GEM",
"SEARCH": "SØG",
"SHARE": "DEL",
"SHOW MORE": "VIS MERE",
"SORT BY": "SORTER EFTER",
"SUBMIT": "INDSEND",
"Search": "Søg",
"Search for user...": "Søg efter bruger...",
"Search users to add...": "Søg efter brugere at tilføje...",
"Select": "Vælg",
"Select Owner": "Vælg Ejer",
"Select all": "Vælg alle",
"Select all media": "Vælg alle medier",
"Select publish state:": "Vælg publiceringsstatus:",
"Selected": "Valgt",
"Shared by me": "Delt af mig",
"Shared with me": "Delt med mig",
"Sign in": "Log ind",
"Sign out": "Log ud",
"Sort By": "Sorter efter",
"Start Recording": "Start optagelse",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Begynd at uploade medier og dele dit arbejde. Medier, du uploader, vil blive vist her.",
"Stop Recording": "Stop optagelse",
"Submit": "Indsend",
"Subtitle was added": "Undertekster tilføjet",
"Subtitles": "Undertekster",
"Successfully Copied": "Kopieret med succes",
"Successfully Disabled Download": "Download deaktiveret med succes",
"Successfully Disabled comments": "Kommentarer deaktiveret med succes",
"Successfully Enabled Download": "Download aktiveret med succes",
"Successfully Enabled comments": "Kommentarer aktiveret med succes",
"Successfully changed owner": "Ejer ændret med succes",
"Successfully deleted": "Slettet med succes",
"Successfully updated": "Opdateret med succes",
"Successfully updated categories": "Kategorier opdateret med succes",
"Successfully updated playlist membership": "Playlistemedlemskab opdateret med succes",
"Successfully updated publish state": "Publiceringsstatus opdateret med succes",
"Successfully updated tags": "Tags opdateret med succes",
"TAGS": "TAGS",
"Tag": "Tag",
"Tags": "Tags",
"Terms": "Vilkår",
"The intersection of categories in the selected media is shown": "Fælles kategorier i de valgte medier vises",
"The intersection of playlists in the selected media is shown": "Fælles playlister i de valgte medier vises",
"The intersection of tags in the selected media is shown": "Fælles tags i de valgte medier vises",
"The intersection of users in the selected media is shown": "Fælles brugere i de valgte medier vises",
"The media was deleted successfully.": "Mediet blev slettet med succes.",
"This month": "Denne måned",
"This week": "Denne uge",
"This works in Chrome, Safari and Edge browsers.": "Dette virker i Chrome, Safari og Edge browsere.",
"This year": "Dette år",
"To add": "At tilføje",
"Today": "I dag",
"Trim": "Beskær",
"UPLOAD": "UPLOAD",
"UPLOAD DATE": "UPLOADDATO",
"UPLOAD MEDIA": "UPLOAD MEDIE",
"Undo removal": "Fortryd fjernelse",
"Unlisted": "Ikke listet",
"Up Next": "Næste",
"Up next": "Næste",
"Upload": "Upload",
"Upload date (newest)": "Uploaddato (nyeste)",
"Upload date (oldest)": "Uploaddato (ældste)",
"Upload date - Newest": "Uploaddato - Nyeste",
"Upload date - Oldest": "Uploaddato - Ældste",
"Upload media": "Upload medie",
"Uploads": "Uploads",
"Users": "Brugere",
"VIEW ALL": "SE ALLE",
"Video": "Video",
"View all": "Se alle",
"View count": "Antal visninger",
"View media": "Se medie",
"Welcome": "Velkommen",
"You are going to copy": "Du er ved at kopiere",
"You are going to delete": "Du er ved at slette",
"You are going to disable comments to": "Du er ved at deaktivere kommentarer til",
"You are going to disable download for": "Du er ved at deaktivere download for",
"You are going to enable comments to": "Du er ved at aktivere kommentarer til",
"You are going to enable download for": "Du er ved at aktivere download for",
"comment": "kommentar",
"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": "er et moderne, fuldt udstyret open source video og medie CMS. Det er udviklet til at imødekomme behovene for moderne webplatforme til visning og deling af medier.",
"media in category": "medier i kategori",
"media in tag": "medier i tag",
"media, are you sure?": "medie, er du sikker?",
"media.": "medie.",
"or": "eller",
"results for": "resultater for",
"selected": "valgt",
"view": "visning",
"views": "visninger",
"yet": "endnu",
}
replacement_strings = {
"Apr": "Apr",
"Aug": "Aug",
"Dec": "Dec",
"Feb": "Feb",
"Jan": "Jan",
"Jul": "Jul",
"Jun": "Jun",
"Mar": "Mar",
"May": "Maj",
"Nov": "Nov",
"Oct": "Okt",
"Sep": "Sep",
"day ago": "dag siden",
"days ago": "dage siden",
"hour ago": "time siden",
"hours ago": "timer siden",
"just now": "lige nu",
"minute ago": "minut siden",
"minutes ago": "minutter siden",
"month ago": "måned siden",
"months ago": "måneder siden",
"second ago": "sekund siden",
"seconds ago": "sekunder siden",
"week ago": "uge siden",
"weeks ago": "uger siden",
"year ago": "år siden",
"years ago": "år siden",
}

View File

@@ -0,0 +1,288 @@
translation_strings = {
"+ Create Playlist": "+ Playlist erstellen",
"00 - 20 min": "00 - 20 Min",
"1 result for": "1 Ergebnis für",
"20 - 40 min": "20 - 40 Min",
"40 - 60 min": "40 - 60 Min",
"60 - 120 min+": "60 - 120 Min+",
"ABOUT": "Über",
"AUTOPLAY": "Automatische Wiedergabe",
"About": "Über",
"Add / Remove Co-Editors": "Co-Editoren hinzufügen/entfernen",
"Add / Remove Co-Owners": "Co-Eigentümer hinzufügen/entfernen",
"Add / Remove Co-Viewers": "Co-Zuschauer hinzufügen/entfernen",
"Add / Remove Tags": "Tags hinzufügen/entfernen",
"Add / Remove from Categories": "Zu/aus Kategorien hinzufügen/entfernen",
"Add a ": "Hinzufügen eines ",
"Add to": "Hinzufügen zu",
"Add to / Remove from Category": "Zu/aus Kategorie hinzufügen/entfernen",
"Add to / Remove from Playlist": "Zu/aus Playlist hinzufügen/entfernen",
"All": "Alle",
"All categories already added": "Alle Kategorien bereits hinzugefügt",
"All tags already added": "Alle Tags bereits hinzugefügt",
"Alphabetically - A-Z": "Alphabetisch - A-Z",
"Alphabetically - Z-A": "Alphabetisch - Z-A",
"Audio": "Audio",
"Browse your files": "Durchsuchen Sie Ihre Dateien",
"Bulk Actions": "Massenaktionen",
"COMMENT": "KOMMENTAR",
"Cancel": "Abbrechen",
"Categories": "Kategorien",
"Category": "Kategorie",
"Change Language": "Sprache ändern",
"Change Owner": "Eigentümer ändern",
"Change password": "Passwort ändern",
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "Klicken Sie auf 'Aufnahme starten' und wählen Sie den Bildschirm oder Tab aus, den Sie aufnehmen möchten. Sobald die Aufnahme beendet ist, klicken Sie auf 'Aufnahme beenden', und die Aufnahme wird hochgeladen.",
"Co-Editors": "Co-Editoren",
"Co-Owners": "Co-Eigentümer",
"Co-Viewers": "Co-Zuschauer",
"Comment": "Kommentar",
"Comments": "Kommentare",
"Comments are disabled": "Kommentare sind deaktiviert",
"Confirm": "Bestätigen",
"Confirm Action": "Aktion bestätigen",
"Contact": "Kontakt",
"Copy Media": "Medien kopieren",
"Create": "Erstellen",
"DELETE": "LÖSCHEN",
"DELETE MEDIA": "MEDIEN LÖSCHEN",
"DOWNLOAD": "HERUNTERLADEN",
"DURATION": "DAUER",
"Delete Media": "Medien löschen",
"Delete media": "Medien löschen",
"Disable Comments": "Kommentare deaktivieren",
"Disable Download": "Download deaktivieren",
"Drag and drop files": "Dateien per Drag & Drop verschieben",
"EDIT MEDIA": "MEDIEN BEARBEITEN",
"EDIT PROFILE": "PROFIL BEARBEITEN",
"EDIT SUBTITLE": "UNTERTITEL BEARBEITEN",
"Edit media": "Medien bearbeiten",
"Edit profile": "Profil bearbeiten",
"Edit subtitle": "Untertitel bearbeiten",
"Enable Comments": "Kommentare aktivieren",
"Enable Download": "Download aktivieren",
"Enter playlist name...": "Playlist-Namen eingeben...",
"Failed to add categories": "Fehler beim Hinzufügen der Kategorien",
"Failed to add media to playlists": "Fehler beim Hinzufügen der Medien zu Playlists",
"Failed to add tags": "Fehler beim Hinzufügen der Tags",
"Failed to add users": "Fehler beim Hinzufügen der Benutzer",
"Failed to change owner": "Fehler beim Ändern des Eigentümers",
"Failed to change owner. Please try again.": "Fehler beim Ändern des Eigentümers. Bitte versuchen Sie es erneut.",
"Failed to copy media.": "Fehler beim Kopieren der Medien.",
"Failed to create playlist": "Fehler beim Erstellen der Playlist",
"Failed to delete media. Please try again.": "Fehler beim Löschen der Medien. Bitte versuchen Sie es erneut.",
"Failed to disable comments.": "Fehler beim Deaktivieren der Kommentare.",
"Failed to disable download.": "Fehler beim Deaktivieren des Downloads.",
"Failed to enable comments.": "Fehler beim Aktivieren der Kommentare.",
"Failed to enable download.": "Fehler beim Aktivieren des Downloads.",
"Failed to fetch all categories": "Fehler beim Laden aller Kategorien",
"Failed to fetch all tags": "Fehler beim Laden aller Tags",
"Failed to fetch existing categories": "Fehler beim Laden vorhandener Kategorien",
"Failed to fetch existing tags": "Fehler beim Laden vorhandener Tags",
"Failed to fetch existing users": "Fehler beim Laden vorhandener Benutzer",
"Failed to fetch playlist membership": "Fehler beim Laden der Playlist-Mitgliedschaft",
"Failed to fetch playlists": "Fehler beim Laden der Playlists",
"Failed to load categories": "Fehler beim Laden der Kategorien",
"Failed to load existing permissions": "Fehler beim Laden vorhandener Berechtigungen",
"Failed to load playlists": "Fehler beim Laden der Playlists",
"Failed to load tags": "Fehler beim Laden der Tags",
"Failed to remove categories": "Fehler beim Entfernen der Kategorien",
"Failed to remove media from playlists": "Fehler beim Entfernen der Medien aus Playlists",
"Failed to remove tags": "Fehler beim Entfernen der Tags",
"Failed to remove users": "Fehler beim Entfernen der Benutzer",
"Failed to search users": "Fehler bei der Benutzersuche",
"Failed to set publish state": "Fehler beim Festlegen des Veröffentlichungsstatus",
"Failed to set publish state. Please try again.": "Fehler beim Festlegen des Veröffentlichungsstatus. Bitte versuchen Sie es erneut.",
"Failed to update categories. Please try again.": "Fehler beim Aktualisieren der Kategorien. Bitte versuchen Sie es erneut.",
"Failed to update permissions. Please try again.": "Fehler beim Aktualisieren der Berechtigungen. Bitte versuchen Sie es erneut.",
"Failed to update playlists. Please try again.": "Fehler beim Aktualisieren der Playlists. Bitte versuchen Sie es erneut.",
"Failed to update tags. Please try again.": "Fehler beim Aktualisieren der Tags. Bitte versuchen Sie es erneut.",
"Featured": "Empfohlen",
"Filter existing users...": "Vorhandene Benutzer filtern...",
"Filter playlists...": "Playlists filtern...",
"Filters": "Filter",
"Go": "Los",
"History": "Verlauf",
"Home": "Startseite",
"Image": "Bild",
"Language": "Sprache",
"Latest": "Neueste",
"Like count": "Anzahl der Likes",
"Liked media": "Beliebte Medien",
"Likes - Least": "Likes - Wenigste",
"Likes - Most": "Likes - Meiste",
"Loading categories...": "Kategorien werden geladen...",
"Loading existing users...": "Vorhandene Benutzer werden geladen...",
"Loading playlists...": "Playlists werden geladen...",
"Loading tags...": "Tags werden geladen...",
"MEDIA TYPE": "MEDIENTYP",
"Manage": "Verwalten",
"Manage Playlists": "Playlists verwalten",
"Manage comments": "Kommentare verwalten",
"Manage media": "Medien verwalten",
"Manage users": "Benutzer verwalten",
"Media": "Medien",
"Media I own": "Medien, die mir gehören",
"Media was edited": "Medien wurden bearbeitet",
"Members": "Mitglieder",
"My media": "Meine Medien",
"My playlists": "Meine Playlists",
"No": "Nein",
"No categories": "Keine Kategorien",
"No comment yet": "Noch kein Kommentar",
"No comments yet": "Noch keine Kommentare",
"No existing": "Keine vorhanden",
"No playlists available": "Keine Playlists verfügbar",
"No playlists selected": "Keine Playlists ausgewählt",
"No results for": "Keine Ergebnisse für",
"No tags": "Keine Tags",
"No users to add": "Keine Benutzer hinzuzufügen",
"PLAYLISTS": "PLAYLISTS",
"PUBLISH STATE": "VERÖFFENTLICHUNGSSTATUS",
"Pdf": "PDF",
"Playlists": "Playlists",
"Plays - Least": "Wiedergaben - Wenigste",
"Plays - Most": "Wiedergaben - Meiste",
"Please select a publish state": "Bitte wählen Sie einen Veröffentlichungsstatus aus",
"Please select a user": "Bitte wählen Sie einen Benutzer aus",
"Powered by": "Bereitgestellt von",
"Private": "Privat",
"Proceed": "Fortfahren",
"Processing...": "Wird verarbeitet...",
"Public": "Öffentlich",
"Publish": "Veröffentlichen",
"Publish State": "Veröffentlichungsstatus",
"Published": "Veröffentlicht",
"Published on": "Veröffentlicht am",
"Recent uploads": "Neue Uploads",
"Recommended": "Empfohlen",
"Record Screen": "Bildschirm aufnehmen",
"Register": "Registrieren",
"Remove category": "Kategorie entfernen",
"Remove from list": "Aus Liste entfernen",
"Remove tag": "Tag entfernen",
"Remove user": "Benutzer entfernen",
"SAVE": "SPEICHERN",
"SEARCH": "SUCHE",
"SHARE": "TEILEN",
"SHOW MORE": "MEHR ANZEIGEN",
"SORT BY": "SORTIEREN NACH",
"SUBMIT": "ABSENDEN",
"Search": "Suche",
"Search for user...": "Nach Benutzer suchen...",
"Search users to add...": "Nach Benutzern zum Hinzufügen suchen...",
"Select": "Auswählen",
"Select Owner": "Eigentümer auswählen",
"Select all": "Alle auswählen",
"Select all media": "Alle Medien auswählen",
"Select publish state:": "Veröffentlichungsstatus auswählen:",
"Selected": "Ausgewählt",
"Shared by me": "Von mir geteilt",
"Shared with me": "Mit mir geteilt",
"Sign in": "Anmelden",
"Sign out": "Abmelden",
"Sort By": "Sortieren nach",
"Start Recording": "Aufnahme starten",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Beginnen Sie mit dem Hochladen von Medien und dem Teilen Ihrer Arbeit. Hochgeladene Medien werden hier angezeigt.",
"Stop Recording": "Aufnahme stoppen",
"Submit": "Absenden",
"Subtitle was added": "Untertitel wurde hinzugefügt",
"Subtitles": "Untertitel",
"Successfully Copied": "Erfolgreich kopiert",
"Successfully Disabled Download": "Download erfolgreich deaktiviert",
"Successfully Disabled comments": "Kommentare erfolgreich deaktiviert",
"Successfully Enabled Download": "Download erfolgreich aktiviert",
"Successfully Enabled comments": "Kommentare erfolgreich aktiviert",
"Successfully changed owner": "Eigentümer erfolgreich geändert",
"Successfully deleted": "Erfolgreich gelöscht",
"Successfully updated": "Erfolgreich aktualisiert",
"Successfully updated categories": "Kategorien erfolgreich aktualisiert",
"Successfully updated playlist membership": "Playlist-Mitgliedschaft erfolgreich aktualisiert",
"Successfully updated publish state": "Veröffentlichungsstatus erfolgreich aktualisiert",
"Successfully updated tags": "Tags erfolgreich aktualisiert",
"TAGS": "TAGS",
"Tag": "Tag",
"Tags": "Tags",
"Terms": "Bedingungen",
"The intersection of categories in the selected media is shown": "Die Schnittmenge der Kategorien der ausgewählten Medien wird angezeigt",
"The intersection of playlists in the selected media is shown": "Die Schnittmenge der Playlists der ausgewählten Medien wird angezeigt",
"The intersection of tags in the selected media is shown": "Die Schnittmenge der Tags der ausgewählten Medien wird angezeigt",
"The intersection of users in the selected media is shown": "Die Schnittmenge der Benutzer der ausgewählten Medien wird angezeigt",
"The media was deleted successfully.": "Die Medien wurden erfolgreich gelöscht.",
"This month": "Dieser Monat",
"This week": "Diese Woche",
"This works in Chrome, Safari and Edge browsers.": "Dies funktioniert in den Browsern Chrome, Safari und Edge.",
"This year": "Dieses Jahr",
"To add": "Hinzuzufügen",
"Today": "Heute",
"Trim": "Trimmen",
"UPLOAD": "HOCHLADEN",
"UPLOAD DATE": "UPLOAD-DATUM",
"UPLOAD MEDIA": "MEDIEN HOCHLADEN",
"Undo removal": "Entfernen rückgängig machen",
"Unlisted": "Nicht gelistet",
"Up Next": "Als nächstes",
"Up next": "Als nächstes",
"Upload": "Hochladen",
"Upload date (newest)": "Upload-Datum (neueste)",
"Upload date (oldest)": "Upload-Datum (älteste)",
"Upload date - Newest": "Upload-Datum - Neueste",
"Upload date - Oldest": "Upload-Datum - Älteste",
"Upload media": "Medien hochladen",
"Uploads": "Uploads",
"Users": "Benutzer",
"VIEW ALL": "ALLE ANZEIGEN",
"Video": "Video",
"View all": "Alle anzeigen",
"View count": "Anzahl der Aufrufe",
"View media": "Medien anzeigen",
"Welcome": "Willkommen",
"You are going to copy": "Sie werden kopieren",
"You are going to delete": "Sie werden löschen",
"You are going to disable comments to": "Sie werden Kommentare deaktivieren für",
"You are going to disable download for": "Sie werden Download deaktivieren für",
"You are going to enable comments to": "Sie werden Kommentare aktivieren für",
"You are going to enable download for": "Sie werden Download aktivieren für",
"comment": "Kommentar",
"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": "ist ein modernes, voll ausgestattetes Open-Source-Video- und Medien-CMS. Es wurde entwickelt, um den Anforderungen moderner Webplattformen für das Ansehen und Teilen von Medien gerecht zu werden",
"media in category": "Medien in Kategorie",
"media in tag": "Medien in Tag",
"media, are you sure?": "Medien, sind Sie sicher?",
"media.": "Medien.",
"or": "oder",
"results for": "Ergebnisse für",
"selected": "ausgewählt",
"view": "Ansicht",
"views": "Ansichten",
"yet": "noch",
}
replacement_strings = {
"Apr": "Apr",
"Aug": "Aug",
"Dec": "Dez",
"Feb": "Feb",
"Jan": "Jan",
"Jul": "Jul",
"Jun": "Jun",
"Mar": "Mär",
"May": "Mai",
"Nov": "Nov",
"Oct": "Okt",
"Sep": "Sep",
"day ago": "vor einem Tag",
"days ago": "vor Tagen",
"hour ago": "vor einer Stunde",
"hours ago": "vor Stunden",
"just now": "gerade eben",
"minute ago": "vor einer Minute",
"minutes ago": "vor Minuten",
"month ago": "vor einem Monat",
"months ago": "vor Monaten",
"second ago": "vor einer Sekunde",
"seconds ago": "vor Sekunden",
"week ago": "vor einer Woche",
"weeks ago": "vor Wochen",
"year ago": "vor einem Jahr",
"years ago": "vor Jahren",
}

View 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": "Αλφαβητικά - Α",
"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": "Αριθμός likes",
"Liked media": "Αγαπημένα αρχεία",
"Likes - Least": "Likes - Λιγότερα",
"Likes - Most": "Likes - Περισσότερα",
"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": "είναι ένα σύγχρονο, πλήρως λειτουργικό ανοιχτού κώδικα 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": "χρόνια πριν",
}

View File

@@ -0,0 +1,288 @@
translation_strings = {
"+ Create Playlist": "",
"00 - 20 min": "",
"1 result for": "",
"20 - 40 min": "",
"40 - 60 min": "",
"60 - 120 min+": "",
"ABOUT": "",
"About": "",
"Add / Remove Co-Editors": "",
"Add / Remove Co-Owners": "",
"Add / Remove Co-Viewers": "",
"Add / Remove from Categories": "",
"Add / Remove Tags": "",
"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": "",
"AUTOPLAY": "",
"Browse your files": "",
"Bulk Actions": "",
"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": "",
"Comment": "",
"comment": "",
"Comments": "",
"Comments are disabled": "",
"Confirm": "",
"Confirm Action": "",
"Contact": "",
"Copy Media": "",
"Create": "",
"DELETE": "",
"DELETE MEDIA": "",
"Delete media": "",
"Delete Media": "",
"Disable Comments": "",
"Disable Download": "",
"DOWNLOAD": "",
"Drag and drop files": "",
"DURATION": "",
"EDIT MEDIA": "",
"Edit media": "",
"EDIT PROFILE": "",
"Edit profile": "",
"EDIT SUBTITLE": "",
"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": "",
"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": "",
"Language": "",
"Latest": "",
"Like count": "",
"Liked media": "",
"Likes - Least": "",
"Likes - Most": "",
"Loading categories...": "",
"Loading existing users...": "",
"Loading playlists...": "",
"Loading tags...": "",
"Manage": "",
"Manage comments": "",
"Manage media": "",
"Manage Playlists": "",
"Manage users": "",
"Media": "",
"Media I own": "",
"media in category": "",
"media in tag": "",
"MEDIA TYPE": "",
"Media was edited": "",
"media, are you sure?": "",
"media.": "",
"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": "",
"or": "",
"Pdf": "",
"PLAYLISTS": "",
"Playlists": "",
"Plays - Least": "",
"Plays - Most": "",
"Please select a publish state": "",
"Please select a user": "",
"Powered by": "",
"Private": "",
"Proceed": "",
"Processing...": "",
"Public": "",
"Publish": "",
"PUBLISH STATE": "",
"Publish State": "",
"Published": "",
"Published on": "",
"Recent uploads": "",
"Recommended": "",
"Record Screen": "",
"Register": "",
"Remove category": "",
"Remove from list": "",
"Remove tag": "",
"Remove user": "",
"results for": "",
"SAVE": "",
"SEARCH": "",
"Search": "",
"Search for user...": "",
"Search users to add...": "",
"Select": "",
"Select all": "",
"Select all media": "",
"Select Owner": "",
"Select publish state:": "",
"Selected": "",
"selected": "",
"SHARE": "",
"Shared by me": "",
"Shared with me": "",
"SHOW MORE": "",
"Sign in": "",
"Sign out": "",
"SORT BY": "",
"Sort By": "",
"Start Recording": "",
"Start uploading media and sharing your work. Media that you upload will show up here.": "",
"Stop Recording": "",
"SUBMIT": "",
"Submit": "",
"Subtitle was added": "",
"Subtitles": "",
"Successfully changed owner": "",
"Successfully Copied": "",
"Successfully deleted": "",
"Successfully Disabled comments": "",
"Successfully Disabled Download": "",
"Successfully Enabled comments": "",
"Successfully Enabled Download": "",
"Successfully updated": "",
"Successfully updated categories": "",
"Successfully updated playlist membership": "",
"Successfully updated publish state": "",
"Successfully updated tags": "",
"Tag": "",
"TAGS": "",
"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": "",
"Undo removal": "",
"Unlisted": "",
"Up Next": "",
"Up next": "",
"UPLOAD": "",
"Upload": "",
"UPLOAD DATE": "",
"Upload date (newest)": "",
"Upload date (oldest)": "",
"Upload date - Newest": "",
"Upload date - Oldest": "",
"UPLOAD MEDIA": "",
"Upload media": "",
"Uploads": "",
"Users": "",
"Video": "",
"view": "",
"VIEW ALL": "",
"View all": "",
"View count": "",
"View media": "",
"views": "",
"Welcome": "",
"yet": "",
"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": "",
}
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": "",
}

View File

@@ -0,0 +1,288 @@
translation_strings = {
"+ Create Playlist": "+ Crear Lista de Reproducción",
"00 - 20 min": "00 - 20 min",
"1 result for": "1 resultado para",
"20 - 40 min": "20 - 40 min",
"40 - 60 min": "40 - 60 min",
"60 - 120 min+": "60 - 120 min+",
"ABOUT": "Acerca de",
"AUTOPLAY": "Reproducción automática",
"About": "Acerca de",
"Add / Remove Co-Editors": "Agregar / Eliminar Coeditores",
"Add / Remove Co-Owners": "Agregar / Eliminar Copropietarios",
"Add / Remove Co-Viewers": "Agregar / Eliminar Covisores",
"Add / Remove Tags": "Agregar / Eliminar Etiquetas",
"Add / Remove from Categories": "Agregar / Eliminar de Categorías",
"Add a ": "Agregar un ",
"Add to": "Agregar a",
"Add to / Remove from Category": "Agregar / Eliminar de Categoría",
"Add to / Remove from Playlist": "Agregar / Eliminar de Lista de Reproducción",
"All": "Todos",
"All categories already added": "Todas las categorías ya agregadas",
"All tags already added": "Todas las etiquetas ya agregadas",
"Alphabetically - A-Z": "Alfabéticamente - A-Z",
"Alphabetically - Z-A": "Alfabéticamente - Z-A",
"Audio": "Audio",
"Browse your files": "Explorar sus archivos",
"Bulk Actions": "Acciones Masivas",
"COMMENT": "COMENTARIO",
"Cancel": "Cancelar",
"Categories": "Categorías",
"Category": "Categoría",
"Change Language": "Cambiar idioma",
"Change Owner": "Cambiar Propietario",
"Change password": "Cambiar contraseña",
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "Haga clic en 'Iniciar grabación' y seleccione la pantalla o pestaña para grabar. Una vez finalizada la grabación, haga clic en 'Detener grabación' y la grabación se subirá.",
"Co-Editors": "Coeditores",
"Co-Owners": "Copropietarios",
"Co-Viewers": "Covisores",
"Comment": "Comentario",
"Comments": "Comentarios",
"Comments are disabled": "Los comentarios están deshabilitados",
"Confirm": "Confirmar",
"Confirm Action": "Confirmar Acción",
"Contact": "Contacto",
"Copy Media": "Copiar Medio",
"Create": "Crear",
"DELETE": "ELIMINAR",
"DELETE MEDIA": "ELIMINAR MEDIOS",
"DOWNLOAD": "DESCARGAR",
"DURATION": "DURACIÓN",
"Delete Media": "Eliminar Medio",
"Delete media": "Eliminar medios",
"Disable Comments": "Deshabilitar Comentarios",
"Disable Download": "Deshabilitar Descarga",
"Drag and drop files": "Arrastre y suelte archivos",
"EDIT MEDIA": "EDITAR MEDIOS",
"EDIT PROFILE": "EDITAR PERFIL",
"EDIT SUBTITLE": "EDITAR SUBTÍTULO",
"Edit media": "Editar medios",
"Edit profile": "Editar perfil",
"Edit subtitle": "Editar subtítulo",
"Enable Comments": "Habilitar Comentarios",
"Enable Download": "Habilitar Descarga",
"Enter playlist name...": "Ingrese nombre de lista de reproducción...",
"Failed to add categories": "Error al agregar categorías",
"Failed to add media to playlists": "Error al agregar medios a listas de reproducción",
"Failed to add tags": "Error al agregar etiquetas",
"Failed to add users": "Error al agregar usuarios",
"Failed to change owner": "Error al cambiar propietario",
"Failed to change owner. Please try again.": "Error al cambiar propietario. Por favor, inténtelo de nuevo.",
"Failed to copy media.": "Error al copiar medios.",
"Failed to create playlist": "Error al crear lista de reproducción",
"Failed to delete media. Please try again.": "Error al eliminar medios. Por favor, inténtelo de nuevo.",
"Failed to disable comments.": "Error al deshabilitar comentarios.",
"Failed to disable download.": "Error al deshabilitar descarga.",
"Failed to enable comments.": "Error al habilitar comentarios.",
"Failed to enable download.": "Error al habilitar descarga.",
"Failed to fetch all categories": "Error al obtener todas las categorías",
"Failed to fetch all tags": "Error al obtener todas las etiquetas",
"Failed to fetch existing categories": "Error al obtener categorías existentes",
"Failed to fetch existing tags": "Error al obtener etiquetas existentes",
"Failed to fetch existing users": "Error al obtener usuarios existentes",
"Failed to fetch playlist membership": "Error al obtener membresía de lista de reproducción",
"Failed to fetch playlists": "Error al obtener listas de reproducción",
"Failed to load categories": "Error al cargar categorías",
"Failed to load existing permissions": "Error al cargar permisos existentes",
"Failed to load playlists": "Error al cargar listas de reproducción",
"Failed to load tags": "Error al cargar etiquetas",
"Failed to remove categories": "Error al eliminar categorías",
"Failed to remove media from playlists": "Error al eliminar medios de listas de reproducción",
"Failed to remove tags": "Error al eliminar etiquetas",
"Failed to remove users": "Error al eliminar usuarios",
"Failed to search users": "Error al buscar usuarios",
"Failed to set publish state": "Error al establecer estado de publicación",
"Failed to set publish state. Please try again.": "Error al establecer estado de publicación. Por favor, inténtelo de nuevo.",
"Failed to update categories. Please try again.": "Error al actualizar categorías. Por favor, inténtelo de nuevo.",
"Failed to update permissions. Please try again.": "Error al actualizar permisos. Por favor, inténtelo de nuevo.",
"Failed to update playlists. Please try again.": "Error al actualizar listas de reproducción. Por favor, inténtelo de nuevo.",
"Failed to update tags. Please try again.": "Error al actualizar etiquetas. Por favor, inténtelo de nuevo.",
"Featured": "Destacado",
"Filter existing users...": "Filtrar usuarios existentes...",
"Filter playlists...": "Filtrar listas de reproducción...",
"Filters": "Filtros",
"Go": "Ir",
"History": "Historial",
"Home": "Inicio",
"Image": "Imagen",
"Language": "Idioma",
"Latest": "Último",
"Like count": "Cantidad de me gusta",
"Liked media": "Medios que me gustan",
"Likes - Least": "Me gusta - Menos",
"Likes - Most": "Me gusta - Más",
"Loading categories...": "Cargando categorías...",
"Loading existing users...": "Cargando usuarios existentes...",
"Loading playlists...": "Cargando listas de reproducción...",
"Loading tags...": "Cargando etiquetas...",
"MEDIA TYPE": "TIPO DE MEDIO",
"Manage": "Gestionar",
"Manage Playlists": "Gestionar Listas de Reproducción",
"Manage comments": "Gestionar comentarios",
"Manage media": "Gestionar medios",
"Manage users": "Gestionar usuarios",
"Media": "Medios",
"Media I own": "Medios que poseo",
"Media was edited": "El medio fue editado",
"Members": "Miembros",
"My media": "Mis medios",
"My playlists": "Mis listas de reproducción",
"No": "No",
"No categories": "Sin categorías",
"No comment yet": "Aún no hay comentarios",
"No comments yet": "Aún no hay comentarios",
"No existing": "No existente",
"No playlists available": "No hay listas de reproducción disponibles",
"No playlists selected": "No hay listas de reproducción seleccionadas",
"No results for": "No hay resultados para",
"No tags": "Sin etiquetas",
"No users to add": "No hay usuarios para agregar",
"PLAYLISTS": "LISTAS DE REPRODUCCIÓN",
"PUBLISH STATE": "ESTADO DE PUBLICACIÓN",
"Pdf": "PDF",
"Playlists": "Listas de reproducción",
"Plays - Least": "Reproducciones - Menos",
"Plays - Most": "Reproducciones - Más",
"Please select a publish state": "Por favor seleccione un estado de publicación",
"Please select a user": "Por favor seleccione un usuario",
"Powered by": "Desarrollado por",
"Private": "Privado",
"Proceed": "Continuar",
"Processing...": "Procesando...",
"Public": "Público",
"Publish": "Publicar",
"Publish State": "Estado de Publicación",
"Published": "Publicado",
"Published on": "Publicado en",
"Recent uploads": "Subidas recientes",
"Recommended": "Recomendado",
"Record Screen": "Grabar pantalla",
"Register": "Registrarse",
"Remove category": "Eliminar categoría",
"Remove from list": "Eliminar de la lista",
"Remove tag": "Eliminar etiqueta",
"Remove user": "Eliminar usuario",
"SAVE": "GUARDAR",
"SEARCH": "BUSCAR",
"SHARE": "COMPARTIR",
"SHOW MORE": "MOSTRAR MÁS",
"SORT BY": "ORDENAR POR",
"SUBMIT": "ENVIAR",
"Search": "Buscar",
"Search for user...": "Buscar usuario...",
"Search users to add...": "Buscar usuarios para agregar...",
"Select": "Seleccionar",
"Select Owner": "Seleccionar Propietario",
"Select all": "Seleccionar todo",
"Select all media": "Seleccionar todos los medios",
"Select publish state:": "Seleccionar estado de publicación:",
"Selected": "Seleccionado",
"Shared by me": "Compartido por mí",
"Shared with me": "Compartido conmigo",
"Sign in": "Iniciar sesión",
"Sign out": "Cerrar sesión",
"Sort By": "Ordenar por",
"Start Recording": "Iniciar grabación",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Comience a subir medios y compartir su trabajo. Los medios que suba aparecerán aquí.",
"Stop Recording": "Detener grabación",
"Submit": "Enviar",
"Subtitle was added": "El subtítulo fue agregado",
"Subtitles": "Subtítulos",
"Successfully Copied": "Copiado exitosamente",
"Successfully Disabled Download": "Descarga deshabilitada exitosamente",
"Successfully Disabled comments": "Comentarios deshabilitados exitosamente",
"Successfully Enabled Download": "Descarga habilitada exitosamente",
"Successfully Enabled comments": "Comentarios habilitados exitosamente",
"Successfully changed owner": "Propietario cambiado exitosamente",
"Successfully deleted": "Eliminado exitosamente",
"Successfully updated": "Actualizado exitosamente",
"Successfully updated categories": "Categorías actualizadas exitosamente",
"Successfully updated playlist membership": "Membresía de lista de reproducción actualizada exitosamente",
"Successfully updated publish state": "Estado de publicación actualizado exitosamente",
"Successfully updated tags": "Etiquetas actualizadas exitosamente",
"TAGS": "ETIQUETAS",
"Tag": "Etiqueta",
"Tags": "Etiquetas",
"Terms": "Términos",
"The intersection of categories in the selected media is shown": "Se muestran las categorías comunes en los medios seleccionados",
"The intersection of playlists in the selected media is shown": "Se muestran las listas de reproducción comunes en los medios seleccionados",
"The intersection of tags in the selected media is shown": "Se muestran las etiquetas comunes en los medios seleccionados",
"The intersection of users in the selected media is shown": "Se muestran los usuarios comunes en los medios seleccionados",
"The media was deleted successfully.": "El medio fue eliminado exitosamente.",
"This month": "Este mes",
"This week": "Esta semana",
"This works in Chrome, Safari and Edge browsers.": "Esto funciona en los navegadores Chrome, Safari y Edge.",
"This year": "Este año",
"To add": "Para agregar",
"Today": "Hoy",
"Trim": "Recortar",
"UPLOAD": "SUBIR",
"UPLOAD DATE": "FECHA DE SUBIDA",
"UPLOAD MEDIA": "SUBIR MEDIOS",
"Undo removal": "Deshacer eliminación",
"Unlisted": "No listado",
"Up Next": "A continuación",
"Up next": "A continuación",
"Upload": "Subir",
"Upload date (newest)": "Fecha de subida (más reciente)",
"Upload date (oldest)": "Fecha de subida (más antigua)",
"Upload date - Newest": "Fecha de subida - Más reciente",
"Upload date - Oldest": "Fecha de subida - Más antigua",
"Upload media": "Subir medios",
"Uploads": "Subidas",
"Users": "Usuarios",
"VIEW ALL": "VER TODO",
"Video": "Video",
"View all": "Ver todo",
"View count": "Cantidad de vistas",
"View media": "Ver medios",
"Welcome": "Bienvenido",
"You are going to copy": "Vas a copiar",
"You are going to delete": "Vas a eliminar",
"You are going to disable comments to": "Vas a deshabilitar comentarios de",
"You are going to disable download for": "Vas a deshabilitar descarga de",
"You are going to enable comments to": "Vas a habilitar comentarios de",
"You are going to enable download for": "Vas a habilitar descarga de",
"comment": "comentario",
"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": "es un CMS de video y medios de código abierto, moderno y completamente equipado. Está desarrollado para satisfacer las necesidades de las plataformas web modernas para ver y compartir medios",
"media in category": "medios en la categoría",
"media in tag": "medios en la etiqueta",
"media, are you sure?": "medios, ¿está seguro?",
"media.": "medios.",
"or": "o",
"results for": "resultados para",
"selected": "seleccionado",
"view": "vista",
"views": "vistas",
"yet": "aún",
}
replacement_strings = {
"Apr": "Abr",
"Aug": "Ago",
"Dec": "Dic",
"Feb": "Feb",
"Jan": "Ene",
"Jul": "Jul",
"Jun": "Jun",
"Mar": "Mar",
"May": "May",
"Nov": "Nov",
"Oct": "Oct",
"Sep": "Sep",
"day ago": "hace un día",
"days ago": "hace días",
"hour ago": "hace una hora",
"hours ago": "hace horas",
"just now": "justo ahora",
"minute ago": "hace un minuto",
"minutes ago": "hace minutos",
"month ago": "hace un mes",
"months ago": "hace meses",
"second ago": "hace un segundo",
"seconds ago": "hace segundos",
"week ago": "hace una semana",
"weeks ago": "hace semanas",
"year ago": "hace un año",
"years ago": "hace años",
}

View File

@@ -0,0 +1,289 @@
translation_strings = {
"+ Create Playlist": "+ Créer une playlist",
"00 - 20 min": "00 - 20 min",
"1 result for": "1 résultat pour",
"20 - 40 min": "20 - 40 min",
"40 - 60 min": "40 - 60 min",
"60 - 120 min+": "60 - 120 min+",
"ABOUT": "À PROPOS",
"AUTOPLAY": "Lecture automatique",
"About": "À propos",
"Add / Remove Co-Editors": "Ajouter / Supprimer des co-éditeurs",
"Add / Remove Co-Owners": "Ajouter / Supprimer des co-propriétaires",
"Add / Remove Co-Viewers": "Ajouter / Supprimer des co-visualisateurs",
"Add / Remove Tags": "Ajouter / Supprimer des tags",
"Add / Remove from Categories": "Ajouter / Supprimer des catégories",
"Add a": "Ajouter un",
"Add a ": "Ajouter un ",
"Add to": "Ajouter à",
"Add to / Remove from Category": "Ajouter / Supprimer de la catégorie",
"Add to / Remove from Playlist": "Ajouter / Supprimer de la playlist",
"All": "Tout",
"All categories already added": "Toutes les catégories déjà ajoutées",
"All tags already added": "Tous les tags déjà ajoutés",
"Alphabetically - A-Z": "Alphabétiquement - A-Z",
"Alphabetically - Z-A": "Alphabétiquement - Z-A",
"Audio": "Audio",
"Browse your files": "Parcourir vos fichiers",
"Bulk Actions": "Actions groupées",
"COMMENT": "COMMENTAIRE",
"Cancel": "Annuler",
"Categories": "Catégories",
"Category": "Catégorie",
"Change Language": "Changer de langue",
"Change Owner": "Changer de propriétaire",
"Change password": "Changer le mot de passe",
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "Cliquez sur 'Démarrer l'enregistrement' et sélectionnez l'écran ou l'onglet à enregistrer. Une fois l'enregistrement terminé, cliquez sur 'Arrêter l'enregistrement', et l'enregistrement sera téléversé.",
"Co-Editors": "Co-éditeurs",
"Co-Owners": "Co-propriétaires",
"Co-Viewers": "Co-visualisateurs",
"Comment": "Commentaire",
"Comments": "Commentaires",
"Comments are disabled": "Les commentaires sont désactivés",
"Confirm": "Confirmer",
"Confirm Action": "Confirmer l'action",
"Contact": "Contact",
"Copy Media": "Copier le média",
"Create": "Créer",
"DELETE": "SUPPRIMER",
"DELETE MEDIA": "SUPPRIMER LE MÉDIA",
"DOWNLOAD": "TÉLÉCHARGER",
"DURATION": "DURÉE",
"Delete Media": "Supprimer le média",
"Delete media": "Supprimer le média",
"Disable Comments": "Désactiver les commentaires",
"Disable Download": "Désactiver le téléchargement",
"Drag and drop files": "Glisser-déposer des fichiers",
"EDIT MEDIA": "MODIFIER LE MÉDIA",
"EDIT PROFILE": "MODIFIER LE PROFIL",
"EDIT SUBTITLE": "MODIFIER LE SOUS-TITRE",
"Edit media": "Modifier le média",
"Edit profile": "Modifier le profil",
"Edit subtitle": "Modifier le sous-titre",
"Enable Comments": "Activer les commentaires",
"Enable Download": "Activer le téléchargement",
"Enter playlist name...": "Entrez le nom de la playlist...",
"Failed to add categories": "Échec de l'ajout des catégories",
"Failed to add media to playlists": "Échec de l'ajout du média aux playlists",
"Failed to add tags": "Échec de l'ajout des tags",
"Failed to add users": "Échec de l'ajout des utilisateurs",
"Failed to change owner": "Échec du changement de propriétaire",
"Failed to change owner. Please try again.": "Échec du changement de propriétaire. Veuillez réessayer.",
"Failed to copy media.": "Échec de la copie du média.",
"Failed to create playlist": "Échec de la création de la playlist",
"Failed to delete media. Please try again.": "Échec de la suppression du média. Veuillez réessayer.",
"Failed to disable comments.": "Échec de la désactivation des commentaires.",
"Failed to disable download.": "Échec de la désactivation du téléchargement.",
"Failed to enable comments.": "Échec de l'activation des commentaires.",
"Failed to enable download.": "Échec de l'activation du téléchargement.",
"Failed to fetch all categories": "Échec de la récupération de toutes les catégories",
"Failed to fetch all tags": "Échec de la récupération de tous les tags",
"Failed to fetch existing categories": "Échec de la récupération des catégories existantes",
"Failed to fetch existing tags": "Échec de la récupération des tags existants",
"Failed to fetch existing users": "Échec de la récupération des utilisateurs existants",
"Failed to fetch playlist membership": "Échec de la récupération de l'adhésion à la playlist",
"Failed to fetch playlists": "Échec de la récupération des playlists",
"Failed to load categories": "Échec du chargement des catégories",
"Failed to load existing permissions": "Échec du chargement des permissions existantes",
"Failed to load playlists": "Échec du chargement des playlists",
"Failed to load tags": "Échec du chargement des tags",
"Failed to remove categories": "Échec de la suppression des catégories",
"Failed to remove media from playlists": "Échec de la suppression du média des playlists",
"Failed to remove tags": "Échec de la suppression des tags",
"Failed to remove users": "Échec de la suppression des utilisateurs",
"Failed to search users": "Échec de la recherche d'utilisateurs",
"Failed to set publish state": "Échec de la définition de l'état de publication",
"Failed to set publish state. Please try again.": "Échec de la définition de l'état de publication. Veuillez réessayer.",
"Failed to update categories. Please try again.": "Échec de la mise à jour des catégories. Veuillez réessayer.",
"Failed to update permissions. Please try again.": "Échec de la mise à jour des permissions. Veuillez réessayer.",
"Failed to update playlists. Please try again.": "Échec de la mise à jour des playlists. Veuillez réessayer.",
"Failed to update tags. Please try again.": "Échec de la mise à jour des tags. Veuillez réessayer.",
"Featured": "En vedette",
"Filter existing users...": "Filtrer les utilisateurs existants...",
"Filter playlists...": "Filtrer les playlists...",
"Filters": "Filtres",
"Go": "Aller",
"History": "Historique",
"Home": "Accueil",
"Image": "Image",
"Language": "Langue",
"Latest": "Dernier",
"Like count": "Nombre de j'aime",
"Liked media": "Médias aimés",
"Likes - Least": "J'aime - Moins",
"Likes - Most": "J'aime - Plus",
"Loading categories...": "Chargement des catégories...",
"Loading existing users...": "Chargement des utilisateurs existants...",
"Loading playlists...": "Chargement des playlists...",
"Loading tags...": "Chargement des tags...",
"MEDIA TYPE": "TYPE DE MÉDIA",
"Manage": "Gérer",
"Manage Playlists": "Gérer les playlists",
"Manage comments": "Gérer les commentaires",
"Manage media": "Gérer les médias",
"Manage users": "Gérer les utilisateurs",
"Media": "Média",
"Media I own": "Médias que je possède",
"Media was edited": "Le média a été modifié",
"Members": "Membres",
"My media": "Mes médias",
"My playlists": "Mes playlists",
"No": "Non",
"No categories": "Aucune catégorie",
"No comment yet": "Pas encore de commentaire",
"No comments yet": "Pas encore de commentaires",
"No existing": "Aucun existant",
"No playlists available": "Aucune playlist disponible",
"No playlists selected": "Aucune playlist sélectionnée",
"No results for": "Aucun résultat pour",
"No tags": "Aucun tag",
"No users to add": "Aucun utilisateur à ajouter",
"PLAYLISTS": "PLAYLISTS",
"PUBLISH STATE": "ÉTAT DE PUBLICATION",
"Pdf": "PDF",
"Playlists": "Playlists",
"Plays - Least": "Lectures - Moins",
"Plays - Most": "Lectures - Plus",
"Please select a publish state": "Veuillez sélectionner un état de publication",
"Please select a user": "Veuillez sélectionner un utilisateur",
"Powered by": "Propulsé par",
"Private": "Privé",
"Proceed": "Continuer",
"Processing...": "Traitement en cours...",
"Public": "Public",
"Publish": "Publier",
"Publish State": "État de publication",
"Published": "Publié",
"Published on": "Publié le",
"Recent uploads": "Téléchargements récents",
"Recommended": "Recommandé",
"Record Screen": "Enregistrer l'écran",
"Register": "S'inscrire",
"Remove category": "Supprimer la catégorie",
"Remove from list": "Supprimer de la liste",
"Remove tag": "Supprimer le tag",
"Remove user": "Supprimer l'utilisateur",
"SAVE": "ENREGISTRER",
"SEARCH": "RECHERCHER",
"SHARE": "PARTAGER",
"SHOW MORE": "MONTRER PLUS",
"SORT BY": "TRIER PAR",
"SUBMIT": "SOUMETTRE",
"Search": "Rechercher",
"Search for user...": "Rechercher un utilisateur...",
"Search users to add...": "Rechercher des utilisateurs à ajouter...",
"Select": "Sélectionner",
"Select Owner": "Sélectionner le propriétaire",
"Select all": "Tout sélectionner",
"Select all media": "Sélectionner tous les médias",
"Select publish state:": "Sélectionner l'état de publication:",
"Selected": "Sélectionné",
"Shared by me": "Partagé par moi",
"Shared with me": "Partagé avec moi",
"Sign in": "Se connecter",
"Sign out": "Se déconnecter",
"Sort By": "Trier par",
"Start Recording": "Commencer l'enregistrement",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Commencez à télécharger des médias et à partager votre travail. Les médias que vous téléchargez apparaîtront ici.",
"Stop Recording": "Arrêter l'enregistrement",
"Submit": "Soumettre",
"Subtitle was added": "Le sous-titre a été ajouté",
"Subtitles": "Sous-titres",
"Successfully Copied": "Copié avec succès",
"Successfully Disabled Download": "Téléchargement désactivé avec succès",
"Successfully Disabled comments": "Commentaires désactivés avec succès",
"Successfully Enabled Download": "Téléchargement activé avec succès",
"Successfully Enabled comments": "Commentaires activés avec succès",
"Successfully changed owner": "Propriétaire changé avec succès",
"Successfully deleted": "Supprimé avec succès",
"Successfully updated": "Mis à jour avec succès",
"Successfully updated categories": "Catégories mises à jour avec succès",
"Successfully updated playlist membership": "Adhésion à la playlist mise à jour avec succès",
"Successfully updated publish state": "État de publication mis à jour avec succès",
"Successfully updated tags": "Tags mis à jour avec succès",
"TAGS": "TAGS",
"Tag": "Tag",
"Tags": "Tags",
"Terms": "Conditions",
"The intersection of categories in the selected media is shown": "L'intersection des catégories dans le média sélectionné est affichée",
"The intersection of playlists in the selected media is shown": "L'intersection des playlists dans le média sélectionné est affichée",
"The intersection of tags in the selected media is shown": "L'intersection des tags dans le média sélectionné est affichée",
"The intersection of users in the selected media is shown": "L'intersection des utilisateurs dans le média sélectionné est affichée",
"The media was deleted successfully.": "Le média a été supprimé avec succès.",
"This month": "Ce mois-ci",
"This week": "Cette semaine",
"This works in Chrome, Safari and Edge browsers.": "Cela fonctionne dans les navigateurs Chrome, Safari et Edge.",
"This year": "Cette année",
"To add": "À ajouter",
"Today": "Aujourd'hui",
"Trim": "Couper",
"UPLOAD": "TÉLÉCHARGER",
"UPLOAD DATE": "DATE DE TÉLÉCHARGEMENT",
"UPLOAD MEDIA": "TÉLÉCHARGER DES MÉDIAS",
"Undo removal": "Annuler la suppression",
"Unlisted": "Non répertorié",
"Up Next": "À suivre",
"Up next": "À suivre",
"Upload": "Télécharger",
"Upload date (newest)": "Date de téléchargement (plus récent)",
"Upload date (oldest)": "Date de téléchargement (plus ancien)",
"Upload date - Newest": "Date de téléchargement - Plus récent",
"Upload date - Oldest": "Date de téléchargement - Plus ancien",
"Upload media": "Télécharger des médias",
"Uploads": "Téléchargements",
"Users": "Utilisateurs",
"VIEW ALL": "VOIR TOUT",
"Video": "Vidéo",
"View all": "Voir tout",
"View count": "Nombre de vues",
"View media": "Voir le média",
"Welcome": "Bienvenue",
"You are going to copy": "Vous allez copier",
"You are going to delete": "Vous allez supprimer",
"You are going to disable comments to": "Vous allez désactiver les commentaires de",
"You are going to disable download for": "Vous allez désactiver le téléchargement de",
"You are going to enable comments to": "Vous allez activer les commentaires de",
"You are going to enable download for": "Vous allez activer le téléchargement de",
"comment": "commentaire",
"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": "est un CMS vidéo et média open source moderne et complet. Il est développé pour répondre aux besoins des plateformes web modernes pour la visualisation et le partage de médias",
"media in category": "média dans la catégorie",
"media in tag": "média dans le tag",
"media, are you sure?": "médias, êtes-vous sûr?",
"media.": "médias.",
"or": "ou",
"results for": "résultats pour",
"selected": "sélectionné",
"view": "vue",
"views": "vues",
"yet": "encore",
}
replacement_strings = {
"Apr": "Avr",
"Aug": "Aoû",
"Dec": "Déc",
"Feb": "Fév",
"Jan": "Jan",
"Jul": "Juil",
"Jun": "Juin",
"Mar": "Mar",
"May": "Mai",
"Nov": "Nov",
"Oct": "Oct",
"Sep": "Sep",
"day ago": "il y a un jour",
"days ago": "il y a quelques jours",
"hour ago": "il y a une heure",
"hours ago": "il y a quelques heures",
"just now": "à l'instant",
"minute ago": "il y a une minute",
"minutes ago": "il y a quelques minutes",
"month ago": "il y a un mois",
"months ago": "il y a quelques mois",
"second ago": "il y a une seconde",
"seconds ago": "il y a quelques secondes",
"week ago": "il y a une semaine",
"weeks ago": "il y a quelques semaines",
"year ago": "il y a un an",
"years ago": "il y a quelques années",
}

View 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": "לפני שנים",
}

View 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": "साल पहले",
}

View File

@@ -0,0 +1,288 @@
translation_strings = {
"+ Create Playlist": "+ Buat Daftar Putar",
"00 - 20 min": "00 - 20 menit",
"1 result for": "1 hasil untuk",
"20 - 40 min": "20 - 40 menit",
"40 - 60 min": "40 - 60 menit",
"60 - 120 min+": "60 - 120 menit+",
"ABOUT": "TENTANG",
"AUTOPLAY": "PUTAR OTOMATIS",
"About": "Tentang",
"Add / Remove Co-Editors": "Tambah / Hapus Editor Bersama",
"Add / Remove Co-Owners": "Tambah / Hapus Pemilik Bersama",
"Add / Remove Co-Viewers": "Tambah / Hapus Penonton Bersama",
"Add / Remove Tags": "Tambah / Hapus Tag",
"Add / Remove from Categories": "Tambah / Hapus dari Kategori",
"Add a ": "Tambahkan ",
"Add to": "Tambahkan ke",
"Add to / Remove from Category": "Tambah / Hapus dari Kategori",
"Add to / Remove from Playlist": "Tambah / Hapus dari Daftar Putar",
"All": "Semua",
"All categories already added": "Semua kategori sudah ditambahkan",
"All tags already added": "Semua tag sudah ditambahkan",
"Alphabetically - A-Z": "Alfabetis - A-Z",
"Alphabetically - Z-A": "Alfabetis - Z-A",
"Audio": "Audio",
"Browse your files": "Jelajahi file Anda",
"Bulk Actions": "Aksi Massal",
"COMMENT": "KOMENTAR",
"Cancel": "Batal",
"Categories": "Kategori",
"Category": "Kategori",
"Change Language": "Ganti Bahasa",
"Change Owner": "Ganti Pemilik",
"Change password": "Ganti kata sandi",
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "Klik 'Mulai Merekam' dan pilih layar atau tab untuk merekam. Setelah perekaman selesai, klik 'Hentikan Perekaman,' dan rekaman akan diunggah.",
"Co-Editors": "Editor Bersama",
"Co-Owners": "Pemilik Bersama",
"Co-Viewers": "Penonton Bersama",
"Comment": "Komentar",
"Comments": "Komentar",
"Comments are disabled": "Komentar dinonaktifkan",
"Confirm": "Konfirmasi",
"Confirm Action": "Konfirmasi Aksi",
"Contact": "Kontak",
"Copy Media": "Salin Media",
"Create": "Buat",
"DELETE": "HAPUS",
"DELETE MEDIA": "HAPUS MEDIA",
"DOWNLOAD": "UNDUH",
"DURATION": "DURASI",
"Delete Media": "Hapus Media",
"Delete media": "Hapus media",
"Disable Comments": "Nonaktifkan Komentar",
"Disable Download": "Nonaktifkan Unduhan",
"Drag and drop files": "Seret dan lepas file",
"EDIT MEDIA": "EDIT MEDIA",
"EDIT PROFILE": "EDIT PROFIL",
"EDIT SUBTITLE": "EDIT SUBTITLE",
"Edit media": "Edit media",
"Edit profile": "Edit profil",
"Edit subtitle": "Edit subtitle",
"Enable Comments": "Aktifkan Komentar",
"Enable Download": "Aktifkan Unduhan",
"Enter playlist name...": "Masukkan nama daftar putar...",
"Failed to add categories": "Gagal menambahkan kategori",
"Failed to add media to playlists": "Gagal menambahkan media ke daftar putar",
"Failed to add tags": "Gagal menambahkan tag",
"Failed to add users": "Gagal menambahkan pengguna",
"Failed to change owner": "Gagal mengganti pemilik",
"Failed to change owner. Please try again.": "Gagal mengganti pemilik. Silakan coba lagi.",
"Failed to copy media.": "Gagal menyalin media.",
"Failed to create playlist": "Gagal membuat daftar putar",
"Failed to delete media. Please try again.": "Gagal menghapus media. Silakan coba lagi.",
"Failed to disable comments.": "Gagal menonaktifkan komentar.",
"Failed to disable download.": "Gagal menonaktifkan unduhan.",
"Failed to enable comments.": "Gagal mengaktifkan komentar.",
"Failed to enable download.": "Gagal mengaktifkan unduhan.",
"Failed to fetch all categories": "Gagal mengambil semua kategori",
"Failed to fetch all tags": "Gagal mengambil semua tag",
"Failed to fetch existing categories": "Gagal mengambil kategori yang ada",
"Failed to fetch existing tags": "Gagal mengambil tag yang ada",
"Failed to fetch existing users": "Gagal mengambil pengguna yang ada",
"Failed to fetch playlist membership": "Gagal mengambil keanggotaan daftar putar",
"Failed to fetch playlists": "Gagal mengambil daftar putar",
"Failed to load categories": "Gagal memuat kategori",
"Failed to load existing permissions": "Gagal memuat izin yang ada",
"Failed to load playlists": "Gagal memuat daftar putar",
"Failed to load tags": "Gagal memuat tag",
"Failed to remove categories": "Gagal menghapus kategori",
"Failed to remove media from playlists": "Gagal menghapus media dari daftar putar",
"Failed to remove tags": "Gagal menghapus tag",
"Failed to remove users": "Gagal menghapus pengguna",
"Failed to search users": "Gagal mencari pengguna",
"Failed to set publish state": "Gagal mengatur status publikasi",
"Failed to set publish state. Please try again.": "Gagal mengatur status publikasi. Silakan coba lagi.",
"Failed to update categories. Please try again.": "Gagal memperbarui kategori. Silakan coba lagi.",
"Failed to update permissions. Please try again.": "Gagal memperbarui izin. Silakan coba lagi.",
"Failed to update playlists. Please try again.": "Gagal memperbarui daftar putar. Silakan coba lagi.",
"Failed to update tags. Please try again.": "Gagal memperbarui tag. Silakan coba lagi.",
"Featured": "Unggulan",
"Filter existing users...": "Filter pengguna yang ada...",
"Filter playlists...": "Filter daftar putar...",
"Filters": "Filter",
"Go": "Pergi",
"History": "Riwayat",
"Home": "Beranda",
"Image": "Gambar",
"Language": "Bahasa",
"Latest": "Terbaru",
"Like count": "Jumlah suka",
"Liked media": "Media yang disukai",
"Likes - Least": "Suka - Paling Sedikit",
"Likes - Most": "Suka - Paling Banyak",
"Loading categories...": "Memuat kategori...",
"Loading existing users...": "Memuat pengguna yang ada...",
"Loading playlists...": "Memuat daftar putar...",
"Loading tags...": "Memuat tag...",
"MEDIA TYPE": "JENIS MEDIA",
"Manage": "Kelola",
"Manage Playlists": "Kelola Daftar Putar",
"Manage comments": "Kelola komentar",
"Manage media": "Kelola media",
"Manage users": "Kelola pengguna",
"Media": "Media",
"Media I own": "Media yang saya miliki",
"Media was edited": "Media telah diedit",
"Members": "Anggota",
"My media": "Media saya",
"My playlists": "Daftar putar saya",
"No": "Tidak",
"No categories": "Tidak ada kategori",
"No comment yet": "Belum ada komentar",
"No comments yet": "Belum ada komentar",
"No existing": "Tidak ada yang sudah ada",
"No playlists available": "Tidak ada daftar putar yang tersedia",
"No playlists selected": "Tidak ada daftar putar yang dipilih",
"No results for": "Tidak ada hasil untuk",
"No tags": "Tidak ada tag",
"No users to add": "Tidak ada pengguna untuk ditambahkan",
"PLAYLISTS": "DAFTAR PUTAR",
"PUBLISH STATE": "STATUS PUBLIKASI",
"Pdf": "PDF",
"Playlists": "Daftar putar",
"Plays - Least": "Pemutaran - Paling Sedikit",
"Plays - Most": "Pemutaran - Paling Banyak",
"Please select a publish state": "Silakan pilih status publikasi",
"Please select a user": "Silakan pilih pengguna",
"Powered by": "Didukung oleh",
"Private": "Pribadi",
"Proceed": "Lanjutkan",
"Processing...": "Memproses...",
"Public": "Publik",
"Publish": "Terbitkan",
"Publish State": "Status Publikasi",
"Published": "Diterbitkan",
"Published on": "Diterbitkan pada",
"Recent uploads": "Unggahan terbaru",
"Recommended": "Direkomendasikan",
"Record Screen": "Rekam Layar",
"Register": "Daftar",
"Remove category": "Hapus kategori",
"Remove from list": "Hapus dari daftar",
"Remove tag": "Hapus tag",
"Remove user": "Hapus pengguna",
"SAVE": "SIMPAN",
"SEARCH": "CARI",
"SHARE": "BAGIKAN",
"SHOW MORE": "TAMPILKAN LEBIH BANYAK",
"SORT BY": "URUTKAN BERDASARKAN",
"SUBMIT": "KIRIM",
"Search": "Cari",
"Search for user...": "Cari pengguna...",
"Search users to add...": "Cari pengguna untuk ditambahkan...",
"Select": "Pilih",
"Select Owner": "Pilih Pemilik",
"Select all": "Pilih semua",
"Select all media": "Pilih semua media",
"Select publish state:": "Pilih status publikasi:",
"Selected": "Dipilih",
"Shared by me": "Dibagikan oleh saya",
"Shared with me": "Dibagikan dengan saya",
"Sign in": "Masuk",
"Sign out": "Keluar",
"Sort By": "Urutkan Berdasarkan",
"Start Recording": "Mulai Merekam",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Mulai mengunggah media dan berbagi karya Anda. Media yang Anda unggah akan muncul di sini.",
"Stop Recording": "Hentikan Perekaman",
"Submit": "Kirim",
"Subtitle was added": "Subtitle telah ditambahkan",
"Subtitles": "Subtitel",
"Successfully Copied": "Berhasil Disalin",
"Successfully Disabled Download": "Unduhan Berhasil Dinonaktifkan",
"Successfully Disabled comments": "Komentar berhasil dinonaktifkan",
"Successfully Enabled Download": "Unduhan Berhasil Diaktifkan",
"Successfully Enabled comments": "Komentar berhasil diaktifkan",
"Successfully changed owner": "Berhasil mengganti pemilik",
"Successfully deleted": "Berhasil dihapus",
"Successfully updated": "Berhasil diperbarui",
"Successfully updated categories": "Kategori berhasil diperbarui",
"Successfully updated playlist membership": "Keanggotaan daftar putar berhasil diperbarui",
"Successfully updated publish state": "Status publikasi berhasil diperbarui",
"Successfully updated tags": "Tag berhasil diperbarui",
"TAGS": "TAG",
"Tag": "Tag",
"Tags": "Tag",
"Terms": "Ketentuan",
"The intersection of categories in the selected media is shown": "Irisan kategori dalam media yang dipilih ditampilkan",
"The intersection of playlists in the selected media is shown": "Irisan daftar putar dalam media yang dipilih ditampilkan",
"The intersection of tags in the selected media is shown": "Irisan tag dalam media yang dipilih ditampilkan",
"The intersection of users in the selected media is shown": "Irisan pengguna dalam media yang dipilih ditampilkan",
"The media was deleted successfully.": "Media berhasil dihapus.",
"This month": "Bulan ini",
"This week": "Minggu ini",
"This works in Chrome, Safari and Edge browsers.": "Ini berfungsi di browser Chrome, Safari, dan Edge.",
"This year": "Tahun ini",
"To add": "Untuk ditambahkan",
"Today": "Hari ini",
"Trim": "Potong",
"UPLOAD": "UNGGAH",
"UPLOAD DATE": "TANGGAL UNGGAH",
"UPLOAD MEDIA": "UNGGAH MEDIA",
"Undo removal": "Batalkan penghapusan",
"Unlisted": "Tidak terdaftar",
"Up Next": "Selanjutnya",
"Up next": "Selanjutnya",
"Upload": "Unggah",
"Upload date (newest)": "Tanggal unggah (terbaru)",
"Upload date (oldest)": "Tanggal unggah (terlama)",
"Upload date - Newest": "Tanggal unggah - Terbaru",
"Upload date - Oldest": "Tanggal unggah - Terlama",
"Upload media": "Unggah media",
"Uploads": "Unggahan",
"Users": "Pengguna",
"VIEW ALL": "LIHAT SEMUA",
"Video": "Video",
"View all": "Lihat semua",
"View count": "Jumlah tampilan",
"View media": "Lihat media",
"Welcome": "Selamat datang",
"You are going to copy": "Anda akan menyalin",
"You are going to delete": "Anda akan menghapus",
"You are going to disable comments to": "Anda akan menonaktifkan komentar untuk",
"You are going to disable download for": "Anda akan menonaktifkan unduhan untuk",
"You are going to enable comments to": "Anda akan mengaktifkan komentar untuk",
"You are going to enable download for": "Anda akan mengaktifkan unduhan untuk",
"comment": "komentar",
"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": "adalah CMS video dan media open source yang modern dan lengkap. Ini dikembangkan untuk memenuhi kebutuhan platform web modern untuk menonton dan berbagi media",
"media in category": "media dalam kategori",
"media in tag": "media dalam tag",
"media, are you sure?": "media, apakah Anda yakin?",
"media.": "media.",
"or": "atau",
"results for": "hasil untuk",
"selected": "dipilih",
"view": "lihat",
"views": "tampilan",
"yet": "belum",
}
replacement_strings = {
"Apr": "Apr",
"Aug": "Agu",
"Dec": "Des",
"Feb": "Feb",
"Jan": "Jan",
"Jul": "Jul",
"Jun": "Jun",
"Mar": "Mar",
"May": "Mei",
"Nov": "Nov",
"Oct": "Okt",
"Sep": "Sep",
"day ago": "hari yang lalu",
"days ago": "hari yang lalu",
"hour ago": "jam yang lalu",
"hours ago": "jam yang lalu",
"just now": "baru saja",
"minute ago": "menit yang lalu",
"minutes ago": "menit yang lalu",
"month ago": "bulan yang lalu",
"months ago": "bulan yang lalu",
"second ago": "detik yang lalu",
"seconds ago": "detik yang lalu",
"week ago": "minggu yang lalu",
"weeks ago": "minggu yang lalu",
"year ago": "tahun yang lalu",
"years ago": "tahun yang lalu",
}

View File

@@ -0,0 +1,289 @@
translation_strings = {
"+ Create Playlist": "+ Crea Playlist",
"00 - 20 min": "00 - 20 min",
"1 result for": "1 risultato per",
"20 - 40 min": "20 - 40 min",
"40 - 60 min": "40 - 60 min",
"60 - 120 min+": "60 - 120 min+",
"ABOUT": "SU DI NOI",
"AUTOPLAY": "RIPRODUZIONE AUTOMATICA",
"About": "Su di noi",
"Add / Remove Co-Editors": "Aggiungi / Rimuovi Co-Editor",
"Add / Remove Co-Owners": "Aggiungi / Rimuovi Co-Proprietari",
"Add / Remove Co-Viewers": "Aggiungi / Rimuovi Co-Visualizzatori",
"Add / Remove Tags": "Aggiungi / Rimuovi Tag",
"Add / Remove from Categories": "Aggiungi / Rimuovi dalle Categorie",
"Add a": "Aggiungi un",
"Add a ": "Aggiungi un ",
"Add to": "Aggiungi a",
"Add to / Remove from Category": "Aggiungi / Rimuovi dalla Categoria",
"Add to / Remove from Playlist": "Aggiungi / Rimuovi dalla Playlist",
"All": "Tutti",
"All categories already added": "Tutte le categorie già aggiunte",
"All tags already added": "Tutti i tag già aggiunti",
"Alphabetically - A-Z": "Alfabeticamente - A-Z",
"Alphabetically - Z-A": "Alfabeticamente - Z-A",
"Audio": "Audio",
"Browse your files": "Sfoglia i tuoi file",
"Bulk Actions": "Azioni di Massa",
"COMMENT": "COMMENTA",
"Cancel": "Annulla",
"Categories": "Categorie",
"Category": "Categoria",
"Change Language": "Cambia lingua",
"Change Owner": "Cambia Proprietario",
"Change password": "Cambia 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.": "Fai clic su 'Avvia registrazione' e seleziona lo schermo o la scheda da registrare. Una volta terminata la registrazione, fai clic su 'Interrompi registrazione' e la registrazione verrà caricata.",
"Co-Editors": "Co-Editor",
"Co-Owners": "Co-Proprietari",
"Co-Viewers": "Co-Visualizzatori",
"Comment": "Commento",
"Comments": "Commenti",
"Comments are disabled": "I commenti sono disabilitati",
"Confirm": "Conferma",
"Confirm Action": "Conferma Azione",
"Contact": "Contatti",
"Copy Media": "Copia Media",
"Create": "Crea",
"DELETE": "ELIMINA",
"DELETE MEDIA": "ELIMINA MEDIA",
"DOWNLOAD": "SCARICA",
"DURATION": "DURATA",
"Delete Media": "Elimina Media",
"Delete media": "Elimina media",
"Disable Comments": "Disabilita Commenti",
"Disable Download": "Disabilita Download",
"Drag and drop files": "Trascina e rilascia i file",
"EDIT MEDIA": "MODIFICA IL MEDIA",
"EDIT PROFILE": "MODIFICA IL PROFILO",
"EDIT SUBTITLE": "MODIFICA I SOTTOTITOLI",
"Edit media": "Modifica il media",
"Edit profile": "Modifica il profilo",
"Edit subtitle": "Modifica i sottotitoli",
"Enable Comments": "Abilita Commenti",
"Enable Download": "Abilita Download",
"Enter playlist name...": "Inserisci nome playlist...",
"Failed to add categories": "Impossibile aggiungere categorie",
"Failed to add media to playlists": "Impossibile aggiungere media alle playlist",
"Failed to add tags": "Impossibile aggiungere tag",
"Failed to add users": "Impossibile aggiungere utenti",
"Failed to change owner": "Impossibile cambiare proprietario",
"Failed to change owner. Please try again.": "Impossibile cambiare proprietario. Riprova.",
"Failed to copy media.": "Impossibile copiare il media.",
"Failed to create playlist": "Impossibile creare playlist",
"Failed to delete media. Please try again.": "Impossibile eliminare il media. Riprova.",
"Failed to disable comments.": "Impossibile disabilitare i commenti.",
"Failed to disable download.": "Impossibile disabilitare il download.",
"Failed to enable comments.": "Impossibile abilitare i commenti.",
"Failed to enable download.": "Impossibile abilitare il download.",
"Failed to fetch all categories": "Impossibile recuperare tutte le categorie",
"Failed to fetch all tags": "Impossibile recuperare tutti i tag",
"Failed to fetch existing categories": "Impossibile recuperare le categorie esistenti",
"Failed to fetch existing tags": "Impossibile recuperare i tag esistenti",
"Failed to fetch existing users": "Impossibile recuperare gli utenti esistenti",
"Failed to fetch playlist membership": "Impossibile recuperare l'appartenenza alla playlist",
"Failed to fetch playlists": "Impossibile recuperare le playlist",
"Failed to load categories": "Impossibile caricare le categorie",
"Failed to load existing permissions": "Impossibile caricare i permessi esistenti",
"Failed to load playlists": "Impossibile caricare le playlist",
"Failed to load tags": "Impossibile caricare i tag",
"Failed to remove categories": "Impossibile rimuovere le categorie",
"Failed to remove media from playlists": "Impossibile rimuovere media dalle playlist",
"Failed to remove tags": "Impossibile rimuovere i tag",
"Failed to remove users": "Impossibile rimuovere gli utenti",
"Failed to search users": "Impossibile cercare utenti",
"Failed to set publish state": "Impossibile impostare lo stato di pubblicazione",
"Failed to set publish state. Please try again.": "Impossibile impostare lo stato di pubblicazione. Riprova.",
"Failed to update categories. Please try again.": "Impossibile aggiornare le categorie. Riprova.",
"Failed to update permissions. Please try again.": "Impossibile aggiornare i permessi. Riprova.",
"Failed to update playlists. Please try again.": "Impossibile aggiornare le playlist. Riprova.",
"Failed to update tags. Please try again.": "Impossibile aggiornare i tag. Riprova.",
"Featured": "In evidenza",
"Filter existing users...": "Filtra utenti esistenti...",
"Filter playlists...": "Filtra playlist...",
"Filters": "Filtri",
"Go": "Vai",
"History": "Cronologia",
"Home": "Home",
"Image": "Immagine",
"Language": "Lingua",
"Latest": "Ultimi",
"Like count": "Numero di mi piace",
"Liked media": "Piaciuti",
"Likes - Least": "Mi piace - Meno",
"Likes - Most": "Mi piace - Più",
"Loading categories...": "Caricamento categorie...",
"Loading existing users...": "Caricamento utenti esistenti...",
"Loading playlists...": "Caricamento playlist...",
"Loading tags...": "Caricamento tag...",
"MEDIA TYPE": "TIPO DI MEDIA",
"Manage": "Gestisci",
"Manage Playlists": "Gestisci Playlist",
"Manage comments": "Gestisci i commenti",
"Manage media": "Gestisci i media",
"Manage users": "Gestisci gli utenti",
"Media": "Media",
"Media I own": "Media di mia proprietà",
"Media was edited": "Il media è stato modificato",
"Members": "Membri",
"My media": "I miei media",
"My playlists": "Le mie playlist",
"No": "No",
"No categories": "Nessuna categoria",
"No comment yet": "Ancora nessun commento",
"No comments yet": "Ancora nessun commento",
"No existing": "Nessun esistente",
"No playlists available": "Nessuna playlist disponibile",
"No playlists selected": "Nessuna playlist selezionata",
"No results for": "Nessun risultato per",
"No tags": "Nessun tag",
"No users to add": "Nessun utente da aggiungere",
"PLAYLISTS": "PLAYLIST",
"PUBLISH STATE": "STATO DI PUBBLICAZIONE",
"Pdf": "PDF",
"Playlists": "Playlist",
"Plays - Least": "Riproduzioni - Meno",
"Plays - Most": "Riproduzioni - Più",
"Please select a publish state": "Seleziona uno stato di pubblicazione",
"Please select a user": "Seleziona un utente",
"Powered by": "Powered by",
"Private": "Privato",
"Proceed": "Procedi",
"Processing...": "Elaborazione...",
"Public": "Pubblico",
"Publish": "Pubblica",
"Publish State": "Stato di Pubblicazione",
"Published": "Pubblicato",
"Published on": "Pubblicato il",
"Recent uploads": "Caricamenti recenti",
"Recommended": "Raccomandati",
"Record Screen": "Registra schermo",
"Register": "Registrati",
"Remove category": "Rimuovi categoria",
"Remove from list": "Rimuovi dalla lista",
"Remove tag": "Rimuovi tag",
"Remove user": "Rimuovi utente",
"SAVE": "SALVA",
"SEARCH": "CERCA",
"SHARE": "CONDIVIDI",
"SHOW MORE": "MOSTRA DI PIÙ",
"SORT BY": "ORDINA PER",
"SUBMIT": "INVIA",
"Search": "Cerca",
"Search for user...": "Cerca utente...",
"Search users to add...": "Cerca utenti da aggiungere...",
"Select": "Seleziona",
"Select Owner": "Seleziona Proprietario",
"Select all": "Seleziona tutto",
"Select all media": "Seleziona tutti i media",
"Select publish state:": "Seleziona stato di pubblicazione:",
"Selected": "Selezionato",
"Shared by me": "Condiviso da me",
"Shared with me": "Condiviso con me",
"Sign in": "Login",
"Sign out": "Logout",
"Sort By": "Ordina per",
"Start Recording": "Inizia registrazione",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Inizia a caricare media e condividere il tuo lavoro. I media caricati appariranno qui.",
"Stop Recording": "Interrompi registrazione",
"Submit": "Invia",
"Subtitle was added": "I sottotitoli sono stati aggiunti",
"Subtitles": "Sottotitoli",
"Successfully Copied": "Copiato con successo",
"Successfully Disabled Download": "Download disabilitato con successo",
"Successfully Disabled comments": "Commenti disabilitati con successo",
"Successfully Enabled Download": "Download abilitato con successo",
"Successfully Enabled comments": "Commenti abilitati con successo",
"Successfully changed owner": "Proprietario cambiato con successo",
"Successfully deleted": "Eliminato con successo",
"Successfully updated": "Aggiornato con successo",
"Successfully updated categories": "Categorie aggiornate con successo",
"Successfully updated playlist membership": "Appartenenza alla playlist aggiornata con successo",
"Successfully updated publish state": "Stato di pubblicazione aggiornato con successo",
"Successfully updated tags": "Tag aggiornati con successo",
"TAGS": "TAG",
"Tag": "Tag",
"Tags": "Tag",
"Terms": "Termini e condizioni",
"The intersection of categories in the selected media is shown": "Viene mostrata l'intersezione delle categorie nei media selezionati",
"The intersection of playlists in the selected media is shown": "Viene mostrata l'intersezione delle playlist nei media selezionati",
"The intersection of tags in the selected media is shown": "Viene mostrata l'intersezione dei tag nei media selezionati",
"The intersection of users in the selected media is shown": "Viene mostrata l'intersezione degli utenti nei media selezionati",
"The media was deleted successfully.": "Il media è stato eliminato con successo.",
"This month": "Questo mese",
"This week": "Questa settimana",
"This works in Chrome, Safari and Edge browsers.": "Questo funziona nei browser Chrome, Safari e Edge.",
"This year": "Quest'anno",
"To add": "Da aggiungere",
"Today": "Oggi",
"Trim": "Taglia",
"UPLOAD": "CARICA",
"UPLOAD DATE": "DATA DI CARICAMENTO",
"UPLOAD MEDIA": "CARICA MEDIA",
"Undo removal": "Annulla rimozione",
"Unlisted": "Non in elenco",
"Up Next": "A seguire",
"Up next": "A seguire",
"Upload": "Carica",
"Upload date (newest)": "Data di caricamento (più recente)",
"Upload date (oldest)": "Data di caricamento (più vecchia)",
"Upload date - Newest": "Data di caricamento - Più recente",
"Upload date - Oldest": "Data di caricamento - Più vecchia",
"Upload media": "Carica i media",
"Uploads": "Caricamenti",
"Users": "Utenti",
"VIEW ALL": "MOSTRA TUTTI",
"Video": "Video",
"View all": "Mostra tutti",
"View count": "Numero di visualizzazioni",
"View media": "Visualizza media",
"Welcome": "Benvenuto",
"You are going to copy": "Stai per copiare",
"You are going to delete": "Stai per eliminare",
"You are going to disable comments to": "Stai per disabilitare i commenti di",
"You are going to disable download for": "Stai per disabilitare il download di",
"You are going to enable comments to": "Stai per abilitare i commenti di",
"You are going to enable download for": "Stai per abilitare il download di",
"comment": "commento",
"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": "è un CMS per media open source moderno e completo. È stato sviluppato per rispondere per venire incontro alle esigenze delle moderne piattaforme web di visualizzazione e condivisione media",
"media in category": "media nella categoria",
"media in tag": "media con tag",
"media, are you sure?": "media, sei sicuro?",
"media.": "media.",
"or": "o",
"results for": "risultati per",
"selected": "selezionato",
"view": "visualizzazione",
"views": "visualizzazioni",
"yet": "ancora",
}
replacement_strings = {
"Apr": "Apr",
"Aug": "Ago",
"Dec": "Dic",
"Feb": "Feb",
"Jan": "Gen",
"Jul": "Lug",
"Jun": "Giu",
"Mar": "Mar",
"May": "Mag",
"Nov": "Nov",
"Oct": "Ott",
"Sep": "Set",
"day ago": "giorno fa",
"days ago": "giorni fa",
"hour ago": "ora fa",
"hours ago": "ore fa",
"just now": "adesso",
"minute ago": "minuto fa",
"minutes ago": "minuti fa",
"month ago": "mese fa",
"months ago": "mesi fa",
"second ago": "secondo fa",
"seconds ago": "secondi fa",
"week ago": "settimana fa",
"weeks ago": "settimane fa",
"year ago": "anno fa",
"years ago": "anni fa",
}

View 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.": "これは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": "は、現代のウェブプラットフォームのニーズに応えるために開発された、最新のフル機能のオープンソースビデオおよびメディアCMSです。",
"media in category": "カテゴリー内のメディア",
"media in tag": "タグ内のメディア",
"media, are you sure?": "メディア、よろしいですか?",
"media.": "メディア。",
"or": "または",
"results for": "件の結果",
"selected": "選択済み",
"view": "ビュー",
"views": "ビュー",
"yet": "まだ",
}
replacement_strings = {
"Apr": "4月",
"Aug": "8月",
"Dec": "12月",
"Feb": "2月",
"Jan": "1月",
"Jul": "7月",
"Jun": "6月",
"Mar": "3月",
"May": "5月",
"Nov": "11月",
"Oct": "10月",
"Sep": "9月",
"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": "年前",
}

View 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.": "이 기능은 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": "현대적인, 완전한 기능을 갖춘 오픈 소스 비디오 및 미디어 CMS입니다. 미디어를 시청하고 공유하기 위한 현대 웹 플랫폼의 요구를 충족시키기 위해 개발되었습니다",
"media in category": "카테고리의 미디어",
"media in tag": "태그의 미디어",
"media, are you sure?": "미디어, 확실합니까?",
"media.": "미디어.",
"or": "또는",
"results for": "개 결과",
"selected": "선택됨",
"view": "보기",
"views": "조회수",
"yet": "아직",
}
replacement_strings = {
"Apr": "4월",
"Aug": "8월",
"Dec": "12월",
"Feb": "2월",
"Jan": "1월",
"Jul": "7월",
"Jun": "6월",
"Mar": "3월",
"May": "5월",
"Nov": "11월",
"Oct": "10월",
"Sep": "9월",
"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": "년 전",
}

View File

@@ -0,0 +1,288 @@
translation_strings = {
"+ Create Playlist": "+ Afspeellijst maken",
"00 - 20 min": "00 - 20 min",
"1 result for": "1 resultaat voor",
"20 - 40 min": "20 - 40 min",
"40 - 60 min": "40 - 60 min",
"60 - 120 min+": "60 - 120 min+",
"ABOUT": "OVER",
"AUTOPLAY": "AUTOMATISCH AFSPELEN",
"About": "Over",
"Add / Remove Co-Editors": "Co-Editors toevoegen / verwijderen",
"Add / Remove Co-Owners": "Co-Eigenaren toevoegen / verwijderen",
"Add / Remove Co-Viewers": "Co-Kijkers toevoegen / verwijderen",
"Add / Remove Tags": "Tags toevoegen / verwijderen",
"Add / Remove from Categories": "Toevoegen / verwijderen uit categorieën",
"Add a ": "Voeg een ",
"Add to": "Toevoegen aan",
"Add to / Remove from Category": "Toevoegen / verwijderen uit categorie",
"Add to / Remove from Playlist": "Toevoegen / verwijderen uit afspeellijst",
"All": "Alles",
"All categories already added": "Alle categorieën al toegevoegd",
"All tags already added": "Alle tags al toegevoegd",
"Alphabetically - A-Z": "Alfabetisch - A-Z",
"Alphabetically - Z-A": "Alfabetisch - Z-A",
"Audio": "Audio",
"Browse your files": "Blader door uw bestanden",
"Bulk Actions": "Bulkacties",
"COMMENT": "REACTIE",
"Cancel": "Annuleren",
"Categories": "Categorieën",
"Category": "Categorie",
"Change Language": "Taal wijzigen",
"Change Owner": "Eigenaar wijzigen",
"Change password": "Wachtwoord wijzigen",
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "Klik op 'Opname starten' en selecteer het scherm of tabblad dat u wilt opnemen. Zodra de opname is voltooid, klikt u op 'Opname stoppen' en de opname wordt geüpload.",
"Co-Editors": "Co-Editors",
"Co-Owners": "Co-Eigenaren",
"Co-Viewers": "Co-Kijkers",
"Comment": "Reactie",
"Comments": "Reacties",
"Comments are disabled": "Reacties zijn uitgeschakeld",
"Confirm": "Bevestigen",
"Confirm Action": "Actie bevestigen",
"Contact": "Contact",
"Copy Media": "Media kopiëren",
"Create": "Aanmaken",
"DELETE": "VERWIJDEREN",
"DELETE MEDIA": "MEDIA VERWIJDEREN",
"DOWNLOAD": "DOWNLOADEN",
"DURATION": "DUUR",
"Delete Media": "Media verwijderen",
"Delete media": "Media verwijderen",
"Disable Comments": "Reacties uitschakelen",
"Disable Download": "Download uitschakelen",
"Drag and drop files": "Sleep bestanden en zet ze neer",
"EDIT MEDIA": "MEDIA BEWERKEN",
"EDIT PROFILE": "PROFIEL BEWERKEN",
"EDIT SUBTITLE": "ONDERTITEL BEWERKEN",
"Edit media": "Media bewerken",
"Edit profile": "Profiel bewerken",
"Edit subtitle": "Ondertitel bewerken",
"Enable Comments": "Reacties inschakelen",
"Enable Download": "Download inschakelen",
"Enter playlist name...": "Voer afspeellijstnaam in...",
"Failed to add categories": "Categorieën toevoegen mislukt",
"Failed to add media to playlists": "Media toevoegen aan afspeellijsten mislukt",
"Failed to add tags": "Tags toevoegen mislukt",
"Failed to add users": "Gebruikers toevoegen mislukt",
"Failed to change owner": "Eigenaar wijzigen mislukt",
"Failed to change owner. Please try again.": "Eigenaar wijzigen mislukt. Probeer het opnieuw.",
"Failed to copy media.": "Media kopiëren mislukt.",
"Failed to create playlist": "Afspeellijst maken mislukt",
"Failed to delete media. Please try again.": "Media verwijderen mislukt. Probeer het opnieuw.",
"Failed to disable comments.": "Reacties uitschakelen mislukt.",
"Failed to disable download.": "Download uitschakelen mislukt.",
"Failed to enable comments.": "Reacties inschakelen mislukt.",
"Failed to enable download.": "Download inschakelen mislukt.",
"Failed to fetch all categories": "Alle categorieën ophalen mislukt",
"Failed to fetch all tags": "Alle tags ophalen mislukt",
"Failed to fetch existing categories": "Bestaande categorieën ophalen mislukt",
"Failed to fetch existing tags": "Bestaande tags ophalen mislukt",
"Failed to fetch existing users": "Bestaande gebruikers ophalen mislukt",
"Failed to fetch playlist membership": "Afspeellijstlidmaatschap ophalen mislukt",
"Failed to fetch playlists": "Afspeellijsten ophalen mislukt",
"Failed to load categories": "Categorieën laden mislukt",
"Failed to load existing permissions": "Bestaande machtigingen laden mislukt",
"Failed to load playlists": "Afspeellijsten laden mislukt",
"Failed to load tags": "Tags laden mislukt",
"Failed to remove categories": "Categorieën verwijderen mislukt",
"Failed to remove media from playlists": "Media verwijderen uit afspeellijsten mislukt",
"Failed to remove tags": "Tags verwijderen mislukt",
"Failed to remove users": "Gebruikers verwijderen mislukt",
"Failed to search users": "Gebruikers zoeken mislukt",
"Failed to set publish state": "Publicatiestatus instellen mislukt",
"Failed to set publish state. Please try again.": "Publicatiestatus instellen mislukt. Probeer het opnieuw.",
"Failed to update categories. Please try again.": "Categorieën bijwerken mislukt. Probeer het opnieuw.",
"Failed to update permissions. Please try again.": "Machtigingen bijwerken mislukt. Probeer het opnieuw.",
"Failed to update playlists. Please try again.": "Afspeellijsten bijwerken mislukt. Probeer het opnieuw.",
"Failed to update tags. Please try again.": "Tags bijwerken mislukt. Probeer het opnieuw.",
"Featured": "Aanbevolen",
"Filter existing users...": "Filter bestaande gebruikers...",
"Filter playlists...": "Filter afspeellijsten...",
"Filters": "Filters",
"Go": "Ga",
"History": "Geschiedenis",
"Home": "Home",
"Image": "Afbeelding",
"Language": "Taal",
"Latest": "Laatste",
"Like count": "Aantal likes",
"Liked media": "Leuke media",
"Likes - Least": "Likes - Minst",
"Likes - Most": "Likes - Meest",
"Loading categories...": "Categorieën laden...",
"Loading existing users...": "Bestaande gebruikers laden...",
"Loading playlists...": "Afspeellijsten laden...",
"Loading tags...": "Tags laden...",
"MEDIA TYPE": "MEDIATYPE",
"Manage": "Beheren",
"Manage Playlists": "Afspeellijsten beheren",
"Manage comments": "Reacties beheren",
"Manage media": "Media beheren",
"Manage users": "Gebruikers beheren",
"Media": "Media",
"Media I own": "Media die ik bezit",
"Media was edited": "Media is bewerkt",
"Members": "Leden",
"My media": "Mijn media",
"My playlists": "Mijn afspeellijsten",
"No": "Nee",
"No categories": "Geen categorieën",
"No comment yet": "Nog geen reactie",
"No comments yet": "Nog geen reacties",
"No existing": "Geen bestaande",
"No playlists available": "Geen afspeellijsten beschikbaar",
"No playlists selected": "Geen afspeellijsten geselecteerd",
"No results for": "Geen resultaten voor",
"No tags": "Geen tags",
"No users to add": "Geen gebruikers om toe te voegen",
"PLAYLISTS": "AFSPEELLIJSTEN",
"PUBLISH STATE": "PUBLICATIESTATUS",
"Pdf": "PDF",
"Playlists": "Afspeellijsten",
"Plays - Least": "Afspelingen - Minst",
"Plays - Most": "Afspelingen - Meest",
"Please select a publish state": "Selecteer een publicatiestatus",
"Please select a user": "Selecteer een gebruiker",
"Powered by": "Aangedreven door",
"Private": "Privé",
"Proceed": "Doorgaan",
"Processing...": "Verwerken...",
"Public": "Openbaar",
"Publish": "Publiceren",
"Publish State": "Publicatiestatus",
"Published": "Gepubliceerd",
"Published on": "Gepubliceerd op",
"Recent uploads": "Recente uploads",
"Recommended": "Aanbevolen",
"Record Screen": "Scherm opnemen",
"Register": "Registreren",
"Remove category": "Categorie verwijderen",
"Remove from list": "Verwijderen uit lijst",
"Remove tag": "Tag verwijderen",
"Remove user": "Gebruiker verwijderen",
"SAVE": "OPSLAAN",
"SEARCH": "ZOEKEN",
"SHARE": "DELEN",
"SHOW MORE": "MEER WEERGEVEN",
"SORT BY": "SORTEER OP",
"SUBMIT": "INDIENEN",
"Search": "Zoeken",
"Search for user...": "Zoek naar gebruiker...",
"Search users to add...": "Zoek gebruikers om toe te voegen...",
"Select": "Selecteer",
"Select Owner": "Selecteer eigenaar",
"Select all": "Alles selecteren",
"Select all media": "Alle media selecteren",
"Select publish state:": "Selecteer publicatiestatus:",
"Selected": "Geselecteerd",
"Shared by me": "Gedeeld door mij",
"Shared with me": "Gedeeld met mij",
"Sign in": "Inloggen",
"Sign out": "Uitloggen",
"Sort By": "Sorteer op",
"Start Recording": "Opname starten",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Begin met het uploaden van media en het delen van uw werk. Media die u uploadt, verschijnt hier.",
"Stop Recording": "Opname stoppen",
"Submit": "Indienen",
"Subtitle was added": "Ondertitel is toegevoegd",
"Subtitles": "Ondertitels",
"Successfully Copied": "Succesvol gekopieerd",
"Successfully Disabled Download": "Download succesvol uitgeschakeld",
"Successfully Disabled comments": "Reacties succesvol uitgeschakeld",
"Successfully Enabled Download": "Download succesvol ingeschakeld",
"Successfully Enabled comments": "Reacties succesvol ingeschakeld",
"Successfully changed owner": "Eigenaar succesvol gewijzigd",
"Successfully deleted": "Succesvol verwijderd",
"Successfully updated": "Succesvol bijgewerkt",
"Successfully updated categories": "Categorieën succesvol bijgewerkt",
"Successfully updated playlist membership": "Afspeellijstlidmaatschap succesvol bijgewerkt",
"Successfully updated publish state": "Publicatiestatus succesvol bijgewerkt",
"Successfully updated tags": "Tags succesvol bijgewerkt",
"TAGS": "TAGS",
"Tag": "Tag",
"Tags": "Tags",
"Terms": "Voorwaarden",
"The intersection of categories in the selected media is shown": "De doorsnede van categorieën in de geselecteerde media wordt getoond",
"The intersection of playlists in the selected media is shown": "De doorsnede van afspeellijsten in de geselecteerde media wordt getoond",
"The intersection of tags in the selected media is shown": "De doorsnede van tags in de geselecteerde media wordt getoond",
"The intersection of users in the selected media is shown": "De doorsnede van gebruikers in de geselecteerde media wordt getoond",
"The media was deleted successfully.": "De media is succesvol verwijderd.",
"This month": "Deze maand",
"This week": "Deze week",
"This works in Chrome, Safari and Edge browsers.": "Dit werkt in Chrome, Safari en Edge browsers.",
"This year": "Dit jaar",
"To add": "Toe te voegen",
"Today": "Vandaag",
"Trim": "Bijsnijden",
"UPLOAD": "UPLOADEN",
"UPLOAD DATE": "UPLOADDATUM",
"UPLOAD MEDIA": "MEDIA UPLOADEN",
"Undo removal": "Verwijdering ongedaan maken",
"Unlisted": "Niet vermeld",
"Up Next": "Hierna",
"Up next": "Hierna",
"Upload": "Uploaden",
"Upload date (newest)": "Uploaddatum (nieuwste)",
"Upload date (oldest)": "Uploaddatum (oudste)",
"Upload date - Newest": "Uploaddatum - Nieuwste",
"Upload date - Oldest": "Uploaddatum - Oudste",
"Upload media": "Media uploaden",
"Uploads": "Uploads",
"Users": "Gebruikers",
"VIEW ALL": "BEKIJK ALLES",
"Video": "Video",
"View all": "Bekijk alles",
"View count": "Aantal weergaven",
"View media": "Media bekijken",
"Welcome": "Welkom",
"You are going to copy": "Je gaat kopiëren",
"You are going to delete": "Je gaat verwijderen",
"You are going to disable comments to": "Je gaat reacties uitschakelen voor",
"You are going to disable download for": "Je gaat download uitschakelen voor",
"You are going to enable comments to": "Je gaat reacties inschakelen voor",
"You are going to enable download for": "Je gaat download inschakelen voor",
"comment": "reactie",
"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": "is een modern, volledig uitgerust open source video- en media-CMS. Het is ontwikkeld om te voldoen aan de behoeften van moderne webplatforms voor het bekijken en delen van media",
"media in category": "media in categorie",
"media in tag": "media in tag",
"media, are you sure?": "media, weet je het zeker?",
"media.": "media.",
"or": "of",
"results for": "resultaten voor",
"selected": "geselecteerd",
"view": "bekijk",
"views": "weergaven",
"yet": "nog",
}
replacement_strings = {
"Apr": "Apr",
"Aug": "Aug",
"Dec": "Dec",
"Feb": "Feb",
"Jan": "Jan",
"Jul": "Jul",
"Jun": "Jun",
"Mar": "Mrt",
"May": "Mei",
"Nov": "Nov",
"Oct": "Okt",
"Sep": "Sep",
"day ago": "dag geleden",
"days ago": "dagen geleden",
"hour ago": "uur geleden",
"hours ago": "uren geleden",
"just now": "zojuist",
"minute ago": "minuut geleden",
"minutes ago": "minuten geleden",
"month ago": "maand geleden",
"months ago": "maanden geleden",
"second ago": "seconde geleden",
"seconds ago": "seconden geleden",
"week ago": "week geleden",
"weeks ago": "weken geleden",
"year ago": "jaar geleden",
"years ago": "jaren geleden",
}

Some files were not shown because too many files have changed in this diff Show More