Compare commits

..

31 Commits
v1.1.1 ... v1.3

Author SHA1 Message Date
Markos Gogoulos
5602422d29 adds drf-yasg and automated generation of Swagger Schemas (#165)
* adds drf-yasg and automated generation of Swagger Schemas

* swagger url

* swagger docs

* adds swagger url on Readme

* swagger API

* Code of Conduct file

* doc
2021-05-29 16:34:36 +03:00
Markos Gogoulos
110695ae2f improvements on flake8 (#200) 2021-05-27 17:40:52 +03:00
Markos Gogoulos
6df942ac4e format content (#198) 2021-05-26 18:35:21 +03:00
Markos Gogoulos
2d49b1df29 adds PR template (#197) 2021-05-26 18:33:26 +03:00
Shubhank Saxena
8f228d6844 Add pre-commit (#140)
* Add pre-commit config files

* Add linting test on github action
2021-05-26 17:58:17 +03:00
Markos Gogoulos
94b26a8781 add Debian Buster info on Readme 2021-05-26 17:53:13 +03:00
sthierolf
9002930994 Update install.sh (#128)
added check for Debian 10 (buster)
2021-05-26 17:50:50 +03:00
Markos Gogoulos
1e6ee280ca adds drf_yasg dependency 2021-05-25 09:26:15 +03:00
dependabot[bot]
cf278211fb Bump django from 3.1.4 to 3.1.8 (#164)
Bumps [django](https://github.com/django/django) from 3.1.4 to 3.1.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.4...3.1.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-16 16:54:04 +03:00
Markos Gogoulos
2226b6afbf add is_stuff to admin (#156) 2021-04-27 21:12:44 +03:00
Markos Gogoulos
adf3d4377f allow media of all states to be added to playlist (#154) 2021-04-26 16:32:18 +03:00
Dan1ell
be41c6876d Set default theme in settings (#132)
* Update DEFAULT_THEME variable

* Update site.html to use DEFAULT_THEME variable
2021-04-20 22:05:31 +03:00
Markos Gogoulos
784a18ad27 add users url with ending slash (#129) 2021-04-20 21:53:38 +03:00
Markos Gogoulos
6a5c57f2b2 fix permission for user deletion (#127) 2021-04-20 21:52:09 +03:00
Dan1ell
10f198fff3 Add deploy/docker/local_settings.py to .gitignore (#142) 2021-04-20 21:48:43 +03:00
Markos Gogoulos
6b89d9722b Update FUNDING.yml 2021-04-07 22:16:29 +03:00
Markos Gogoulos
04f59ffbb8 add FUNDING.yml file 2021-03-23 19:12:48 +02:00
Markos Gogoulos
632db06ca3 command for mp4hls path 2021-03-22 21:35:32 +02:00
swiftugandan
0129ab6732 Fix docker chown recursion performance and bento4 upgrade (#95)
* optimize docker chown performance

* upgrades bento4
2021-03-22 20:04:12 +02:00
dependabot[bot]
2b65afc8dd Bump pillow from 8.0.1 to 8.1.1 (#108)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.0.1 to 8.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.0.1...8.1.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-22 20:02:59 +02:00
Dan1ell
b27e3ca6f6 Update Configuration.md (#86)
Clarifying which local_settings.py file to use.
2021-03-21 20:38:40 +02:00
Markos Gogoulos
1a7adb80da Update Configuration.md (#93)
add option
2021-03-21 20:38:18 +02:00
Markos Gogoulos
2552551662 removes redundant usage of FRONTEND_HOST variable (#102) 2021-03-21 20:36:32 +02:00
Markos Gogoulos
3b35ce0262 use mediacms image from docker hub (#69)
* use mediacms image from docker hub
2021-03-08 21:54:09 +02:00
Markos Gogoulos
883af9bb4a on Readme page resize screenshots (#68)
* image formating on Readme
2021-03-08 21:36:41 +02:00
swiftugandan
47f2279098 docker remove dangling pids (#58)
Co-authored-by: Munaawa Philip <munaawap@kainos.com>
2021-02-18 21:25:40 +02:00
Markos Gogoulos
c3d411ede1 set http prefix to variable on installation (#56) 2021-02-17 22:32:30 +02:00
swiftugandan
4450350dba fixes hls generation for docker builds (#55)
Co-authored-by: Munaawa Philip <munaawap@kainos.com>
2021-02-17 22:32:18 +02:00
Markos Gogoulos
ba1bf7d263 Feat doc docker (#57)
* updated Readme, unset variabla

* unset variabla

* documentation
2021-02-17 22:32:06 +02:00
Markos Gogoulos
41c66469e2 Updated material icons version (#54)
Co-authored-by: root <1515939+styiannis@users.noreply.github.com>
2021-02-17 22:30:34 +02:00
Markos Gogoulos
9d1a22e4a9 reset variable 2021-02-11 17:38:09 +02:00
67 changed files with 991 additions and 1118 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [mgogoulos]

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,31 @@
---
name: Issue report
about: Create a report to help us improve MediaCMS
title: ''
labels: 'issue: bug'
assignees: mgogoulos
---
**Describe the issue**
A clear and concise description of what the issue is.
**To Reproduce**
Steps to reproduce the issue:
1. Go to ...
2. Perform action ...
3. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS: [e.g. Ubuntu Linux]
- Installation method: [Docker install, or single server install]
- Browser, if applicable
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea
title: ''
labels: 'issue: enhancement'
assignees: mgogoulos
---
**Describe the feature you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

10
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,10 @@
## Description
<!-- Describe the changes introduced by this PR for the reviewers to fully understand. -->
## Steps
<!-- Actions to be done pre and post deployment -->
*Pre-deploy*
*Post-deploy*

15
.github/workflows/lint_test.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
on:
pull_request:
push:
branches:
- main
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: pre-commit/action@v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}

4
.gitignore vendored
View File

@@ -1,5 +1,8 @@
media_files/encoded/
media_files/original/
media_files/hls/
media_files/chunks/
media_files/uploads/
postgres_data/
celerybeat-schedule
logs/
@@ -10,3 +13,4 @@ static/debug_toolbar/
static/mptt/
static/rest_framework/
cms/local_settings.py
deploy/docker/local_settings.py

15
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,15 @@
repos:
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
hooks:
- id: flake8
- repo: https://github.com/pycqa/isort
rev: 5.5.4
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 20.8b1
hooks:
- id: black
language_version: python3

13
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,13 @@
# Contributor Code of Conduct
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at https://www.contributor-covenant.org/version/1/0/0/code-of-conduct.html

View File

@@ -16,10 +16,12 @@ 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-632.x86_64-unknown-linux.zip && \
mkdir -p /home/mediacms.io/mediacms/media_files/hls Bento4-SDK-1-6-0-632.x86_64-unknown-linux/bin/ && \
unzip -j Bento4-SDK-1-6-0-632.x86_64-unknown-linux.zip Bento4-SDK-1-6-0-632.x86_64-unknown-linux/bin/mp4hls -d Bento4-SDK-1-6-0-632.x86_64-unknown-linux/bin/ && \
rm Bento4-SDK-1-6-0-632.x86_64-unknown-linux.zip
RUN wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d ../bento4 && \
mv ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* ../bento4/ && \
rm -rf ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
rm -rf ../bento4/docs && \
rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
############ RUNTIME IMAGE ############
FROM python:3.8-slim-buster as runtime-image
@@ -45,7 +47,7 @@ ENV ENABLE_MIGRATIONS='yes'
ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
COPY --from=compile-image /home/mediacms.io /home/mediacms.io
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 ffmpeg imagemagick procps -y && \

View File

@@ -9,18 +9,11 @@ A demo is available at https://demo.mediacms.io
## Screenshots
![MediaCMS](docs/images/index.jpg)
Vanilla MediaCMS index page
![MediaCMS](docs/images/video.jpg)
Video page with player different options
![MediaCMS](docs/images/embed.jpg)
Embed video page
<p align="center">
<img src="https://raw.githubusercontent.com/mediacms-io/mediacms/main/docs/images/index.jpg" width="340">
<img src="https://raw.githubusercontent.com/mediacms-io/mediacms/main/docs/images/video.jpg" width="340">
<img src="https://raw.githubusercontent.com/mediacms-io/mediacms/main/docs/images/embed.jpg" width="340">
</p>
## Features
- **Complete control over your data**: host it yourself!
@@ -82,19 +75,27 @@ In terms of disk space, think of what the needs will be. A general rule is to mu
## Installation
MediaCMS can be installed through an automated script that installs and configures all needed services on a single server, and through Docker Compose.
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.
### Docker Compose installation
With a recent version of Docker Compose installed, run as root
Install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
Run as root
```bash
git clone https://github.com/mediacms-io/mediacms
cd mediacms
docker-compose build && docker-compose up
```
This will build an image, download and setup necessary images and start all containers. Once it finishes, MediaCMS will be installed on http://localhost
The default option is to serve MediaCMS on all ips available of the server (including localhost).
Now run
```bash
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
For more instructions, checkout the docs on the [Docker deployment](docs/Docker_deployment.md) page. Docker Compose support has been contributed by @swiftugandan.
@@ -106,7 +107,7 @@ The core dependencies are Python3, Django3, Celery, PostgreSQL, Redis, ffmpeg. A
Installation on a Ubuntu 18 or 20 system with git utility installed should be completed in a few minutes with the following steps.
Make sure you run it as user root, on a clear system, since the automatic script will install and configure the following services: Celery/PostgreSQL/Redis/Nginx and will override any existing settings.
Automated script - to run on Ubuntu 18 or Ubuntu 20 flavors only!
Automated script - tested on Ubuntu 18, Ubuntu 20, and Debian Buster
```bash
mkdir /home/mediacms.io && cd /home/mediacms.io/
@@ -157,28 +158,13 @@ This software uses the following list of awesome technologies:
## Who is using it
- **EngageMedia** non-profit media, technology and culture organization - https://video.engagemedia.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
- **Heritales** International Heritage Film Festival - https://stage.heritales.org
## Thanks To
- **Anna Helme**, for such a great partnership all these years!
- **Steve Anderson**, for trusting us and helping the Wordgames team make this real.
- **Andrew Lowenthal, King Catoy, Rezwan Islam** and the rest of the great team of [Engage Media](https://engagemedia.org).
- **Ioannis Korovesis, Ioannis Maistros, Diomidis Spinellis and Theodoros Karounos**, for their mentorship all these years, their contribution to science and the promotion of open source and free software technologies.
- **Antonis Ikonomou**, for hosting us on the excellent [Innovathens](https://www.innovathens.gr) space.
- **Werner Robitza**, for helping us with ffmpeg related stuff.
## How to contribute
If you like the project, here's a few things you can do
@@ -190,5 +176,12 @@ If you like the project, here's a few things you can do
- Star the project
- Add functionality, work on a PR, fix an issue!
## Developers info
- API documentation available under /swagger URL (example https://demo.mediacms.io/swagger/)
- We're working on proper documentation for users, managers and developers, until then checkout what's available on the docs/ folder of this repository
- Before you send a PR, make sure your code is properly formatted. For that, use `pre-commit install` to install a pre-commit hook and run `pre-commit run --all` and fix everything before you commit. This pre-commit will check for your code lint everytime you commit a code.
- Checkout the [Code of conduct page](CODE_OF_CONDUCT.md) if you want to contribute to this repository
## Contact
info@mediacms.io

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1.4 on 2020-12-01 07:12
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,8 +1,8 @@
# Generated by Django 3.1.4 on 2020-12-01 07:12
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
@@ -35,8 +35,6 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="mediaaction",
index=models.Index(
fields=["session_key", "action"], name="actions_med_session_fac55a_idx"
),
index=models.Index(fields=["session_key", "action"], name="actions_med_session_fac55a_idx"),
),
]

View File

@@ -1,6 +1,7 @@
from django.db import models
from users.models import User
from files.models import Media
from users.models import User
USER_MEDIA_ACTIONS = (
("like", "Like"),
@@ -30,15 +31,11 @@ class MediaAction(models.Model):
help_text="for not logged in users",
)
action = models.CharField(
max_length=20, choices=USER_MEDIA_ACTIONS, default="watch"
)
action = models.CharField(max_length=20, choices=USER_MEDIA_ACTIONS, default="watch")
# keeps extra info, eg on report action, why it is reported
extra_info = models.TextField(blank=True, null=True)
media = models.ForeignKey(
Media, on_delete=models.CASCADE, related_name="mediaactions"
)
media = models.ForeignKey(Media, on_delete=models.CASCADE, related_name="mediaactions")
action_date = models.DateTimeField(auto_now_add=True)
remote_ip = models.CharField(max_length=40, blank=True, null=True)

View File

@@ -1,4 +1,5 @@
from __future__ import absolute_import
from .celery import app as celery_app
__all__ = ["celery_app"]

View File

@@ -1,5 +1,7 @@
from __future__ import absolute_import
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")

View File

@@ -1,8 +1,9 @@
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
from collections import OrderedDict # requires Python 2.7 or later
from django.core.paginator import Paginator
from django.utils.functional import cached_property
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
class FasterDjangoPaginator(Paginator):

View File

@@ -1,5 +1,6 @@
from django.conf import settings
from rest_framework import permissions
from files.methods import is_mediacms_editor, is_mediacms_manager
@@ -24,7 +25,10 @@ class IsUserOrManager(permissions.BasePermission):
if is_mediacms_manager(request.user):
return True
return obj.user == request.user
if hasattr(obj, 'user'):
return obj.user == request.user
else:
return obj == request.user
class IsUserOrEditor(permissions.BasePermission):

View File

@@ -1,4 +1,5 @@
import os
from celery.schedules import crontab
DEBUG = False
@@ -16,7 +17,8 @@ CAN_ADD_MEDIA = "all"
# valid choices here are 'public', 'private', 'unlisted
PORTAL_WORKFLOW = "public"
DEFAULT_THEME = "black" # this is not taken under consideration currently
# valid values: 'light', 'dark'.
DEFAULT_THEME = "light"
# These are passed on every request
@@ -42,7 +44,11 @@ ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY = True
# ip of the server should be part of this
ALLOWED_HOSTS = ["*", "mediacms.io", "127.0.0.1", "localhost"]
FRONTEND_HOST = "http://localhost"
# this variable - along with SSL_FRONTEND_HOST is used on several places
# as email where a URL need appear etc
# FRONTEND_HOST needs an http prefix - at the end of the file
# there's a conversion to https with the SSL_FRONTEND_HOST env
INTERNAL_IPS = "127.0.0.1"
@@ -208,9 +214,7 @@ POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY = ""
CANNOT_ADD_MEDIA_MESSAGE = ""
# mp4hls command, part of Bendo4
MP4HLS_COMMAND = (
"/home/mediacms.io/mediacms/Bento4-SDK-1-6-0-632.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 = "c2b8e1838b6128asd333ddc5e24"
@@ -288,6 +292,7 @@ INSTALLED_APPS = [
"uploader.apps.UploaderConfig",
"djcelery_email",
"ckeditor",
"drf_yasg",
]
MIDDLEWARE = [
@@ -419,6 +424,7 @@ CELERY_BEAT_SCHEDULE = {
# TODO: beat, delete chunks from media root
# chunks_dir after xx days...(also uploads_dir)
LOCAL_INSTALL = False
try:

View File

@@ -1,7 +1,17 @@
from django.contrib import admin
from django.urls import path
from django.conf.urls import url, include
import debug_toolbar
from django.conf.urls import include, url
from django.contrib import admin
from django.urls import path, re_path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework.permissions import AllowAny
schema_view = get_schema_view(
openapi.Info(title="MediaCMS API", default_version='v1', contact=openapi.Contact(url="https://mediacms.io"), x_logo={"url": "../../static/images/logo_dark.svg"}),
public=True,
permission_classes=(AllowAny,),
)
urlpatterns = [
url(r"^__debug__/", include(debug_toolbar.urls)),
@@ -10,4 +20,7 @@ urlpatterns = [
url(r"^accounts/", include("allauth.urls")),
url(r"^api-auth/", include("rest_framework.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/$', 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'),
]

View File

@@ -4,12 +4,14 @@ set -e
# forward request and error logs to docker log collector
ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log && \
ln -sf /dev/stdout /var/log/nginx/mediacms.io.access.log && ln -sf /dev/stderr /var/log/nginx/mediacms.io.error.log
cp /home/mediacms.io/mediacms/deploy/docker/local_settings.py /home/mediacms.io/mediacms/cms/local_settings.py
mkdir -p /home/mediacms.io/mediacms/{logs,pids}
mkdir -p /home/mediacms.io/mediacms/{logs,pids,media_files/hls}
touch /home/mediacms.io/mediacms/logs/debug.log
chown -R www-data. /home/mediacms.io/
# Remove any dangling pids
rm -rf /home/mediacms.io/mediacms/pids/*
TARGET_GID=$(stat -c "%g" /home/mediacms.io/mediacms/)
@@ -25,6 +27,9 @@ else
usermod -a -G $GROUP www-data
fi
# We should do this only for folders that have a different owner, since it is an expensive operation
find /home/mediacms.io/ ! \( -user www-data -group $TARGET_GID \) -exec chown www-data:$TARGET_GID {} +
chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh
exec "$@"

View File

@@ -29,4 +29,6 @@ CACHES = {
BROKER_URL = REDIS_LOCATION
CELERY_RESULT_BACKEND = BROKER_URL
MP4HLS_COMMAND = "/home/mediacms.io/bento4/bin/mp4hls"
DEBUG = False

View File

@@ -7,15 +7,15 @@ server {
error_log /var/log/nginx/mediacms.io.error.log warn;
# redirect to https if logged in
if ($http_cookie ~* "sessionid") {
rewrite ^/(.*)$ https://localhost/$1 permanent;
}
# # redirect to https if logged in
# if ($http_cookie ~* "sessionid") {
# rewrite ^/(.*)$ https://localhost/$1 permanent;
# }
# redirect basic forms to https
location ~ (login|login_form|register|mail_password_form)$ {
rewrite ^/(.*)$ https://localhost/$1 permanent;
}
# # redirect basic forms to https
# location ~ (login|login_form|register|mail_password_form)$ {
# rewrite ^/(.*)$ https://localhost/$1 permanent;
# }
location /static {
alias /home/mediacms.io/mediacms/static ;

View File

@@ -9,7 +9,7 @@ services:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./deploy/docker/reverse_proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
migrations:
image: mediacms:latest
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
@@ -24,10 +24,7 @@ services:
db:
condition: service_healthy
web:
build:
context: .
target: runtime-image
image: mediacms:latest
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:
@@ -41,7 +38,7 @@ services:
depends_on:
- migrations
celery_beat:
image: mediacms:latest
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
@@ -53,7 +50,7 @@ services:
depends_on:
- redis
celery_worker:
image: mediacms:latest
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:

View File

@@ -11,7 +11,7 @@ services:
- ./deploy/docker/reverse_proxy/certs/:/etc/nginx/certs/
- ./deploy/docker/reverse_proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
migrations:
image: mediacms:latest
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
@@ -26,10 +26,7 @@ services:
db:
condition: service_healthy
web:
build:
context: .
target: runtime-image
image: mediacms:latest
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:
@@ -43,7 +40,7 @@ services:
depends_on:
- migrations
celery_beat:
image: mediacms:latest
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
@@ -55,7 +52,7 @@ services:
depends_on:
- redis
celery_worker:
image: mediacms:latest
image: mediacms/mediacms:latest
deploy:
replicas: 2
volumes:

View File

@@ -2,7 +2,7 @@ version: "3"
services:
migrations:
image: mediacms:latest
image: mediacms/mediacms:latest
volumes:
- ./deploy/docker/local_settings.py:/home/mediacms.io/mediacms/deploy/docker/local_settings.py
environment:
@@ -17,10 +17,7 @@ services:
db:
condition: service_healthy
web:
build:
context: .
target: runtime-image
image: mediacms:latest
image: mediacms/mediacms:latest
deploy:
replicas: 1
ports:
@@ -37,7 +34,7 @@ services:
depends_on:
- migrations
celery_beat:
image: mediacms:latest
image: mediacms/mediacms:latest
volumes:
- ./deploy/docker/local_settings.py:/home/mediacms.io/mediacms/deploy/docker/local_settings.py
environment:
@@ -49,7 +46,7 @@ services:
depends_on:
- redis
celery_worker:
image: mediacms:latest
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:

View File

@@ -2,7 +2,7 @@ version: "3"
services:
migrations:
image: mediacms:latest
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
@@ -17,10 +17,7 @@ services:
db:
condition: service_healthy
web:
build:
context: .
target: runtime-image
image: mediacms:latest
image: mediacms/mediacms:latest
deploy:
replicas: 1
ports:
@@ -35,7 +32,7 @@ services:
depends_on:
- migrations
celery_beat:
image: mediacms:latest
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
@@ -47,7 +44,7 @@ services:
depends_on:
- redis
celery_worker:
image: mediacms:latest
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:
@@ -80,4 +77,4 @@ services:
test: ["CMD", "redis-cli","ping"]
interval: 30s
timeout: 10s
retries: 3
retries: 3

View File

@@ -2,7 +2,11 @@
A number of options are available on `cms/settings.py`.
It is advisable to override any of them by adding it to `cms/local_settings.py` .
It is advisable to override any of them by adding it to `local_settings.py` .
In case of a the single server installation, add to `cms/local_settings.py` .
In case of a docker compose installation, add to `deploy/docker/local_settings.py` . This will automatically overwrite `cms/local_settings.py` .
Any change needs restart of MediaCMS in order to take effect. So edit `cms/local_settings.py`, make a change and restart MediaCMS
@@ -94,6 +98,14 @@ Make changes (True/False) to any of the following:
- CAN_SHARE_MEDIA = True # whether the share media appears
```
### show/hide the download option on a media
Edit `templates/config/installation/features.html` and set
```
download: false
```
### automatically hide media upon being reported
set a low number for variable `REPORTED_TIMES_THRESHOLD`

View File

@@ -37,4 +37,4 @@ The reverse proxy (`jwilder/nginx-proxy`) can be configured to provide SSL termi
The architecture below generalises all the deployment scenarios above, and provides a conceptual design for other deployments based on kubernetes and docker swarm. It allows for horizontal scaleability through the use of multiple mediacms_web instances and celery_workers. For large deployments, managed postgres, redis and storage may be adopted.
![MediaCMS](images/architecture.png)
![MediaCMS](images/architecture.png)

View File

@@ -1,14 +1,14 @@
from django.contrib import admin
from .models import (
Media,
Encoding,
EncodeProfile,
Category,
Comment,
Tag,
EncodeProfile,
Encoding,
Language,
Media,
Subtitle,
Tag,
)

View File

@@ -1,9 +1,9 @@
# ffmpeg only backend
from subprocess import PIPE, Popen
import locale
import re
import logging
import re
from subprocess import PIPE, Popen
logger = logging.getLogger(__name__)

View File

@@ -1,16 +1,12 @@
from django.conf import settings
from .methods import is_mediacms_editor, is_mediacms_manager
def stuff(request):
"""Pass settings to the frontend"""
ret = {}
if request.is_secure():
# in case session is https, pass this setting so
# that the frontend uses https too
ret["FRONTEND_HOST"] = settings.SSL_FRONTEND_HOST
else:
ret["FRONTEND_HOST"] = settings.FRONTEND_HOST
ret["FRONTEND_HOST"] = request.build_absolute_uri('/')
ret["DEFAULT_THEME"] = settings.DEFAULT_THEME
ret["PORTAL_NAME"] = settings.PORTAL_NAME
ret["LOAD_FROM_CDN"] = settings.LOAD_FROM_CDN
@@ -24,18 +20,12 @@ def stuff(request):
ret["UPLOAD_MAX_SIZE"] = settings.UPLOAD_MAX_SIZE
ret["UPLOAD_MAX_FILES_NUMBER"] = settings.UPLOAD_MAX_FILES_NUMBER
ret["PRE_UPLOAD_MEDIA_MESSAGE"] = settings.PRE_UPLOAD_MEDIA_MESSAGE
ret[
"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_EDITOR"] = is_mediacms_editor(request.user)
ret["IS_MEDIACMS_MANAGER"] = is_mediacms_manager(request.user)
ret["ALLOW_RATINGS"] = settings.ALLOW_RATINGS
ret[
"ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY"
] = settings.ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY
ret[
"VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE"
] = settings.VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE
ret["ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY"] = settings.ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY
ret["VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE"] = settings.VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE
ret["RSS_URL"] = "/rss"
return ret

View File

@@ -1,12 +1,12 @@
from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Rss201rev2Feed
from django.urls import reverse
from django.db.models import Q
from django.conf import settings
from django.contrib.postgres.search import SearchQuery
from django.contrib.syndication.views import Feed
from django.db.models import Q
from django.urls import reverse
from django.utils.feedgenerator import Rss201rev2Feed
from .models import Media, Category
from . import helpers
from .models import Category, Media
from .stop_words import STOP_WORDS
@@ -119,11 +119,7 @@ class SearchRSSFeed(Feed):
elif query:
# same as on files.views.MediaSearch: move this processing to a prepare_query function
query = helpers.clean_query(query)
q_parts = [
q_part.rstrip("y")
for q_part in query.split()
if q_part not in STOP_WORDS
]
q_parts = [q_part.rstrip("y") for q_part in query.split() if q_part not in STOP_WORDS]
if q_parts:
query = SearchQuery(q_parts[0] + ":*", search_type="raw")
for part in q_parts[1:]:

View File

@@ -1,6 +1,7 @@
from django import forms
from .methods import get_next_state, is_mediacms_editor
from .models import Media, Subtitle
from .methods import is_mediacms_editor, get_next_state
class MultipleSelect(forms.CheckboxSelectMultiple):
@@ -8,9 +9,7 @@ class MultipleSelect(forms.CheckboxSelectMultiple):
class MediaForm(forms.ModelForm):
new_tags = forms.CharField(
label="Tags", help_text="a comma separated list of new tags.", required=False
)
new_tags = forms.CharField(label="Tags", help_text="a comma separated list of new tags.", required=False)
class Meta:
model = Media
@@ -27,7 +26,7 @@ class MediaForm(forms.ModelForm):
"thumbnail_time",
"reported_times",
"is_reviewed",
"allow_download"
"allow_download",
)
widgets = {
"tags": MultipleSelect(),
@@ -42,9 +41,7 @@ class MediaForm(forms.ModelForm):
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()])
def clean_uploaded_poster(self):
image = self.cleaned_data.get("uploaded_poster", False)
@@ -57,9 +54,7 @@ class MediaForm(forms.ModelForm):
data = self.cleaned_data
state = data.get("state")
if state != self.initial["state"]:
self.instance.state = get_next_state(
self.user, self.initial["state"], self.instance.state
)
self.instance.state = get_next_state(self.user, self.initial["state"], self.instance.state)
media = super(MediaForm, self).save(*args, **kwargs)
return media

View File

@@ -1,19 +1,19 @@
# Kudos to Werner Robitza, AVEQ GmbH, for helping with ffmpeg
# related content
import os
import math
import shutil
import tempfile
import random
import hashlib
import subprocess
import json
import math
import os
import random
import shutil
import subprocess
import tempfile
from fractions import Fraction
import filetype
from django.conf import settings
CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
CRF_ENCODING_NUM_SECONDS = 2 # 0 * 60 # videos with greater duration will get
@@ -168,9 +168,7 @@ def rm_dir(directory):
def url_from_path(filename):
# TODO: find a way to preserver http - https ...
return "{0}{1}".format(
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):
@@ -210,9 +208,7 @@ def run_command(cmd, cwd=None):
cmd = cmd.split()
ret = {}
if cwd:
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd
)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
else:
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
@@ -331,9 +327,7 @@ def media_file_info(input_file):
except ValueError:
hms, msec = duration_str.split(",")
total_dur = sum(
int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":")))
)
total_dur = sum(int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":"))))
video_duration = total_dur + float("0." + msec)
else:
# fallback to format, eg for webm
@@ -370,7 +364,7 @@ def media_file_info(input_file):
input_file,
]
stdout = run_command(cmd).get("out")
stream_size = sum([int(l) for l in stdout.split("\n") if l != ""])
stream_size = sum([int(line) for line in stdout.split("\n") if line != ""])
video_bitrate = round((stream_size * 8 / 1024.0) / video_duration, 2)
ret = {
@@ -396,9 +390,7 @@ def media_file_info(input_file):
hms, msec = duration_str.split(".")
except ValueError:
hms, msec = duration_str.split(",")
total_dur = sum(
int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":")))
)
total_dur = sum(int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":"))))
audio_duration = total_dur + float("0." + msec)
else:
# fallback to format, eg for webm
@@ -432,7 +424,7 @@ def media_file_info(input_file):
input_file,
]
stdout = run_command(cmd).get("out")
stream_size = sum([int(l) for l in stdout.split("\n") if l != ""])
stream_size = sum([int(line) for line in stdout.split("\n") if line != ""])
audio_bitrate = round((stream_size * 8 / 1024.0) / audio_duration, 2)
ret.update(
@@ -660,9 +652,7 @@ def get_base_ffmpeg_command(
return cmd
def produce_ffmpeg_commands(
media_file, media_info, resolution, codec, output_filename, pass_file, chunk=False
):
def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_filename, pass_file, chunk=False):
try:
media_info = json.loads(media_info)
except BaseException:
@@ -670,13 +660,13 @@ def produce_ffmpeg_commands(
if codec == "h264":
encoder = "libx264"
ext = "mp4"
# ext = "mp4"
elif codec in ["h265", "hevc"]:
encoder = "libx265"
ext = "mp4"
# ext = "mp4"
elif codec == "vp9":
encoder = "libvpx-vp9"
ext = "webm"
# ext = "webm"
else:
return False
@@ -699,9 +689,7 @@ def produce_ffmpeg_commands(
# else:
# adjust the target frame rate if the input is fractional
target_fps = (
src_framerate if isinstance(src_framerate, int) else math.ceil(src_framerate)
)
target_fps = src_framerate if isinstance(src_framerate, int) else math.ceil(src_framerate)
if media_info.get("video_duration") > CRF_ENCODING_NUM_SECONDS:
enc_type = "crf"

View File

@@ -1,16 +1,18 @@
from rest_framework.views import APIView
from rest_framework.parsers import JSONParser
from rest_framework.settings import api_settings
from rest_framework.response import Response
from drf_yasg import openapi as openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework.parsers import JSONParser
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.views import APIView
from users.models import User
from users.serializers import UserSerializer
from .permissions import IsMediacmsEditor
from .models import Media, Comment
from .methods import is_mediacms_manager
from .serializers import MediaSerializer, CommentSerializer
from .methods import is_mediacms_manager
from .models import Comment, Media
from .permissions import IsMediacmsEditor
from .serializers import CommentSerializer, MediaSerializer
class MediaList(APIView):
@@ -23,6 +25,17 @@ class MediaList(APIView):
permission_classes = (IsMediacmsEditor,)
parser_classes = (JSONParser,)
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(name='sort_by', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='Sort by any of: title, add_date, edit_date, views, likes, reported_times'),
openapi.Parameter(name='ordering', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='Order by: asc, desc'),
openapi.Parameter(name='state', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='Media state, options: private", "public", "unlisted'),
openapi.Parameter(name='encoding_status', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='Encoding status, options "pending", "running", "fail", "success"'),
],
tags=['Manage'],
operation_summary='Manage Media',
operation_description='Manage media for MediaCMS managers and reviewers',
)
def get(self, request, format=None):
params = self.request.query_params
ordering = params.get("ordering", "").strip()
@@ -94,6 +107,12 @@ class MediaList(APIView):
serializer = MediaSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data)
@swagger_auto_schema(
manual_parameters=[],
tags=['Manage'],
operation_summary='Delete Media',
operation_description='Delete media for MediaCMS managers and reviewers',
)
def delete(self, request, format=None):
tokens = request.GET.get("tokens")
if tokens:
@@ -112,6 +131,12 @@ class CommentList(APIView):
permission_classes = (IsMediacmsEditor,)
parser_classes = (JSONParser,)
@swagger_auto_schema(
manual_parameters=[],
tags=['Manage'],
operation_summary='Manage Comments',
operation_description='Manage comments for MediaCMS managers and reviewers',
)
def get(self, request, format=None):
params = self.request.query_params
ordering = params.get("ordering", "").strip()
@@ -137,6 +162,12 @@ class CommentList(APIView):
serializer = CommentSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data)
@swagger_auto_schema(
manual_parameters=[],
tags=['Manage'],
operation_summary='Delete Comments',
operation_description='Delete comments for MediaCMS managers and reviewers',
)
def delete(self, request, format=None):
comment_ids = request.GET.get("comment_ids")
if comment_ids:
@@ -156,6 +187,12 @@ class UserList(APIView):
permission_classes = (IsMediacmsEditor,)
parser_classes = (JSONParser,)
@swagger_auto_schema(
manual_parameters=[],
tags=['Manage'],
operation_summary='Manage Users',
operation_description='Manage users for MediaCMS managers and reviewers',
)
def get(self, request, format=None):
params = self.request.query_params
ordering = params.get("ordering", "").strip()
@@ -187,11 +224,15 @@ class UserList(APIView):
serializer = UserSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data)
@swagger_auto_schema(
manual_parameters=[],
tags=['Manage'],
operation_summary='Delete Users',
operation_description='Delete users for MediaCMS managers',
)
def delete(self, request, format=None):
if not is_mediacms_manager(request.user):
return Response(
{"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
tokens = request.GET.get("tokens")
if tokens:

View File

@@ -1,15 +1,17 @@
# Kudos to Werner Robitza, AVEQ GmbH, for helping with ffmpeg
# related content
import itertools
import logging
import random
import itertools
from datetime import datetime
from cms import celery_app
from django.conf import settings
from django.core.cache import cache
from django.db.models import Q
from django.core.mail import EmailMessage
from django.db.models import Q
from cms import celery_app
from . import models
from .helpers import mask_ip
@@ -48,9 +50,7 @@ def pre_save_action(media, user, session_key, action, remote_ip):
if user:
query = MediaAction.objects.filter(media=media, action=action, user=user)
else:
query = MediaAction.objects.filter(
media=media, action=action, session_key=session_key
)
query = MediaAction.objects.filter(media=media, action=action, session_key=session_key)
query = query.order_by("-action_date")
if query:
@@ -71,11 +71,7 @@ def pre_save_action(media, user, session_key, action, remote_ip):
# perform some checking for requests where no session
# id is specified (and user is anonymous) to avoid spam
# eg allow for the same remote_ip for a specific number of actions
query = (
MediaAction.objects.filter(media=media, action=action, remote_ip=remote_ip)
.filter(user=None)
.order_by("-action_date")
)
query = MediaAction.objects.filter(media=media, action=action, remote_ip=remote_ip).filter(user=None).order_by("-action_date")
if query:
query = query.first()
now = datetime.now(query.action_date.tzinfo)
@@ -204,11 +200,8 @@ URL: %s
d["to"] = [media.user.email]
notify_items.append(d)
for item in notify_items:
email = EmailMessage(
item["title"], item["msg"], settings.DEFAULT_FROM_EMAIL, item["to"]
)
email = EmailMessage(item["title"], item["msg"], settings.DEFAULT_FROM_EMAIL, item["to"])
email.send(fail_silently=True)
return True
@@ -222,17 +215,9 @@ def show_recommended_media(request, limit=100):
pmi = cache.get("popular_media_ids")
# produced by task get_list_of_popular_media and cached
if pmi:
media = list(
models.Media.objects.filter(friendly_token__in=pmi)
.filter(basic_query)
.prefetch_related("user")[:limit]
)
media = list(models.Media.objects.filter(friendly_token__in=pmi).filter(basic_query).prefetch_related("user")[:limit])
else:
media = list(
models.Media.objects.filter(basic_query)
.order_by("-views", "-likes")
.prefetch_related("user")[:limit]
)
media = list(models.Media.objects.filter(basic_query).order_by("-views", "-likes").prefetch_related("user")[:limit])
random.shuffle(media)
return media
@@ -257,11 +242,7 @@ def show_related_media_content(media, request, limit):
# and include author videos in any case
q_author = Q(listable=True, user=media.user)
m = list(
models.Media.objects.filter(q_author)
.order_by()
.prefetch_related("user")[:limit]
)
m = list(models.Media.objects.filter(q_author).order_by().prefetch_related("user")[:limit])
# order by random criteria so that it doesn't bring the same results
# attention: only fields that are indexed make sense here! also need
@@ -282,20 +263,12 @@ def show_related_media_content(media, request, limit):
category = media.category.first()
if category:
q_category = Q(listable=True, category=category)
q_res = (
models.Media.objects.filter(q_category)
.order_by(order_criteria[random.randint(0, len(order_criteria) - 1)])
.prefetch_related("user")[: limit - media.user.media_count]
)
q_res = models.Media.objects.filter(q_category).order_by(order_criteria[random.randint(0, len(order_criteria) - 1)]).prefetch_related("user")[: limit - media.user.media_count]
m = list(itertools.chain(m, q_res))
if len(m) < limit:
q_generic = Q(listable=True)
q_res = (
models.Media.objects.filter(q_generic)
.order_by(order_criteria[random.randint(0, len(order_criteria) - 1)])
.prefetch_related("user")[: limit - media.user.media_count]
)
q_res = models.Media.objects.filter(q_generic).order_by(order_criteria[random.randint(0, len(order_criteria) - 1)]).prefetch_related("user")[: limit - media.user.media_count]
m = list(itertools.chain(m, q_res))
m = list(set(m[:limit])) # remove duplicates
@@ -313,11 +286,7 @@ def show_related_media_author(media, request, limit):
"""Return a list of related media form the same author"""
q_author = Q(listable=True, user=media.user)
m = list(
models.Media.objects.filter(q_author)
.order_by()
.prefetch_related("user")[:limit]
)
m = list(models.Media.objects.filter(q_author).order_by().prefetch_related("user")[:limit])
# order by random criteria so that it doesn't bring the same results
# attention: only fields that are indexed make sense here! also need
@@ -347,13 +316,7 @@ def update_user_ratings(user, media, user_ratings):
"""Populate user ratings for a media"""
for rating in user_ratings:
user_rating = (
models.Rating.objects.filter(
user=user, media_id=media, rating_category_id=rating.get("category_id")
)
.only("score")
.first()
)
user_rating = models.Rating.objects.filter(user=user, media_id=media, rating_category_id=rating.get("category_id")).only("score").first()
if user_rating:
rating["score"] = user_rating.score
return user_ratings
@@ -379,9 +342,7 @@ View it on %s
media.title,
media_url,
)
email = EmailMessage(
title, msg, settings.DEFAULT_FROM_EMAIL, [media.user.email]
)
email = EmailMessage(title, msg, settings.DEFAULT_FROM_EMAIL, [media.user.email])
email.send(fail_silently=True)
return True
@@ -420,27 +381,17 @@ def list_tasks():
friendly_token = task_args.split()[0]
profile_id = task_args.split()[1]
media = models.Media.objects.filter(
friendly_token=friendly_token
).first()
media = models.Media.objects.filter(friendly_token=friendly_token).first()
if media:
profile = models.EncodeProfile.objects.filter(
id=profile_id
).first()
profile = models.EncodeProfile.objects.filter(id=profile_id).first()
if profile:
media_profile_pairs.append(
(media.friendly_token, profile.id)
)
media_profile_pairs.append((media.friendly_token, profile.id))
task_dict["info"] = {}
task_dict["info"]["profile name"] = profile.name
task_dict["info"]["media title"] = media.title
encoding = models.Encoding.objects.filter(
task_id=task.get("id")
).first()
encoding = models.Encoding.objects.filter(task_id=task.get("id")).first()
if encoding:
task_dict["info"][
"encoding progress"
] = encoding.progress
task_dict["info"]["encoding progress"] = encoding.progress
ret[state]["tasks"].append(task_dict)
ret["task_ids"] = task_ids

View File

@@ -1,11 +1,13 @@
# Generated by Django 3.1.4 on 2020-12-01 07:12
import django.contrib.postgres.search
from django.db import migrations, models
import files.models
import imagekit.models.fields
import uuid
import django.contrib.postgres.search
import imagekit.models.fields
from django.db import migrations, models
import files.models
class Migration(migrations.Migration):
@@ -32,9 +34,7 @@ class Migration(migrations.Migration):
("description", models.TextField(blank=True)),
(
"is_global",
models.BooleanField(
default=False, help_text="global categories or user specific"
),
models.BooleanField(default=False, help_text="global categories or user specific"),
),
(
"media_count",
@@ -42,9 +42,7 @@ class Migration(migrations.Migration):
),
(
"thumbnail",
imagekit.models.fields.ProcessedImageField(
blank=True, upload_to=files.models.category_thumb_path
),
imagekit.models.fields.ProcessedImageField(blank=True, upload_to=files.models.category_thumb_path),
),
(
"listings_thumbnail",
@@ -153,9 +151,7 @@ class Migration(migrations.Migration):
("commands", models.TextField(blank=True, help_text="commands run")),
(
"chunk",
models.BooleanField(
db_index=True, default=False, help_text="is chunk?"
),
models.BooleanField(db_index=True, default=False, help_text="is chunk?"),
),
("chunk_file_path", models.CharField(blank=True, max_length=400)),
("chunks_info", models.TextField(blank=True)),
@@ -317,9 +313,7 @@ class Migration(migrations.Migration):
("likes", models.IntegerField(db_index=True, default=1)),
(
"listable",
models.BooleanField(
default=False, help_text="Whether it will appear on listings"
),
models.BooleanField(default=False, help_text="Whether it will appear on listings"),
),
(
"md5sum",
@@ -341,9 +335,7 @@ class Migration(migrations.Migration):
),
(
"media_info",
models.TextField(
blank=True, help_text="extracted media metadata info"
),
models.TextField(blank=True, help_text="extracted media metadata info"),
),
(
"media_type",
@@ -387,9 +379,7 @@ class Migration(migrations.Migration):
),
(
"reported_times",
models.IntegerField(
default=0, help_text="how many time a Medis is reported"
),
models.IntegerField(default=0, help_text="how many time a Medis is reported"),
),
(
"search",
@@ -485,9 +475,7 @@ class Migration(migrations.Migration):
),
(
"user_featured",
models.BooleanField(
default=False, help_text="Featured by the user"
),
models.BooleanField(default=False, help_text="Featured by the user"),
),
("video_height", models.IntegerField(default=1)),
("views", models.IntegerField(db_index=True, default=1)),

View File

@@ -1,10 +1,10 @@
# Generated by Django 3.1.4 on 2020-12-01 07:12
from django.conf import settings
import django.contrib.postgres.indexes
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
@@ -31,9 +31,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="subtitle",
name="language",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="files.language"
),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.language"),
),
migrations.AddField(
model_name="subtitle",
@@ -47,9 +45,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="subtitle",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name="rating",
@@ -63,37 +59,27 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="rating",
name="rating_category",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="files.ratingcategory"
),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.ratingcategory"),
),
migrations.AddField(
model_name="rating",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name="playlistmedia",
name="media",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="files.media"
),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.media"),
),
migrations.AddField(
model_name="playlistmedia",
name="playlist",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="files.playlist"
),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.playlist"),
),
migrations.AddField(
model_name="playlist",
name="media",
field=models.ManyToManyField(
blank=True, through="files.PlaylistMedia", to="files.Media"
),
field=models.ManyToManyField(blank=True, through="files.PlaylistMedia", to="files.Media"),
),
migrations.AddField(
model_name="playlist",
@@ -173,9 +159,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="encoding",
name="profile",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="files.encodeprofile"
),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.encodeprofile"),
),
migrations.AddField(
model_name="comment",
@@ -200,9 +184,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="comment",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name="category",
@@ -216,9 +198,7 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="rating",
index=models.Index(
fields=["user", "media"], name="files_ratin_user_id_72ca6a_idx"
),
index=models.Index(fields=["user", "media"], name="files_ratin_user_id_72ca6a_idx"),
),
migrations.AlterUniqueTogether(
name="rating",
@@ -226,8 +206,6 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="media",
index=django.contrib.postgres.indexes.GinIndex(
fields=["search"], name="files_media_search_7194c6_gin"
),
index=django.contrib.postgres.indexes.GinIndex(fields=["search"], name="files_media_search_7194c6_gin"),
),
]

View File

@@ -1,28 +1,27 @@
import json
import logging
import uuid
import os
import random
import re
import tempfile
import random
import json
import uuid
import m3u8
from django.utils import timezone
from django.db import connection
from django.db import models
from django.template.defaultfilters import slugify
from django.conf import settings
from django.contrib.postgres.indexes import GinIndex
from django.db.models.signals import pre_delete, post_delete, post_save, m2m_changed
from django.core.files import File
from django.core.exceptions import ValidationError
from django.dispatch import receiver
from django.urls import reverse
from django.utils.html import strip_tags
from django.contrib.postgres.search import SearchVectorField
from mptt.models import MPTTModel, TreeForeignKey
from imagekit.processors import ResizeToFit
from django.core.exceptions import ValidationError
from django.core.files import File
from django.db import connection, models
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
from django.dispatch import receiver
from django.template.defaultfilters import slugify
from django.urls import reverse
from django.utils import timezone
from django.utils.html import strip_tags
from imagekit.models import ProcessedImageField
from imagekit.processors import ResizeToFit
from mptt.models import MPTTModel, TreeForeignKey
from . import helpers
from .methods import notify_users
@@ -88,36 +87,26 @@ ENCODE_RESOLUTIONS_KEYS = [resolution for resolution, name in ENCODE_RESOLUTIONS
def original_media_file_path(instance, filename):
"""Helper function to place original media file"""
file_name = "{0}.{1}".format(instance.uid.hex, helpers.get_file_name(filename))
return settings.MEDIA_UPLOAD_DIR + "user/{0}/{1}".format(
instance.user.username, file_name
)
return settings.MEDIA_UPLOAD_DIR + "user/{0}/{1}".format(instance.user.username, file_name)
def encoding_media_file_path(instance, filename):
"""Helper function to place encoded media file"""
file_name = "{0}.{1}".format(
instance.media.uid.hex, helpers.get_file_name(filename)
)
return settings.MEDIA_ENCODING_DIR + "{0}/{1}/{2}".format(
instance.profile.id, instance.media.user.username, file_name
)
file_name = "{0}.{1}".format(instance.media.uid.hex, helpers.get_file_name(filename))
return settings.MEDIA_ENCODING_DIR + "{0}/{1}/{2}".format(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 + "user/{0}/{1}".format(
instance.user.username, filename
)
return settings.THUMBNAIL_UPLOAD_DIR + "user/{0}/{1}".format(instance.user.username, filename)
def subtitles_file_path(instance, filename):
"""Helper function to place subtitle file"""
return settings.SUBTITLES_UPLOAD_DIR + "user/{0}/{1}".format(
instance.media.user.username, filename
)
return settings.SUBTITLES_UPLOAD_DIR + "user/{0}/{1}".format(instance.media.user.username, filename)
def category_thumb_path(instance, filename):
@@ -130,17 +119,11 @@ def category_thumb_path(instance, filename):
class Media(models.Model):
"""The most important model for MediaCMS"""
add_date = models.DateTimeField(
"Date produced", blank=True, null=True, db_index=True
)
add_date = models.DateTimeField("Date produced", blank=True, null=True, db_index=True)
allow_download = models.BooleanField(
default=True, help_text="Whether option to download media is shown"
)
allow_download = models.BooleanField(default=True, help_text="Whether option to download media is shown")
category = models.ManyToManyField(
"Category", blank=True, help_text="Media can be part of one or more categories"
)
category = models.ManyToManyField("Category", blank=True, help_text="Media can be part of one or more categories")
channel = models.ForeignKey(
"users.Channel",
@@ -158,13 +141,9 @@ class Media(models.Model):
edit_date = models.DateTimeField(auto_now=True)
enable_comments = models.BooleanField(
default=True, help_text="Whether comments will be allowed for this media"
)
enable_comments = models.BooleanField(default=True, help_text="Whether comments will be allowed for this media")
encoding_status = models.CharField(
max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending", db_index=True
)
encoding_status = models.CharField(max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending", db_index=True)
featured = models.BooleanField(
default=False,
@@ -172,13 +151,9 @@ class Media(models.Model):
help_text="Whether media is globally featured by a MediaCMS editor",
)
friendly_token = models.CharField(
blank=True, max_length=12, db_index=True, help_text="Identifier for the Media"
)
friendly_token = models.CharField(blank=True, max_length=12, db_index=True, help_text="Identifier for the Media")
hls_file = models.CharField(
max_length=1000, blank=True, help_text="Path to HLS file for videos"
)
hls_file = models.CharField(max_length=1000, blank=True, help_text="Path to HLS file for videos")
is_reviewed = models.BooleanField(
default=settings.MEDIA_IS_REVIEWED,
@@ -186,19 +161,13 @@ class Media(models.Model):
help_text="Whether media is reviewed, so it can appear on public listings",
)
license = models.ForeignKey(
"License", on_delete=models.CASCADE, db_index=True, blank=True, null=True
)
license = models.ForeignKey("License", on_delete=models.CASCADE, db_index=True, blank=True, null=True)
likes = models.IntegerField(db_index=True, default=1)
listable = models.BooleanField(
default=False, help_text="Whether it will appear on listings"
)
listable = models.BooleanField(default=False, help_text="Whether it will appear on listings")
md5sum = models.CharField(
max_length=50, blank=True, null=True, help_text="Not exposed, used internally"
)
md5sum = models.CharField(max_length=50, blank=True, null=True, help_text="Not exposed, used internally")
media_file = models.FileField(
"media file",
@@ -217,9 +186,7 @@ class Media(models.Model):
default="video",
)
password = models.CharField(
max_length=100, blank=True, help_text="password for private media"
)
password = models.CharField(max_length=100, blank=True, help_text="password for private media")
preview_file_path = models.CharField(
max_length=500,
@@ -243,9 +210,7 @@ class Media(models.Model):
help_text="Rating category, if media Rating is allowed",
)
reported_times = models.IntegerField(
default=0, help_text="how many time a Medis is reported"
)
reported_times = models.IntegerField(default=0, help_text="how many time a Medis is reported")
search = SearchVectorField(
null=True,
@@ -274,13 +239,9 @@ class Media(models.Model):
help_text="state of Media",
)
tags = models.ManyToManyField(
"Tag", blank=True, help_text="select one or more out of the existing tags"
)
tags = models.ManyToManyField("Tag", blank=True, help_text="select one or more out of the existing tags")
title = models.CharField(
max_length=100, help_text="media title", blank=True, db_index=True
)
title = models.CharField(max_length=100, help_text="media title", blank=True, db_index=True)
thumbnail = ProcessedImageField(
upload_to=original_thumbnail_file_path,
@@ -292,13 +253,9 @@ class Media(models.Model):
help_text="media extracted small thumbnail, shown on listings",
)
thumbnail_time = models.FloatField(
blank=True, null=True, help_text="Time on video that a thumbnail will be taken"
)
thumbnail_time = models.FloatField(blank=True, null=True, help_text="Time on video that a thumbnail will be taken")
uid = models.UUIDField(
unique=True, default=uuid.uuid4, help_text="A unique identifier for the Media"
)
uid = models.UUIDField(unique=True, default=uuid.uuid4, help_text="A unique identifier for the Media")
uploaded_thumbnail = ProcessedImageField(
upload_to=original_thumbnail_file_path,
@@ -321,9 +278,7 @@ class Media(models.Model):
max_length=500,
)
user = models.ForeignKey(
"users.User", on_delete=models.CASCADE, help_text="user that uploads the media"
)
user = models.ForeignKey("users.User", on_delete=models.CASCADE, help_text="user that uploads the media")
user_featured = models.BooleanField(default=False, help_text="Featured by the user")
@@ -406,11 +361,7 @@ class Media(models.Model):
self.state = helpers.get_default_state(user=self.user)
# condition to appear on listings
if (
self.state == "public"
and self.encoding_status == "success"
and self.is_reviewed == True
):
if self.state == "public" and self.encoding_status == "success" and self.is_reviewed is True:
self.listable = True
else:
self.listable = False
@@ -419,10 +370,7 @@ class Media(models.Model):
# produce a thumbnail out of an uploaded poster
# will run only when a poster is uploaded for the first time
if (
self.uploaded_poster
and self.uploaded_poster != self.__original_uploaded_poster
):
if self.uploaded_poster and self.uploaded_poster != self.__original_uploaded_poster:
with open(self.uploaded_poster.path, "rb") as f:
# set this otherwise gets to infinite loop
@@ -458,9 +406,7 @@ class Media(models.Model):
]
items = [item for item in items if item]
text = " ".join(items)
text = " ".join(
[token for token in text.lower().split(" ") if token not in STOP_WORDS]
)
text = " ".join([token for token in text.lower().split(" ") if token not in STOP_WORDS])
sql_code = """
UPDATE {db_table} SET search = to_tsvector(
@@ -561,9 +507,7 @@ class Media(models.Model):
if self.media_type == "image":
with open(self.media_file.path, "rb") as f:
myfile = File(f)
thumbnail_name = (
helpers.get_file_name(self.media_file.path) + ".jpg"
)
thumbnail_name = helpers.get_file_name(self.media_file.path) + ".jpg"
self.thumbnail.save(content=myfile, name=thumbnail_name)
self.poster.save(content=myfile, name=thumbnail_name)
return True
@@ -585,9 +529,7 @@ class Media(models.Model):
command = [
settings.FFMPEG_COMMAND,
"-ss",
str(
thumbnail_time
), # -ss need to be firt here otherwise time taken is huge
str(thumbnail_time), # -ss need to be firt here otherwise time taken is huge
"-i",
self.media_file.path,
"-vframes",
@@ -595,7 +537,7 @@ class Media(models.Model):
"-y",
tf,
]
ret = helpers.run_command(command)
helpers.run_command(command)
if os.path.exists(tf) and helpers.get_file_type(tf) == "image":
with open(tf, "rb") as f:
@@ -650,10 +592,7 @@ class Media(models.Model):
for profile in profiles:
if profile.extension != "gif":
if self.video_height and self.video_height < profile.resolution:
if (
profile.resolution
not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE
):
if profile.resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
continue
encoding = Encoding(media=self, profile=profile)
encoding.save()
@@ -688,12 +627,7 @@ class Media(models.Model):
self.save(update_fields=["encoding_status", "listable"])
if (
encoding
and encoding.status == "success"
and encoding.profile.codec == "h264"
and action == "add"
):
if encoding and encoding.status == "success" and encoding.profile.codec == "h264" and action == "add":
from . import tasks
tasks.create_hls(self.friendly_token)
@@ -704,10 +638,7 @@ class Media(models.Model):
"""Set encoding_status for videos
Set success if at least one mp4 exists
"""
mp4_statuses = set(
encoding.status
for encoding in self.encodings.filter(profile__extension="mp4", chunk=False)
)
mp4_statuses = set(encoding.status for encoding in self.encodings.filter(profile__extension="mp4", chunk=False))
if not mp4_statuses:
encoding_status = "pending"
@@ -726,7 +657,6 @@ class Media(models.Model):
"""Property used on serializers"""
ret = {}
chunks_ret = {}
if self.media_type not in ["video"]:
return ret
@@ -752,12 +682,8 @@ class Media(models.Model):
extra.append(encoding.profile.codec)
for codec in extra:
ret[resolution][codec] = {}
v = self.encodings.filter(chunk=True, profile__codec=codec).values(
"progress"
)
ret[resolution][codec]["progress"] = (
sum([p["progress"] for p in v]) / v.count()
)
v = self.encodings.filter(chunk=True, profile__codec=codec).values("progress")
ret[resolution][codec]["progress"] = sum([p["progress"] for p in v]) / v.count()
# TODO; status/logs/errors
return ret
@@ -897,19 +823,13 @@ class Media(models.Model):
for iframe_playlist in m3u8_obj.iframe_playlists:
uri = os.path.join(p, iframe_playlist.uri)
if os.path.exists(uri):
resolution = iframe_playlist.iframe_stream_info.resolution[
1
]
res["{}_iframe".format(resolution)] = helpers.url_from_path(
uri
)
resolution = iframe_playlist.iframe_stream_info.resolution[1]
res["{}_iframe".format(resolution)] = helpers.url_from_path(uri)
for playlist in m3u8_obj.playlists:
uri = os.path.join(p, playlist.uri)
if os.path.exists(uri):
resolution = playlist.stream_info.resolution[1]
res[
"{}_playlist".format(resolution)
] = helpers.url_from_path(uri)
res["{}_playlist".format(resolution)] = helpers.url_from_path(uri)
return res
@property
@@ -930,9 +850,7 @@ class Media(models.Model):
if edit:
return reverse("edit_media") + "?m={0}".format(self.friendly_token)
if api:
return reverse(
"api_get_media", kwargs={"friendly_token": self.friendly_token}
)
return reverse("api_get_media", kwargs={"friendly_token": self.friendly_token})
else:
return reverse("get_media") + "?m={0}".format(self.friendly_token)
@@ -988,13 +906,9 @@ class Category(models.Model):
description = models.TextField(blank=True)
user = models.ForeignKey(
"users.User", on_delete=models.CASCADE, blank=True, null=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"
)
is_global = models.BooleanField(default=False, help_text="global categories or user specific")
media_count = models.IntegerField(default=0, help_text="number of media")
@@ -1006,9 +920,7 @@ class Category(models.Model):
blank=True,
)
listings_thumbnail = models.CharField(
max_length=400, blank=True, null=True, help_text="Thumbnail to show on listings"
)
listings_thumbnail = models.CharField(max_length=400, blank=True, null=True, help_text="Thumbnail to show on listings")
def __str__(self):
return self.title
@@ -1039,11 +951,7 @@ class Category(models.Model):
if self.thumbnail:
return helpers.url_from_path(self.thumbnail.path)
media = (
Media.objects.filter(category=self, state="public")
.order_by("-views")
.first()
)
media = Media.objects.filter(category=self, state="public").order_by("-views").first()
if media:
return media.thumbnail_url
@@ -1061,9 +969,7 @@ class Tag(models.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
)
user = models.ForeignKey("users.User", on_delete=models.CASCADE, blank=True, null=True)
media_count = models.IntegerField(default=0, help_text="number of media")
@@ -1085,9 +991,7 @@ class Tag(models.Model):
return reverse("search") + "?t={0}".format(self.title)
def update_tag_media(self):
self.media_count = Media.objects.filter(
state="public", is_reviewed=True, tags=self
).count()
self.media_count = Media.objects.filter(state="public", is_reviewed=True, tags=self).count()
self.save(update_fields=["media_count"])
return True
@@ -1102,9 +1006,7 @@ class Tag(models.Model):
def thumbnail_url(self):
if self.listings_thumbnail:
return self.listings_thumbnail
media = (
Media.objects.filter(tags=self, state="public").order_by("-views").first()
)
media = Media.objects.filter(tags=self, state="public").order_by("-views").first()
if media:
return media.thumbnail_url
@@ -1154,9 +1056,7 @@ class Encoding(models.Model):
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
)
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)
@@ -1168,9 +1068,7 @@ class Encoding(models.Model):
size = models.CharField(max_length=20, blank=True)
status = models.CharField(
max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending"
)
status = models.CharField(max_length=20, choices=MEDIA_ENCODING_STATUS, default="pending")
temp_file = models.CharField(max_length=400, blank=True)
@@ -1305,9 +1203,7 @@ class Rating(models.Model):
unique_together = ("user", "media", "rating_category")
def __str__(self):
return "{0}, rate for {1} for category {2}".format(
self.user.username, self.media.title, self.rating_category.title
)
return "{0}, rate for {1} for category {2}".format(self.user.username, self.media.title, self.rating_category.title)
class Playlist(models.Model):
@@ -1325,9 +1221,7 @@ class Playlist(models.Model):
uid = models.UUIDField(unique=True, default=uuid.uuid4)
user = models.ForeignKey(
"users.User", on_delete=models.CASCADE, db_index=True, related_name="playlists"
)
user = models.ForeignKey("users.User", on_delete=models.CASCADE, db_index=True, related_name="playlists")
def __str__(self):
return self.title
@@ -1338,13 +1232,9 @@ class Playlist(models.Model):
def get_absolute_url(self, api=False):
if api:
return reverse(
"api_get_playlist", kwargs={"friendly_token": self.friendly_token}
)
return reverse("api_get_playlist", kwargs={"friendly_token": self.friendly_token})
else:
return reverse(
"get_playlist", kwargs={"friendly_token": self.friendly_token}
)
return reverse("get_playlist", kwargs={"friendly_token": self.friendly_token})
@property
def url(self):
@@ -1386,7 +1276,7 @@ class Playlist(models.Model):
@property
def thumbnail_url(self):
pm = self.playlistmedia_set.first()
if pm:
if pm and pm.media.thumbnail:
return helpers.url_from_path(pm.media.thumbnail.path)
return None
@@ -1411,13 +1301,9 @@ class Comment(MPTTModel):
add_date = models.DateTimeField(auto_now_add=True)
media = models.ForeignKey(
Media, on_delete=models.CASCADE, db_index=True, related_name="comments"
)
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"
)
parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")
text = models.TextField(help_text="text")
@@ -1566,13 +1452,9 @@ def encoding_file_save(sender, instance, created, **kwargs):
# 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:
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=".{0}".format(instance.profile.extension), dir=temp_dir
)
tf = helpers.create_temp_file(suffix=".{0}".format(instance.profile.extension), dir=temp_dir)
with open(seg_file, "w") as ff:
for f in chunks_paths:
ff.write("file {}\n".format(f))
@@ -1602,9 +1484,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
progress=100,
)
all_logs = "\n".join([st.logs for st in chunks])
encoding.logs = "{0}\n{1}\n{2}".format(
chunks_paths, stdout, all_logs
)
encoding.logs = "{0}\n{1}\n{2}".format(chunks_paths, stdout, all_logs)
workers = list(set([st.worker for st in chunks]))
encoding.worker = json.dumps({"workers": workers})
@@ -1635,9 +1515,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
):
# 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 = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
who.delete()
else:
encoding.delete()
@@ -1652,13 +1530,9 @@ def encoding_file_save(sender, instance, created, **kwargs):
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
)
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 = 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]
@@ -1671,9 +1545,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
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 = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
who.delete()
pass # TODO: merge with above if, do not repeat code
@@ -1681,22 +1553,10 @@ def encoding_file_save(sender, instance, created, **kwargs):
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)
]
)
encodings = set([encoding.status for encoding in Encoding.objects.filter(media=instance.media)])
if ("running" in encodings) or ("pending" in encodings):
return
workers = list(
set(
[
encoding.worker
for encoding in Encoding.objects.filter(media=instance.media)
]
)
)
workers = list(set([encoding.worker for encoding in Encoding.objects.filter(media=instance.media)]))
@receiver(post_delete, sender=Encoding)
@@ -1712,4 +1572,3 @@ def encoding_file_delete(sender, instance, **kwargs):
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

View File

@@ -1,4 +1,5 @@
from rest_framework import permissions
from .methods import is_mediacms_editor

View File

@@ -1,6 +1,6 @@
from rest_framework import serializers
from .models import Media, EncodeProfile, Playlist, Comment, Category, Tag
from .models import Category, Comment, EncodeProfile, Media, Playlist, Tag
# TODO: put them in a more DRY way
@@ -18,9 +18,7 @@ class MediaSerializer(serializers.ModelSerializer):
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
def get_api_url(self, obj):
return self.context["request"].build_absolute_uri(
obj.get_absolute_url(api=True)
)
return self.context["request"].build_absolute_uri(obj.get_absolute_url(api=True))
def get_thumbnail_url(self, obj):
if obj.thumbnail_url:
@@ -210,16 +208,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
read_only_fields = ("add_date", "user")
fields = (
"add_date",
"title",
"description",
"user",
"media_count",
"url",
"api_url",
"thumbnail_url"
)
fields = ("add_date", "title", "description", "user", "media_count", "url", "api_url", "thumbnail_url")
class PlaylistDetailSerializer(serializers.ModelSerializer):
@@ -228,16 +217,7 @@ class PlaylistDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
read_only_fields = ("add_date", "user")
fields = (
"title",
"add_date",
"user_thumbnail_url",
"description",
"user",
"media_count",
"url",
"thumbnail_url"
)
fields = ("title", "add_date", "user_thumbnail_url", "description", "user", "media_count", "url", "thumbnail_url")
class CommentSerializer(serializers.ModelSerializer):

View File

@@ -1,41 +1,40 @@
import re
import os
import json
import subprocess
from datetime import datetime, timedelta
import tempfile
import os
import re
import shutil
from django.core.cache import cache
from django.conf import settings
from django.core.files import File
from django.db.models import Q
import subprocess
import tempfile
from datetime import datetime, timedelta
from celery import Task
from celery.decorators import task
from celery.utils.log import get_task_logger
from celery.exceptions import SoftTimeLimitExceeded
from celery.task.control import revoke
from celery.signals import task_revoked
from celery.task.control import revoke
from celery.utils.log import get_task_logger
from django.conf import settings
from django.core.cache import cache
from django.core.files import File
from django.db.models import Q
from actions.models import USER_MEDIA_ACTIONS, MediaAction
from users.models import User
from .backends import FFmpegBackend
from .exceptions import VideoEncodingError
from .helpers import (
calculate_seconds,
rm_file,
create_temp_file,
get_file_name,
get_file_type,
media_file_info,
run_command,
produce_ffmpeg_commands,
produce_friendly_token,
rm_file,
run_command,
)
from actions.models import MediaAction, USER_MEDIA_ACTIONS
from users.models import User
from .models import Encoding, EncodeProfile, Media, Category, Rating, Tag
from .methods import list_tasks, pre_save_action, notify_users
from .methods import list_tasks, notify_users, pre_save_action
from .models import Category, EncodeProfile, Encoding, Media, Rating, Tag
logger = get_task_logger(__name__)
@@ -83,10 +82,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
chunks.append(ch[0])
if not chunks:
# command completely failed to segment file.putting to normal encode
logger.info(
"Failed to break file {0} in chunks."
" Putting to normal encode queue".format(friendly_token)
)
logger.info("Failed to break file {0} in chunks." " Putting to normal encode queue".format(friendly_token))
for profile in profiles:
if media.video_height and media.video_height < profile.resolution:
if profile.resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
@@ -94,9 +90,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
encoding = Encoding(media=media, profile=profile)
encoding.save()
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
encode_media.delay(
friendly_token, profile.id, encoding.id, enc_url, force=force
)
encode_media.delay(friendly_token, profile.id, encoding.id, enc_url, force=force)
return False
chunks = [os.path.join(cwd, ch) for ch in chunks]
@@ -137,11 +131,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
priority=priority,
)
logger.info(
"got {0} chunks and will encode to {1} profiles".format(
len(chunks), to_profiles
)
)
logger.info("got {0} chunks and will encode to {1} profiles".format(len(chunks), to_profiles))
return True
@@ -180,11 +170,7 @@ def encode_media(
):
"""Encode a media to given profile, using ffmpeg, storing progress"""
logger.info(
"Encode Media started, friendly token {0}, profile id {1}, force {2}".format(
friendly_token, profile_id, force
)
)
logger.info("Encode Media started, friendly token {0}, profile id {1}, force {2}".format(friendly_token, profile_id, force))
if self.request.id:
task_id = self.request.id
@@ -202,13 +188,7 @@ def encode_media(
# TODO: in case a video is chunkized and this enters here many times
# it will always run since chunk_file_path is always different
# thus find a better way for this check
if (
Encoding.objects.filter(
media=media, profile=profile, chunk_file_path=chunk_file_path
).count()
> 1
and force == False
):
if Encoding.objects.filter(media=media, profile=profile, chunk_file_path=chunk_file_path).count() > 1 and force is False:
Encoding.objects.filter(id=encoding_id).delete()
return False
else:
@@ -230,19 +210,14 @@ def encode_media(
chunk_file_path=chunk_file_path,
)
else:
if (
Encoding.objects.filter(media=media, profile=profile).count() > 1
and force is False
):
if Encoding.objects.filter(media=media, profile=profile).count() > 1 and force is False:
Encoding.objects.filter(id=encoding_id).delete()
return False
else:
try:
encoding = Encoding.objects.get(id=encoding_id)
encoding.status = "running"
Encoding.objects.filter(media=media, profile=profile).exclude(
id=encoding_id
).delete()
Encoding.objects.filter(media=media, profile=profile).exclude(id=encoding_id).delete()
except BaseException:
encoding = Encoding(media=media, profile=profile, status="running")
@@ -287,7 +262,7 @@ def encode_media(
else:
original_media_path = media.media_file.path
#if not media.duration:
# if not media.duration:
# encoding.status = "fail"
# encoding.save(update_fields=["status"])
# return False
@@ -337,9 +312,7 @@ def encode_media(
if n_times % 60 == 0:
encoding.progress = percent
try:
encoding.save(
update_fields=["progress", "update_date"]
)
encoding.save(update_fields=["progress", "update_date"])
logger.info("Saved {0}".format(round(percent, 2)))
except BaseException:
pass
@@ -383,18 +356,12 @@ def encode_media(
with open(tf, "rb") as f:
myfile = File(f)
output_name = "{0}.{1}".format(
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.total_run_time = (
encoding.update_date - encoding.add_date
).seconds
encoding.total_run_time = (encoding.update_date - encoding.add_date).seconds
try:
encoding.save(
update_fields=["status", "logs", "progress", "total_run_time"]
)
encoding.save(update_fields=["status", "logs", "progress", "total_run_time"])
# this will raise a django.db.utils.DatabaseError error when task is revoked,
# since we delete the encoding at that stage
except BaseException:
@@ -424,7 +391,7 @@ def produce_sprite_from_video(friendly_token):
tmpdirname,
output_name,
)
ret = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
if os.path.exists(output_name) and get_file_type(output_name) == "image":
with open(output_name, "rb") as f:
myfile = File(f)
@@ -457,23 +424,19 @@ def create_hls(friendly_token):
p = media.uid.hex
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:
existing_output_dir = None
if os.path.exists(output_dir):
existing_output_dir = output_dir
output_dir = os.path.join(settings.HLS_DIR, p + produce_friendly_token())
files = " ".join([f.media_file.path for f in encodings if f.media_file])
cmd = "{0} --segment-duration=4 --output-dir={1} {2}".format(
settings.MP4HLS_COMMAND, output_dir, files
)
ret = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
cmd = "{0} --segment-duration=4 --output-dir={1} {2}".format(settings.MP4HLS_COMMAND, output_dir, files)
subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
if existing_output_dir:
# override content with -T !
cmd = "cp -rT {0} {1}".format(output_dir, existing_output_dir)
ret = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
shutil.rmtree(output_dir)
output_dir = existing_output_dir
pp = os.path.join(output_dir, "master.m3u8")
@@ -515,11 +478,7 @@ def check_running_states():
def check_media_states():
# Experimental - unused
# 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("got {0} media that are not in state success".format(media.count()))
@@ -564,11 +523,7 @@ def check_pending_states():
media.encode(profiles=[profile], force=False)
changed += 1
if changed:
logger.info(
"set to the encode queue {0} encodings that were on pending state".format(
changed
)
)
logger.info("set to the encode queue {0} encodings that were on pending state".format(changed))
return True
@@ -602,6 +557,7 @@ def clear_sessions():
try:
from importlib import import_module
from django.conf import settings
engine = import_module(settings.SESSION_ENGINE)
@@ -612,9 +568,7 @@ def clear_sessions():
@task(name="save_user_action", queue="short_tasks")
def save_user_action(
user_or_session, friendly_token=None, action="watch", extra_info=None
):
def save_user_action(user_or_session, friendly_token=None, action="watch", extra_info=None):
"""Short task that saves a user action"""
if action not in VALID_USER_ACTIONS:
@@ -652,9 +606,7 @@ def save_user_action(
if user:
MediaAction.objects.filter(user=user, media=media, action="watch").delete()
else:
MediaAction.objects.filter(
session_key=session_key, media=media, action="watch"
).delete()
MediaAction.objects.filter(session_key=session_key, media=media, action="watch").delete()
if action == "rate":
try:
score = extra_info.get("score")
@@ -663,9 +615,7 @@ def save_user_action(
# TODO: better error handling?
return False
try:
rating = Rating.objects.filter(
user=user, media=media, rating_category_id=rating_category
).first()
rating = Rating.objects.filter(user=user, media=media, rating_category_id=rating_category).first()
if rating:
rating.score = score
rating.save(update_fields=["score"])
@@ -676,7 +626,7 @@ def save_user_action(
rating_category_id=rating_category,
score=score,
)
except Exception as exc:
except Exception:
# TODO: more specific handling, for errors in score, or
# rating_category?
return False
@@ -735,14 +685,10 @@ def get_list_of_popular_media():
for media in media_x:
ft = media["friendly_token"]
num = MediaAction.objects.filter(
action_date__gte=period_x, action="watch", media__friendly_token=ft
).count()
num = MediaAction.objects.filter(action_date__gte=period_x, action="watch", media__friendly_token=ft).count()
if num:
valid_media_x[ft] = num
num = MediaAction.objects.filter(
action_date__gte=period_y, action="like", media__friendly_token=ft
).count()
num = MediaAction.objects.filter(action_date__gte=period_y, action="like", media__friendly_token=ft).count()
if num:
valid_media_y[ft] = num
@@ -767,12 +713,7 @@ def update_listings_thumbnails():
saved = 0
qs = Category.objects.filter().order_by("-media_count")
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:
object.listings_thumbnail = media.thumbnail_url
object.save(update_fields=["listings_thumbnail"])
@@ -785,12 +726,7 @@ def update_listings_thumbnails():
saved = 0
qs = Tag.objects.filter().order_by("-media_count")
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:
object.listings_thumbnail = media.thumbnail_url
object.save(update_fields=["listings_thumbnail"])

View File

@@ -1,10 +1,9 @@
from django.conf.urls.static import static
from django.conf import settings
from django.conf.urls import url, include
from django.conf.urls import include, url
from django.conf.urls.static import static
from django.urls import path
from . import views
from . import management_views
from . import management_views, views
from .feeds import IndexRSSFeed, SearchRSSFeed
urlpatterns = [

View File

@@ -1,69 +1,69 @@
from datetime import datetime, timedelta
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.db.models import Q
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.template.defaultfilters import slugify
from django.core.mail import EmailMessage
from django.contrib.postgres.search import SearchQuery
from rest_framework import permissions
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.exceptions import PermissionDenied
from rest_framework import status
from rest_framework.parsers import (
JSONParser,
MultiPartParser,
FileUploadParser,
FormParser,
)
from celery.task.control import revoke
from cms.permissions import IsAuthorizedToAdd, IsUserOrEditor
from cms.permissions import user_allowed_to_upload
from cms.custom_pagination import FastPaginationWithoutCount
from actions.models import MediaAction, USER_MEDIA_ACTIONS
from users.models import User
from .helpers import produce_ffmpeg_commands, clean_query
from .models import (
Media,
EncodeProfile,
Encoding,
Playlist,
PlaylistMedia,
Comment,
Category,
Tag,
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.postgres.search import SearchQuery
from django.core.mail import EmailMessage
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.template.defaultfilters import slugify
from drf_yasg import openapi as openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import permissions, status
from rest_framework.exceptions import PermissionDenied
from rest_framework.parsers import (
FileUploadParser,
FormParser,
JSONParser,
MultiPartParser,
)
from .forms import MediaForm, ContactForm, SubtitleForm
from .tasks import save_user_action
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.views import APIView
from actions.models import USER_MEDIA_ACTIONS, MediaAction
from cms.custom_pagination import FastPaginationWithoutCount
from cms.permissions import IsAuthorizedToAdd, IsUserOrEditor, user_allowed_to_upload
from users.models import User
from .forms import ContactForm, MediaForm, SubtitleForm
from .helpers import clean_query, produce_ffmpeg_commands
from .methods import (
list_tasks,
get_user_or_session,
show_recommended_media,
show_related_media,
is_mediacms_editor,
is_mediacms_manager,
update_user_ratings,
list_tasks,
notify_user_on_comment,
show_recommended_media,
show_related_media,
update_user_ratings,
)
from .models import (
Category,
Comment,
EncodeProfile,
Encoding,
Media,
Playlist,
PlaylistMedia,
Tag,
)
from .serializers import (
MediaSerializer,
CategorySerializer,
TagSerializer,
SingleMediaSerializer,
CommentSerializer,
EncodeProfileSerializer,
MediaSearchSerializer,
PlaylistSerializer,
MediaSerializer,
PlaylistDetailSerializer,
CommentSerializer,
PlaylistSerializer,
SingleMediaSerializer,
TagSerializer,
)
from .stop_words import STOP_WORDS
from .tasks import save_user_action
VALID_USER_ACTIONS = [action for action, name in USER_MEDIA_ACTIONS]
@@ -86,11 +86,7 @@ def add_subtitle(request):
if not media:
return HttpResponseRedirect("/")
if not (
request.user == media.user
or is_mediacms_editor(request.user)
or is_mediacms_manager(request.user)
):
if not (request.user == media.user or is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
return HttpResponseRedirect("/")
if request.method == "POST":
@@ -175,11 +171,7 @@ def edit_media(request):
if not media:
return HttpResponseRedirect("/")
if not (
request.user == media.user
or is_mediacms_editor(request.user)
or is_mediacms_manager(request.user)
):
if not (request.user == media.user or is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
return HttpResponseRedirect("/")
if request.method == "POST":
form = MediaForm(request.user, request.POST, request.FILES, instance=media)
@@ -220,8 +212,6 @@ def embed_media(request):
if not media:
return HttpResponseRedirect("/")
user_or_session = get_user_or_session(request)
context = {}
context["media"] = friendly_token
return render(request, "cms/embed.html", context)
@@ -342,9 +332,7 @@ def view_media(request):
return render(request, "cms/media.html", context)
user_or_session = get_user_or_session(request)
save_user_action.delay(
user_or_session, friendly_token=friendly_token, action="watch"
)
save_user_action.delay(user_or_session, friendly_token=friendly_token, action="watch")
context = {}
context["media"] = friendly_token
context["media_object"] = media
@@ -354,11 +342,7 @@ def view_media(request):
context["CAN_DELETE_COMMENTS"] = False
if request.user.is_authenticated:
if (
(media.user.id == request.user.id)
or is_mediacms_editor(request.user)
or is_mediacms_manager(request.user)
):
if (media.user.id == request.user.id) or is_mediacms_editor(request.user) or is_mediacms_manager(request.user):
context["CAN_DELETE_MEDIA"] = True
context["CAN_EDIT_MEDIA"] = True
context["CAN_DELETE_COMMENTS"] = True
@@ -384,6 +368,12 @@ class MediaList(APIView):
permission_classes = (IsAuthorizedToAdd,)
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def get(self, request, format=None):
# Show media
params = self.request.query_params
@@ -423,6 +413,12 @@ class MediaList(APIView):
serializer = MediaSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def post(self, request, format=None):
# Add new media
serializer = MediaSerializer(data=request.data, context={"request": request})
@@ -443,39 +439,33 @@ class MediaDetail(APIView):
def get_object(self, friendly_token, password=None):
try:
media = (
Media.objects.select_related("user")
.prefetch_related("encodings__profile")
.get(friendly_token=friendly_token)
)
media = Media.objects.select_related("user").prefetch_related("encodings__profile").get(friendly_token=friendly_token)
# this need be explicitly called, and will call
# has_object_permission() after has_permission has succeeded
self.check_object_permissions(self.request, media)
if media.state == "private" and not (
self.request.user == media.user or is_mediacms_editor(self.request.user)
):
if (
(not password)
or (not media.password)
or (password != media.password)
):
if media.state == "private" and not (self.request.user == media.user or is_mediacms_editor(self.request.user)):
if (not password) or (not media.password) or (password != media.password):
return Response(
{"detail": "media is private"},
status=status.HTTP_401_UNAUTHORIZED,
)
return media
except PermissionDenied:
return Response(
{"detail": "bad permissions"}, status=status.HTTP_401_UNAUTHORIZED
)
return Response({"detail": "bad permissions"}, status=status.HTTP_401_UNAUTHORIZED)
except BaseException:
return Response(
{"detail": "media file does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def get(self, request, friendly_token, format=None):
# Get media details
password = request.GET.get("password")
@@ -488,27 +478,25 @@ class MediaDetail(APIView):
related_media = []
else:
related_media = show_related_media(media, request=request, limit=100)
related_media_serializer = MediaSerializer(
related_media, many=True, context={"request": request}
)
related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request})
related_media = related_media_serializer.data
ret = serializer.data
# update rattings info with user specific ratings
# eg user has already rated for this media
# this only affects user rating and only if enabled
if (
settings.ALLOW_RATINGS
and ret.get("ratings_info")
and not request.user.is_anonymous
):
ret["ratings_info"] = update_user_ratings(
request.user, media, ret.get("ratings_info")
)
if settings.ALLOW_RATINGS and ret.get("ratings_info") and not request.user.is_anonymous:
ret["ratings_info"] = update_user_ratings(request.user, media, ret.get("ratings_info"))
ret["related_media"] = related_media
return Response(ret)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def post(self, request, friendly_token, format=None):
"""superuser actions
Available only to MediaCMS editors and managers
@@ -521,9 +509,7 @@ class MediaDetail(APIView):
return media
if not (is_mediacms_editor(request.user) or is_mediacms_manager(request.user)):
return Response(
{"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
action = request.data.get("type")
profiles_list = request.data.get("encoding_profiles")
@@ -544,44 +530,48 @@ class MediaDetail(APIView):
valid_profiles.append(p)
except ValueError:
return Response(
{
"detail": "encoding_profiles must be int or list of ints of valid encode profiles"
},
{"detail": "encoding_profiles must be int or list of ints of valid encode profiles"},
status=status.HTTP_400_BAD_REQUEST,
)
media.encode(profiles=valid_profiles)
return Response(
{"detail": "media will be encoded"}, status=status.HTTP_201_CREATED
)
return Response({"detail": "media will be encoded"}, status=status.HTTP_201_CREATED)
elif action == "review":
if result:
media.is_reviewed = True
elif result == False:
elif result is False:
media.is_reviewed = False
media.save(update_fields=["is_reviewed"])
return Response(
{"detail": "media reviewed set"}, status=status.HTTP_201_CREATED
)
return Response({"detail": "media reviewed set"}, status=status.HTTP_201_CREATED)
return Response(
{"detail": "not valid action or no action specified"},
status=status.HTTP_400_BAD_REQUEST,
)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def put(self, request, friendly_token, format=None):
# Update a media object
media = self.get_object(friendly_token)
if isinstance(media, Response):
return media
serializer = MediaSerializer(
media, data=request.data, context={"request": request}
)
serializer = MediaSerializer(media, data=request.data, context={"request": request})
if serializer.is_valid():
media_file = request.data["media_file"]
serializer.save(user=request.user, media_file=media_file)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def delete(self, request, friendly_token, format=None):
# Delete a media object
media = self.get_object(friendly_token)
@@ -601,26 +591,24 @@ class MediaActions(APIView):
def get_object(self, friendly_token):
try:
media = (
Media.objects.select_related("user")
.prefetch_related("encodings__profile")
.get(friendly_token=friendly_token)
)
media = Media.objects.select_related("user").prefetch_related("encodings__profile").get(friendly_token=friendly_token)
if media.state == "private" and self.request.user != media.user:
return Response(
{"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST)
return media
except PermissionDenied:
return Response(
{"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
except BaseException:
return Response(
{"detail": "media file does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def get(self, request, friendly_token, format=None):
# show date and reason for each time media was reported
media = self.get_object(friendly_token)
@@ -636,6 +624,12 @@ class MediaActions(APIView):
return Response(ret, status=status.HTTP_200_OK)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def post(self, request, friendly_token, format=None):
# perform like/dislike/report actions
media = self.get_object(friendly_token)
@@ -661,23 +655,23 @@ class MediaActions(APIView):
extra_info=extra,
)
return Response(
{"detail": "action received"}, status=status.HTTP_201_CREATED
)
return Response({"detail": "action received"}, status=status.HTTP_201_CREATED)
else:
return Response(
{"detail": "no action specified"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "no action specified"}, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def delete(self, request, friendly_token, format=None):
media = self.get_object(friendly_token)
if isinstance(media, Response):
return media
if not request.user.is_superuser:
return Response(
{"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
action = request.data.get("type")
if action:
@@ -690,9 +684,7 @@ class MediaActions(APIView):
status=status.HTTP_201_CREATED,
)
else:
return Response(
{"detail": "no action specified"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "no action specified"}, status=status.HTTP_400_BAD_REQUEST)
class MediaSearch(APIView):
@@ -703,6 +695,12 @@ class MediaSearch(APIView):
parser_classes = (JSONParser,)
@swagger_auto_schema(
manual_parameters=[],
tags=['Search'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def get(self, request, format=None):
params = self.request.query_params
query = params.get("q", "").strip().lower()
@@ -736,11 +734,7 @@ class MediaSearch(APIView):
if query:
# move this processing to a prepare_query function
query = clean_query(query)
q_parts = [
q_part.rstrip("y")
for q_part in query.split()
if q_part not in STOP_WORDS
]
q_parts = [q_part.rstrip("y") for q_part in query.split() if q_part not in STOP_WORDS]
if q_parts:
query = SearchQuery(q_parts[0] + ":*", search_type="raw")
for part in q_parts[1:]:
@@ -771,10 +765,10 @@ class MediaSearch(APIView):
if upload_date == 'this_month':
year = datetime.now().date().year
month = datetime.now().date().month
gte = datetime(year,month,1)
gte = datetime(year, month, 1)
if upload_date == 'this_year':
year = datetime.now().date().year
gte = datetime(year,1,1)
gte = datetime(year, 1, 1)
if lte:
media = media.filter(add_date__lte=lte)
if gte:
@@ -794,9 +788,7 @@ class MediaSearch(APIView):
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
paginator = pagination_class()
page = paginator.paginate_queryset(media, request)
serializer = MediaSearchSerializer(
page, many=True, context={"request": request}
)
serializer = MediaSearchSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data)
@@ -806,6 +798,12 @@ class PlaylistList(APIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsAuthorizedToAdd)
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
@swagger_auto_schema(
manual_parameters=[],
tags=['Playlists'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def get(self, request, format=None):
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
paginator = pagination_class()
@@ -820,6 +818,12 @@ class PlaylistList(APIView):
serializer = PlaylistSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data)
@swagger_auto_schema(
manual_parameters=[],
tags=['Playlists'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def post(self, request, format=None):
serializer = PlaylistSerializer(data=request.data, context={"request": request})
if serializer.is_valid():
@@ -840,15 +844,19 @@ class PlaylistDetail(APIView):
self.check_object_permissions(self.request, playlist)
return playlist
except PermissionDenied:
return Response(
{"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST)
except BaseException:
return Response(
{"detail": "Playlist does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
@swagger_auto_schema(
manual_parameters=[],
tags=['Playlists'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def get(self, request, friendly_token, format=None):
playlist = self.get_playlist(friendly_token)
if isinstance(playlist, Response):
@@ -856,31 +864,37 @@ class PlaylistDetail(APIView):
serializer = PlaylistDetailSerializer(playlist, context={"request": request})
playlist_media = PlaylistMedia.objects.filter(
playlist=playlist
).prefetch_related("media__user")
playlist_media = PlaylistMedia.objects.filter(playlist=playlist).prefetch_related("media__user")
playlist_media = [c.media for c in playlist_media]
playlist_media_serializer = MediaSerializer(
playlist_media, many=True, context={"request": request}
)
playlist_media_serializer = MediaSerializer(playlist_media, many=True, context={"request": request})
ret = serializer.data
ret["playlist_media"] = playlist_media_serializer.data
return Response(ret)
@swagger_auto_schema(
manual_parameters=[],
tags=['Playlists'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def post(self, request, friendly_token, format=None):
playlist = self.get_playlist(friendly_token)
if isinstance(playlist, Response):
return playlist
serializer = PlaylistDetailSerializer(
playlist, data=request.data, context={"request": request}
)
serializer = PlaylistDetailSerializer(playlist, data=request.data, context={"request": request})
if serializer.is_valid():
serializer.save(user=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
manual_parameters=[],
tags=['Playlists'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def put(self, request, friendly_token, format=None):
playlist = self.get_playlist(friendly_token)
if isinstance(playlist, Response):
@@ -895,14 +909,10 @@ class PlaylistDetail(APIView):
pass
if action in ["add", "remove", "ordering"]:
media = Media.objects.filter(
friendly_token=media_friendly_token, state="public"
).first()
media = Media.objects.filter(friendly_token=media_friendly_token).first()
if media:
if action == "add":
media_in_playlist = PlaylistMedia.objects.filter(
playlist=playlist
).count()
media_in_playlist = PlaylistMedia.objects.filter(playlist=playlist).count()
if media_in_playlist >= settings.MAX_MEDIA_PER_PLAYLIST:
return Response(
{"detail": "max number of media for a Playlist reached"},
@@ -920,9 +930,7 @@ class PlaylistDetail(APIView):
status=status.HTTP_201_CREATED,
)
elif action == "remove":
PlaylistMedia.objects.filter(
playlist=playlist, media=media
).delete()
PlaylistMedia.objects.filter(playlist=playlist, media=media).delete()
return Response(
{"detail": "media removed from Playlist"},
status=status.HTTP_201_CREATED,
@@ -935,14 +943,18 @@ class PlaylistDetail(APIView):
status=status.HTTP_201_CREATED,
)
else:
return Response(
{"detail": "media is not valid"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "media is not valid"}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{"detail": "invalid or not specified action"},
status=status.HTTP_400_BAD_REQUEST,
)
@swagger_auto_schema(
manual_parameters=[],
tags=['Playlists'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def delete(self, request, friendly_token, format=None):
playlist = self.get_playlist(friendly_token)
if isinstance(playlist, Response):
@@ -960,6 +972,7 @@ class EncodingDetail(APIView):
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)
@@ -993,7 +1006,7 @@ class EncodingDetail(APIView):
chunk_file_path=chunk_file_path,
).count()
> 1
and force == False
and force is False
):
Encoding.objects.filter(id=encoding_id).delete()
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
@@ -1013,15 +1026,11 @@ class EncodingDetail(APIView):
if chunk:
original_media_path = chunk_file_path
original_media_md5sum = encoding.md5sum
original_media_url = (
settings.SSL_FRONTEND_HOST + encoding.media_chunk_url
)
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
)
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
@@ -1089,6 +1098,7 @@ class EncodingDetail(APIView):
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()
@@ -1106,6 +1116,15 @@ class CommentList(APIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsAuthorizedToAdd)
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
openapi.Parameter(name='author', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='username'),
],
tags=['Comments'],
operation_summary='Lists Comments',
operation_description='Paginated listing of all comments',
)
def get(self, request, format=None):
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
paginator = pagination_class()
@@ -1137,25 +1156,25 @@ class CommentDetail(APIView):
def get_object(self, friendly_token):
try:
media = Media.objects.select_related("user").get(
friendly_token=friendly_token
)
media = Media.objects.select_related("user").get(friendly_token=friendly_token)
self.check_object_permissions(self.request, media)
if media.state == "private" and self.request.user != media.user:
return Response(
{"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "media is private"}, status=status.HTTP_400_BAD_REQUEST)
return media
except PermissionDenied:
return Response(
{"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
except BaseException:
return Response(
{"detail": "media file does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def get(self, request, friendly_token):
# list comments for a media
media = self.get_object(friendly_token)
@@ -1168,6 +1187,12 @@ class CommentDetail(APIView):
serializer = CommentSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def delete(self, request, friendly_token, uid=None):
"""Delete a comment
Administrators, MediaCMS editors and managers,
@@ -1181,18 +1206,18 @@ class CommentDetail(APIView):
{"detail": "comment does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
if (
(comment.user == self.request.user)
or comment.media.user == self.request.user
or is_mediacms_editor(self.request.user)
):
if (comment.user == self.request.user) or comment.media.user == self.request.user or is_mediacms_editor(self.request.user):
comment.delete()
else:
return Response(
{"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
@swagger_auto_schema(
manual_parameters=[],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def post(self, request, friendly_token):
"""Create a comment"""
media = self.get_object(friendly_token)
@@ -1217,17 +1242,19 @@ class CommentDetail(APIView):
class UserActions(APIView):
parser_classes = (JSONParser,)
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(name='action', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='action', required=True, enum=VALID_USER_ACTIONS),
],
tags=['Users'],
operation_summary='List user actions',
operation_description='Lists user actions',
)
def get(self, request, action):
media = []
if action in VALID_USER_ACTIONS:
if request.user.is_authenticated:
media = (
Media.objects.select_related("user")
.filter(
mediaactions__user=request.user, mediaactions__action=action
)
.order_by("-mediaactions__action_date")
)
media = Media.objects.select_related("user").filter(mediaactions__user=request.user, mediaactions__action=action).order_by("-mediaactions__action_date")
elif request.session.session_key:
media = (
Media.objects.select_related("user")
@@ -1248,11 +1275,15 @@ class UserActions(APIView):
class CategoryList(APIView):
"""List categories"""
@swagger_auto_schema(
manual_parameters=[],
tags=['Categories'],
operation_summary='Lists Categories',
operation_description='Lists all categories',
)
def get(self, request, format=None):
categories = Category.objects.filter().order_by("title")
serializer = CategorySerializer(
categories, many=True, context={"request": request}
)
serializer = CategorySerializer(categories, many=True, context={"request": request})
ret = serializer.data
return Response(ret)
@@ -1260,6 +1291,14 @@ class CategoryList(APIView):
class TagList(APIView):
"""List tags"""
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
],
tags=['Tags'],
operation_summary='Lists Tags',
operation_description='Paginated listing of all tags',
)
def get(self, request, format=None):
tags = Tag.objects.filter().order_by("-media_count")
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
@@ -1272,17 +1311,23 @@ class TagList(APIView):
class EncodeProfileList(APIView):
"""List encode profiles"""
@swagger_auto_schema(
manual_parameters=[],
tags=['Encoding Profiles'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def get(self, request, format=None):
profiles = EncodeProfile.objects.all()
serializer = EncodeProfileSerializer(
profiles, many=True, context={"request": request}
)
serializer = EncodeProfileSerializer(profiles, many=True, context={"request": request})
return Response(serializer.data)
class TasksList(APIView):
"""List tasks"""
swagger_schema = None
permission_classes = (permissions.IsAdminUser,)
def get(self, request, format=None):
@@ -1293,6 +1338,8 @@ class TasksList(APIView):
class TaskDetail(APIView):
"""Cancel a task"""
swagger_schema = None
permission_classes = (permissions.IsAdminUser,)
def delete(self, request, uid, format=None):

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# should be run as root and only on Ubuntu 18/20 versions!
# should be run as root and only on Ubuntu 18/20, Debian Buster versions!
echo "Welcome to the MediacMS installation!";
if [ `id -u` -ne 0 ]
@@ -27,6 +27,10 @@ if [[ `lsb_release -d` == *"Ubuntu 20"* ]]; then
elif [[ `lsb_release -d` = *"Ubuntu 18"* ]]; then
echo 'Performing system update and dependency installation, this will take a few minutes'
apt-get update && apt-get -y upgrade && apt install python3-venv python3-dev virtualenv redis-server postgresql nginx git gcc vim unzip ffmpeg imagemagick python3-certbot-nginx certbot wget -y
# added check for Debian 10 (buster)
elif [[ `lsb_release -d` == *"buster"* ]]; then
echo 'Performing system update and dependency installation, this will take a few minutes'
apt-get update && apt-get -y upgrade && apt install python3-venv python3-dev virtualenv redis-server postgresql nginx git gcc vim unzip ffmpeg imagemagick python3-certbot-nginx certbot wget -y
else
echo "This script is tested for Ubuntu 18 and 20 versions only, if you want to try MediaCMS on another system you have to perform the manual installation"
exit
@@ -60,8 +64,9 @@ FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/https:\/\///g'`
sed -i s/localhost/$FRONTEND_HOST/g deploy/local_install/mediacms.io
FRONTEND_HOST_HTTP_PREFIX='http://'$FRONTEND_HOST
echo 'FRONTEND_HOST='\'"$FRONTEND_HOST"\' >> cms/local_settings.py
echo 'FRONTEND_HOST='\'"$FRONTEND_HOST_HTTP_PREFIX"\' >> cms/local_settings.py
echo 'PORTAL_NAME='\'"$PORTAL_NAME"\' >> cms/local_settings.py
echo "SSL_FRONTEND_HOST = FRONTEND_HOST.replace('http', 'https')" >> cms/local_settings.py
@@ -114,8 +119,8 @@ fi
# Bento4 utility installation, for HLS
cd /home/mediacms.io/mediacms
wget http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-632.x86_64-unknown-linux.zip
unzip Bento4-SDK-1-6-0-632.x86_64-unknown-linux.zip
wget 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
mkdir /home/mediacms.io/mediacms/media_files/hls
# last, set default owner

View File

@@ -7,9 +7,5 @@ if __name__ == "__main__":
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
raise ImportError("Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?") from exc
execute_from_command_line(sys.argv)

17
pyproject.toml Normal file
View File

@@ -0,0 +1,17 @@
[tool.black]
line-length = 200
target-version = ['py36', 'py37', 'py38']
skip-string-normalization = true
include = '\.pyi?$'
exclude = '''
/(
\.*
| \.git
| \.mypy_cache
| \.venv
# The following are specific to Black, you probably don't want those.
| blib2to3
| tests/data
| profiling
)/
'''

View File

@@ -1,4 +1,4 @@
Django==3.1.4
Django==3.1.8
djangorestframework==3.12.2
django-allauth==0.44.0
@@ -9,14 +9,21 @@ uwsgi==2.0.19.1
django-redis==4.12.1
celery==4.4.7
Pillow==8.0.1
drf-yasg==1.20.0
Pillow==8.1.1
django-imagekit
markdown
django-filter
filetype
django-mptt
django-crispy-forms
requests==2.25.0
django-celery-email
m3u8
@@ -30,3 +37,4 @@ flake8
pep8
django-silk
django-debug-toolbar
pre-commit

4
setup.cfg Normal file
View File

@@ -0,0 +1,4 @@
[flake8]
exclude = .git,*migrations*
max-line-length = 119
ignore=F401,F403,E501,W503

View File

@@ -1,10 +1,8 @@
/*@import url('https://fonts.googleapis.com/icon?family=Material+Icons');*/
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url('../../lib/material-icons/v50/icons.woff2') format('woff2');
src: url('../../lib/material-icons/v77/icons.woff2') format('woff2');
font-display: swap;
}

Binary file not shown.

View File

@@ -1,37 +1,37 @@
MediaCMS.url = {
home: "{{FRONTEND_HOST}}",
search: "{{FRONTEND_HOST}}/search",
latestMedia: "{{FRONTEND_HOST}}/latest",
featuredMedia: "{{FRONTEND_HOST}}/featured",
recommendedMedia: "{{FRONTEND_HOST}}/recommended",
members: "{{FRONTEND_HOST}}/members",
/* Error pages */
error404: "{{FRONTEND_HOST}}/error",
/* Taxonomies pages */
tags: "{{FRONTEND_HOST}}/tags",
categories: "{{FRONTEND_HOST}}/categories",
topics: "{{FRONTEND_HOST}}/topics",
languages: "{{FRONTEND_HOST}}/languages",
countries: "{{FRONTEND_HOST}}/countries",
/* User pages */
likedMedia: "{{FRONTEND_HOST}}/liked",
history: "{{FRONTEND_HOST}}/history",
/* Add/edit pages */
addMedia: "{{FRONTEND_HOST}}/upload",
/* User account pages */
editProfile: "{{user.edit_url}}",
{% if request.user.is_authenticated %}
signout: "{{FRONTEND_HOST}}/accounts/logout/",
editChannel: "{{user.default_channel_edit_url}}",
changePassword: "{{FRONTEND_HOST}}/accounts/password/change/",
/* Administration pages */
{% if IS_MEDIACMS_ADMIN %}admin: '/admin',{% endif %}
/* Management pages */
{% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}manageMedia: "{{FRONTEND_HOST}}/manage/media",{% endif %}
{% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER %}manageUsers: "{{FRONTEND_HOST}}/manage/users",{% endif %}
{% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}manageComments: "{{FRONTEND_HOST}}/manage/comments",{% endif %}
{% else %}
signin: "{{FRONTEND_HOST}}/accounts/login/",
register: "{{FRONTEND_HOST}}/accounts/signup/",
{% endif %}
};
MediaCMS.url = {
home: "/",
search: "/search",
latestMedia: "/latest",
featuredMedia: "/featured",
recommendedMedia: "/recommended",
members: "/members",
/* Error pages */
error404: "/error",
/* Taxonomies pages */
tags: "/tags",
categories: "/categories",
topics: "/topics",
languages: "/languages",
countries: "/countries",
/* User pages */
likedMedia: "/liked",
history: "/history",
/* Add/edit pages */
addMedia: "/upload",
/* User account pages */
editProfile: "{{user.edit_url}}",
{% if request.user.is_authenticated %}
signout: "/accounts/logout/",
editChannel: "{{user.default_channel_edit_url}}",
changePassword: "/accounts/password/change/",
/* Administration pages */
{% if IS_MEDIACMS_ADMIN %}admin: '/admin',{% endif %}
/* Management pages */
{% if IS_MEDIACMS_EDITOR %}manageMedia: "/manage/media",{% endif %}
{% if IS_MEDIACMS_MANAGER %}manageUsers: "/manage/users",{% endif %}
{% if IS_MEDIACMS_EDITOR %}manageComments: "/manage/comments",{% endif %}
{% else %}
signin: "/accounts/login/",
register: "/accounts/signup/",
{% endif %}
};

View File

@@ -15,7 +15,7 @@ MediaCMS.user = {
addComment: true,
deleteComment: {% if CAN_DELETE_COMMENTS %}true{% else %}false{% endif %},
editProfile: {% if CAN_EDIT %}true{% else %}false{% endif %},
deleteProfile: {% if CAN_DELETE_PROFILE %}true{% else %}false{% endif %},
deleteProfile: {% if CAN_DELETE %}true{% else %}false{% endif %},
manageMedia: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}true{% else %}false{% endif %},
manageUsers: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER %}true{% else %}false{% endif %},
manageComments: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}true{% else %}false{% endif %},

View File

@@ -1,22 +1,22 @@
MediaCMS.site = {
id: 'mediacms',
title: "{{PORTAL_NAME}}",
url: '{{FRONTEND_HOST}}/',
api: '{{FRONTEND_HOST}}/api/v1',
url: '{{FRONTEND_HOST}}',
api: '{{FRONTEND_HOST}}api/v1',
theme: {
mode: 'light', // Valid values: 'light', 'dark'.
mode: '{{DEFAULT_THEME}}',
switch: {
position: 'header', // Valid values: 'header', 'sidebar'.
},
},
logo:{
lightMode:{
img: "{{FRONTEND_HOST}}/static/images/logo_dark.png",
svg: "{{FRONTEND_HOST}}/static/images/logo_dark.svg",
img: "/static/images/logo_dark.png",
svg: "/static/images/logo_dark.svg",
},
darkMode:{
img: "{{FRONTEND_HOST}}/static/images/logo_light.png",
svg: "{{FRONTEND_HOST}}/static/images/logo_light.svg",
img: "/static/images/logo_light.png",
svg: "/static/images/logo_light.svg",
},
},
pages: {
@@ -49,4 +49,5 @@ MediaCMS.site = {
title: 'Categories',
},
},
};
};

View File

@@ -1,6 +1,7 @@
from os.path import join
from io import StringIO
import shutil
from io import StringIO
from os.path import join
from django.conf import settings
from . import utils

View File

@@ -1,6 +1,7 @@
from django.core.exceptions import ImproperlyConfigured
from importlib import import_module
from django.core.exceptions import ImproperlyConfigured
def import_class(path):
path_bits = path.split(".")
@@ -14,9 +15,7 @@ def import_class(path):
module_itself = import_module(module_path)
if not hasattr(module_itself, class_name):
message = "The Python module '{}' has no '{}' class.".format(
module_path, class_name
)
message = "The Python module '{}' has no '{}' class.".format(module_path, class_name)
raise ImportError(message)
return getattr(module_itself, class_name)

View File

@@ -2,17 +2,18 @@
import os
import shutil
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.files import File
from django.http import JsonResponse
from django.views import generic
from django.conf import settings
from django.core.files import File
from django.core.exceptions import PermissionDenied
from cms.permissions import user_allowed_to_upload
from files.models import Media
from files.helpers import rm_file
from .forms import FineUploaderUploadForm, FineUploaderUploadSuccessForm
from files.models import Media
from .fineuploader import ChunkedFineUploader
from .forms import FineUploaderUploadForm, FineUploaderUploadSuccessForm
class FineUploaderView(generic.FormView):
@@ -67,9 +68,7 @@ class FineUploaderView(generic.FormView):
new = Media.objects.create(media_file=myfile, user=self.request.user)
rm_file(media_file)
shutil.rmtree(os.path.join(settings.MEDIA_ROOT, self.upload.file_path))
return self.make_response(
{"success": True, "media_url": new.get_absolute_url()}
)
return self.make_response({"success": True, "media_url": new.get_absolute_url()})
def form_invalid(self, form):
data = {"success": False, "error": "%s" % repr(form.errors)}

View File

@@ -1,7 +1,7 @@
from django.urls import reverse
from django.conf import settings
from allauth.account.adapter import DefaultAccountAdapter
from django.conf import settings
from django.core.exceptions import ValidationError
from django.urls import reverse
class MyAccountAdapter(DefaultAccountAdapter):

View File

@@ -17,7 +17,6 @@ class UserAdmin(admin.ModelAdmin):
"last_name",
"media_count",
"date_joined",
"is_staff",
"is_active",
)
list_display = [

View File

@@ -1,5 +1,6 @@
from django import forms
from .models import User, Channel
from .models import Channel, User
class SignupForm(forms.Form):
@@ -23,7 +24,7 @@ class UserForm(forms.ModelForm):
"advancedUser",
"is_manager",
"is_editor",
#"allow_contact",
# "allow_contact",
)
def clean_logo(self):

View File

@@ -1,12 +1,12 @@
# Generated by Django 3.1.4 on 2020-12-01 07:12
from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import imagekit.models.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
@@ -33,9 +33,7 @@ class Migration(migrations.Migration):
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
models.DateTimeField(blank=True, null=True, verbose_name="last login"),
),
(
"is_superuser",
@@ -48,35 +46,25 @@ class Migration(migrations.Migration):
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
error_messages={"unique": "A user with that username already exists."},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
models.CharField(blank=True, max_length=150, verbose_name="first name"),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
models.CharField(blank=True, max_length=150, verbose_name="last name"),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="email address"
),
models.EmailField(blank=True, max_length=254, verbose_name="email address"),
),
(
"is_staff",
@@ -96,9 +84,7 @@ class Migration(migrations.Migration):
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined"),
),
(
"logo",
@@ -111,9 +97,7 @@ class Migration(migrations.Migration):
("description", models.TextField(blank=True, verbose_name="About me")),
(
"name",
models.CharField(
db_index=True, max_length=250, verbose_name="full name"
),
models.CharField(db_index=True, max_length=250, verbose_name="full name"),
),
(
"date_added",
@@ -125,9 +109,7 @@ class Migration(migrations.Migration):
),
(
"is_featured",
models.BooleanField(
db_index=True, default=False, verbose_name="Is featured"
),
models.BooleanField(db_index=True, default=False, verbose_name="Is featured"),
),
(
"title",
@@ -135,9 +117,7 @@ class Migration(migrations.Migration):
),
(
"advancedUser",
models.BooleanField(
db_index=True, default=False, verbose_name="advanced user"
),
models.BooleanField(db_index=True, default=False, verbose_name="advanced user"),
),
("media_count", models.IntegerField(default=0)),
(
@@ -156,21 +136,15 @@ class Migration(migrations.Migration):
),
(
"location",
models.CharField(
blank=True, max_length=250, verbose_name="Location"
),
models.CharField(blank=True, max_length=250, verbose_name="Location"),
),
(
"is_editor",
models.BooleanField(
db_index=True, default=False, verbose_name="MediaCMS Editor"
),
models.BooleanField(db_index=True, default=False, verbose_name="MediaCMS Editor"),
),
(
"is_manager",
models.BooleanField(
db_index=True, default=False, verbose_name="MediaCMS Manager"
),
models.BooleanField(db_index=True, default=False, verbose_name="MediaCMS Manager"),
),
(
"groups",
@@ -218,9 +192,7 @@ class Migration(migrations.Migration):
("notify", models.BooleanField(default=False)),
(
"method",
models.CharField(
choices=[("email", "Email")], default="email", max_length=20
),
models.CharField(choices=[("email", "Email")], default="email", max_length=20),
),
(
"user",
@@ -276,8 +248,6 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["-date_added", "name"], name="users_user_date_ad_4eb0b8_idx"
),
index=models.Index(fields=["-date_added", "name"], name="users_user_date_ad_4eb0b8_idx"),
),
]

View File

@@ -1,18 +1,17 @@
from django.db import models
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.utils import timezone
from django.urls import reverse
from django.dispatch import receiver
from django.db.models.signals import post_save, post_delete
from django.utils.html import strip_tags
from django.core.mail import EmailMessage
from imagekit.processors import ResizeToFill
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 django.utils import timezone
from django.utils.html import strip_tags
from imagekit.models import ProcessedImageField
from imagekit.processors import ResizeToFill
import files.helpers as helpers
from files.models import Media, Tag, Category
from files.models import Category, Media, Tag
class User(AbstractUser):
@@ -40,9 +39,7 @@ class User(AbstractUser):
location = models.CharField("Location", max_length=250, blank=True)
is_editor = models.BooleanField("MediaCMS Editor", default=False, db_index=True)
is_manager = models.BooleanField("MediaCMS Manager", default=False, db_index=True)
allow_contact = models.BooleanField(
"Whether allow contact will be shown on profile page", default=False
)
allow_contact = models.BooleanField("Whether allow contact will be shown on profile page", default=False)
class Meta:
ordering = ["-date_added", "name"]
@@ -117,9 +114,7 @@ class User(AbstractUser):
class Channel(models.Model):
title = models.CharField(max_length=90, db_index=True)
description = models.TextField(blank=True, help_text="description")
user = models.ForeignKey(
User, on_delete=models.CASCADE, db_index=True, related_name="channels"
)
user = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True, related_name="channels")
add_date = models.DateTimeField(auto_now_add=True, db_index=True)
subscribers = models.ManyToManyField(User, related_name="subscriptions", blank=True)
friendly_token = models.CharField(blank=True, max_length=12)
@@ -150,13 +145,9 @@ class Channel(models.Model):
def get_absolute_url(self, edit=False):
if edit:
return reverse(
"edit_channel", kwargs={"friendly_token": self.friendly_token}
)
return reverse("edit_channel", kwargs={"friendly_token": self.friendly_token})
else:
return reverse(
"view_channel", kwargs={"friendly_token": self.friendly_token}
)
return reverse("view_channel", kwargs={"friendly_token": self.friendly_token})
@property
def edit_url(self):
@@ -178,9 +169,7 @@ Visit user profile page at %s
instance.email,
settings.SSL_FRONTEND_HOST + instance.get_absolute_url(),
)
email = EmailMessage(
title, msg, settings.DEFAULT_FROM_EMAIL, settings.ADMIN_EMAIL_LIST
)
email = EmailMessage(title, msg, settings.DEFAULT_FROM_EMAIL, settings.ADMIN_EMAIL_LIST)
email.send(fail_silently=True)
@@ -193,14 +182,10 @@ class Notification(models.Model):
Needs work
"""
user = models.ForeignKey(
User, on_delete=models.CASCADE, db_index=True, related_name="notifications"
)
user = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True, related_name="notifications")
action = models.CharField(max_length=30, blank=True)
notify = models.BooleanField(default=False)
method = models.CharField(
max_length=20, choices=NOTIFICATION_METHODS, default="email"
)
method = models.CharField(max_length=20, choices=NOTIFICATION_METHODS, default="email")
def save(self, *args, **kwargs):
super(Notification, self).save(*args, **kwargs)

View File

@@ -1,4 +1,5 @@
from rest_framework import serializers
from .models import User
@@ -11,9 +12,7 @@ class UserSerializer(serializers.ModelSerializer):
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
def get_api_url(self, obj):
return self.context["request"].build_absolute_uri(
obj.get_absolute_url(api=True)
)
return self.context["request"].build_absolute_uri(obj.get_absolute_url(api=True))
def get_thumbnail_url(self, obj):
return self.context["request"].build_absolute_uri(obj.thumbnail_url())
@@ -55,9 +54,7 @@ class UserDetailSerializer(serializers.ModelSerializer):
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
def get_api_url(self, obj):
return self.context["request"].build_absolute_uri(
obj.get_absolute_url(api=True)
)
return self.context["request"].build_absolute_uri(obj.get_absolute_url(api=True))
def get_thumbnail_url(self, obj):
return self.context["request"].build_absolute_uri(obj.thumbnail_url())

View File

@@ -1,8 +1,10 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(r"^user/(?P<username>[\w@._-]*)$", views.view_user, name="get_user"),
url(r"^user/(?P<username>[\w@._-]*)/$", views.view_user, name="get_user"),
url(
r"^user/(?P<username>[\w@.]*)/media$",
views.view_user_media,
@@ -19,9 +21,7 @@ urlpatterns = [
name="get_user_about",
),
url(r"^user/(?P<username>[\w@.]*)/edit$", views.edit_user, name="edit_user"),
url(
r"^channel/(?P<friendly_token>[\w]*)$", views.view_channel, name="view_channel"
),
url(r"^channel/(?P<friendly_token>[\w]*)$", views.view_channel, name="view_channel"),
url(
r"^channel/(?P<friendly_token>[\w]*)/edit$",
views.edit_channel,

View File

@@ -8,10 +8,7 @@ from django.utils.translation import gettext_lazy as _
@deconstructible
class ASCIIUsernameValidator(validators.RegexValidator):
regex = r"^[\w]+$"
message = _(
"Enter a valid username. This value may contain only "
"English letters and numbers"
)
message = _("Enter a valid username. This value may contain only " "English letters and numbers")
flags = re.ASCII

View File

@@ -1,27 +1,29 @@
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.mail import EmailMessage
from django.conf import settings
from rest_framework import permissions
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.settings import api_settings
from django.http import HttpResponseRedirect
from django.shortcuts import render
from drf_yasg import openapi as openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import permissions, status
from rest_framework.decorators import api_view
from rest_framework.exceptions import PermissionDenied
from rest_framework import status
from rest_framework.parsers import (
JSONParser,
MultiPartParser,
FileUploadParser,
FormParser,
JSONParser,
MultiPartParser,
)
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.views import APIView
from cms.permissions import IsUserOrManager
from files.methods import is_mediacms_manager, is_mediacms_editor
from .models import User, Channel
from .forms import UserForm, ChannelForm
from .serializers import UserSerializer, UserDetailSerializer
from files.methods import is_mediacms_editor, is_mediacms_manager
from .forms import ChannelForm, UserForm
from .models import Channel, User
from .serializers import UserDetailSerializer, UserSerializer
def get_user(username):
@@ -38,15 +40,9 @@ def view_user(request, username):
if not user:
return HttpResponseRedirect("/members")
context["user"] = user
context["CAN_EDIT"] = (
True
if ((user and user == request.user) or is_mediacms_manager(request.user))
else False
)
context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
context["SHOW_CONTACT_FORM"] = (
True if (user.allow_contact or is_mediacms_editor(request.user)) else False
)
context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
return render(request, "cms/user.html", context)
@@ -57,15 +53,9 @@ def view_user_media(request, username):
return HttpResponseRedirect("/members")
context["user"] = user
context["CAN_EDIT"] = (
True
if ((user and user == request.user) or request.user.is_superuser)
else False
)
context["CAN_DELETE"] = True if request.user.is_superuser else False
context["SHOW_CONTACT_FORM"] = (
True if (user.allow_contact or is_mediacms_editor(request.user)) else False
)
context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
return render(request, "cms/user_media.html", context)
@@ -76,15 +66,9 @@ def view_user_playlists(request, username):
return HttpResponseRedirect("/members")
context["user"] = user
context["CAN_EDIT"] = (
True
if ((user and user == request.user) or request.user.is_superuser)
else False
)
context["CAN_DELETE"] = True if request.user.is_superuser else False
context["SHOW_CONTACT_FORM"] = (
True if (user.allow_contact or is_mediacms_editor(request.user)) else False
)
context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
return render(request, "cms/user_playlists.html", context)
@@ -96,15 +80,9 @@ def view_user_about(request, username):
return HttpResponseRedirect("/members")
context["user"] = user
context["CAN_EDIT"] = (
True
if ((user and user == request.user) or request.user.is_superuser)
else False
)
context["CAN_DELETE"] = True if request.user.is_superuser else False
context["SHOW_CONTACT_FORM"] = (
True if (user.allow_contact or is_mediacms_editor(request.user)) else False
)
context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
return render(request, "cms/user_about.html", context)
@@ -134,20 +112,14 @@ def view_channel(request, friendly_token):
else:
user = channel.user
context["user"] = user
context["CAN_EDIT"] = (
True
if ((user and user == request.user) or request.user.is_superuser)
else False
)
context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
return render(request, "cms/channel.html", context)
@login_required
def edit_channel(request, friendly_token):
channel = Channel.objects.filter(friendly_token=friendly_token).first()
if not (
channel and request.user.is_authenticated and (request.user == channel.user)
):
if not (channel and request.user.is_authenticated and (request.user == channel.user)):
return HttpResponseRedirect("/")
if request.method == "POST":
@@ -161,6 +133,13 @@ def edit_channel(request, friendly_token):
return render(request, "cms/channel_edit.html", {"form": form})
@swagger_auto_schema(
methods=['post'],
manual_parameters=[],
tags=['Users'],
operation_summary='Contact user',
operation_description='Contact user through email, if user has set this option',
)
@api_view(["POST"])
def contact_user(request, username):
if not request.user.is_authenticated:
@@ -197,9 +176,18 @@ Sender email: %s\n
class UserList(APIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
],
tags=['Users'],
operation_summary='List users',
operation_description='Paginated listing of users',
)
def get(self, request, format=None):
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
paginator = pagination_class()
@@ -228,14 +216,18 @@ class UserDetail(APIView):
self.check_object_permissions(self.request, user)
return user
except PermissionDenied:
return Response(
{"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST)
except User.DoesNotExist:
return Response(
{"detail": "user does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "user does not exist"}, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(name='username', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='username', required=True),
],
tags=['Users'],
operation_summary='List user details',
operation_description='Get user details',
)
def get(self, request, username, format=None):
# Get user details
user = self.get_user(username)
@@ -245,15 +237,28 @@ class UserDetail(APIView):
serializer = UserDetailSerializer(user, context={"request": request})
return Response(serializer.data)
def post(self, request, uid, format=None):
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(name='username', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='username', required=True),
],
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'description': openapi.Schema(type=openapi.TYPE_STRING, description='description'),
'name': openapi.Schema(type=openapi.TYPE_STRING, description='name'),
},
),
tags=['Users'],
operation_summary='Edit user details',
operation_description='Post user details - authenticated view',
)
def post(self, request, username, format=None):
# USER
user = self.get_user(uid)
user = self.get_user(username)
if isinstance(user, Response):
return user
serializer = UserDetailSerializer(
user, data=request.data, context={"request": request}
)
serializer = UserDetailSerializer(user, data=request.data, context={"request": request})
if serializer.is_valid():
logo = request.data.get("logo")
if logo:
@@ -264,6 +269,12 @@ class UserDetail(APIView):
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
manual_parameters=[],
tags=['Users'],
operation_summary='Xto_be_written',
operation_description='to_be_written',
)
def put(self, request, uid, format=None):
# ADMIN
user = self.get_user(uid)
@@ -271,9 +282,7 @@ class UserDetail(APIView):
return user
if not request.user.is_superuser:
return Response(
{"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
action = request.data.get("action")
if action == "feature":
@@ -286,6 +295,12 @@ class UserDetail(APIView):
serializer = UserDetailSerializer(user, context={"request": request})
return Response(serializer.data)
@swagger_auto_schema(
manual_parameters=[],
tags=['Users'],
operation_summary='to_be_written',
operation_description='to_be_written',
)
def delete(self, request, username, format=None):
# Delete a user
user = self.get_user(username)