Compare commits

..

26 Commits
v1.1.3 ... 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
64 changed files with 975 additions and 1116 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 }}

3
.gitignore vendored
View File

@@ -1,6 +1,8 @@
media_files/encoded/ media_files/encoded/
media_files/original/ media_files/original/
media_files/hls/ media_files/hls/
media_files/chunks/
media_files/uploads/
postgres_data/ postgres_data/
celerybeat-schedule celerybeat-schedule
logs/ logs/
@@ -11,3 +13,4 @@ static/debug_toolbar/
static/mptt/ static/mptt/
static/rest_framework/ static/rest_framework/
cms/local_settings.py 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,12 +16,12 @@ RUN pip install -r requirements.txt
COPY . /home/mediacms.io/mediacms COPY . /home/mediacms.io/mediacms
WORKDIR /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 && \ 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-632.x86_64-unknown-linux.zip -d ../bento4 && \ unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d ../bento4 && \
mv ../bento4/Bento4-SDK-1-6-0-632.x86_64-unknown-linux/* ../bento4/ && \ mv ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* ../bento4/ && \
rm -rf ../bento4/Bento4-SDK-1-6-0-632.x86_64-unknown-linux && \ rm -rf ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
rm -rf ../bento4/docs && \ rm -rf ../bento4/docs && \
rm Bento4-SDK-1-6-0-632.x86_64-unknown-linux.zip rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
############ RUNTIME IMAGE ############ ############ RUNTIME IMAGE ############
FROM python:3.8-slim-buster as runtime-image FROM python:3.8-slim-buster as runtime-image
@@ -47,7 +47,7 @@ ENV ENABLE_MIGRATIONS='yes'
ENV VIRTUAL_ENV=/home/mediacms.io ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH" 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 \ RUN apt-get update -y && apt-get -y upgrade && apt-get install --no-install-recommends \
supervisor nginx ffmpeg imagemagick procps -y && \ supervisor nginx ffmpeg imagemagick procps -y && \

View File

@@ -9,18 +9,11 @@ A demo is available at https://demo.mediacms.io
## Screenshots ## Screenshots
![MediaCMS](docs/images/index.jpg) <p align="center">
<img src="https://raw.githubusercontent.com/mediacms-io/mediacms/main/docs/images/index.jpg" width="340">
Vanilla MediaCMS index page <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">
![MediaCMS](docs/images/video.jpg) </p>
Video page with player different options
![MediaCMS](docs/images/embed.jpg)
Embed video page
## Features ## Features
- **Complete control over your data**: host it yourself! - **Complete control over your data**: host it yourself!
@@ -94,15 +87,15 @@ git clone https://github.com/mediacms-io/mediacms
cd mediacms cd mediacms
``` ```
The default option to serve MediaCMS is on http://localhost. If you want to set a url and have it served there, set the `FRONTEND_HOST` variable on file `deploy/docker/local_settings.py`. The default option is to serve MediaCMS on all ips available of the server (including localhost).
Now run Now run
```bash ```bash
docker-compose build && docker-compose up docker-compose up
``` ```
This will build an image, download and setup necessary Docker images and start all containers. Once it finishes, MediaCMS will be installed and available on http://localhost (or the url you've set as `FRONTEND_HOST` on file `deploy/docker/local_settings.py`) 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. For more instructions, checkout the docs on the [Docker deployment](docs/Docker_deployment.md) page. Docker Compose support has been contributed by @swiftugandan.
@@ -114,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. 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. 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 ```bash
mkdir /home/mediacms.io && cd /home/mediacms.io/ mkdir /home/mediacms.io && cd /home/mediacms.io/
@@ -165,28 +158,13 @@ This software uses the following list of awesome technologies:
## Who is using it ## 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 - **Critical Commons** public media archive and fair use advocacy network - https://criticalcommons.org
- **Heritales** International Heritage Film Festival - https://stage.heritales.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 ## How to contribute
If you like the project, here's a few things you can do If you like the project, here's a few things you can do
@@ -198,5 +176,12 @@ If you like the project, here's a few things you can do
- Star the project - Star the project
- Add functionality, work on a PR, fix an issue! - 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 ## Contact
info@mediacms.io info@mediacms.io

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
from __future__ import absolute_import from __future__ import absolute_import
import os import os
from celery import Celery from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings") 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 collections import OrderedDict # requires Python 2.7 or later
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.utils.functional import cached_property from django.utils.functional import cached_property
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
class FasterDjangoPaginator(Paginator): class FasterDjangoPaginator(Paginator):

View File

@@ -1,5 +1,6 @@
from django.conf import settings from django.conf import settings
from rest_framework import permissions from rest_framework import permissions
from files.methods import is_mediacms_editor, is_mediacms_manager from files.methods import is_mediacms_editor, is_mediacms_manager
@@ -24,7 +25,10 @@ class IsUserOrManager(permissions.BasePermission):
if is_mediacms_manager(request.user): if is_mediacms_manager(request.user):
return True 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): class IsUserOrEditor(permissions.BasePermission):

View File

@@ -1,4 +1,5 @@
import os import os
from celery.schedules import crontab from celery.schedules import crontab
DEBUG = False DEBUG = False
@@ -16,7 +17,8 @@ CAN_ADD_MEDIA = "all"
# valid choices here are 'public', 'private', 'unlisted # valid choices here are 'public', 'private', 'unlisted
PORTAL_WORKFLOW = "public" 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 # 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 # ip of the server should be part of this
ALLOWED_HOSTS = ["*", "mediacms.io", "127.0.0.1", "localhost"] ALLOWED_HOSTS = ["*", "mediacms.io", "127.0.0.1", "localhost"]
FRONTEND_HOST = "http://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 # FRONTEND_HOST needs an http prefix - at the end of the file
# there's a conversion to https with the SSL_FRONTEND_HOST env # there's a conversion to https with the SSL_FRONTEND_HOST env
INTERNAL_IPS = "127.0.0.1" INTERNAL_IPS = "127.0.0.1"
@@ -208,9 +214,7 @@ POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY = ""
CANNOT_ADD_MEDIA_MESSAGE = "" CANNOT_ADD_MEDIA_MESSAGE = ""
# mp4hls command, part of Bendo4 # mp4hls command, part of Bendo4
MP4HLS_COMMAND = ( MP4HLS_COMMAND = "/home/mediacms.io/mediacms/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/bin/mp4hls"
"/home/mediacms.io/mediacms/Bento4-SDK-1-6-0-632.x86_64-unknown-linux/bin/mp4hls"
)
# highly experimental, related with remote workers # highly experimental, related with remote workers
ADMIN_TOKEN = "c2b8e1838b6128asd333ddc5e24" ADMIN_TOKEN = "c2b8e1838b6128asd333ddc5e24"
@@ -288,6 +292,7 @@ INSTALLED_APPS = [
"uploader.apps.UploaderConfig", "uploader.apps.UploaderConfig",
"djcelery_email", "djcelery_email",
"ckeditor", "ckeditor",
"drf_yasg",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -419,6 +424,7 @@ CELERY_BEAT_SCHEDULE = {
# TODO: beat, delete chunks from media root # TODO: beat, delete chunks from media root
# chunks_dir after xx days...(also uploads_dir) # chunks_dir after xx days...(also uploads_dir)
LOCAL_INSTALL = False LOCAL_INSTALL = False
try: 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 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 = [ urlpatterns = [
url(r"^__debug__/", include(debug_toolbar.urls)), url(r"^__debug__/", include(debug_toolbar.urls)),
@@ -10,4 +20,7 @@ urlpatterns = [
url(r"^accounts/", include("allauth.urls")), url(r"^accounts/", include("allauth.urls")),
url(r"^api-auth/", include("rest_framework.urls")), url(r"^api-auth/", include("rest_framework.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
re_path(r'^swagger/$', 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 # 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/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 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 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,media_files/hls} mkdir -p /home/mediacms.io/mediacms/{logs,pids,media_files/hls}
touch /home/mediacms.io/mediacms/logs/debug.log 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/) TARGET_GID=$(stat -c "%g" /home/mediacms.io/mediacms/)
@@ -25,6 +27,9 @@ else
usermod -a -G $GROUP www-data usermod -a -G $GROUP www-data
fi 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 chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh
exec "$@" exec "$@"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ version: "3"
services: services:
migrations: migrations:
image: mediacms:latest image: mediacms/mediacms:latest
volumes: volumes:
- ./:/home/mediacms.io/mediacms/ - ./:/home/mediacms.io/mediacms/
environment: environment:
@@ -17,10 +17,7 @@ services:
db: db:
condition: service_healthy condition: service_healthy
web: web:
build: image: mediacms/mediacms:latest
context: .
target: runtime-image
image: mediacms:latest
deploy: deploy:
replicas: 1 replicas: 1
ports: ports:
@@ -35,7 +32,7 @@ services:
depends_on: depends_on:
- migrations - migrations
celery_beat: celery_beat:
image: mediacms:latest image: mediacms/mediacms:latest
volumes: volumes:
- ./:/home/mediacms.io/mediacms/ - ./:/home/mediacms.io/mediacms/
environment: environment:
@@ -47,7 +44,7 @@ services:
depends_on: depends_on:
- redis - redis
celery_worker: celery_worker:
image: mediacms:latest image: mediacms/mediacms:latest
deploy: deploy:
replicas: 1 replicas: 1
volumes: volumes:

View File

@@ -2,7 +2,11 @@
A number of options are available on `cms/settings.py`. 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 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 - 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 ### automatically hide media upon being reported
set a low number for variable `REPORTED_TIMES_THRESHOLD` set a low number for variable `REPORTED_TIMES_THRESHOLD`

View File

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

View File

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

View File

@@ -1,16 +1,12 @@
from django.conf import settings from django.conf import settings
from .methods import is_mediacms_editor, is_mediacms_manager from .methods import is_mediacms_editor, is_mediacms_manager
def stuff(request): def stuff(request):
"""Pass settings to the frontend""" """Pass settings to the frontend"""
ret = {} ret = {}
if request.is_secure(): ret["FRONTEND_HOST"] = request.build_absolute_uri('/')
# 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["DEFAULT_THEME"] = settings.DEFAULT_THEME ret["DEFAULT_THEME"] = settings.DEFAULT_THEME
ret["PORTAL_NAME"] = settings.PORTAL_NAME ret["PORTAL_NAME"] = settings.PORTAL_NAME
ret["LOAD_FROM_CDN"] = settings.LOAD_FROM_CDN 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_SIZE"] = settings.UPLOAD_MAX_SIZE
ret["UPLOAD_MAX_FILES_NUMBER"] = settings.UPLOAD_MAX_FILES_NUMBER ret["UPLOAD_MAX_FILES_NUMBER"] = settings.UPLOAD_MAX_FILES_NUMBER
ret["PRE_UPLOAD_MEDIA_MESSAGE"] = settings.PRE_UPLOAD_MEDIA_MESSAGE ret["PRE_UPLOAD_MEDIA_MESSAGE"] = settings.PRE_UPLOAD_MEDIA_MESSAGE
ret[ ret["POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY"] = settings.POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY
"POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY"
] = settings.POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY
ret["IS_MEDIACMS_ADMIN"] = request.user.is_superuser ret["IS_MEDIACMS_ADMIN"] = request.user.is_superuser
ret["IS_MEDIACMS_EDITOR"] = is_mediacms_editor(request.user) ret["IS_MEDIACMS_EDITOR"] = is_mediacms_editor(request.user)
ret["IS_MEDIACMS_MANAGER"] = is_mediacms_manager(request.user) ret["IS_MEDIACMS_MANAGER"] = is_mediacms_manager(request.user)
ret["ALLOW_RATINGS"] = settings.ALLOW_RATINGS ret["ALLOW_RATINGS"] = settings.ALLOW_RATINGS
ret[ ret["ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY"] = settings.ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY
"ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY" ret["VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE"] = settings.VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE
] = settings.ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY
ret[
"VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE"
] = settings.VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE
ret["RSS_URL"] = "/rss" ret["RSS_URL"] = "/rss"
return ret 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.conf import settings
from django.contrib.postgres.search import SearchQuery 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 . import helpers
from .models import Category, Media
from .stop_words import STOP_WORDS from .stop_words import STOP_WORDS
@@ -119,11 +119,7 @@ class SearchRSSFeed(Feed):
elif query: elif query:
# same as on files.views.MediaSearch: move this processing to a prepare_query function # same as on files.views.MediaSearch: move this processing to a prepare_query function
query = helpers.clean_query(query) query = helpers.clean_query(query)
q_parts = [ q_parts = [q_part.rstrip("y") for q_part in query.split() if q_part not in STOP_WORDS]
q_part.rstrip("y")
for q_part in query.split()
if q_part not in STOP_WORDS
]
if q_parts: if q_parts:
query = SearchQuery(q_parts[0] + ":*", search_type="raw") query = SearchQuery(q_parts[0] + ":*", search_type="raw")
for part in q_parts[1:]: for part in q_parts[1:]:

View File

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

View File

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

View File

@@ -1,16 +1,18 @@
from rest_framework.views import APIView from drf_yasg import openapi as openapi
from rest_framework.parsers import JSONParser from drf_yasg.utils import swagger_auto_schema
from rest_framework.settings import api_settings
from rest_framework.response import Response
from rest_framework import status 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.models import User
from users.serializers import UserSerializer 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): class MediaList(APIView):
@@ -23,6 +25,17 @@ class MediaList(APIView):
permission_classes = (IsMediacmsEditor,) permission_classes = (IsMediacmsEditor,)
parser_classes = (JSONParser,) 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): def get(self, request, format=None):
params = self.request.query_params params = self.request.query_params
ordering = params.get("ordering", "").strip() ordering = params.get("ordering", "").strip()
@@ -94,6 +107,12 @@ class MediaList(APIView):
serializer = MediaSerializer(page, many=True, context={"request": request}) serializer = MediaSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data) 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): def delete(self, request, format=None):
tokens = request.GET.get("tokens") tokens = request.GET.get("tokens")
if tokens: if tokens:
@@ -112,6 +131,12 @@ class CommentList(APIView):
permission_classes = (IsMediacmsEditor,) permission_classes = (IsMediacmsEditor,)
parser_classes = (JSONParser,) 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): def get(self, request, format=None):
params = self.request.query_params params = self.request.query_params
ordering = params.get("ordering", "").strip() ordering = params.get("ordering", "").strip()
@@ -137,6 +162,12 @@ class CommentList(APIView):
serializer = CommentSerializer(page, many=True, context={"request": request}) serializer = CommentSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data) 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): def delete(self, request, format=None):
comment_ids = request.GET.get("comment_ids") comment_ids = request.GET.get("comment_ids")
if comment_ids: if comment_ids:
@@ -156,6 +187,12 @@ class UserList(APIView):
permission_classes = (IsMediacmsEditor,) permission_classes = (IsMediacmsEditor,)
parser_classes = (JSONParser,) 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): def get(self, request, format=None):
params = self.request.query_params params = self.request.query_params
ordering = params.get("ordering", "").strip() ordering = params.get("ordering", "").strip()
@@ -187,11 +224,15 @@ class UserList(APIView):
serializer = UserSerializer(page, many=True, context={"request": request}) serializer = UserSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data) 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): def delete(self, request, format=None):
if not is_mediacms_manager(request.user): if not is_mediacms_manager(request.user):
return Response( return Response({"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST)
{"detail": "bad permissions"}, status=status.HTTP_400_BAD_REQUEST
)
tokens = request.GET.get("tokens") tokens = request.GET.get("tokens")
if tokens: if tokens:

View File

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

View File

@@ -1,11 +1,13 @@
# Generated by Django 3.1.4 on 2020-12-01 07:12 # 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 uuid
import django.contrib.postgres.search
import imagekit.models.fields
from django.db import migrations, models
import files.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -32,9 +34,7 @@ class Migration(migrations.Migration):
("description", models.TextField(blank=True)), ("description", models.TextField(blank=True)),
( (
"is_global", "is_global",
models.BooleanField( models.BooleanField(default=False, help_text="global categories or user specific"),
default=False, help_text="global categories or user specific"
),
), ),
( (
"media_count", "media_count",
@@ -42,9 +42,7 @@ class Migration(migrations.Migration):
), ),
( (
"thumbnail", "thumbnail",
imagekit.models.fields.ProcessedImageField( imagekit.models.fields.ProcessedImageField(blank=True, upload_to=files.models.category_thumb_path),
blank=True, upload_to=files.models.category_thumb_path
),
), ),
( (
"listings_thumbnail", "listings_thumbnail",
@@ -153,9 +151,7 @@ class Migration(migrations.Migration):
("commands", models.TextField(blank=True, help_text="commands run")), ("commands", models.TextField(blank=True, help_text="commands run")),
( (
"chunk", "chunk",
models.BooleanField( models.BooleanField(db_index=True, default=False, help_text="is chunk?"),
db_index=True, default=False, help_text="is chunk?"
),
), ),
("chunk_file_path", models.CharField(blank=True, max_length=400)), ("chunk_file_path", models.CharField(blank=True, max_length=400)),
("chunks_info", models.TextField(blank=True)), ("chunks_info", models.TextField(blank=True)),
@@ -317,9 +313,7 @@ class Migration(migrations.Migration):
("likes", models.IntegerField(db_index=True, default=1)), ("likes", models.IntegerField(db_index=True, default=1)),
( (
"listable", "listable",
models.BooleanField( models.BooleanField(default=False, help_text="Whether it will appear on listings"),
default=False, help_text="Whether it will appear on listings"
),
), ),
( (
"md5sum", "md5sum",
@@ -341,9 +335,7 @@ class Migration(migrations.Migration):
), ),
( (
"media_info", "media_info",
models.TextField( models.TextField(blank=True, help_text="extracted media metadata info"),
blank=True, help_text="extracted media metadata info"
),
), ),
( (
"media_type", "media_type",
@@ -387,9 +379,7 @@ class Migration(migrations.Migration):
), ),
( (
"reported_times", "reported_times",
models.IntegerField( models.IntegerField(default=0, help_text="how many time a Medis is reported"),
default=0, help_text="how many time a Medis is reported"
),
), ),
( (
"search", "search",
@@ -485,9 +475,7 @@ class Migration(migrations.Migration):
), ),
( (
"user_featured", "user_featured",
models.BooleanField( models.BooleanField(default=False, help_text="Featured by the user"),
default=False, help_text="Featured by the user"
),
), ),
("video_height", models.IntegerField(default=1)), ("video_height", models.IntegerField(default=1)),
("views", models.IntegerField(db_index=True, 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 # Generated by Django 3.1.4 on 2020-12-01 07:12
from django.conf import settings
import django.contrib.postgres.indexes import django.contrib.postgres.indexes
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import mptt.fields import mptt.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -31,9 +31,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name="subtitle", model_name="subtitle",
name="language", name="language",
field=models.ForeignKey( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.language"),
on_delete=django.db.models.deletion.CASCADE, to="files.language"
),
), ),
migrations.AddField( migrations.AddField(
model_name="subtitle", model_name="subtitle",
@@ -47,9 +45,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name="subtitle", model_name="subtitle",
name="user", name="user",
field=models.ForeignKey( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
), ),
migrations.AddField( migrations.AddField(
model_name="rating", model_name="rating",
@@ -63,37 +59,27 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name="rating", model_name="rating",
name="rating_category", name="rating_category",
field=models.ForeignKey( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.ratingcategory"),
on_delete=django.db.models.deletion.CASCADE, to="files.ratingcategory"
),
), ),
migrations.AddField( migrations.AddField(
model_name="rating", model_name="rating",
name="user", name="user",
field=models.ForeignKey( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
), ),
migrations.AddField( migrations.AddField(
model_name="playlistmedia", model_name="playlistmedia",
name="media", name="media",
field=models.ForeignKey( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.media"),
on_delete=django.db.models.deletion.CASCADE, to="files.media"
),
), ),
migrations.AddField( migrations.AddField(
model_name="playlistmedia", model_name="playlistmedia",
name="playlist", name="playlist",
field=models.ForeignKey( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.playlist"),
on_delete=django.db.models.deletion.CASCADE, to="files.playlist"
),
), ),
migrations.AddField( migrations.AddField(
model_name="playlist", model_name="playlist",
name="media", name="media",
field=models.ManyToManyField( field=models.ManyToManyField(blank=True, through="files.PlaylistMedia", to="files.Media"),
blank=True, through="files.PlaylistMedia", to="files.Media"
),
), ),
migrations.AddField( migrations.AddField(
model_name="playlist", model_name="playlist",
@@ -173,9 +159,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name="encoding", model_name="encoding",
name="profile", name="profile",
field=models.ForeignKey( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="files.encodeprofile"),
on_delete=django.db.models.deletion.CASCADE, to="files.encodeprofile"
),
), ),
migrations.AddField( migrations.AddField(
model_name="comment", model_name="comment",
@@ -200,9 +184,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name="comment", model_name="comment",
name="user", name="user",
field=models.ForeignKey( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
), ),
migrations.AddField( migrations.AddField(
model_name="category", model_name="category",
@@ -216,9 +198,7 @@ class Migration(migrations.Migration):
), ),
migrations.AddIndex( migrations.AddIndex(
model_name="rating", model_name="rating",
index=models.Index( index=models.Index(fields=["user", "media"], name="files_ratin_user_id_72ca6a_idx"),
fields=["user", "media"], name="files_ratin_user_id_72ca6a_idx"
),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name="rating", name="rating",
@@ -226,8 +206,6 @@ class Migration(migrations.Migration):
), ),
migrations.AddIndex( migrations.AddIndex(
model_name="media", model_name="media",
index=django.contrib.postgres.indexes.GinIndex( index=django.contrib.postgres.indexes.GinIndex(fields=["search"], name="files_media_search_7194c6_gin"),
fields=["search"], name="files_media_search_7194c6_gin"
),
), ),
] ]

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
from rest_framework import serializers 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 # 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()) return self.context["request"].build_absolute_uri(obj.get_absolute_url())
def get_api_url(self, obj): def get_api_url(self, obj):
return self.context["request"].build_absolute_uri( return self.context["request"].build_absolute_uri(obj.get_absolute_url(api=True))
obj.get_absolute_url(api=True)
)
def get_thumbnail_url(self, obj): def get_thumbnail_url(self, obj):
if obj.thumbnail_url: if obj.thumbnail_url:
@@ -210,16 +208,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Playlist model = Playlist
read_only_fields = ("add_date", "user") read_only_fields = ("add_date", "user")
fields = ( fields = ("add_date", "title", "description", "user", "media_count", "url", "api_url", "thumbnail_url")
"add_date",
"title",
"description",
"user",
"media_count",
"url",
"api_url",
"thumbnail_url"
)
class PlaylistDetailSerializer(serializers.ModelSerializer): class PlaylistDetailSerializer(serializers.ModelSerializer):
@@ -228,16 +217,7 @@ class PlaylistDetailSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Playlist model = Playlist
read_only_fields = ("add_date", "user") read_only_fields = ("add_date", "user")
fields = ( fields = ("title", "add_date", "user_thumbnail_url", "description", "user", "media_count", "url", "thumbnail_url")
"title",
"add_date",
"user_thumbnail_url",
"description",
"user",
"media_count",
"url",
"thumbnail_url"
)
class CommentSerializer(serializers.ModelSerializer): class CommentSerializer(serializers.ModelSerializer):

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
#!/bin/bash #!/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!"; echo "Welcome to the MediacMS installation!";
if [ `id -u` -ne 0 ] if [ `id -u` -ne 0 ]
@@ -27,6 +27,10 @@ if [[ `lsb_release -d` == *"Ubuntu 20"* ]]; then
elif [[ `lsb_release -d` = *"Ubuntu 18"* ]]; then elif [[ `lsb_release -d` = *"Ubuntu 18"* ]]; then
echo 'Performing system update and dependency installation, this will take a few minutes' 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 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 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" 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 exit
@@ -115,8 +119,8 @@ fi
# Bento4 utility installation, for HLS # Bento4 utility installation, for HLS
cd /home/mediacms.io/mediacms cd /home/mediacms.io/mediacms
wget http://zebulon.bok.net/Bento4/binaries/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-632.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 mkdir /home/mediacms.io/mediacms/media_files/hls
# last, set default owner # last, set default owner

View File

@@ -7,9 +7,5 @@ if __name__ == "__main__":
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
raise ImportError( 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
"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) 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 djangorestframework==3.12.2
django-allauth==0.44.0 django-allauth==0.44.0
@@ -9,14 +9,21 @@ uwsgi==2.0.19.1
django-redis==4.12.1 django-redis==4.12.1
celery==4.4.7 celery==4.4.7
Pillow==8.0.1 drf-yasg==1.20.0
Pillow==8.1.1
django-imagekit django-imagekit
markdown markdown
django-filter django-filter
filetype filetype
django-mptt django-mptt
django-crispy-forms django-crispy-forms
requests==2.25.0 requests==2.25.0
django-celery-email django-celery-email
m3u8 m3u8
@@ -30,3 +37,4 @@ flake8
pep8 pep8
django-silk django-silk
django-debug-toolbar 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,37 +1,37 @@
MediaCMS.url = { MediaCMS.url = {
home: "{{FRONTEND_HOST}}", home: "/",
search: "{{FRONTEND_HOST}}/search", search: "/search",
latestMedia: "{{FRONTEND_HOST}}/latest", latestMedia: "/latest",
featuredMedia: "{{FRONTEND_HOST}}/featured", featuredMedia: "/featured",
recommendedMedia: "{{FRONTEND_HOST}}/recommended", recommendedMedia: "/recommended",
members: "{{FRONTEND_HOST}}/members", members: "/members",
/* Error pages */ /* Error pages */
error404: "{{FRONTEND_HOST}}/error", error404: "/error",
/* Taxonomies pages */ /* Taxonomies pages */
tags: "{{FRONTEND_HOST}}/tags", tags: "/tags",
categories: "{{FRONTEND_HOST}}/categories", categories: "/categories",
topics: "{{FRONTEND_HOST}}/topics", topics: "/topics",
languages: "{{FRONTEND_HOST}}/languages", languages: "/languages",
countries: "{{FRONTEND_HOST}}/countries", countries: "/countries",
/* User pages */ /* User pages */
likedMedia: "{{FRONTEND_HOST}}/liked", likedMedia: "/liked",
history: "{{FRONTEND_HOST}}/history", history: "/history",
/* Add/edit pages */ /* Add/edit pages */
addMedia: "{{FRONTEND_HOST}}/upload", addMedia: "/upload",
/* User account pages */ /* User account pages */
editProfile: "{{user.edit_url}}", editProfile: "{{user.edit_url}}",
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
signout: "{{FRONTEND_HOST}}/accounts/logout/", signout: "/accounts/logout/",
editChannel: "{{user.default_channel_edit_url}}", editChannel: "{{user.default_channel_edit_url}}",
changePassword: "{{FRONTEND_HOST}}/accounts/password/change/", changePassword: "/accounts/password/change/",
/* Administration pages */ /* Administration pages */
{% if IS_MEDIACMS_ADMIN %}admin: '/admin',{% endif %} {% if IS_MEDIACMS_ADMIN %}admin: '/admin',{% endif %}
/* Management pages */ /* Management pages */
{% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}manageMedia: "{{FRONTEND_HOST}}/manage/media",{% endif %} {% if IS_MEDIACMS_EDITOR %}manageMedia: "/manage/media",{% endif %}
{% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER %}manageUsers: "{{FRONTEND_HOST}}/manage/users",{% endif %} {% if IS_MEDIACMS_MANAGER %}manageUsers: "/manage/users",{% endif %}
{% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}manageComments: "{{FRONTEND_HOST}}/manage/comments",{% endif %} {% if IS_MEDIACMS_EDITOR %}manageComments: "/manage/comments",{% endif %}
{% else %} {% else %}
signin: "{{FRONTEND_HOST}}/accounts/login/", signin: "/accounts/login/",
register: "{{FRONTEND_HOST}}/accounts/signup/", register: "/accounts/signup/",
{% endif %} {% endif %}
}; };

View File

@@ -15,7 +15,7 @@ MediaCMS.user = {
addComment: true, addComment: true,
deleteComment: {% if CAN_DELETE_COMMENTS %}true{% else %}false{% endif %}, deleteComment: {% if CAN_DELETE_COMMENTS %}true{% else %}false{% endif %},
editProfile: {% if CAN_EDIT %}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 %}, 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 %}, 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 %}, 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 = { MediaCMS.site = {
id: 'mediacms', id: 'mediacms',
title: "{{PORTAL_NAME}}", title: "{{PORTAL_NAME}}",
url: '{{FRONTEND_HOST}}/', url: '{{FRONTEND_HOST}}',
api: '{{FRONTEND_HOST}}/api/v1', api: '{{FRONTEND_HOST}}api/v1',
theme: { theme: {
mode: 'light', // Valid values: 'light', 'dark'. mode: '{{DEFAULT_THEME}}',
switch: { switch: {
position: 'header', // Valid values: 'header', 'sidebar'. position: 'header', // Valid values: 'header', 'sidebar'.
}, },
}, },
logo:{ logo:{
lightMode:{ lightMode:{
img: "{{FRONTEND_HOST}}/static/images/logo_dark.png", img: "/static/images/logo_dark.png",
svg: "{{FRONTEND_HOST}}/static/images/logo_dark.svg", svg: "/static/images/logo_dark.svg",
}, },
darkMode:{ darkMode:{
img: "{{FRONTEND_HOST}}/static/images/logo_light.png", img: "/static/images/logo_light.png",
svg: "{{FRONTEND_HOST}}/static/images/logo_light.svg", svg: "/static/images/logo_light.svg",
}, },
}, },
pages: { pages: {
@@ -50,3 +50,4 @@ MediaCMS.site = {
}, },
}, },
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
# Generated by Django 3.1.4 on 2020-12-01 07: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.models
import django.contrib.auth.validators import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
import imagekit.models.fields import imagekit.models.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -33,9 +33,7 @@ class Migration(migrations.Migration):
("password", models.CharField(max_length=128, verbose_name="password")), ("password", models.CharField(max_length=128, verbose_name="password")),
( (
"last_login", "last_login",
models.DateTimeField( models.DateTimeField(blank=True, null=True, verbose_name="last login"),
blank=True, null=True, verbose_name="last login"
),
), ),
( (
"is_superuser", "is_superuser",
@@ -48,35 +46,25 @@ class Migration(migrations.Migration):
( (
"username", "username",
models.CharField( models.CharField(
error_messages={ error_messages={"unique": "A user with that username already exists."},
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150, max_length=150,
unique=True, unique=True,
validators=[ validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username", verbose_name="username",
), ),
), ),
( (
"first_name", "first_name",
models.CharField( models.CharField(blank=True, max_length=150, verbose_name="first name"),
blank=True, max_length=150, verbose_name="first name"
),
), ),
( (
"last_name", "last_name",
models.CharField( models.CharField(blank=True, max_length=150, verbose_name="last name"),
blank=True, max_length=150, verbose_name="last name"
),
), ),
( (
"email", "email",
models.EmailField( models.EmailField(blank=True, max_length=254, verbose_name="email address"),
blank=True, max_length=254, verbose_name="email address"
),
), ),
( (
"is_staff", "is_staff",
@@ -96,9 +84,7 @@ class Migration(migrations.Migration):
), ),
( (
"date_joined", "date_joined",
models.DateTimeField( models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined"),
default=django.utils.timezone.now, verbose_name="date joined"
),
), ),
( (
"logo", "logo",
@@ -111,9 +97,7 @@ class Migration(migrations.Migration):
("description", models.TextField(blank=True, verbose_name="About me")), ("description", models.TextField(blank=True, verbose_name="About me")),
( (
"name", "name",
models.CharField( models.CharField(db_index=True, max_length=250, verbose_name="full name"),
db_index=True, max_length=250, verbose_name="full name"
),
), ),
( (
"date_added", "date_added",
@@ -125,9 +109,7 @@ class Migration(migrations.Migration):
), ),
( (
"is_featured", "is_featured",
models.BooleanField( models.BooleanField(db_index=True, default=False, verbose_name="Is featured"),
db_index=True, default=False, verbose_name="Is featured"
),
), ),
( (
"title", "title",
@@ -135,9 +117,7 @@ class Migration(migrations.Migration):
), ),
( (
"advancedUser", "advancedUser",
models.BooleanField( models.BooleanField(db_index=True, default=False, verbose_name="advanced user"),
db_index=True, default=False, verbose_name="advanced user"
),
), ),
("media_count", models.IntegerField(default=0)), ("media_count", models.IntegerField(default=0)),
( (
@@ -156,21 +136,15 @@ class Migration(migrations.Migration):
), ),
( (
"location", "location",
models.CharField( models.CharField(blank=True, max_length=250, verbose_name="Location"),
blank=True, max_length=250, verbose_name="Location"
),
), ),
( (
"is_editor", "is_editor",
models.BooleanField( models.BooleanField(db_index=True, default=False, verbose_name="MediaCMS Editor"),
db_index=True, default=False, verbose_name="MediaCMS Editor"
),
), ),
( (
"is_manager", "is_manager",
models.BooleanField( models.BooleanField(db_index=True, default=False, verbose_name="MediaCMS Manager"),
db_index=True, default=False, verbose_name="MediaCMS Manager"
),
), ),
( (
"groups", "groups",
@@ -218,9 +192,7 @@ class Migration(migrations.Migration):
("notify", models.BooleanField(default=False)), ("notify", models.BooleanField(default=False)),
( (
"method", "method",
models.CharField( models.CharField(choices=[("email", "Email")], default="email", max_length=20),
choices=[("email", "Email")], default="email", max_length=20
),
), ),
( (
"user", "user",
@@ -276,8 +248,6 @@ class Migration(migrations.Migration):
), ),
migrations.AddIndex( migrations.AddIndex(
model_name="user", model_name="user",
index=models.Index( index=models.Index(fields=["-date_added", "name"], name="users_user_date_ad_4eb0b8_idx"),
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.conf import settings
from django.contrib.auth.models import AbstractUser 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 django.core.mail import EmailMessage
from django.db import models
from imagekit.processors import ResizeToFill 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.models import ProcessedImageField
from imagekit.processors import ResizeToFill
import files.helpers as helpers import files.helpers as helpers
from files.models import Media, Tag, Category from files.models import Category, Media, Tag
class User(AbstractUser): class User(AbstractUser):
@@ -40,9 +39,7 @@ class User(AbstractUser):
location = models.CharField("Location", max_length=250, blank=True) location = models.CharField("Location", max_length=250, blank=True)
is_editor = models.BooleanField("MediaCMS Editor", default=False, db_index=True) is_editor = models.BooleanField("MediaCMS Editor", default=False, db_index=True)
is_manager = models.BooleanField("MediaCMS Manager", default=False, db_index=True) is_manager = models.BooleanField("MediaCMS Manager", default=False, db_index=True)
allow_contact = models.BooleanField( allow_contact = models.BooleanField("Whether allow contact will be shown on profile page", default=False)
"Whether allow contact will be shown on profile page", default=False
)
class Meta: class Meta:
ordering = ["-date_added", "name"] ordering = ["-date_added", "name"]
@@ -117,9 +114,7 @@ class User(AbstractUser):
class Channel(models.Model): class Channel(models.Model):
title = models.CharField(max_length=90, db_index=True) title = models.CharField(max_length=90, db_index=True)
description = models.TextField(blank=True, help_text="description") description = models.TextField(blank=True, help_text="description")
user = models.ForeignKey( user = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True, related_name="channels")
User, on_delete=models.CASCADE, db_index=True, related_name="channels"
)
add_date = models.DateTimeField(auto_now_add=True, db_index=True) add_date = models.DateTimeField(auto_now_add=True, db_index=True)
subscribers = models.ManyToManyField(User, related_name="subscriptions", blank=True) subscribers = models.ManyToManyField(User, related_name="subscriptions", blank=True)
friendly_token = models.CharField(blank=True, max_length=12) friendly_token = models.CharField(blank=True, max_length=12)
@@ -150,13 +145,9 @@ class Channel(models.Model):
def get_absolute_url(self, edit=False): def get_absolute_url(self, edit=False):
if edit: if edit:
return reverse( return reverse("edit_channel", kwargs={"friendly_token": self.friendly_token})
"edit_channel", kwargs={"friendly_token": self.friendly_token}
)
else: else:
return reverse( return reverse("view_channel", kwargs={"friendly_token": self.friendly_token})
"view_channel", kwargs={"friendly_token": self.friendly_token}
)
@property @property
def edit_url(self): def edit_url(self):
@@ -178,9 +169,7 @@ Visit user profile page at %s
instance.email, instance.email,
settings.SSL_FRONTEND_HOST + instance.get_absolute_url(), settings.SSL_FRONTEND_HOST + instance.get_absolute_url(),
) )
email = EmailMessage( email = EmailMessage(title, msg, settings.DEFAULT_FROM_EMAIL, settings.ADMIN_EMAIL_LIST)
title, msg, settings.DEFAULT_FROM_EMAIL, settings.ADMIN_EMAIL_LIST
)
email.send(fail_silently=True) email.send(fail_silently=True)
@@ -193,14 +182,10 @@ class Notification(models.Model):
Needs work Needs work
""" """
user = models.ForeignKey( user = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True, related_name="notifications")
User, on_delete=models.CASCADE, db_index=True, related_name="notifications"
)
action = models.CharField(max_length=30, blank=True) action = models.CharField(max_length=30, blank=True)
notify = models.BooleanField(default=False) notify = models.BooleanField(default=False)
method = models.CharField( method = models.CharField(max_length=20, choices=NOTIFICATION_METHODS, default="email")
max_length=20, choices=NOTIFICATION_METHODS, default="email"
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super(Notification, self).save(*args, **kwargs) super(Notification, self).save(*args, **kwargs)

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,29 @@
from django.shortcuts import render from django.conf import settings
from django.http import HttpResponseRedirect
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.conf import settings from django.http import HttpResponseRedirect
from django.shortcuts import render
from rest_framework import permissions from drf_yasg import openapi as openapi
from rest_framework.views import APIView from drf_yasg.utils import swagger_auto_schema
from rest_framework.response import Response from rest_framework import permissions, status
from rest_framework.settings import api_settings from rest_framework.decorators import api_view
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework import status
from rest_framework.parsers import ( from rest_framework.parsers import (
JSONParser,
MultiPartParser,
FileUploadParser, FileUploadParser,
FormParser, 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 cms.permissions import IsUserOrManager
from files.methods import is_mediacms_manager, is_mediacms_editor from files.methods import is_mediacms_editor, is_mediacms_manager
from .models import User, Channel
from .forms import UserForm, ChannelForm from .forms import ChannelForm, UserForm
from .serializers import UserSerializer, UserDetailSerializer from .models import Channel, User
from .serializers import UserDetailSerializer, UserSerializer
def get_user(username): def get_user(username):
@@ -38,15 +40,9 @@ def view_user(request, username):
if not user: if not user:
return HttpResponseRedirect("/members") return HttpResponseRedirect("/members")
context["user"] = user context["user"] = user
context["CAN_EDIT"] = ( context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
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["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
context["SHOW_CONTACT_FORM"] = ( context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
True if (user.allow_contact or is_mediacms_editor(request.user)) else False
)
return render(request, "cms/user.html", context) return render(request, "cms/user.html", context)
@@ -57,15 +53,9 @@ def view_user_media(request, username):
return HttpResponseRedirect("/members") return HttpResponseRedirect("/members")
context["user"] = user context["user"] = user
context["CAN_EDIT"] = ( context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
True context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
if ((user and user == request.user) or request.user.is_superuser) context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
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
)
return render(request, "cms/user_media.html", context) return render(request, "cms/user_media.html", context)
@@ -76,15 +66,9 @@ def view_user_playlists(request, username):
return HttpResponseRedirect("/members") return HttpResponseRedirect("/members")
context["user"] = user context["user"] = user
context["CAN_EDIT"] = ( context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
True context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
if ((user and user == request.user) or request.user.is_superuser) context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
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
)
return render(request, "cms/user_playlists.html", context) return render(request, "cms/user_playlists.html", context)
@@ -96,15 +80,9 @@ def view_user_about(request, username):
return HttpResponseRedirect("/members") return HttpResponseRedirect("/members")
context["user"] = user context["user"] = user
context["CAN_EDIT"] = ( context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
True context["CAN_DELETE"] = True if is_mediacms_manager(request.user) else False
if ((user and user == request.user) or request.user.is_superuser) context["SHOW_CONTACT_FORM"] = True if (user.allow_contact or is_mediacms_editor(request.user)) else False
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
)
return render(request, "cms/user_about.html", context) return render(request, "cms/user_about.html", context)
@@ -134,20 +112,14 @@ def view_channel(request, friendly_token):
else: else:
user = channel.user user = channel.user
context["user"] = user context["user"] = user
context["CAN_EDIT"] = ( context["CAN_EDIT"] = True if ((user and user == request.user) or is_mediacms_manager(request.user)) else False
True
if ((user and user == request.user) or request.user.is_superuser)
else False
)
return render(request, "cms/channel.html", context) return render(request, "cms/channel.html", context)
@login_required @login_required
def edit_channel(request, friendly_token): def edit_channel(request, friendly_token):
channel = Channel.objects.filter(friendly_token=friendly_token).first() channel = Channel.objects.filter(friendly_token=friendly_token).first()
if not ( if not (channel and request.user.is_authenticated and (request.user == channel.user)):
channel and request.user.is_authenticated and (request.user == channel.user)
):
return HttpResponseRedirect("/") return HttpResponseRedirect("/")
if request.method == "POST": if request.method == "POST":
@@ -161,6 +133,13 @@ def edit_channel(request, friendly_token):
return render(request, "cms/channel_edit.html", {"form": form}) 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"]) @api_view(["POST"])
def contact_user(request, username): def contact_user(request, username):
if not request.user.is_authenticated: if not request.user.is_authenticated:
@@ -197,9 +176,18 @@ Sender email: %s\n
class UserList(APIView): class UserList(APIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser) 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): def get(self, request, format=None):
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
paginator = pagination_class() paginator = pagination_class()
@@ -228,14 +216,18 @@ class UserDetail(APIView):
self.check_object_permissions(self.request, user) self.check_object_permissions(self.request, user)
return user return user
except PermissionDenied: except PermissionDenied:
return Response( return Response({"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST)
{"detail": "not enough permissions"}, status=status.HTTP_400_BAD_REQUEST
)
except User.DoesNotExist: except User.DoesNotExist:
return Response( return Response({"detail": "user does not exist"}, status=status.HTTP_400_BAD_REQUEST)
{"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): def get(self, request, username, format=None):
# Get user details # Get user details
user = self.get_user(username) user = self.get_user(username)
@@ -245,15 +237,28 @@ class UserDetail(APIView):
serializer = UserDetailSerializer(user, context={"request": request}) serializer = UserDetailSerializer(user, context={"request": request})
return Response(serializer.data) 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
user = self.get_user(uid) user = self.get_user(username)
if isinstance(user, Response): if isinstance(user, Response):
return user return user
serializer = UserDetailSerializer( serializer = UserDetailSerializer(user, data=request.data, context={"request": request})
user, data=request.data, context={"request": request}
)
if serializer.is_valid(): if serializer.is_valid():
logo = request.data.get("logo") logo = request.data.get("logo")
if logo: if logo:
@@ -264,6 +269,12 @@ class UserDetail(APIView):
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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): def put(self, request, uid, format=None):
# ADMIN # ADMIN
user = self.get_user(uid) user = self.get_user(uid)
@@ -271,9 +282,7 @@ class UserDetail(APIView):
return user return user
if not request.user.is_superuser: if not request.user.is_superuser:
return Response( return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
{"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST
)
action = request.data.get("action") action = request.data.get("action")
if action == "feature": if action == "feature":
@@ -286,6 +295,12 @@ class UserDetail(APIView):
serializer = UserDetailSerializer(user, context={"request": request}) serializer = UserDetailSerializer(user, context={"request": request})
return Response(serializer.data) 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): def delete(self, request, username, format=None):
# Delete a user # Delete a user
user = self.get_user(username) user = self.get_user(username)