mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-06 15:38:53 -05:00
Compare commits
No commits in common. "main" and "v3.5.0" have entirely different histories.
50
.github/workflows/docker-build-push.yml
vendored
50
.github/workflows/docker-build-push.yml
vendored
@ -15,18 +15,15 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Docker meta
|
||||||
uses: docker/login-action@v2.2.0
|
id: meta
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Docker meta for base image
|
|
||||||
id: meta-base
|
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
|
# List of Docker images to use as base name for tags
|
||||||
images: |
|
images: |
|
||||||
mediacms/mediacms
|
mediacms/mediacms
|
||||||
|
# Generate Docker tags based on the following events/attributes
|
||||||
|
# Set latest tag for default branch
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
@ -40,39 +37,16 @@ jobs:
|
|||||||
org.opencontainers.image.source=https://github.com/mediacms-io/mediacms
|
org.opencontainers.image.source=https://github.com/mediacms-io/mediacms
|
||||||
org.opencontainers.image.licenses=AGPL-3.0
|
org.opencontainers.image.licenses=AGPL-3.0
|
||||||
|
|
||||||
- name: Docker meta for full image
|
- name: Login to Docker Hub
|
||||||
id: meta-full
|
uses: docker/login-action@v2.2.0
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
with:
|
||||||
images: |
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
mediacms/mediacms
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
tags: |
|
|
||||||
type=raw,value=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 Full
|
|
||||||
org.opencontainers.image.description=MediaCMS is a modern, fully featured open source video and media CMS, written in Python/Django and React, featuring a REST API. This is the full version with additional dependencies.
|
|
||||||
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 full image
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
target: full
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta-full.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta-full.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
- name: Build and push base image
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
target: base
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta-base.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta-base.outputs.labels }}
|
|
||||||
|
|||||||
8
.github/workflows/python.yml
vendored
8
.github/workflows/python.yml
vendored
@ -13,10 +13,10 @@ jobs:
|
|||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: Build the Stack
|
- 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
|
- 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
|
- name: List containers
|
||||||
run: docker ps
|
run: docker ps
|
||||||
@ -26,10 +26,10 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Run Django Tests
|
- 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 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
|
# 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
|
- name: Tear down the Stack
|
||||||
run: docker compose -f docker-compose-dev.yaml down
|
run: docker-compose -f docker-compose-dev.yaml down
|
||||||
|
|||||||
18
.gitignore
vendored
18
.gitignore
vendored
@ -5,7 +5,6 @@ media_files/original/
|
|||||||
media_files/hls/
|
media_files/hls/
|
||||||
media_files/chunks/
|
media_files/chunks/
|
||||||
media_files/uploads/
|
media_files/uploads/
|
||||||
media_files/tinymce_media/
|
|
||||||
postgres_data/
|
postgres_data/
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
logs/
|
logs/
|
||||||
@ -18,20 +17,3 @@ static/rest_framework/
|
|||||||
static/drf-yasg
|
static/drf-yasg
|
||||||
cms/local_settings.py
|
cms/local_settings.py
|
||||||
deploy/docker/local_settings.py
|
deploy/docker/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
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
/templates/cms/*
|
|
||||||
/templates/*.html
|
|
||||||
*.scss
|
|
||||||
21
.prettierrc
21
.prettierrc
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
149
Dockerfile
149
Dockerfile
@ -1,113 +1,70 @@
|
|||||||
FROM python:3.13.5-slim-bookworm AS build-image
|
FROM python:3.11.4-bookworm AS compile-image
|
||||||
|
|
||||||
# Install system dependencies needed for downloading and extracting
|
SHELL ["/bin/bash", "-c"]
|
||||||
RUN apt-get update -y && \
|
|
||||||
apt-get install -y --no-install-recommends wget xz-utils unzip && \
|
# 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 && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
apt-get purge --auto-remove && \
|
apt-get purge --auto-remove && \
|
||||||
apt-get clean
|
apt-get clean
|
||||||
|
|
||||||
RUN wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
RUN wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
|
||||||
|
mkdir -p ffmpeg-tmp && \
|
||||||
RUN mkdir -p ffmpeg-tmp && \
|
|
||||||
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C 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 && \
|
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
|
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
|
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
|
EXPOSE 9000 80
|
||||||
|
|
||||||
RUN chmod +x ./deploy/docker/entrypoint.sh
|
RUN chmod +x ./deploy/docker/entrypoint.sh
|
||||||
|
|
||||||
ENTRYPOINT ["./deploy/docker/entrypoint.sh"]
|
ENTRYPOINT ["./deploy/docker/entrypoint.sh"]
|
||||||
CMD ["./deploy/docker/start.sh"]
|
|
||||||
|
|
||||||
############ FULL IMAGE ############
|
CMD ["./deploy/docker/start.sh"]
|
||||||
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
|
|
||||||
|
|||||||
73
Dockerfile-dev
Normal file
73
Dockerfile-dev
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
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"]
|
||||||
19
Makefile
19
Makefile
@ -1,19 +0,0 @@
|
|||||||
.PHONY: admin-shell build-frontend
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
48
README.md
48
README.md
@ -23,15 +23,11 @@ A demo is available at https://demo.mediacms.io
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
- **Complete control over your data**: host it yourself!
|
- **Complete control over your data**: host it yourself!
|
||||||
- **Modern technologies**: Django/Python/Celery, React.
|
|
||||||
- **Support for multiple publishing workflows**: public, private, unlisted and custom
|
- **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
|
- **Modern technologies**: Django/Python/Celery, React.
|
||||||
- **Automatic transcription**: through integration with Whisper running locally
|
|
||||||
- **Multiple media types support**: video, audio, image, pdf
|
- **Multiple media types support**: video, audio, image, pdf
|
||||||
- **Multiple media classification options**: categories, tags and custom
|
- **Multiple media classification options**: categories, tags and custom
|
||||||
- **Multiple media sharing options**: social media share, videos embed code generation
|
- **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
|
- **Easy media searching**: enriched with live search functionality
|
||||||
- **Playlists for audio and video content**: create playlists, add and reorder content
|
- **Playlists for audio and video content**: create playlists, add and reorder content
|
||||||
- **Responsive design**: including light and dark themes
|
- **Responsive design**: including light and dark themes
|
||||||
@ -39,25 +35,29 @@ A demo is available at https://demo.mediacms.io
|
|||||||
- **Configurable actions**: allow download, add comments, add likes, dislikes, report media
|
- **Configurable actions**: allow download, add comments, add likes, dislikes, report media
|
||||||
- **Configuration options**: change logos, fonts, styling, add more pages
|
- **Configuration options**: change logos, fonts, styling, add more pages
|
||||||
- **Enhanced video player**: customized video.js player with multiple resolution and playback speed options
|
- **Enhanced video player**: customized video.js player with multiple resolution and playback speed options
|
||||||
- **Multiple transcoding profiles**: sane defaults for multiple dimensions (144p, 240p, 360p, 480p, 720p, 1080p) and multiple profiles (h264, h265, vp9)
|
- **Multiple transcoding profiles**: sane defaults for multiple dimensions (240p, 360p, 480p, 720p, 1080p) and multiple profiles (h264, h265, vp9)
|
||||||
- **Adaptive video streaming**: possible through HLS protocol
|
- **Adaptive video streaming**: possible through HLS protocol
|
||||||
- **Subtitles/CC**: support for multilingual subtitle files
|
- **Subtitles/CC**: support for multilingual subtitle files
|
||||||
- **Scalable transcoding**: transcoding through priorities. Experimental support for remote workers
|
- **Scalable transcoding**: transcoding through priorities. Experimental support for remote workers
|
||||||
- **Chunked file uploads**: for pausable/resumable upload of content
|
- **Chunked file uploads**: for pausable/resumable upload of content
|
||||||
- **REST API**: Documented through Swagger
|
- **REST API**: Documented through Swagger
|
||||||
- **Translation**: Most of the CMS is translated to a number of languages
|
|
||||||
|
|
||||||
## Example cases
|
## Example cases
|
||||||
|
|
||||||
- **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.
|
- **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.
|
- **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!
|
- **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.
|
- **Personal portal.** Organize, categorize and host your content the way you prefer.
|
||||||
|
|
||||||
|
|
||||||
## Philosophy
|
## 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 believe there's a need for quality open source web applications that can be used to build community portals and support collaboration.
|
||||||
|
|
||||||
We have three goals for MediaCMS: a) deliver all functionality one would expect from a modern system, b) allow for easy installation and maintenance, c) allow easy customization and addition of features.
|
We 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,12 +71,7 @@ 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.
|
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.
|
|
||||||
|
|
||||||
[](https://elest.io/open-source/mediacms)
|
|
||||||
|
|
||||||
## Hardware considerations
|
## Hardware considerations
|
||||||
|
|
||||||
@ -84,26 +79,35 @@ 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).
|
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).
|
||||||
|
|
||||||
In order to support automatic transcriptions through Whisper, consider more CPUs.
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
Visit [Releases Page](https://github.com/mediacms-io/mediacms/releases) for detailed Changelog
|
||||||
|
|
||||||
|
|
||||||
## Installation / Maintanance
|
## 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:
|
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
|
* [Single Server](docs/admins_docs.md#2-server-installation) page
|
||||||
- [Docker Compose](docs/admins_docs.md#3-docker-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).
|
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
|
## Documentation
|
||||||
|
|
||||||
* [Users documentation](docs/user_docs.md) page
|
* [Users documentation](docs/user_docs.md) page
|
||||||
* [Administrators documentation](docs/admins_docs.md) page
|
* [Administrators documentation](docs/admins_docs.md) page
|
||||||
* [Developers documentation](docs/developers_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
|
## Technology
|
||||||
@ -112,10 +116,10 @@ This software uses the following list of awesome technologies: Python, Django, D
|
|||||||
|
|
||||||
|
|
||||||
## Who is using it
|
## Who is using it
|
||||||
- **Multiple Universities** for hosting educational videos
|
|
||||||
- **Cinemata** non-profit media, technology and culture organization - https://cinemata.org
|
- **Cinemata** non-profit media, technology and culture organization - https://cinemata.org
|
||||||
- **Critical Commons** public media archive and fair use advocacy network - https://criticalcommons.org
|
- **Critical Commons** public media archive and fair use advocacy network - https://criticalcommons.org
|
||||||
- **American Association of Gynecologic Laparoscopists** - https://surgeryu.org/
|
- **American Association of Gynecologic Laparoscopists** - https://surgeryu.aagl.org/
|
||||||
|
|
||||||
|
|
||||||
## How to contribute
|
## How to contribute
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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
|
|
||||||
@ -4,54 +4,45 @@ import os
|
|||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"admin_customizations",
|
'django.contrib.admin',
|
||||||
"django.contrib.auth",
|
'django.contrib.auth',
|
||||||
"allauth",
|
'allauth',
|
||||||
"allauth.account",
|
'allauth.account',
|
||||||
"allauth.socialaccount",
|
'allauth.socialaccount',
|
||||||
"django.contrib.contenttypes",
|
'django.contrib.contenttypes',
|
||||||
"django.contrib.sessions",
|
'django.contrib.sessions',
|
||||||
"django.contrib.messages",
|
'django.contrib.messages',
|
||||||
"django.contrib.staticfiles",
|
'django.contrib.staticfiles',
|
||||||
"jazzmin",
|
'django.contrib.sites',
|
||||||
"django.contrib.admin",
|
'rest_framework',
|
||||||
"django.contrib.sites",
|
'rest_framework.authtoken',
|
||||||
"rest_framework",
|
'imagekit',
|
||||||
"rest_framework.authtoken",
|
'files.apps.FilesConfig',
|
||||||
"imagekit",
|
'users.apps.UsersConfig',
|
||||||
"files.apps.FilesConfig",
|
'actions.apps.ActionsConfig',
|
||||||
"users.apps.UsersConfig",
|
'debug_toolbar',
|
||||||
"actions.apps.ActionsConfig",
|
'mptt',
|
||||||
"rbac.apps.RbacConfig",
|
'crispy_forms',
|
||||||
"identity_providers.apps.IdentityProvidersConfig",
|
'uploader.apps.UploaderConfig',
|
||||||
"debug_toolbar",
|
'djcelery_email',
|
||||||
"mptt",
|
'ckeditor',
|
||||||
"crispy_forms",
|
'drf_yasg',
|
||||||
"crispy_bootstrap5",
|
'corsheaders',
|
||||||
"uploader.apps.UploaderConfig",
|
|
||||||
"djcelery_email",
|
|
||||||
"drf_yasg",
|
|
||||||
"allauth.socialaccount.providers.saml",
|
|
||||||
"saml_auth.apps.SamlAuthConfig",
|
|
||||||
"corsheaders",
|
|
||||||
"tinymce",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
"django.middleware.locale.LocaleMiddleware",
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||||
"allauth.account.middleware.AccountMiddleware",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
CORS_ORIGIN_ALLOW_ALL = True
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)
|
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static/'),)
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static_collected')
|
STATIC_ROOT = None
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class ApprovalMiddleware:
|
|
||||||
def __init__(self, get_response):
|
|
||||||
self.get_response = get_response
|
|
||||||
|
|
||||||
def __call__(self, request):
|
|
||||||
if settings.USERS_NEEDS_TO_BE_APPROVED and request.user.is_authenticated and not request.user.is_superuser and not getattr(request.user, 'is_approved', False):
|
|
||||||
allowed_paths = [
|
|
||||||
reverse('approval_required'),
|
|
||||||
reverse('account_logout'),
|
|
||||||
]
|
|
||||||
if request.path not in allowed_paths:
|
|
||||||
if request.path.startswith('/api/'):
|
|
||||||
return JsonResponse({'detail': 'User account not approved.'}, status=403)
|
|
||||||
return redirect('approval_required')
|
|
||||||
|
|
||||||
response = self.get_response(request)
|
|
||||||
return response
|
|
||||||
@ -1,22 +1,14 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.exceptions import PermissionDenied
|
|
||||||
|
|
||||||
from files.methods import (
|
from files.methods import is_mediacms_editor, is_mediacms_manager
|
||||||
is_mediacms_editor,
|
|
||||||
is_mediacms_manager,
|
|
||||||
user_allowed_to_upload,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class IsAuthorizedToAdd(permissions.BasePermission):
|
class IsAuthorizedToAdd(permissions.BasePermission):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
if request.method in permissions.SAFE_METHODS:
|
if request.method in permissions.SAFE_METHODS:
|
||||||
return True
|
return True
|
||||||
if not user_allowed_to_upload(request):
|
return 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):
|
class IsAuthorizedToAddComment(permissions.BasePermission):
|
||||||
@ -63,6 +55,26 @@ class IsUserOrEditor(permissions.BasePermission):
|
|||||||
return obj.user == request.user
|
return obj.user == request.user
|
||||||
|
|
||||||
|
|
||||||
|
def user_allowed_to_upload(request):
|
||||||
|
"""Any custom logic for whether a user is allowed
|
||||||
|
to upload content lives here
|
||||||
|
"""
|
||||||
|
if request.user.is_anonymous:
|
||||||
|
return False
|
||||||
|
if request.user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if settings.CAN_ADD_MEDIA == "all":
|
||||||
|
return True
|
||||||
|
elif settings.CAN_ADD_MEDIA == "email_verified":
|
||||||
|
if request.user.email_is_verified:
|
||||||
|
return True
|
||||||
|
elif settings.CAN_ADD_MEDIA == "advancedUser":
|
||||||
|
if request.user.advancedUser:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def user_allowed_to_comment(request):
|
def user_allowed_to_comment(request):
|
||||||
"""Any custom logic for whether a user is allowed
|
"""Any custom logic for whether a user is allowed
|
||||||
to comment lives here
|
to comment lives here
|
||||||
|
|||||||
264
cms/settings.py
264
cms/settings.py
@ -1,7 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
@ -9,6 +8,7 @@ DEBUG = False
|
|||||||
# is also shown on several places as emails
|
# is also shown on several places as emails
|
||||||
PORTAL_NAME = "MediaCMS"
|
PORTAL_NAME = "MediaCMS"
|
||||||
PORTAL_DESCRIPTION = ""
|
PORTAL_DESCRIPTION = ""
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
TIME_ZONE = "Europe/London"
|
TIME_ZONE = "Europe/London"
|
||||||
|
|
||||||
# who can add media
|
# who can add media
|
||||||
@ -105,34 +105,17 @@ USE_L10N = True
|
|||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
SITE_ID = 1
|
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
|
|
||||||
# or set as empty strings
|
|
||||||
# example:
|
|
||||||
# PORTAL_LOGO_DARK_SVG = ""
|
|
||||||
# PORTAL_LOGO_LIGHT_SVG = ""
|
|
||||||
# place the files on static/images folder
|
|
||||||
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"
|
|
||||||
|
|
||||||
# paths to extra css files to be included, eg "/static/css/custom.css"
|
|
||||||
# place css inside static/css folder
|
|
||||||
EXTRA_CSS_PATHS = []
|
|
||||||
# protection agains anonymous users
|
# protection agains anonymous users
|
||||||
# per ip address limit, for actions as like/dislike/report
|
# per ip address limit, for actions as like/dislike/report
|
||||||
TIME_TO_ACTION_ANONYMOUS = 10 * 60
|
TIME_TO_ACTION_ANONYMOUS = 10 * 60
|
||||||
|
|
||||||
# django-allauth settings
|
# django-allauth settings
|
||||||
ACCOUNT_SESSION_REMEMBER = True
|
ACCOUNT_SESSION_REMEMBER = True
|
||||||
ACCOUNT_LOGIN_METHODS = {"username", "email"}
|
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
|
||||||
ACCOUNT_EMAIL_REQUIRED = True # new users need to specify email
|
ACCOUNT_EMAIL_REQUIRED = True # new users need to specify email
|
||||||
ACCOUNT_EMAIL_VERIFICATION = "optional" # 'mandatory' 'none'
|
ACCOUNT_EMAIL_VERIFICATION = "optional" # 'mandatory' 'none'
|
||||||
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||||
ACCOUNT_USERNAME_MIN_LENGTH = 4
|
ACCOUNT_USERNAME_MIN_LENGTH = "4"
|
||||||
ACCOUNT_ADAPTER = "users.adapter.MyAccountAdapter"
|
ACCOUNT_ADAPTER = "users.adapter.MyAccountAdapter"
|
||||||
ACCOUNT_SIGNUP_FORM_CLASS = "users.forms.SignupForm"
|
ACCOUNT_SIGNUP_FORM_CLASS = "users.forms.SignupForm"
|
||||||
ACCOUNT_USERNAME_VALIDATORS = "users.validators.custom_username_validators"
|
ACCOUNT_USERNAME_VALIDATORS = "users.validators.custom_username_validators"
|
||||||
@ -140,19 +123,13 @@ ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
|
|||||||
ACCOUNT_USERNAME_REQUIRED = True
|
ACCOUNT_USERNAME_REQUIRED = True
|
||||||
ACCOUNT_LOGIN_ON_PASSWORD_RESET = True
|
ACCOUNT_LOGIN_ON_PASSWORD_RESET = True
|
||||||
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 1
|
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
|
# registration won't be open, might also consider to remove links for register
|
||||||
USERS_CAN_SELF_REGISTER = True
|
USERS_CAN_SELF_REGISTER = True
|
||||||
|
|
||||||
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = ["xxx.com", "emaildomainwhatever.com"]
|
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
|
# django rest settings
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||||
@ -207,7 +184,7 @@ CHUNKIZE_VIDEO_DURATION = 60 * 5
|
|||||||
VIDEO_CHUNKS_DURATION = 60 * 4
|
VIDEO_CHUNKS_DURATION = 60 * 4
|
||||||
|
|
||||||
# always get these two, even if upscaling
|
# always get these two, even if upscaling
|
||||||
MINIMUM_RESOLUTIONS_TO_ENCODE = [144, 240]
|
MINIMUM_RESOLUTIONS_TO_ENCODE = [240, 360]
|
||||||
|
|
||||||
# default settings for notifications
|
# default settings for notifications
|
||||||
# not all of them are implemented
|
# not all of them are implemented
|
||||||
@ -247,13 +224,13 @@ POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY = ""
|
|||||||
# only in case where unlisted workflow is used and no commentary
|
# only in case where unlisted workflow is used and no commentary
|
||||||
# exists
|
# exists
|
||||||
|
|
||||||
CANNOT_ADD_MEDIA_MESSAGE = "User cannot add media, or maximum number of media uploads has been reached."
|
CANNOT_ADD_MEDIA_MESSAGE = ""
|
||||||
|
|
||||||
# mp4hls command, part of Bento4
|
# 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 = "/home/mediacms.io/mediacms/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/bin/mp4hls"
|
||||||
|
|
||||||
# highly experimental, related with remote workers
|
# highly experimental, related with remote workers
|
||||||
ADMIN_TOKEN = ""
|
ADMIN_TOKEN = "c2b8e1838b6128asd333ddc5e24"
|
||||||
# this is used by remote workers to push
|
# this is used by remote workers to push
|
||||||
# encodings once they are done
|
# encodings once they are done
|
||||||
# USE_BASIC_HTTP = True
|
# USE_BASIC_HTTP = True
|
||||||
@ -268,6 +245,35 @@ ADMIN_TOKEN = ""
|
|||||||
# uncomment the two lines related to htpasswd
|
# 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"
|
AUTH_USER_MODEL = "users.User"
|
||||||
LOGIN_REDIRECT_URL = "/"
|
LOGIN_REDIRECT_URL = "/"
|
||||||
|
|
||||||
@ -277,7 +283,7 @@ AUTHENTICATION_BACKENDS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"admin_customizations",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"allauth",
|
"allauth",
|
||||||
"allauth.account",
|
"allauth.account",
|
||||||
@ -286,8 +292,6 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"jazzmin",
|
|
||||||
"django.contrib.admin",
|
|
||||||
"django.contrib.sites",
|
"django.contrib.sites",
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"rest_framework.authtoken",
|
"rest_framework.authtoken",
|
||||||
@ -295,31 +299,24 @@ INSTALLED_APPS = [
|
|||||||
"files.apps.FilesConfig",
|
"files.apps.FilesConfig",
|
||||||
"users.apps.UsersConfig",
|
"users.apps.UsersConfig",
|
||||||
"actions.apps.ActionsConfig",
|
"actions.apps.ActionsConfig",
|
||||||
"rbac.apps.RbacConfig",
|
|
||||||
"identity_providers.apps.IdentityProvidersConfig",
|
|
||||||
"debug_toolbar",
|
"debug_toolbar",
|
||||||
"mptt",
|
"mptt",
|
||||||
"crispy_forms",
|
"crispy_forms",
|
||||||
"crispy_bootstrap5",
|
|
||||||
"uploader.apps.UploaderConfig",
|
"uploader.apps.UploaderConfig",
|
||||||
"djcelery_email",
|
"djcelery_email",
|
||||||
|
"ckeditor",
|
||||||
"drf_yasg",
|
"drf_yasg",
|
||||||
"allauth.socialaccount.providers.saml",
|
|
||||||
"saml_auth.apps.SamlAuthConfig",
|
|
||||||
"tinymce",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.locale.LocaleMiddleware",
|
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||||
"allauth.account.middleware.AccountMiddleware",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "cms.urls"
|
ROOT_URLCONF = "cms.urls"
|
||||||
@ -347,15 +344,11 @@ WSGI_APPLICATION = "cms.wsgi.application"
|
|||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
"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",
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"min_length": 7,
|
"min_length": 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -398,7 +391,16 @@ LOGGING = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
DATABASES = {"default": {"ENGINE": "django.db.backends.postgresql", "NAME": "mediacms", "HOST": "127.0.0.1", "PORT": "5432", "USER": "mediacms", "PASSWORD": "mediacms", "OPTIONS": {'pool': True}}}
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"NAME": "mediacms",
|
||||||
|
"HOST": "127.0.0.1",
|
||||||
|
"PORT": "5432",
|
||||||
|
"USER": "mediacms",
|
||||||
|
"PASSWORD": "mediacms",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
REDIS_LOCATION = "redis://127.0.0.1:6379/1"
|
REDIS_LOCATION = "redis://127.0.0.1:6379/1"
|
||||||
@ -458,138 +460,6 @@ CELERY_TASK_ALWAYS_EAGER = False
|
|||||||
if os.environ.get("TESTING"):
|
if os.environ.get("TESTING"):
|
||||||
CELERY_TASK_ALWAYS_EAGER = True
|
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"
|
|
||||||
|
|
||||||
# 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:
|
try:
|
||||||
# keep a local_settings.py file for local overrides
|
# keep a local_settings.py file for local overrides
|
||||||
@ -601,7 +471,6 @@ except ImportError:
|
|||||||
# local_settings not in use
|
# local_settings not in use
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Don't add new settings below that could be overridden in local_settings.py!!!
|
|
||||||
|
|
||||||
if "http" not in FRONTEND_HOST:
|
if "http" not in FRONTEND_HOST:
|
||||||
# FRONTEND_HOST needs a http:// preffix
|
# FRONTEND_HOST needs a http:// preffix
|
||||||
@ -612,11 +481,22 @@ if LOCAL_INSTALL:
|
|||||||
else:
|
else:
|
||||||
SSL_FRONTEND_HOST = FRONTEND_HOST
|
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]+/',
|
||||||
|
]
|
||||||
|
|
||||||
# CSRF_COOKIE_SECURE = True
|
# if True, only show original, don't perform any action on videos
|
||||||
# SESSION_COOKIE_SECURE = True
|
DO_NOT_TRANSCODE_VIDEO = False
|
||||||
|
|
||||||
PYSUBS_COMMAND = "pysubs2"
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||||
|
|
||||||
# the following is related to local development using docker
|
# the following is related to local development using docker
|
||||||
# and docker-compose-dev.yaml
|
# and docker-compose-dev.yaml
|
||||||
@ -627,17 +507,3 @@ try:
|
|||||||
from .dev_settings import * # noqa
|
from .dev_settings import * # noqa
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
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")
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
from django.conf import settings
|
|
||||||
from django.conf.urls import include
|
from django.conf.urls import include
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
@ -14,7 +13,6 @@ schema_view = get_schema_view(
|
|||||||
permission_classes=(AllowAny,),
|
permission_classes=(AllowAny,),
|
||||||
)
|
)
|
||||||
|
|
||||||
# refactor seriously
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||||
@ -26,13 +24,8 @@ urlpatterns = [
|
|||||||
re_path(r"^", include("users.urls")),
|
re_path(r"^", include("users.urls")),
|
||||||
re_path(r"^accounts/", include("allauth.urls")),
|
re_path(r"^accounts/", include("allauth.urls")),
|
||||||
re_path(r"^api-auth/", include("rest_framework.urls")),
|
re_path(r"^api-auth/", include("rest_framework.urls")),
|
||||||
path(settings.DJANGO_ADMIN_URL, admin.site.urls),
|
path("admin/", 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(?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'),
|
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('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 +0,0 @@
|
|||||||
VERSION = "7.0.1-beta.8"
|
|
||||||
5
conftest.py
Normal file
5
conftest.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from pytest_factoryboy import register
|
||||||
|
|
||||||
|
from tests.users.factories import UserFactory
|
||||||
|
|
||||||
|
register(UserFactory)
|
||||||
@ -1,75 +0,0 @@
|
|||||||
# MediaCMS: Document Changes for DEIC
|
|
||||||
|
|
||||||
## Configuration Changes
|
|
||||||
The following changes are required in `deploy/docker/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
|
|
||||||
@ -7,7 +7,6 @@ ln -sf /dev/stdout /var/log/nginx/mediacms.io.access.log && ln -sf /dev/stderr /
|
|||||||
|
|
||||||
cp /home/mediacms.io/mediacms/deploy/docker/local_settings.py /home/mediacms.io/mediacms/cms/local_settings.py
|
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}
|
mkdir -p /home/mediacms.io/mediacms/{logs,media_files/hls}
|
||||||
touch /home/mediacms.io/mediacms/logs/debug.log
|
touch /home/mediacms.io/mediacms/logs/debug.log
|
||||||
|
|
||||||
@ -30,8 +29,7 @@ fi
|
|||||||
|
|
||||||
# We should do this only for folders that have a different owner, since it is an expensive operation
|
# 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
|
# Also ignoring .git folder to fix this issue https://github.com/mediacms-io/mediacms/issues/934
|
||||||
# Exclude package-lock.json files that may not exist or be removed during frontend setup
|
find /home/mediacms.io/mediacms ! \( -path "*.git*" \) -exec chown www-data:$TARGET_GID {} +
|
||||||
find /home/mediacms.io/mediacms ! \( -path "*.git*" -o -name "package-lock.json" \) -exec chown www-data:$TARGET_GID {} + 2>/dev/null || true
|
|
||||||
|
|
||||||
chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh
|
chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,17 @@
|
|||||||
import os
|
FRONTEND_HOST = 'http://localhost'
|
||||||
|
PORTAL_NAME = 'MediaCMS'
|
||||||
FRONTEND_HOST = os.getenv('FRONTEND_HOST', 'http://localhost')
|
SECRET_KEY = 'ma!s3^b-cw!f#7s6s0m3*jx77a@riw(7701**(r=ww%w!2+yk2'
|
||||||
PORTAL_NAME = os.getenv('PORTAL_NAME', 'MediaCMS')
|
POSTGRES_HOST = 'db'
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY', 'ma!s3^b-cw!f#7s6s0m3*jx77a@riw(7701**(r=ww%w!2+yk2')
|
REDIS_LOCATION = "redis://redis:6379/1"
|
||||||
REDIS_LOCATION = os.getenv('REDIS_LOCATION', 'redis://redis:6379/1')
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
"NAME": os.getenv('POSTGRES_NAME', 'mediacms'),
|
"NAME": "mediacms",
|
||||||
"HOST": os.getenv('POSTGRES_HOST', 'db'),
|
"HOST": POSTGRES_HOST,
|
||||||
"PORT": os.getenv('POSTGRES_PORT', '5432'),
|
"PORT": "5432",
|
||||||
"USER": os.getenv('POSTGRES_USER', 'mediacms'),
|
"USER": "mediacms",
|
||||||
"PASSWORD": os.getenv('POSTGRES_PASSWORD', 'mediacms'),
|
"PASSWORD": "mediacms",
|
||||||
"OPTIONS": {'pool': True},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,4 +31,4 @@ CELERY_RESULT_BACKEND = BROKER_URL
|
|||||||
|
|
||||||
MP4HLS_COMMAND = "/home/mediacms.io/bento4/bin/mp4hls"
|
MP4HLS_COMMAND = "/home/mediacms.io/bento4/bin/mp4hls"
|
||||||
|
|
||||||
DEBUG = os.getenv('DEBUG', 'False') == 'True'
|
DEBUG = False
|
||||||
|
|||||||
@ -1,99 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE policymap [
|
|
||||||
<!ELEMENT policymap (policy)*>
|
|
||||||
<!ATTLIST policymap xmlns CDATA #FIXED ''>
|
|
||||||
<!ELEMENT policy EMPTY>
|
|
||||||
<!ATTLIST policy xmlns CDATA #FIXED '' domain NMTOKEN #REQUIRED
|
|
||||||
name NMTOKEN #IMPLIED pattern CDATA #IMPLIED rights NMTOKEN #IMPLIED
|
|
||||||
stealth NMTOKEN #IMPLIED value CDATA #IMPLIED>
|
|
||||||
]>
|
|
||||||
<!--
|
|
||||||
Configure ImageMagick policies.
|
|
||||||
|
|
||||||
Domains include system, delegate, coder, filter, path, or resource.
|
|
||||||
|
|
||||||
Rights include none, read, write, execute and all. Use | to combine them,
|
|
||||||
for example: "read | write" to permit read from, or write to, a path.
|
|
||||||
|
|
||||||
Use a glob expression as a pattern.
|
|
||||||
|
|
||||||
Suppose we do not want users to process MPEG video images:
|
|
||||||
|
|
||||||
<policy domain="delegate" rights="none" pattern="mpeg:decode" />
|
|
||||||
|
|
||||||
Here we do not want users reading images from HTTP:
|
|
||||||
|
|
||||||
<policy domain="coder" rights="none" pattern="HTTP" />
|
|
||||||
|
|
||||||
The /repository file system is restricted to read only. We use a glob
|
|
||||||
expression to match all paths that start with /repository:
|
|
||||||
|
|
||||||
<policy domain="path" rights="read" pattern="/repository/*" />
|
|
||||||
|
|
||||||
Lets prevent users from executing any image filters:
|
|
||||||
|
|
||||||
<policy domain="filter" rights="none" pattern="*" />
|
|
||||||
|
|
||||||
Any large image is cached to disk rather than memory:
|
|
||||||
|
|
||||||
<policy domain="resource" name="area" value="1GP"/>
|
|
||||||
|
|
||||||
Use the default system font unless overwridden by the application:
|
|
||||||
|
|
||||||
<policy domain="system" name="font" value="/usr/share/fonts/favorite.ttf"/>
|
|
||||||
|
|
||||||
Define arguments for the memory, map, area, width, height and disk resources
|
|
||||||
with SI prefixes (.e.g 100MB). In addition, resource policies are maximums
|
|
||||||
for each instance of ImageMagick (e.g. policy memory limit 1GB, -limit 2GB
|
|
||||||
exceeds policy maximum so memory limit is 1GB).
|
|
||||||
|
|
||||||
Rules are processed in order. Here we want to restrict ImageMagick to only
|
|
||||||
read or write a small subset of proven web-safe image types:
|
|
||||||
|
|
||||||
<policy domain="delegate" rights="none" pattern="*" />
|
|
||||||
<policy domain="filter" rights="none" pattern="*" />
|
|
||||||
<policy domain="coder" rights="none" pattern="*" />
|
|
||||||
<policy domain="coder" rights="read|write" pattern="{GIF,JPEG,PNG,WEBP}" />
|
|
||||||
-->
|
|
||||||
<policymap>
|
|
||||||
<!-- <policy domain="resource" name="temporary-path" value="/tmp"/> -->
|
|
||||||
<policy domain="resource" name="memory" value="1GiB"/>
|
|
||||||
<policy domain="resource" name="map" value="30GiB"/>
|
|
||||||
<policy domain="resource" name="width" value="16MP"/>
|
|
||||||
<policy domain="resource" name="height" value="16MP"/>
|
|
||||||
<!-- <policy domain="resource" name="list-length" value="128"/> -->
|
|
||||||
<policy domain="resource" name="area" value="40GP"/>
|
|
||||||
<policy domain="resource" name="disk" value="100GiB"/>
|
|
||||||
<!-- <policy domain="resource" name="file" value="768"/> -->
|
|
||||||
<!-- <policy domain="resource" name="thread" value="4"/> -->
|
|
||||||
<!-- <policy domain="resource" name="throttle" value="0"/> -->
|
|
||||||
<!-- <policy domain="resource" name="time" value="3600"/> -->
|
|
||||||
<!-- <policy domain="coder" rights="none" pattern="MVG" /> -->
|
|
||||||
<!-- <policy domain="module" rights="none" pattern="{PS,PDF,XPS}" /> -->
|
|
||||||
<!-- <policy domain="path" rights="none" pattern="@*" /> -->
|
|
||||||
<!-- <policy domain="cache" name="memory-map" value="anonymous"/> -->
|
|
||||||
<!-- <policy domain="cache" name="synchronize" value="True"/> -->
|
|
||||||
<!-- <policy domain="cache" name="shared-secret" value="passphrase" stealth="true"/>
|
|
||||||
<!-- <policy domain="system" name="max-memory-request" value="256MiB"/> -->
|
|
||||||
<!-- <policy domain="system" name="shred" value="2"/> -->
|
|
||||||
<!-- <policy domain="system" name="precision" value="6"/> -->
|
|
||||||
<!-- <policy domain="system" name="font" value="/path/to/font.ttf"/> -->
|
|
||||||
<!-- <policy domain="system" name="pixel-cache-memory" value="anonymous"/> -->
|
|
||||||
<!-- <policy domain="system" name="shred" value="2"/> -->
|
|
||||||
<!-- <policy domain="system" name="precision" value="6"/> -->
|
|
||||||
<!-- not needed due to the need to use explicitly by mvg: -->
|
|
||||||
<!-- <policy domain="delegate" rights="none" pattern="MVG" /> -->
|
|
||||||
<!-- use curl -->
|
|
||||||
<policy domain="delegate" rights="none" pattern="URL" />
|
|
||||||
<policy domain="delegate" rights="none" pattern="HTTPS" />
|
|
||||||
<policy domain="delegate" rights="none" pattern="HTTP" />
|
|
||||||
<!-- in order to avoid to get image with password text -->
|
|
||||||
<policy domain="path" rights="none" pattern="@*"/>
|
|
||||||
<!-- disable ghostscript format types -->
|
|
||||||
<policy domain="coder" rights="none" pattern="PS" />
|
|
||||||
<policy domain="coder" rights="none" pattern="PS2" />
|
|
||||||
<policy domain="coder" rights="none" pattern="PS3" />
|
|
||||||
<policy domain="coder" rights="none" pattern="EPS" />
|
|
||||||
<policy domain="coder" rights="none" pattern="PDF" />
|
|
||||||
<policy domain="coder" rights="none" pattern="XPS" />
|
|
||||||
</policymap>
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
#!/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!"
|
|
||||||
@ -4,24 +4,13 @@ services:
|
|||||||
migrations:
|
migrations:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./Dockerfile
|
dockerfile: ./Dockerfile-dev
|
||||||
target: base
|
|
||||||
args:
|
|
||||||
- DEVELOPMENT_MODE=True
|
|
||||||
image: mediacms/mediacms-dev:latest
|
image: mediacms/mediacms-dev:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/mediacms.io/mediacms/
|
- ./:/home/mediacms.io/mediacms/
|
||||||
command: "./deploy/docker/prestart.sh"
|
command: "python manage.py migrate"
|
||||||
environment:
|
environment:
|
||||||
DEVELOPMENT_MODE: True
|
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
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
redis:
|
redis:
|
||||||
@ -29,7 +18,7 @@ services:
|
|||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
frontend:
|
frontend:
|
||||||
image: node:20
|
image: node:14
|
||||||
volumes:
|
volumes:
|
||||||
- ${PWD}/frontend:/home/mediacms.io/mediacms/frontend/
|
- ${PWD}/frontend:/home/mediacms.io/mediacms/frontend/
|
||||||
working_dir: /home/mediacms.io/mediacms/frontend/
|
working_dir: /home/mediacms.io/mediacms/frontend/
|
||||||
@ -41,10 +30,16 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- web
|
- web
|
||||||
web:
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./Dockerfile-dev
|
||||||
image: mediacms/mediacms-dev:latest
|
image: mediacms/mediacms-dev:latest
|
||||||
command: "python manage.py runserver 0.0.0.0:80"
|
command: "python manage.py runserver 0.0.0.0:80"
|
||||||
environment:
|
environment:
|
||||||
DEVELOPMENT_MODE: True
|
DEVELOPMENT_MODE: "True"
|
||||||
|
ADMIN_USER: 'admin'
|
||||||
|
ADMIN_PASSWORD: 'admin'
|
||||||
|
ADMIN_EMAIL: 'admin@localhost'
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
volumes:
|
volumes:
|
||||||
@ -52,7 +47,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- migrations
|
- migrations
|
||||||
db:
|
db:
|
||||||
image: postgres:17.2-alpine
|
image: postgres:15.2-alpine
|
||||||
volumes:
|
volumes:
|
||||||
- ../postgres_data:/var/lib/postgresql/data/
|
- ../postgres_data:/var/lib/postgresql/data/
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
@ -68,7 +68,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- migrations
|
- migrations
|
||||||
db:
|
db:
|
||||||
image: postgres:17.2-alpine
|
image: postgres:15.2-alpine
|
||||||
volumes:
|
volumes:
|
||||||
- ../postgres_data/:/var/lib/postgresql/data/
|
- ../postgres_data/:/var/lib/postgresql/data/
|
||||||
restart: always
|
restart: always
|
||||||
@ -70,7 +70,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- migrations
|
- migrations
|
||||||
db:
|
db:
|
||||||
image: postgres:17.2-alpine
|
image: postgres:15.2-alpine
|
||||||
volumes:
|
volumes:
|
||||||
- ../postgres_data/:/var/lib/postgresql/data/
|
- ../postgres_data/:/var/lib/postgresql/data/
|
||||||
restart: always
|
restart: always
|
||||||
@ -90,7 +90,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- migrations
|
- migrations
|
||||||
db:
|
db:
|
||||||
image: postgres:17.2-alpine
|
image: postgres:15.2-alpine
|
||||||
volumes:
|
volumes:
|
||||||
- ../postgres_data:/var/lib/postgresql/data/
|
- ../postgres_data:/var/lib/postgresql/data/
|
||||||
restart: always
|
restart: always
|
||||||
@ -66,7 +66,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- migrations
|
- migrations
|
||||||
db:
|
db:
|
||||||
image: postgres:17.2-alpine
|
image: postgres:15.2-alpine
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data/
|
- postgres_data:/var/lib/postgresql/data/
|
||||||
restart: always
|
restart: always
|
||||||
@ -1,5 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
|
||||||
celery_worker:
|
|
||||||
image: mediacms/mediacms:full
|
|
||||||
@ -62,7 +62,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- migrations
|
- migrations
|
||||||
db:
|
db:
|
||||||
image: postgres:17.2-alpine
|
image: postgres:15.2-alpine
|
||||||
volumes:
|
volumes:
|
||||||
- ../postgres_data:/var/lib/postgresql/data/
|
- ../postgres_data:/var/lib/postgresql/data/
|
||||||
restart: always
|
restart: always
|
||||||
@ -72,7 +72,7 @@ services:
|
|||||||
POSTGRES_DB: mediacms
|
POSTGRES_DB: mediacms
|
||||||
TZ: Europe/London
|
TZ: Europe/London
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@ -81,6 +81,6 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli","ping"]
|
test: ["CMD", "redis-cli","ping"]
|
||||||
interval: 10s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@ -1,124 +0,0 @@
|
|||||||
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:
|
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
- [1. Welcome](#1-welcome)
|
- [1. Welcome](#1-welcome)
|
||||||
- [2. Single Server Installaton](#2-single-server-installation)
|
- [2. Server Installaton](#2-server-installation)
|
||||||
- [3. Docker Installation](#3-docker-installation)
|
- [3. Docker Installation](#3-docker-installation)
|
||||||
- [4. Docker Deployment options](#4-docker-deployment-options)
|
- [4. Docker Deployment options](#4-docker-deployment-options)
|
||||||
- [5. Configuration](#5-configuration)
|
- [5. Configuration](#5-configuration)
|
||||||
@ -20,28 +20,18 @@
|
|||||||
- [17. Cookie consent code](#17-cookie-consent-code)
|
- [17. Cookie consent code](#17-cookie-consent-code)
|
||||||
- [18. Disable encoding and show only original file](#18-disable-encoding-and-show-only-original-file)
|
- [18. Disable encoding and show only original file](#18-disable-encoding-and-show-only-original-file)
|
||||||
- [19. Rounded corners on videos](#19-rounded-corners)
|
- [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
|
## 1. Welcome
|
||||||
This page is created for MediaCMS administrators that are responsible for setting up the software, maintaining it and making modifications.
|
This page is created for MediaCMS administrators that are responsible for setting up the software, maintaining it and making modifications.
|
||||||
|
|
||||||
## 2. Single Server Installation
|
## 2. Server Installation
|
||||||
|
|
||||||
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.
|
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).
|
||||||
|
|
||||||
Installation on an Ubuntu 22/24 system with git utility installed should be completed in a few minutes with the following steps.
|
Installation on an Ubuntu 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.
|
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
|
```bash
|
||||||
mkdir /home/mediacms.io && cd /home/mediacms.io/
|
mkdir /home/mediacms.io && cd /home/mediacms.io/
|
||||||
@ -92,11 +82,13 @@ Database can be backed up with pg_dump and media_files on /home/mediacms.io/medi
|
|||||||
## Installation
|
## Installation
|
||||||
Install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
|
Install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
|
||||||
|
|
||||||
For Ubuntu systems this is:
|
For Ubuntu 20/22 systems this is:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
sudo sh 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
|
Then run as root
|
||||||
@ -112,7 +104,7 @@ If you want to explore more options (including setup of https with letsencrypt c
|
|||||||
Run
|
Run
|
||||||
|
|
||||||
```bash
|
```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
|
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
|
||||||
@ -125,12 +117,6 @@ 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
|
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
|
### Update
|
||||||
|
|
||||||
Get latest MediaCMS image and stop/start containers
|
Get latest MediaCMS image and stop/start containers
|
||||||
@ -138,8 +124,8 @@ Get latest MediaCMS image and stop/start containers
|
|||||||
```bash
|
```bash
|
||||||
cd /path/to/mediacms/installation
|
cd /path/to/mediacms/installation
|
||||||
docker pull mediacms/mediacms
|
docker pull mediacms/mediacms
|
||||||
docker compose down
|
docker-compose down
|
||||||
docker compose up
|
docker-compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
### Update from version 2 to version 3
|
### Update from version 2 to version 3
|
||||||
@ -177,7 +163,9 @@ By default, all these services are enabled, but in order to create a scaleable d
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
To run, update the configs above if necessary, build the image by running `docker compose build`, then run `docker compose run`
|
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`
|
||||||
|
|
||||||
|
To run, update the configs above if necessary, build the image by running `docker-compose build`, then run `docker-compose run`
|
||||||
|
|
||||||
### Simple Deployment, accessed as http://localhost
|
### Simple Deployment, accessed as http://localhost
|
||||||
|
|
||||||
@ -194,7 +182,7 @@ Edit this file and set `VIRTUAL_HOST` as my_domain.com, `LETSENCRYPT_HOST` as my
|
|||||||
|
|
||||||
Edit `deploy/docker/local_settings.py` and set https://my_domain.com as `FRONTEND_HOST`
|
Edit `deploy/docker/local_settings.py` and set https://my_domain.com as `FRONTEND_HOST`
|
||||||
|
|
||||||
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!
|
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!
|
||||||
|
|
||||||
### Advanced Deployment, accessed as http://localhost:8000
|
### Advanced Deployment, accessed as http://localhost:8000
|
||||||
|
|
||||||
@ -235,17 +223,12 @@ Single server installation: edit `cms/local_settings.py`, make a change and rest
|
|||||||
Docker Compose installation: edit `deploy/docker/local_settings.py`, make a change and restart MediaCMS containers
|
Docker Compose installation: edit `deploy/docker/local_settings.py`, make a change and restart MediaCMS containers
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#docker compose restart web celery_worker celery_beat
|
#docker-compose restart web celery_worker celery_beat
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.1 Change portal logo
|
### 5.1 Change portal logo
|
||||||
|
|
||||||
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`
|
Set a new svg file for the white theme (`static/images/logo_dark.svg`) or the dark theme (`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
|
### 5.2 Set global portal title
|
||||||
|
|
||||||
@ -370,22 +353,13 @@ ADMIN_EMAIL_LIST = ['info@mediacms.io']
|
|||||||
|
|
||||||
### 5.13 Disallow user registrations from specific domains
|
### 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 = [
|
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = [
|
||||||
'xxx.com', 'emaildomainwhatever.com']
|
'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
|
### 5.14 Require a review by MediaCMS editors/managers/admins
|
||||||
|
|
||||||
set value
|
set value
|
||||||
@ -514,30 +488,6 @@ By default `CAN_COMMENT = "all"` means that all registered users can add comment
|
|||||||
|
|
||||||
- **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.
|
- **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 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.
|
|
||||||
|
|
||||||
|
|
||||||
## 6. Manage pages
|
## 6. Manage pages
|
||||||
to be written
|
to be written
|
||||||
@ -843,198 +793,11 @@ This will disable the transcoding process and only the original file will be sho
|
|||||||
|
|
||||||
## 19. Rounded corners on videos
|
## 19. Rounded corners on videos
|
||||||
|
|
||||||
By default the video player and media items are now having rounded corners, on larger screens (not in mobile). If you don't like this change, set `USE_ROUNDED_CORNERS = False` in `local_settings.py`.
|
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:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 20. Translations
|
|
||||||
|
|
||||||
### 20.1 Set a default language
|
|
||||||
|
|
||||||
By default MediaCMS is available in a number of languages. To set the default language, edit `settings.py` and set LANGUAGE_CODE to the code of one of the languages.
|
|
||||||
|
|
||||||
### 20.2 Remove existing languages
|
|
||||||
To limit the number of languages that are shown as available, remove them from the LANGUAGES list in `settings.py` or comment them. Only what is there is shown.
|
|
||||||
|
|
||||||
### 20.3 Improve existing translation
|
|
||||||
To make improvements in existing translated content, in a language that is already translated, check the language by the code name in `files/frontend-translations/` and edit the corresponding file.
|
|
||||||
|
|
||||||
### 20.4 Add more content to existing translation
|
|
||||||
Not all text is translated, so at any time you may find strings missing that need to be added to the translation. The idea here is that
|
|
||||||
|
|
||||||
a) you made the text as translatable, in the code
|
|
||||||
b) you add the translated string
|
|
||||||
|
|
||||||
For a), you have to see if the string to be translated lives in the frontend directory (React app) or on the Django templates. There are examples for both.
|
|
||||||
1. the Django templates, which is found in templates/ dir. Have a look on `templates/cms/about.html` to see an example of how it is done
|
|
||||||
2. the frontend code (React), have a look how `translateString` is used in `frontend`
|
|
||||||
|
|
||||||
|
|
||||||
After the string is marked as translatable, add the string to `files/frontend-translations/en.py` first, and then run
|
|
||||||
|
|
||||||
```
|
```
|
||||||
python manage.py process_translations
|
frontend/src/static/css/_extra.css
|
||||||
|
frontend/src/static/js/components/list-item/Item.scss
|
||||||
|
frontend/src/static/js/components/media-page/MediaPage.scss
|
||||||
```
|
```
|
||||||
|
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`.
|
|
||||||
|
|||||||
@ -4,10 +4,10 @@ There is ongoing effort to provide a better developer experience and document it
|
|||||||
## How to develop locally with Docker
|
## 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/).
|
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
|
In a few minutes the app will be available at http://localhost . Login via admin/admin
|
||||||
@ -34,14 +34,6 @@ Check it on http://localhost:8088/
|
|||||||
### How to develop in Django
|
### How to develop in Django
|
||||||
Django starts at http://localhost and is reloading automatically. Making any change to the python code should refresh 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
|
### 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.
|
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.
|
||||||
|
|
||||||
@ -62,28 +54,7 @@ In order to make changes to React code, edit code on frontend/src and check it's
|
|||||||
### Development workflow with the frontend
|
### Development workflow with the frontend
|
||||||
1. Edit frontend/src/ files
|
1. Edit frontend/src/ files
|
||||||
2. Check changes on http://localhost:8088/
|
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/`
|
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
|
5. Restart Django - `docker-compose -f docker-compose-dev.yaml restart web` so that it uses the new static files
|
||||||
6. Commit the changes
|
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
|
|
||||||
...
|
|
||||||
```
|
|
||||||
@ -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)
|
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 build
|
||||||
docker compose -f docker-compose-dev.yaml up
|
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`:
|
An `admin` user is created during the installation process. Its attributes are defined in `docker-compose-dev.yaml`:
|
||||||
@ -65,7 +65,7 @@ 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
|
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,
|
||||||
@ -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
|
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
|
## How video is transcoded
|
||||||
@ -122,19 +122,19 @@ This instructions assume that you're using the docker installation
|
|||||||
1. start docker-compose
|
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)
|
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
|
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)
|
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
|
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
|
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 ;)
|
and of course...you are very welcome to help us increase it ;)
|
||||||
|
|||||||
@ -1,166 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@ -1,315 +0,0 @@
|
|||||||
# Integrating Microsoft Entra ID (formerly Azure AD) with MediaCMS via SAML Authentication
|
|
||||||
|
|
||||||
This guide provides step-by-step instructions on how to configure Microsoft Entra ID as a SAML Identity Provider (IdP) for MediaCMS, an open-source content management system. The goal is to enable single sign-on (SSO) authentication for users in a secure and scalable way.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
1. [Overview](#overview)
|
|
||||||
2. [Prerequisites](#prerequisites)
|
|
||||||
3. [Step 1: Configure MediaCMS for SAML](#step-1-configure-mediacms-for-saml)
|
|
||||||
4. [Step 2: Register MediaCMS as an Enterprise App in Entra ID](#step-2-register-mediacms-as-an-enterprise-app-in-entra-id)
|
|
||||||
5. [Step 3: Configure SAML Settings in Entra ID](#step-3-configure-saml-settings-in-entra-id)
|
|
||||||
6. [Step 4: Configure SAML Settings in MediaCMS](#step-4-configure-saml-settings-in-mediacms)
|
|
||||||
7. [Step 5: Allow Users or Groups to Log Into the Application](#step-5-allow-users-or-groups-to-log-into-the-application)
|
|
||||||
8. [Step 6: Test and Validate Login Flow](#step-6-test-and-validate-login-flow)
|
|
||||||
9. [Troubleshooting](#troubleshooting)
|
|
||||||
10. [Resources](#resources)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
MediaCMS supports SAML 2.0 authentication by acting as a Service Provider (SP). By integrating with Microsoft Entra ID, organizations can allow users to authenticate using their existing enterprise credentials.
|
|
||||||
|
|
||||||
In our particular deployment of MediaCMS, the application is hosted internally with no direct inbound access from the public Internet. As an internal company application, it was essential to integrate it with our existing authentication systems and provide a seamless single sign-on experience. This is where the SAML protocol shines.
|
|
||||||
|
|
||||||
One of the major advantages of SAML authentication is that all communication between the Identity Provider (IdP) — in this case, Microsoft Entra ID — and the Service Provider (SP) — MediaCMS — is brokered entirely by the end user's browser. The browser initiates the authentication flow, communicates securely with Microsoft’s login portal, receives the identity assertion, and then passes it back to the internal MediaCMS server.
|
|
||||||
|
|
||||||
This architecture enables the MediaCMS server to remain isolated from the Internet while still participating in a modern and seamless federated login experience.
|
|
||||||
|
|
||||||
Even though the deployment method outlined in this tutorial is for EntraID on an isolated MediaCMS server, the same steps and general information could be applied to another authentication SAML provider/identity provider on a non-isolated system.
|
|
||||||
|
|
||||||
> **Note**: This guide assumes you are running MediaCMS with Django backend and that the `django-allauth` library is enabled and configured.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Before beginning, ensure the following:
|
|
||||||
|
|
||||||
* You have administrator access to both MediaCMS and Microsoft Entra ID (Azure portal).
|
|
||||||
* MediaCMS is installed and accessible via HTTPS, with a valid SSL certificate.
|
|
||||||
* Your MediaCMS installation has SAML support enabled (via `django-allauth`).
|
|
||||||
* You have a dedicated domain or subdomain for MediaCMS (e.g., `https://<MyMediaCMS.MyDomain.com>`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1: Configure MediaCMS for SAML
|
|
||||||
|
|
||||||
The first step in enabling SAML authentication is to modify the `local_settings.py` (for Docker: `./deploy/docker/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 `./deploy/docker/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.*
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
# Transcoding in MediaCMS
|
|
||||||
|
|
||||||
MediaCMS uses FFmpeg for transcoding media files. Most of the transcoding settings and configurations are defined in `files/helpers.py`.
|
|
||||||
|
|
||||||
## Configuration Options
|
|
||||||
|
|
||||||
Several transcoding parameters can be customized in `cms/settings.py`:
|
|
||||||
|
|
||||||
### FFmpeg Preset
|
|
||||||
|
|
||||||
The default FFmpeg preset is set to "medium". This setting controls the encoding speed and compression efficiency trade-off.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# ffmpeg options
|
|
||||||
FFMPEG_DEFAULT_PRESET = "medium" # see https://trac.ffmpeg.org/wiki/Encode/H.264
|
|
||||||
```
|
|
||||||
|
|
||||||
Available presets include:
|
|
||||||
- ultrafast
|
|
||||||
- superfast
|
|
||||||
- veryfast
|
|
||||||
- faster
|
|
||||||
- fast
|
|
||||||
- medium (default)
|
|
||||||
- slow
|
|
||||||
- slower
|
|
||||||
- veryslow
|
|
||||||
|
|
||||||
Faster presets result in larger file sizes for the same quality, while slower presets provide better compression but take longer to encode.
|
|
||||||
|
|
||||||
### Other Transcoding Settings
|
|
||||||
|
|
||||||
Additional transcoding settings in `settings.py` include:
|
|
||||||
|
|
||||||
- `FFMPEG_COMMAND`: Path to the FFmpeg executable
|
|
||||||
- `FFPROBE_COMMAND`: Path to the FFprobe executable
|
|
||||||
- `DO_NOT_TRANSCODE_VIDEO`: If set to True, only the original video is shown without transcoding
|
|
||||||
- `CHUNKIZE_VIDEO_DURATION`: For videos longer than this duration (in seconds), they get split into chunks and encoded independently
|
|
||||||
- `VIDEO_CHUNKS_DURATION`: Duration of each chunk (must be smaller than CHUNKIZE_VIDEO_DURATION)
|
|
||||||
- `MINIMUM_RESOLUTIONS_TO_ENCODE`: Always encode these resolutions, even if upscaling is required
|
|
||||||
|
|
||||||
## Advanced Configuration
|
|
||||||
|
|
||||||
For more advanced transcoding settings, you may need to modify the following in `files/helpers.py`:
|
|
||||||
|
|
||||||
- Video bitrates for different codecs and resolutions
|
|
||||||
- Audio encoders and bitrates
|
|
||||||
- CRF (Constant Rate Factor) values
|
|
||||||
- Keyframe settings
|
|
||||||
- Encoding parameters for different codecs (H.264, H.265, VP9)
|
|
||||||
@ -11,7 +11,6 @@
|
|||||||
- [Share media](#share-media)
|
- [Share media](#share-media)
|
||||||
- [Embed media](#embed-media)
|
- [Embed media](#embed-media)
|
||||||
- [Customize my profile options](#customize-my-profile-options)
|
- [Customize my profile options](#customize-my-profile-options)
|
||||||
- [Trim videos](#trim-videos)
|
|
||||||
|
|
||||||
## Uploading media
|
## Uploading media
|
||||||
|
|
||||||
@ -258,7 +257,3 @@ How to use the embed media option
|
|||||||
|
|
||||||
## Customize my profile options
|
## Customize my profile options
|
||||||
Customize profile and channel
|
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.
|
|
||||||
|
|
||||||
|
|||||||
171
files/admin.py
171
files/admin.py
@ -1,11 +1,4 @@
|
|||||||
from django import forms
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import admin
|
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 (
|
from .models import (
|
||||||
Category,
|
Category,
|
||||||
@ -14,12 +7,8 @@ from .models import (
|
|||||||
Encoding,
|
Encoding,
|
||||||
Language,
|
Language,
|
||||||
Media,
|
Media,
|
||||||
Page,
|
|
||||||
Subtitle,
|
Subtitle,
|
||||||
Tag,
|
Tag,
|
||||||
TinyMCEMedia,
|
|
||||||
TranscriptionRequest,
|
|
||||||
VideoTrimRequest,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -60,126 +49,12 @@ class MediaAdmin(admin.ModelAdmin):
|
|||||||
get_comments_count.short_description = "Comments count"
|
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):
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
form = CategoryAdminForm
|
search_fields = ["title"]
|
||||||
|
list_display = ["title", "user", "add_date", "is_global", "media_count"]
|
||||||
search_fields = ["title", "uid"]
|
list_filter = ["is_global"]
|
||||||
list_display = ["title", "user", "add_date", "media_count"]
|
|
||||||
list_filter = []
|
|
||||||
ordering = ("-add_date",)
|
ordering = ("-add_date",)
|
||||||
readonly_fields = ("user", "media_count")
|
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):
|
class TagAdmin(admin.ModelAdmin):
|
||||||
@ -204,10 +79,6 @@ class SubtitleAdmin(admin.ModelAdmin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class VideoTrimRequestAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EncodingAdmin(admin.ModelAdmin):
|
class EncodingAdmin(admin.ModelAdmin):
|
||||||
list_display = ["get_title", "chunk", "profile", "progress", "status", "has_file"]
|
list_display = ["get_title", "chunk", "profile", "progress", "status", "has_file"]
|
||||||
list_filter = ["chunk", "profile", "status"]
|
list_filter = ["chunk", "profile", "status"]
|
||||||
@ -223,47 +94,11 @@ class EncodingAdmin(admin.ModelAdmin):
|
|||||||
has_file.short_description = "Has file"
|
has_file.short_description = "Has file"
|
||||||
|
|
||||||
|
|
||||||
class TranscriptionRequestAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
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(EncodeProfile, EncodeProfileAdmin)
|
||||||
admin.site.register(Comment, CommentAdmin)
|
admin.site.register(Comment, CommentAdmin)
|
||||||
admin.site.register(Media, MediaAdmin)
|
admin.site.register(Media, MediaAdmin)
|
||||||
admin.site.register(Encoding, EncodingAdmin)
|
admin.site.register(Encoding, EncodingAdmin)
|
||||||
admin.site.register(Category, CategoryAdmin)
|
admin.site.register(Category, CategoryAdmin)
|
||||||
admin.site.register(Page, PageAdmin)
|
|
||||||
admin.site.register(Tag, TagAdmin)
|
admin.site.register(Tag, TagAdmin)
|
||||||
admin.site.register(Subtitle, SubtitleAdmin)
|
admin.site.register(Subtitle, SubtitleAdmin)
|
||||||
admin.site.register(Language, LanguageAdmin)
|
admin.site.register(Language, LanguageAdmin)
|
||||||
admin.site.register(VideoTrimRequest, VideoTrimRequestAdmin)
|
|
||||||
admin.site.register(TranscriptionRequest, TranscriptionRequestAdmin)
|
|
||||||
|
|
||||||
Media._meta.app_config.verbose_name = "Media"
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ class VideoEncodingError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
RE_TIMECODE = re.compile(r"time=(\d+:\d+:\d+.\d+)")
|
RE_TIMECODE = re.compile(r"time=(\d+:\d+:\d+.\d+)")
|
||||||
console_encoding = locale.getlocale()[1] or "UTF-8"
|
console_encoding = locale.getdefaultlocale()[1] or "UTF-8"
|
||||||
|
|
||||||
|
|
||||||
class FFmpegBackend(object):
|
class FFmpegBackend(object):
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
from django.conf import settings
|
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
|
from .methods import is_mediacms_editor, is_mediacms_manager
|
||||||
|
|
||||||
|
|
||||||
@ -12,12 +9,6 @@ def stuff(request):
|
|||||||
ret["FRONTEND_HOST"] = request.build_absolute_uri('/').rstrip('/')
|
ret["FRONTEND_HOST"] = request.build_absolute_uri('/').rstrip('/')
|
||||||
ret["DEFAULT_THEME"] = settings.DEFAULT_THEME
|
ret["DEFAULT_THEME"] = settings.DEFAULT_THEME
|
||||||
ret["PORTAL_NAME"] = settings.PORTAL_NAME
|
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["PORTAL_DESCRIPTION"] = settings.PORTAL_DESCRIPTION
|
||||||
ret["LOAD_FROM_CDN"] = settings.LOAD_FROM_CDN
|
ret["LOAD_FROM_CDN"] = settings.LOAD_FROM_CDN
|
||||||
ret["CAN_LOGIN"] = settings.LOGIN_ALLOWED
|
ret["CAN_LOGIN"] = settings.LOGIN_ALLOWED
|
||||||
@ -32,34 +23,12 @@ def stuff(request):
|
|||||||
ret["UPLOAD_MAX_SIZE"] = settings.UPLOAD_MAX_SIZE
|
ret["UPLOAD_MAX_SIZE"] = settings.UPLOAD_MAX_SIZE
|
||||||
ret["UPLOAD_MAX_FILES_NUMBER"] = settings.UPLOAD_MAX_FILES_NUMBER
|
ret["UPLOAD_MAX_FILES_NUMBER"] = settings.UPLOAD_MAX_FILES_NUMBER
|
||||||
ret["PRE_UPLOAD_MEDIA_MESSAGE"] = settings.PRE_UPLOAD_MEDIA_MESSAGE
|
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["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_ADMIN"] = request.user.is_superuser
|
||||||
ret["IS_MEDIACMS_EDITOR"] = is_mediacms_editor(request.user)
|
ret["IS_MEDIACMS_EDITOR"] = is_mediacms_editor(request.user)
|
||||||
ret["IS_MEDIACMS_MANAGER"] = is_mediacms_manager(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"] = settings.ALLOW_RATINGS
|
||||||
ret["ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY"] = settings.ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY
|
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["VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE"] = settings.VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE
|
||||||
ret["RSS_URL"] = "/rss"
|
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["VERSION"] = VERSION
|
|
||||||
|
|
||||||
if request.user.is_superuser:
|
|
||||||
ret["DJANGO_ADMIN_URL"] = settings.DJANGO_ADMIN_URL
|
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@ -83,7 +83,7 @@ class IndexRSSFeed(Feed):
|
|||||||
return item.edit_date
|
return item.edit_date
|
||||||
|
|
||||||
def item_link(self, item):
|
def item_link(self, item):
|
||||||
return f"{reverse('get_media')}?m={item.friendly_token}"
|
return reverse("get_media") + "?m={0}".format(item.friendly_token)
|
||||||
|
|
||||||
def item_extra_kwargs(self, item):
|
def item_extra_kwargs(self, item):
|
||||||
item = {
|
item = {
|
||||||
@ -151,7 +151,7 @@ class SearchRSSFeed(Feed):
|
|||||||
return item.edit_date
|
return item.edit_date
|
||||||
|
|
||||||
def item_link(self, item):
|
def item_link(self, item):
|
||||||
return f"{reverse('get_media')}?m={item.friendly_token}"
|
return reverse("get_media") + "?m={0}".format(item.friendly_token)
|
||||||
|
|
||||||
def item_extra_kwargs(self, item):
|
def item_extra_kwargs(self, item):
|
||||||
item = {
|
item = {
|
||||||
|
|||||||
275
files/forms.py
275
files/forms.py
@ -1,104 +1,48 @@
|
|||||||
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 import forms
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from .methods import get_next_state, is_mediacms_editor
|
from .methods import get_next_state, is_mediacms_editor
|
||||||
from .models import MEDIA_STATES, Category, Media, Subtitle
|
from .models import Media, Subtitle
|
||||||
|
|
||||||
|
|
||||||
class CustomField(Field):
|
|
||||||
template = 'cms/crispy_custom_field.html'
|
|
||||||
|
|
||||||
|
|
||||||
class MultipleSelect(forms.CheckboxSelectMultiple):
|
class MultipleSelect(forms.CheckboxSelectMultiple):
|
||||||
input_type = "checkbox"
|
input_type = "checkbox"
|
||||||
|
|
||||||
|
|
||||||
class MediaMetadataForm(forms.ModelForm):
|
class MediaForm(forms.ModelForm):
|
||||||
new_tags = forms.CharField(label="Tags", help_text="a comma separated list of tags.", required=False)
|
new_tags = forms.CharField(label="Tags", help_text="a comma separated list of new tags.", required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Media
|
model = Media
|
||||||
fields = (
|
fields = (
|
||||||
"friendly_token",
|
|
||||||
"title",
|
"title",
|
||||||
|
"category",
|
||||||
"new_tags",
|
"new_tags",
|
||||||
"add_date",
|
"add_date",
|
||||||
"uploaded_poster",
|
"uploaded_poster",
|
||||||
"description",
|
"description",
|
||||||
|
"state",
|
||||||
"enable_comments",
|
"enable_comments",
|
||||||
|
"featured",
|
||||||
"thumbnail_time",
|
"thumbnail_time",
|
||||||
|
"reported_times",
|
||||||
|
"is_reviewed",
|
||||||
|
"allow_download",
|
||||||
)
|
)
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
"new_tags": MultipleSelect(),
|
"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):
|
def __init__(self, user, *args, **kwargs):
|
||||||
self.user = user
|
self.user = user
|
||||||
super(MediaMetadataForm, self).__init__(*args, **kwargs)
|
super(MediaForm, self).__init__(*args, **kwargs)
|
||||||
if not getattr(settings, 'ALLOW_CUSTOM_MEDIA_URLS', False):
|
|
||||||
self.fields.pop("friendly_token")
|
|
||||||
if self.instance.media_type != "video":
|
if self.instance.media_type != "video":
|
||||||
self.fields.pop("thumbnail_time")
|
self.fields.pop("thumbnail_time")
|
||||||
if self.instance.media_type == "image":
|
if not is_mediacms_editor(user):
|
||||||
self.fields.pop("uploaded_poster")
|
self.fields.pop("featured")
|
||||||
|
self.fields.pop("reported_times")
|
||||||
|
self.fields.pop("is_reviewed")
|
||||||
self.fields["new_tags"].initial = ", ".join([tag.title for tag in self.instance.tags.all()])
|
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):
|
def clean_uploaded_poster(self):
|
||||||
image = self.cleaned_data.get("uploaded_poster", False)
|
image = self.cleaned_data.get("uploaded_poster", False)
|
||||||
if image:
|
if image:
|
||||||
@ -106,218 +50,31 @@ class MediaMetadataForm(forms.ModelForm):
|
|||||||
raise forms.ValidationError("Image file too large ( > 5mb )")
|
raise forms.ValidationError("Image file too large ( > 5mb )")
|
||||||
return image
|
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 getattr(settings, 'USE_RBAC', False) and 'category' in self.fields:
|
|
||||||
rbac_categories = categories.filter(is_rbac_category=True).values_list('title', flat=True)
|
|
||||||
|
|
||||||
if rbac_categories and state in ['private', 'unlisted']:
|
|
||||||
# Make the confirm_state field visible and add it to the layout
|
|
||||||
self.fields['confirm_state'].widget = forms.CheckboxInput()
|
|
||||||
|
|
||||||
# add it after the state field
|
|
||||||
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'):
|
|
||||||
error_message = f"I understand that although media state is {state}, the media is also shared with users that have access to the following categories: {', '.join(rbac_categories)}"
|
|
||||||
self.add_error('confirm_state', error_message)
|
|
||||||
|
|
||||||
return cleaned_data
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
data = self.cleaned_data
|
data = self.cleaned_data
|
||||||
state = data.get("state")
|
state = data.get("state")
|
||||||
if state != self.initial["state"]:
|
if state != self.initial["state"]:
|
||||||
self.instance.state = get_next_state(self.user, self.initial["state"], self.instance.state)
|
self.instance.state = get_next_state(self.user, self.initial["state"], self.instance.state)
|
||||||
|
|
||||||
media = super(MediaPublishForm, self).save(*args, **kwargs)
|
media = super(MediaForm, self).save(*args, **kwargs)
|
||||||
|
|
||||||
return media
|
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 SubtitleForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Subtitle
|
model = Subtitle
|
||||||
fields = ["language", "subtitle_file"]
|
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):
|
def __init__(self, media_item, *args, **kwargs):
|
||||||
super(SubtitleForm, self).__init__(*args, **kwargs)
|
super(SubtitleForm, self).__init__(*args, **kwargs)
|
||||||
self.instance.media = media_item
|
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):
|
def save(self, *args, **kwargs):
|
||||||
self.instance.user = self.instance.media.user
|
self.instance.user = self.instance.media.user
|
||||||
media = super(SubtitleForm, self).save(*args, **kwargs)
|
media = super(SubtitleForm, self).save(*args, **kwargs)
|
||||||
return media
|
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):
|
class ContactForm(forms.Form):
|
||||||
from_email = forms.EmailField(required=True)
|
from_email = forms.EmailField(required=True)
|
||||||
name = forms.CharField(required=False)
|
name = forms.CharField(required=False)
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
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)
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "حول",
|
|
||||||
"AUTOPLAY": "تشغيل تلقائي",
|
|
||||||
"About": "حول",
|
|
||||||
"Add a ": "أضف ",
|
|
||||||
"Browse your files": "تصفح ملفاتك",
|
|
||||||
"COMMENT": "تعليق",
|
|
||||||
"Categories": "الفئات",
|
|
||||||
"Category": "الفئة",
|
|
||||||
"Change Language": "تغيير اللغة",
|
|
||||||
"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.": "انقر على 'بدء التسجيل' واختر الشاشة أو علامة التبويب المراد تسجيلها. بمجرد الانتهاء من التسجيل، انقر على 'إيقاف التسجيل'، وسيتم تحميل التسجيل.",
|
|
||||||
"Comment": "تعليق",
|
|
||||||
"Comments": "تعليقات",
|
|
||||||
"Comments are disabled": "التعليقات معطلة",
|
|
||||||
"Contact": "اتصل",
|
|
||||||
"DELETE MEDIA": "حذف الوسائط",
|
|
||||||
"DOWNLOAD": "تحميل",
|
|
||||||
"Drag and drop files": "سحب وإفلات الملفات",
|
|
||||||
"EDIT MEDIA": "تعديل الوسائط",
|
|
||||||
"EDIT PROFILE": "تعديل الملف الشخصي",
|
|
||||||
"EDIT SUBTITLE": "تعديل الترجمة",
|
|
||||||
"Edit media": "تعديل الوسائط",
|
|
||||||
"Edit profile": "تعديل الملف الشخصي",
|
|
||||||
"Edit subtitle": "تعديل الترجمة",
|
|
||||||
"Featured": "مميز",
|
|
||||||
"Go": "اذهب",
|
|
||||||
"History": "التاريخ",
|
|
||||||
"Home": "الرئيسية",
|
|
||||||
"Language": "اللغة",
|
|
||||||
"Latest": "الأحدث",
|
|
||||||
"Liked media": "الوسائط المفضلة",
|
|
||||||
"Manage comments": "إدارة التعليقات",
|
|
||||||
"Manage media": "إدارة الوسائط",
|
|
||||||
"Manage users": "إدارة المستخدمين",
|
|
||||||
"Media": "وسائط",
|
|
||||||
"Media was edited": "تم تعديل الوسائط",
|
|
||||||
"Members": "الأعضاء",
|
|
||||||
"My media": "وسائطي",
|
|
||||||
"My playlists": "قوائم التشغيل الخاصة بي",
|
|
||||||
"No": "لا",
|
|
||||||
"No comment yet": "لا يوجد تعليق بعد",
|
|
||||||
"No comments yet": "لا توجد تعليقات بعد",
|
|
||||||
"No results for": "لا توجد نتائج لـ",
|
|
||||||
"PLAYLISTS": "قوائم التشغيل",
|
|
||||||
"Playlists": "قوائم التشغيل",
|
|
||||||
"Powered by": "مدعوم من",
|
|
||||||
"Publish": "نشر",
|
|
||||||
"Published on": "نشر في",
|
|
||||||
"Recommended": "موصى به",
|
|
||||||
"Record Screen": "تسجيل الشاشة",
|
|
||||||
"Register": "تسجيل",
|
|
||||||
"SAVE": "حفظ",
|
|
||||||
"SEARCH": "بحث",
|
|
||||||
"SHARE": "مشاركة",
|
|
||||||
"SHOW MORE": "عرض المزيد",
|
|
||||||
"SUBMIT": "إرسال",
|
|
||||||
"Search": "بحث",
|
|
||||||
"Select": "اختر",
|
|
||||||
"Sign in": "تسجيل الدخول",
|
|
||||||
"Sign out": "تسجيل الخروج",
|
|
||||||
"Start Recording": "بدء التسجيل",
|
|
||||||
"Stop Recording": "إيقاف التسجيل",
|
|
||||||
"Subtitle was added": "تمت إضافة الترجمة",
|
|
||||||
"Subtitles": "ترجمات",
|
|
||||||
"Tags": "العلامات",
|
|
||||||
"Terms": "الشروط",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "هذا يعمل في متصفحات Chrome و Safari و Edge.",
|
|
||||||
"Trim": "قص",
|
|
||||||
"UPLOAD": "رفع",
|
|
||||||
"Up next": "التالي",
|
|
||||||
"Upload": "رفع",
|
|
||||||
"Upload media": "رفع الوسائط",
|
|
||||||
"Uploads": "التحميلات",
|
|
||||||
"VIEW ALL": "عرض الكل",
|
|
||||||
"View all": "عرض الكل",
|
|
||||||
"View media": "عرض الوسائط",
|
|
||||||
"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": "وسائط في العلامة",
|
|
||||||
"or": "أو",
|
|
||||||
"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": "منذ سنوات",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "সম্পর্কে",
|
|
||||||
"AUTOPLAY": "স্বয়ংক্রিয় প্লে",
|
|
||||||
"About": "সম্পর্কে",
|
|
||||||
"Add a ": "যোগ করুন",
|
|
||||||
"Browse your files": "আপনার ফাইল ব্রাউজ করুন",
|
|
||||||
"COMMENT": "মন্তব্য",
|
|
||||||
"Categories": "বিভাগসমূহ",
|
|
||||||
"Category": "বিভাগ",
|
|
||||||
"Change Language": "ভাষা পরিবর্তন করুন",
|
|
||||||
"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.": "'রেকর্ডিং শুরু করুন'-এ ক্লিক করুন এবং রেকর্ড করার জন্য স্ক্রিন বা ট্যাব নির্বাচন করুন। রেকর্ডিং শেষ হলে, 'রেকর্ডিং বন্ধ করুন'-এ ক্লিক করুন এবং রেকর্ডিং আপলোড হয়ে যাবে।",
|
|
||||||
"Comment": "মন্তব্য",
|
|
||||||
"Comments": "মন্তব্যসমূহ",
|
|
||||||
"Comments are disabled": "মন্তব্য নিষ্ক্রিয় করা হয়েছে",
|
|
||||||
"Contact": "যোগাযোগ",
|
|
||||||
"DELETE MEDIA": "মিডিয়া মুছুন",
|
|
||||||
"DOWNLOAD": "ডাউনলোড",
|
|
||||||
"Drag and drop files": "ফাইল টেনে আনুন",
|
|
||||||
"EDIT MEDIA": "মিডিয়া সম্পাদনা করুন",
|
|
||||||
"EDIT PROFILE": "প্রোফাইল সম্পাদনা করুন",
|
|
||||||
"EDIT SUBTITLE": "সাবটাইটেল সম্পাদনা করুন",
|
|
||||||
"Edit media": "মিডিয়া সম্পাদনা করুন",
|
|
||||||
"Edit profile": "প্রোফাইল সম্পাদনা করুন",
|
|
||||||
"Edit subtitle": "সাবটাইটেল সম্পাদনা করুন",
|
|
||||||
"Featured": "বৈশিষ্ট্যযুক্ত",
|
|
||||||
"Go": "যাও",
|
|
||||||
"History": "ইতিহাস",
|
|
||||||
"Home": "বাড়ি",
|
|
||||||
"Language": "ভাষা",
|
|
||||||
"Latest": "সর্বশেষ",
|
|
||||||
"Liked media": "পছন্দের মিডিয়া",
|
|
||||||
"Manage comments": "মন্তব্য পরিচালনা করুন",
|
|
||||||
"Manage media": "মিডিয়া পরিচালনা করুন",
|
|
||||||
"Manage users": "ব্যবহারকারীদের পরিচালনা করুন",
|
|
||||||
"Media": "মিডিয়া",
|
|
||||||
"Media was edited": "মিডিয়া সম্পাদিত হয়েছে",
|
|
||||||
"Members": "সদস্যরা",
|
|
||||||
"My media": "আমার মিডিয়া",
|
|
||||||
"My playlists": "আমার প্লেলিস্ট",
|
|
||||||
"No": "না",
|
|
||||||
"No comment yet": "এখনও কোন মন্তব্য নেই",
|
|
||||||
"No comments yet": "এখনও কোন মন্তব্য নেই",
|
|
||||||
"No results for": "এর জন্য কোন ফলাফল নেই",
|
|
||||||
"PLAYLISTS": "প্লেলিস্ট",
|
|
||||||
"Playlists": "প্লেলিস্ট",
|
|
||||||
"Powered by": "দ্বারা চালিত",
|
|
||||||
"Publish": "প্রকাশ করুন",
|
|
||||||
"Published on": "প্রকাশিত",
|
|
||||||
"Recommended": "প্রস্তাবিত",
|
|
||||||
"Record Screen": "স্ক্রিন রেকর্ড করুন",
|
|
||||||
"Register": "নিবন্ধন করুন",
|
|
||||||
"SAVE": "সংরক্ষণ করুন",
|
|
||||||
"SEARCH": "অনুসন্ধান",
|
|
||||||
"SHARE": "শেয়ার করুন",
|
|
||||||
"SHOW MORE": "আরও দেখুন",
|
|
||||||
"SUBMIT": "জমা দিন",
|
|
||||||
"Search": "অনুসন্ধান",
|
|
||||||
"Select": "নির্বাচন করুন",
|
|
||||||
"Sign in": "সাইন ইন করুন",
|
|
||||||
"Sign out": "সাইন আউট করুন",
|
|
||||||
"Start Recording": "রেকর্ডিং শুরু করুন",
|
|
||||||
"Stop Recording": "রেকর্ডিং বন্ধ করুন",
|
|
||||||
"Subtitle was added": "সাবটাইটেল যোগ করা হয়েছে",
|
|
||||||
"Subtitles": "সাবটাইটেল",
|
|
||||||
"Tags": "ট্যাগ",
|
|
||||||
"Terms": "শর্তাবলী",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "এটি ক্রোম, সাফারি এবং এজ ব্রাউজারে কাজ করে।",
|
|
||||||
"Trim": "ছাঁটাই",
|
|
||||||
"UPLOAD": "আপলোড করুন",
|
|
||||||
"Up next": "পরবর্তী",
|
|
||||||
"Upload": "আপলোড করুন",
|
|
||||||
"Upload media": "মিডিয়া আপলোড করুন",
|
|
||||||
"Uploads": "আপলোডসমূহ",
|
|
||||||
"VIEW ALL": "সব দেখুন",
|
|
||||||
"View all": "সব দেখুন",
|
|
||||||
"View media": "মিডিয়া দেখুন",
|
|
||||||
"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": "ট্যাগে মিডিয়া",
|
|
||||||
"or": "অথবা",
|
|
||||||
"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": "বছর আগে",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "OM",
|
|
||||||
"AUTOPLAY": "Automatisk afspilning",
|
|
||||||
"About": "Om",
|
|
||||||
"Add a ": "Tilføj en ",
|
|
||||||
"Browse your files": "Gennemse dine filer",
|
|
||||||
"COMMENT": "KOMMENTAR",
|
|
||||||
"Categories": "Kategorier",
|
|
||||||
"Category": "Kategori",
|
|
||||||
"Change Language": "Skift sprog",
|
|
||||||
"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.",
|
|
||||||
"Comment": "Kommentar",
|
|
||||||
"Comments": "Kommentarer",
|
|
||||||
"Comments are disabled": "Kommentarer er slået fra",
|
|
||||||
"Contact": "Kontakt",
|
|
||||||
"DELETE MEDIA": "SLET MEDIE",
|
|
||||||
"DOWNLOAD": "HENT",
|
|
||||||
"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",
|
|
||||||
"Featured": "Fremhævede",
|
|
||||||
"Go": "Vælg",
|
|
||||||
"History": "Historik",
|
|
||||||
"Home": "Hjem",
|
|
||||||
"Language": "Sprog",
|
|
||||||
"Latest": "Nyeste",
|
|
||||||
"Liked media": "Medier du har liket",
|
|
||||||
"Manage comments": "Administrer kommentarer",
|
|
||||||
"Manage media": "Administrer medier",
|
|
||||||
"Manage users": "Administrer brugere",
|
|
||||||
"Media": "Medier",
|
|
||||||
"Media was edited": "Mediet er blevet redigeret",
|
|
||||||
"Members": "Medlemmer",
|
|
||||||
"My media": "Mine medier",
|
|
||||||
"My playlists": "Mine playlister",
|
|
||||||
"No": "Nej",
|
|
||||||
"No comment yet": "Ingen kommentar endnu",
|
|
||||||
"No comments yet": "Ingen komentarer endnu",
|
|
||||||
"No results for": "Ingen resultater for",
|
|
||||||
"PLAYLISTS": "PLAYLISTER",
|
|
||||||
"Playlists": "Playlister",
|
|
||||||
"Powered by": "Drevet af",
|
|
||||||
"Publish": "Udgiv",
|
|
||||||
"Published on": "Udgivet på",
|
|
||||||
"Recommended": "Anbefalet",
|
|
||||||
"Record Screen": "Optag skærm",
|
|
||||||
"Register": "Registrer",
|
|
||||||
"SAVE": "GEM",
|
|
||||||
"SEARCH": "SØG",
|
|
||||||
"SHARE": "DEL",
|
|
||||||
"SHOW MORE": "VIS MERE",
|
|
||||||
"SUBMIT": "INDSEND",
|
|
||||||
"Search": "Søg",
|
|
||||||
"Select": "Vælg",
|
|
||||||
"Sign in": "Log ind",
|
|
||||||
"Sign out": "Log ud",
|
|
||||||
"Start Recording": "Start optagelse",
|
|
||||||
"Stop Recording": "Stop optagelse",
|
|
||||||
"Subtitle was added": "Undertekster tilføjet",
|
|
||||||
"Subtitles": "Undertekster",
|
|
||||||
"Tags": "Tags",
|
|
||||||
"Terms": "Vilkår",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "Dette virker i Chrome, Safari og Edge browsere.",
|
|
||||||
"Trim": "Beskær",
|
|
||||||
"UPLOAD": "UPLOAD",
|
|
||||||
"Up next": "Næste",
|
|
||||||
"Upload": "Upload",
|
|
||||||
"Upload media": "Upload medie",
|
|
||||||
"Uploads": "Uploads",
|
|
||||||
"VIEW ALL": "SE ALLE",
|
|
||||||
"View all": "Se alle",
|
|
||||||
"View media": "Se medie",
|
|
||||||
"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",
|
|
||||||
"or": "eller",
|
|
||||||
"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",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "Über",
|
|
||||||
"AUTOPLAY": "Automatische Wiedergabe",
|
|
||||||
"About": "Über",
|
|
||||||
"Add a ": "Hinzufügen eines ",
|
|
||||||
"Browse your files": "Durchsuchen Sie Ihre Dateien",
|
|
||||||
"COMMENT": "KOMMENTAR",
|
|
||||||
"Categories": "Kategorien",
|
|
||||||
"Category": "Kategorie",
|
|
||||||
"Change Language": "Sprache ä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.",
|
|
||||||
"Comment": "Kommentar",
|
|
||||||
"Comments": "Kommentare",
|
|
||||||
"Comments are disabled": "Kommentare sind deaktiviert",
|
|
||||||
"Contact": "Kontakt",
|
|
||||||
"DELETE MEDIA": "MEDIEN LÖSCHEN",
|
|
||||||
"DOWNLOAD": "HERUNTERLADEN",
|
|
||||||
"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",
|
|
||||||
"Featured": "Empfohlen",
|
|
||||||
"Go": "Los",
|
|
||||||
"History": "Verlauf",
|
|
||||||
"Home": "Startseite",
|
|
||||||
"Language": "Sprache",
|
|
||||||
"Latest": "Neueste",
|
|
||||||
"Liked media": "Beliebte Medien",
|
|
||||||
"Manage comments": "Kommentare verwalten",
|
|
||||||
"Manage media": "Medien verwalten",
|
|
||||||
"Manage users": "Benutzer verwalten",
|
|
||||||
"Media": "Medien",
|
|
||||||
"Media was edited": "Medien wurden bearbeitet",
|
|
||||||
"Members": "Mitglieder",
|
|
||||||
"My media": "Meine Medien",
|
|
||||||
"My playlists": "Meine Playlists",
|
|
||||||
"No": "Nein",
|
|
||||||
"No comment yet": "Noch kein Kommentar",
|
|
||||||
"No comments yet": "Noch keine Kommentare",
|
|
||||||
"No results for": "Keine Ergebnisse für",
|
|
||||||
"PLAYLISTS": "PLAYLISTS",
|
|
||||||
"Playlists": "Playlists",
|
|
||||||
"Powered by": "Bereitgestellt von",
|
|
||||||
"Publish": "Veröffentlichen",
|
|
||||||
"Published on": "Veröffentlicht am",
|
|
||||||
"Recommended": "Empfohlen",
|
|
||||||
"Record Screen": "Bildschirm aufnehmen",
|
|
||||||
"Register": "Registrieren",
|
|
||||||
"SAVE": "SPEICHERN",
|
|
||||||
"SEARCH": "SUCHE",
|
|
||||||
"SHARE": "TEILEN",
|
|
||||||
"SHOW MORE": "MEHR ANZEIGEN",
|
|
||||||
"SUBMIT": "ABSENDEN",
|
|
||||||
"Search": "Suche",
|
|
||||||
"Select": "Auswählen",
|
|
||||||
"Sign in": "Anmelden",
|
|
||||||
"Sign out": "Abmelden",
|
|
||||||
"Start Recording": "Aufnahme starten",
|
|
||||||
"Stop Recording": "Aufnahme stoppen",
|
|
||||||
"Subtitle was added": "Untertitel wurde hinzugefügt",
|
|
||||||
"Subtitles": "Untertitel",
|
|
||||||
"Tags": "Tags",
|
|
||||||
"Terms": "Bedingungen",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "Dies funktioniert in den Browsern Chrome, Safari und Edge.",
|
|
||||||
"Trim": "Trimmen",
|
|
||||||
"UPLOAD": "HOCHLADEN",
|
|
||||||
"Up next": "Als nächstes",
|
|
||||||
"Upload": "Hochladen",
|
|
||||||
"Upload media": "Medien hochladen",
|
|
||||||
"Uploads": "Uploads",
|
|
||||||
"VIEW ALL": "ALLE ANZEIGEN",
|
|
||||||
"View all": "Alle anzeigen",
|
|
||||||
"View media": "Medien anzeigen",
|
|
||||||
"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",
|
|
||||||
"or": "oder",
|
|
||||||
"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",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "ΣΧΕΤΙΚΑ",
|
|
||||||
"AUTOPLAY": "Αυτόματη αναπαραγωγή",
|
|
||||||
"About": "Σχετικά",
|
|
||||||
"Add a ": "Προσθέστε ένα ",
|
|
||||||
"Browse your files": "Περιήγηση στα αρχεία σας",
|
|
||||||
"COMMENT": "ΣΧΟΛΙΟ",
|
|
||||||
"Categories": "Κατηγορίες",
|
|
||||||
"Category": "Κατηγορία",
|
|
||||||
"Change Language": "Αλλαγή Γλώσσας",
|
|
||||||
"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.": "Κάντε κλικ στο 'Έναρξη εγγραφής' και επιλέξτε την οθόνη ή την καρτέλα για εγγραφή. Μόλις ολοκληρωθεί η εγγραφή, κάντε κλικ στο 'Διακοπή εγγραφής' και η εγγραφή θα μεταφορτωθεί.",
|
|
||||||
"Comment": "Σχόλιο",
|
|
||||||
"Comments": "Σχόλια",
|
|
||||||
"Comments are disabled": "Τα σχόλια είναι απενεργοποιημένα",
|
|
||||||
"Contact": "Επικοινωνία",
|
|
||||||
"DELETE MEDIA": "ΔΙΑΓΡΑΦΗ ΑΡΧΕΙΟΥ",
|
|
||||||
"DOWNLOAD": "ΚΑΤΕΒΑΣΜΑ",
|
|
||||||
"Drag and drop files": "Σύρετε και αποθέστε αρχεία",
|
|
||||||
"EDIT MEDIA": "ΕΠΕΞΕΡΓΑΣΙΑ ΑΡΧΕΙΟΥ",
|
|
||||||
"EDIT PROFILE": "ΕΠΕΞΕΡΓΑΣΙΑ ΠΡΟΦΙΛ",
|
|
||||||
"EDIT SUBTITLE": "ΕΠΕΞΕΡΓΑΣΙΑ ΥΠΟΤΙΤΛΩΝ",
|
|
||||||
"Edit media": "Επεξεργασία αρχείου",
|
|
||||||
"Edit profile": "Επεξεργασία προφίλ",
|
|
||||||
"Edit subtitle": "Επεξεργασία υποτίτλων",
|
|
||||||
"Featured": "Επιλεγμένα",
|
|
||||||
"Go": "Μετάβαση",
|
|
||||||
"History": "Ιστορικό",
|
|
||||||
"Home": "Αρχική",
|
|
||||||
"Language": "Γλώσσα",
|
|
||||||
"Latest": "Πρόσφατα",
|
|
||||||
"Liked media": "Αγαπημένα αρχεία",
|
|
||||||
"Manage comments": "Διαχείριση σχολίων",
|
|
||||||
"Manage media": "Διαχείριση αρχείων",
|
|
||||||
"Manage users": "Διαχείριση χρηστών",
|
|
||||||
"Media": "Αρχεία",
|
|
||||||
"Media was edited": "Το αρχείο επεξεργάστηκε",
|
|
||||||
"Members": "Μέλη",
|
|
||||||
"My media": "Τα αρχεία μου",
|
|
||||||
"My playlists": "Οι λίστες μου",
|
|
||||||
"No": "Όχι",
|
|
||||||
"No comment yet": "Δεν υπάρχει ακόμα σχόλιο",
|
|
||||||
"No comments yet": "Δεν υπάρχουν ακόμα σχόλια",
|
|
||||||
"No results for": "Δεν υπάρχουν αποτελέσματα για",
|
|
||||||
"PLAYLISTS": "ΛΙΣΤΕΣ",
|
|
||||||
"Playlists": "Λίστες",
|
|
||||||
"Powered by": "Υποστηρίζεται από το",
|
|
||||||
"Publish": "Δημοσίευση",
|
|
||||||
"Published on": "Δημοσιεύτηκε στις",
|
|
||||||
"Recommended": "Προτεινόμενα",
|
|
||||||
"Record Screen": "Καταγραφή οθόνης",
|
|
||||||
"Register": "Εγγραφή",
|
|
||||||
"SAVE": "ΑΠΟΘΗΚΕΥΣΗ",
|
|
||||||
"SEARCH": "ΑΝΑΖΗΤΗΣΗ",
|
|
||||||
"SHARE": "ΚΟΙΝΟΠΟΙΗΣΗ",
|
|
||||||
"SHOW MORE": "ΠΕΡΙΣΣΟΤΕΡΑ",
|
|
||||||
"SUBMIT": "ΥΠΟΒΟΛΗ",
|
|
||||||
"Search": "Αναζήτηση",
|
|
||||||
"Select": "Επιλογή",
|
|
||||||
"Sign in": "Σύνδεση",
|
|
||||||
"Sign out": "Αποσύνδεση",
|
|
||||||
"Start Recording": "Έναρξη εγγραφής",
|
|
||||||
"Stop Recording": "Διακοπή εγγραφής",
|
|
||||||
"Subtitle was added": "Οι υπότιτλοι προστέθηκαν",
|
|
||||||
"Subtitles": "Υπότιτλοι",
|
|
||||||
"Tags": "Ετικέτες",
|
|
||||||
"Terms": "Όροι",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "Αυτό λειτουργεί σε προγράμματα περιήγησης Chrome, Safari και Edge.",
|
|
||||||
"Trim": "Περικοπή",
|
|
||||||
"UPLOAD": "ΑΝΕΒΑΣΜΑ",
|
|
||||||
"Up next": "Επόμενο",
|
|
||||||
"Upload": "Ανέβασμα",
|
|
||||||
"Upload media": "Ανέβασμα αρχείων",
|
|
||||||
"Uploads": "Ανεβάσματα",
|
|
||||||
"VIEW ALL": "ΔΕΣ ΤΑ ΟΛΑ",
|
|
||||||
"View all": "Δες τα όλα",
|
|
||||||
"View media": "Προβολή αρχείου",
|
|
||||||
"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": "αρχεία με ετικέτα",
|
|
||||||
"or": "ή",
|
|
||||||
"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": "χρόνια πριν",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "",
|
|
||||||
"AUTOPLAY": "",
|
|
||||||
"Add a ": "",
|
|
||||||
"Browse your files": "",
|
|
||||||
"COMMENT": "",
|
|
||||||
"Categories": "",
|
|
||||||
"Category": "",
|
|
||||||
"Change Language": "",
|
|
||||||
"Change password": "",
|
|
||||||
"About": "",
|
|
||||||
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "",
|
|
||||||
"Comment": "",
|
|
||||||
"Comments": "",
|
|
||||||
"Comments are disabled": "",
|
|
||||||
"Contact": "",
|
|
||||||
"DELETE MEDIA": "",
|
|
||||||
"Drag and drop files": "",
|
|
||||||
"DOWNLOAD": "",
|
|
||||||
"EDIT MEDIA": "",
|
|
||||||
"EDIT PROFILE": "",
|
|
||||||
"EDIT SUBTITLE": "",
|
|
||||||
"Edit media": "",
|
|
||||||
"Edit profile": "",
|
|
||||||
"Edit subtitle": "",
|
|
||||||
"Featured": "",
|
|
||||||
"Go": "",
|
|
||||||
"History": "",
|
|
||||||
"Home": "",
|
|
||||||
"Language": "",
|
|
||||||
"Latest": "",
|
|
||||||
"Liked media": "",
|
|
||||||
"Manage comments": "",
|
|
||||||
"Manage media": "",
|
|
||||||
"Manage users": "",
|
|
||||||
"Media": "",
|
|
||||||
"Media was edited": "",
|
|
||||||
"Members": "",
|
|
||||||
"My media": "",
|
|
||||||
"My playlists": "",
|
|
||||||
"No": "",
|
|
||||||
"No comment yet": "",
|
|
||||||
"No comments yet": "",
|
|
||||||
"No results for": "",
|
|
||||||
"PLAYLISTS": "",
|
|
||||||
"Playlists": "",
|
|
||||||
"Powered by": "",
|
|
||||||
"Publish": "",
|
|
||||||
"Published on": "",
|
|
||||||
"Recommended": "",
|
|
||||||
"Record Screen": "",
|
|
||||||
"Register": "",
|
|
||||||
"SAVE": "",
|
|
||||||
"SEARCH": "",
|
|
||||||
"SHARE": "",
|
|
||||||
"SHOW MORE": "",
|
|
||||||
"SUBMIT": "",
|
|
||||||
"Subtitles": "",
|
|
||||||
"Search": "",
|
|
||||||
"Select": "",
|
|
||||||
"Sign in": "",
|
|
||||||
"Sign out": "",
|
|
||||||
"Start Recording": "",
|
|
||||||
"Stop Recording": "",
|
|
||||||
"Subtitle was added": "",
|
|
||||||
"Tags": "",
|
|
||||||
"Terms": "",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "",
|
|
||||||
"Trim": "",
|
|
||||||
"UPLOAD": "",
|
|
||||||
"Up next": "",
|
|
||||||
"Upload": "",
|
|
||||||
"Upload media": "",
|
|
||||||
"Uploads": "",
|
|
||||||
"VIEW ALL": "",
|
|
||||||
"View all": "",
|
|
||||||
"View media": "",
|
|
||||||
"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": "",
|
|
||||||
"or": "",
|
|
||||||
"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": "",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "Acerca de",
|
|
||||||
"AUTOPLAY": "Reproducción automática",
|
|
||||||
"About": "Acerca de",
|
|
||||||
"Add a ": "Agregar un ",
|
|
||||||
"Browse your files": "Explorar sus archivos",
|
|
||||||
"COMMENT": "COMENTARIO",
|
|
||||||
"Categories": "Categorías",
|
|
||||||
"Category": "Categoría",
|
|
||||||
"Change Language": "Cambiar idioma",
|
|
||||||
"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á.",
|
|
||||||
"Comment": "Comentario",
|
|
||||||
"Comments": "Comentarios",
|
|
||||||
"Comments are disabled": "Los comentarios están deshabilitados",
|
|
||||||
"Contact": "Contacto",
|
|
||||||
"DELETE MEDIA": "ELIMINAR MEDIOS",
|
|
||||||
"DOWNLOAD": "DESCARGAR",
|
|
||||||
"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",
|
|
||||||
"Featured": "Destacado",
|
|
||||||
"Go": "Ir",
|
|
||||||
"History": "Historial",
|
|
||||||
"Home": "Inicio",
|
|
||||||
"Language": "Idioma",
|
|
||||||
"Latest": "Último",
|
|
||||||
"Liked media": "Medios que me gustan",
|
|
||||||
"Manage comments": "Gestionar comentarios",
|
|
||||||
"Manage media": "Gestionar medios",
|
|
||||||
"Manage users": "Gestionar usuarios",
|
|
||||||
"Media": "Medios",
|
|
||||||
"Media was edited": "El medio fue editado",
|
|
||||||
"Members": "Miembros",
|
|
||||||
"My media": "Mis medios",
|
|
||||||
"My playlists": "Mis listas de reproducción",
|
|
||||||
"No": "No",
|
|
||||||
"No comment yet": "Aún no hay comentarios",
|
|
||||||
"No comments yet": "Aún no hay comentarios",
|
|
||||||
"No results for": "No hay resultados para",
|
|
||||||
"PLAYLISTS": "LISTAS DE REPRODUCCIÓN",
|
|
||||||
"Playlists": "Listas de reproducción",
|
|
||||||
"Powered by": "Desarrollado por",
|
|
||||||
"Publish": "Publicar",
|
|
||||||
"Published on": "Publicado en",
|
|
||||||
"Recommended": "Recomendado",
|
|
||||||
"Record Screen": "Grabar pantalla",
|
|
||||||
"Register": "Registrarse",
|
|
||||||
"SAVE": "GUARDAR",
|
|
||||||
"SEARCH": "BUSCAR",
|
|
||||||
"SHARE": "COMPARTIR",
|
|
||||||
"SHOW MORE": "MOSTRAR MÁS",
|
|
||||||
"SUBMIT": "ENVIAR",
|
|
||||||
"Search": "Buscar",
|
|
||||||
"Select": "Seleccionar",
|
|
||||||
"Sign in": "Iniciar sesión",
|
|
||||||
"Sign out": "Cerrar sesión",
|
|
||||||
"Start Recording": "Iniciar grabación",
|
|
||||||
"Stop Recording": "Detener grabación",
|
|
||||||
"Subtitle was added": "El subtítulo fue agregado",
|
|
||||||
"Subtitles": "Subtítulos",
|
|
||||||
"Tags": "Etiquetas",
|
|
||||||
"Terms": "Términos",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "Esto funciona en los navegadores Chrome, Safari y Edge.",
|
|
||||||
"Trim": "Recortar",
|
|
||||||
"UPLOAD": "SUBIR",
|
|
||||||
"Up next": "A continuación",
|
|
||||||
"Upload": "Subir",
|
|
||||||
"Upload media": "Subir medios",
|
|
||||||
"Uploads": "Subidas",
|
|
||||||
"VIEW ALL": "VER TODO",
|
|
||||||
"View all": "Ver todo",
|
|
||||||
"View media": "Ver medios",
|
|
||||||
"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",
|
|
||||||
"or": "o",
|
|
||||||
"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",
|
|
||||||
}
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "À PROPOS",
|
|
||||||
"AUTOPLAY": "Lecture automatique",
|
|
||||||
"About": "À propos",
|
|
||||||
"Add a": "Ajouter un",
|
|
||||||
"Add a ": "Ajouter un ",
|
|
||||||
"Browse your files": "Parcourir vos fichiers",
|
|
||||||
"COMMENT": "COMMENTAIRE",
|
|
||||||
"Categories": "Catégories",
|
|
||||||
"Category": "Catégorie",
|
|
||||||
"Change Language": "Changer de langue",
|
|
||||||
"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é.",
|
|
||||||
"Comment": "Commentaire",
|
|
||||||
"Comments": "Commentaires",
|
|
||||||
"Comments are disabled": "Les commentaires sont désactivés",
|
|
||||||
"Contact": "Contact",
|
|
||||||
"DELETE MEDIA": "SUPPRIMER LE MÉDIA",
|
|
||||||
"DOWNLOAD": "TÉLÉCHARGER",
|
|
||||||
"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",
|
|
||||||
"Featured": "En vedette",
|
|
||||||
"Go": "Aller",
|
|
||||||
"History": "Historique",
|
|
||||||
"Home": "Accueil",
|
|
||||||
"Language": "Langue",
|
|
||||||
"Latest": "Dernier",
|
|
||||||
"Liked media": "Médias aimés",
|
|
||||||
"Manage comments": "Gérer les commentaires",
|
|
||||||
"Manage media": "Gérer les médias",
|
|
||||||
"Manage users": "Gérer les utilisateurs",
|
|
||||||
"Media": "Média",
|
|
||||||
"Media was edited": "Le média a été modifié",
|
|
||||||
"Members": "Membres",
|
|
||||||
"My media": "Mes médias",
|
|
||||||
"My playlists": "Mes playlists",
|
|
||||||
"No": "Non",
|
|
||||||
"No comment yet": "Pas encore de commentaire",
|
|
||||||
"No comments yet": "Pas encore de commentaires",
|
|
||||||
"No results for": "Aucun résultat pour",
|
|
||||||
"PLAYLISTS": "PLAYLISTS",
|
|
||||||
"Playlists": "Playlists",
|
|
||||||
"Powered by": "Propulsé par",
|
|
||||||
"Publish": "Publier",
|
|
||||||
"Published on": "Publié le",
|
|
||||||
"Recommended": "Recommandé",
|
|
||||||
"Record Screen": "Enregistrer l'écran",
|
|
||||||
"Register": "S'inscrire",
|
|
||||||
"SAVE": "ENREGISTRER",
|
|
||||||
"SEARCH": "RECHERCHER",
|
|
||||||
"SHARE": "PARTAGER",
|
|
||||||
"SHOW MORE": "MONTRER PLUS",
|
|
||||||
"SUBMIT": "SOUMETTRE",
|
|
||||||
"Search": "Rechercher",
|
|
||||||
"Select": "Sélectionner",
|
|
||||||
"Sign in": "Se connecter",
|
|
||||||
"Sign out": "Se déconnecter",
|
|
||||||
"Start Recording": "Commencer l'enregistrement",
|
|
||||||
"Stop Recording": "Arrêter l'enregistrement",
|
|
||||||
"Subtitle was added": "Le sous-titre a été ajouté",
|
|
||||||
"Subtitles": "Sous-titres",
|
|
||||||
"Tags": "Tags",
|
|
||||||
"Terms": "Conditions",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "Cela fonctionne dans les navigateurs Chrome, Safari et Edge.",
|
|
||||||
"Trim": "Couper",
|
|
||||||
"UPLOAD": "TÉLÉCHARGER",
|
|
||||||
"Up next": "À suivre",
|
|
||||||
"Upload": "Télécharger",
|
|
||||||
"Upload media": "Télécharger des médias",
|
|
||||||
"Uploads": "Téléchargements",
|
|
||||||
"VIEW ALL": "VOIR TOUT",
|
|
||||||
"View all": "Voir tout",
|
|
||||||
"View media": "Voir le média",
|
|
||||||
"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",
|
|
||||||
"or": "ou",
|
|
||||||
"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",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "על אודות",
|
|
||||||
"AUTOPLAY": "ניגון אוטומטי",
|
|
||||||
"About": "על אודות",
|
|
||||||
"Add a ": "הוסף",
|
|
||||||
"Browse your files": "עיין בקבצים שלך",
|
|
||||||
"COMMENT": "תגובה",
|
|
||||||
"Categories": "קטגוריות",
|
|
||||||
"Category": "קטגוריה",
|
|
||||||
"Change Language": "שנה שפה",
|
|
||||||
"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.": "לחץ על 'התחל הקלטה' ובחר את המסך או הכרטיסייה להקלטה. לאחר סיום ההקלטה, לחץ על 'עצור הקלטה', וההקלטה תועלה.",
|
|
||||||
"Comment": "תגובה",
|
|
||||||
"Comments": "תגובות",
|
|
||||||
"Comments are disabled": "התגובות מושבתות",
|
|
||||||
"Contact": "צור קשר",
|
|
||||||
"DELETE MEDIA": "מחק מדיה",
|
|
||||||
"DOWNLOAD": "הורד",
|
|
||||||
"Drag and drop files": "גרור ושחרר קבצים",
|
|
||||||
"EDIT MEDIA": "ערוך מדיה",
|
|
||||||
"EDIT PROFILE": "ערוך פרופיל",
|
|
||||||
"EDIT SUBTITLE": "ערוך כתוביות",
|
|
||||||
"Edit media": "ערוך מדיה",
|
|
||||||
"Edit profile": "ערוך פרופיל",
|
|
||||||
"Edit subtitle": "ערוך כתוביות",
|
|
||||||
"Featured": "מומלצים",
|
|
||||||
"Go": "בצע",
|
|
||||||
"History": "היסטוריה",
|
|
||||||
"Home": "דף הבית",
|
|
||||||
"Language": "שפה",
|
|
||||||
"Latest": "העדכונים האחרונים",
|
|
||||||
"Liked media": "מדיה שאהבתי",
|
|
||||||
"Manage comments": "ניהול תגובות",
|
|
||||||
"Manage media": "ניהול מדיה",
|
|
||||||
"Manage users": "ניהול משתמשים",
|
|
||||||
"Media": "מדיה",
|
|
||||||
"Media was edited": "המדיה נערכה",
|
|
||||||
"Members": "משתמשים",
|
|
||||||
"My media": "המדיה שלי",
|
|
||||||
"My playlists": "הפלייליסטים שלי",
|
|
||||||
"No": "לא",
|
|
||||||
"No comment yet": "עדיין אין תגובות",
|
|
||||||
"No comments yet": "עדיין אין תגובות",
|
|
||||||
"No results for": "אין תוצאות עבור",
|
|
||||||
"PLAYLISTS": "פלייליסטים",
|
|
||||||
"Playlists": "פלייליסטים",
|
|
||||||
"Powered by": "מופעל על ידי",
|
|
||||||
"Publish": "פרסם",
|
|
||||||
"Published on": "פורסם בתאריך",
|
|
||||||
"Recommended": "מומלץ",
|
|
||||||
"Record Screen": "הקלטת מסך",
|
|
||||||
"Register": "הרשמה",
|
|
||||||
"SAVE": "שמור",
|
|
||||||
"SEARCH": "חפש",
|
|
||||||
"SHARE": "שתף",
|
|
||||||
"SHOW MORE": "הצג עוד",
|
|
||||||
"SUBMIT": "שלח",
|
|
||||||
"Search": "חפש",
|
|
||||||
"Select": "בחר",
|
|
||||||
"Sign in": "התחבר",
|
|
||||||
"Sign out": "התנתק",
|
|
||||||
"Start Recording": "התחל הקלטה",
|
|
||||||
"Stop Recording": "עצור הקלטה",
|
|
||||||
"Subtitle was added": "הכתובית נוספה",
|
|
||||||
"Subtitles": "כתוביות",
|
|
||||||
"Tags": "תגיות",
|
|
||||||
"Terms": "תנאים",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "זה עובד בדפדפני Chrome, Safari ו-Edge.",
|
|
||||||
"Trim": "גזירה",
|
|
||||||
"UPLOAD": "העלה",
|
|
||||||
"Up next": "הבא בתור",
|
|
||||||
"Upload": "העלה",
|
|
||||||
"Upload media": "העלה מדיה",
|
|
||||||
"Uploads": "העלאות",
|
|
||||||
"VIEW ALL": "הצג הכל",
|
|
||||||
"View all": "הצג הכל",
|
|
||||||
"View media": "צפה במדיה",
|
|
||||||
"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": "מדיה בתגית",
|
|
||||||
"or": "או",
|
|
||||||
"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": "לפני שנים",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "के बारे में",
|
|
||||||
"AUTOPLAY": "स्वतः चलाएं",
|
|
||||||
"About": "के बारे में",
|
|
||||||
"Add a ": "जोड़ें",
|
|
||||||
"Browse your files": "अपनी फ़ाइलें ब्राउज़ करें",
|
|
||||||
"COMMENT": "टिप्पणी",
|
|
||||||
"Categories": "श्रेणियाँ",
|
|
||||||
"Category": "श्रेणी",
|
|
||||||
"Change Language": "भाषा बदलें",
|
|
||||||
"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.": "'रिकॉर्डिंग प्रारंभ करें' पर क्लिक करें और रिकॉर्ड करने के लिए स्क्रीन या टैब का चयन करें। रिकॉर्डिंग समाप्त होने के बाद, 'रिकॉर्डिंग रोकें' पर क्लिक करें, और रिकॉर्डिंग अपलोड हो जाएगी।",
|
|
||||||
"Comment": "टिप्पणी",
|
|
||||||
"Comments": "टिप्पणियाँ",
|
|
||||||
"Comments are disabled": "टिप्पणियाँ अक्षम हैं",
|
|
||||||
"Contact": "संपर्क करें",
|
|
||||||
"DELETE MEDIA": "मीडिया हटाएं",
|
|
||||||
"DOWNLOAD": "डाउनलोड करें",
|
|
||||||
"Drag and drop files": "फ़ाइलें खींचें और छोड़ें",
|
|
||||||
"EDIT MEDIA": "मीडिया संपादित करें",
|
|
||||||
"EDIT PROFILE": "प्रोफ़ाइल संपादित करें",
|
|
||||||
"EDIT SUBTITLE": "उपशीर्षक संपादित करें",
|
|
||||||
"Edit media": "मीडिया संपादित करें",
|
|
||||||
"Edit profile": "प्रोफ़ाइल संपादित करें",
|
|
||||||
"Edit subtitle": "उपशीर्षक संपादित करें",
|
|
||||||
"Featured": "विशेष रुप से प्रदर्शित",
|
|
||||||
"Go": "जाएं",
|
|
||||||
"History": "इतिहास",
|
|
||||||
"Home": "मुख्य पृष्ठ",
|
|
||||||
"Language": "भाषा",
|
|
||||||
"Latest": "नवीनतम",
|
|
||||||
"Liked media": "पसंदीदा मीडिया",
|
|
||||||
"Manage comments": "टिप्पणियाँ प्रबंधित करें",
|
|
||||||
"Manage media": "मीडिया प्रबंधित करें",
|
|
||||||
"Manage users": "उपयोगकर्ताओं को प्रबंधित करें",
|
|
||||||
"Media": "मीडिया",
|
|
||||||
"Media was edited": "मीडिया संपादित किया गया था",
|
|
||||||
"Members": "सदस्य",
|
|
||||||
"My media": "मेरा मीडिया",
|
|
||||||
"My playlists": "मेरी प्लेलिस्ट",
|
|
||||||
"No": "नहीं",
|
|
||||||
"No comment yet": "अभी तक कोई टिप्पणी नहीं",
|
|
||||||
"No comments yet": "अभी तक कोई टिप्पणियाँ नहीं",
|
|
||||||
"No results for": "के लिए कोई परिणाम नहीं",
|
|
||||||
"PLAYLISTS": "प्लेलिस्ट",
|
|
||||||
"Playlists": "प्लेलिस्ट",
|
|
||||||
"Powered by": "द्वारा संचालित",
|
|
||||||
"Publish": "प्रकाशित करें",
|
|
||||||
"Published on": "पर प्रकाशित",
|
|
||||||
"Recommended": "अनुशंसित",
|
|
||||||
"Record Screen": "स्क्रीन रिकॉर्ड करें",
|
|
||||||
"Register": "पंजीकरण करें",
|
|
||||||
"SAVE": "सहेजें",
|
|
||||||
"SEARCH": "खोजें",
|
|
||||||
"SHARE": "साझा करें",
|
|
||||||
"SHOW MORE": "और दिखाएं",
|
|
||||||
"SUBMIT": "प्रस्तुत करें",
|
|
||||||
"Search": "खोजें",
|
|
||||||
"Select": "चुनें",
|
|
||||||
"Sign in": "साइन इन करें",
|
|
||||||
"Sign out": "साइन आउट करें",
|
|
||||||
"Start Recording": "रिकॉर्डिंग प्रारंभ करें",
|
|
||||||
"Stop Recording": "रिकॉर्डिंग रोकें",
|
|
||||||
"Subtitle was added": "उपशीर्षक जोड़ा गया",
|
|
||||||
"Subtitles": "उपशीर्षक",
|
|
||||||
"Tags": "टैग",
|
|
||||||
"Terms": "शर्तें",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "यह क्रोम, सफारी और एज ब्राउज़र में काम करता है।",
|
|
||||||
"Trim": "छांटें",
|
|
||||||
"UPLOAD": "अपलोड करें",
|
|
||||||
"Up next": "अगला",
|
|
||||||
"Upload": "अपलोड करें",
|
|
||||||
"Upload media": "मीडिया अपलोड करें",
|
|
||||||
"Uploads": "अपलोड",
|
|
||||||
"VIEW ALL": "सभी देखें",
|
|
||||||
"View all": "सभी देखें",
|
|
||||||
"View media": "मीडिया देखें",
|
|
||||||
"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": "टैग में मीडिया",
|
|
||||||
"or": "या",
|
|
||||||
"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": "साल पहले",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "TENTANG",
|
|
||||||
"AUTOPLAY": "PUTAR OTOMATIS",
|
|
||||||
"About": "Tentang",
|
|
||||||
"Add a ": "Tambahkan ",
|
|
||||||
"Browse your files": "Jelajahi file Anda",
|
|
||||||
"COMMENT": "KOMENTAR",
|
|
||||||
"Categories": "Kategori",
|
|
||||||
"Category": "Kategori",
|
|
||||||
"Change Language": "Ganti Bahasa",
|
|
||||||
"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.",
|
|
||||||
"Comment": "Komentar",
|
|
||||||
"Comments": "Komentar",
|
|
||||||
"Comments are disabled": "Komentar dinonaktifkan",
|
|
||||||
"Contact": "Kontak",
|
|
||||||
"DELETE MEDIA": "HAPUS MEDIA",
|
|
||||||
"DOWNLOAD": "UNDUH",
|
|
||||||
"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",
|
|
||||||
"Featured": "Unggulan",
|
|
||||||
"Go": "Pergi",
|
|
||||||
"History": "Riwayat",
|
|
||||||
"Home": "Beranda",
|
|
||||||
"Language": "Bahasa",
|
|
||||||
"Latest": "Terbaru",
|
|
||||||
"Liked media": "Media yang disukai",
|
|
||||||
"Manage comments": "Kelola komentar",
|
|
||||||
"Manage media": "Kelola media",
|
|
||||||
"Manage users": "Kelola pengguna",
|
|
||||||
"Media": "Media",
|
|
||||||
"Media was edited": "Media telah diedit",
|
|
||||||
"Members": "Anggota",
|
|
||||||
"My media": "Media saya",
|
|
||||||
"My playlists": "Daftar putar saya",
|
|
||||||
"No": "Tidak",
|
|
||||||
"No comment yet": "Belum ada komentar",
|
|
||||||
"No comments yet": "Belum ada komentar",
|
|
||||||
"No results for": "Tidak ada hasil untuk",
|
|
||||||
"PLAYLISTS": "DAFTAR PUTAR",
|
|
||||||
"Playlists": "Daftar putar",
|
|
||||||
"Powered by": "Didukung oleh",
|
|
||||||
"Publish": "Terbitkan",
|
|
||||||
"Published on": "Diterbitkan pada",
|
|
||||||
"Recommended": "Direkomendasikan",
|
|
||||||
"Record Screen": "Rekam Layar",
|
|
||||||
"Register": "Daftar",
|
|
||||||
"SAVE": "SIMPAN",
|
|
||||||
"SEARCH": "CARI",
|
|
||||||
"SHARE": "BAGIKAN",
|
|
||||||
"SHOW MORE": "TAMPILKAN LEBIH BANYAK",
|
|
||||||
"SUBMIT": "KIRIM",
|
|
||||||
"Search": "Cari",
|
|
||||||
"Select": "Pilih",
|
|
||||||
"Sign in": "Masuk",
|
|
||||||
"Sign out": "Keluar",
|
|
||||||
"Start Recording": "Mulai Merekam",
|
|
||||||
"Stop Recording": "Hentikan Perekaman",
|
|
||||||
"Subtitle was added": "Subtitle telah ditambahkan",
|
|
||||||
"Subtitles": "Subtitel",
|
|
||||||
"Tags": "Tag",
|
|
||||||
"Terms": "Ketentuan",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "Ini berfungsi di browser Chrome, Safari, dan Edge.",
|
|
||||||
"Trim": "Potong",
|
|
||||||
"UPLOAD": "UNGGAH",
|
|
||||||
"Up next": "Selanjutnya",
|
|
||||||
"Upload": "Unggah",
|
|
||||||
"Upload media": "Unggah media",
|
|
||||||
"Uploads": "Unggahan",
|
|
||||||
"VIEW ALL": "LIHAT SEMUA",
|
|
||||||
"View all": "Lihat semua",
|
|
||||||
"View media": "Lihat media",
|
|
||||||
"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",
|
|
||||||
"or": "atau",
|
|
||||||
"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",
|
|
||||||
}
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "SU DI NOI",
|
|
||||||
"AUTOPLAY": "RIPRODUZIONE AUTOMATICA",
|
|
||||||
"About": "Su di noi",
|
|
||||||
"Add a": "Aggiungi un",
|
|
||||||
"Add a ": "Aggiungi un ",
|
|
||||||
"Browse your files": "Sfoglia i tuoi file",
|
|
||||||
"COMMENT": "COMMENTA",
|
|
||||||
"Categories": "Categorie",
|
|
||||||
"Category": "Categoria",
|
|
||||||
"Change Language": "Cambia lingua",
|
|
||||||
"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.",
|
|
||||||
"Comment": "Commento",
|
|
||||||
"Comments": "Commenti",
|
|
||||||
"Comments are disabled": "I commenti sono disabilitati",
|
|
||||||
"Contact": "Contatti",
|
|
||||||
"DELETE MEDIA": "ELIMINA MEDIA",
|
|
||||||
"DOWNLOAD": "SCARICA",
|
|
||||||
"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",
|
|
||||||
"Featured": "In evidenza",
|
|
||||||
"Go": "Vai",
|
|
||||||
"History": "Cronologia",
|
|
||||||
"Home": "Home",
|
|
||||||
"Language": "Lingua",
|
|
||||||
"Latest": "Ultimi",
|
|
||||||
"Liked media": "Piaciuti",
|
|
||||||
"Manage comments": "Gestisci i commenti",
|
|
||||||
"Manage media": "Gestisci i media",
|
|
||||||
"Manage users": "Gestisci gli utenti",
|
|
||||||
"Media": "Media",
|
|
||||||
"Media was edited": "Il media è stato modificato",
|
|
||||||
"Members": "Membri",
|
|
||||||
"My media": "I miei media",
|
|
||||||
"My playlists": "Le mie playlist",
|
|
||||||
"No": "No",
|
|
||||||
"No comment yet": "Ancora nessun commento",
|
|
||||||
"No comments yet": "Ancora nessun commento",
|
|
||||||
"No results for": "Nessun risultato per",
|
|
||||||
"PLAYLISTS": "PLAYLIST",
|
|
||||||
"Playlists": "Playlist",
|
|
||||||
"Powered by": "Powered by",
|
|
||||||
"Publish": "Pubblica",
|
|
||||||
"Published on": "Pubblicato il",
|
|
||||||
"Recommended": "Raccomandati",
|
|
||||||
"Record Screen": "Registra schermo",
|
|
||||||
"Register": "Registrati",
|
|
||||||
"SAVE": "SALVA",
|
|
||||||
"SEARCH": "CERCA",
|
|
||||||
"SHARE": "CONDIVIDI",
|
|
||||||
"SHOW MORE": "MOSTRA DI PIÙ",
|
|
||||||
"SUBMIT": "INVIA",
|
|
||||||
"Search": "Cerca",
|
|
||||||
"Select": "Seleziona",
|
|
||||||
"Sign in": "Login",
|
|
||||||
"Sign out": "Logout",
|
|
||||||
"Start Recording": "Inizia registrazione",
|
|
||||||
"Stop Recording": "Interrompi registrazione",
|
|
||||||
"Subtitle was added": "I sottotitoli sono stati aggiunti",
|
|
||||||
"Subtitles": "Sottotitoli",
|
|
||||||
"Tags": "Tag",
|
|
||||||
"Terms": "Termini e condizioni",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "Questo funziona nei browser Chrome, Safari e Edge.",
|
|
||||||
"Trim": "Taglia",
|
|
||||||
"UPLOAD": "CARICA",
|
|
||||||
"Up next": "A seguire",
|
|
||||||
"Upload": "Carica",
|
|
||||||
"Upload media": "Carica i media",
|
|
||||||
"Uploads": "Caricamenti",
|
|
||||||
"VIEW ALL": "MOSTRA TUTTI",
|
|
||||||
"View all": "Mostra tutti",
|
|
||||||
"View media": "Visualizza media",
|
|
||||||
"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",
|
|
||||||
"or": "o",
|
|
||||||
"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",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "約",
|
|
||||||
"AUTOPLAY": "自動再生",
|
|
||||||
"About": "約",
|
|
||||||
"Add a ": "追加",
|
|
||||||
"Browse your files": "ファイルを参照",
|
|
||||||
"COMMENT": "コメント",
|
|
||||||
"Categories": "カテゴリー",
|
|
||||||
"Category": "カテゴリー",
|
|
||||||
"Change Language": "言語を変更",
|
|
||||||
"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.": "「録画開始」をクリックして、録画する画面またはタブを選択します。録画が終了したら、「録画停止」をクリックすると、録画がアップロードされます。",
|
|
||||||
"Comment": "コメント",
|
|
||||||
"Comments": "コメント",
|
|
||||||
"Comments are disabled": "コメントは無効です",
|
|
||||||
"Contact": "連絡先",
|
|
||||||
"DELETE MEDIA": "メディアを削除",
|
|
||||||
"DOWNLOAD": "ダウンロード",
|
|
||||||
"Drag and drop files": "ファイルをドラッグアンドドロップ",
|
|
||||||
"EDIT MEDIA": "メディアを編集",
|
|
||||||
"EDIT PROFILE": "プロフィールを編集",
|
|
||||||
"EDIT SUBTITLE": "字幕を編集",
|
|
||||||
"Edit media": "メディアを編集",
|
|
||||||
"Edit profile": "プロフィールを編集",
|
|
||||||
"Edit subtitle": "字幕を編集",
|
|
||||||
"Featured": "注目",
|
|
||||||
"Go": "行く",
|
|
||||||
"History": "履歴",
|
|
||||||
"Home": "ホーム",
|
|
||||||
"Language": "言語",
|
|
||||||
"Latest": "最新",
|
|
||||||
"Liked media": "いいねしたメディア",
|
|
||||||
"Manage comments": "コメントを管理",
|
|
||||||
"Manage media": "メディアを管理",
|
|
||||||
"Manage users": "ユーザーを管理",
|
|
||||||
"Media": "メディア",
|
|
||||||
"Media was edited": "メディアが編集されました",
|
|
||||||
"Members": "メンバー",
|
|
||||||
"My media": "私のメディア",
|
|
||||||
"My playlists": "私のプレイリスト",
|
|
||||||
"No": "いいえ",
|
|
||||||
"No comment yet": "まだコメントはありません",
|
|
||||||
"No comments yet": "まだコメントはありません",
|
|
||||||
"No results for": "の結果はありません",
|
|
||||||
"PLAYLISTS": "プレイリスト",
|
|
||||||
"Playlists": "プレイリスト",
|
|
||||||
"Powered by": "提供",
|
|
||||||
"Publish": "公開",
|
|
||||||
"Published on": "公開日",
|
|
||||||
"Recommended": "おすすめ",
|
|
||||||
"Record Screen": "画面を録画",
|
|
||||||
"Register": "登録",
|
|
||||||
"SAVE": "保存",
|
|
||||||
"SEARCH": "検索",
|
|
||||||
"SHARE": "共有",
|
|
||||||
"SHOW MORE": "もっと見る",
|
|
||||||
"SUBMIT": "送信",
|
|
||||||
"Search": "検索",
|
|
||||||
"Select": "選択",
|
|
||||||
"Sign in": "サインイン",
|
|
||||||
"Sign out": "サインアウト",
|
|
||||||
"Start Recording": "録画開始",
|
|
||||||
"Stop Recording": "録画停止",
|
|
||||||
"Subtitle was added": "字幕が追加されました",
|
|
||||||
"Subtitles": "字幕",
|
|
||||||
"Tags": "タグ",
|
|
||||||
"Terms": "利用規約",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "これはChrome、Safari、Edgeブラウザで動作します。",
|
|
||||||
"Trim": "トリム",
|
|
||||||
"UPLOAD": "アップロード",
|
|
||||||
"Up next": "次に再生",
|
|
||||||
"Upload": "アップロード",
|
|
||||||
"Upload media": "メディアをアップロード",
|
|
||||||
"Uploads": "アップロード",
|
|
||||||
"VIEW ALL": "すべて表示",
|
|
||||||
"View all": "すべて表示",
|
|
||||||
"View media": "メディアを見る",
|
|
||||||
"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": "タグ内のメディア",
|
|
||||||
"or": "または",
|
|
||||||
"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": "年前",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "정보",
|
|
||||||
"AUTOPLAY": "자동 재생",
|
|
||||||
"About": "정보",
|
|
||||||
"Add a ": "추가",
|
|
||||||
"Browse your files": "파일 찾아보기",
|
|
||||||
"COMMENT": "댓글",
|
|
||||||
"Categories": "카테고리",
|
|
||||||
"Category": "카테고리",
|
|
||||||
"Change Language": "언어 변경",
|
|
||||||
"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.": "'녹화 시작'을 클릭하고 녹화할 화면이나 탭을 선택하세요. 녹화가 끝나면 '녹화 중지'를 클릭하면 녹화 파일이 업로드됩니다.",
|
|
||||||
"Comment": "댓글",
|
|
||||||
"Comments": "댓글",
|
|
||||||
"Comments are disabled": "댓글이 비활성화되었습니다",
|
|
||||||
"Contact": "연락처",
|
|
||||||
"DELETE MEDIA": "미디어 삭제",
|
|
||||||
"DOWNLOAD": "다운로드",
|
|
||||||
"Drag and drop files": "파일을 끌어다 놓기",
|
|
||||||
"EDIT MEDIA": "미디어 편집",
|
|
||||||
"EDIT PROFILE": "프로필 편집",
|
|
||||||
"EDIT SUBTITLE": "자막 편집",
|
|
||||||
"Edit media": "미디어 편집",
|
|
||||||
"Edit profile": "프로필 편집",
|
|
||||||
"Edit subtitle": "자막 편집",
|
|
||||||
"Featured": "추천",
|
|
||||||
"Go": "이동",
|
|
||||||
"History": "기록",
|
|
||||||
"Home": "홈",
|
|
||||||
"Language": "언어",
|
|
||||||
"Latest": "최신",
|
|
||||||
"Liked media": "좋아한 미디어",
|
|
||||||
"Manage comments": "댓글 관리",
|
|
||||||
"Manage media": "미디어 관리",
|
|
||||||
"Manage users": "사용자 관리",
|
|
||||||
"Media": "미디어",
|
|
||||||
"Media was edited": "미디어가 편집되었습니다",
|
|
||||||
"Members": "회원",
|
|
||||||
"My media": "내 미디어",
|
|
||||||
"My playlists": "내 재생 목록",
|
|
||||||
"No": "아니요",
|
|
||||||
"No comment yet": "아직 댓글이 없습니다",
|
|
||||||
"No comments yet": "아직 댓글이 없습니다",
|
|
||||||
"No results for": "결과 없음",
|
|
||||||
"PLAYLISTS": "재생 목록",
|
|
||||||
"Playlists": "재생 목록",
|
|
||||||
"Powered by": "제공",
|
|
||||||
"Publish": "게시",
|
|
||||||
"Published on": "게시일",
|
|
||||||
"Recommended": "추천",
|
|
||||||
"Record Screen": "화면 녹화",
|
|
||||||
"Register": "등록",
|
|
||||||
"SAVE": "저장",
|
|
||||||
"SEARCH": "검색",
|
|
||||||
"SHARE": "공유",
|
|
||||||
"SHOW MORE": "더 보기",
|
|
||||||
"SUBMIT": "제출",
|
|
||||||
"Search": "검색",
|
|
||||||
"Select": "선택",
|
|
||||||
"Sign in": "로그인",
|
|
||||||
"Sign out": "로그아웃",
|
|
||||||
"Start Recording": "녹화 시작",
|
|
||||||
"Stop Recording": "녹화 중지",
|
|
||||||
"Subtitle was added": "자막이 추가되었습니다",
|
|
||||||
"Subtitles": "자막",
|
|
||||||
"Tags": "태그",
|
|
||||||
"Terms": "약관",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "이 기능은 Chrome, Safari 및 Edge 브라우저에서 작동합니다.",
|
|
||||||
"Trim": "자르기",
|
|
||||||
"UPLOAD": "업로드",
|
|
||||||
"Up next": "다음",
|
|
||||||
"Upload": "업로드",
|
|
||||||
"Upload media": "미디어 업로드",
|
|
||||||
"Uploads": "업로드",
|
|
||||||
"VIEW ALL": "모두 보기",
|
|
||||||
"View all": "모두 보기",
|
|
||||||
"View media": "미디어 보기",
|
|
||||||
"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": "태그의 미디어",
|
|
||||||
"or": "또는",
|
|
||||||
"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": "년 전",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "OVER",
|
|
||||||
"AUTOPLAY": "AUTOMATISCH AFSPELEN",
|
|
||||||
"About": "Over",
|
|
||||||
"Add a ": "Voeg een ",
|
|
||||||
"Browse your files": "Blader door uw bestanden",
|
|
||||||
"COMMENT": "REACTIE",
|
|
||||||
"Categories": "Categorieën",
|
|
||||||
"Category": "Categorie",
|
|
||||||
"Change Language": "Taal 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.",
|
|
||||||
"Comment": "Reactie",
|
|
||||||
"Comments": "Reacties",
|
|
||||||
"Comments are disabled": "Reacties zijn uitgeschakeld",
|
|
||||||
"Contact": "Contact",
|
|
||||||
"DELETE MEDIA": "MEDIA VERWIJDEREN",
|
|
||||||
"DOWNLOAD": "DOWNLOADEN",
|
|
||||||
"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",
|
|
||||||
"Featured": "Aanbevolen",
|
|
||||||
"Go": "Ga",
|
|
||||||
"History": "Geschiedenis",
|
|
||||||
"Home": "Home",
|
|
||||||
"Language": "Taal",
|
|
||||||
"Latest": "Laatste",
|
|
||||||
"Liked media": "Leuke media",
|
|
||||||
"Manage comments": "Reacties beheren",
|
|
||||||
"Manage media": "Media beheren",
|
|
||||||
"Manage users": "Gebruikers beheren",
|
|
||||||
"Media": "Media",
|
|
||||||
"Media was edited": "Media is bewerkt",
|
|
||||||
"Members": "Leden",
|
|
||||||
"My media": "Mijn media",
|
|
||||||
"My playlists": "Mijn afspeellijsten",
|
|
||||||
"No": "Nee",
|
|
||||||
"No comment yet": "Nog geen reactie",
|
|
||||||
"No comments yet": "Nog geen reacties",
|
|
||||||
"No results for": "Geen resultaten voor",
|
|
||||||
"PLAYLISTS": "AFSPEELLIJSTEN",
|
|
||||||
"Playlists": "Afspeellijsten",
|
|
||||||
"Powered by": "Aangedreven door",
|
|
||||||
"Publish": "Publiceren",
|
|
||||||
"Published on": "Gepubliceerd op",
|
|
||||||
"Recommended": "Aanbevolen",
|
|
||||||
"Record Screen": "Scherm opnemen",
|
|
||||||
"Register": "Registreren",
|
|
||||||
"SAVE": "OPSLAAN",
|
|
||||||
"SEARCH": "ZOEKEN",
|
|
||||||
"SHARE": "DELEN",
|
|
||||||
"SHOW MORE": "MEER WEERGEVEN",
|
|
||||||
"SUBMIT": "INDIENEN",
|
|
||||||
"Search": "Zoeken",
|
|
||||||
"Select": "Selecteer",
|
|
||||||
"Sign in": "Inloggen",
|
|
||||||
"Sign out": "Uitloggen",
|
|
||||||
"Start Recording": "Opname starten",
|
|
||||||
"Stop Recording": "Opname stoppen",
|
|
||||||
"Subtitle was added": "Ondertitel is toegevoegd",
|
|
||||||
"Subtitles": "Ondertitels",
|
|
||||||
"Tags": "Tags",
|
|
||||||
"Terms": "Voorwaarden",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "Dit werkt in Chrome, Safari en Edge browsers.",
|
|
||||||
"Trim": "Bijsnijden",
|
|
||||||
"UPLOAD": "UPLOADEN",
|
|
||||||
"Up next": "Hierna",
|
|
||||||
"Upload": "Uploaden",
|
|
||||||
"Upload media": "Media uploaden",
|
|
||||||
"Uploads": "Uploads",
|
|
||||||
"VIEW ALL": "BEKIJK ALLES",
|
|
||||||
"View all": "Bekijk alles",
|
|
||||||
"View media": "Media bekijken",
|
|
||||||
"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",
|
|
||||||
"or": "of",
|
|
||||||
"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",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "SOBRE",
|
|
||||||
"AUTOPLAY": "REPRODUÇÃO AUTOMÁTICA",
|
|
||||||
"About": "Sobre",
|
|
||||||
"Add a ": "Adicionar um ",
|
|
||||||
"Browse your files": "Procurar seus arquivos",
|
|
||||||
"COMMENT": "COMENTÁRIO",
|
|
||||||
"Categories": "Categorias",
|
|
||||||
"Category": "Categoria",
|
|
||||||
"Change Language": "Mudar idioma",
|
|
||||||
"Change password": "Mudar senha",
|
|
||||||
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "Clique em 'Iniciar gravação' e selecione a tela ou guia para gravar. Quando a gravação terminar, clique em 'Parar gravação' e a gravação será enviada.",
|
|
||||||
"Comment": "Comentário",
|
|
||||||
"Comments": "Comentários",
|
|
||||||
"Comments are disabled": "Comentários estão desativados",
|
|
||||||
"Contact": "Contato",
|
|
||||||
"DELETE MEDIA": "EXCLUIR MÍDIA",
|
|
||||||
"DOWNLOAD": "BAIXAR",
|
|
||||||
"Drag and drop files": "Arraste e solte arquivos",
|
|
||||||
"EDIT MEDIA": "EDITAR MÍDIA",
|
|
||||||
"EDIT PROFILE": "EDITAR PERFIL",
|
|
||||||
"EDIT SUBTITLE": "EDITAR LEGENDA",
|
|
||||||
"Edit media": "Editar mídia",
|
|
||||||
"Edit profile": "Editar perfil",
|
|
||||||
"Edit subtitle": "Editar legenda",
|
|
||||||
"Featured": "Destaque",
|
|
||||||
"Go": "Ir",
|
|
||||||
"History": "Histórico",
|
|
||||||
"Home": "Início",
|
|
||||||
"Language": "Idioma",
|
|
||||||
"Latest": "Últimos",
|
|
||||||
"Liked media": "Mídia curtida",
|
|
||||||
"Manage comments": "Gerenciar comentários",
|
|
||||||
"Manage media": "Gerenciar mídia",
|
|
||||||
"Manage users": "Gerenciar usuários",
|
|
||||||
"Media": "Mídia",
|
|
||||||
"Media was edited": "Mídia foi editada",
|
|
||||||
"Members": "Membros",
|
|
||||||
"My media": "Minhas mídias",
|
|
||||||
"My playlists": "Minhas playlists",
|
|
||||||
"No": "Não",
|
|
||||||
"No comment yet": "Nenhum comentário ainda",
|
|
||||||
"No comments yet": "Nenhum comentário ainda",
|
|
||||||
"No results for": "Nenhum resultado para",
|
|
||||||
"PLAYLISTS": "PLAYLISTS",
|
|
||||||
"Playlists": "Playlists",
|
|
||||||
"Powered by": "Desenvolvido por",
|
|
||||||
"Publish": "Publicar",
|
|
||||||
"Published on": "Publicado em",
|
|
||||||
"Recommended": "Recomendado",
|
|
||||||
"Record Screen": "Gravar tela",
|
|
||||||
"Register": "Registrar",
|
|
||||||
"SAVE": "SALVAR",
|
|
||||||
"SEARCH": "PESQUISAR",
|
|
||||||
"SHARE": "COMPARTILHAR",
|
|
||||||
"SHOW MORE": "MOSTRAR MAIS",
|
|
||||||
"SUBMIT": "ENVIAR",
|
|
||||||
"Search": "Pesquisar",
|
|
||||||
"Select": "Selecionar",
|
|
||||||
"Sign in": "Entrar",
|
|
||||||
"Sign out": "Sair",
|
|
||||||
"Start Recording": "Iniciar Gravação",
|
|
||||||
"Stop Recording": "Parar Gravação",
|
|
||||||
"Subtitle was added": "Legenda foi adicionada",
|
|
||||||
"Subtitles": "Legendas",
|
|
||||||
"Tags": "Tags",
|
|
||||||
"Terms": "Termos",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "Isso funciona nos navegadores Chrome, Safari e Edge.",
|
|
||||||
"Trim": "Cortar",
|
|
||||||
"UPLOAD": "CARREGAR",
|
|
||||||
"Up next": "A seguir",
|
|
||||||
"Upload": "Carregar",
|
|
||||||
"Upload media": "Carregar mídia",
|
|
||||||
"Uploads": "Uploads",
|
|
||||||
"VIEW ALL": "VER TODOS",
|
|
||||||
"View all": "Ver todos",
|
|
||||||
"View media": "Ver mídia",
|
|
||||||
"comment": "comentário",
|
|
||||||
"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": "é um CMS de vídeo e mídia de código aberto, moderno e completo. Foi desenvolvido para atender às necessidades das plataformas web modernas para visualização e compartilhamento de mídia",
|
|
||||||
"media in category": "mídia na categoria",
|
|
||||||
"media in tag": "mídia na tag",
|
|
||||||
"or": "ou",
|
|
||||||
"view": "visualização",
|
|
||||||
"views": "visualizações",
|
|
||||||
"yet": "ainda",
|
|
||||||
}
|
|
||||||
|
|
||||||
replacement_strings = {
|
|
||||||
"Apr": "Abr",
|
|
||||||
"Aug": "Ago",
|
|
||||||
"Dec": "Dez",
|
|
||||||
"Feb": "Fev",
|
|
||||||
"Jan": "Jan",
|
|
||||||
"Jul": "Jul",
|
|
||||||
"Jun": "Jun",
|
|
||||||
"Mar": "Mar",
|
|
||||||
"May": "Mai",
|
|
||||||
"Nov": "Nov",
|
|
||||||
"Oct": "Out",
|
|
||||||
"Sep": "Set",
|
|
||||||
"day ago": "dia atrás",
|
|
||||||
"days ago": "dias atrás",
|
|
||||||
"hour ago": "hora atrás",
|
|
||||||
"hours ago": "horas atrás",
|
|
||||||
"just now": "agora mesmo",
|
|
||||||
"minute ago": "minuto atrás",
|
|
||||||
"minutes ago": "minutos atrás",
|
|
||||||
"month ago": "mês atrás",
|
|
||||||
"months ago": "meses atrás",
|
|
||||||
"second ago": "segundo atrás",
|
|
||||||
"seconds ago": "segundos atrás",
|
|
||||||
"week ago": "semana atrás",
|
|
||||||
"weeks ago": "semanas atrás",
|
|
||||||
"year ago": "ano atrás",
|
|
||||||
"years ago": "anos atrás",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "О",
|
|
||||||
"AUTOPLAY": "Автовоспроизведение",
|
|
||||||
"About": "О",
|
|
||||||
"Add a ": "Добавить ",
|
|
||||||
"Browse your files": "Просмотреть файлы",
|
|
||||||
"COMMENT": "КОММЕНТАРИЙ",
|
|
||||||
"Categories": "Категории",
|
|
||||||
"Category": "Категория",
|
|
||||||
"Change Language": "Изменить язык",
|
|
||||||
"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.": "Нажмите 'Начать запись' и выберите экран или вкладку для записи. После окончания записи нажмите 'Остановить запись', и запись будет загружена.",
|
|
||||||
"Comment": "Комментарий",
|
|
||||||
"Comments": "Комментарии",
|
|
||||||
"Comments are disabled": "Комментарии отключены",
|
|
||||||
"Contact": "Контакт",
|
|
||||||
"DELETE MEDIA": "УДАЛИТЬ МЕДИА",
|
|
||||||
"DOWNLOAD": "СКАЧАТЬ",
|
|
||||||
"Drag and drop files": "Перетащите файлы",
|
|
||||||
"EDIT MEDIA": "РЕДАКТИРОВАТЬ МЕДИА",
|
|
||||||
"EDIT PROFILE": "РЕДАКТИРОВАТЬ ПРОФИЛЬ",
|
|
||||||
"EDIT SUBTITLE": "РЕДАКТИРОВАТЬ СУБТИТРЫ",
|
|
||||||
"Edit media": "Редактировать медиа",
|
|
||||||
"Edit profile": "Редактировать профиль",
|
|
||||||
"Edit subtitle": "Редактировать субтитры",
|
|
||||||
"Featured": "Рекомендуемое",
|
|
||||||
"Go": "Перейти",
|
|
||||||
"History": "История",
|
|
||||||
"Home": "Главная",
|
|
||||||
"Language": "Язык",
|
|
||||||
"Latest": "Последние",
|
|
||||||
"Liked media": "Понравившиеся медиа",
|
|
||||||
"Manage comments": "Управление комментариями",
|
|
||||||
"Manage media": "Управление медиа",
|
|
||||||
"Manage users": "Управление пользователями",
|
|
||||||
"Media": "Медиа",
|
|
||||||
"Media was edited": "Медиа было отредактировано",
|
|
||||||
"Members": "Участники",
|
|
||||||
"My media": "Мои медиа",
|
|
||||||
"My playlists": "Мои плейлисты",
|
|
||||||
"No": "Нет",
|
|
||||||
"No comment yet": "Комментариев пока нет",
|
|
||||||
"No comments yet": "Комментариев пока нет",
|
|
||||||
"No results for": "Нет результатов для",
|
|
||||||
"PLAYLISTS": "ПЛЕЙЛИСТЫ",
|
|
||||||
"Playlists": "Плейлисты",
|
|
||||||
"Powered by": "Работает на",
|
|
||||||
"Publish": "Опубликовать",
|
|
||||||
"Published on": "Опубликовано",
|
|
||||||
"Recommended": "Рекомендуемое",
|
|
||||||
"Record Screen": "Запись экрана",
|
|
||||||
"Register": "Регистрация",
|
|
||||||
"SAVE": "СОХРАНИТЬ",
|
|
||||||
"SEARCH": "ПОИСК",
|
|
||||||
"SHARE": "ПОДЕЛИТЬСЯ",
|
|
||||||
"SHOW MORE": "ПОКАЗАТЬ БОЛЬШЕ",
|
|
||||||
"SUBMIT": "ОТПРАВИТЬ",
|
|
||||||
"Search": "Поиск",
|
|
||||||
"Select": "Выбрать",
|
|
||||||
"Sign in": "Войти",
|
|
||||||
"Sign out": "Выйти",
|
|
||||||
"Start Recording": "Начать запись",
|
|
||||||
"Stop Recording": "Остановить запись",
|
|
||||||
"Subtitle was added": "Субтитры были добавлены",
|
|
||||||
"Subtitles": "Субтитры",
|
|
||||||
"Tags": "Теги",
|
|
||||||
"Terms": "Условия",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "Это работает в браузерах Chrome, Safari и Edge.",
|
|
||||||
"Trim": "Обрезать",
|
|
||||||
"UPLOAD": "ЗАГРУЗИТЬ",
|
|
||||||
"Up next": "Далее",
|
|
||||||
"Upload": "Загрузить",
|
|
||||||
"Upload media": "Загрузить медиа",
|
|
||||||
"Uploads": "Загрузки",
|
|
||||||
"VIEW ALL": "ПОКАЗАТЬ ВСЕ",
|
|
||||||
"View all": "Показать все",
|
|
||||||
"View media": "Просмотр медиа",
|
|
||||||
"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": "медиа в теге",
|
|
||||||
"or": "или",
|
|
||||||
"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": "лет назад",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "O NAS",
|
|
||||||
"AUTOPLAY": "SAMODEJNO PREDVAJANJE",
|
|
||||||
"About": "O nas",
|
|
||||||
"Add a ": "Dodaj ",
|
|
||||||
"Browse your files": "Prebrskaj datoteke",
|
|
||||||
"COMMENT": "KOMENTAR",
|
|
||||||
"Categories": "Kategorije",
|
|
||||||
"Category": "Kategorija",
|
|
||||||
"Change Language": "Spremeni jezik",
|
|
||||||
"Change password": "Spremeni geslo",
|
|
||||||
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "Kliknite 'Začni snemanje' in izberite zaslon ali zavihek za snemanje. Ko je snemanje končano, kliknite 'Ustavi snemanje' in posnetek bo naložen.",
|
|
||||||
"Comment": "Komentar",
|
|
||||||
"Comments": "Komentarji",
|
|
||||||
"Comments are disabled": "Komentarji so onemogočeni",
|
|
||||||
"Contact": "Kontakt",
|
|
||||||
"DELETE MEDIA": "IZBRIŠI MEDIJ",
|
|
||||||
"DOWNLOAD": "PRENESI",
|
|
||||||
"Drag and drop files": "Povleci in spusti datoteke",
|
|
||||||
"EDIT MEDIA": "UREDI MEDIJ",
|
|
||||||
"EDIT PROFILE": "UREDI PROFIL",
|
|
||||||
"EDIT SUBTITLE": "UREDI PODNAPISE",
|
|
||||||
"Edit media": "Uredi medij",
|
|
||||||
"Edit profile": "Uredi profil",
|
|
||||||
"Edit subtitle": "Uredi podnapise",
|
|
||||||
"Featured": "Izbrani",
|
|
||||||
"Go": "Pojdi",
|
|
||||||
"History": "Zgodovina",
|
|
||||||
"Home": "Domov",
|
|
||||||
"Language": "Jezik",
|
|
||||||
"Latest": "Najnovejši",
|
|
||||||
"Liked media": "Všečkani mediji",
|
|
||||||
"Manage comments": "Upravljaj komentarje",
|
|
||||||
"Manage media": "Upravljaj medije",
|
|
||||||
"Manage users": "Upravljaj uporabnike",
|
|
||||||
"Media": "Mediji",
|
|
||||||
"Media was edited": "Medij je bil urejen",
|
|
||||||
"Members": "Člani",
|
|
||||||
"My media": "Moji mediji",
|
|
||||||
"My playlists": "Moji seznami predvajanja",
|
|
||||||
"No": "Ne",
|
|
||||||
"No comment yet": "Brez komentarja",
|
|
||||||
"No comments yet": "Brez komentarjev",
|
|
||||||
"No results for": "Ni rezultatov za",
|
|
||||||
"PLAYLISTS": "SEZNAMI PREDVAJANJA",
|
|
||||||
"Playlists": "Seznami predvajanja",
|
|
||||||
"Powered by": "Poganja",
|
|
||||||
"Publish": "Objavi",
|
|
||||||
"Published on": "Objavljeno",
|
|
||||||
"Recommended": "Priporočeno",
|
|
||||||
"Record Screen": "Snemanje zaslona",
|
|
||||||
"Register": "Registracija",
|
|
||||||
"SAVE": "SHRANI",
|
|
||||||
"SEARCH": "ISKANJE",
|
|
||||||
"SHARE": "DELI",
|
|
||||||
"SHOW MORE": "PRIKAŽI VEČ",
|
|
||||||
"SUBMIT": "POŠLJI",
|
|
||||||
"Search": "Iskanje",
|
|
||||||
"Select": "Izberi",
|
|
||||||
"Sign in": "Prijava",
|
|
||||||
"Sign out": "Odjava",
|
|
||||||
"Start Recording": "Začni snemanje",
|
|
||||||
"Stop Recording": "Ustavi snemanje",
|
|
||||||
"Subtitle was added": "Podnapisi so bili dodani",
|
|
||||||
"Subtitles": "Podnapisi",
|
|
||||||
"Tags": "Oznake",
|
|
||||||
"Terms": "Pogoji",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "To deluje v brskalnikih Chrome, Safari in Edge.",
|
|
||||||
"Trim": "Obreži",
|
|
||||||
"UPLOAD": "NALOŽI",
|
|
||||||
"Up next": "Naslednji",
|
|
||||||
"Upload": "Naloži",
|
|
||||||
"Upload media": "Naloži medij",
|
|
||||||
"Uploads": "Naloženi",
|
|
||||||
"VIEW ALL": "PRIKAŽI VSE",
|
|
||||||
"View all": "Prikaži vse",
|
|
||||||
"View media": "Ogled medija",
|
|
||||||
"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": "je moderni, popolnoma opremljen odprtokodni video in medijski CMS. Razvit je za potrebe sodobnih spletnih platform za ogled in deljenje medijev",
|
|
||||||
"media in category": "mediji v kategoriji",
|
|
||||||
"media in tag": "mediji z oznako",
|
|
||||||
"or": "ali",
|
|
||||||
"view": "ogled",
|
|
||||||
"views": "ogledi",
|
|
||||||
"yet": "še",
|
|
||||||
}
|
|
||||||
|
|
||||||
replacement_strings = {
|
|
||||||
"Apr": "Apr",
|
|
||||||
"Aug": "Avg",
|
|
||||||
"Dec": "Dec",
|
|
||||||
"Feb": "Feb",
|
|
||||||
"Jan": "Jan",
|
|
||||||
"Jul": "Jul",
|
|
||||||
"Jun": "Jun",
|
|
||||||
"Mar": "Mar",
|
|
||||||
"May": "Maj",
|
|
||||||
"Nov": "Nov",
|
|
||||||
"Oct": "Okt",
|
|
||||||
"Sep": "Sep",
|
|
||||||
"day ago": "dan nazaj",
|
|
||||||
"days ago": "dni nazaj",
|
|
||||||
"hour ago": "ura nazaj",
|
|
||||||
"hours ago": "ur nazaj",
|
|
||||||
"just now": "pravkar",
|
|
||||||
"minute ago": "minuta nazaj",
|
|
||||||
"minutes ago": "minut nazaj",
|
|
||||||
"month ago": "mesec nazaj",
|
|
||||||
"months ago": "mesecev nazaj",
|
|
||||||
"second ago": "sekunda nazaj",
|
|
||||||
"seconds ago": "sekund nazaj",
|
|
||||||
"week ago": "teden nazaj",
|
|
||||||
"weeks ago": "tednov nazaj",
|
|
||||||
"year ago": "leto nazaj",
|
|
||||||
"years ago": "let nazaj",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "HAKKINDA",
|
|
||||||
"AUTOPLAY": "OTOMATİK OYNATMA",
|
|
||||||
"About": "Hakkında",
|
|
||||||
"Add a ": "Ekle ",
|
|
||||||
"Browse your files": "Dosyalarınıza göz atın",
|
|
||||||
"COMMENT": "YORUM",
|
|
||||||
"Categories": "Kategoriler",
|
|
||||||
"Category": "Kategori",
|
|
||||||
"Change Language": "Dili Değiştir",
|
|
||||||
"Change password": "Şifreyi Değiştir",
|
|
||||||
"Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded.": "'Kaydı Başlat'a tıklayın ve kaydedilecek ekranı veya sekmeyi seçin. Kayıt bittiğinde, 'Kaydı Durdur'a tıklayın ve kayıt yüklenecektir.",
|
|
||||||
"Comment": "Yorum",
|
|
||||||
"Comments": "Yorumlar",
|
|
||||||
"Comments are disabled": "Yorumlar devre dışı",
|
|
||||||
"Contact": "İletişim",
|
|
||||||
"DELETE MEDIA": "MEDYAYI SİL",
|
|
||||||
"DOWNLOAD": "İNDİR",
|
|
||||||
"Drag and drop files": "Dosyaları sürükleyip bırakın",
|
|
||||||
"EDIT MEDIA": "MEDYAYI DÜZENLE",
|
|
||||||
"EDIT PROFILE": "PROFİLİ DÜZENLE",
|
|
||||||
"EDIT SUBTITLE": "ALT YAZIYI DÜZENLE",
|
|
||||||
"Edit media": "Medyayı düzenle",
|
|
||||||
"Edit profile": "Profili düzenle",
|
|
||||||
"Edit subtitle": "Alt yazıyı düzenle",
|
|
||||||
"Featured": "Öne Çıkan",
|
|
||||||
"Go": "Git",
|
|
||||||
"History": "Geçmiş",
|
|
||||||
"Home": "Ana Sayfa",
|
|
||||||
"Language": "Dil",
|
|
||||||
"Latest": "En Son",
|
|
||||||
"Liked media": "Beğenilen medya",
|
|
||||||
"Manage comments": "Yorumları yönet",
|
|
||||||
"Manage media": "Medyayı yönet",
|
|
||||||
"Manage users": "Kullanıcıları yönet",
|
|
||||||
"Media": "Medya",
|
|
||||||
"Media was edited": "Medya düzenlendi",
|
|
||||||
"Members": "Üyeler",
|
|
||||||
"My media": "Medyam",
|
|
||||||
"My playlists": "Çalma listelerim",
|
|
||||||
"No": "Hayır",
|
|
||||||
"No comment yet": "Henüz yorum yok",
|
|
||||||
"No comments yet": "Henüz yorum yok",
|
|
||||||
"No results for": "Sonuç bulunamadı",
|
|
||||||
"PLAYLISTS": "ÇALMA LİSTELERİ",
|
|
||||||
"Playlists": "Çalma listeleri",
|
|
||||||
"Powered by": "Tarafından desteklenmektedir",
|
|
||||||
"Publish": "Yayınla",
|
|
||||||
"Published on": "Yayınlanma tarihi",
|
|
||||||
"Recommended": "Önerilen",
|
|
||||||
"Record Screen": "Ekranı Kaydet",
|
|
||||||
"Register": "Kayıt Ol",
|
|
||||||
"SAVE": "KAYDET",
|
|
||||||
"SEARCH": "ARA",
|
|
||||||
"SHARE": "PAYLAŞ",
|
|
||||||
"SHOW MORE": "DAHA FAZLA GÖSTER",
|
|
||||||
"SUBMIT": "GÖNDER",
|
|
||||||
"Search": "Ara",
|
|
||||||
"Select": "Seç",
|
|
||||||
"Sign in": "Giriş Yap",
|
|
||||||
"Sign out": "Çıkış Yap",
|
|
||||||
"Start Recording": "Kaydı Başlat",
|
|
||||||
"Stop Recording": "Kaydı Durdur",
|
|
||||||
"Subtitle was added": "Alt yazı eklendi",
|
|
||||||
"Subtitles": "Altyazılar",
|
|
||||||
"Tags": "Etiketler",
|
|
||||||
"Terms": "Şartlar",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "Bu, Chrome, Safari ve Edge tarayıcılarında çalışır.",
|
|
||||||
"Trim": "Kırp",
|
|
||||||
"UPLOAD": "YÜKLE",
|
|
||||||
"Up next": "Sıradaki",
|
|
||||||
"Upload": "Yükle",
|
|
||||||
"Upload media": "Medya yükle",
|
|
||||||
"Uploads": "Yüklemeler",
|
|
||||||
"VIEW ALL": "HEPSİNİ GÖR",
|
|
||||||
"View all": "Hepsini gör",
|
|
||||||
"View media": "Medyayı Görüntüle",
|
|
||||||
"comment": "yorum",
|
|
||||||
"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": "modern, tam özellikli açık kaynaklı bir video ve medya CMS'sidir. Medya izleme ve paylaşma ihtiyaçlarını karşılamak için geliştirilmiştir",
|
|
||||||
"media in category": "kategorideki medya",
|
|
||||||
"media in tag": "etiketteki medya",
|
|
||||||
"or": "veya",
|
|
||||||
"view": "görünüm",
|
|
||||||
"views": "görünümler",
|
|
||||||
"yet": "henüz",
|
|
||||||
}
|
|
||||||
|
|
||||||
replacement_strings = {
|
|
||||||
"Apr": "Nis",
|
|
||||||
"Aug": "Ağu",
|
|
||||||
"Dec": "Ara",
|
|
||||||
"Feb": "Şub",
|
|
||||||
"Jan": "Oca",
|
|
||||||
"Jul": "Tem",
|
|
||||||
"Jun": "Haz",
|
|
||||||
"Mar": "Mar",
|
|
||||||
"May": "May",
|
|
||||||
"Nov": "Kas",
|
|
||||||
"Oct": "Eki",
|
|
||||||
"Sep": "Eyl",
|
|
||||||
"day ago": "gün önce",
|
|
||||||
"days ago": "gün önce",
|
|
||||||
"hour ago": "saat önce",
|
|
||||||
"hours ago": "saat önce",
|
|
||||||
"just now": "şimdi",
|
|
||||||
"minute ago": "dakika önce",
|
|
||||||
"minutes ago": "dakika önce",
|
|
||||||
"month ago": "ay önce",
|
|
||||||
"months ago": "ay önce",
|
|
||||||
"second ago": "saniye önce",
|
|
||||||
"seconds ago": "saniye önce",
|
|
||||||
"week ago": "hafta önce",
|
|
||||||
"weeks ago": "hafta önce",
|
|
||||||
"year ago": "yıl önce",
|
|
||||||
"years ago": "yıl önce",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "کے بارے میں",
|
|
||||||
"AUTOPLAY": "خودکار پلے",
|
|
||||||
"About": "کے بارے میں",
|
|
||||||
"Add a ": "شامل کریں",
|
|
||||||
"Browse your files": "اپنی فائلیں براؤز کریں",
|
|
||||||
"COMMENT": "تبصرہ",
|
|
||||||
"Categories": "اقسام",
|
|
||||||
"Category": "قسم",
|
|
||||||
"Change Language": "زبان تبدیل کریں",
|
|
||||||
"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.": "'ریکارڈنگ شروع کریں' پر کلک کریں اور ریکارڈ کرنے کے لیے اسکرین یا ٹیب منتخب کریں۔ ریکارڈنگ مکمل ہونے کے بعد، 'ریکارڈنگ بند کریں' پر کلک کریں، اور ریکارڈنگ اپ لوڈ ہو جائے گی۔",
|
|
||||||
"Comment": "تبصرہ",
|
|
||||||
"Comments": "تبصرے",
|
|
||||||
"Comments are disabled": "تبصرے غیر فعال ہیں",
|
|
||||||
"Contact": "رابطہ کریں",
|
|
||||||
"DELETE MEDIA": "میڈیا حذف کریں",
|
|
||||||
"DOWNLOAD": "ڈاؤن لوڈ",
|
|
||||||
"Drag and drop files": "فائلیں گھسیٹیں اور چھوڑیں",
|
|
||||||
"EDIT MEDIA": "میڈیا ترمیم کریں",
|
|
||||||
"EDIT PROFILE": "پروفائل ترمیم کریں",
|
|
||||||
"EDIT SUBTITLE": "سب ٹائٹل ترمیم کریں",
|
|
||||||
"Edit media": "میڈیا ترمیم کریں",
|
|
||||||
"Edit profile": "پروفائل ترمیم کریں",
|
|
||||||
"Edit subtitle": "سب ٹائٹل ترمیم کریں",
|
|
||||||
"Featured": "نمایاں",
|
|
||||||
"Go": "جائیں",
|
|
||||||
"History": "تاریخ",
|
|
||||||
"Home": "ہوم",
|
|
||||||
"Language": "زبان",
|
|
||||||
"Latest": "تازہ ترین",
|
|
||||||
"Liked media": "پسندیدہ میڈیا",
|
|
||||||
"Manage comments": "تبصرے منظم کریں",
|
|
||||||
"Manage media": "میڈیا منظم کریں",
|
|
||||||
"Manage users": "صارفین منظم کریں",
|
|
||||||
"Media": "میڈیا",
|
|
||||||
"Media was edited": "میڈیا ترمیم کیا گیا",
|
|
||||||
"Members": "اراکین",
|
|
||||||
"My media": "میرا میڈیا",
|
|
||||||
"My playlists": "میری پلے لسٹس",
|
|
||||||
"No": "نہیں",
|
|
||||||
"No comment yet": "ابھی تک کوئی تبصرہ نہیں",
|
|
||||||
"No comments yet": "ابھی تک کوئی تبصرے نہیں",
|
|
||||||
"No results for": "کے لئے کوئی نتائج نہیں",
|
|
||||||
"PLAYLISTS": "پلے لسٹس",
|
|
||||||
"Playlists": "پلے لسٹس",
|
|
||||||
"Powered by": "کے ذریعہ تقویت یافتہ",
|
|
||||||
"Publish": "شائع کریں",
|
|
||||||
"Published on": "پر شائع ہوا",
|
|
||||||
"Recommended": "تجویز کردہ",
|
|
||||||
"Record Screen": "اسکرین ریکارڈ کریں",
|
|
||||||
"Register": "رجسٹر کریں",
|
|
||||||
"SAVE": "محفوظ کریں",
|
|
||||||
"SEARCH": "تلاش کریں",
|
|
||||||
"SHARE": "شیئر کریں",
|
|
||||||
"SHOW MORE": "مزید دکھائیں",
|
|
||||||
"SUBMIT": "جمع کرائیں",
|
|
||||||
"Search": "تلاش کریں",
|
|
||||||
"Select": "منتخب کریں",
|
|
||||||
"Sign in": "سائن ان کریں",
|
|
||||||
"Sign out": "سائن آؤٹ کریں",
|
|
||||||
"Start Recording": "ریکارڈنگ شروع کریں",
|
|
||||||
"Stop Recording": "ریکارڈنگ روکیں",
|
|
||||||
"Subtitle was added": "سب ٹائٹل شامل کیا گیا",
|
|
||||||
"Subtitles": "سب ٹائٹلز",
|
|
||||||
"Tags": "ٹیگز",
|
|
||||||
"Terms": "شرائط",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "یہ کروم، سفاری اور ایج براؤزرز میں کام کرتا ہے۔",
|
|
||||||
"Trim": "تراشیں",
|
|
||||||
"UPLOAD": "اپ لوڈ کریں",
|
|
||||||
"Up next": "اگلا",
|
|
||||||
"Upload": "اپ لوڈ کریں",
|
|
||||||
"Upload media": "میڈیا اپ لوڈ کریں",
|
|
||||||
"Uploads": "اپ لوڈز",
|
|
||||||
"VIEW ALL": "سب دیکھیں",
|
|
||||||
"View all": "سب دیکھیں",
|
|
||||||
"View media": "میڈیا دیکھیں",
|
|
||||||
"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": "ٹیگ میں میڈیا",
|
|
||||||
"or": "یا",
|
|
||||||
"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": "سال پہلے",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "关于",
|
|
||||||
"AUTOPLAY": "自动播放",
|
|
||||||
"About": "关于",
|
|
||||||
"Add a ": "添加一个",
|
|
||||||
"Browse your files": "浏览文件",
|
|
||||||
"COMMENT": "评论",
|
|
||||||
"Categories": "分类",
|
|
||||||
"Category": "类别",
|
|
||||||
"Change Language": "更改语言",
|
|
||||||
"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.": "点击“开始录制”并选择要录制的屏幕或标签页。录制完成后,点击“停止录制”,录制内容将被上传。",
|
|
||||||
"Comment": "评论",
|
|
||||||
"Comments": "评论",
|
|
||||||
"Comments are disabled": "评论已禁用",
|
|
||||||
"Contact": "联系",
|
|
||||||
"DELETE MEDIA": "删除媒体",
|
|
||||||
"DOWNLOAD": "下载",
|
|
||||||
"Drag and drop files": "拖放文件",
|
|
||||||
"EDIT MEDIA": "编辑媒体",
|
|
||||||
"EDIT PROFILE": "编辑个人资料",
|
|
||||||
"EDIT SUBTITLE": "编辑字幕",
|
|
||||||
"Edit media": "编辑媒体",
|
|
||||||
"Edit profile": "编辑个人资料",
|
|
||||||
"Edit subtitle": "编辑字幕",
|
|
||||||
"Featured": "精选",
|
|
||||||
"Go": "去",
|
|
||||||
"History": "历史",
|
|
||||||
"Home": "主页",
|
|
||||||
"Language": "语言",
|
|
||||||
"Latest": "最新",
|
|
||||||
"Liked media": "喜欢的媒体",
|
|
||||||
"Manage comments": "管理评论",
|
|
||||||
"Manage media": "管理媒体",
|
|
||||||
"Manage users": "管理用户",
|
|
||||||
"Media": "媒体",
|
|
||||||
"Media was edited": "媒体已编辑",
|
|
||||||
"Members": "成员",
|
|
||||||
"My media": "我的媒体",
|
|
||||||
"My playlists": "我的播放列表",
|
|
||||||
"No": "否",
|
|
||||||
"No comment yet": "还没有评论",
|
|
||||||
"No comments yet": "还没有评论",
|
|
||||||
"No results for": "没有结果",
|
|
||||||
"PLAYLISTS": "播放列表",
|
|
||||||
"Playlists": "播放列表",
|
|
||||||
"Powered by": "由...提供技术支持",
|
|
||||||
"Publish": "发布",
|
|
||||||
"Published on": "发布于",
|
|
||||||
"Recommended": "推荐",
|
|
||||||
"Record Screen": "录制屏幕",
|
|
||||||
"Register": "注册",
|
|
||||||
"SAVE": "保存",
|
|
||||||
"SEARCH": "搜索",
|
|
||||||
"SHARE": "分享",
|
|
||||||
"SHOW MORE": "显示更多",
|
|
||||||
"SUBMIT": "提交",
|
|
||||||
"Search": "搜索",
|
|
||||||
"Select": "选择",
|
|
||||||
"Sign in": "登录",
|
|
||||||
"Sign out": "登出",
|
|
||||||
"Start Recording": "开始录制",
|
|
||||||
"Stop Recording": "停止录制",
|
|
||||||
"Subtitle was added": "字幕已添加",
|
|
||||||
"Subtitles": "字幕",
|
|
||||||
"Tags": "标签",
|
|
||||||
"Terms": "条款",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "此功能适用于 Chrome、Safari 和 Edge 浏览器。",
|
|
||||||
"Trim": "修剪",
|
|
||||||
"UPLOAD": "上传",
|
|
||||||
"Up next": "接下来",
|
|
||||||
"Upload": "上传",
|
|
||||||
"Upload media": "上传媒体",
|
|
||||||
"Uploads": "上传",
|
|
||||||
"VIEW ALL": "查看全部",
|
|
||||||
"View all": "查看全部",
|
|
||||||
"View media": "查看媒体",
|
|
||||||
"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": "标签中的媒体",
|
|
||||||
"or": "或",
|
|
||||||
"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": "年前",
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
translation_strings = {
|
|
||||||
"ABOUT": "關於",
|
|
||||||
"AUTOPLAY": "自動播放",
|
|
||||||
"About": "關於",
|
|
||||||
"Add a ": "新增",
|
|
||||||
"Browse your files": "瀏覽您的檔案",
|
|
||||||
"COMMENT": "留言",
|
|
||||||
"Categories": "分類",
|
|
||||||
"Category": "分類",
|
|
||||||
"Change Language": "切換語言",
|
|
||||||
"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.": "點擊「開始錄製」並選擇要錄製的螢幕或分頁。錄製完成後,點擊「停止錄製」,錄製的內容將會上傳。",
|
|
||||||
"Comment": "留言",
|
|
||||||
"Comments": "留言",
|
|
||||||
"Comments are disabled": "留言功能已關閉",
|
|
||||||
"Contact": "聯絡資訊",
|
|
||||||
"DELETE MEDIA": "刪除影片",
|
|
||||||
"DOWNLOAD": "下載",
|
|
||||||
"Drag and drop files": "拖放檔案",
|
|
||||||
"EDIT MEDIA": "編輯影片",
|
|
||||||
"EDIT PROFILE": "編輯個人資料",
|
|
||||||
"EDIT SUBTITLE": "編輯字幕",
|
|
||||||
"Edit media": "編輯影片",
|
|
||||||
"Edit profile": "編輯個人資料",
|
|
||||||
"Edit subtitle": "編輯字幕",
|
|
||||||
"Featured": "精選內容",
|
|
||||||
"Go": "執行",
|
|
||||||
"History": "觀看紀錄",
|
|
||||||
"Home": "首頁",
|
|
||||||
"Language": "語言",
|
|
||||||
"Latest": "最新內容",
|
|
||||||
"Liked media": "我喜歡的影片",
|
|
||||||
"Manage comments": "留言管理",
|
|
||||||
"Manage media": "媒體管理",
|
|
||||||
"Manage users": "使用者管理",
|
|
||||||
"Media": "媒體",
|
|
||||||
"Media was edited": "媒體已更新",
|
|
||||||
"Members": "會員",
|
|
||||||
"My media": "我的媒體",
|
|
||||||
"My playlists": "我的播放清單",
|
|
||||||
"No": "無",
|
|
||||||
"No comment yet": "尚無留言",
|
|
||||||
"No comments yet": "尚未有留言",
|
|
||||||
"No results for": "查無相關結果:",
|
|
||||||
"PLAYLISTS": "播放清單",
|
|
||||||
"Playlists": "播放清單",
|
|
||||||
"Powered by": "技術提供為",
|
|
||||||
"Publish": "發布",
|
|
||||||
"Published on": "發布日期為",
|
|
||||||
"Recommended": "推薦內容",
|
|
||||||
"Record Screen": "螢幕錄製",
|
|
||||||
"Register": "註冊",
|
|
||||||
"SAVE": "儲存",
|
|
||||||
"SEARCH": "搜尋",
|
|
||||||
"SHARE": "分享",
|
|
||||||
"SHOW MORE": "顯示更多",
|
|
||||||
"SUBMIT": "送出",
|
|
||||||
"Search": "搜尋",
|
|
||||||
"Select": "選擇",
|
|
||||||
"Sign in": "登入",
|
|
||||||
"Sign out": "登出",
|
|
||||||
"Start Recording": "開始錄製",
|
|
||||||
"Stop Recording": "停止錄製",
|
|
||||||
"Subtitle was added": "字幕已新增",
|
|
||||||
"Subtitles": "字幕",
|
|
||||||
"Tags": "標籤",
|
|
||||||
"Terms": "使用條款",
|
|
||||||
"This works in Chrome, Safari and Edge browsers.": "此功能適用於 Chrome、Safari 和 Edge 瀏覽器。",
|
|
||||||
"Trim": "修剪",
|
|
||||||
"UPLOAD": "上傳",
|
|
||||||
"Up next": "即將播放",
|
|
||||||
"Upload": "上傳",
|
|
||||||
"Upload media": "上傳媒體",
|
|
||||||
"Uploads": "上傳內容",
|
|
||||||
"VIEW ALL": "查看全部",
|
|
||||||
"View all": "瀏覽全部",
|
|
||||||
"View media": "查看媒體",
|
|
||||||
"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": "此標籤下的媒體",
|
|
||||||
"or": "或者",
|
|
||||||
"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": "年前",
|
|
||||||
}
|
|
||||||
200
files/helpers.py
200
files/helpers.py
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
@ -16,9 +15,6 @@ from django.conf import settings
|
|||||||
|
|
||||||
CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
CRF_ENCODING_NUM_SECONDS = 2 # 0 * 60 # videos with greater duration will get
|
CRF_ENCODING_NUM_SECONDS = 2 # 0 * 60 # videos with greater duration will get
|
||||||
# CRF encoding and not two-pass
|
# CRF encoding and not two-pass
|
||||||
# Encoding individual chunks may yield quality variations if you use a
|
# Encoding individual chunks may yield quality variations if you use a
|
||||||
@ -34,6 +30,12 @@ BUF_SIZE_MULTIPLIER = 1.5
|
|||||||
KEYFRAME_DISTANCE = 4
|
KEYFRAME_DISTANCE = 4
|
||||||
KEYFRAME_DISTANCE_MIN = 2
|
KEYFRAME_DISTANCE_MIN = 2
|
||||||
|
|
||||||
|
# speed presets
|
||||||
|
# see https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||||
|
X26x_PRESET = "medium" # "medium"
|
||||||
|
X265_PRESET = "medium"
|
||||||
|
X26x_PRESET_BIG_HEIGHT = "faster"
|
||||||
|
|
||||||
# VP9_SPEED = 1 # between 0 and 4, lower is slower
|
# VP9_SPEED = 1 # between 0 and 4, lower is slower
|
||||||
VP9_SPEED = 2
|
VP9_SPEED = 2
|
||||||
|
|
||||||
@ -49,7 +51,6 @@ VIDEO_CRFS = {
|
|||||||
VIDEO_BITRATES = {
|
VIDEO_BITRATES = {
|
||||||
"h264": {
|
"h264": {
|
||||||
25: {
|
25: {
|
||||||
144: 150,
|
|
||||||
240: 300,
|
240: 300,
|
||||||
360: 500,
|
360: 500,
|
||||||
480: 1000,
|
480: 1000,
|
||||||
@ -62,7 +63,6 @@ VIDEO_BITRATES = {
|
|||||||
},
|
},
|
||||||
"h265": {
|
"h265": {
|
||||||
25: {
|
25: {
|
||||||
144: 75,
|
|
||||||
240: 150,
|
240: 150,
|
||||||
360: 275,
|
360: 275,
|
||||||
480: 500,
|
480: 500,
|
||||||
@ -75,7 +75,6 @@ VIDEO_BITRATES = {
|
|||||||
},
|
},
|
||||||
"vp9": {
|
"vp9": {
|
||||||
25: {
|
25: {
|
||||||
144: 75,
|
|
||||||
240: 150,
|
240: 150,
|
||||||
360: 275,
|
360: 275,
|
||||||
480: 500,
|
480: 500,
|
||||||
@ -170,7 +169,7 @@ def rm_dir(directory):
|
|||||||
|
|
||||||
def url_from_path(filename):
|
def url_from_path(filename):
|
||||||
# TODO: find a way to preserver http - https ...
|
# TODO: find a way to preserver http - https ...
|
||||||
return f"{settings.MEDIA_URL}{filename.replace(settings.MEDIA_ROOT, '')}"
|
return "{0}{1}".format(settings.MEDIA_URL, filename.replace(settings.MEDIA_ROOT, ""))
|
||||||
|
|
||||||
|
|
||||||
def create_temp_file(suffix=None, dir=settings.TEMP_DIRECTORY):
|
def create_temp_file(suffix=None, dir=settings.TEMP_DIRECTORY):
|
||||||
@ -485,7 +484,7 @@ def show_file_size(size):
|
|||||||
if size:
|
if size:
|
||||||
size = size / 1000000
|
size = size / 1000000
|
||||||
size = round(size, 1)
|
size = round(size, 1)
|
||||||
size = f"{str(size)}MB"
|
size = "{0}MB".format(str(size))
|
||||||
return size
|
return size
|
||||||
|
|
||||||
|
|
||||||
@ -593,13 +592,17 @@ def get_base_ffmpeg_command(
|
|||||||
cmd = base_cmd[:]
|
cmd = base_cmd[:]
|
||||||
|
|
||||||
# preset settings
|
# preset settings
|
||||||
preset = getattr(settings, "FFMPEG_DEFAULT_PRESET", "medium")
|
|
||||||
|
|
||||||
if encoder == "libvpx-vp9":
|
if encoder == "libvpx-vp9":
|
||||||
if pass_number == 1:
|
if pass_number == 1:
|
||||||
speed = 4
|
speed = 4
|
||||||
else:
|
else:
|
||||||
speed = VP9_SPEED
|
speed = VP9_SPEED
|
||||||
|
elif encoder in ["libx264"]:
|
||||||
|
preset = X26x_PRESET
|
||||||
|
elif encoder in ["libx265"]:
|
||||||
|
preset = X265_PRESET
|
||||||
|
if target_height >= 720:
|
||||||
|
preset = X26x_PRESET_BIG_HEIGHT
|
||||||
|
|
||||||
if encoder == "libx264":
|
if encoder == "libx264":
|
||||||
level = "4.2" if target_height <= 1080 else "5.2"
|
level = "4.2" if target_height <= 1080 else "5.2"
|
||||||
@ -723,7 +726,7 @@ def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_fi
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if media_info.get("video_height") < resolution:
|
if media_info.get("video_height") < resolution:
|
||||||
if resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
if resolution not in [240, 360]: # always get these two
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# if codec == "h264_baseline":
|
# if codec == "h264_baseline":
|
||||||
@ -784,179 +787,6 @@ def clean_query(query):
|
|||||||
return query.lower()
|
return query.lower()
|
||||||
|
|
||||||
|
|
||||||
def timestamp_to_seconds(timestamp):
|
|
||||||
"""Convert a timestamp in format HH:MM:SS.mmm to seconds
|
|
||||||
|
|
||||||
Args:
|
|
||||||
timestamp (str): Timestamp in format HH:MM:SS.mmm
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Timestamp in seconds
|
|
||||||
"""
|
|
||||||
h, m, s = timestamp.split(':')
|
|
||||||
s, ms = s.split('.')
|
|
||||||
return int(h) * 3600 + int(m) * 60 + int(s) + float('0.' + ms)
|
|
||||||
|
|
||||||
|
|
||||||
def seconds_to_timestamp(seconds):
|
|
||||||
"""Convert seconds to timestamp in format HH:MM:SS.mmm
|
|
||||||
|
|
||||||
Args:
|
|
||||||
seconds (float): Time in seconds
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Timestamp in format HH:MM:SS.mmm
|
|
||||||
"""
|
|
||||||
hours = int(seconds // 3600)
|
|
||||||
minutes = int((seconds % 3600) // 60)
|
|
||||||
seconds_remainder = seconds % 60
|
|
||||||
seconds_int = int(seconds_remainder)
|
|
||||||
milliseconds = int((seconds_remainder - seconds_int) * 1000)
|
|
||||||
|
|
||||||
return f"{hours:02d}:{minutes:02d}:{seconds_int:02d}.{milliseconds:03d}" # noqa
|
|
||||||
|
|
||||||
|
|
||||||
def get_trim_timestamps(media_file_path, timestamps_list, run_ffprobe=False):
|
|
||||||
"""Process a list of timestamps to align start times with I-frames for better video trimming
|
|
||||||
|
|
||||||
Args:
|
|
||||||
media_file_path (str): Path to the media file
|
|
||||||
timestamps_list (list): List of dictionaries with startTime and endTime
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: Processed timestamps with adjusted startTime values
|
|
||||||
"""
|
|
||||||
if not isinstance(timestamps_list, list):
|
|
||||||
return []
|
|
||||||
|
|
||||||
timestamps_results = []
|
|
||||||
timestamps_to_process = []
|
|
||||||
|
|
||||||
for item in timestamps_list:
|
|
||||||
if isinstance(item, dict) and 'startTime' in item and 'endTime' in item:
|
|
||||||
timestamps_to_process.append(item)
|
|
||||||
|
|
||||||
if not timestamps_to_process:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# just a single timestamp with no startTime, no need to process
|
|
||||||
if len(timestamps_to_process) == 1 and timestamps_to_process[0]['startTime'] == "00:00:00.000":
|
|
||||||
return timestamps_list
|
|
||||||
|
|
||||||
# Process each timestamp
|
|
||||||
for item in timestamps_to_process:
|
|
||||||
startTime = item['startTime']
|
|
||||||
endTime = item['endTime']
|
|
||||||
|
|
||||||
# with ffmpeg -ss -i that is getting run, there is no need to call ffprobe to find the I-frame,
|
|
||||||
# as ffmpeg will do that. Keeping this for now in case it is needed
|
|
||||||
|
|
||||||
i_frames = []
|
|
||||||
if run_ffprobe:
|
|
||||||
SEC_TO_SUBTRACT = 10
|
|
||||||
start_seconds = timestamp_to_seconds(startTime)
|
|
||||||
search_start = max(0, start_seconds - SEC_TO_SUBTRACT)
|
|
||||||
|
|
||||||
# Create ffprobe command to find nearest I-frame
|
|
||||||
cmd = [
|
|
||||||
settings.FFPROBE_COMMAND,
|
|
||||||
"-v",
|
|
||||||
"error",
|
|
||||||
"-select_streams",
|
|
||||||
"v:0",
|
|
||||||
"-show_entries",
|
|
||||||
"frame=pts_time,pict_type",
|
|
||||||
"-of",
|
|
||||||
"csv=p=0",
|
|
||||||
"-read_intervals",
|
|
||||||
f"{search_start}%{startTime}",
|
|
||||||
media_file_path,
|
|
||||||
]
|
|
||||||
cmd = [str(s) for s in cmd]
|
|
||||||
logger.info(f"trim cmd: {cmd}")
|
|
||||||
|
|
||||||
stdout = run_command(cmd).get("out")
|
|
||||||
|
|
||||||
if stdout:
|
|
||||||
for line in stdout.strip().split('\n'):
|
|
||||||
if line and line.endswith(',I'):
|
|
||||||
i_frames.append(line.replace(',I', ''))
|
|
||||||
|
|
||||||
if i_frames:
|
|
||||||
adjusted_startTime = seconds_to_timestamp(float(i_frames[-1]))
|
|
||||||
|
|
||||||
if not i_frames:
|
|
||||||
adjusted_startTime = startTime
|
|
||||||
|
|
||||||
timestamps_results.append({'startTime': adjusted_startTime, 'endTime': endTime})
|
|
||||||
|
|
||||||
return timestamps_results
|
|
||||||
|
|
||||||
|
|
||||||
def trim_video_method(media_file_path, timestamps_list):
|
|
||||||
"""Trim a video file based on a list of timestamps
|
|
||||||
|
|
||||||
Args:
|
|
||||||
media_file_path (str): Path to the media file
|
|
||||||
timestamps_list (list): List of dictionaries with startTime and endTime
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if successful, False otherwise
|
|
||||||
"""
|
|
||||||
if not isinstance(timestamps_list, list) or not timestamps_list:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not os.path.exists(media_file_path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
|
||||||
output_file = os.path.join(temp_dir, "output.mp4")
|
|
||||||
|
|
||||||
segment_files = []
|
|
||||||
for i, item in enumerate(timestamps_list):
|
|
||||||
start_time = timestamp_to_seconds(item['startTime'])
|
|
||||||
end_time = timestamp_to_seconds(item['endTime'])
|
|
||||||
duration = end_time - start_time
|
|
||||||
|
|
||||||
# For single timestamp, we can use the output file directly
|
|
||||||
# For multiple timestamps, we need to create segment files
|
|
||||||
segment_file = output_file if len(timestamps_list) == 1 else os.path.join(temp_dir, f"segment_{i}.mp4")
|
|
||||||
|
|
||||||
cmd = [settings.FFMPEG_COMMAND, "-y", "-ss", str(item['startTime']), "-i", media_file_path, "-t", str(duration), "-c", "copy", "-avoid_negative_ts", "1", segment_file]
|
|
||||||
|
|
||||||
result = run_command(cmd) # noqa
|
|
||||||
|
|
||||||
if os.path.exists(segment_file) and os.path.getsize(segment_file) > 0:
|
|
||||||
if len(timestamps_list) > 1:
|
|
||||||
segment_files.append(segment_file)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if len(timestamps_list) > 1:
|
|
||||||
if not segment_files:
|
|
||||||
return False
|
|
||||||
|
|
||||||
concat_list_path = os.path.join(temp_dir, "concat_list.txt")
|
|
||||||
with open(concat_list_path, "w") as f:
|
|
||||||
for segment in segment_files:
|
|
||||||
f.write(f"file '{segment}'\n")
|
|
||||||
concat_cmd = [settings.FFMPEG_COMMAND, "-y", "-f", "concat", "-safe", "0", "-i", concat_list_path, "-c", "copy", output_file]
|
|
||||||
|
|
||||||
concat_result = run_command(concat_cmd) # noqa
|
|
||||||
|
|
||||||
if not os.path.exists(output_file) or os.path.getsize(output_file) == 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Replace the original file with the trimmed version
|
|
||||||
try:
|
|
||||||
rm_file(media_file_path)
|
|
||||||
shutil.copy2(output_file, media_file_path)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.info(f"Failed to replace original file: {str(e)}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_alphanumeric_only(string):
|
def get_alphanumeric_only(string):
|
||||||
"""Returns a query that contains only alphanumeric characters
|
"""Returns a query that contains only alphanumeric characters
|
||||||
This include characters other than the English alphabet too
|
This include characters other than the English alphabet too
|
||||||
|
|||||||
@ -1,58 +0,0 @@
|
|||||||
import importlib
|
|
||||||
import os
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'Process translation files to add missing keys and sort them'
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
translations_dir = os.path.join(settings.BASE_DIR, 'files', 'frontend_translations')
|
|
||||||
self.process_translation_files(translations_dir)
|
|
||||||
self.stdout.write(self.style.SUCCESS('Successfully processed translation files'))
|
|
||||||
|
|
||||||
def process_translation_files(self, translations_dir):
|
|
||||||
files = os.listdir(translations_dir)
|
|
||||||
files = [f for f in files if f.endswith('.py') and f not in ('__init__.py', 'en.py')]
|
|
||||||
# Import the original English translations
|
|
||||||
from files.frontend_translations.en import (
|
|
||||||
replacement_strings,
|
|
||||||
translation_strings,
|
|
||||||
)
|
|
||||||
|
|
||||||
for file in files:
|
|
||||||
file_path = os.path.join(translations_dir, file)
|
|
||||||
module_name = f"files.frontend_translations.{file[:-3]}"
|
|
||||||
|
|
||||||
module = importlib.import_module(module_name)
|
|
||||||
|
|
||||||
translation_strings_wip = getattr(module, 'translation_strings', {})
|
|
||||||
replacement_strings_wip = getattr(module, 'replacement_strings', {})
|
|
||||||
|
|
||||||
for key in translation_strings:
|
|
||||||
if key not in translation_strings_wip:
|
|
||||||
translation_strings_wip[key] = translation_strings[key]
|
|
||||||
|
|
||||||
translation_strings_wip = OrderedDict(sorted(translation_strings_wip.items()))
|
|
||||||
|
|
||||||
for key in replacement_strings:
|
|
||||||
if key not in replacement_strings_wip:
|
|
||||||
replacement_strings_wip[key] = replacement_strings[key]
|
|
||||||
|
|
||||||
replacement_strings_wip = OrderedDict(sorted(replacement_strings_wip.items()))
|
|
||||||
|
|
||||||
with open(file_path, 'w') as f:
|
|
||||||
f.write("translation_strings = {\n")
|
|
||||||
for key, value in translation_strings_wip.items():
|
|
||||||
f.write(f' "{key}": "{value}",\n') # noqa
|
|
||||||
f.write("}\n\n")
|
|
||||||
|
|
||||||
f.write("replacement_strings = {\n")
|
|
||||||
for key, value in replacement_strings_wip.items():
|
|
||||||
f.write(f' "{key}": "{value}",\n') # noqa
|
|
||||||
f.write("}\n")
|
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS(f'Processed {file}'))
|
|
||||||
@ -1,5 +1,3 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.db.models import Q
|
|
||||||
from drf_yasg import openapi as openapi
|
from drf_yasg import openapi as openapi
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -48,7 +46,6 @@ class MediaList(APIView):
|
|||||||
|
|
||||||
featured = params.get("featured", "").strip()
|
featured = params.get("featured", "").strip()
|
||||||
is_reviewed = params.get("is_reviewed", "").strip()
|
is_reviewed = params.get("is_reviewed", "").strip()
|
||||||
category = params.get("category", "").strip()
|
|
||||||
|
|
||||||
sort_by_options = [
|
sort_by_options = [
|
||||||
"title",
|
"title",
|
||||||
@ -101,9 +98,6 @@ class MediaList(APIView):
|
|||||||
if is_reviewed != "all":
|
if is_reviewed != "all":
|
||||||
qs = qs.filter(is_reviewed=is_reviewed)
|
qs = qs.filter(is_reviewed=is_reviewed)
|
||||||
|
|
||||||
if category:
|
|
||||||
qs = qs.filter(category__title__contains=category)
|
|
||||||
|
|
||||||
media = qs.order_by(f"{ordering}{sort_by}")
|
media = qs.order_by(f"{ordering}{sort_by}")
|
||||||
|
|
||||||
paginator = pagination_class()
|
paginator = pagination_class()
|
||||||
@ -221,13 +215,6 @@ class UserList(APIView):
|
|||||||
elif role == "editor":
|
elif role == "editor":
|
||||||
qs = qs.filter(is_editor=True)
|
qs = qs.filter(is_editor=True)
|
||||||
|
|
||||||
if settings.USERS_NEEDS_TO_BE_APPROVED:
|
|
||||||
is_approved = request.GET.get("is_approved")
|
|
||||||
if is_approved == "true":
|
|
||||||
qs = qs.filter(is_approved=True)
|
|
||||||
elif is_approved == "false":
|
|
||||||
qs = qs.filter(Q(is_approved=False) | Q(is_approved__isnull=True))
|
|
||||||
|
|
||||||
users = qs.order_by(f"{ordering}{sort_by}")
|
users = qs.order_by(f"{ordering}{sort_by}")
|
||||||
|
|
||||||
paginator = pagination_class()
|
paginator = pagination_class()
|
||||||
|
|||||||
224
files/methods.py
224
files/methods.py
@ -5,19 +5,16 @@ import itertools
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.files import File
|
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from cms import celery_app
|
from cms import celery_app
|
||||||
|
|
||||||
from . import helpers, models
|
from . import models
|
||||||
from .helpers import mask_ip
|
from .helpers import mask_ip
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -122,16 +119,12 @@ def get_next_state(user, current_state, next_state):
|
|||||||
|
|
||||||
if next_state not in ["public", "private", "unlisted"]:
|
if next_state not in ["public", "private", "unlisted"]:
|
||||||
next_state = settings.PORTAL_WORKFLOW # get default state
|
next_state = settings.PORTAL_WORKFLOW # get default state
|
||||||
|
|
||||||
if is_mediacms_editor(user):
|
if is_mediacms_editor(user):
|
||||||
# allow any transition
|
# allow any transition
|
||||||
return next_state
|
return next_state
|
||||||
|
|
||||||
if settings.PORTAL_WORKFLOW == "private":
|
if settings.PORTAL_WORKFLOW == "private":
|
||||||
if next_state in ["private", "unlisted"]:
|
next_state = "private"
|
||||||
next_state = next_state
|
|
||||||
else:
|
|
||||||
next_state = current_state
|
|
||||||
|
|
||||||
if settings.PORTAL_WORKFLOW == "unlisted":
|
if settings.PORTAL_WORKFLOW == "unlisted":
|
||||||
# don't allow to make media public in this case
|
# don't allow to make media public in this case
|
||||||
@ -166,14 +159,14 @@ Media becomes private if it gets reported %s times\n
|
|||||||
)
|
)
|
||||||
|
|
||||||
if settings.ADMINS_NOTIFICATIONS.get("MEDIA_REPORTED", False):
|
if settings.ADMINS_NOTIFICATIONS.get("MEDIA_REPORTED", False):
|
||||||
title = f"[{settings.PORTAL_NAME}] - Media was reported"
|
title = "[{}] - Media was reported".format(settings.PORTAL_NAME)
|
||||||
d = {}
|
d = {}
|
||||||
d["title"] = title
|
d["title"] = title
|
||||||
d["msg"] = msg
|
d["msg"] = msg
|
||||||
d["to"] = settings.ADMIN_EMAIL_LIST
|
d["to"] = settings.ADMIN_EMAIL_LIST
|
||||||
notify_items.append(d)
|
notify_items.append(d)
|
||||||
if settings.USERS_NOTIFICATIONS.get("MEDIA_REPORTED", False):
|
if settings.USERS_NOTIFICATIONS.get("MEDIA_REPORTED", False):
|
||||||
title = f"[{settings.PORTAL_NAME}] - Media was reported"
|
title = "[{}] - Media was reported".format(settings.PORTAL_NAME)
|
||||||
d = {}
|
d = {}
|
||||||
d["title"] = title
|
d["title"] = title
|
||||||
d["msg"] = msg
|
d["msg"] = msg
|
||||||
@ -182,7 +175,7 @@ Media becomes private if it gets reported %s times\n
|
|||||||
|
|
||||||
if action == "media_added" and media:
|
if action == "media_added" and media:
|
||||||
if settings.ADMINS_NOTIFICATIONS.get("MEDIA_ADDED", False):
|
if settings.ADMINS_NOTIFICATIONS.get("MEDIA_ADDED", False):
|
||||||
title = f"[{settings.PORTAL_NAME}] - Media was added"
|
title = "[{}] - Media was added".format(settings.PORTAL_NAME)
|
||||||
msg = """
|
msg = """
|
||||||
Media %s was added by user %s.
|
Media %s was added by user %s.
|
||||||
""" % (
|
""" % (
|
||||||
@ -195,7 +188,7 @@ Media %s was added by user %s.
|
|||||||
d["to"] = settings.ADMIN_EMAIL_LIST
|
d["to"] = settings.ADMIN_EMAIL_LIST
|
||||||
notify_items.append(d)
|
notify_items.append(d)
|
||||||
if settings.USERS_NOTIFICATIONS.get("MEDIA_ADDED", False):
|
if settings.USERS_NOTIFICATIONS.get("MEDIA_ADDED", False):
|
||||||
title = f"[{settings.PORTAL_NAME}] - Your media was added"
|
title = "[{}] - Your media was added".format(settings.PORTAL_NAME)
|
||||||
msg = """
|
msg = """
|
||||||
Your media has been added! It will be encoded and will be available soon.
|
Your media has been added! It will be encoded and will be available soon.
|
||||||
URL: %s
|
URL: %s
|
||||||
@ -265,7 +258,7 @@ def show_related_media_content(media, request, limit):
|
|||||||
"user_featured",
|
"user_featured",
|
||||||
"-user_featured",
|
"-user_featured",
|
||||||
]
|
]
|
||||||
# TODO: Make this mess more readable, and add TAGS support - aka related
|
# TODO: MAke this mess more readable, and add TAGS support - aka related
|
||||||
# tags rather than random media
|
# tags rather than random media
|
||||||
if len(m) < limit:
|
if len(m) < limit:
|
||||||
category = media.category.first()
|
category = media.category.first()
|
||||||
@ -339,7 +332,7 @@ def notify_user_on_comment(friendly_token):
|
|||||||
media_url = settings.SSL_FRONTEND_HOST + media.get_absolute_url()
|
media_url = settings.SSL_FRONTEND_HOST + media.get_absolute_url()
|
||||||
|
|
||||||
if user.notification_on_comments:
|
if user.notification_on_comments:
|
||||||
title = f"[{settings.PORTAL_NAME}] - A comment was added"
|
title = "[{}] - A comment was added".format(settings.PORTAL_NAME)
|
||||||
msg = """
|
msg = """
|
||||||
A comment has been added to your media %s .
|
A comment has been added to your media %s .
|
||||||
View it on %s
|
View it on %s
|
||||||
@ -363,7 +356,7 @@ def notify_user_on_mention(friendly_token, user_mentioned, cleaned_comment):
|
|||||||
media_url = settings.SSL_FRONTEND_HOST + media.get_absolute_url()
|
media_url = settings.SSL_FRONTEND_HOST + media.get_absolute_url()
|
||||||
|
|
||||||
if user.notification_on_comments:
|
if user.notification_on_comments:
|
||||||
title = f"[{settings.PORTAL_NAME}] - You were mentioned in a comment"
|
title = "[{}] - You were mentioned in a comment".format(settings.PORTAL_NAME)
|
||||||
msg = """
|
msg = """
|
||||||
You were mentioned in a comment on %s .
|
You were mentioned in a comment on %s .
|
||||||
View it on %s
|
View it on %s
|
||||||
@ -401,149 +394,6 @@ def clean_comment(raw_comment):
|
|||||||
return cleaned_comment
|
return cleaned_comment
|
||||||
|
|
||||||
|
|
||||||
def user_allowed_to_upload(request):
|
|
||||||
"""Any custom logic for whether a user is allowed
|
|
||||||
to upload content lives here
|
|
||||||
"""
|
|
||||||
|
|
||||||
if request.user.is_anonymous:
|
|
||||||
return False
|
|
||||||
if is_mediacms_editor(request.user):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check if user has reached the maximum number of uploads
|
|
||||||
if hasattr(settings, 'NUMBER_OF_MEDIA_USER_CAN_UPLOAD'):
|
|
||||||
if models.Media.objects.filter(user=request.user).count() >= settings.NUMBER_OF_MEDIA_USER_CAN_UPLOAD:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if settings.CAN_ADD_MEDIA == "all":
|
|
||||||
return True
|
|
||||||
elif settings.CAN_ADD_MEDIA == "email_verified":
|
|
||||||
if request.user.email_is_verified:
|
|
||||||
return True
|
|
||||||
elif settings.CAN_ADD_MEDIA == "advancedUser":
|
|
||||||
if request.user.advancedUser:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def can_transcribe_video(user):
|
|
||||||
"""Checks if a user can transcribe a video."""
|
|
||||||
if not getattr(settings, 'USE_WHISPER_TRANSCRIBE', False):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if is_mediacms_editor(user):
|
|
||||||
return True
|
|
||||||
if getattr(settings, 'USER_CAN_TRANSCRIBE_VIDEO', False):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def kill_ffmpeg_process(filepath):
|
|
||||||
"""Kill ffmpeg process that is processing a specific file
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filepath: Path to the file being processed by ffmpeg
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
subprocess.CompletedProcess: Result of the kill command
|
|
||||||
"""
|
|
||||||
if not filepath:
|
|
||||||
return False
|
|
||||||
cmd = "ps aux|grep 'ffmpeg'|grep %s|grep -v grep |awk '{print $2}'" % filepath
|
|
||||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
|
||||||
pid = result.stdout.decode("utf-8").strip()
|
|
||||||
if pid:
|
|
||||||
cmd = "kill -9 %s" % pid
|
|
||||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def copy_video(original_media, copy_encodings=True, title_suffix="(Trimmed)"):
|
|
||||||
"""Create a copy of a media object
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original_media: Original Media object to copy
|
|
||||||
copy_encodings: Whether to copy the encodings too
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
New Media object
|
|
||||||
"""
|
|
||||||
|
|
||||||
with open(original_media.media_file.path, "rb") as f:
|
|
||||||
myfile = File(f)
|
|
||||||
new_media = models.Media(
|
|
||||||
media_file=myfile,
|
|
||||||
title=f"{original_media.title} {title_suffix}",
|
|
||||||
description=original_media.description,
|
|
||||||
user=original_media.user,
|
|
||||||
media_type="video",
|
|
||||||
enable_comments=original_media.enable_comments,
|
|
||||||
allow_download=original_media.allow_download,
|
|
||||||
state=original_media.state,
|
|
||||||
is_reviewed=original_media.is_reviewed,
|
|
||||||
encoding_status=original_media.encoding_status,
|
|
||||||
listable=original_media.listable,
|
|
||||||
add_date=timezone.now(),
|
|
||||||
video_height=original_media.video_height,
|
|
||||||
media_info=original_media.media_info,
|
|
||||||
)
|
|
||||||
models.Media.objects.bulk_create([new_media])
|
|
||||||
# avoids calling signals since signals will call media_init and we don't want that
|
|
||||||
|
|
||||||
if copy_encodings:
|
|
||||||
for encoding in original_media.encodings.filter(chunk=False, status="success"):
|
|
||||||
if encoding.media_file:
|
|
||||||
with open(encoding.media_file.path, "rb") as f:
|
|
||||||
myfile = File(f)
|
|
||||||
new_encoding = models.Encoding(
|
|
||||||
media_file=myfile, media=new_media, profile=encoding.profile, status="success", progress=100, chunk=False, logs=f"Copied from encoding {encoding.id}"
|
|
||||||
)
|
|
||||||
models.Encoding.objects.bulk_create([new_encoding])
|
|
||||||
# avoids calling signals as this is still not ready
|
|
||||||
|
|
||||||
# Copy categories and tags
|
|
||||||
for category in original_media.category.all():
|
|
||||||
new_media.category.add(category)
|
|
||||||
|
|
||||||
for tag in original_media.tags.all():
|
|
||||||
new_media.tags.add(tag)
|
|
||||||
|
|
||||||
if original_media.thumbnail:
|
|
||||||
with open(original_media.thumbnail.path, 'rb') as f:
|
|
||||||
thumbnail_name = helpers.get_file_name(original_media.thumbnail.path)
|
|
||||||
new_media.thumbnail.save(thumbnail_name, File(f))
|
|
||||||
|
|
||||||
if original_media.poster:
|
|
||||||
with open(original_media.poster.path, 'rb') as f:
|
|
||||||
poster_name = helpers.get_file_name(original_media.poster.path)
|
|
||||||
new_media.poster.save(poster_name, File(f))
|
|
||||||
|
|
||||||
return new_media
|
|
||||||
|
|
||||||
|
|
||||||
def create_video_trim_request(media, data):
|
|
||||||
"""Create a video trim request for a media
|
|
||||||
|
|
||||||
Args:
|
|
||||||
media: Media object
|
|
||||||
data: Dictionary with trim request data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
VideoTrimRequest object
|
|
||||||
"""
|
|
||||||
|
|
||||||
video_action = "replace"
|
|
||||||
if data.get('saveIndividualSegments'):
|
|
||||||
video_action = "create_segments"
|
|
||||||
elif data.get('saveAsCopy'):
|
|
||||||
video_action = "save_new"
|
|
||||||
|
|
||||||
video_trim_request = models.VideoTrimRequest.objects.create(media=media, status="initial", video_action=video_action, media_trim_style='no_encoding', timestamps=data.get('segments', {}))
|
|
||||||
|
|
||||||
return video_trim_request
|
|
||||||
|
|
||||||
|
|
||||||
def list_tasks():
|
def list_tasks():
|
||||||
"""Lists celery tasks
|
"""Lists celery tasks
|
||||||
To be used in an admin dashboard
|
To be used in an admin dashboard
|
||||||
@ -594,59 +444,3 @@ def list_tasks():
|
|||||||
ret["task_ids"] = task_ids
|
ret["task_ids"] = task_ids
|
||||||
ret["media_profile_pairs"] = media_profile_pairs
|
ret["media_profile_pairs"] = media_profile_pairs
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def handle_video_chapters(media, chapters):
|
|
||||||
video_chapter = models.VideoChapterData.objects.filter(media=media).first()
|
|
||||||
if video_chapter:
|
|
||||||
video_chapter.data = chapters
|
|
||||||
video_chapter.save()
|
|
||||||
else:
|
|
||||||
video_chapter = models.VideoChapterData.objects.create(media=media, data=chapters)
|
|
||||||
|
|
||||||
return {'chapters': media.chapter_data}
|
|
||||||
|
|
||||||
|
|
||||||
def change_media_owner(media_id, new_user):
|
|
||||||
"""Change the owner of a media
|
|
||||||
|
|
||||||
Args:
|
|
||||||
media_id: ID of the media to change owner
|
|
||||||
new_user: New user object to set as owner
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Media object or None if media not found
|
|
||||||
"""
|
|
||||||
media = models.Media.objects.filter(id=media_id).first()
|
|
||||||
if not media:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Change the owner
|
|
||||||
media.user = new_user
|
|
||||||
media.save(update_fields=["user"])
|
|
||||||
|
|
||||||
# Update any related permissions
|
|
||||||
media_permissions = models.MediaPermission.objects.filter(media=media)
|
|
||||||
for permission in media_permissions:
|
|
||||||
permission.owner_user = new_user
|
|
||||||
permission.save(update_fields=["owner_user"])
|
|
||||||
|
|
||||||
return media
|
|
||||||
|
|
||||||
|
|
||||||
def copy_media(media_id):
|
|
||||||
"""Create a copy of a media
|
|
||||||
|
|
||||||
Args:
|
|
||||||
media_id: ID of the media to copy
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def is_media_allowed_type(media):
|
|
||||||
if "all" in settings.ALLOWED_MEDIA_UPLOAD_TYPES:
|
|
||||||
return True
|
|
||||||
return media.media_type in settings.ALLOWED_MEDIA_UPLOAD_TYPES
|
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-18 17:40
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('files', '0003_auto_20210927_1245'),
|
|
||||||
('socialaccount', '0006_alter_socialaccount_extra_data'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='subtitle',
|
|
||||||
options={'ordering': ['language__title']},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='category',
|
|
||||||
name='identity_provider',
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
help_text='If category is related with a specific Identity Provider',
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name='categories',
|
|
||||||
to='socialaccount.socialapp',
|
|
||||||
verbose_name='IDP Config Name',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='category',
|
|
||||||
name='is_rbac_category',
|
|
||||||
field=models.BooleanField(db_index=True, default=False, help_text='If access to Category is controlled by role based membership of Groups'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='media',
|
|
||||||
name='state',
|
|
||||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public'), ('unlisted', 'Unlisted')], db_index=True, default='private', help_text='state of Media', max_length=20),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-25 14:13
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import files.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('files', '0004_alter_subtitle_options_category_identity_provider_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='category',
|
|
||||||
name='uid',
|
|
||||||
field=models.CharField(default=files.models.generate_uid, max_length=36, unique=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-27 09:11
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('files', '0005_alter_category_uid'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='category',
|
|
||||||
name='title',
|
|
||||||
field=models.CharField(db_index=True, max_length=100),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-04-15 07:26
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('files', '0006_alter_category_title'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='VideoChapterData',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('data', models.JSONField(help_text='Chapter data')),
|
|
||||||
('media', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chapters', to='files.media')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('media',)},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-05-02 14:23
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('files', '0007_alter_media_state_videochapterdata'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='media',
|
|
||||||
name='state',
|
|
||||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public'), ('unlisted', 'Unlisted')], db_index=True, default='public', help_text='state of Media', max_length=20),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='VideoTrimRequest',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('status', models.CharField(choices=[('initial', 'Initial'), ('running', 'Running'), ('success', 'Success'), ('fail', 'Fail')], default='initial', max_length=20)),
|
|
||||||
('add_date', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('video_action', models.CharField(choices=[('replace', 'Replace Original'), ('save_new', 'Save as New'), ('create_segments', 'Create Segments')], max_length=20)),
|
|
||||||
('media_trim_style', models.CharField(choices=[('no_encoding', 'No Encoding'), ('precise', 'Precise')], default='no_encoding', max_length=20)),
|
|
||||||
('timestamps', models.JSONField(help_text='Timestamps for trimming')),
|
|
||||||
('media', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trim_requests', to='files.media')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-06-20 08:13
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('files', '0008_alter_media_state_videotrimrequest'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='media',
|
|
||||||
name='friendly_token',
|
|
||||||
field=models.CharField(blank=True, db_index=True, help_text='Identifier for the Media', max_length=150, unique=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-07-05 11:49
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('files', '0009_alter_media_friendly_token'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='encodeprofile',
|
|
||||||
name='resolution',
|
|
||||||
field=models.IntegerField(blank=True, choices=[(2160, '2160'), (1440, '1440'), (1080, '1080'), (720, '720'), (480, '480'), (360, '360'), (240, '240'), (144, '144')], null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-07-08 19:15
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('files', '0010_alter_encodeprofile_resolution'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='MediaPermission',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('permission', models.CharField(choices=[('viewer', 'Viewer'), ('editor', 'Editor'), ('owner', 'Owner')], max_length=20)),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('media', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permissions', to='files.media')),
|
|
||||||
('owner_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='granted_permissions', to=settings.AUTH_USER_MODEL)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('user', 'media')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-08-31 08:28
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('files', '0011_mediapermission'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='language',
|
|
||||||
name='code',
|
|
||||||
field=models.CharField(help_text='language code', max_length=30),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='media',
|
|
||||||
name='allow_whisper_transcribe',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='Transcribe auto-detected language'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='media',
|
|
||||||
name='allow_whisper_transcribe_and_translate',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='Transcribe auto-detected language and translate to English'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='TranscriptionRequest',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('add_date', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('status', models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('fail', 'Fail'), ('success', 'Success')], db_index=True, default='pending', max_length=20)),
|
|
||||||
('translate_to_english', models.BooleanField(default=False)),
|
|
||||||
('logs', models.TextField(blank=True, null=True)),
|
|
||||||
('media', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transcriptionrequests', to='files.media')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-09-21 11:49
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('files', '0012_media_allow_whisper_transcribe_and_more'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Page',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('slug', models.SlugField(max_length=200, unique=True)),
|
|
||||||
('title', models.CharField(max_length=200)),
|
|
||||||
('description', models.TextField(blank=True)),
|
|
||||||
('add_date', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('edit_date', models.DateTimeField(auto_now=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='TinyMCEMedia',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('file', models.FileField(upload_to='tinymce_media/')),
|
|
||||||
('uploaded_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('file_type', models.CharField(choices=[('image', 'Image'), ('media', 'Media')], max_length=10)),
|
|
||||||
('original_filename', models.CharField(max_length=255)),
|
|
||||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'TinyMCE Media',
|
|
||||||
'verbose_name_plural': 'TinyMCE Media',
|
|
||||||
'ordering': ['-uploaded_at'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,26 +0,0 @@
|
|||||||
# Import all models for backward compatibility
|
|
||||||
from .category import Category, Tag # noqa: F401
|
|
||||||
from .comment import Comment # noqa: F401
|
|
||||||
from .encoding import EncodeProfile, Encoding # noqa: F401
|
|
||||||
from .license import License # noqa: F401
|
|
||||||
from .media import Media, MediaPermission # noqa: F401
|
|
||||||
from .page import Page, TinyMCEMedia # noqa: F401
|
|
||||||
from .playlist import Playlist, PlaylistMedia # noqa: F401
|
|
||||||
from .rating import Rating, RatingCategory # noqa: F401
|
|
||||||
from .subtitle import Language, Subtitle, TranscriptionRequest # noqa: F401
|
|
||||||
from .utils import CODECS # noqa: F401
|
|
||||||
from .utils import ENCODE_EXTENSIONS # noqa: F401
|
|
||||||
from .utils import ENCODE_EXTENSIONS_KEYS # noqa: F401
|
|
||||||
from .utils import ENCODE_RESOLUTIONS # noqa: F401
|
|
||||||
from .utils import ENCODE_RESOLUTIONS_KEYS # noqa: F401
|
|
||||||
from .utils import MEDIA_ENCODING_STATUS # noqa: F401
|
|
||||||
from .utils import MEDIA_STATES # noqa: F401
|
|
||||||
from .utils import MEDIA_TYPES_SUPPORTED # noqa: F401
|
|
||||||
from .utils import category_thumb_path # noqa: F401
|
|
||||||
from .utils import encoding_media_file_path # noqa: F401
|
|
||||||
from .utils import generate_uid # noqa: F401
|
|
||||||
from .utils import original_media_file_path # noqa: F401
|
|
||||||
from .utils import original_thumbnail_file_path # noqa: F401
|
|
||||||
from .utils import subtitles_file_path # noqa: F401
|
|
||||||
from .utils import validate_rating # noqa: F401
|
|
||||||
from .video_data import VideoChapterData, VideoTrimRequest # noqa: F401
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.html import strip_tags
|
|
||||||
from imagekit.models import ProcessedImageField
|
|
||||||
from imagekit.processors import ResizeToFit
|
|
||||||
|
|
||||||
from .. import helpers
|
|
||||||
from .utils import category_thumb_path, generate_uid
|
|
||||||
|
|
||||||
|
|
||||||
class Category(models.Model):
|
|
||||||
"""A Category base model"""
|
|
||||||
|
|
||||||
uid = models.CharField(unique=True, max_length=36, default=generate_uid)
|
|
||||||
|
|
||||||
add_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
title = models.CharField(max_length=100, db_index=True)
|
|
||||||
|
|
||||||
description = models.TextField(blank=True)
|
|
||||||
|
|
||||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
|
||||||
|
|
||||||
is_global = models.BooleanField(default=False, help_text="global categories or user specific")
|
|
||||||
|
|
||||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
|
||||||
|
|
||||||
thumbnail = ProcessedImageField(
|
|
||||||
upload_to=category_thumb_path,
|
|
||||||
processors=[ResizeToFit(width=344, height=None)],
|
|
||||||
format="JPEG",
|
|
||||||
options={"quality": 85},
|
|
||||||
blank=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
listings_thumbnail = models.CharField(max_length=400, blank=True, null=True, help_text="Thumbnail to show on listings")
|
|
||||||
|
|
||||||
is_rbac_category = models.BooleanField(default=False, db_index=True, help_text='If access to Category is controlled by role based membership of Groups')
|
|
||||||
|
|
||||||
identity_provider = models.ForeignKey(
|
|
||||||
'socialaccount.SocialApp',
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='categories',
|
|
||||||
help_text='If category is related with a specific Identity Provider',
|
|
||||||
verbose_name='IDP Config Name',
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["title"]
|
|
||||||
verbose_name_plural = "Categories"
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return f"{reverse('search')}?c={self.title}"
|
|
||||||
|
|
||||||
def update_category_media(self):
|
|
||||||
"""Set media_count"""
|
|
||||||
|
|
||||||
# Always set number of Category the total number of media
|
|
||||||
# Depending on how RBAC is set and Permissions etc it is
|
|
||||||
# possible that users won't see all media in a Category
|
|
||||||
# but it's worth to handle this on the UI level
|
|
||||||
# (eg through a message that says that you see only files you have permissions to see)
|
|
||||||
|
|
||||||
self.media_count = Media.objects.filter(category=self).count()
|
|
||||||
self.save(update_fields=["media_count"])
|
|
||||||
|
|
||||||
# OLD logic
|
|
||||||
# if getattr(settings, 'USE_RBAC', False) and self.is_rbac_category:
|
|
||||||
# self.media_count = Media.objects.filter(category=self).count()
|
|
||||||
# else:
|
|
||||||
# self.media_count = Media.objects.filter(listable=True, category=self).count()
|
|
||||||
|
|
||||||
self.save(update_fields=["media_count"])
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def thumbnail_url(self):
|
|
||||||
"""Return thumbnail for category
|
|
||||||
prioritize processed value of listings_thumbnail
|
|
||||||
then thumbnail
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.thumbnail:
|
|
||||||
return helpers.url_from_path(self.thumbnail.path)
|
|
||||||
|
|
||||||
if self.listings_thumbnail:
|
|
||||||
return self.listings_thumbnail
|
|
||||||
|
|
||||||
if Media.objects.filter(category=self, state="public").exists():
|
|
||||||
media = Media.objects.filter(category=self, state="public").order_by("-views").first()
|
|
||||||
if media:
|
|
||||||
return media.thumbnail_url
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
strip_text_items = ["title", "description"]
|
|
||||||
for item in strip_text_items:
|
|
||||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
|
||||||
super(Category, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(models.Model):
|
|
||||||
"""A Tag model"""
|
|
||||||
|
|
||||||
title = models.CharField(max_length=100, unique=True, db_index=True)
|
|
||||||
|
|
||||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
|
|
||||||
|
|
||||||
media_count = models.IntegerField(default=0, help_text="number of media")
|
|
||||||
|
|
||||||
listings_thumbnail = models.CharField(
|
|
||||||
max_length=400,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text="Thumbnail to show on listings",
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["title"]
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return f"{reverse('search')}?t={self.title}"
|
|
||||||
|
|
||||||
def update_tag_media(self):
|
|
||||||
self.media_count = Media.objects.filter(state="public", is_reviewed=True, tags=self).count()
|
|
||||||
self.save(update_fields=["media_count"])
|
|
||||||
return True
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.title = helpers.get_alphanumeric_only(self.title)
|
|
||||||
self.title = self.title[:100]
|
|
||||||
super(Tag, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def thumbnail_url(self):
|
|
||||||
if self.listings_thumbnail:
|
|
||||||
return self.listings_thumbnail
|
|
||||||
media = Media.objects.filter(tags=self, state="public").order_by("-views").first()
|
|
||||||
if media:
|
|
||||||
return media.thumbnail_url
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# Import Media to avoid circular imports
|
|
||||||
from .media import Media # noqa
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import uuid
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.html import strip_tags
|
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
|
||||||
|
|
||||||
|
|
||||||
class Comment(MPTTModel):
|
|
||||||
"""Comments model"""
|
|
||||||
|
|
||||||
add_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, db_index=True, related_name="comments")
|
|
||||||
|
|
||||||
parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")
|
|
||||||
|
|
||||||
text = models.TextField(help_text="text")
|
|
||||||
|
|
||||||
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
|
||||||
|
|
||||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True)
|
|
||||||
|
|
||||||
class MPTTMeta:
|
|
||||||
order_insertion_by = ["add_date"]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"On {self.media.title} by {self.user.username}"
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
strip_text_items = ["text"]
|
|
||||||
for item in strip_text_items:
|
|
||||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
|
||||||
|
|
||||||
if self.text:
|
|
||||||
self.text = self.text[: settings.MAX_CHARS_FOR_COMMENT]
|
|
||||||
|
|
||||||
super(Comment, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return f"{reverse('get_media')}?m={self.media.friendly_token}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_url(self):
|
|
||||||
return self.get_absolute_url()
|
|
||||||
@ -1,303 +0,0 @@
|
|||||||
import json
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.files import File
|
|
||||||
from django.db import models
|
|
||||||
from django.db.models.signals import post_delete, post_save
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from .. import helpers
|
|
||||||
from .utils import (
|
|
||||||
CODECS,
|
|
||||||
ENCODE_EXTENSIONS,
|
|
||||||
ENCODE_RESOLUTIONS,
|
|
||||||
MEDIA_ENCODING_STATUS,
|
|
||||||
encoding_media_file_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EncodeProfile(models.Model):
|
|
||||||
"""Encode Profile model
|
|
||||||
keeps information for each profile
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = models.CharField(max_length=90)
|
|
||||||
|
|
||||||
extension = models.CharField(max_length=10, choices=ENCODE_EXTENSIONS)
|
|
||||||
|
|
||||||
resolution = models.IntegerField(choices=ENCODE_RESOLUTIONS, blank=True, null=True)
|
|
||||||
|
|
||||||
codec = models.CharField(max_length=10, choices=CODECS, blank=True, null=True)
|
|
||||||
|
|
||||||
description = models.TextField(blank=True, help_text="description")
|
|
||||||
|
|
||||||
active = models.BooleanField(default=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["resolution"]
|
|
||||||
|
|
||||||
|
|
||||||
class Encoding(models.Model):
|
|
||||||
"""Encoding Media Instances"""
|
|
||||||
|
|
||||||
add_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
commands = models.TextField(blank=True, help_text="commands run")
|
|
||||||
|
|
||||||
chunk = models.BooleanField(default=False, db_index=True, help_text="is chunk?")
|
|
||||||
|
|
||||||
chunk_file_path = models.CharField(max_length=400, blank=True)
|
|
||||||
|
|
||||||
chunks_info = models.TextField(blank=True)
|
|
||||||
|
|
||||||
logs = models.TextField(blank=True)
|
|
||||||
|
|
||||||
md5sum = models.CharField(max_length=50, blank=True, null=True)
|
|
||||||
|
|
||||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, related_name="encodings")
|
|
||||||
|
|
||||||
media_file = models.FileField("encoding file", upload_to=encoding_media_file_path, blank=True, max_length=500)
|
|
||||||
|
|
||||||
profile = models.ForeignKey(EncodeProfile, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
progress = models.PositiveSmallIntegerField(default=0)
|
|
||||||
|
|
||||||
update_date = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
retries = models.IntegerField(default=0)
|
|
||||||
|
|
||||||
size = models.CharField(max_length=20, blank=True)
|
|
||||||
|
|
||||||
status = models.CharField(max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending")
|
|
||||||
|
|
||||||
temp_file = models.CharField(max_length=400, blank=True)
|
|
||||||
|
|
||||||
task_id = models.CharField(max_length=100, blank=True)
|
|
||||||
|
|
||||||
total_run_time = models.IntegerField(default=0)
|
|
||||||
|
|
||||||
worker = models.CharField(max_length=100, blank=True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_encoding_url(self):
|
|
||||||
if self.media_file:
|
|
||||||
return helpers.url_from_path(self.media_file.path)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_chunk_url(self):
|
|
||||||
if self.chunk_file_path:
|
|
||||||
return helpers.url_from_path(self.chunk_file_path)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
if self.media_file:
|
|
||||||
cmd = ["stat", "-c", "%s", self.media_file.path]
|
|
||||||
stdout = helpers.run_command(cmd).get("out")
|
|
||||||
if stdout:
|
|
||||||
size = int(stdout.strip())
|
|
||||||
self.size = helpers.show_file_size(size)
|
|
||||||
if self.chunk_file_path and not self.md5sum:
|
|
||||||
cmd = ["md5sum", self.chunk_file_path]
|
|
||||||
stdout = helpers.run_command(cmd).get("out")
|
|
||||||
if stdout:
|
|
||||||
md5sum = stdout.strip().split()[0]
|
|
||||||
self.md5sum = md5sum
|
|
||||||
|
|
||||||
super(Encoding, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
def update_size_without_save(self):
|
|
||||||
"""Update the size of an encoding without saving to avoid calling signals"""
|
|
||||||
if self.media_file:
|
|
||||||
cmd = ["stat", "-c", "%s", self.media_file.path]
|
|
||||||
stdout = helpers.run_command(cmd).get("out")
|
|
||||||
if stdout:
|
|
||||||
size = int(stdout.strip())
|
|
||||||
size = helpers.show_file_size(size)
|
|
||||||
Encoding.objects.filter(pk=self.pk).update(size=size)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def set_progress(self, progress, commit=True):
|
|
||||||
if isinstance(progress, int):
|
|
||||||
if 0 <= progress <= 100:
|
|
||||||
self.progress = progress
|
|
||||||
# save object with filter update
|
|
||||||
# to avoid calling signals
|
|
||||||
Encoding.objects.filter(pk=self.pk).update(progress=progress)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.profile.name}-{self.media.title}"
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("api_get_encoding", kwargs={"encoding_id": self.id})
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Encoding)
|
|
||||||
def encoding_file_save(sender, instance, created, **kwargs):
|
|
||||||
"""Performs actions on encoding file delete
|
|
||||||
For example, if encoding is a chunk file, with encoding_status success,
|
|
||||||
perform a check if this is the final chunk file of a media, then
|
|
||||||
concatenate chunks, create final encoding file and delete chunks
|
|
||||||
"""
|
|
||||||
|
|
||||||
if instance.chunk and instance.status == "success":
|
|
||||||
# a chunk got completed
|
|
||||||
|
|
||||||
# check if all chunks are OK
|
|
||||||
# then concatenate to new Encoding - and remove chunks
|
|
||||||
# this should run only once!
|
|
||||||
if instance.media_file:
|
|
||||||
try:
|
|
||||||
orig_chunks = json.loads(instance.chunks_info).keys()
|
|
||||||
except BaseException:
|
|
||||||
instance.delete()
|
|
||||||
return False
|
|
||||||
|
|
||||||
chunks = Encoding.objects.filter(
|
|
||||||
media=instance.media,
|
|
||||||
profile=instance.profile,
|
|
||||||
chunks_info=instance.chunks_info,
|
|
||||||
chunk=True,
|
|
||||||
).order_by("add_date")
|
|
||||||
|
|
||||||
complete = True
|
|
||||||
|
|
||||||
# perform validation, make sure everything is there
|
|
||||||
for chunk in orig_chunks:
|
|
||||||
if not chunks.filter(chunk_file_path=chunk):
|
|
||||||
complete = False
|
|
||||||
break
|
|
||||||
|
|
||||||
for chunk in chunks:
|
|
||||||
if not (chunk.media_file and chunk.media_file.path):
|
|
||||||
complete = False
|
|
||||||
break
|
|
||||||
|
|
||||||
if complete:
|
|
||||||
# concatenate chunks and create final encoding file
|
|
||||||
chunks_paths = [f.media_file.path for f in chunks]
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
|
||||||
seg_file = helpers.create_temp_file(suffix=".txt", dir=temp_dir)
|
|
||||||
tf = helpers.create_temp_file(suffix=f".{instance.profile.extension}", dir=temp_dir)
|
|
||||||
with open(seg_file, "w") as ff:
|
|
||||||
for f in chunks_paths:
|
|
||||||
ff.write(f"file {f}\n")
|
|
||||||
cmd = [
|
|
||||||
settings.FFMPEG_COMMAND,
|
|
||||||
"-y",
|
|
||||||
"-f",
|
|
||||||
"concat",
|
|
||||||
"-safe",
|
|
||||||
"0",
|
|
||||||
"-i",
|
|
||||||
seg_file,
|
|
||||||
"-c",
|
|
||||||
"copy",
|
|
||||||
"-pix_fmt",
|
|
||||||
"yuv420p",
|
|
||||||
"-movflags",
|
|
||||||
"faststart",
|
|
||||||
tf,
|
|
||||||
]
|
|
||||||
stdout = helpers.run_command(cmd)
|
|
||||||
|
|
||||||
encoding = Encoding(
|
|
||||||
media=instance.media,
|
|
||||||
profile=instance.profile,
|
|
||||||
status="success",
|
|
||||||
progress=100,
|
|
||||||
)
|
|
||||||
all_logs = "\n".join([st.logs for st in chunks])
|
|
||||||
encoding.logs = f"{chunks_paths}\n{stdout}\n{all_logs}"
|
|
||||||
workers = list(set([st.worker for st in chunks]))
|
|
||||||
encoding.worker = json.dumps({"workers": workers})
|
|
||||||
|
|
||||||
start_date = min([st.add_date for st in chunks])
|
|
||||||
end_date = max([st.update_date for st in chunks])
|
|
||||||
encoding.total_run_time = (end_date - start_date).seconds
|
|
||||||
encoding.save()
|
|
||||||
|
|
||||||
with open(tf, "rb") as f:
|
|
||||||
myfile = File(f)
|
|
||||||
output_name = f"{helpers.get_file_name(instance.media.media_file.path)}.{instance.profile.extension}"
|
|
||||||
encoding.media_file.save(content=myfile, name=output_name)
|
|
||||||
|
|
||||||
# encoding is saved, deleting chunks
|
|
||||||
# and any other encoding that might exist
|
|
||||||
# first perform one last validation
|
|
||||||
# to avoid that this is run twice
|
|
||||||
if (
|
|
||||||
len(orig_chunks)
|
|
||||||
== Encoding.objects.filter( # noqa
|
|
||||||
media=instance.media,
|
|
||||||
profile=instance.profile,
|
|
||||||
chunks_info=instance.chunks_info,
|
|
||||||
).count()
|
|
||||||
):
|
|
||||||
# if two chunks are finished at the same time, this
|
|
||||||
# will be changed
|
|
||||||
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
|
||||||
who.delete()
|
|
||||||
else:
|
|
||||||
encoding.delete()
|
|
||||||
if not Encoding.objects.filter(chunks_info=instance.chunks_info):
|
|
||||||
# TODO: in case of remote workers, files should be deleted
|
|
||||||
# example
|
|
||||||
# for worker in workers:
|
|
||||||
# for chunk in json.loads(instance.chunks_info).keys():
|
|
||||||
# remove_media_file.delay(media_file=chunk)
|
|
||||||
for chunk in json.loads(instance.chunks_info).keys():
|
|
||||||
helpers.rm_file(chunk)
|
|
||||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
|
||||||
|
|
||||||
elif instance.chunk and instance.status == "fail":
|
|
||||||
encoding = Encoding(media=instance.media, profile=instance.profile, status="fail", progress=100)
|
|
||||||
|
|
||||||
chunks = Encoding.objects.filter(media=instance.media, chunks_info=instance.chunks_info, chunk=True).order_by("add_date")
|
|
||||||
|
|
||||||
chunks_paths = [f.media_file.path for f in chunks]
|
|
||||||
|
|
||||||
all_logs = "\n".join([st.logs for st in chunks])
|
|
||||||
encoding.logs = f"{chunks_paths}\n{all_logs}"
|
|
||||||
workers = list(set([st.worker for st in chunks]))
|
|
||||||
encoding.worker = json.dumps({"workers": workers})
|
|
||||||
start_date = min([st.add_date for st in chunks])
|
|
||||||
end_date = max([st.update_date for st in chunks])
|
|
||||||
encoding.total_run_time = (end_date - start_date).seconds
|
|
||||||
encoding.save()
|
|
||||||
|
|
||||||
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
|
|
||||||
|
|
||||||
who.delete()
|
|
||||||
# TODO: merge with above if, do not repeat code
|
|
||||||
else:
|
|
||||||
if instance.status in ["fail", "success"]:
|
|
||||||
instance.media.post_encode_actions(encoding=instance, action="add")
|
|
||||||
|
|
||||||
encodings = set([encoding.status for encoding in Encoding.objects.filter(media=instance.media)])
|
|
||||||
if ("running" in encodings) or ("pending" in encodings):
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=Encoding)
|
|
||||||
def encoding_file_delete(sender, instance, **kwargs):
|
|
||||||
"""
|
|
||||||
Deletes file from filesystem
|
|
||||||
when corresponding `Encoding` object is deleted.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if instance.media_file:
|
|
||||||
helpers.rm_file(instance.media_file.path)
|
|
||||||
if not instance.chunk:
|
|
||||||
instance.media.post_encode_actions(encoding=instance, action="delete")
|
|
||||||
# delete local chunks, and remote chunks + media file. Only when the
|
|
||||||
# last encoding of a media is complete
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class License(models.Model):
|
|
||||||
"""A Base license model to be used in Media"""
|
|
||||||
|
|
||||||
title = models.CharField(max_length=100, unique=True)
|
|
||||||
description = models.TextField(blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class Page(models.Model):
|
|
||||||
slug = models.SlugField(max_length=200, unique=True)
|
|
||||||
title = models.CharField(max_length=200)
|
|
||||||
description = models.TextField(blank=True)
|
|
||||||
add_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
edit_date = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("get_page", args=[str(self.slug)])
|
|
||||||
|
|
||||||
|
|
||||||
class TinyMCEMedia(models.Model):
|
|
||||||
file = models.FileField(upload_to='tinymce_media/')
|
|
||||||
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
file_type = models.CharField(
|
|
||||||
max_length=10,
|
|
||||||
choices=(
|
|
||||||
('image', 'Image'),
|
|
||||||
('media', 'Media'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
original_filename = models.CharField(max_length=255)
|
|
||||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, null=True, blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = 'TinyMCE Media'
|
|
||||||
verbose_name_plural = 'TinyMCE Media'
|
|
||||||
ordering = ['-uploaded_at']
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.original_filename} ({self.file_type})"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url(self):
|
|
||||||
return self.file.url
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
import uuid
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.html import strip_tags
|
|
||||||
|
|
||||||
from .. import helpers
|
|
||||||
|
|
||||||
|
|
||||||
class Playlist(models.Model):
|
|
||||||
"""Playlists model"""
|
|
||||||
|
|
||||||
add_date = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
||||||
|
|
||||||
description = models.TextField(blank=True, help_text="description")
|
|
||||||
|
|
||||||
friendly_token = models.CharField(blank=True, max_length=12, db_index=True)
|
|
||||||
|
|
||||||
media = models.ManyToManyField("Media", through="playlistmedia", blank=True)
|
|
||||||
|
|
||||||
title = models.CharField(max_length=100, db_index=True)
|
|
||||||
|
|
||||||
uid = models.UUIDField(unique=True, default=uuid.uuid4)
|
|
||||||
|
|
||||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True, related_name="playlists")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_count(self):
|
|
||||||
return self.media.filter(listable=True).count()
|
|
||||||
|
|
||||||
def get_absolute_url(self, api=False):
|
|
||||||
if api:
|
|
||||||
return reverse("api_get_playlist", kwargs={"friendly_token": self.friendly_token})
|
|
||||||
else:
|
|
||||||
return reverse("get_playlist", kwargs={"friendly_token": self.friendly_token})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url(self):
|
|
||||||
return self.get_absolute_url()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def api_url(self):
|
|
||||||
return self.get_absolute_url(api=True)
|
|
||||||
|
|
||||||
def user_thumbnail_url(self):
|
|
||||||
if self.user.logo:
|
|
||||||
return helpers.url_from_path(self.user.logo.path)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def set_ordering(self, media, ordering):
|
|
||||||
if media not in self.media.all():
|
|
||||||
return False
|
|
||||||
pm = PlaylistMedia.objects.filter(playlist=self, media=media).first()
|
|
||||||
if pm and isinstance(ordering, int) and 0 < ordering:
|
|
||||||
pm.ordering = ordering
|
|
||||||
pm.save()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
strip_text_items = ["title", "description"]
|
|
||||||
for item in strip_text_items:
|
|
||||||
setattr(self, item, strip_tags(getattr(self, item, None)))
|
|
||||||
self.title = self.title[:100]
|
|
||||||
|
|
||||||
if not self.friendly_token:
|
|
||||||
while True:
|
|
||||||
friendly_token = helpers.produce_friendly_token()
|
|
||||||
if not Playlist.objects.filter(friendly_token=friendly_token):
|
|
||||||
self.friendly_token = friendly_token
|
|
||||||
break
|
|
||||||
super(Playlist, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def thumbnail_url(self):
|
|
||||||
pm = self.playlistmedia_set.filter(media__listable=True).first()
|
|
||||||
if pm and pm.media.thumbnail:
|
|
||||||
return helpers.url_from_path(pm.media.thumbnail.path)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class PlaylistMedia(models.Model):
|
|
||||||
"""Helper model to store playlist specific media"""
|
|
||||||
|
|
||||||
action_date = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
media = models.ForeignKey("Media", on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
ordering = models.IntegerField(default=1)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["ordering", "-action_date"]
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
from .utils import validate_rating
|
|
||||||
|
|
||||||
|
|
||||||
class RatingCategory(models.Model):
|
|
||||||
"""Rating Category
|
|
||||||
Facilitate user ratings.
|
|
||||||
One or more rating categories per Category can exist
|
|
||||||
will be shown to the media if they are enabled
|
|
||||||
"""
|
|
||||||
|
|
||||||
description = models.TextField(blank=True)
|
|
||||||
|
|
||||||
enabled = models.BooleanField(default=True)
|
|
||||||
|
|
||||||
title = models.CharField(max_length=200, unique=True, db_index=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name_plural = "Rating Categories"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.title}"
|
|
||||||
|
|
||||||
|
|
||||||
class Rating(models.Model):
|
|
||||||
"""User Rating"""
|
|
||||||
|
|
||||||
add_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, related_name="ratings")
|
|
||||||
|
|
||||||
rating_category = models.ForeignKey(RatingCategory, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
score = models.IntegerField(validators=[validate_rating])
|
|
||||||
|
|
||||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name_plural = "Ratings"
|
|
||||||
indexes = [
|
|
||||||
models.Index(fields=["user", "media"]),
|
|
||||||
]
|
|
||||||
unique_together = ("user", "media", "rating_category")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.user.username}, rate for {self.media.title} for category {self.rating_category.title}"
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
import os
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from .. import helpers
|
|
||||||
from .utils import MEDIA_ENCODING_STATUS, subtitles_file_path
|
|
||||||
|
|
||||||
|
|
||||||
class Language(models.Model):
|
|
||||||
"""Language model
|
|
||||||
to be used with Subtitles
|
|
||||||
"""
|
|
||||||
|
|
||||||
code = models.CharField(max_length=30, help_text="language code")
|
|
||||||
|
|
||||||
title = models.CharField(max_length=100, help_text="language code")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["id"]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.code}-{self.title}"
|
|
||||||
|
|
||||||
|
|
||||||
class Subtitle(models.Model):
|
|
||||||
"""Subtitles model"""
|
|
||||||
|
|
||||||
language = models.ForeignKey(Language, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, related_name="subtitles")
|
|
||||||
|
|
||||||
subtitle_file = models.FileField(
|
|
||||||
"Subtitle/CC file",
|
|
||||||
help_text="File has to be WebVTT format",
|
|
||||||
upload_to=subtitles_file_path,
|
|
||||||
max_length=500,
|
|
||||||
)
|
|
||||||
|
|
||||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["language__title"]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.media.title}-{self.language.title}"
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return f"{reverse('edit_subtitle')}?id={self.id}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url(self):
|
|
||||||
return self.get_absolute_url()
|
|
||||||
|
|
||||||
def convert_to_srt(self):
|
|
||||||
input_path = self.subtitle_file.path
|
|
||||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as tmpdirname:
|
|
||||||
pysub = settings.PYSUBS_COMMAND
|
|
||||||
|
|
||||||
cmd = [pysub, input_path, "--to", "vtt", "-o", tmpdirname]
|
|
||||||
stdout = helpers.run_command(cmd)
|
|
||||||
|
|
||||||
list_of_files = os.listdir(tmpdirname)
|
|
||||||
if list_of_files:
|
|
||||||
subtitles_file = os.path.join(tmpdirname, list_of_files[0])
|
|
||||||
cmd = ["cp", subtitles_file, input_path]
|
|
||||||
stdout = helpers.run_command(cmd) # noqa
|
|
||||||
else:
|
|
||||||
raise Exception("Could not convert to srt")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class TranscriptionRequest(models.Model):
|
|
||||||
# Whisper transcription request
|
|
||||||
media = models.ForeignKey("Media", on_delete=models.CASCADE, related_name="transcriptionrequests")
|
|
||||||
add_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
status = models.CharField(max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending", db_index=True)
|
|
||||||
translate_to_english = models.BooleanField(default=False)
|
|
||||||
logs = models.TextField(blank=True, null=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Transcription request for {self.media.title} - {self.status}"
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.crypto import get_random_string
|
|
||||||
|
|
||||||
from .. import helpers
|
|
||||||
|
|
||||||
# this is used by Media and Encoding models
|
|
||||||
# reflects media encoding status for objects
|
|
||||||
MEDIA_ENCODING_STATUS = (
|
|
||||||
("pending", "Pending"),
|
|
||||||
("running", "Running"),
|
|
||||||
("fail", "Fail"),
|
|
||||||
("success", "Success"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# the media state of a Media object
|
|
||||||
# this is set by default according to the portal workflow
|
|
||||||
MEDIA_STATES = (
|
|
||||||
("private", "Private"),
|
|
||||||
("public", "Public"),
|
|
||||||
("unlisted", "Unlisted"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# each uploaded Media gets a media_type hint
|
|
||||||
# by helpers.get_file_type
|
|
||||||
|
|
||||||
MEDIA_TYPES_SUPPORTED = (
|
|
||||||
("video", "Video"),
|
|
||||||
("image", "Image"),
|
|
||||||
("pdf", "Pdf"),
|
|
||||||
("audio", "Audio"),
|
|
||||||
)
|
|
||||||
|
|
||||||
ENCODE_EXTENSIONS = (
|
|
||||||
("mp4", "mp4"),
|
|
||||||
("webm", "webm"),
|
|
||||||
("gif", "gif"),
|
|
||||||
)
|
|
||||||
|
|
||||||
ENCODE_RESOLUTIONS = (
|
|
||||||
(2160, "2160"),
|
|
||||||
(1440, "1440"),
|
|
||||||
(1080, "1080"),
|
|
||||||
(720, "720"),
|
|
||||||
(480, "480"),
|
|
||||||
(360, "360"),
|
|
||||||
(240, "240"),
|
|
||||||
(144, "144"),
|
|
||||||
)
|
|
||||||
|
|
||||||
CODECS = (
|
|
||||||
("h265", "h265"),
|
|
||||||
("h264", "h264"),
|
|
||||||
("vp9", "vp9"),
|
|
||||||
)
|
|
||||||
|
|
||||||
ENCODE_EXTENSIONS_KEYS = [extension for extension, name in ENCODE_EXTENSIONS]
|
|
||||||
ENCODE_RESOLUTIONS_KEYS = [resolution for resolution, name in ENCODE_RESOLUTIONS]
|
|
||||||
|
|
||||||
|
|
||||||
def generate_uid():
|
|
||||||
return get_random_string(length=16)
|
|
||||||
|
|
||||||
|
|
||||||
def original_media_file_path(instance, filename):
|
|
||||||
"""Helper function to place original media file"""
|
|
||||||
file_name = f"{instance.uid.hex}.{helpers.get_file_name(filename)}"
|
|
||||||
return settings.MEDIA_UPLOAD_DIR + f"user/{instance.user.username}/{file_name}"
|
|
||||||
|
|
||||||
|
|
||||||
def encoding_media_file_path(instance, filename):
|
|
||||||
"""Helper function to place encoded media file"""
|
|
||||||
|
|
||||||
file_name = f"{instance.media.uid.hex}.{helpers.get_file_name(filename)}"
|
|
||||||
return settings.MEDIA_ENCODING_DIR + f"{instance.profile.id}/{instance.media.user.username}/{file_name}"
|
|
||||||
|
|
||||||
|
|
||||||
def original_thumbnail_file_path(instance, filename):
|
|
||||||
"""Helper function to place original media thumbnail file"""
|
|
||||||
|
|
||||||
return settings.THUMBNAIL_UPLOAD_DIR + f"user/{instance.user.username}/{filename}"
|
|
||||||
|
|
||||||
|
|
||||||
def subtitles_file_path(instance, filename):
|
|
||||||
"""Helper function to place subtitle file"""
|
|
||||||
|
|
||||||
return settings.SUBTITLES_UPLOAD_DIR + f"user/{instance.media.user.username}/{filename}"
|
|
||||||
|
|
||||||
|
|
||||||
def category_thumb_path(instance, filename):
|
|
||||||
"""Helper function to place category thumbnail file"""
|
|
||||||
|
|
||||||
file_name = f"{instance.uid}.{helpers.get_file_name(filename)}"
|
|
||||||
return settings.MEDIA_UPLOAD_DIR + f"categories/{file_name}"
|
|
||||||
|
|
||||||
|
|
||||||
def validate_rating(value):
|
|
||||||
if -1 >= value or value > 5:
|
|
||||||
raise ValidationError("score has to be between 0 and 5")
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from django.db.models.signals import post_delete
|
|
||||||
from django.dispatch import receiver
|
|
||||||
|
|
||||||
from .. import helpers
|
|
||||||
|
|
||||||
|
|
||||||
class VideoChapterData(models.Model):
|
|
||||||
data = models.JSONField(null=False, blank=False, help_text="Chapter data")
|
|
||||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='chapters')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = ['media']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def chapter_data(self):
|
|
||||||
# ensure response is consistent
|
|
||||||
data = []
|
|
||||||
if self.data and isinstance(self.data, list):
|
|
||||||
for item in self.data:
|
|
||||||
if item.get("startTime") and item.get("endTime") and item.get("chapterTitle"):
|
|
||||||
chapter_item = {
|
|
||||||
'startTime': item.get("startTime"),
|
|
||||||
'endTime': item.get("endTime"),
|
|
||||||
'chapterTitle': item.get("chapterTitle"),
|
|
||||||
}
|
|
||||||
data.append(chapter_item)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class VideoTrimRequest(models.Model):
|
|
||||||
"""Model to handle video trimming requests"""
|
|
||||||
|
|
||||||
VIDEO_TRIM_STATUS = (
|
|
||||||
("initial", "Initial"),
|
|
||||||
("running", "Running"),
|
|
||||||
("success", "Success"),
|
|
||||||
("fail", "Fail"),
|
|
||||||
)
|
|
||||||
|
|
||||||
VIDEO_ACTION_CHOICES = (
|
|
||||||
("replace", "Replace Original"),
|
|
||||||
("save_new", "Save as New"),
|
|
||||||
("create_segments", "Create Segments"),
|
|
||||||
)
|
|
||||||
|
|
||||||
TRIM_STYLE_CHOICES = (
|
|
||||||
("no_encoding", "No Encoding"),
|
|
||||||
("precise", "Precise"),
|
|
||||||
)
|
|
||||||
|
|
||||||
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='trim_requests')
|
|
||||||
status = models.CharField(max_length=20, choices=VIDEO_TRIM_STATUS, default="initial")
|
|
||||||
add_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
video_action = models.CharField(max_length=20, choices=VIDEO_ACTION_CHOICES)
|
|
||||||
media_trim_style = models.CharField(max_length=20, choices=TRIM_STYLE_CHOICES, default="no_encoding")
|
|
||||||
timestamps = models.JSONField(null=False, blank=False, help_text="Timestamps for trimming")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Trim request for {self.media.title} ({self.status})"
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=VideoChapterData)
|
|
||||||
def videochapterdata_delete(sender, instance, **kwargs):
|
|
||||||
helpers.rm_dir(instance.media.video_chapters_folder)
|
|
||||||
@ -1,7 +1,5 @@
|
|||||||
from django.conf import settings
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .methods import is_mediacms_editor
|
|
||||||
from .models import Category, Comment, EncodeProfile, Media, Playlist, Tag
|
from .models import Category, Comment, EncodeProfile, Media, Playlist, Tag
|
||||||
|
|
||||||
# TODO: put them in a more DRY way
|
# TODO: put them in a more DRY way
|
||||||
@ -78,25 +76,8 @@ class MediaSerializer(serializers.ModelSerializer):
|
|||||||
"featured",
|
"featured",
|
||||||
"user_featured",
|
"user_featured",
|
||||||
"size",
|
"size",
|
||||||
# "category",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
request = self.context.get('request')
|
|
||||||
|
|
||||||
if False and request and 'category' in self.fields:
|
|
||||||
# this is not working
|
|
||||||
user = request.user
|
|
||||||
if is_mediacms_editor(user):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if getattr(settings, 'USE_RBAC', False):
|
|
||||||
# Filter category queryset based on user permissions
|
|
||||||
non_rbac_categories = Category.objects.filter(is_rbac_category=False)
|
|
||||||
rbac_categories = user.get_rbac_categories_as_contributor()
|
|
||||||
self.fields['category'].queryset = non_rbac_categories.union(rbac_categories)
|
|
||||||
|
|
||||||
|
|
||||||
class SingleMediaSerializer(serializers.ModelSerializer):
|
class SingleMediaSerializer(serializers.ModelSerializer):
|
||||||
user = serializers.ReadOnlyField(source="user.username")
|
user = serializers.ReadOnlyField(source="user.username")
|
||||||
@ -161,11 +142,9 @@ class SingleMediaSerializer(serializers.ModelSerializer):
|
|||||||
"hls_info",
|
"hls_info",
|
||||||
"license",
|
"license",
|
||||||
"subtitles_info",
|
"subtitles_info",
|
||||||
"chapter_data",
|
|
||||||
"ratings_info",
|
"ratings_info",
|
||||||
"add_subtitle_url",
|
"add_subtitle_url",
|
||||||
"allow_download",
|
"allow_download",
|
||||||
"slideshow_items",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
418
files/tasks.py
418
files/tasks.py
@ -2,11 +2,13 @@ import json
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from celery import Task
|
from celery import Task
|
||||||
from celery import shared_task as task
|
from celery import shared_task as task
|
||||||
|
from celery.exceptions import SoftTimeLimitExceeded
|
||||||
from celery.signals import task_revoked
|
from celery.signals import task_revoked
|
||||||
|
|
||||||
# from celery.task.control import revoke
|
# from celery.task.control import revoke
|
||||||
@ -14,7 +16,6 @@ from celery.utils.log import get_task_logger
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import DatabaseError
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from actions.models import USER_MEDIA_ACTIONS, MediaAction
|
from actions.models import USER_MEDIA_ACTIONS, MediaAction
|
||||||
@ -27,33 +28,14 @@ from .helpers import (
|
|||||||
create_temp_file,
|
create_temp_file,
|
||||||
get_file_name,
|
get_file_name,
|
||||||
get_file_type,
|
get_file_type,
|
||||||
get_trim_timestamps,
|
|
||||||
media_file_info,
|
media_file_info,
|
||||||
produce_ffmpeg_commands,
|
produce_ffmpeg_commands,
|
||||||
produce_friendly_token,
|
produce_friendly_token,
|
||||||
rm_file,
|
rm_file,
|
||||||
run_command,
|
run_command,
|
||||||
trim_video_method,
|
|
||||||
)
|
|
||||||
from .methods import (
|
|
||||||
copy_video,
|
|
||||||
kill_ffmpeg_process,
|
|
||||||
list_tasks,
|
|
||||||
notify_users,
|
|
||||||
pre_save_action,
|
|
||||||
)
|
|
||||||
from .models import (
|
|
||||||
Category,
|
|
||||||
EncodeProfile,
|
|
||||||
Encoding,
|
|
||||||
Language,
|
|
||||||
Media,
|
|
||||||
Rating,
|
|
||||||
Subtitle,
|
|
||||||
Tag,
|
|
||||||
TranscriptionRequest,
|
|
||||||
VideoTrimRequest,
|
|
||||||
)
|
)
|
||||||
|
from .methods import list_tasks, notify_users, pre_save_action
|
||||||
|
from .models import Category, EncodeProfile, Encoding, Media, Rating, Tag
|
||||||
|
|
||||||
logger = get_task_logger(__name__)
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
@ -66,70 +48,7 @@ ERRORS_LIST = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def handle_pending_running_encodings(media):
|
@task(name="chunkize_media", bind=True, queue="short_tasks", soft_time_limit=60 * 30)
|
||||||
"""Handle pending and running encodings for a media object.
|
|
||||||
|
|
||||||
we are trimming the original file. If there are encodings in success state, this means that the encoding has run
|
|
||||||
and has succeeded, so we can keep them (they will be trimmed) or if we dont keep them we dont have to delete them
|
|
||||||
here
|
|
||||||
|
|
||||||
However for encodings that are in pending or running phase, just delete them
|
|
||||||
|
|
||||||
Args:
|
|
||||||
media: The media object to handle encodings for
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if any encodings were deleted, False otherwise
|
|
||||||
"""
|
|
||||||
encodings = media.encodings.exclude(status="success")
|
|
||||||
deleted = False
|
|
||||||
for encoding in encodings:
|
|
||||||
if encoding.temp_file:
|
|
||||||
kill_ffmpeg_process(encoding.temp_file)
|
|
||||||
if encoding.chunk_file_path:
|
|
||||||
kill_ffmpeg_process(encoding.chunk_file_path)
|
|
||||||
deleted = True
|
|
||||||
encoding.delete()
|
|
||||||
|
|
||||||
return deleted
|
|
||||||
|
|
||||||
|
|
||||||
def pre_trim_video_actions(media):
|
|
||||||
# the reason for this function is to perform tasks before trimming a video
|
|
||||||
|
|
||||||
# avoid re-running unnecessary encodings (or chunkize_media, which is the first step for them)
|
|
||||||
# if the video is already completed
|
|
||||||
# however if it is a new video (user uploded the video and starts trimming
|
|
||||||
# before the video is processed), this is necessary, so encode has to be called to give it a chance to encode
|
|
||||||
|
|
||||||
# if a video is fully processed (all encodings are success), or if a video is new, then things are clear
|
|
||||||
|
|
||||||
# HOWEVER there is a race condition and this is that some encodings are success and some are pending/running
|
|
||||||
# Since we are making speed cutting, we will perform an ffmpeg -c copy on all of them and the result will be
|
|
||||||
# that they will end up differently cut, because ffmpeg checks for I-frames
|
|
||||||
# The result is fine if playing the video but is bad in case of HLS
|
|
||||||
# So we need to delete all encodings inevitably to produce same results, if there are some that are success and some that
|
|
||||||
# are still not finished.
|
|
||||||
|
|
||||||
profiles = EncodeProfile.objects.filter(active=True, extension='mp4', resolution__lte=media.video_height)
|
|
||||||
media_encodings = EncodeProfile.objects.filter(encoding__in=media.encodings.filter(status="success", chunk=False), extension='mp4').distinct()
|
|
||||||
|
|
||||||
picked = []
|
|
||||||
for profile in profiles:
|
|
||||||
if profile in media_encodings:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
picked.append(profile)
|
|
||||||
|
|
||||||
if picked:
|
|
||||||
# by calling encode will re-encode all. The logic is explained above...
|
|
||||||
logger.info(f"Encoding media {media.friendly_token} will have to be performed for all profiles")
|
|
||||||
media.encode()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@task(name="chunkize_media", bind=True, queue="short_tasks", soft_time_limit=60 * 30 * 4)
|
|
||||||
def chunkize_media(self, friendly_token, profiles, force=True):
|
def chunkize_media(self, friendly_token, profiles, force=True):
|
||||||
"""Break media in chunks and start encoding tasks"""
|
"""Break media in chunks and start encoding tasks"""
|
||||||
|
|
||||||
@ -138,8 +57,8 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
|||||||
cwd = os.path.dirname(os.path.realpath(media.media_file.path))
|
cwd = os.path.dirname(os.path.realpath(media.media_file.path))
|
||||||
file_name = media.media_file.path.split("/")[-1]
|
file_name = media.media_file.path.split("/")[-1]
|
||||||
random_prefix = produce_friendly_token()
|
random_prefix = produce_friendly_token()
|
||||||
file_format = f"{random_prefix}_{file_name}"
|
file_format = "{0}_{1}".format(random_prefix, file_name)
|
||||||
chunks_file_name = f"%02d_{file_format}"
|
chunks_file_name = "%02d_{0}".format(file_format)
|
||||||
chunks_file_name += ".mkv"
|
chunks_file_name += ".mkv"
|
||||||
cmd = [
|
cmd = [
|
||||||
settings.FFMPEG_COMMAND,
|
settings.FFMPEG_COMMAND,
|
||||||
@ -164,7 +83,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
|||||||
chunks.append(ch[0])
|
chunks.append(ch[0])
|
||||||
if not chunks:
|
if not chunks:
|
||||||
# command completely failed to segment file.putting to normal encode
|
# command completely failed to segment file.putting to normal encode
|
||||||
logger.info(f"Failed to break file {friendly_token} in chunks. Putting to normal encode queue")
|
logger.info("Failed to break file {0} in chunks." " Putting to normal encode queue".format(friendly_token))
|
||||||
for profile in profiles:
|
for profile in profiles:
|
||||||
if media.video_height and media.video_height < profile.resolution:
|
if media.video_height and media.video_height < profile.resolution:
|
||||||
if profile.resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
if profile.resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
||||||
@ -213,7 +132,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
|||||||
priority=priority,
|
priority=priority,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"got {len(chunks)} chunks and will encode to {to_profiles} profiles")
|
logger.info("got {0} chunks and will encode to {1} profiles".format(len(chunks), to_profiles))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -226,7 +145,6 @@ class EncodingTask(Task):
|
|||||||
self.encoding.status = "fail"
|
self.encoding.status = "fail"
|
||||||
self.encoding.save(update_fields=["status"])
|
self.encoding.save(update_fields=["status"])
|
||||||
kill_ffmpeg_process(self.encoding.temp_file)
|
kill_ffmpeg_process(self.encoding.temp_file)
|
||||||
kill_ffmpeg_process(self.encoding.chunk_file_path)
|
|
||||||
if hasattr(self.encoding, "media"):
|
if hasattr(self.encoding, "media"):
|
||||||
self.encoding.media.post_encode_actions()
|
self.encoding.media.post_encode_actions()
|
||||||
except BaseException:
|
except BaseException:
|
||||||
@ -253,13 +171,7 @@ def encode_media(
|
|||||||
):
|
):
|
||||||
"""Encode a media to given profile, using ffmpeg, storing progress"""
|
"""Encode a media to given profile, using ffmpeg, storing progress"""
|
||||||
|
|
||||||
logger.info(f"encode_media for {friendly_token}/{profile_id}/{encoding_id}/{force}/{chunk}")
|
logger.info("Encode Media started, friendly token {0}, profile id {1}, force {2}".format(friendly_token, profile_id, force))
|
||||||
# TODO: this is new behavior, check whether it performs well. Before that check it would end up saving the Encoding
|
|
||||||
# at some point below. Now it exits the task. Could it be that before it would give it a chance to re-run? Or it was
|
|
||||||
# not being used at all?
|
|
||||||
if not Encoding.objects.filter(id=encoding_id).exists():
|
|
||||||
logger.info(f"Exiting for {friendly_token}/{profile_id}/{encoding_id}/{force} since encoding id not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.request.id:
|
if self.request.id:
|
||||||
task_id = self.request.id
|
task_id = self.request.id
|
||||||
@ -357,8 +269,8 @@ def encode_media(
|
|||||||
# return False
|
# return False
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
||||||
tf = create_temp_file(suffix=f".{profile.extension}", dir=temp_dir)
|
tf = create_temp_file(suffix=".{0}".format(profile.extension), dir=temp_dir)
|
||||||
tfpass = create_temp_file(suffix=f".{profile.extension}", dir=temp_dir)
|
tfpass = create_temp_file(suffix=".{0}".format(profile.extension), dir=temp_dir)
|
||||||
ffmpeg_commands = produce_ffmpeg_commands(
|
ffmpeg_commands = produce_ffmpeg_commands(
|
||||||
original_media_path,
|
original_media_path,
|
||||||
media.media_info,
|
media.media_info,
|
||||||
@ -399,37 +311,28 @@ def encode_media(
|
|||||||
percent = duration * 100 / media.duration
|
percent = duration * 100 / media.duration
|
||||||
if n_times % 60 == 0:
|
if n_times % 60 == 0:
|
||||||
encoding.progress = percent
|
encoding.progress = percent
|
||||||
|
try:
|
||||||
encoding.save(update_fields=["progress", "update_date"])
|
encoding.save(update_fields=["progress", "update_date"])
|
||||||
logger.info(f"Saved {round(percent, 2)}")
|
logger.info("Saved {0}".format(round(percent, 2)))
|
||||||
|
except BaseException:
|
||||||
|
pass
|
||||||
n_times += 1
|
n_times += 1
|
||||||
except DatabaseError:
|
|
||||||
# primary reason for this is that the encoding has been deleted, because
|
|
||||||
# the media file was deleted, or also that there was a trim video request
|
|
||||||
# so it would be redundant to let it complete the encoding
|
|
||||||
kill_ffmpeg_process(encoding.temp_file)
|
|
||||||
kill_ffmpeg_process(encoding.chunk_file_path)
|
|
||||||
return False
|
|
||||||
|
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
break
|
break
|
||||||
except VideoEncodingError:
|
except VideoEncodingError:
|
||||||
# ffmpeg error, or ffmpeg was killed
|
# ffmpeg error, or ffmpeg was killed
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
try:
|
try:
|
||||||
# output is empty, fail message is on the exception
|
# output is empty, fail message is on the exception
|
||||||
output = e.message
|
output = e.message
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
output = ""
|
output = ""
|
||||||
|
if isinstance(e, SoftTimeLimitExceeded):
|
||||||
kill_ffmpeg_process(encoding.temp_file)
|
kill_ffmpeg_process(encoding.temp_file)
|
||||||
kill_ffmpeg_process(encoding.chunk_file_path)
|
|
||||||
encoding.logs = output
|
encoding.logs = output
|
||||||
encoding.status = "fail"
|
encoding.status = "fail"
|
||||||
try:
|
|
||||||
encoding.save(update_fields=["status", "logs"])
|
encoding.save(update_fields=["status", "logs"])
|
||||||
except DatabaseError:
|
|
||||||
return False
|
|
||||||
raise_exception = True
|
raise_exception = True
|
||||||
# if this is an ffmpeg's valid error
|
# if this is an ffmpeg's valid error
|
||||||
# no need for the task to be re-run
|
# no need for the task to be re-run
|
||||||
@ -453,7 +356,7 @@ def encode_media(
|
|||||||
|
|
||||||
with open(tf, "rb") as f:
|
with open(tf, "rb") as f:
|
||||||
myfile = File(f)
|
myfile = File(f)
|
||||||
output_name = f"{get_file_name(original_media_path)}.{profile.extension}"
|
output_name = "{0}.{1}".format(get_file_name(original_media_path), profile.extension)
|
||||||
encoding.media_file.save(content=myfile, name=output_name)
|
encoding.media_file.save(content=myfile, name=output_name)
|
||||||
encoding.total_run_time = (encoding.update_date - encoding.add_date).seconds
|
encoding.total_run_time = (encoding.update_date - encoding.add_date).seconds
|
||||||
|
|
||||||
@ -467,67 +370,6 @@ def encode_media(
|
|||||||
return success
|
return success
|
||||||
|
|
||||||
|
|
||||||
@task(name="whisper_transcribe", queue="long_tasks", soft_time_limit=60 * 60 * 2)
|
|
||||||
def whisper_transcribe(friendly_token, translate_to_english=False):
|
|
||||||
try:
|
|
||||||
media = Media.objects.get(friendly_token=friendly_token)
|
|
||||||
except: # noqa
|
|
||||||
logger.info(f"failed to get media {friendly_token}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
request = TranscriptionRequest.objects.filter(media=media, status="pending", translate_to_english=translate_to_english).first()
|
|
||||||
if not request:
|
|
||||||
logger.info(f"No pending transcription request for media {friendly_token}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if translate_to_english:
|
|
||||||
language = Language.objects.filter(code="whisper-translation").first()
|
|
||||||
if not language:
|
|
||||||
language = Language.objects.create(code="whisper-translation", title="English Translation")
|
|
||||||
else:
|
|
||||||
language = Language.objects.filter(code="whisper").first()
|
|
||||||
if not language:
|
|
||||||
language = Language.objects.create(code="whisper", title="Transcription")
|
|
||||||
|
|
||||||
cwd = os.path.dirname(os.path.realpath(media.media_file.path))
|
|
||||||
request.status = "running"
|
|
||||||
request.save(update_fields=["status"])
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as tmpdirname:
|
|
||||||
video_file_path = get_file_name(media.media_file.name)
|
|
||||||
video_file_path = '.'.join(video_file_path.split('.')[:-1]) # needed by whisper without the extension
|
|
||||||
subtitle_name = f"{video_file_path}.vtt"
|
|
||||||
output_name = f"{tmpdirname}/{subtitle_name}"
|
|
||||||
|
|
||||||
cmd = f"whisper /home/mediacms.io/mediacms/media_files/{media.media_file.name} --model {settings.WHISPER_MODEL} --output_dir {tmpdirname}"
|
|
||||||
if translate_to_english:
|
|
||||||
cmd += " --task translate"
|
|
||||||
|
|
||||||
logger.info(f"Whisper transcribe: ready to run command {cmd}")
|
|
||||||
|
|
||||||
start_time = datetime.now()
|
|
||||||
ret = run_command(cmd, cwd=cwd) # noqa
|
|
||||||
end_time = datetime.now()
|
|
||||||
duration = (end_time - start_time).total_seconds()
|
|
||||||
|
|
||||||
if os.path.exists(output_name):
|
|
||||||
subtitle = Subtitle.objects.create(media=media, user=media.user, language=language)
|
|
||||||
|
|
||||||
with open(output_name, 'rb') as f:
|
|
||||||
subtitle.subtitle_file.save(subtitle_name, File(f))
|
|
||||||
|
|
||||||
request.status = "success"
|
|
||||||
request.logs = f"Transcription took {duration:.2f} seconds." # noqa
|
|
||||||
request.save(update_fields=["status", "logs"])
|
|
||||||
return True
|
|
||||||
|
|
||||||
request.status = "fail"
|
|
||||||
request.logs = f"Transcription failed after {duration:.2f} seconds. Error: {ret.get('error')}" # noqa
|
|
||||||
request.save(update_fields=["status", "logs"])
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@task(name="produce_sprite_from_video", queue="long_tasks")
|
@task(name="produce_sprite_from_video", queue="long_tasks")
|
||||||
def produce_sprite_from_video(friendly_token):
|
def produce_sprite_from_video(friendly_token):
|
||||||
"""Produces a sprites file for a video, uses ffmpeg"""
|
"""Produces a sprites file for a video, uses ffmpeg"""
|
||||||
@ -535,32 +377,30 @@ def produce_sprite_from_video(friendly_token):
|
|||||||
try:
|
try:
|
||||||
media = Media.objects.get(friendly_token=friendly_token)
|
media = Media.objects.get(friendly_token=friendly_token)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.info(f"failed to get media with friendly_token {friendly_token}")
|
logger.info("failed to get media with friendly_token %s" % friendly_token)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as tmpdirname:
|
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as tmpdirname:
|
||||||
try:
|
try:
|
||||||
tmpdir_image_files = tmpdirname + "/img%03d.jpg"
|
tmpdir_image_files = tmpdirname + "/img%03d.jpg"
|
||||||
output_name = tmpdirname + "/sprites.jpg"
|
output_name = tmpdirname + "/sprites.jpg"
|
||||||
|
cmd = "{0} -i {1} -f image2 -vf 'fps=1/10, scale=160:90' {2}&&files=$(ls {3}/img*.jpg | sort -t '-' -n -k 2 | tr '\n' ' ')&&convert $files -append {4}".format(
|
||||||
fps = getattr(settings, 'SPRITE_NUM_SECS', 10)
|
settings.FFMPEG_COMMAND,
|
||||||
ffmpeg_cmd = [settings.FFMPEG_COMMAND, "-i", media.media_file.path, "-f", "image2", "-vf", f"fps=1/{fps}, scale=160:90", tmpdir_image_files] # noqa
|
media.media_file.path,
|
||||||
run_command(ffmpeg_cmd)
|
tmpdir_image_files,
|
||||||
image_files = [f for f in os.listdir(tmpdirname) if f.startswith("img") and f.endswith(".jpg")]
|
tmpdirname,
|
||||||
image_files = sorted(image_files, key=lambda x: int(re.search(r'\d+', x).group()))
|
output_name,
|
||||||
image_files = [os.path.join(tmpdirname, f) for f in image_files]
|
)
|
||||||
cmd_convert = ["convert", *image_files, "-append", output_name] # image files, unpacked into the list
|
subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
ret = run_command(cmd_convert) # noqa
|
|
||||||
|
|
||||||
if os.path.exists(output_name) and get_file_type(output_name) == "image":
|
if os.path.exists(output_name) and get_file_type(output_name) == "image":
|
||||||
with open(output_name, "rb") as f:
|
with open(output_name, "rb") as f:
|
||||||
myfile = File(f)
|
myfile = File(f)
|
||||||
# SOS: avoid race condition, since this runs for a long time and will replace any other media changes on the meanwhile!!!
|
media.sprites.save(
|
||||||
media.sprites.save(content=myfile, name=get_file_name(media.media_file.path) + "sprites.jpg", save=False)
|
content=myfile,
|
||||||
media.save(update_fields=["sprites"])
|
name=get_file_name(media.media_file.path) + "sprites.jpg",
|
||||||
|
)
|
||||||
except Exception as e:
|
except BaseException:
|
||||||
print(e)
|
pass
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -579,38 +419,31 @@ def create_hls(friendly_token):
|
|||||||
try:
|
try:
|
||||||
media = Media.objects.get(friendly_token=friendly_token)
|
media = Media.objects.get(friendly_token=friendly_token)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.info(f"failed to get media with friendly_token {friendly_token}")
|
logger.info("failed to get media with friendly_token %s" % friendly_token)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
p = media.uid.hex
|
p = media.uid.hex
|
||||||
output_dir = os.path.join(settings.HLS_DIR, p)
|
output_dir = os.path.join(settings.HLS_DIR, p)
|
||||||
encodings = media.encodings.filter(profile__extension="mp4", status="success", chunk=False, profile__codec="h264")
|
encodings = media.encodings.filter(profile__extension="mp4", status="success", chunk=False, profile__codec="h264")
|
||||||
|
|
||||||
if encodings:
|
if encodings:
|
||||||
existing_output_dir = None
|
existing_output_dir = None
|
||||||
if os.path.exists(output_dir):
|
if os.path.exists(output_dir):
|
||||||
existing_output_dir = output_dir
|
existing_output_dir = output_dir
|
||||||
output_dir = os.path.join(settings.HLS_DIR, p + produce_friendly_token())
|
output_dir = os.path.join(settings.HLS_DIR, p + produce_friendly_token())
|
||||||
files = [f.media_file.path for f in encodings if f.media_file]
|
files = " ".join([f.media_file.path for f in encodings if f.media_file])
|
||||||
cmd = [settings.MP4HLS_COMMAND, '--segment-duration=4', f'--output-dir={output_dir}', *files]
|
cmd = "{0} --segment-duration=4 --output-dir={1} {2}".format(settings.MP4HLS_COMMAND, output_dir, files)
|
||||||
run_command(cmd)
|
subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
|
|
||||||
if existing_output_dir:
|
if existing_output_dir:
|
||||||
# override content with -T !
|
# override content with -T !
|
||||||
cmd = ["cp", "-rT", output_dir, existing_output_dir]
|
cmd = "cp -rT {0} {1}".format(output_dir, existing_output_dir)
|
||||||
run_command(cmd)
|
subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
|
|
||||||
try:
|
|
||||||
shutil.rmtree(output_dir)
|
shutil.rmtree(output_dir)
|
||||||
except: # noqa
|
|
||||||
# this was breaking in some cases where it was already deleted
|
|
||||||
# because create_hls was running multiple times
|
|
||||||
pass
|
|
||||||
output_dir = existing_output_dir
|
output_dir = existing_output_dir
|
||||||
pp = os.path.join(output_dir, "master.m3u8")
|
pp = os.path.join(output_dir, "master.m3u8")
|
||||||
if os.path.exists(pp):
|
if os.path.exists(pp):
|
||||||
if media.hls_file != pp:
|
if media.hls_file != pp:
|
||||||
Media.objects.filter(pk=media.pk).update(hls_file=pp)
|
media.hls_file = pp
|
||||||
|
media.save(update_fields=["hls_file"])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -621,7 +454,7 @@ def check_running_states():
|
|||||||
|
|
||||||
encodings = Encoding.objects.filter(status="running")
|
encodings = Encoding.objects.filter(status="running")
|
||||||
|
|
||||||
logger.info(f"got {encodings.count()} encodings that are in state running")
|
logger.info("got {0} encodings that are in state running".format(encodings.count()))
|
||||||
changed = 0
|
changed = 0
|
||||||
for encoding in encodings:
|
for encoding in encodings:
|
||||||
now = datetime.now(encoding.update_date.tzinfo)
|
now = datetime.now(encoding.update_date.tzinfo)
|
||||||
@ -638,7 +471,7 @@ def check_running_states():
|
|||||||
# TODO: allign with new code + chunksize...
|
# TODO: allign with new code + chunksize...
|
||||||
changed += 1
|
changed += 1
|
||||||
if changed:
|
if changed:
|
||||||
logger.info(f"changed from running to pending on {changed} items")
|
logger.info("changed from running to pending on {0} items".format(changed))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -648,7 +481,7 @@ def check_media_states():
|
|||||||
# check encoding status of not success media
|
# check encoding status of not success media
|
||||||
media = Media.objects.filter(Q(encoding_status="running") | Q(encoding_status="fail") | Q(encoding_status="pending"))
|
media = Media.objects.filter(Q(encoding_status="running") | Q(encoding_status="fail") | Q(encoding_status="pending"))
|
||||||
|
|
||||||
logger.info(f"got {media.count()} media that are not in state success")
|
logger.info("got {0} media that are not in state success".format(media.count()))
|
||||||
|
|
||||||
changed = 0
|
changed = 0
|
||||||
for m in media:
|
for m in media:
|
||||||
@ -656,7 +489,7 @@ def check_media_states():
|
|||||||
m.save(update_fields=["encoding_status"])
|
m.save(update_fields=["encoding_status"])
|
||||||
changed += 1
|
changed += 1
|
||||||
if changed:
|
if changed:
|
||||||
logger.info(f"changed encoding status to {changed} media items")
|
logger.info("changed encoding status to {0} media items".format(changed))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -691,7 +524,7 @@ def check_pending_states():
|
|||||||
media.encode(profiles=[profile], force=False)
|
media.encode(profiles=[profile], force=False)
|
||||||
changed += 1
|
changed += 1
|
||||||
if changed:
|
if changed:
|
||||||
logger.info(f"set to the encode queue {changed} encodings that were on pending state")
|
logger.info("set to the encode queue {0} encodings that were on pending state".format(changed))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -715,7 +548,7 @@ def check_missing_profiles():
|
|||||||
# if they appear on the meanwhile (eg on a big queue)
|
# if they appear on the meanwhile (eg on a big queue)
|
||||||
changed += 1
|
changed += 1
|
||||||
if changed:
|
if changed:
|
||||||
logger.info(f"set to the encode queue {changed} profiles")
|
logger.info("set to the encode queue {0} profiles".format(changed))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -883,7 +716,7 @@ def update_listings_thumbnails():
|
|||||||
# Categories
|
# Categories
|
||||||
used_media = []
|
used_media = []
|
||||||
saved = 0
|
saved = 0
|
||||||
qs = Category.objects.filter()
|
qs = Category.objects.filter().order_by("-media_count")
|
||||||
for object in qs:
|
for object in qs:
|
||||||
media = Media.objects.exclude(friendly_token__in=used_media).filter(category=object, state="public", is_reviewed=True).order_by("-views").first()
|
media = Media.objects.exclude(friendly_token__in=used_media).filter(category=object, state="public", is_reviewed=True).order_by("-views").first()
|
||||||
if media:
|
if media:
|
||||||
@ -891,12 +724,12 @@ def update_listings_thumbnails():
|
|||||||
object.save(update_fields=["listings_thumbnail"])
|
object.save(update_fields=["listings_thumbnail"])
|
||||||
used_media.append(media.friendly_token)
|
used_media.append(media.friendly_token)
|
||||||
saved += 1
|
saved += 1
|
||||||
logger.info(f"updated {saved} categories")
|
logger.info("updated {} categories".format(saved))
|
||||||
|
|
||||||
# Tags
|
# Tags
|
||||||
used_media = []
|
used_media = []
|
||||||
saved = 0
|
saved = 0
|
||||||
qs = Tag.objects.filter()
|
qs = Tag.objects.filter().order_by("-media_count")
|
||||||
for object in qs:
|
for object in qs:
|
||||||
media = Media.objects.exclude(friendly_token__in=used_media).filter(tags=object, state="public", is_reviewed=True).order_by("-views").first()
|
media = Media.objects.exclude(friendly_token__in=used_media).filter(tags=object, state="public", is_reviewed=True).order_by("-views").first()
|
||||||
if media:
|
if media:
|
||||||
@ -904,7 +737,7 @@ def update_listings_thumbnails():
|
|||||||
object.save(update_fields=["listings_thumbnail"])
|
object.save(update_fields=["listings_thumbnail"])
|
||||||
used_media.append(media.friendly_token)
|
used_media.append(media.friendly_token)
|
||||||
saved += 1
|
saved += 1
|
||||||
logger.info(f"updated {saved} tags")
|
logger.info("updated {} tags".format(saved))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -933,150 +766,23 @@ def task_sent_handler(sender=None, headers=None, body=None, **kwargs):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def kill_ffmpeg_process(filepath):
|
||||||
|
# this is not ideal, ffmpeg pid could be linked to the Encoding object
|
||||||
|
cmd = "ps aux|grep 'ffmpeg'|grep %s|grep -v grep |awk '{print $2}'" % filepath
|
||||||
|
result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
|
pid = result.stdout.decode("utf-8").strip()
|
||||||
|
if pid:
|
||||||
|
cmd = "kill -9 %s" % pid
|
||||||
|
result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@task(name="remove_media_file", base=Task, queue="long_tasks")
|
@task(name="remove_media_file", base=Task, queue="long_tasks")
|
||||||
def remove_media_file(media_file=None):
|
def remove_media_file(media_file=None):
|
||||||
rm_file(media_file)
|
rm_file(media_file)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@task(name="update_encoding_size", queue="short_tasks")
|
|
||||||
def update_encoding_size(encoding_id):
|
|
||||||
"""Update the size of an encoding without saving to avoid calling signals"""
|
|
||||||
encoding = Encoding.objects.filter(id=encoding_id).first()
|
|
||||||
if encoding:
|
|
||||||
encoding.update_size_without_save()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@task(name="post_trim_action", queue="short_tasks", soft_time_limit=600)
|
|
||||||
def post_trim_action(friendly_token):
|
|
||||||
"""Perform post-processing actions after video trimming
|
|
||||||
|
|
||||||
Args:
|
|
||||||
friendly_token: The friendly token of the media
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if successful, False otherwise
|
|
||||||
"""
|
|
||||||
logger.info(f"Post trim action for {friendly_token}")
|
|
||||||
try:
|
|
||||||
media = Media.objects.get(friendly_token=friendly_token)
|
|
||||||
except Media.DoesNotExist:
|
|
||||||
logger.info(f"Media with friendly token {friendly_token} not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
media.set_media_type()
|
|
||||||
encodings = media.encodings.filter(status="success", profile__extension='mp4', chunk=False)
|
|
||||||
# if they are still not encoded, when the first one will be encoded, it will have the chance to
|
|
||||||
# call post_trim_action again
|
|
||||||
if encodings:
|
|
||||||
for encoding in encodings:
|
|
||||||
# update encoding size, in case they don't have one, due to the
|
|
||||||
# way the copy_video took place
|
|
||||||
update_encoding_size(encoding.id)
|
|
||||||
|
|
||||||
media.produce_thumbnails_from_video()
|
|
||||||
produce_sprite_from_video.delay(friendly_token)
|
|
||||||
create_hls.delay(friendly_token)
|
|
||||||
|
|
||||||
vt_request = VideoTrimRequest.objects.filter(media=media, status="running").first()
|
|
||||||
if vt_request:
|
|
||||||
vt_request.status = "success"
|
|
||||||
vt_request.save(update_fields=["status"])
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@task(name="video_trim_task", bind=True, queue="short_tasks", soft_time_limit=600)
|
|
||||||
def video_trim_task(self, trim_request_id):
|
|
||||||
# SOS: if at some point we move from ffmpeg copy, then this need be changed
|
|
||||||
# to long_tasks
|
|
||||||
try:
|
|
||||||
trim_request = VideoTrimRequest.objects.get(id=trim_request_id)
|
|
||||||
except VideoTrimRequest.DoesNotExist:
|
|
||||||
logger.info(f"VideoTrimRequest with ID {trim_request_id} not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
trim_request.status = "running"
|
|
||||||
trim_request.save(update_fields=["status"])
|
|
||||||
|
|
||||||
timestamps_encodings = get_trim_timestamps(trim_request.media.trim_video_path, trim_request.timestamps)
|
|
||||||
timestamps_original = get_trim_timestamps(trim_request.media.media_file.path, trim_request.timestamps)
|
|
||||||
|
|
||||||
if not timestamps_encodings:
|
|
||||||
trim_request.status = "fail"
|
|
||||||
trim_request.save(update_fields=["status"])
|
|
||||||
return False
|
|
||||||
|
|
||||||
target_media = trim_request.media
|
|
||||||
original_media = trim_request.media
|
|
||||||
|
|
||||||
# splitting the logic for single file and multiple files
|
|
||||||
if trim_request.video_action in ["save_new", "replace"]:
|
|
||||||
proceed_with_single_file = True
|
|
||||||
if trim_request.video_action == "create_segments":
|
|
||||||
if len(timestamps_encodings) == 1:
|
|
||||||
proceed_with_single_file = True
|
|
||||||
else:
|
|
||||||
proceed_with_single_file = False
|
|
||||||
|
|
||||||
if proceed_with_single_file:
|
|
||||||
if trim_request.video_action == "save_new" or trim_request.video_action == "create_segments" and len(timestamps_encodings) == 1:
|
|
||||||
new_media = copy_video(original_media, copy_encodings=True)
|
|
||||||
|
|
||||||
target_media = new_media
|
|
||||||
trim_request.media = new_media
|
|
||||||
trim_request.save(update_fields=["media"])
|
|
||||||
|
|
||||||
# processing timestamps differently on encodings and original file, in case we do accuracy trimming (currently not)
|
|
||||||
# these have different I-frames and the cut is made based on the I-frames
|
|
||||||
|
|
||||||
original_trim_result = trim_video_method(target_media.media_file.path, timestamps_original)
|
|
||||||
if not original_trim_result:
|
|
||||||
logger.info(f"Failed to trim original file for media {target_media.friendly_token}")
|
|
||||||
|
|
||||||
deleted_encodings = handle_pending_running_encodings(target_media)
|
|
||||||
# the following could be un-necessary, read commend in pre_trim_video_actions to see why
|
|
||||||
encodings = target_media.encodings.filter(status="success", profile__extension='mp4', chunk=False)
|
|
||||||
for encoding in encodings:
|
|
||||||
trim_result = trim_video_method(encoding.media_file.path, timestamps_encodings)
|
|
||||||
if not trim_result:
|
|
||||||
logger.info(f"Failed to trim encoding {encoding.id} for media {target_media.friendly_token}")
|
|
||||||
encoding.delete()
|
|
||||||
|
|
||||||
pre_trim_video_actions(target_media)
|
|
||||||
post_trim_action.delay(target_media.friendly_token)
|
|
||||||
|
|
||||||
else:
|
|
||||||
for i, timestamp in enumerate(timestamps_encodings, start=1):
|
|
||||||
# copy the original file for each of the segments. This could be optimized to avoid the overhead but
|
|
||||||
# for now is necessary because the ffmpeg trim command will be run towards the original
|
|
||||||
# file on different times.
|
|
||||||
target_media = copy_video(original_media, title_suffix=f"(Trimmed) {i}", copy_encodings=True)
|
|
||||||
|
|
||||||
video_trim_request = VideoTrimRequest.objects.create(media=target_media, status="running", video_action="create_segments", media_trim_style='no_encoding', timestamps=[timestamp]) # noqa
|
|
||||||
|
|
||||||
original_trim_result = trim_video_method(target_media.media_file.path, [timestamp])
|
|
||||||
deleted_encodings = handle_pending_running_encodings(target_media) # noqa
|
|
||||||
# the following could be un-necessary, read commend in pre_trim_video_actions to see why
|
|
||||||
encodings = target_media.encodings.filter(status="success", profile__extension='mp4', chunk=False)
|
|
||||||
for encoding in encodings:
|
|
||||||
trim_result = trim_video_method(encoding.media_file.path, [timestamp])
|
|
||||||
if not trim_result:
|
|
||||||
logger.info(f"Failed to trim encoding {encoding.id} for media {target_media.friendly_token}")
|
|
||||||
encoding.delete()
|
|
||||||
|
|
||||||
pre_trim_video_actions(target_media)
|
|
||||||
post_trim_action.delay(target_media.friendly_token)
|
|
||||||
|
|
||||||
# set as completed the initial trim_request
|
|
||||||
trim_request.status = "success"
|
|
||||||
trim_request.save(update_fields=["status"])
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# TODO LIST
|
# TODO LIST
|
||||||
# 1 chunks are deleted from original server when file is fully encoded.
|
# 1 chunks are deleted from original server when file is fully encoded.
|
||||||
# however need to enter this logic in cases of fail as well
|
# however need to enter this logic in cases of fail as well
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user