mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-06-07 09:24:20 -04:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98d5d6af8b | |||
| 9302559d4b | |||
| 279cccb980 | |||
| 25e91e9d5e | |||
| d6a11514e5 | |||
| c7a1d60d73 | |||
| 6ee5bef6ce | |||
| 2e01000559 | |||
| 4f11addcfd | |||
| b11f2f561c | |||
| b6da9c4662 |
@@ -24,7 +24,6 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
|
|
||||||
**Environment (please complete the following information):**
|
**Environment (please complete the following information):**
|
||||||
- OS: [e.g. Ubuntu Linux]
|
- OS: [e.g. Ubuntu Linux]
|
||||||
- Installation method: [Docker install, or single server install]
|
|
||||||
- Browser, if applicable
|
- Browser, if applicable
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
|||||||
@@ -37,3 +37,5 @@ frontend-tools/chapters-editor/client/public/videos/sample-video.mp3
|
|||||||
static/chapters_editor/videos/sample-video.mp3
|
static/chapters_editor/videos/sample-video.mp3
|
||||||
static/video_editor/videos/sample-video.mp3
|
static/video_editor/videos/sample-video.mp3
|
||||||
templates/todo-MS4.md
|
templates/todo-MS4.md
|
||||||
|
.secret_key
|
||||||
|
.secret_key.lock
|
||||||
|
|||||||
@@ -1,5 +1,35 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [8.1.0](https://github.com/mediacms-io/mediacms/compare/v8.0.8...v8.1.0) (2026-05-17)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* introduce x-accell headers ([9302559](https://github.com/mediacms-io/mediacms/commit/9302559d4bb3e4d0adb299ed37438b04c39e1864))
|
||||||
|
|
||||||
|
## [8.0.8](https://github.com/mediacms-io/mediacms/compare/v8.0.7...v8.0.8) (2026-05-13)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update documentation and fix smaller issues ([#1520](https://github.com/mediacms-io/mediacms/issues/1520)) ([d6a1151](https://github.com/mediacms-io/mediacms/commit/d6a11514e54b9341ec8a306a259adce6b4199d42))
|
||||||
|
|
||||||
|
## [8.0.7](https://github.com/mediacms-io/mediacms/compare/v8.0.6...v8.0.7) (2026-05-12)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bring related items back ([#1515](https://github.com/mediacms-io/mediacms/issues/1515)) ([6ee5bef](https://github.com/mediacms-io/mediacms/commit/6ee5bef6ce31cf849941f65d0817e53b8f03362f))
|
||||||
|
|
||||||
|
## [8.0.6](https://github.com/mediacms-io/mediacms/compare/v8.0.5...v8.0.6) (2026-05-11)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* better place secret key settings ([4f11add](https://github.com/mediacms-io/mediacms/commit/4f11addcfd6657e7e63eed0570b1d4d9bca75698))
|
||||||
|
|
||||||
|
## [8.0.5](https://github.com/mediacms-io/mediacms/compare/v8.0.4...v8.0.5) (2026-05-11)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add secret key to gitignore ([b6da9c4](https://github.com/mediacms-io/mediacms/commit/b6da9c4662b3fba234b8dc69700ffa44fced7482))
|
||||||
|
|
||||||
## [8.0.4](https://github.com/mediacms-io/mediacms/compare/v8.0.3...v8.0.4) (2026-05-11)
|
## [8.0.4](https://github.com/mediacms-io/mediacms/compare/v8.0.3...v8.0.4) (2026-05-11)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ A demo is available at https://demo.mediacms.io
|
|||||||
- **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 (144p, 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.
|
||||||
- **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
|
- **Translation**: Most of the CMS is translated to a number of languages
|
||||||
@@ -91,7 +91,6 @@ In order to support automatic transcriptions through Whisper, consider more CPUs
|
|||||||
|
|
||||||
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
|
|
||||||
- [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).
|
||||||
|
|||||||
+22
-40
@@ -1,7 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
from django.core.management.utils import get_random_secret_key
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
@@ -172,30 +171,19 @@ REST_FRAMEWORK = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Set the SECRET_KEY env var in production. If unset, a fresh random key is
|
# In docker, deploy/docker/entrypoint.sh ensures the SECRET_KEY env var is
|
||||||
# generated or read from a .secret_key file to ensure all workers share the same key.
|
# set (generating .secret_key once on first start if needed). Outside docker,
|
||||||
def get_secret_key():
|
# either set SECRET_KEY in the environment or create a .secret_key file at the
|
||||||
key = os.getenv('SECRET_KEY')
|
# project root, e.g.:
|
||||||
if key:
|
# python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' > .secret_key
|
||||||
return key
|
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||||
|
if not SECRET_KEY:
|
||||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
_secret_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.secret_key')
|
||||||
secret_path = os.path.join(base_dir, '.secret_key')
|
if os.path.exists(_secret_path):
|
||||||
|
with open(_secret_path) as _f:
|
||||||
if os.path.exists(secret_path):
|
SECRET_KEY = _f.read().strip()
|
||||||
with open(secret_path) as f:
|
if not SECRET_KEY:
|
||||||
return f.read().strip()
|
raise RuntimeError("SECRET_KEY is not set. Set the SECRET_KEY env var or create a .secret_key file at the project root.")
|
||||||
|
|
||||||
key = get_random_secret_key()
|
|
||||||
try:
|
|
||||||
with open(secret_path, 'w') as f:
|
|
||||||
f.write(key)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return key
|
|
||||||
|
|
||||||
|
|
||||||
SECRET_KEY = get_secret_key()
|
|
||||||
|
|
||||||
TEMP_DIRECTORY = "/tmp" # Don't use a temp directory inside BASE_DIR!!!
|
TEMP_DIRECTORY = "/tmp" # Don't use a temp directory inside BASE_DIR!!!
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
@@ -214,6 +202,15 @@ THUMBNAIL_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/thumbnails/"
|
|||||||
SUBTITLES_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/subtitles/"
|
SUBTITLES_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/subtitles/"
|
||||||
HLS_DIR = os.path.join(MEDIA_ROOT, "hls/")
|
HLS_DIR = os.path.join(MEDIA_ROOT, "hls/")
|
||||||
|
|
||||||
|
# Protect media files via nginx auth_request
|
||||||
|
# When True, nginx delegates authorization for /media/<protected>/... to a
|
||||||
|
# Django endpoint that checks the Media's state and the user's access.
|
||||||
|
USE_X_ACCEL_REDIRECT = True
|
||||||
|
# Subdirectories of MEDIA_ROOT that should be gated. "chunks" is intentionally
|
||||||
|
# omitted (upload state, not playback).
|
||||||
|
X_ACCEL_PROTECTED_PATHS = ["encoded", "hls", "original"]
|
||||||
|
X_ACCEL_AUTH_CACHE_SECONDS = 300
|
||||||
|
|
||||||
FFMPEG_COMMAND = "ffmpeg" # this is the path
|
FFMPEG_COMMAND = "ffmpeg" # this is the path
|
||||||
FFPROBE_COMMAND = "ffprobe" # this is the path
|
FFPROBE_COMMAND = "ffprobe" # this is the path
|
||||||
MP4HLS = "mp4hls"
|
MP4HLS = "mp4hls"
|
||||||
@@ -278,21 +275,6 @@ CANNOT_ADD_MEDIA_MESSAGE = "User cannot add media, or maximum number of media up
|
|||||||
# mp4hls command, part of Bento4
|
# mp4hls command, part of Bento4
|
||||||
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
|
|
||||||
ADMIN_TOKEN = ""
|
|
||||||
# this is used by remote workers to push
|
|
||||||
# encodings once they are done
|
|
||||||
# USE_BASIC_HTTP = True
|
|
||||||
# BASIC_HTTP_USER_PAIR = ('user', 'password')
|
|
||||||
# specify basic auth user/password pair for use with the
|
|
||||||
# remote workers, if nginx basic auth is setup
|
|
||||||
# apache2-utils need be installed
|
|
||||||
# then run
|
|
||||||
# htpasswd -c /home/mediacms.io/mediacms/deploy/.htpasswd user
|
|
||||||
# and set a password
|
|
||||||
# edit /etc/nginx/sites-enabled/mediacms.io and
|
|
||||||
# uncomment the two lines related to htpasswd
|
|
||||||
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = "users.User"
|
AUTH_USER_MODEL = "users.User"
|
||||||
LOGIN_REDIRECT_URL = "/"
|
LOGIN_REDIRECT_URL = "/"
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
VERSION = "8.0.4"
|
VERSION = "8.1.0"
|
||||||
|
|||||||
@@ -35,4 +35,29 @@ find /home/mediacms.io/mediacms ! \( -path "*.git*" -o -name "package-lock.json"
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
# Generate or read SECRET_KEY once, shared across all containers via the
|
||||||
|
# host-mounted project volume. Atomic create-or-read so parallel container
|
||||||
|
# starts (web + celery_worker + celery_beat + migrations) can't race.
|
||||||
|
# Uses `mkdir` as the lock primitive (POSIX-atomic, no dependency on flock).
|
||||||
|
SECRET_KEY_FILE="${SECRET_KEY_FILE:-/home/mediacms.io/mediacms/.secret_key}"
|
||||||
|
SECRET_KEY_LOCK="${SECRET_KEY_FILE}.lock"
|
||||||
|
|
||||||
|
if [ -z "${SECRET_KEY:-}" ]; then
|
||||||
|
if [ ! -s "$SECRET_KEY_FILE" ]; then
|
||||||
|
# Spin-acquire the lock. mkdir is atomic; first caller wins, others retry.
|
||||||
|
while ! mkdir "$SECRET_KEY_LOCK" 2>/dev/null; do
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
# Re-check inside the lock: another container may have just written it.
|
||||||
|
if [ ! -s "$SECRET_KEY_FILE" ]; then
|
||||||
|
python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' > "$SECRET_KEY_FILE"
|
||||||
|
chown www-data:www-data "$SECRET_KEY_FILE"
|
||||||
|
chmod 600 "$SECRET_KEY_FILE"
|
||||||
|
echo "entrypoint.sh: generated new SECRET_KEY at $SECRET_KEY_FILE"
|
||||||
|
fi
|
||||||
|
rmdir "$SECRET_KEY_LOCK"
|
||||||
|
fi
|
||||||
|
export SECRET_KEY="$(cat "$SECRET_KEY_FILE")"
|
||||||
|
fi
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -15,9 +15,27 @@ server {
|
|||||||
location /static {
|
location /static {
|
||||||
alias /home/mediacms.io/mediacms/static ;
|
alias /home/mediacms.io/mediacms/static ;
|
||||||
}
|
}
|
||||||
|
location = /_media_auth {
|
||||||
|
internal;
|
||||||
|
proxy_pass http://127.0.0.1:9000/api/v1/media-auth;
|
||||||
|
proxy_pass_request_body off;
|
||||||
|
proxy_set_header Content-Length "";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Original-URI $request_uri;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
||||||
|
proxy_set_header Cookie $http_cookie;
|
||||||
|
}
|
||||||
|
|
||||||
location /media/original {
|
location ~ ^/media/(encoded|hls|original)/(.*)$ {
|
||||||
alias /home/mediacms.io/mediacms/media_files/original;
|
auth_request /_media_auth;
|
||||||
|
alias /home/mediacms.io/mediacms/media_files/$1/$2;
|
||||||
|
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||||
|
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||||
}
|
}
|
||||||
|
|
||||||
location /media {
|
location /media {
|
||||||
|
|||||||
+1
-65
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
- [1. Welcome](#1-welcome)
|
- [1. Welcome](#1-welcome)
|
||||||
- [2. Single Server Installaton](#2-single-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)
|
||||||
@@ -34,58 +33,6 @@
|
|||||||
## 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
|
|
||||||
|
|
||||||
The core dependencies are python3, Django, celery, PostgreSQL, redis, ffmpeg. Any system that can have these dependencies installed, can run MediaCMS. But the install.sh is only tested in Linux Ubuntu 24 and 22 versions.
|
|
||||||
|
|
||||||
Installation on an Ubuntu 22/24 system with git utility installed should be completed in a few minutes with the following steps.
|
|
||||||
Make sure you run it as user root, on a clear system, since the automatic script will install and configure the following services: Celery/PostgreSQL/Redis/Nginx and will override any existing settings.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir /home/mediacms.io && cd /home/mediacms.io/
|
|
||||||
git clone https://github.com/mediacms-io/mediacms
|
|
||||||
cd /home/mediacms.io/mediacms/ && bash ./install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The script will ask if you have a URL where you want to deploy MediaCMS, otherwise it will use localhost. If you provide a URL, it will use Let's Encrypt service to install a valid ssl certificate.
|
|
||||||
|
|
||||||
|
|
||||||
### Update
|
|
||||||
|
|
||||||
If you've used the above way to install MediaCMS, update with the following:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/mediacms.io/mediacms # enter mediacms directory
|
|
||||||
source /home/mediacms.io/bin/activate # use virtualenv
|
|
||||||
git pull # update code
|
|
||||||
pip install -r requirements.txt -U # run pip install to update
|
|
||||||
python manage.py migrate # run Django migrations
|
|
||||||
sudo systemctl restart mediacms celery_long celery_short # restart services
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update from version 2 to version 3
|
|
||||||
Version 3 is using Django 4 and Celery 5, and needs a recent Python 3.x version. If you are updating from an older version, make sure Python is updated first. Version 2 could run on Python 3.6, but version 3 needs Python3.8 and higher.
|
|
||||||
The syntax for starting Celery has also changed, so you have to copy the celery related systemctl files and restart
|
|
||||||
|
|
||||||
```
|
|
||||||
# cp deploy/local_install/celery_long.service /etc/systemd/system/celery_long.service
|
|
||||||
# cp deploy/local_install/celery_short.service /etc/systemd/system/celery_short.service
|
|
||||||
# cp deploy/local_install/celery_beat.service /etc/systemd/system/celery_beat.service
|
|
||||||
# systemctl daemon-reload
|
|
||||||
# systemctl start celery_long celery_short celery_beat
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
Checkout the configuration section here.
|
|
||||||
|
|
||||||
|
|
||||||
### Maintenance
|
|
||||||
Database can be backed up with pg_dump and media_files on /home/mediacms.io/mediacms/media_files include original files and encoded/transcoded versions
|
|
||||||
|
|
||||||
|
|
||||||
## 3. Docker Installation
|
## 3. Docker Installation
|
||||||
|
|
||||||
@@ -220,14 +167,10 @@ Several options are available on `cms/settings.py`, most of the things that are
|
|||||||
|
|
||||||
It is advisable to override any of them by adding it to `local_settings.py` .
|
It is advisable to override any of them by adding it to `local_settings.py` .
|
||||||
|
|
||||||
In case of a the single server installation, add to `cms/local_settings.py` .
|
|
||||||
|
|
||||||
In case of a docker compose installation, add to `deploy/docker/local_settings.py` . This will automatically overwrite `cms/local_settings.py` .
|
In case of a docker compose installation, add to `deploy/docker/local_settings.py` . This will automatically overwrite `cms/local_settings.py` .
|
||||||
|
|
||||||
Any change needs restart of MediaCMS in order to take effect.
|
Any change needs restart of MediaCMS in order to take effect.
|
||||||
|
|
||||||
Single server installation: edit `cms/local_settings.py`, make a change and restart MediaCMS
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#systemctl restart mediacms
|
#systemctl restart mediacms
|
||||||
```
|
```
|
||||||
@@ -795,14 +738,7 @@ Instructions contributed by @alberto98fx
|
|||||||
On the [Configuration](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#5-configuration) section of this guide we've see how to edit the email settings.
|
On the [Configuration](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#5-configuration) section of this guide we've see how to edit the email settings.
|
||||||
In case we are yet unable to receive email from MediaCMS, the following may help us debug the issue - in most cases it is an issue of setting the correct username, password or TLS option
|
In case we are yet unable to receive email from MediaCMS, the following may help us debug the issue - in most cases it is an issue of setting the correct username, password or TLS option
|
||||||
|
|
||||||
Enter the Django shell, example if you're using the Single Server installation:
|
Enter the Django shell and inside the shell
|
||||||
|
|
||||||
```bash
|
|
||||||
source /home/mediacms.io/bin/activate
|
|
||||||
python manage.py shell
|
|
||||||
```
|
|
||||||
|
|
||||||
and inside the shell
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
|
|||||||
@@ -120,9 +120,11 @@ class MediaList(APIView):
|
|||||||
operation_description='Delete media for MediaCMS managers and reviewers',
|
operation_description='Delete media for MediaCMS managers and reviewers',
|
||||||
)
|
)
|
||||||
def delete(self, request, format=None):
|
def delete(self, request, format=None):
|
||||||
|
if not is_mediacms_manager(request.user):
|
||||||
|
return Response({"detail": "bad permissions"}, status=status.HTTP_403_FORBIDDEN)
|
||||||
tokens = request.GET.get("tokens")
|
tokens = request.GET.get("tokens")
|
||||||
if tokens:
|
if tokens:
|
||||||
tokens = tokens.split(",")
|
tokens = [t for t in tokens.split(",") if t][:50]
|
||||||
Media.objects.filter(friendly_token__in=tokens).delete()
|
Media.objects.filter(friendly_token__in=tokens).delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@@ -177,7 +179,7 @@ class CommentList(APIView):
|
|||||||
def delete(self, request, format=None):
|
def delete(self, request, format=None):
|
||||||
comment_ids = request.GET.get("comment_ids")
|
comment_ids = request.GET.get("comment_ids")
|
||||||
if comment_ids:
|
if comment_ids:
|
||||||
comments = comment_ids.split(",")
|
comments = [c for c in comment_ids.split(",") if c][:50]
|
||||||
Comment.objects.filter(uid__in=comments).delete()
|
Comment.objects.filter(uid__in=comments).delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|||||||
+13
-9
@@ -453,17 +453,21 @@ def kill_ffmpeg_process(filepath):
|
|||||||
filepath: Path to the file being processed by ffmpeg
|
filepath: Path to the file being processed by ffmpeg
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
subprocess.CompletedProcess: Result of the kill command
|
bool: True if the lookup ran, False if input was unusable
|
||||||
"""
|
"""
|
||||||
if not filepath:
|
if not filepath or not isinstance(filepath, str):
|
||||||
return False
|
return False
|
||||||
cmd = "ps aux|grep 'ffmpeg'|grep %s|grep -v grep |awk '{print $2}'" % filepath
|
try:
|
||||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
ps = subprocess.run(["ps", "aux"], stdout=subprocess.PIPE, check=False)
|
||||||
pid = result.stdout.decode("utf-8").strip()
|
except OSError:
|
||||||
if pid:
|
return False
|
||||||
cmd = "kill -9 %s" % pid
|
for line in ps.stdout.decode("utf-8", "replace").splitlines():
|
||||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
if "ffmpeg" not in line or filepath not in line or "grep" in line:
|
||||||
return result
|
continue
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) > 1 and parts[1].isdigit():
|
||||||
|
subprocess.run(["kill", "-9", parts[1]], check=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def copy_video(original_media, copy_encodings=True, title_suffix="(Trimmed)"):
|
def copy_video(original_media, copy_encodings=True, title_suffix="(Trimmed)"):
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from django.core.files import File
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.signals import post_delete, post_save
|
from django.db.models.signals import post_delete, post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from .. import helpers
|
from .. import helpers
|
||||||
from .utils import (
|
from .utils import (
|
||||||
@@ -136,9 +135,6 @@ class Encoding(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.profile.name}-{self.media.title}"
|
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)
|
@receiver(post_save, sender=Encoding)
|
||||||
def encoding_file_save(sender, instance, created, **kwargs):
|
def encoding_file_save(sender, instance, created, **kwargs):
|
||||||
|
|||||||
@@ -559,9 +559,8 @@ class Media(models.Model):
|
|||||||
profiles.remove(profile)
|
profiles.remove(profile)
|
||||||
encoding = Encoding(media=self, profile=profile)
|
encoding = Encoding(media=self, profile=profile)
|
||||||
encoding.save()
|
encoding.save()
|
||||||
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
|
|
||||||
tasks.encode_media.apply_async(
|
tasks.encode_media.apply_async(
|
||||||
args=[self.friendly_token, profile.id, encoding.id, enc_url],
|
args=[self.friendly_token, profile.id, encoding.id],
|
||||||
kwargs={"force": force},
|
kwargs={"force": force},
|
||||||
priority=0,
|
priority=0,
|
||||||
)
|
)
|
||||||
@@ -575,13 +574,12 @@ class Media(models.Model):
|
|||||||
continue
|
continue
|
||||||
encoding = Encoding(media=self, profile=profile)
|
encoding = Encoding(media=self, profile=profile)
|
||||||
encoding.save()
|
encoding.save()
|
||||||
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
|
|
||||||
if profile.resolution in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
if profile.resolution in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
||||||
priority = 9
|
priority = 9
|
||||||
else:
|
else:
|
||||||
priority = 0
|
priority = 0
|
||||||
tasks.encode_media.apply_async(
|
tasks.encode_media.apply_async(
|
||||||
args=[self.friendly_token, profile.id, encoding.id, enc_url],
|
args=[self.friendly_token, profile.id, encoding.id],
|
||||||
kwargs={"force": force},
|
kwargs={"force": force},
|
||||||
priority=priority,
|
priority=priority,
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-5
@@ -171,8 +171,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
|||||||
continue
|
continue
|
||||||
encoding = Encoding(media=media, profile=profile)
|
encoding = Encoding(media=media, profile=profile)
|
||||||
encoding.save()
|
encoding.save()
|
||||||
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
|
encode_media.delay(friendly_token, profile.id, encoding.id, force=force)
|
||||||
encode_media.delay(friendly_token, profile.id, encoding.id, enc_url, force=force)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
chunks = [os.path.join(cwd, ch) for ch in chunks]
|
chunks = [os.path.join(cwd, ch) for ch in chunks]
|
||||||
@@ -202,13 +201,12 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
|||||||
)
|
)
|
||||||
|
|
||||||
encoding.save()
|
encoding.save()
|
||||||
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
|
|
||||||
if profile.resolution in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
if profile.resolution in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
||||||
priority = 0
|
priority = 0
|
||||||
else:
|
else:
|
||||||
priority = 9
|
priority = 9
|
||||||
encode_media.apply_async(
|
encode_media.apply_async(
|
||||||
args=[friendly_token, profile.id, encoding.id, enc_url],
|
args=[friendly_token, profile.id, encoding.id],
|
||||||
kwargs={"force": force, "chunk": True, "chunk_file_path": chunk},
|
kwargs={"force": force, "chunk": True, "chunk_file_path": chunk},
|
||||||
priority=priority,
|
priority=priority,
|
||||||
)
|
)
|
||||||
@@ -246,7 +244,6 @@ def encode_media(
|
|||||||
friendly_token,
|
friendly_token,
|
||||||
profile_id,
|
profile_id,
|
||||||
encoding_id,
|
encoding_id,
|
||||||
encoding_url,
|
|
||||||
force=True,
|
force=True,
|
||||||
chunk=False,
|
chunk=False,
|
||||||
chunk_file_path="",
|
chunk_file_path="",
|
||||||
|
|||||||
+1
-5
@@ -61,11 +61,6 @@ urlpatterns = [
|
|||||||
views.MediaDetail.as_view(),
|
views.MediaDetail.as_view(),
|
||||||
name="api_get_media",
|
name="api_get_media",
|
||||||
),
|
),
|
||||||
re_path(
|
|
||||||
r"^api/v1/media/encoding/(?P<encoding_id>[\w]*)$",
|
|
||||||
views.EncodingDetail.as_view(),
|
|
||||||
name="api_get_encoding",
|
|
||||||
),
|
|
||||||
re_path(r"^api/v1/search$", views.MediaSearch.as_view()),
|
re_path(r"^api/v1/search$", views.MediaSearch.as_view()),
|
||||||
re_path(
|
re_path(
|
||||||
rf"^api/v1/media/{friendly_token}/share$",
|
rf"^api/v1/media/{friendly_token}/share$",
|
||||||
@@ -111,6 +106,7 @@ urlpatterns = [
|
|||||||
re_path(r"^api/v1/tasks$", views.TasksList.as_view()),
|
re_path(r"^api/v1/tasks$", views.TasksList.as_view()),
|
||||||
re_path(r"^api/v1/tasks/$", views.TasksList.as_view()),
|
re_path(r"^api/v1/tasks/$", views.TasksList.as_view()),
|
||||||
re_path(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
|
re_path(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
|
||||||
|
re_path(r"^api/v1/media-auth$", views.media_auth, name="media_auth"),
|
||||||
re_path(r"^manage/comments$", views.manage_comments, name="manage_comments"),
|
re_path(r"^manage/comments$", views.manage_comments, name="manage_comments"),
|
||||||
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
|
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
|
||||||
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
|
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
from .auth import custom_login_view, saml_metadata # noqa: F401
|
from .auth import custom_login_view, saml_metadata # noqa: F401
|
||||||
from .categories import CategoryList, CategoryListContributor, TagList # noqa: F401
|
from .categories import CategoryList, CategoryListContributor, TagList # noqa: F401
|
||||||
from .comments import CommentDetail, CommentList # noqa: F401
|
from .comments import CommentDetail, CommentList # noqa: F401
|
||||||
from .encoding import EncodeProfileList, EncodingDetail # noqa: F401
|
from .encoding import EncodeProfileList # noqa: F401
|
||||||
from .media import MediaActions # noqa: F401
|
from .media import MediaActions # noqa: F401
|
||||||
from .media import MediaBulkUserActions # noqa: F401
|
from .media import MediaBulkUserActions # noqa: F401
|
||||||
from .media import MediaDetail # noqa: F401
|
from .media import MediaDetail # noqa: F401
|
||||||
from .media import MediaList # noqa: F401
|
from .media import MediaList # noqa: F401
|
||||||
from .media import MediaSearch # noqa: F401
|
from .media import MediaSearch # noqa: F401
|
||||||
from .media import media_share # noqa: F401
|
from .media import media_share # noqa: F401
|
||||||
|
from .media_auth import media_auth # noqa: F401
|
||||||
from .pages import about # noqa: F401
|
from .pages import about # noqa: F401
|
||||||
from .pages import add_subtitle # noqa: F401
|
from .pages import add_subtitle # noqa: F401
|
||||||
from .pages import approval_required # noqa: F401
|
from .pages import approval_required # noqa: F401
|
||||||
|
|||||||
+1
-158
@@ -1,168 +1,11 @@
|
|||||||
from django.conf import settings
|
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework import permissions, status
|
|
||||||
from rest_framework.parsers import (
|
|
||||||
FileUploadParser,
|
|
||||||
FormParser,
|
|
||||||
JSONParser,
|
|
||||||
MultiPartParser,
|
|
||||||
)
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from ..helpers import produce_ffmpeg_commands
|
from ..models import EncodeProfile
|
||||||
from ..models import EncodeProfile, Encoding
|
|
||||||
from ..serializers import EncodeProfileSerializer
|
from ..serializers import EncodeProfileSerializer
|
||||||
|
|
||||||
|
|
||||||
class EncodingDetail(APIView):
|
|
||||||
"""Experimental. This View is used by remote workers
|
|
||||||
Needs heavy testing and documentation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
permission_classes = (permissions.IsAdminUser,)
|
|
||||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
|
||||||
|
|
||||||
@swagger_auto_schema(auto_schema=None)
|
|
||||||
def post(self, request, encoding_id):
|
|
||||||
ret = {}
|
|
||||||
force = request.data.get("force", False)
|
|
||||||
task_id = request.data.get("task_id", False)
|
|
||||||
action = request.data.get("action", "")
|
|
||||||
chunk = request.data.get("chunk", False)
|
|
||||||
chunk_file_path = request.data.get("chunk_file_path", "")
|
|
||||||
|
|
||||||
encoding_status = request.data.get("status", "")
|
|
||||||
progress = request.data.get("progress", "")
|
|
||||||
commands = request.data.get("commands", "")
|
|
||||||
logs = request.data.get("logs", "")
|
|
||||||
retries = request.data.get("retries", "")
|
|
||||||
worker = request.data.get("worker", "")
|
|
||||||
temp_file = request.data.get("temp_file", "")
|
|
||||||
total_run_time = request.data.get("total_run_time", "")
|
|
||||||
if action == "start":
|
|
||||||
try:
|
|
||||||
encoding = Encoding.objects.get(id=encoding_id)
|
|
||||||
media = encoding.media
|
|
||||||
profile = encoding.profile
|
|
||||||
except BaseException:
|
|
||||||
Encoding.objects.filter(id=encoding_id).delete()
|
|
||||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
# TODO: break chunk True/False logic here
|
|
||||||
if (
|
|
||||||
Encoding.objects.filter(
|
|
||||||
media=media,
|
|
||||||
profile=profile,
|
|
||||||
chunk=chunk,
|
|
||||||
chunk_file_path=chunk_file_path,
|
|
||||||
).count()
|
|
||||||
> 1 # noqa
|
|
||||||
and force is False # noqa
|
|
||||||
):
|
|
||||||
Encoding.objects.filter(id=encoding_id).delete()
|
|
||||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
else:
|
|
||||||
Encoding.objects.filter(
|
|
||||||
media=media,
|
|
||||||
profile=profile,
|
|
||||||
chunk=chunk,
|
|
||||||
chunk_file_path=chunk_file_path,
|
|
||||||
).exclude(id=encoding.id).delete()
|
|
||||||
|
|
||||||
encoding.status = "running"
|
|
||||||
if task_id:
|
|
||||||
encoding.task_id = task_id
|
|
||||||
|
|
||||||
encoding.save()
|
|
||||||
if chunk:
|
|
||||||
original_media_path = chunk_file_path
|
|
||||||
original_media_md5sum = encoding.md5sum
|
|
||||||
original_media_url = settings.SSL_FRONTEND_HOST + encoding.media_chunk_url
|
|
||||||
else:
|
|
||||||
original_media_path = media.media_file.path
|
|
||||||
original_media_md5sum = media.md5sum
|
|
||||||
original_media_url = settings.SSL_FRONTEND_HOST + media.original_media_url
|
|
||||||
|
|
||||||
ret["original_media_url"] = original_media_url
|
|
||||||
ret["original_media_path"] = original_media_path
|
|
||||||
ret["original_media_md5sum"] = original_media_md5sum
|
|
||||||
|
|
||||||
# generating the commands here, and will replace these with temporary
|
|
||||||
# files created on the remote server
|
|
||||||
tf = "TEMP_FILE_REPLACE"
|
|
||||||
tfpass = "TEMP_FPASS_FILE_REPLACE"
|
|
||||||
ffmpeg_commands = produce_ffmpeg_commands(
|
|
||||||
original_media_path,
|
|
||||||
media.media_info,
|
|
||||||
resolution=profile.resolution,
|
|
||||||
codec=profile.codec,
|
|
||||||
output_filename=tf,
|
|
||||||
pass_file=tfpass,
|
|
||||||
chunk=chunk,
|
|
||||||
)
|
|
||||||
if not ffmpeg_commands:
|
|
||||||
encoding.delete()
|
|
||||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
ret["duration"] = media.duration
|
|
||||||
ret["ffmpeg_commands"] = ffmpeg_commands
|
|
||||||
ret["profile_extension"] = profile.extension
|
|
||||||
return Response(ret, status=status.HTTP_201_CREATED)
|
|
||||||
elif action == "update_fields":
|
|
||||||
try:
|
|
||||||
encoding = Encoding.objects.get(id=encoding_id)
|
|
||||||
except BaseException:
|
|
||||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
to_update = ["size", "update_date"]
|
|
||||||
if encoding_status:
|
|
||||||
encoding.status = encoding_status
|
|
||||||
to_update.append("status")
|
|
||||||
if progress:
|
|
||||||
encoding.progress = progress
|
|
||||||
to_update.append("progress")
|
|
||||||
if logs:
|
|
||||||
encoding.logs = logs
|
|
||||||
to_update.append("logs")
|
|
||||||
if commands:
|
|
||||||
encoding.commands = commands
|
|
||||||
to_update.append("commands")
|
|
||||||
if task_id:
|
|
||||||
encoding.task_id = task_id
|
|
||||||
to_update.append("task_id")
|
|
||||||
if total_run_time:
|
|
||||||
encoding.total_run_time = total_run_time
|
|
||||||
to_update.append("total_run_time")
|
|
||||||
if worker:
|
|
||||||
encoding.worker = worker
|
|
||||||
to_update.append("worker")
|
|
||||||
if temp_file:
|
|
||||||
encoding.temp_file = temp_file
|
|
||||||
to_update.append("temp_file")
|
|
||||||
|
|
||||||
if retries:
|
|
||||||
encoding.retries = retries
|
|
||||||
to_update.append("retries")
|
|
||||||
|
|
||||||
try:
|
|
||||||
encoding.save(update_fields=to_update)
|
|
||||||
except BaseException:
|
|
||||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
return Response({"status": "success"}, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
@swagger_auto_schema(auto_schema=None)
|
|
||||||
def put(self, request, encoding_id, format=None):
|
|
||||||
encoding_file = request.data["file"]
|
|
||||||
encoding = Encoding.objects.filter(id=encoding_id).first()
|
|
||||||
if not encoding:
|
|
||||||
return Response(
|
|
||||||
{"detail": "encoding does not exist"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
encoding.media_file = encoding_file
|
|
||||||
encoding.save()
|
|
||||||
return Response({"detail": "ok"}, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
|
|
||||||
class EncodeProfileList(APIView):
|
class EncodeProfileList(APIView):
|
||||||
"""List encode profiles"""
|
"""List encode profiles"""
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.views.decorators.http import require_GET
|
||||||
|
|
||||||
|
from ..methods import is_mediacms_editor
|
||||||
|
from ..models import Media
|
||||||
|
|
||||||
|
UID_RE = re.compile(r"[0-9a-f]{32}")
|
||||||
|
|
||||||
|
|
||||||
|
def _ttl():
|
||||||
|
return getattr(settings, "X_ACCEL_AUTH_CACHE_SECONDS", 300)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_uid(uri):
|
||||||
|
if not uri:
|
||||||
|
return None
|
||||||
|
match = UID_RE.search(uri)
|
||||||
|
return match.group(0) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_state(uid):
|
||||||
|
"""Return (state, owner_id) for a uid, or (None, None) if missing.
|
||||||
|
|
||||||
|
Cached on uid alone since state/ownership do not depend on the requester.
|
||||||
|
Uses .values() rather than .only() because Media.__init__ touches deferred
|
||||||
|
file fields, which would otherwise recurse via refresh_from_db.
|
||||||
|
"""
|
||||||
|
state_key = f"xaccel:state:{uid}"
|
||||||
|
cached = cache.get(state_key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
row = Media.objects.filter(uid=uid).values("state", "user_id").first()
|
||||||
|
value = (row["state"], row["user_id"]) if row else (None, None)
|
||||||
|
cache.set(state_key, value, _ttl())
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _decide(uid, user):
|
||||||
|
state, owner_id = _lookup_state(uid)
|
||||||
|
if state is None:
|
||||||
|
return False
|
||||||
|
if state in ("public", "unlisted"):
|
||||||
|
return True
|
||||||
|
# private
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return False
|
||||||
|
if owner_id == user.id:
|
||||||
|
return True
|
||||||
|
if is_mediacms_editor(user):
|
||||||
|
return True
|
||||||
|
# RBAC / MediaPermission path needs a full Media instance.
|
||||||
|
try:
|
||||||
|
media = Media.objects.get(uid=uid)
|
||||||
|
except Media.DoesNotExist:
|
||||||
|
return False
|
||||||
|
return user.has_member_access_to_media(media)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_GET
|
||||||
|
def media_auth(request):
|
||||||
|
"""Authorize a protected media request from nginx auth_request.
|
||||||
|
|
||||||
|
nginx passes the original request URI in the X-Original-URI header. The
|
||||||
|
Media.uid (32 hex chars, no dashes) is embedded somewhere in that URI for
|
||||||
|
every protected path. No uid => deny. Unknown uid => deny.
|
||||||
|
"""
|
||||||
|
if not getattr(settings, "USE_X_ACCEL_REDIRECT", True):
|
||||||
|
return HttpResponse(status=204)
|
||||||
|
|
||||||
|
uri = request.META.get("HTTP_X_ORIGINAL_URI", "")
|
||||||
|
uid = _extract_uid(uri)
|
||||||
|
if not uid:
|
||||||
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
cache_key = f"xaccel:auth:{uid}:{user.id if user.is_authenticated else 'anon'}"
|
||||||
|
cached = cache.get(cache_key)
|
||||||
|
if cached is None:
|
||||||
|
allowed = _decide(uid, user)
|
||||||
|
cache.set(cache_key, allowed, _ttl())
|
||||||
|
else:
|
||||||
|
allowed = cached
|
||||||
|
|
||||||
|
return HttpResponse(status=204 if allowed else 403)
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { MediaPageStore } from '../../utils/stores/';
|
||||||
|
import { AutoPlay } from './AutoPlay';
|
||||||
|
import { RelatedMedia } from './RelatedMedia';
|
||||||
import PlaylistView from './PlaylistView';
|
import PlaylistView from './PlaylistView';
|
||||||
|
|
||||||
export default class ViewerSidebar extends React.PureComponent {
|
export default class ViewerSidebar extends React.PureComponent {
|
||||||
@@ -9,6 +12,7 @@ export default class ViewerSidebar extends React.PureComponent {
|
|||||||
playlistData: props.playlistData,
|
playlistData: props.playlistData,
|
||||||
isPlaylistPage: !!props.playlistData,
|
isPlaylistPage: !!props.playlistData,
|
||||||
activeItem: 0,
|
activeItem: 0,
|
||||||
|
mediaType: MediaPageStore.get('media-type'),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.playlistData) {
|
if (props.playlistData) {
|
||||||
@@ -23,6 +27,21 @@ export default class ViewerSidebar extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.onMediaLoad = this.onMediaLoad.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
MediaPageStore.on('loaded_media_data', this.onMediaLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
MediaPageStore.removeListener('loaded_media_data', this.onMediaLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMediaLoad() {
|
||||||
|
this.setState({
|
||||||
|
mediaType: MediaPageStore.get('media-type'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -30,7 +49,10 @@ export default class ViewerSidebar extends React.PureComponent {
|
|||||||
<div className="viewer-sidebar">
|
<div className="viewer-sidebar">
|
||||||
{this.state.isPlaylistPage ? (
|
{this.state.isPlaylistPage ? (
|
||||||
<PlaylistView activeItem={this.state.activeItem} playlistData={this.props.playlistData} />
|
<PlaylistView activeItem={this.state.activeItem} playlistData={this.props.playlistData} />
|
||||||
|
) : 'video' === this.state.mediaType || 'audio' === this.state.mediaType ? (
|
||||||
|
<AutoPlay />
|
||||||
) : null}
|
) : null}
|
||||||
|
<RelatedMedia hideFirst={!this.state.isPlaylistPage} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// check templates/config/installation/translations.html for more
|
// check templates/config/installation/translations.html for more
|
||||||
|
|
||||||
export function translateString(str) {
|
export function translateString(str) {
|
||||||
return window.TRANSLATION?.[str] || str;
|
return window.TRANSLATION?.[str] ?? str;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mediacms",
|
"name": "mediacms",
|
||||||
"version": "8.0.4",
|
"version": "8.1.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -34,7 +34,6 @@ class TestImports(TestCase):
|
|||||||
from files.views import CommentDetail # noqa: F401
|
from files.views import CommentDetail # noqa: F401
|
||||||
from files.views import CommentList # noqa: F401
|
from files.views import CommentList # noqa: F401
|
||||||
from files.views import EncodeProfileList # noqa: F401
|
from files.views import EncodeProfileList # noqa: F401
|
||||||
from files.views import EncodingDetail # noqa: F401
|
|
||||||
from files.views import MediaActions # noqa: F401
|
from files.views import MediaActions # noqa: F401
|
||||||
from files.views import MediaBulkUserActions # noqa: F401
|
from files.views import MediaBulkUserActions # noqa: F401
|
||||||
from files.views import MediaDetail # noqa: F401
|
from files.views import MediaDetail # noqa: F401
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.password_validation import validate_password
|
||||||
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
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.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
@@ -369,9 +371,15 @@ class UserDetail(APIView):
|
|||||||
|
|
||||||
if action == "change_password":
|
if action == "change_password":
|
||||||
# Permission to edit user is already checked by self.get_user -> self.check_object_permissions
|
# Permission to edit user is already checked by self.get_user -> self.check_object_permissions
|
||||||
|
if user.is_superuser and not request.user.is_superuser:
|
||||||
|
raise PermissionDenied("You do not have permission to change a superuser's password.")
|
||||||
password = request.data.get("password")
|
password = request.data.get("password")
|
||||||
if not password:
|
if not password:
|
||||||
return Response({"detail": "Password is required"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": "Password is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
try:
|
||||||
|
validate_password(password, user=user)
|
||||||
|
except DjangoValidationError as exc:
|
||||||
|
return Response({"detail": list(exc.messages)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user