Compare commits

..

41 Commits
v1.0 ... v1.3

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

* swagger url

* swagger docs

* adds swagger url on Readme

* swagger API

* Code of Conduct file

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

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

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

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

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

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

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

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

* unset variabla

* documentation
2021-02-17 22:32:06 +02:00
Markos Gogoulos
41c66469e2 Updated material icons version (#54)
Co-authored-by: root <1515939+styiannis@users.noreply.github.com>
2021-02-17 22:30:34 +02:00
Markos Gogoulos
9d1a22e4a9 reset variable 2021-02-11 17:38:09 +02:00
Markos Gogoulos
17eae8eefb add demo link, set False to variable (#52) 2021-02-10 18:38:40 +02:00
Markos Gogoulos
86d819d1f6 Feat docker compose readme (#47)
* add Docker Compose on README

* add contributors section

* installation section on README

* Update README.md
2021-02-02 22:59:42 +02:00
Markos Gogoulos
58d703c3eb Merge pull request #43 from mediacms-io/fix-less-than-1-sec-video
allow encoding for small videos
2021-02-02 22:15:08 +02:00
Markos Gogoulos
8473b505c3 Merge pull request #30 from swiftugandan/deploy-using-docker-compose
Deploy using docker compose
2021-02-02 22:14:43 +02:00
Swift Ugandan
952665beab fix local_install 2021-01-29 18:40:00 +00:00
Swift Ugandan
ac3ce569a8 mount src folder, fix permissions, store db and media on host directories 2021-01-28 16:42:44 +00:00
Markos Gogoulos
fec21c2f40 allow encoding for small videos 2021-01-26 22:03:06 +02:00
Munaawa Philip
a15ed70d44 initial docker deployment implementation 2021-01-23 10:49:33 +00:00
Markos Gogoulos
087206346a Merge pull request #31 from mediacms-io/feat-doc
add playlist on Readme
2021-01-21 16:40:49 +02:00
Markos Gogoulos
b2fcecf5ab add playlist on Readme 2021-01-21 16:40:06 +02:00
97 changed files with 1842 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 }}

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
media_files/encoded/
media_files/original/
media_files/hls/
media_files/chunks/
media_files/uploads/
postgres_data/
celerybeat-schedule
logs/
pids/
static/admin/
static/ckeditor/
static/debug_toolbar/
static/mptt/
static/rest_framework/
cms/local_settings.py
deploy/docker/local_settings.py

1
.mailmap Normal file
View File

@@ -0,0 +1 @@
Swift Ugandan <swiftugandan@gmail.com> <swiftugandan@gmail.com>

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

View File

@@ -2,3 +2,7 @@ Wordgames.gr - https://www.wordgames.gr
Yiannis Stergiou - ys.stergiou@gmail.com Yiannis Stergiou - ys.stergiou@gmail.com
Markos Gogoulos - mgogoulos@gmail.com Markos Gogoulos - mgogoulos@gmail.com
Contributors
Swift Ugandan - swiftugandan@gmail.com

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

66
Dockerfile Normal file
View File

@@ -0,0 +1,66 @@
FROM python:3.8-buster AS compile-image
SHELL ["/bin/bash", "-c"]
# Set up virtualenv
ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ENV PIP_NO_CACHE_DIR=1
RUN mkdir -p /home/mediacms.io/mediacms/{logs,pids} && cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
# Install dependencies:
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . /home/mediacms.io/mediacms
WORKDIR /home/mediacms.io/mediacms
RUN wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d ../bento4 && \
mv ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* ../bento4/ && \
rm -rf ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
rm -rf ../bento4/docs && \
rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
############ RUNTIME IMAGE ############
FROM python:3.8-slim-buster as runtime-image
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV ADMIN_USER='admin'
ENV ADMIN_PASSWORD='mediacms'
ENV ADMIN_EMAIL='admin@localhost'
# See: https://github.com/celery/celery/issues/6285#issuecomment-715316219
ENV CELERY_APP='cms'
# Use these to toggle which processes supervisord should run
ENV ENABLE_UWSGI='yes'
ENV ENABLE_NGINX='yes'
ENV ENABLE_CELERY_BEAT='yes'
ENV ENABLE_CELERY_SHORT='yes'
ENV ENABLE_CELERY_LONG='yes'
ENV ENABLE_MIGRATIONS='yes'
# Set up virtualenv
ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
COPY --chown=www-data:www-data --from=compile-image /home/mediacms.io /home/mediacms.io
RUN apt-get update -y && apt-get -y upgrade && apt-get install --no-install-recommends \
supervisor nginx ffmpeg imagemagick procps -y && \
rm -rf /var/lib/apt/lists/* && \
apt-get purge --auto-remove && \
apt-get clean
WORKDIR /home/mediacms.io/mediacms
EXPOSE 9000 80
RUN chmod +x ./deploy/docker/entrypoint.sh
ENTRYPOINT ["./deploy/docker/entrypoint.sh"]
CMD ["./deploy/docker/start.sh"]

View File

@@ -4,20 +4,16 @@ MediaCMS is a modern, fully featured open source video and media CMS. It is deve
It is built mostly using the modern stack Django + React and includes a REST API. It is built mostly using the modern stack Django + React and includes a REST API.
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!
@@ -27,6 +23,7 @@ Embed video page
- **Multiple media classification options**: categories, tags and custom - **Multiple media classification options**: categories, tags and custom
- **Multiple media sharing options**: social media share, videos embed code generation - **Multiple media sharing options**: social media share, videos embed code generation
- **Easy media searching**: enriched with live search functionality - **Easy media searching**: enriched with live search functionality
- **Playlists for audio and video content**: create playlists, add and reorder content
- **Responsive design**: including light and dark themes - **Responsive design**: including light and dark themes
- **Advanced users management**: allow self registration, invite only, closed. - **Advanced users management**: allow self registration, invite only, closed.
- **Configurable actions**: allow download, add comments, add likes, dislikes, report media - **Configurable actions**: allow download, add comments, add likes, dislikes, report media
@@ -76,14 +73,41 @@ For a small to medium installation, with a few hours of video uploaded daily, an
In terms of disk space, think of what the needs will be. A general rule is to multiply by three the size of the expected uploaded videos (since the system keeps original versions, encoded versions plus HLS), so if you receive 1G of videos daily and maintain all of them, you should consider a 1T disk across a year (1G * 3 * 365). In terms of disk space, think of what the needs will be. A general rule is to multiply by three the size of the expected uploaded videos (since the system keeps original versions, encoded versions plus HLS), so if you receive 1G of videos daily and maintain all of them, you should consider a 1T disk across a year (1G * 3 * 365).
## Install ## Installation
There are two ways to run MediaCMS, through Docker Compose and through installing it on a server via an automation script that installs and configures all needed services.
### Docker Compose installation
Install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
Run as root
```bash
git clone https://github.com/mediacms-io/mediacms
cd mediacms
```
The default option is to serve MediaCMS on all ips available of the server (including localhost).
Now run
```bash
docker-compose up
```
This will download all MediaCMS related Docker images and start all containers. Once it finishes, MediaCMS will be installed and available on http://localhost or http://ip
For more instructions, checkout the docs on the [Docker deployment](docs/Docker_deployment.md) page. Docker Compose support has been contributed by @swiftugandan.
### Single server installation
The core dependencies are Python3, Django3, Celery, PostgreSQL, Redis, ffmpeg. Any system that can have these dependencies installed, can run MediaCMS. But we strongly suggest installing on Linux Ubuntu 18 or 20 versions. The core dependencies are Python3, Django3, Celery, PostgreSQL, Redis, ffmpeg. Any system that can have these dependencies installed, can run MediaCMS. But we strongly suggest installing on Linux Ubuntu 18 or 20 versions.
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/
@@ -112,7 +136,7 @@ sudo systemctl restart mediacms celery_long celery_short # restart services
Several options are available on cms/settings.py, most of the things that are allowed or should be disallowed are described there. It is advisable to override any of them by adding it to cms/local_settings.py. All configuration options will be documented gradually on the [Configuration](docs/Configuration.md) page. Several options are available on cms/settings.py, most of the things that are allowed or should be disallowed are described there. It is advisable to override any of them by adding it to cms/local_settings.py. All configuration options will be documented gradually on the [Configuration](docs/Configuration.md) page.
## Authors ## Authors
MediaCMS is developed by Yiannis Stergiou and Markos Gogoulos. We are Wordgames - https://wordgames.gr MediaCMS is developed by Yiannis Stergiou and Markos Gogoulos. We are Wordgames - https://wordgames.gr.
## Technology ## Technology
@@ -134,27 +158,30 @@ 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 ## How to contribute
- **Anna Helme**, for such a great partnership all these years! If you like the project, here's a few things you can do
- Hire us, for custom installations, training, support, maintenance work
- Suggest us to others that are interested to hire us
- Write a blog post/article about MediaCMS
- Share on social media about the project
- Open issues, participate on discussions, report bugs, suggest ideas
- Star the project
- Add functionality, work on a PR, fix an issue!
- **Steve Anderson**, for trusting us and helping the Wordgames team make this real. ## Developers info
- **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.
- 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,14 +17,15 @@ 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
# if set to False will not fetch external content # if set to False will not fetch external content
# this is only for the static files, as fonts/css/js files loaded from CDNs # this is only for the static files, as fonts/css/js files loaded from CDNs
# not for user uploaded media! # not for user uploaded media!
LOAD_FROM_CDN = True LOAD_FROM_CDN = False
LOGIN_ALLOWED = True # whether the login button appears LOGIN_ALLOWED = True # whether the login button appears
REGISTER_ALLOWED = True # whether the register button appears REGISTER_ALLOWED = True # whether the register button appears
UPLOAD_MEDIA_ALLOWED = True # whether the upload media button appears UPLOAD_MEDIA_ALLOWED = True # whether the upload media button appears
@@ -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,9 @@ 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
try: try:
# keep a local_settings.py file for local overrides # keep a local_settings.py file for local overrides
from .local_settings import * from .local_settings import *
@@ -434,4 +442,7 @@ if "http" not in FRONTEND_HOST:
# FRONTEND_HOST needs a http:// preffix # FRONTEND_HOST needs a http:// preffix
FRONTEND_HOST = f"http://{FRONTEND_HOST}" FRONTEND_HOST = f"http://{FRONTEND_HOST}"
SSL_FRONTEND_HOST = FRONTEND_HOST.replace("http", "https") if LOCAL_INSTALL:
SSL_FRONTEND_HOST = FRONTEND_HOST.replace("http", "https")
else:
SSL_FRONTEND_HOST = FRONTEND_HOST

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'),
] ]

3
deploy/docker/README.md Normal file
View File

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

35
deploy/docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
set -e
# forward request and error logs to docker log collector
ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log && \
ln -sf /dev/stdout /var/log/nginx/mediacms.io.access.log && ln -sf /dev/stderr /var/log/nginx/mediacms.io.error.log
cp /home/mediacms.io/mediacms/deploy/docker/local_settings.py /home/mediacms.io/mediacms/cms/local_settings.py
mkdir -p /home/mediacms.io/mediacms/{logs,pids,media_files/hls}
touch /home/mediacms.io/mediacms/logs/debug.log
# Remove any dangling pids
rm -rf /home/mediacms.io/mediacms/pids/*
TARGET_GID=$(stat -c "%g" /home/mediacms.io/mediacms/)
EXISTS=$(cat /etc/group | grep $TARGET_GID | wc -l)
# Create new group using target GID and add www-data user
if [ $EXISTS == "0" ]; then
groupadd -g $TARGET_GID tempgroup
usermod -a -G tempgroup www-data
else
# GID exists, find group name and add
GROUP=$(getent group $TARGET_GID | cut -d: -f1)
usermod -a -G $GROUP www-data
fi
# We should do this only for folders that have a different owner, since it is an expensive operation
find /home/mediacms.io/ ! \( -user www-data -group $TARGET_GID \) -exec chown www-data:$TARGET_GID {} +
chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh
exec "$@"

View File

@@ -0,0 +1,34 @@
FRONTEND_HOST = 'http://localhost'
PORTAL_NAME = 'MediaCMS'
SECRET_KEY = 'ma!s3^b-cw!f#7s6s0m3*jx77a@riw(7701**(r=ww%w!2+yk2'
POSTGRES_HOST = 'db'
REDIS_LOCATION = "redis://redis:6379/1"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "mediacms",
"HOST": POSTGRES_HOST,
"PORT": "5432",
"USER": "mediacms",
"PASSWORD": "mediacms",
}
}
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_LOCATION,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
# CELERY STUFF
BROKER_URL = REDIS_LOCATION
CELERY_RESULT_BACKEND = BROKER_URL
MP4HLS_COMMAND = "/home/mediacms.io/bento4/bin/mp4hls"
DEBUG = False

View File

@@ -0,0 +1,30 @@
server {
listen 80 ;
gzip on;
access_log /var/log/nginx/mediacms.io.access.log;
error_log /var/log/nginx/mediacms.io.error.log warn;
location /static {
alias /home/mediacms.io/mediacms/static ;
}
location /media/original {
alias /home/mediacms.io/mediacms/media_files/original;
}
location /media {
alias /home/mediacms.io/mediacms/media_files ;
}
location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
include /etc/nginx/sites-enabled/uwsgi_params;
uwsgi_pass 127.0.0.1:9000;
}
}

61
deploy/docker/prestart.sh Executable file
View File

@@ -0,0 +1,61 @@
#!/bin/bash
RANDOM_ADMIN_PASS=`python -c "import secrets;chars = 'abcdefghijklmnopqrstuvwxyz0123456789';print(''.join(secrets.choice(chars) for i in range(10)))"`
ADMIN_PASSWORD=${ADMIN_PASSWORD:-$RANDOM_ADMIN_PASS}
if [ X"$ENABLE_MIGRATIONS" = X"yes" ]; then
python manage.py migrate
python manage.py loaddata fixtures/encoding_profiles.json
python manage.py loaddata fixtures/categories.json
python manage.py collectstatic --noinput
echo "Admin Password: $ADMIN_PASSWORD"
# post_save, needs redis to succeed (ie. migrate depends on redis)
DJANGO_SUPERUSER_PASSWORD=$ADMIN_PASSWORD python manage.py createsuperuser \
--no-input \
--username=$ADMIN_USER \
--email=$ADMIN_EMAIL \
--database=default || true
# echo "Updating hostname ..."
# TODO: Get the FRONTEND_HOST from cms/local_settings.py
# echo "from django.contrib.sites.models import Site; Site.objects.update(name='$FRONTEND_HOST', domain='$FRONTEND_HOST')" | python manage.py shell
fi
# Setting up internal nginx server
# HTTPS setup is delegated to a reverse proxy running infront of the application
cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-available/default
cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-enabled/default
cp deploy/docker/uwsgi_params /etc/nginx/sites-enabled/uwsgi_params
cp deploy/docker/nginx.conf /etc/nginx/
#### Supervisord Configurations #####
cp deploy/docker/supervisord/supervisord-debian.conf /etc/supervisor/conf.d/supervisord-debian.conf
if [ X"$ENABLE_UWSGI" = X"yes" ] ; then
echo "Enabling uwsgi app server"
cp deploy/docker/supervisord/supervisord-uwsgi.conf /etc/supervisor/conf.d/supervisord-uwsgi.conf
fi
if [ X"$ENABLE_NGINX" = X"yes" ] ; then
echo "Enabling nginx as uwsgi app proxy and media server"
cp deploy/docker/supervisord/supervisord-nginx.conf /etc/supervisor/conf.d/supervisord-nginx.conf
fi
if [ X"$ENABLE_CELERY_BEAT" = X"yes" ] ; then
echo "Enabling celery-beat scheduling server"
cp deploy/docker/supervisord/supervisord-celery_beat.conf /etc/supervisor/conf.d/supervisord-celery_beat.conf
fi
if [ X"$ENABLE_CELERY_SHORT" = X"yes" ] ; then
echo "Enabling celery-short task worker"
cp deploy/docker/supervisord/supervisord-celery_short.conf /etc/supervisor/conf.d/supervisord-celery_short.conf
fi
if [ X"$ENABLE_CELERY_LONG" = X"yes" ] ; then
echo "Enabling celery-long task worker"
cp deploy/docker/supervisord/supervisord-celery_long.conf /etc/supervisor/conf.d/supervisord-celery_long.conf
fi

View File

@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICwzCCAaugAwIBAgIJAOyvdwguJQd+MA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
BAMTCWxvY2FsaG9zdDAeFw0yMTAxMjQxMjUwMzFaFw0zMTAxMjIxMjUwMzFaMBQx
EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAONswEwBzkgoO+lkewiKUnwvYqC54qleCUg9hidqjoyzd5XWKh1mIF7aaSCG
rJGSxCce8CbqAqGkpvsgXzwwbY72l7FwmAXFHO5ObQfpmFhjt2fsKRM9MTCo/UyU
liuhgP+Q+BNzUontTUC40NVHs8R7IHG4z8unB7qB/7zGK2tfilLB8JDqPTkc22vN
C4P1YxiGyY5bm37wQrroC9zPJ8bqanrF9Y90QJHubibnPWqnZvK2HkDWjp5LYkn8
IuzBycs1cLd8eMjU9aT72kweykvnGDDc3YbXFzT2zBTGSFEBROsVdPrNF9PaeE3j
pu4UZ8Ge3Fp3VYd+04DnWtbQq0MCAwEAAaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxo
b3N0MA0GCSqGSIb3DQEBBQUAA4IBAQAdm2aGn4evosbdWgBHgzr6oYWBIiPpf1SA
GXizuf5OaMActFP0rZ0mogndLH5d51J2qqSfOtaWSA5qwlPvDSTn1nvJeHoVLfZf
kQHaB7/DaOPGsZCQBELPhYHwl7+Ej3HYE+siiaRfjC2NVgf8P/pAsTlKbe2e+34l
GwWSFol24w5xAmUezCF41JiZbqHoZhSh7s/PuJnK2RvhpjkrIot8GvxnbvOcKDIv
JzEKo3qPq8pc5RBkpP7Kp2+EgAYn1xAn0CekxZracW/MY+tg2mCeFucZW2V1iwVs
LpAw6GJnjYz5mbrQskPbrJ9t78JGUKQ0kL/VUTfryUHMHYCiJlvd
-----END CERTIFICATE-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA42zATAHOSCg76WR7CIpSfC9ioLniqV4JSD2GJ2qOjLN3ldYq
HWYgXtppIIaskZLEJx7wJuoCoaSm+yBfPDBtjvaXsXCYBcUc7k5tB+mYWGO3Z+wp
Ez0xMKj9TJSWK6GA/5D4E3NSie1NQLjQ1UezxHsgcbjPy6cHuoH/vMYra1+KUsHw
kOo9ORzba80Lg/VjGIbJjlubfvBCuugL3M8nxupqesX1j3RAke5uJuc9aqdm8rYe
QNaOnktiSfwi7MHJyzVwt3x4yNT1pPvaTB7KS+cYMNzdhtcXNPbMFMZIUQFE6xV0
+s0X09p4TeOm7hRnwZ7cWndVh37TgOda1tCrQwIDAQABAoIBAQCmKKyOW7tlCNBN
AzbI1JbTWKOMnoM2DxhlCV5cqgOgVPcIKEL428bGxniMZRjr+vkJRBddtxdZFj1R
uSMbjJ5fF1dZMtQ/UvaCPhZ283p1CdXUPbz863ZnAPCf5Oea1RK0piw5ucYSM6h/
owgg65Qx92uK6uYW+uAwqg440+ihNvnaZoVTx5CjZbL9KISkrlNJnuYiB5vzOD0i
UVklO5Qz8VCuOcOVGZCA2SxHm4HAbg/aiQnpaUa9de4TsZ4ygF66pZh77T0wNOos
sS1riKtHQpX+osJyoTI/rIKFAhycsZ+AA7Qpu6GW4xQlNS6K8vRiIbktwkC+IT0O
RSn8Dg7BAoGBAPe5R8SpgXx9jKdA1eFa/Vjx5bmB96r2MviIOIWF8rs2K33xe+rj
v+BZ2ZjdpVjcm2nRMf9r/eDq2ScNFWmKoZsUmdyT84Qq9yLcTSUdno+zCy+L0LNH
DqJq5jIxJaV7amHeR/w10BVuiDmzhSsTmhfnXTUGRO/h2PjRyC3yEYdxAoGBAOsF
2+gTsdOGlq6AVzW5MLZkreq8WCU2wWpZRiCPh6HJa8htuynYxO5AWUiNUbYKddj2
0za9DFiXgH+Oo8wrkTYLEdN0T5/o+ScL5t3VG3m9R6pnuudLC2vmGQP0hNuZUpnF
7FzdJ85h6taR2bM1zFzOfl81K0BhTHGxTU2r70vzAoGAVXuLJ3LyqtnMKn72DzDN
0d6PTkdqBoW0qwyerHy/eRjFQ02MXE7BDJMUwmphv1tJCefVX/WNAwsnahFavTPI
dnJSccpgMtB8vXvV5yPkbmPzTTHrD6JKi4Nl8hYBjqwa1rDUmFSdfHfK7FZlcqrt
9qexAzYpnbmKnLoPYMNyhxECgYEAm5OCUeuPoL2MS7GLiXWwyFx3QFczZlcLzBGS
uYUpvLBwF/qDlhz3p9uS/tMFzyK3hktF4Ate+9o2ZroOtd31PzgusbJh7zIylGVt
i1VB3eGtaiFGeUuVIPTthE++Dvw80KxTXdnMOvNYmHduDBLF2H2c6/tvSSvfhbdf
u9XgD38CgYAiLcVySxMKNpsXatuC31wjT+rnaH22SD/7pXe2q6MRW/s+bGOspu0v
NeJSLoM98v8F99q0W0lgqesYJVI20Frru0DfXIp60ryaDolzve3Iwk8SOJUlcnUG
cCtmPUkjyr18QAlrcCB4PozJGjpPWyabaY8gGwo8wAEpJWHrIJlHew==
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1 @@
client_max_body_size 1g;

17
deploy/docker/start.sh Executable file
View File

@@ -0,0 +1,17 @@
#! /usr/bin/env sh
set -e
# If there's a prestart.sh script in the /app directory, run it before starting
PRE_START_PATH=deploy/docker/prestart.sh
echo "Checking for script in $PRE_START_PATH"
if [ -f $PRE_START_PATH ] ; then
echo "Running script $PRE_START_PATH"
. $PRE_START_PATH
else
echo "There is no script $PRE_START_PATH"
fi
# Start Supervisor, with Nginx and uWSGI
echo "Starting server using supervisord..."
exec /usr/bin/supervisord

View File

@@ -0,0 +1,12 @@
[program:celery_beat]
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
startsecs=0
numprocs=1
user=www-data
directory=/home/mediacms.io/mediacms
priority=300
startinorder=true
command=/home/mediacms.io/bin/celery beat --pidfile=/home/mediacms.io/mediacms/pids/beat%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_beat.log

View File

@@ -0,0 +1,13 @@
[program:celery_long]
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
startsecs=10
numprocs=1
user=www-data
directory=/home/mediacms.io/mediacms
priority=500
startinorder=true
startsecs=0
command=/home/mediacms.io/bin/celery multi start long1 --pidfile=/home/mediacms.io/mediacms/pids/%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_long.log -Ofair --prefetch-multiplier=1 -Q long_tasks

View File

@@ -0,0 +1,12 @@
[program:celery_short]
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
startsecs=0
numprocs=1
user=www-data
directory=/home/mediacms.io/mediacms
priority=400
startinorder=true
command=/home/mediacms.io/bin/celery multi start short1 short2 --pidfile=/home/mediacms.io/mediacms/pids/%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_short.log --soft-time-limit=300 -c10 -Q short_tasks

View File

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

View File

@@ -0,0 +1,11 @@
[program:nginx]
command=/usr/sbin/nginx -g 'daemon off;'
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
priority=200
startinorder=true
startsecs=0
# Graceful stop, see http://nginx.org/en/docs/control.html
stopsignal=QUIT

View File

@@ -0,0 +1,9 @@
[program:uwsgi]
command=/home/mediacms.io/bin/uwsgi --ini /home/mediacms.io/mediacms/deploy/docker/uwsgi.ini
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
priority=100
startinorder=true
startsecs=0

23
deploy/docker/uwsgi.ini Normal file
View File

@@ -0,0 +1,23 @@
[uwsgi]
chdir = /home/mediacms.io/mediacms/
virtualenv = /home/mediacms.io
module = cms.wsgi
uid=www-data
gid=www-data
processes = 2
threads = 2
master = true
socket = 127.0.0.1:9000
workers = 2
vacuum = true
hook-master-start = unix_signal:15 gracefully_kill_them_all
need-app = true
die-on-term = true

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 ;
@@ -59,7 +59,7 @@ server {
location /media/original { location /media/original {
alias /home/mediacms.io/mediacms/media_files/original; alias /home/mediacms.io/mediacms/media_files/original;
#auth_basic "auth protected area"; #auth_basic "auth protected area";
#auth_basic_user_file /home/mediacms.io/mediacms/deploy/.htpasswd; #auth_basic_user_file /home/mediacms.io/mediacms/deploy/local_install/.htpasswd;
} }
location /media { location /media {

View File

@@ -2,7 +2,7 @@
Description=MediaCMS uwsgi Description=MediaCMS uwsgi
[Service] [Service]
ExecStart=/home/mediacms.io/bin/uwsgi --ini /home/mediacms.io/mediacms/uwsgi.ini ExecStart=/home/mediacms.io/bin/uwsgi --ini /home/mediacms.io/mediacms/deploy/local_install/uwsgi.ini
ExecStop=/usr/bin/killall -9 uwsgi ExecStop=/usr/bin/killall -9 uwsgi
RestartSec=3 RestartSec=3
#ExecRestart=killall -9 uwsgi; sleep 5; /home/sss/bin/uwsgi --ini /home/sss/wordgames/uwsgi.ini #ExecRestart=killall -9 uwsgi; sleep 5; /home/sss/bin/uwsgi --ini /home/sss/wordgames/uwsgi.ini

View File

@@ -0,0 +1,41 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 10240;
}
worker_rlimit_nofile 20000; #each connection needs a filehandle (or 2 if you are proxying)
http {
proxy_connect_timeout 75;
proxy_read_timeout 12000;
client_max_body_size 5800M;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 10;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
log_format compression '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" "$gzip_ratio"';
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

View File

@@ -1,8 +1,8 @@
[uwsgi] [uwsgi]
chdir = /home/mediacms.io/mediacms/ chdir = /home/mediacms.io/mediacms/
virtualenv = /home/mediacms.io virtualenv = /home/mediacms.io
module = cms.wsgi module = cms.wsgi
uid=www-data uid=www-data
gid=www-data gid=www-data
@@ -10,7 +10,7 @@ gid=www-data
processes = 2 processes = 2
threads = 2 threads = 2
master = true master = true
socket = 127.0.0.1:9000 socket = 127.0.0.1:9000
#socket = /home/mediacms.io/mediacms/deploy/uwsgi.sock #socket = /home/mediacms.io/mediacms/deploy/uwsgi.sock
@@ -19,7 +19,7 @@ socket = 127.0.0.1:9000
workers = 2 workers = 2
vacuum = true vacuum = true
logto = /home/mediacms.io/mediacms/logs/errorlog.txt logto = /home/mediacms.io/mediacms/logs/errorlog.txt

View File

@@ -0,0 +1,16 @@
uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REQUEST_SCHEME $scheme;
uwsgi_param HTTPS $https if_not_empty;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;

View File

@@ -1,19 +0,0 @@
[uwsgi]
chdir = /home/mediacms.io/mediacms/
virtualenv = /home/mediacms.io
module = cms.wsgi
uid = www-data
gid = www-data
processes = 10
threads = 10
master = true
workers = 8
vacuum = true
socket = 127.0.0.1:9000
logto = /home/mediacms.io/mediacms/logs/errorlog.txt

View File

@@ -0,0 +1,86 @@
version: "3"
services:
nginx-proxy:
image: jwilder/nginx-proxy
ports:
- "80:80"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./deploy/docker/reverse_proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
migrations:
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_CELERY_BEAT: 'no'
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
web:
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_CELERY_BEAT: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_MIGRATIONS: 'no'
VIRTUAL_HOST: localhost
depends_on:
- migrations
celery_beat:
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- redis
celery_worker:
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_BEAT: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- migrations
db:
image: postgres
volumes:
- ../postgres_data/:/var/lib/postgresql/data/
restart: always
environment:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: "redis:alpine"
restart: always
healthcheck:
test: ["CMD", "redis-cli","ping"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -0,0 +1,88 @@
version: "3"
services:
nginx-proxy:
image: jwilder/nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./deploy/docker/reverse_proxy/certs/:/etc/nginx/certs/
- ./deploy/docker/reverse_proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
migrations:
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_CELERY_BEAT: 'no'
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
web:
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_CELERY_BEAT: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_MIGRATIONS: 'no'
VIRTUAL_HOST: localhost
depends_on:
- migrations
celery_beat:
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- redis
celery_worker:
image: mediacms/mediacms:latest
deploy:
replicas: 2
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_BEAT: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- migrations
db:
image: postgres
volumes:
- ../postgres_data/:/var/lib/postgresql/data/
restart: always
environment:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: "redis:alpine"
restart: always
healthcheck:
test: ["CMD", "redis-cli","ping"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -0,0 +1,88 @@
version: "3"
services:
migrations:
image: mediacms/mediacms:latest
volumes:
- ./deploy/docker/local_settings.py:/home/mediacms.io/mediacms/deploy/docker/local_settings.py
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_CELERY_BEAT: 'no'
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
web:
image: mediacms/mediacms:latest
deploy:
replicas: 1
ports:
- "80:80"
volumes:
- media_store:/home/mediacms.io/mediacms/media_files/
- static_store:/home/mediacms.io/mediacms/static/
- ./deploy/docker/local_settings.py:/home/mediacms.io/mediacms/deploy/docker/local_settings.py
environment:
ENABLE_CELERY_BEAT: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- migrations
celery_beat:
image: mediacms/mediacms:latest
volumes:
- ./deploy/docker/local_settings.py:/home/mediacms.io/mediacms/deploy/docker/local_settings.py
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- redis
celery_worker:
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:
- media_store:/home/mediacms.io/mediacms/media_files/
- static_store:/home/mediacms.io/mediacms/static/
- ./deploy/docker/local_settings.py:/home/mediacms.io/mediacms/deploy/docker/local_settings.py
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_BEAT: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- migrations
db:
image: postgres
volumes:
- postgres_data:/var/lib/postgresql/data/
restart: always
environment:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
interval: 30s
timeout: 10s
retries: 5
redis:
image: "redis:alpine"
restart: always
healthcheck:
test: ["CMD", "redis-cli","ping"]
interval: 30s
timeout: 10s
retries: 3
volumes:
postgres_data:
media_store:
static_store:

80
docker-compose.yaml Normal file
View File

@@ -0,0 +1,80 @@
version: "3"
services:
migrations:
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_CELERY_BEAT: 'no'
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
web:
image: mediacms/mediacms:latest
deploy:
replicas: 1
ports:
- "80:80"
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_CELERY_BEAT: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- migrations
celery_beat:
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- redis
celery_worker:
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_BEAT: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- migrations
db:
image: postgres
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always
environment:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: "redis:alpine"
restart: always
healthcheck:
test: ["CMD", "redis-cli","ping"]
interval: 30s
timeout: 10s
retries: 3

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`

40
docs/Docker_deployment.md Normal file
View File

@@ -0,0 +1,40 @@
# MediaCMS on Docker
The mediacms image is built to use supervisord as the main process, which manages one or more services required to run mediacms. We can toggle which services are run in a given container by setting the environment variables below to `yes` or `no`:
* ENABLE_UWSGI
* ENABLE_NGINX
* ENABLE_CELERY_BEAT
* ENABLE_CELERY_SHORT
* ENABLE_CELERY_LONG
* ENABLE_MIGRATIONS
By default, all these services are enabled, but in order to create a scaleable deployment, some of them can be disabled, splitting the service up into smaller services.
Also see the `Dockerfile` for other environment variables which you may wish to override. Application settings, eg. `FRONTEND_HOST` can also be overridden by updating the `deploy/docker/local_settings.py` file.
See example deployments in the sections below. These example deployments have been tested on `docker-compose version 1.27.4` running on `Docker version 19.03.13`
To run, update the configs above if necessary, build the image by running `docker-compose build`, then run `docker-compose run`
## Simple Deployment, accessed as http://localhost
The main container runs migrations, mediacms_web, celery_beat, celery_workers (celery_short and celery_long services), exposed on port 80 supported by redis and postgres database. The FRONTEND_HOST in `deploy/docker/local_settings.py` is configured as http://localhost, on the docker host machine.
## Advanced Deployment, accessed as http://localhost:8000
Here we can run 1 mediacms_web instance, with the FRONTEND_HOST in `deploy/docker/local_settings.py` configured as http://localhost:8000. This is bootstrapped by a single migrations instance and supported by a single celery_beat instance and 1 or more celery_worker instances. Redis and postgres containers are also used for persistence. Clients can access the service on http://localhost:8000, on the docker host machine. This is similar to [this deployment](../docker-compose.yaml), with a `port` defined in FRONTEND_HOST.
## Advanced Deployment, with reverse proxy, accessed as http://mediacms.io
Here we can use `jwilder/nginx-proxy` to reverse proxy to 1 or more instances of mediacms_web supported by other services as mentioned in the previous deployment. The FRONTEND_HOST in `deploy/docker/local_settings.py` is configured as http://mediacms.io, nginx-proxy has port 80 exposed. Clients can access the service on http://mediacms.io (Assuming DNS or the hosts file is setup correctly to point to the IP of the nginx-proxy instance). This is similar to [this deployment](../docker-compose-http-proxy.yaml).
## Advanced Deployment, with reverse proxy, accessed as https://localhost
The reverse proxy (`jwilder/nginx-proxy`) can be configured to provide SSL termination using self-signed certificates, letsencrypt or CA signed certificates (see: https://hub.docker.com/r/jwilder/nginx-proxy or [LetsEncrypt Example](https://www.singularaspect.com/use-nginx-proxy-and-letsencrypt-companion-to-host-multiple-websites/) ). In this case the FRONTEND_HOST should be set to https://mediacms.io. This is similar to [this deployment](../docker-compose-http-proxy.yaml).
## A Scaleable Deployment Architecture (Docker, Swarm, Kubernetes)
The architecture below generalises all the deployment scenarios above, and provides a conceptual design for other deployments based on kubernetes and docker swarm. It allows for horizontal scaleability through the use of multiple mediacms_web instances and celery_workers. For large deployments, managed postgres, redis and storage may be adopted.
![MediaCMS](images/architecture.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

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,10 +262,10 @@ 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
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir: with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
@@ -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
@@ -58,14 +62,16 @@ SECRET_KEY=`python -c 'from django.core.management.utils import get_random_secre
FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/http:\/\///g'` FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/http:\/\///g'`
FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/https:\/\///g'` FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/https:\/\///g'`
sed -i s/localhost/$FRONTEND_HOST/g deploy/mediacms.io sed -i s/localhost/$FRONTEND_HOST/g deploy/local_install/mediacms.io
FRONTEND_HOST_HTTP_PREFIX='http://'$FRONTEND_HOST
echo 'FRONTEND_HOST='\'"$FRONTEND_HOST"\' >> cms/local_settings.py echo 'FRONTEND_HOST='\'"$FRONTEND_HOST_HTTP_PREFIX"\' >> cms/local_settings.py
echo 'PORTAL_NAME='\'"$PORTAL_NAME"\' >> cms/local_settings.py echo 'PORTAL_NAME='\'"$PORTAL_NAME"\' >> cms/local_settings.py
echo "SSL_FRONTEND_HOST = FRONTEND_HOST.replace('http', 'https')" >> cms/local_settings.py echo "SSL_FRONTEND_HOST = FRONTEND_HOST.replace('http', 'https')" >> cms/local_settings.py
echo 'SECRET_KEY='\'"$SECRET_KEY"\' >> cms/local_settings.py echo 'SECRET_KEY='\'"$SECRET_KEY"\' >> cms/local_settings.py
echo "LOCAL_INSTALL = True" >> cms/local_settings.py
mkdir logs mkdir logs
mkdir pids mkdir pids
@@ -80,19 +86,19 @@ echo "from users.models import User; User.objects.create_superuser('admin', 'adm
echo "from django.contrib.sites.models import Site; Site.objects.update(name='$FRONTEND_HOST', domain='$FRONTEND_HOST')" | python manage.py shell echo "from django.contrib.sites.models import Site; Site.objects.update(name='$FRONTEND_HOST', domain='$FRONTEND_HOST')" | python manage.py shell
chown -R www-data. /home/mediacms.io/ chown -R www-data. /home/mediacms.io/
cp deploy/celery_long.service /etc/systemd/system/celery_long.service && systemctl enable celery_long && systemctl start celery_long cp deploy/local_install/celery_long.service /etc/systemd/system/celery_long.service && systemctl enable celery_long && systemctl start celery_long
cp deploy/celery_short.service /etc/systemd/system/celery_short.service && systemctl enable celery_short && systemctl start celery_short cp deploy/local_install/celery_short.service /etc/systemd/system/celery_short.service && systemctl enable celery_short && systemctl start celery_short
cp deploy/celery_beat.service /etc/systemd/system/celery_beat.service && systemctl enable celery_beat &&systemctl start celery_beat cp deploy/local_install/celery_beat.service /etc/systemd/system/celery_beat.service && systemctl enable celery_beat &&systemctl start celery_beat
cp deploy/mediacms.service /etc/systemd/system/mediacms.service && systemctl enable mediacms.service && systemctl start mediacms.service cp deploy/local_install/mediacms.service /etc/systemd/system/mediacms.service && systemctl enable mediacms.service && systemctl start mediacms.service
mkdir -p /etc/letsencrypt/live/mediacms.io/ mkdir -p /etc/letsencrypt/live/mediacms.io/
mkdir -p /etc/letsencrypt/live/$FRONTEND_HOST mkdir -p /etc/letsencrypt/live/$FRONTEND_HOST
cp deploy/mediacms.io_fullchain.pem /etc/letsencrypt/live/$FRONTEND_HOST/fullchain.pem cp deploy/local_install/mediacms.io_fullchain.pem /etc/letsencrypt/live/$FRONTEND_HOST/fullchain.pem
cp deploy/mediacms.io_privkey.pem /etc/letsencrypt/live/$FRONTEND_HOST/privkey.pem cp deploy/local_install/mediacms.io_privkey.pem /etc/letsencrypt/live/$FRONTEND_HOST/privkey.pem
cp deploy/mediacms.io /etc/nginx/sites-available/default cp deploy/local_install/mediacms.io /etc/nginx/sites-available/default
cp deploy/mediacms.io /etc/nginx/sites-enabled/default cp deploy/local_install/mediacms.io /etc/nginx/sites-enabled/default
cp deploy/uwsgi_params /etc/nginx/sites-enabled/uwsgi_params cp deploy/local_install/uwsgi_params /etc/nginx/sites-enabled/uwsgi_params
cp deploy/nginx.conf /etc/nginx/ cp deploy/local_install/nginx.conf /etc/nginx/
systemctl stop nginx systemctl stop nginx
systemctl start nginx systemctl start nginx
@@ -113,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,10 +1,8 @@
/*@import url('https://fonts.googleapis.com/icon?family=Material+Icons');*/
@font-face { @font-face {
font-family: 'Material Icons'; font-family: 'Material Icons';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('../../lib/material-icons/v50/icons.woff2') format('woff2'); src: url('../../lib/material-icons/v77/icons.woff2') format('woff2');
font-display: swap; font-display: swap;
} }

Binary file not shown.

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)